diff --git a/Dockerfile b/Dockerfile index 7ea90e59..c2a74c37 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,24 @@ -FROM python:3.6.2 +FROM python:3.6.3 # -- Install Pipenv: -RUN pip install pipenv --upgrade +RUN set -ex && pip install pipenv --upgrade # -- Install Application into container: -RUN mkdir /app +RUN set -ex && mkdir /app + WORKDIR /app +# -- Adding Pipfiles +ONBUILD COPY Pipfile Pipfile +ONBUILD COPY Pipfile.lock Pipfile.lock + +# -- Install dependencies: +ONBUILD RUN set -ex && pipenv install --deploy --system + # -------------------- # - Using This File: - # -------------------- # FROM kennethreitz/pipenv -# COPY Pipfile Pipfile -# COPY Pipfile.lock Pipfile.lock # COPY . /app - -# -- Install dependencies: -# RUN pipenv install --deploy --system - -ENTRYPOINT [] -CMD [ "/bin/bash" ] \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index 2543d7c3..b4651aa7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include README.rst LICENSE NOTICES HISTORY.txt pipenv/patched/safety.zip +include pipenv/patched/pip/_vendor/requests/cacert.pem include pipenv/vendor/pipreqs/stdlib include pipenv/vendor/pipreqs/mapping include pipenv/pipenv.1 diff --git a/docs/install.rst b/docs/install.rst index 22fff395..4e82c1b6 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -220,6 +220,16 @@ To upgrade pipenv at any time:: This will install both ``pipenv`` and ``pew`` (one of our dependencies) in an isolated virtualenv, so it doesn't interfere with the rest of your Python installation! +.. _more_proper_installation: + +☤ Referentially Transparent Installation of Pipenv +================================================== + +Nix provides atomic upgrades and rollbacks, it's reliable and reproducible thanks to keeping all dependencies isolated all the way down to libc. + +`Once installed `_ simply run:: + + $ nix-env --install --attr pipenv .. _pragmatic_installation: diff --git a/pipenv/cli.py b/pipenv/cli.py index f916b725..5b9c55e1 100644 --- a/pipenv/cli.py +++ b/pipenv/cli.py @@ -25,19 +25,17 @@ import pipdeptree import requirements import semver import flake8.main.cli - from pipreqs import pipreqs from blindspin import spinner from urllib3.exceptions import InsecureRequestWarning from pip.req.req_file import parse_requirements from click_didyoumean import DYMCommandCollection - from .project import Project from .utils import ( convert_deps_from_pip, convert_deps_to_pip, is_required_version, proper_case, pep423_name, split_vcs, resolve_deps, shellquote, is_vcs, python_version, suggest_package, find_windows_executable, is_file, - prepare_pip_source_args, is_valid_url, download_file + prepare_pip_source_args, temp_environ, is_valid_url, download_file ) from .__version__ import __version__ from . import pep508checker, progress @@ -565,11 +563,15 @@ def ensure_virtualenv(three=None, python=None, site_packages=False): # If --three, --two, or --python were passed... elif (python) or (three is not None) or (site_packages is not False): - click.echo(crayons.red('Virtualenv already exists!'), err=True) - click.echo(crayons.normal(u'Removing existing virtualenv…', bold=True), err=True) USING_DEFAULT_PYTHON = False + # Ensure python is installed before deleting existing virtual env + ensure_python(three=three, python=python) + + click.echo(crayons.red('Virtualenv already exists!'), err=True) + click.echo(crayons.normal(u'Removing existing virtualenv…', bold=True), err=True) + # Remove the virtualenv. cleanup_virtualenv(bare=True) @@ -2030,29 +2032,43 @@ def do_shell(three=None, python=False, fancy=False, shell_args=None): # Standard (properly configured shell) mode: else: + if PIPENV_VENV_IN_PROJECT: + # use .venv as the target virtualenv name + workon_name = '.venv' + else: + workon_name = project.virtualenv_name + cmd = 'pew' - args = ["workon", project.virtualenv_name] + args = ["workon", workon_name] # Grab current terminal dimensions to replace the hardcoded default # dimensions of pexpect terminal_dimensions = get_terminal_size() try: - c = pexpect.spawn( - cmd, - args, - dimensions=( - terminal_dimensions.lines, - terminal_dimensions.columns + with temp_environ(): + if PIPENV_VENV_IN_PROJECT: + os.environ['WORKON_HOME'] = project.project_directory + + c = pexpect.spawn( + cmd, + args, + dimensions=( + terminal_dimensions.lines, + terminal_dimensions.columns + ) ) - ) # Windows! except AttributeError: import subprocess - p = subprocess.Popen([cmd] + list(args), shell=True, universal_newlines=True) - p.communicate() - sys.exit(p.returncode) + # Tell pew to use the project directory as its workon_home + with temp_environ(): + if PIPENV_VENV_IN_PROJECT: + os.environ['WORKON_HOME'] = project.project_directory + p = subprocess.Popen([cmd] + list(args), shell=True, universal_newlines=True) + p.communicate() + sys.exit(p.returncode) # Activate the virtualenv if in compatibility mode. if compat: diff --git a/pipenv/utils.py b/pipenv/utils.py index a796d104..bb118b7c 100644 --- a/pipenv/utils.py +++ b/pipenv/utils.py @@ -23,6 +23,7 @@ try: except ImportError: from urlparse import urlparse +from contextlib import contextmanager from piptools.resolver import Resolver from piptools.repositories.pypi import PyPIRepository from piptools.scripts.compile import get_pip_command @@ -341,6 +342,9 @@ def shellquote(s): """Prepares a string for the shell (on Windows too!)""" if s is None: return None + # Additional escaping for windows paths + if os.name == 'nt': + s = "{}".format(s.replace("\\", "\\\\")) return '"' + s.replace("'", "'\\''") + '"' @@ -530,8 +534,8 @@ def convert_deps_from_pip(dep): extras = {'extras': req.extras} # File installs. - if (req.uri or (os.path.exists(req.path) if req.path else False) or - os.path.exists(req.name)) and not req.vcs: + if (req.uri or (os.path.isfile(req.path) if req.path else False) or + os.path.isfile(req.name)) and not req.vcs: # Assign a package name to the file, last 7 of it's sha256 hex digest. if not req.uri and not req.path: req.path = os.path.abspath(req.name) @@ -850,8 +854,7 @@ def get_windows_path(*args): """Sanitize a path for windows environments Accepts an arbitrary list of arguments and makes a clean windows path""" - clean_path = os.path.join(*args) - return os.path.normpath(clean_path) + return os.path.normpath(os.path.join(*args)) def find_windows_executable(bin_path, exe_name): @@ -915,6 +918,18 @@ def find_requirements(max_depth=3): raise RuntimeError('No requirements.txt found!') +# Borrowed from pew to avoid importing pew which imports psutil +# See https://github.com/berdario/pew/blob/master/pew/_utils.py#L82 +@contextmanager +def temp_environ(): + """Allow the ability to set os.environ temporarily""" + environ = dict(os.environ) + try: + yield + finally: + os.environ.clear() + os.environ.update(environ) + def is_valid_url(url): """Checks if a given string is an url""" pieces = urlparse(url) diff --git a/tests/test_pipenv.py b/tests/test_pipenv.py index c2355e5c..60f6429f 100644 --- a/tests/test_pipenv.py +++ b/tests/test_pipenv.py @@ -6,6 +6,7 @@ import json import pytest from pipenv.cli import activate_virtualenv +from pipenv.utils import temp_environ, get_windows_path from pipenv.vendor import toml from pipenv.vendor import delegator from pipenv.project import Project @@ -227,6 +228,24 @@ class TestPipenv: c = p.pipenv('run python -m requests.help') assert c.return_code > 0 + @pytest.mark.files + @pytest.mark.run + @pytest.mark.uninstall + def test_uninstall_all_local_files(self): + file_name = 'tablib-0.12.1.tar.gz' + # Not sure where travis/appveyor run tests from + test_dir = os.path.dirname(os.path.abspath(__file__)) + source_path = os.path.abspath(os.path.join(test_dir, 'test_artifacts', file_name)) + + with PipenvInstance() as p: + shutil.copy(source_path, os.path.join(p.path, file_name)) + os.mkdir(os.path.join(p.path, "tablib")) + c = p.pipenv('install {}'.format(file_name)) + c = p.pipenv('uninstall --all --verbose') + assert c.return_code == 0 + assert 'tablib' in c.out + assert 'tablib' not in p.pipfile['packages'] + @pytest.mark.extras @pytest.mark.install def test_extras_install(self): @@ -271,33 +290,32 @@ class TestPipenv: @pytest.mark.run @pytest.mark.install def test_multiprocess_bug_and_install(self): - os.environ['PIPENV_MAX_SUBPROCESS'] = '2' + with temp_environ(): + os.environ['PIPENV_MAX_SUBPROCESS'] = '2' - with PipenvInstance() as p: - with open(p.pipfile_path, 'w') as f: - contents = """ + with PipenvInstance() as p: + with open(p.pipfile_path, 'w') as f: + contents = """ [packages] requests = "*" records = "*" tpfd = "*" - """.strip() - f.write(contents) + """.strip() + f.write(contents) - c = p.pipenv('install') - assert c.return_code == 0 + c = p.pipenv('install') + assert c.return_code == 0 - assert 'requests' in p.lockfile['default'] - assert 'idna' in p.lockfile['default'] - assert 'urllib3' in p.lockfile['default'] - assert 'certifi' in p.lockfile['default'] - assert 'records' in p.lockfile['default'] - assert 'tpfd' in p.lockfile['default'] - assert 'parse' in p.lockfile['default'] + assert 'requests' in p.lockfile['default'] + assert 'idna' in p.lockfile['default'] + assert 'urllib3' in p.lockfile['default'] + assert 'certifi' in p.lockfile['default'] + assert 'records' in p.lockfile['default'] + assert 'tpfd' in p.lockfile['default'] + assert 'parse' in p.lockfile['default'] - c = p.pipenv('run python -c "import requests; import idna; import certifi; import records; import tpfd; import parse;"') - assert c.return_code == 0 - - del os.environ['PIPENV_MAX_SUBPROCESS'] + c = p.pipenv('run python -c "import requests; import idna; import certifi; import records; import tpfd; import parse;"') + assert c.return_code == 0 @pytest.mark.sequential @pytest.mark.install @@ -445,14 +463,60 @@ requests = {version = "*"} @pytest.mark.dotvenv def test_venv_in_project(self): - os.environ['PIPENV_VENV_IN_PROJECT'] = '1' - with PipenvInstance() as p: - c = p.pipenv('install requests') - assert c.return_code == 0 + with temp_environ(): + os.environ['PIPENV_VENV_IN_PROJECT'] = '1' + with PipenvInstance() as p: + c = p.pipenv('install requests') + assert c.return_code == 0 - assert p.path in p.pipenv('--venv').out + assert p.path in p.pipenv('--venv').out + + @pytest.mark.dotvenv + @pytest.mark.install + @pytest.mark.complex + @pytest.mark.shell + @pytest.mark.windows + @pytest.mark.pew + def test_shell_nested_venv_in_project(self): + import subprocess + with temp_environ(): + os.environ['PIPENV_VENV_IN_PROJECT'] = '1' + os.environ['PIPENV_IGNORE_VIRTUALENVS'] = '1' + os.environ['PIPENV_SHELL_COMPAT'] = '1' + with PipenvInstance(chdir=True) as p: + # Signal to pew to look in the project directory for the environment + os.environ['WORKON_HOME'] = p.path + project = Project() + c = p.pipenv('install requests') + assert c.return_code == 0 + assert 'requests' in p.pipfile['packages'] + assert 'requests' in p.lockfile['default'] + # Check that .venv now shows in pew's managed list + pew_list = delegator.run('pew ls') + assert '.venv' in pew_list.out + # Check for the venv directory + c = delegator.run('pew dir .venv') + # Compare pew's virtualenv path to what we expect + venv_path = get_windows_path(project.project_directory, '.venv') + # os.path.normpath will normalize slashes + assert venv_path == os.path.normpath(c.out.strip()) + # Have pew run 'pip freeze' in the virtualenv + # This is functionally the same as spawning a subshell + # If we can do this we can theoretically amke a subshell + # This test doesn't work on *nix + if os.name == 'nt': + args = ['pew', 'in', '.venv', 'pip', 'freeze'] + process = subprocess.Popen( + args, + shell=True, + universal_newlines=True, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE + ) + out, _ = process.communicate() + assert any(req.startswith('requests') for req in out.splitlines()) is True - del os.environ['PIPENV_VENV_IN_PROJECT'] @pytest.mark.run @pytest.mark.dotenv diff --git a/tests/test_utils.py b/tests/test_utils.py index e68d9888..176e94ef 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import os import pytest from mock import patch, Mock @@ -165,3 +166,10 @@ class TestUtils: run_ret.out = version_output mocked_delegator.return_value = run_ret assert pipenv.utils.python_version('some/path') == version + + @pytest.mark.windows + @pytest.mark.skipif(os.name != 'nt', reason='Windows test only') + def test_windows_shellquote(self): + test_path = 'C:\Program Files\Python36\python.exe' + expected_path = '"C:\\\\Program Files\\\\Python36\\\\python.exe"' + assert pipenv.utils.shellquote(test_path) == expected_path