diff --git a/.azure-pipelines/steps/run-tests-linux.yml b/.azure-pipelines/steps/run-tests-linux.yml index 01ed3b292f..73eaba8fa6 100644 --- a/.azure-pipelines/steps/run-tests-linux.yml +++ b/.azure-pipelines/steps/run-tests-linux.yml @@ -21,10 +21,11 @@ steps: # Fix Git SSL errors echo "Using pipenv python version: $(PIPENV_DEFAULT_PYTHON_VERSION)" git submodule sync && git submodule update --init --recursive - pipenv run pytest -n 4 -v --junitxml=junit/test-results.xml tests + pipenv run pytest -v -n auto --durations=10 --junitxml=junit/test-results.xml tests displayName: Run integration tests env: PYTHONWARNINGS: ignore:DEPRECATION PIPENV_NOSPIN: '1' + CI: 1 PIPENV_DEFAULT_PYTHON_VERSION: ${{ parameters.python_version }} GIT_SSH_COMMAND: ssh -o StrictHostKeyChecking=accept-new -o CheckHostIP=no diff --git a/.azure-pipelines/steps/run-tests-windows.yml b/.azure-pipelines/steps/run-tests-windows.yml index 3ef60d6940..b37cea2d02 100644 --- a/.azure-pipelines/steps/run-tests-windows.yml +++ b/.azure-pipelines/steps/run-tests-windows.yml @@ -47,11 +47,12 @@ steps: git submodule sync git submodule update --init --recursive $venv = (pipenv --venv)[0] - & $venv/Scripts/pytest.exe -ra -n 4 -v --junit-xml=junit/test-results.xml tests/ + & $venv/Scripts/pytest.exe -ra -n auto -v --durations=10 --junit-xml=junit/test-results.xml tests/ failOnStderr: false displayName: Run integration tests env: TEMP: 'R:\' PYTHONWARNINGS: 'ignore:DEPRECATION' PIPENV_NOSPIN: 1 + CI: 1 GIT_SSH_COMMAND: ssh -o StrictHostKeyChecking=accept-new -o CheckHostIP=no diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 415b0eb3f4..1f676e3b18 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -80,4 +80,4 @@ jobs: PYTHONIOENCODING: 'utf-8' GIT_SSH_COMMAND: ssh -o StrictHostKeyChecking=accept-new -o CheckHostIP=no run: | - pipenv run pytest -ra -n 4 tests + pipenv run pytest -ra -n auto tests diff --git a/pipenv/cli/command.py b/pipenv/cli/command.py index 4387f9864f..4a4bd8142d 100644 --- a/pipenv/cli/command.py +++ b/pipenv/cli/command.py @@ -1,22 +1,21 @@ import os import sys -from click import ( - argument, echo, edit, group, option, pass_context, secho, types, version_option, Choice -) - from pipenv.__version__ import __version__ from pipenv._compat import fix_utf8 -from pipenv.exceptions import PipenvOptionsError -from pipenv.patched import crayons -from pipenv.vendor import click_completion -from pipenv.utils import subprocess_run from pipenv.cli.options import ( CONTEXT_SETTINGS, PipenvGroup, code_option, common_options, deploy_option, general_options, install_options, lock_options, pass_state, pypi_mirror_option, python_option, site_packages_option, skip_lock_option, - sync_options, system_option, three_option, uninstall_options, - verbose_option + sync_options, system_option, three_option, uninstall_options, verbose_option +) +from pipenv.exceptions import PipenvOptionsError +from pipenv.patched import crayons +from pipenv.utils import subprocess_run +from pipenv.vendor import click_completion +from pipenv.vendor.click import ( + Choice, argument, echo, edit, group, option, pass_context, secho, types, + version_option ) @@ -76,7 +75,7 @@ def cli( from .. import shells try: - shell = shells.detect_info()[0] + shell = shells.detect_info(state.project)[0] except shells.ShellDetectionFailure: echo( "Fail to detect shell. Please provide the {} environment " @@ -88,15 +87,8 @@ def cli( return 0 from ..core import ( - system_which, - do_py, - warn_in_virtualenv, - do_where, - project, - cleanup_virtualenv, - ensure_project, - format_help, - do_clear, + cleanup_virtualenv, do_clear, do_py, do_where, ensure_project, + format_help, system_which, warn_in_virtualenv ) from ..utils import create_spinner @@ -110,8 +102,7 @@ def cli( return 1 if envs: echo("The following environment variables can be set, to do various things:\n") - from .. import environments - for key in environments.__dict__: + for key in state.project.__dict__: if key.startswith("PIPENV"): echo(f" - {crayons.normal(key, bold=True)}") echo( @@ -122,46 +113,45 @@ def cli( ) ) return 0 - warn_in_virtualenv() + warn_in_virtualenv(state.project) if ctx.invoked_subcommand is None: # --where was passed... if where: - do_where(bare=True) + do_where(state.project, bare=True) return 0 elif py: - do_py() + do_py(state.project) return 0 # --support was passed... elif support: from ..help import get_pipenv_diagnostics - get_pipenv_diagnostics() + get_pipenv_diagnostics(state.project) return 0 # --clear was passed... elif state.clear: - do_clear() + do_clear(state.project) return 0 # --venv was passed... elif venv: # There is no virtualenv yet. - if not project.virtualenv_exists: + if not state.project.virtualenv_exists: echo( "{}({}){}".format( crayons.red("No virtualenv has been created for this project"), - crayons.normal(project.project_directory, bold=True), + crayons.normal(state.project.project_directory, bold=True), crayons.red(" yet!") ), err=True, ) ctx.abort() else: - echo(project.virtualenv_location) + echo(state.project.virtualenv_location) return 0 # --rm was passed... elif rm: # Abort if --system (or running in a virtualenv). - from ..environments import PIPENV_USE_SYSTEM - if PIPENV_USE_SYSTEM: + if state.project.s.PIPENV_USE_SYSTEM: echo( crayons.red( "You are attempting to remove a virtualenv that " @@ -169,8 +159,8 @@ def cli( ) ) ctx.abort() - if project.virtualenv_exists: - loc = project.virtualenv_location + if state.project.virtualenv_exists: + loc = state.project.virtualenv_location echo( crayons.normal( "{} ({})...".format( @@ -179,9 +169,9 @@ def cli( ) ) ) - with create_spinner(text="Running..."): + with create_spinner(text="Running...", setting=state.project.s): # Remove the virtualenv. - cleanup_virtualenv(bare=True) + cleanup_virtualenv(state.project, bare=True) return 0 else: echo( @@ -195,6 +185,7 @@ def cli( # --two / --three was passed... if (state.python or state.three is not None) or state.site_packages: ensure_project( + state.project, three=state.three, python=state.python, warn=True, @@ -219,16 +210,15 @@ def cli( @skip_lock_option @install_options @pass_state -@pass_context def install( - ctx, state, **kwargs ): """Installs provided packages and adds them to Pipfile, or (if no packages are given), installs all packages from Pipfile.""" from ..core import do_install - retcode = do_install( + do_install( + state.project, dev=state.installstate.dev, three=state.three, python=state.python, @@ -250,8 +240,6 @@ def install( editable_packages=state.installstate.editables, site_packages=state.site_packages ) - if retcode: - ctx.abort() @cli.command( @@ -283,6 +271,7 @@ def uninstall( """Uninstalls a provided package and removes it from Pipfile.""" from ..core import do_uninstall retcode = do_uninstall( + state.project, packages=state.installstate.packages, editable_packages=state.installstate.editables, three=state.three, @@ -325,12 +314,13 @@ def lock( **kwargs ): """Generates Pipfile.lock.""" - from ..core import ensure_project, do_init, do_lock + from ..core import do_init, do_lock, ensure_project + # Ensure that virtualenv is available. # Note that we don't pass clear on to ensure_project as it is also # handled in do_lock ensure_project( - three=state.three, python=state.python, pypi_mirror=state.pypi_mirror, + state.project, three=state.three, python=state.python, pypi_mirror=state.pypi_mirror, warn=(not state.quiet), site_packages=state.site_packages, ) emit_requirements = state.lockoptions.emit_requirements @@ -352,6 +342,7 @@ def lock( # Setting "emit_requirements=True" means do_init() just emits the # install requirements file to stdout, it doesn't install anything do_init( + state.project, dev=dev, dev_only=dev_only, emit_requirements=emit_requirements, @@ -365,6 +356,7 @@ def lock( "Aborting." ) do_lock( + state.project, ctx=ctx, clear=state.clear, pre=pre, @@ -403,7 +395,7 @@ def shell( anyway=False, ): """Spawns a shell within the virtualenv.""" - from ..core import load_dot_env, do_shell + from ..core import do_shell, load_dot_env # Prevent user from activating nested environments. if "PIPENV_ACTIVE" in os.environ: @@ -420,11 +412,12 @@ def shell( ) sys.exit(1) # Load .env file. - load_dot_env() + load_dot_env(state.project) # Use fancy mode for Windows. if os.name == "nt": fancy = True do_shell( + state.project, three=state.three, python=state.python, fancy=fancy, @@ -445,7 +438,7 @@ def run(state, command, args): """Spawns a command installed into the virtualenv.""" from ..core import do_run do_run( - command=command, args=args, three=state.three, python=state.python, pypi_mirror=state.pypi_mirror + state.project, command=command, args=args, three=state.three, python=state.python, pypi_mirror=state.pypi_mirror ) @@ -511,6 +504,7 @@ def check( from ..core import do_check do_check( + state.project, three=state.three, python=state.python, system=state.system, @@ -543,21 +537,15 @@ def update( **kwargs ): """Runs lock, then sync.""" - from ..core import ( - ensure_project, - do_outdated, - do_lock, - do_sync, - project, - ) + from ..core import do_lock, do_outdated, do_sync, ensure_project ensure_project( - three=state.three, python=state.python, pypi_mirror=state.pypi_mirror, + state.project, three=state.three, python=state.python, pypi_mirror=state.pypi_mirror, warn=(not state.quiet), site_packages=state.site_packages, clear=state.clear ) if not outdated: outdated = bool(dry_run) if outdated: - do_outdated(clear=state.clear, pre=state.installstate.pre, pypi_mirror=state.pypi_mirror) + do_outdated(state.project, clear=state.clear, pre=state.installstate.pre, pypi_mirror=state.pypi_mirror) packages = [p for p in state.installstate.packages if p] editable = [p for p in state.installstate.editables if p] if not packages: @@ -572,7 +560,7 @@ def update( ) else: for package in packages + editable: - if package not in project.all_packages: + if package not in state.project.all_packages: echo( "{}: {} was not found in your Pipfile! Aborting." "".format( @@ -583,6 +571,7 @@ def update( ) ctx.abort() do_lock( + state.project, ctx=ctx, clear=state.clear, pre=state.installstate.pre, @@ -591,7 +580,7 @@ def update( write=not state.quiet, ) do_sync( - ctx=ctx, + state.project, dev=state.installstate.dev, three=state.three, python=state.python, @@ -613,11 +602,12 @@ def update( @option("--json", is_flag=True, default=False, help="Output JSON.") @option("--json-tree", is_flag=True, default=False, help="Output JSON in nested tree.") @option("--reverse", is_flag=True, default=False, help="Reversed dependency graph.") -def graph(bare=False, json=False, json_tree=False, reverse=False): +@pass_state +def graph(state, bare=False, json=False, json_tree=False, reverse=False): """Displays currently-installed dependency graph information.""" from ..core import do_graph - do_graph(bare=bare, json=json, json_tree=json_tree, reverse=reverse) + do_graph(state.project, bare=bare, json=json, json_tree=json_tree, reverse=reverse) @cli.command( @@ -635,15 +625,15 @@ def run_open(state, module, *args, **kwargs): EDITOR=atom pipenv open requests """ - from ..core import which, ensure_project, inline_activate_virtual_environment + from ..core import ensure_project, inline_activate_virtual_environment # Ensure that virtualenv is available. ensure_project( - three=state.three, python=state.python, + state.project, three=state.three, python=state.python, validate=False, pypi_mirror=state.pypi_mirror, ) c = subprocess_run( - which("python"), "-c", "import {0}; print({0}.__file__)".format(module) + [state.project._which("python"), "-c", "import {0}; print({0}.__file__)".format(module)] ) if c.returncode: echo(crayons.red("Module not found!")) @@ -653,7 +643,7 @@ def run_open(state, module, *args, **kwargs): else: p = c.stdout.strip().rstrip("cdo") echo(crayons.normal(f"Opening {p!r} in your EDITOR.", bold=True)) - inline_activate_virtual_environment() + inline_activate_virtual_environment(state.project) edit(filename=p) return 0 @@ -679,7 +669,7 @@ def sync( from ..core import do_sync retcode = do_sync( - ctx=ctx, + state.project, dev=state.installstate.dev, three=state.three, python=state.python, @@ -706,11 +696,10 @@ def sync( @three_option @python_option @pass_state -@pass_context -def clean(ctx, state, dry_run=False, bare=False, user=False): +def clean(state, dry_run=False, bare=False, user=False): """Uninstalls all packages not specified in Pipfile.lock.""" from ..core import do_clean - do_clean(ctx=ctx, three=state.three, python=state.python, dry_run=dry_run, + do_clean(state.project, three=state.three, python=state.python, dry_run=dry_run, system=state.system) @@ -719,14 +708,13 @@ def clean(ctx, state, dry_run=False, bare=False, user=False): context_settings=subcommand_context_no_interspersion, ) @common_options -def scripts(): +@pass_state +def scripts(state): """Lists scripts in current environment config.""" - from ..core import project - - if not project.pipfile_exists: + if not state.project.pipfile_exists: echo("No Pipfile present at project home.", err=True) sys.exit(1) - scripts = project.parsed_pipfile.get('scripts', {}) + scripts = state.project.parsed_pipfile.get('scripts', {}) first_column_width = max(len(word) for word in ["Command"] + list(scripts)) second_column_width = max(len(word) for word in ["Script"] + list(scripts.values())) lines = ["{0:<{width}} Script".format("Command", width=first_column_width)] diff --git a/pipenv/cli/options.py b/pipenv/cli/options.py index 7cde367c2b..bc00a572f0 100644 --- a/pipenv/cli/options.py +++ b/pipenv/cli/options.py @@ -1,14 +1,13 @@ import os -import click.types - -from click import ( - BadParameter, BadArgumentUsage, Group, Option, argument, echo, make_pass_decorator, option +from pipenv.project import Project +from pipenv.utils import is_valid_url +from pipenv.vendor.click import ( + BadArgumentUsage, BadParameter, Group, Option, argument, echo, + make_pass_decorator, option ) -from click_didyoumean import DYMMixin - -from .. import environments -from ..utils import is_valid_url +from pipenv.vendor.click import types as click_types +from pipenv.vendor.click_didyoumean import DYMMixin CONTEXT_SETTINGS = { @@ -61,6 +60,7 @@ def __init__(self): self.site_packages = None self.clear = False self.system = False + self.project = Project() self.installstate = InstallState() self.lockoptions = LockOptions() @@ -117,7 +117,7 @@ def callback(ctx, param, value): state.installstate.editables.extend(value) return value return option('-e', '--editable', expose_value=False, multiple=True, - callback=callback, type=click.types.STRING, help=( + callback=callback, type=click_types.STRING, help=( "An editable Python package URL or path, often to a VCS " "repository." ))(f) @@ -130,7 +130,7 @@ def callback(ctx, param, value): return value return option("--sequential", is_flag=True, default=False, expose_value=False, help="Install dependencies one-at-a-time, instead of concurrently.", - callback=callback, type=click.types.BOOL, show_envvar=True)(f) + callback=callback, type=click_types.BOOL, show_envvar=True)(f) def skip_lock_option(f): @@ -140,7 +140,7 @@ def callback(ctx, param, value): return value return option("--skip-lock", is_flag=True, default=False, expose_value=False, help="Skip locking mechanisms and use the Pipfile instead during operation.", - envvar="PIPENV_SKIP_LOCK", callback=callback, type=click.types.BOOL, + envvar="PIPENV_SKIP_LOCK", callback=callback, type=click_types.BOOL, show_envvar=True)(f) @@ -151,7 +151,7 @@ def callback(ctx, param, value): return value return option("--keep-outdated", is_flag=True, default=False, expose_value=False, help="Keep out-dated dependencies from being updated in Pipfile.lock.", - callback=callback, type=click.types.BOOL, show_envvar=True)(f) + callback=callback, type=click_types.BOOL, show_envvar=True)(f) def selective_upgrade_option(f): @@ -159,7 +159,7 @@ def callback(ctx, param, value): state = ctx.ensure_object(State) state.installstate.selective_upgrade = value return value - return option("--selective-upgrade", is_flag=True, default=False, type=click.types.BOOL, + return option("--selective-upgrade", is_flag=True, default=False, type=click_types.BOOL, help="Update specified packages.", callback=callback, expose_value=False)(f) @@ -171,7 +171,7 @@ def callback(ctx, param, value): return value return option("--ignore-pipfile", is_flag=True, default=False, expose_value=False, help="Ignore Pipfile when installing, using the Pipfile.lock.", - callback=callback, type=click.types.BOOL, show_envvar=True)(f) + callback=callback, type=click_types.BOOL, show_envvar=True)(f) def _dev_option(f, help_text): @@ -179,7 +179,7 @@ def callback(ctx, param, value): state = ctx.ensure_object(State) state.installstate.dev = value return value - return option("--dev", "-d", is_flag=True, default=False, type=click.types.BOOL, + return option("--dev", "-d", is_flag=True, default=False, type=click_types.BOOL, help=help_text, callback=callback, expose_value=False, show_envvar=True)(f) @@ -202,7 +202,7 @@ def callback(ctx, param, value): state.installstate.pre = value return value return option("--pre", is_flag=True, default=False, help="Allow pre-releases.", - callback=callback, type=click.types.BOOL, expose_value=False)(f) + callback=callback, type=click_types.BOOL, expose_value=False)(f) def package_arg(f): @@ -211,7 +211,7 @@ def callback(ctx, param, value): state.installstate.packages.extend(value) return value return argument('packages', nargs=-1, callback=callback, expose_value=False, - type=click.types.STRING)(f) + type=click_types.STRING)(f) def three_option(f): @@ -235,17 +235,18 @@ def callback(ctx, param, value): return option("--python", default="", nargs=1, callback=callback, help="Specify which version of Python virtualenv should use.", expose_value=False, allow_from_autoenv=False, - type=click.types.STRING)(f) + type=click_types.STRING)(f) def pypi_mirror_option(f): def callback(ctx, param, value): state = ctx.ensure_object(State) + value = value or state.project.s.PIPENV_PYPI_MIRROR if value is not None: state.pypi_mirror = validate_pypi_mirror(ctx, param, value) return value - return option("--pypi-mirror", default=environments.PIPENV_PYPI_MIRROR, nargs=1, - callback=callback, help="Specify a PyPI mirror.", expose_value=False)(f) + return option("--pypi-mirror", nargs=1, callback=callback, + help="Specify a PyPI mirror.", expose_value=False)(f) def verbose_option(f): @@ -260,7 +261,7 @@ def callback(ctx, param, value): state.verbose = True setup_verbosity(ctx, param, 1) return option("--verbose", "-v", is_flag=True, expose_value=False, - callback=callback, help="Verbose mode.", type=click.types.BOOL)(f) + callback=callback, help="Verbose mode.", type=click_types.BOOL)(f) def quiet_option(f): @@ -275,7 +276,7 @@ def callback(ctx, param, value): state.quiet = True setup_verbosity(ctx, param, -1) return option("--quiet", "-q", is_flag=True, expose_value=False, - callback=callback, help="Quiet mode.", type=click.types.BOOL)(f) + callback=callback, help="Quiet mode.", type=click_types.BOOL)(f) def site_packages_option(f): @@ -294,7 +295,7 @@ def callback(ctx, param, value): state = ctx.ensure_object(State) state.clear = value return value - return option("--clear", is_flag=True, callback=callback, type=click.types.BOOL, + return option("--clear", is_flag=True, callback=callback, type=click_types.BOOL, help="Clears caches (pipenv, pip, and pip-tools).", expose_value=False, show_envvar=True)(f) @@ -306,7 +307,7 @@ def callback(ctx, param, value): state.system = value return value return option("--system", is_flag=True, default=False, help="System pip management.", - callback=callback, type=click.types.BOOL, expose_value=False, + callback=callback, type=click_types.BOOL, expose_value=False, show_envvar=True)(f) @@ -318,7 +319,7 @@ def callback(ctx, param, value): return value return option("--requirements", "-r", nargs=1, default="", expose_value=False, help="Import a requirements.txt file.", callback=callback, - type=click.types.STRING)(f) + type=click_types.STRING)(f) def emit_requirements_flag(f): @@ -359,7 +360,7 @@ def callback(ctx, param, value): return value return option("--code", "-c", nargs=1, default="", help="Install packages " "automatically discovered from import statements.", callback=callback, - expose_value=False, type=click.types.STRING)(f) + expose_value=False, type=click_types.STRING)(f) def deploy_option(f): @@ -367,7 +368,7 @@ def callback(ctx, param, value): state = ctx.ensure_object(State) state.installstate.deploy = value return value - return option("--deploy", is_flag=True, default=False, type=click.types.BOOL, + return option("--deploy", is_flag=True, default=False, type=click_types.BOOL, help="Abort if the Pipfile.lock is out-of-date, or Python version is" " wrong.", callback=callback, expose_value=False)(f) @@ -383,7 +384,7 @@ def setup_verbosity(ctx, param, value): elif value == -1: for logger in loggers: logging.getLogger(logger).setLevel(logging.CRITICAL) - environments.PIPENV_VERBOSITY = value + ctx.ensure_object(State).project.s.PIPENV_VERBOSITY = value def validate_python_path(ctx, param, value): @@ -400,7 +401,7 @@ def validate_python_path(ctx, param, value): def validate_bool_or_none(ctx, param, value): if value is not None: - return click.types.BOOL(value) + return click_types.BOOL(value) return False diff --git a/pipenv/core.py b/pipenv/core.py index 295881cc0a..21b599cee9 100644 --- a/pipenv/core.py +++ b/pipenv/core.py @@ -1,7 +1,7 @@ -import importlib import json as simplejson import logging import os +from posixpath import expandvars import sys import time import warnings @@ -15,27 +15,20 @@ from pipenv import environments, exceptions, pep508checker, progress from pipenv._compat import decode_for_output, fix_utf8 -from pipenv.environments import ( - PIP_EXISTS_ACTION, PIPENV_CACHE_DIR, PIPENV_COLORBLIND, - PIPENV_DEFAULT_PYTHON_VERSION, PIPENV_DONT_USE_ASDF, PIPENV_DONT_USE_PYENV, - PIPENV_HIDE_EMOJIS, PIPENV_MAX_SUBPROCESS, PIPENV_PYUP_API_KEY, - PIPENV_RESOLVE_VCS, PIPENV_SHELL_FANCY, PIPENV_SKIP_VALIDATION, PIPENV_YES, - SESSION_IS_INTERACTIVE, is_type_checking -) from pipenv.patched import crayons -from pipenv.project import Project from pipenv.utils import ( convert_deps_to_pip, create_spinner, download_file, find_python, - find_windows_executable, get_canonical_names, get_source_list, is_pinned, - is_python_command, is_required_version, is_star, is_valid_url, - parse_indexes, pep423_name, prepare_pip_source_args, proper_case, - python_version, run_command, subprocess_run, venv_resolve_deps + get_canonical_names, get_source_list, is_pinned, is_python_command, + is_required_version, is_star, is_valid_url, parse_indexes, pep423_name, + prepare_pip_source_args, proper_case, python_version, run_command, + subprocess_run, venv_resolve_deps ) -if is_type_checking(): +if environments.is_type_checking(): from typing import Dict, List, Optional, Union + from pipenv.project import Project from pipenv.vendor.requirementslib.models.requirements import Requirement TSourceDict = Dict[str, Union[str, bool]] @@ -51,9 +44,8 @@ ) FIRST_PACKAGES = ("cython",) -# Are we using the default Python? -USING_DEFAULT_PYTHON = True -if not PIPENV_HIDE_EMOJIS: + +if not environments.PIPENV_HIDE_EMOJIS: now = time.localtime() # Halloween easter-egg. if ((now.tm_mon == 10) and (now.tm_mday == 30)) or ( @@ -76,41 +68,11 @@ # Enable shell completion. init_completion() # Disable colors, for the color blind and others who do not prefer colors. -if PIPENV_COLORBLIND: +if environments.PIPENV_COLORBLIND: crayons.disable() -def which(command, location=None, allow_global=False): - if not allow_global and location is None: - if project.virtualenv_exists: - location = project.virtualenv_location - else: - location = os.environ.get("VIRTUAL_ENV", None) - if not (location and os.path.exists(location)) and not allow_global: - raise RuntimeError("location not created nor specified") - - version_str = "python{}".format(".".join([str(v) for v in sys.version_info[:2]])) - is_python = command in ("python", os.path.basename(sys.executable), version_str) - if not allow_global: - if os.name == "nt": - p = find_windows_executable(os.path.join(location, "Scripts"), command) - else: - p = os.path.join(location, "bin", command) - else: - if is_python: - p = sys.executable - if not os.path.exists(p): - if is_python: - p = sys.executable or system_which("python") - else: - p = system_which(command) - return p - - -project = Project(which=which) - - -def do_clear(): +def do_clear(project): click.echo(crayons.normal(fix_utf8("Clearing caches..."), bold=True)) try: from pip._internal import locations @@ -118,7 +80,7 @@ def do_clear(): from pip import locations try: - vistir.path.rmtree(PIPENV_CACHE_DIR, onerror=vistir.path.handle_remove_readonly) + vistir.path.rmtree(project.s.PIPENV_CACHE_DIR, onerror=vistir.path.handle_remove_readonly) # Other processes may be writing into this directory simultaneously. vistir.path.rmtree( locations.USER_CACHE_DIR, @@ -134,12 +96,12 @@ def do_clear(): raise -def load_dot_env(): +def load_dot_env(project): """Loads .env file into sys.environ.""" - if not environments.PIPENV_DONT_LOAD_ENV: + if not project.s.PIPENV_DONT_LOAD_ENV: # If the project doesn't exist yet, check current directory for a .env file project_directory = project.project_directory or "." - dotenv_file = environments.PIPENV_DOTENV_LOCATION or os.sep.join( + dotenv_file = project.s.PIPENV_DOTENV_LOCATION or os.sep.join( [project_directory, ".env"] ) @@ -149,27 +111,21 @@ def load_dot_env(): err=True, ) else: - if environments.PIPENV_DOTENV_LOCATION: + if project.s.PIPENV_DOTENV_LOCATION: click.echo( "{}: file {}={} does not exist!!\n{}".format( crayons.red("Warning", bold=True), crayons.normal("PIPENV_DOTENV_LOCATION", bold=True), - crayons.normal(environments.PIPENV_DOTENV_LOCATION, bold=True), + crayons.normal(project.s.PIPENV_DOTENV_LOCATION, bold=True), crayons.red("Not loading environment variables.", bold=True), ), err=True, ) dotenv.load_dotenv(dotenv_file, override=True) - importlib.reload(environments) + project.s.initialize() -def add_to_path(p): - """Adds a given path to the PATH.""" - if p not in os.environ["PATH"]: - os.environ["PATH"] = "{}{}{}".format(p, os.pathsep, os.environ["PATH"]) - - -def cleanup_virtualenv(bare=True): +def cleanup_virtualenv(project, bare=True): """Removes the virtualenv directory from the system.""" if not bare: click.echo(crayons.red("Environment creation aborted.")) @@ -187,7 +143,7 @@ def cleanup_virtualenv(bare=True): click.echo(crayons.cyan(e), err=True) -def import_requirements(r=None, dev=False): +def import_requirements(project, r=None, dev=False): from .patched.notpip._vendor import requests as pip_requests from .vendor.pip_shims.shims import parse_requirements @@ -261,15 +217,14 @@ def import_from_code(path="."): return [] -def ensure_pipfile(validate=True, skip_requirements=False, system=False): +def ensure_pipfile(project, validate=True, skip_requirements=False, system=False): """Creates a Pipfile for the project, if it doesn't exist.""" - from .environments import PIPENV_VIRTUALENV # Assert Pipfile exists. - python = which("python") if not (USING_DEFAULT_PYTHON or system) else None + python = project._which("python") if not (project.s.USING_DEFAULT_PYTHON or system) else None if project.pipfile_is_empty: # Show an error message and exit if system is passed and no pipfile exists - if system and not PIPENV_VIRTUALENV: + if system and not project.s.PIPENV_VIRTUALENV: raise exceptions.PipenvOptionsError( "--system", "--system is intended to be used for pre-existing Pipfile " @@ -285,10 +240,10 @@ def ensure_pipfile(validate=True, skip_requirements=False, system=False): ) # Create a Pipfile... project.create_pipfile(python=python) - with create_spinner("Importing requirements...") as sp: + with create_spinner("Importing requirements...", project.s) as sp: # Import requirements.txt. try: - import_requirements() + import_requirements(project) except Exception: sp.fail(environments.PIPENV_SPINNER_FAIL_TEXT.format("Failed...")) else: @@ -312,7 +267,7 @@ def ensure_pipfile(validate=True, skip_requirements=False, system=False): # Create the pipfile if it doesn't exist. project.create_pipfile(python=python) # Validate the Pipfile's contents. - if validate and project.virtualenv_exists and not PIPENV_SKIP_VALIDATION: + if validate and project.virtualenv_exists and not project.s.PIPENV_SKIP_VALIDATION: # Ensure that Pipfile is using proper casing. p = project.parsed_pipfile changed = project.ensure_proper_casing() @@ -348,12 +303,10 @@ def find_a_system_python(line): return python_entry -def ensure_python(three=None, python=None): +def ensure_python(project, three=None, python=None): # Runtime import is necessary due to the possibility that the environments module may have been reloaded. - from .environments import PIPENV_PYTHON, PIPENV_YES - - if PIPENV_PYTHON and python is False and three is None: - python = PIPENV_PYTHON + if project.s.PIPENV_PYTHON and python is False and three is None: + python = project.s.PIPENV_PYTHON def abort(msg=''): click.echo( @@ -369,17 +322,16 @@ def abort(msg=''): ) sys.exit(1) - global USING_DEFAULT_PYTHON - USING_DEFAULT_PYTHON = three is None and not python + project.s.USING_DEFAULT_PYTHON = three is None and not python # Find out which python is desired. if not python: python = convert_three_to_python(three, python) if not python: python = project.required_python_version if not python: - python = PIPENV_DEFAULT_PYTHON_VERSION + python = project.s.PIPENV_DEFAULT_PYTHON_VERSION path_to_python = find_a_system_python(python) - if environments.is_verbose(): + if project.s.is_verbose(): click.echo(f"Using python: {python}", err=True) click.echo(f"Path to python: {path_to_python}", err=True) if not path_to_python and python is not None: @@ -399,21 +351,21 @@ def abort(msg=''): # dedicated to python installs so probably the preferred # method of the user for new python installs. installer = None - if not PIPENV_DONT_USE_PYENV: + if not project.s.PIPENV_DONT_USE_PYENV: try: - installer = Pyenv() + installer = Pyenv(project) except InstallerNotFound: pass - if installer is None and not PIPENV_DONT_USE_ASDF: + if installer is None and not project.s.PIPENV_DONT_USE_ASDF: try: - installer = Asdf() + installer = Asdf(project) except InstallerNotFound: pass if not installer: abort("Neither 'pyenv' nor 'asdf' could be found to install Python.") else: - if SESSION_IS_INTERACTIVE or PIPENV_YES: + if environments.SESSION_IS_INTERACTIVE or project.s.PIPENV_YES: try: version = installer.find_version_to_install(python) except ValueError: @@ -426,7 +378,7 @@ def abort(msg=''): f"with {installer}?", ) # Prompt the user to continue... - if not (PIPENV_YES or click.confirm(s, default=True)): + if not (project.s.PIPENV_YES or click.confirm(s, default=True)): abort() else: # Tell the user we're installing Python. @@ -439,7 +391,7 @@ def abort(msg=''): crayons.normal("...", bold=True), ) ) - with create_spinner("Installing python...") as sp: + with create_spinner("Installing python...", project.s) as sp: try: c = installer.install(version) except InstallerError as e: @@ -475,25 +427,23 @@ def abort(msg=''): return path_to_python -def ensure_virtualenv(three=None, python=None, site_packages=None, pypi_mirror=None): +def ensure_virtualenv(project, three=None, python=None, site_packages=None, pypi_mirror=None): """Creates a virtualenv, if one doesn't exist.""" - from .environments import PIPENV_USE_SYSTEM def abort(): sys.exit(1) - global USING_DEFAULT_PYTHON if not project.virtualenv_exists: try: # Ensure environment variables are set properly. ensure_environment() # Ensure Python is available. - python = ensure_python(three=three, python=python) + python = ensure_python(project, three=three, python=python) if python is not None and not isinstance(python, str): python = python.path.as_posix() # Create the virtualenv. # Abort if --system (or running in a virtualenv). - if PIPENV_USE_SYSTEM: + if project.s.PIPENV_USE_SYSTEM: click.echo( crayons.red( "You are attempting to re–create a virtualenv that " @@ -502,17 +452,17 @@ def abort(): ) sys.exit(1) do_create_virtualenv( - python=python, site_packages=site_packages, pypi_mirror=pypi_mirror + project, python=python, site_packages=site_packages, pypi_mirror=pypi_mirror ) except KeyboardInterrupt: # If interrupted, cleanup the virtualenv. - cleanup_virtualenv(bare=False) + cleanup_virtualenv(project, bare=False) sys.exit(1) # If --three, --two, or --python were passed... elif (python) or (three is not None) or (site_packages is not None): - USING_DEFAULT_PYTHON = False + project.s.USING_DEFAULT_PYTHON = False # Ensure python is installed before deleting existing virtual env - python = ensure_python(three=three, python=python) + python = ensure_python(project, three=three, python=python) if python is not None and not isinstance(python, str): python = python.path.as_posix() @@ -522,16 +472,17 @@ def abort(): # about, so confirm first. if "VIRTUAL_ENV" in os.environ: if not ( - PIPENV_YES or click.confirm("Remove existing virtualenv?", default=True) + project.s.PIPENV_YES or click.confirm("Remove existing virtualenv?", default=True) ): abort() click.echo( crayons.normal(fix_utf8("Removing existing virtualenv..."), bold=True), err=True ) # Remove the virtualenv. - cleanup_virtualenv(bare=True) + cleanup_virtualenv(project, bare=True) # Call this function again. ensure_virtualenv( + project, three=three, python=python, site_packages=site_packages, @@ -540,6 +491,7 @@ def abort(): def ensure_project( + project, three=None, python=None, validate=True, @@ -552,7 +504,6 @@ def ensure_project( clear=False, ): """Ensures both Pipfile and virtualenv exist for the project.""" - from .environments import PIPENV_USE_SYSTEM # Clear the caches, if appropriate. if clear: @@ -560,13 +511,14 @@ def ensure_project( sys.exit(1) # Automatically use an activated virtualenv. - if PIPENV_USE_SYSTEM: + if project.s.PIPENV_USE_SYSTEM: system = True if not project.pipfile_exists and deploy: raise exceptions.PipfileNotFound # Skip virtualenv creation when --system was used. if not system: ensure_virtualenv( + project, three=three, python=python, site_packages=site_packages, @@ -575,7 +527,7 @@ def ensure_project( if warn: # Warn users if they are using the wrong version of Python. if project.required_python_version: - path_to_python = which("python") or which("py") + path_to_python = project._which("python") or project._which("py") if path_to_python and project.required_python_version not in ( python_version(path_to_python) or "" ): @@ -605,7 +557,7 @@ def ensure_project( raise exceptions.DeployException # Ensure the Pipfile exists. ensure_pipfile( - validate=validate, skip_requirements=skip_requirements, system=system + project, validate=validate, skip_requirements=skip_requirements, system=system ) @@ -623,7 +575,7 @@ def shorten_path(location, bold=False): # return short -def do_where(virtualenv=False, bare=True): +def do_where(project, virtualenv=False, bare=True): """Executes the where functionality.""" if not virtualenv: if not project.pipfile_exists: @@ -655,7 +607,7 @@ def do_where(virtualenv=False, bare=True): click.echo(location) -def _cleanup_procs(procs, failed_deps_queue, retry=True): +def _cleanup_procs(project, procs, failed_deps_queue, retry=True): while not procs.empty(): c = procs.get() try: @@ -665,7 +617,7 @@ def _cleanup_procs(procs, failed_deps_queue, retry=True): failed = c.returncode != 0 if "Ignoring" in out: click.echo(crayons.yellow(out.strip())) - elif environments.is_verbose(): + elif project.s.is_verbose(): click.echo(crayons.cyan(out.strip() or err.strip())) # The Installation failed... if failed: @@ -708,7 +660,7 @@ def _cleanup_procs(procs, failed_deps_queue, retry=True): failed_deps_queue.put(dep) -def batch_install(deps_list, procs, failed_deps_queue, +def batch_install(project, deps_list, procs, failed_deps_queue, requirements_dir, no_deps=True, ignore_hashes=False, allow_global=False, blocking=False, pypi_mirror=None, retry=True, sequential_deps=None): @@ -720,7 +672,7 @@ def batch_install(deps_list, procs, failed_deps_queue, failed = (not retry) install_deps = not no_deps if not failed: - label = INSTALL_LABEL if not PIPENV_HIDE_EMOJIS else "" + label = INSTALL_LABEL if not environments.PIPENV_HIDE_EMOJIS else "" else: label = INSTALL_LABEL2 @@ -752,7 +704,7 @@ def batch_install(deps_list, procs, failed_deps_queue, is_artifact = True elif dep.is_vcs: is_artifact = True - if not PIPENV_RESOLVE_VCS and is_artifact and not dep.editable: + if not project.s.PIPENV_RESOLVE_VCS and is_artifact and not dep.editable: install_deps = True no_deps = False @@ -770,6 +722,7 @@ def batch_install(deps_list, procs, failed_deps_queue, is_sequential = sequential_deps and dep.name in sequential_dep_names is_blocking = any([dep.editable, dep.is_vcs, blocking, is_sequential]) c = pip_install( + project, dep, ignore_hashes=any([ignore_hashes, dep.editable, dep.is_vcs]), allow_global=allow_global, @@ -786,10 +739,11 @@ def batch_install(deps_list, procs, failed_deps_queue, procs.put(c) if procs.full() or procs.qsize() == len(deps_list) or is_sequential: - _cleanup_procs(procs, failed_deps_queue, retry=retry) + _cleanup_procs(project, procs, failed_deps_queue, retry=retry) def do_install_dependencies( + project, dev=False, dev_only=False, bare=False, @@ -835,7 +789,7 @@ def do_install_dependencies( deps_list = list(lockfile.get_requirements(dev=dev, only=dev_only)) if emit_requirements: index_args = prepare_pip_source_args( - get_source_list(pypi_mirror=pypi_mirror, project=project) + get_source_list(project, pypi_mirror=pypi_mirror) ) index_args = " ".join(index_args).replace(" -", "\n-") deps = [ @@ -845,7 +799,7 @@ def do_install_dependencies( click.echo("\n".join(sorted(deps))) sys.exit(0) if concurrent: - nprocs = PIPENV_MAX_SUBPROCESS + nprocs = project.s.PIPENV_MAX_SUBPROCESS else: nprocs = 1 procs = queue.Queue(maxsize=nprocs) @@ -861,11 +815,11 @@ def do_install_dependencies( } batch_install( - normal_deps, procs, failed_deps_queue, requirements_dir, **install_kwargs + project, normal_deps, procs, failed_deps_queue, requirements_dir, **install_kwargs ) if not procs.empty(): - _cleanup_procs(procs, failed_deps_queue) + _cleanup_procs(project, procs, failed_deps_queue) # click.echo(crayons.normal( # decode_for_output("Installing editable and vcs dependencies..."), bold=True @@ -890,10 +844,10 @@ def do_install_dependencies( retry_list.append(failed_dep) install_kwargs.update({"retry": False}) batch_install( - retry_list, procs, failed_deps_queue, requirements_dir, **install_kwargs + project, retry_list, procs, failed_deps_queue, requirements_dir, **install_kwargs ) if not procs.empty(): - _cleanup_procs(procs, failed_deps_queue, retry=False) + _cleanup_procs(project, procs, failed_deps_queue, retry=False) def convert_three_to_python(three, python): @@ -911,7 +865,7 @@ def convert_three_to_python(three, python): return python -def do_create_virtualenv(python=None, site_packages=None, pypi_mirror=None): +def do_create_virtualenv(project, python=None, site_packages=None, pypi_mirror=None): """Creates a virtualenv.""" click.echo( @@ -960,11 +914,11 @@ def do_create_virtualenv(python=None, site_packages=None, pypi_mirror=None): # Actually create the virtualenv. error = None - with create_spinner("Creating virtual environment...") as sp: + with create_spinner("Creating virtual environment...", project.s) as sp: c = subprocess_run(cmd, env=pip_config) click.echo(crayons.cyan(f"{c.stdout}"), err=True) if c.returncode != 0: - error = c.stderr if environments.is_verbose() else exceptions.prettify_exc(c.stderr) + error = c.stderr if project.s.is_verbose() else exceptions.prettify_exc(c.stderr) sp.fail(environments.PIPENV_SPINNER_FAIL_TEXT.format("Failed creating virtual environment")) else: sp.green.ok(environments.PIPENV_SPINNER_OK_TEXT.format("Successfully created virtual environment!")) @@ -991,7 +945,7 @@ def do_create_virtualenv(python=None, site_packages=None, pypi_mirror=None): ) project._environment.add_dist("pipenv") # Say where the virtualenv is. - do_where(virtualenv=True, bare=False) + do_where(project, virtualenv=True, bare=False) def parse_download_fname(fname, name): @@ -1008,7 +962,7 @@ def parse_download_fname(fname, name): return version -def get_downloads_info(names_map, section): +def get_downloads_info(project, names_map, section): from .vendor.requirementslib.models.requirements import Requirement info = [] @@ -1020,7 +974,7 @@ def get_downloads_info(names_map, section): version = parse_download_fname(fname, name) # Get the hash of each file. cmd = [ - which_pip(), + which_pip(project), "hash", os.sep.join([project.download_location, fname]), ] @@ -1043,6 +997,7 @@ def overwrite_dev(prod, dev): def do_lock( + project, ctx=None, system=False, clear=False, @@ -1095,7 +1050,7 @@ def do_lock( # Mutates the lockfile venv_resolve_deps( packages, - which=which, + which=project._which, project=project, dev=is_dev, clear=clear, @@ -1146,7 +1101,7 @@ def do_lock( return lockfile -def do_purge(bare=False, downloads=False, allow_global=False): +def do_purge(project, bare=False, downloads=False, allow_global=False): """Executes the purge functionality.""" if downloads: @@ -1176,10 +1131,10 @@ def do_purge(bare=False, downloads=False, allow_global=False): ) command = [ - which_pip(allow_global=allow_global), + which_pip(project, allow_global=allow_global), "uninstall", "-y", ] + list(to_remove) - if environments.is_verbose(): + if project.s.is_verbose(): click.echo(f"$ {' '.join(command)}") c = subprocess_run(command) if c.returncode != 0: @@ -1191,6 +1146,7 @@ def do_purge(bare=False, downloads=False, allow_global=False): def do_init( + project, dev=False, dev_only=False, emit_requirements=False, @@ -1206,26 +1162,23 @@ def do_init( pypi_mirror=None, ): """Executes the init functionality.""" - from .environments import ( - PIPENV_DEFAULT_PYTHON_VERSION, PIPENV_PYTHON, PIPENV_USE_SYSTEM, - PIPENV_VIRTUALENV - ) + python = None - if PIPENV_PYTHON is not None: - python = PIPENV_PYTHON - elif PIPENV_DEFAULT_PYTHON_VERSION is not None: - python = PIPENV_DEFAULT_PYTHON_VERSION + if project.s.PIPENV_PYTHON is not None: + python = project.s.PIPENV_PYTHON + elif project.s.PIPENV_DEFAULT_PYTHON_VERSION is not None: + python = project.s.PIPENV_DEFAULT_PYTHON_VERSION - if not system and not PIPENV_USE_SYSTEM: + if not system and not project.s.PIPENV_USE_SYSTEM: if not project.virtualenv_exists: try: - do_create_virtualenv(python=python, three=None, pypi_mirror=pypi_mirror) + do_create_virtualenv(project, python=python, three=None, pypi_mirror=pypi_mirror) except KeyboardInterrupt: - cleanup_virtualenv(bare=False) + cleanup_virtualenv(project, bare=False) sys.exit(1) # Ensure the Pipfile exists. if not deploy: - ensure_pipfile(system=system) + ensure_pipfile(project, system=system) if not requirements_dir: requirements_dir = vistir.path.create_tracked_tempdir( suffix="-requirements", prefix="pipenv-" @@ -1245,7 +1198,7 @@ def do_init( ) raise exceptions.DeployException sys.exit(1) - elif (system or allow_global) and not (PIPENV_VIRTUALENV): + elif (system or allow_global) and not (project.s.PIPENV_VIRTUALENV): click.echo( crayons.yellow(fix_utf8( "Pipfile.lock ({}) out of date, but installation " @@ -1267,6 +1220,7 @@ def do_init( err=True, ) do_lock( + project, system=system, pre=pre, keep_outdated=keep_outdated, @@ -1277,7 +1231,7 @@ def do_init( if not project.lockfile_exists and not skip_lock: # Unless we're in a virtualenv not managed by pipenv, abort if we're # using the system's python. - if (system or allow_global) and not (PIPENV_VIRTUALENV): + if (system or allow_global) and not (project.s.PIPENV_VIRTUALENV): raise exceptions.PipenvOptionsError( "--system", "--system is intended to be used for Pipfile installation, " @@ -1290,6 +1244,7 @@ def do_init( err=True, ) do_lock( + project, system=system, pre=pre, keep_outdated=keep_outdated, @@ -1297,6 +1252,7 @@ def do_init( pypi_mirror=pypi_mirror, ) do_install_dependencies( + project, dev=dev, dev_only=dev_only, emit_requirements=emit_requirements, @@ -1319,6 +1275,7 @@ def do_init( def get_pip_args( + project, pre=False, # type: bool verbose=False, # type: bool upgrade=False, # type: bool @@ -1341,7 +1298,7 @@ def get_pip_args( "no_deps": ["--no-deps"], "selective_upgrade": [ "--upgrade-strategy=only-if-needed", - "--exists-action={}".format(PIP_EXISTS_ACTION or "i") + "--exists-action={}".format(project.s.PIP_EXISTS_ACTION or "i") ], "src_dir": src_dir, } @@ -1388,6 +1345,7 @@ def get_requirement_line( def write_requirement_to_file( + project, # type: Project requirement, # type: Requirement requirements_dir=None, # type: Optional[str] src_dir=None, # type: Optional[str] @@ -1405,7 +1363,7 @@ def write_requirement_to_file( prefix="pipenv-", suffix="-requirement.txt", dir=requirements_dir, delete=False ) - if environments.is_verbose(): + if project.s.is_verbose(): click.echo( f"Writing supplied requirement line to temporary file: {line!r}", err=True @@ -1417,6 +1375,7 @@ def write_requirement_to_file( def pip_install( + project, requirement=None, r=None, allow_global=False, @@ -1465,18 +1424,18 @@ def pip_install( no_deps = True r = write_requirement_to_file( - requirement, requirements_dir=requirements_dir, src_dir=src_dir, + project, requirement, requirements_dir=requirements_dir, src_dir=src_dir, include_hashes=not ignore_hashes ) sources = get_source_list( - index, extra_indexes=extra_indexes, trusted_hosts=trusted_hosts, + project, index, extra_indexes=extra_indexes, trusted_hosts=trusted_hosts, pypi_mirror=pypi_mirror ) if r: with open(r, "r") as fh: if "--hash" not in fh.read(): ignore_hashes = True - if environments.is_verbose(): + if project.s.is_verbose(): piplogger.setLevel(logging.WARN) if requirement: click.echo( @@ -1484,9 +1443,9 @@ def pip_install( err=True, ) - pip_command = [which("python", allow_global=allow_global), "-m", "pip", "install"] + pip_command = [project._which("python", allow_global=allow_global), "-m", "pip", "install"] pip_args = get_pip_args( - pre=pre, verbose=environments.is_verbose(), upgrade=True, + project, pre=pre, verbose=project.s.is_verbose(), upgrade=True, selective_upgrade=selective_upgrade, no_use_pep517=not use_pep517, no_deps=no_deps, require_hashes=not ignore_hashes, ) @@ -1496,13 +1455,13 @@ def pip_install( elif line: pip_command.extend(line) pip_command.extend(prepare_pip_source_args(sources)) - if environments.is_verbose(): + if project.s.is_verbose(): click.echo(f"$ {' '.join(pip_command)}", err=True) - cache_dir = vistir.compat.Path(PIPENV_CACHE_DIR) + cache_dir = vistir.compat.Path(project.s.PIPENV_CACHE_DIR) DEFAULT_EXISTS_ACTION = "w" if selective_upgrade: DEFAULT_EXISTS_ACTION = "i" - exists_action = vistir.misc.fs_str(PIP_EXISTS_ACTION or DEFAULT_EXISTS_ACTION) + exists_action = vistir.misc.fs_str(project.s.PIP_EXISTS_ACTION or DEFAULT_EXISTS_ACTION) pip_config = { "PIP_CACHE_DIR": vistir.misc.fs_str(cache_dir.as_posix()), "PIP_WHEEL_DIR": vistir.misc.fs_str(cache_dir.joinpath("wheels").as_posix()), @@ -1513,7 +1472,7 @@ def pip_install( "PATH": vistir.misc.fs_str(os.environ.get("PATH")), } if src_dir: - if environments.is_verbose(): + if project.s.is_verbose(): click.echo(f"Using source directory: {src_dir!r}", err=True) pip_config.update( {"PIP_SRC": vistir.misc.fs_str(src_dir)} @@ -1523,8 +1482,8 @@ def pip_install( return c -def pip_download(package_name): - cache_dir = vistir.compat.Path(PIPENV_CACHE_DIR) +def pip_download(project, package_name): + cache_dir = vistir.compat.Path(project.s.PIPENV_CACHE_DIR) pip_config = { "PIP_CACHE_DIR": vistir.misc.fs_str(cache_dir.as_posix()), "PIP_WHEEL_DIR": vistir.misc.fs_str(cache_dir.joinpath("wheels").as_posix()), @@ -1534,7 +1493,7 @@ def pip_download(package_name): } for source in project.sources: cmd = [ - which_pip(), + which_pip(project), "download", package_name, "-i", source["url"], @@ -1581,7 +1540,7 @@ def fallback_which(command, location=None, allow_global=False, system=False): return "" -def which_pip(allow_global=False): +def which_pip(project, allow_global=False): """Returns the location of virtualenv-installed pip.""" location = None @@ -1589,7 +1548,7 @@ def which_pip(allow_global=False): location = os.environ["VIRTUAL_ENV"] if allow_global: if location: - pip = which("pip", location=location) + pip = project._which("pip", location=location) if pip: return pip @@ -1598,46 +1557,31 @@ def which_pip(allow_global=False): if where: return where - pip = which("pip") + pip = project._which("pip") if not pip: pip = fallback_which("pip", allow_global=allow_global, location=location) return pip -def system_which(command, mult=False): +def system_which(command, path=None): """Emulates the system's which. Returns None if not found.""" - _which = "where" if os.name == "nt" else "which -a" - os.environ.update({ - vistir.compat.fs_str(k): vistir.compat.fs_str(val) - for k, val in os.environ.items() - }) - result = None - try: - c = subprocess_run(f"{_which} {command}", shell=True) - try: - # Which Not found... - if c.returncode == 127: - click.echo( - "{}: the {} system utility is required for Pipenv to find Python installations properly." - "\n Please install it.".format( - crayons.red("Warning", bold=True), crayons.yellow(_which) - ), - err=True, - ) - assert c.returncode == 0 - except AssertionError: - result = fallback_which(command, allow_global=True) - except TypeError: - if not result: - result = fallback_which(command, allow_global=True) - else: - if not result: - result = next(iter([c.stdout, c.stderr]), "").split("\n") - result = next(iter(result)) if not mult else result - return result - if not result: - result = fallback_which(command, allow_global=True) - result = [result] if mult else result + import shutil + + result = shutil.which(command, path=path) + if result is None: + _which = "where" if os.name == "nt" else "which -a" + env = {'PATH': path} if path else None + c = subprocess_run(f"{_which} {command}", shell=True, env=env) + if c.returncode == 127: + click.echo( + "{}: the {} system utility is required for Pipenv to find Python installations properly." + "\n Please install it.".format( + crayons.red("Warning", bold=True), crayons.yellow(_which) + ), + err=True, + ) + if c.returncode == 0: + result = next(iter(c.stdout.splitlines()), None) return result @@ -1732,9 +1676,9 @@ def gen(out): return out -def warn_in_virtualenv(): +def warn_in_virtualenv(project): # Only warn if pipenv isn't already active. - if environments.is_in_virtualenv() and not environments.is_quiet(): + if environments.is_in_virtualenv() and not project.s.is_quiet(): click.echo( "{}: Pipenv found itself running within a virtual environment, " "so it will automatically use that environment, instead of " @@ -1750,7 +1694,7 @@ def warn_in_virtualenv(): ) -def ensure_lockfile(keep_outdated=False, pypi_mirror=None): +def ensure_lockfile(project, keep_outdated=False, pypi_mirror=None): """Ensures that the lockfile is up-to-date.""" if not keep_outdated: keep_outdated = project.settings.get("keep_outdated") @@ -1768,12 +1712,12 @@ def ensure_lockfile(keep_outdated=False, pypi_mirror=None): ), err=True, ) - do_lock(keep_outdated=keep_outdated, pypi_mirror=pypi_mirror) + do_lock(project, keep_outdated=keep_outdated, pypi_mirror=pypi_mirror) else: - do_lock(keep_outdated=keep_outdated, pypi_mirror=pypi_mirror) + do_lock(project, keep_outdated=keep_outdated, pypi_mirror=pypi_mirror) -def do_py(system=False): +def do_py(project, system=False): if not project.virtualenv_exists: click.echo( "{}({}){}".format( @@ -1786,12 +1730,12 @@ def do_py(system=False): return try: - click.echo(which("python", allow_global=system)) + click.echo(project._which("python", allow_global=system)) except AttributeError: click.echo(crayons.red("No project found!")) -def do_outdated(pypi_mirror=None, pre=False, clear=False): +def do_outdated(project, pypi_mirror=None, pre=False, clear=False): # TODO: Allow --skip-lock here? from collections import namedtuple @@ -1817,7 +1761,7 @@ def do_outdated(pypi_mirror=None, pre=False, clear=False): dep = Requirement.from_line(str(result.as_requirement())) packages.update(dep.as_pipfile()) updated_packages = {} - lockfile = do_lock(clear=clear, pre=pre, write=False, pypi_mirror=pypi_mirror) + lockfile = do_lock(project, clear=clear, pre=pre, write=False, pypi_mirror=pypi_mirror) for section in ("develop", "default"): for package in lockfile[section]: try: @@ -1870,6 +1814,7 @@ def do_outdated(pypi_mirror=None, pre=False, clear=False): def do_install( + project, packages=False, editable_packages=False, index_url=False, @@ -1891,7 +1836,6 @@ def do_install( selective_upgrade=False, site_packages=None, ): - from .environments import PIPENV_USE_SYSTEM, PIPENV_VIRTUALENV from .vendor.pip_shims.shims import PipError requirements_directory = vistir.path.create_tracked_tempdir( @@ -1910,6 +1854,7 @@ def do_install( concurrent = not sequential # Ensure that virtualenv is available and pipfile are available ensure_project( + project, three=three, python=python, system=system, @@ -1932,12 +1877,13 @@ def do_install( keep_outdated = project.settings.get("keep_outdated") remote = requirementstxt and is_valid_url(requirementstxt) # Warn and exit if --system is used without a pipfile. - if (system and package_args) and not (PIPENV_VIRTUALENV): + if (system and package_args) and not project.s.PIPENV_VIRTUALENV: raise exceptions.SystemUsageError # Automatically use an activated virtualenv. - if PIPENV_USE_SYSTEM: + if project.s.PIPENV_USE_SYSTEM: system = True if system: + project.s.PIPENV_USE_SYSTEM = True os.environ["PIPENV_USE_SYSTEM"] = "1" # Check if the file is remote or not if remote: @@ -1954,7 +1900,7 @@ def do_install( requirements_url = requirementstxt # Download requirements file try: - download_file(requirements_url, temp_reqs) + download_file(requirements_url, temp_reqs, project.s.PIPENV_MAX_RETRIES) except OSError: fd.close() os.unlink(temp_reqs) @@ -1981,7 +1927,7 @@ def do_install( err=True, ) try: - import_requirements(r=project.path_to(requirementstxt), dev=dev) + import_requirements(project, r=project.path_to(requirementstxt), dev=dev) except (UnicodeDecodeError, PipError) as e: # Don't print the temp file path if remote since it will be deleted. req_path = requirements_url if remote else project.path_to(requirementstxt) @@ -2042,6 +1988,7 @@ def do_install( if pre: project.update_settings({"allow_prereleases": pre}) do_init( + project, dev=dev, allow_global=system, ignore_pipfile=ignore_pipfile, @@ -2063,6 +2010,7 @@ def do_install( pkg_list = packages + [f'-e {pkg}' for pkg in editable_packages] if not system and not project.virtualenv_exists: do_init( + project, dev=dev, system=system, allow_global=system, @@ -2082,7 +2030,7 @@ def do_install( ) ) # pip install: - with vistir.contextmanagers.temp_environ(), create_spinner("Installing...") as sp: + with vistir.contextmanagers.temp_environ(), create_spinner("Installing...", project.s) as sp: if not system: os.environ["PIP_USER"] = vistir.compat.fs_str("0") if "PYTHONHOME" in os.environ: @@ -2100,9 +2048,10 @@ def do_install( sp.text = "Installing..." try: sp.text = f"Installing {pkg_requirement.name}..." - if environments.is_verbose(): + if project.s.is_verbose(): sp.hide_and_write(f"Installing package: {pkg_requirement.as_line(include_hashes=False)}") c = pip_install( + project, pkg_requirement, ignore_hashes=True, allow_global=system, @@ -2124,7 +2073,7 @@ def do_install( vistir.compat.fs_str(f"Error text: {c.stdout}") ) sp.write_err(crayons.cyan(vistir.compat.fs_str(format_pip_error(c.stderr)))) - if environments.is_verbose(): + if project.s.is_verbose(): sp.write_err(crayons.cyan(vistir.compat.fs_str(format_pip_output(c.stdout)))) if "setup.py egg_info" in c.stderr: sp.write_err(vistir.compat.fs_str( @@ -2144,7 +2093,7 @@ def do_install( )) sys.exit(1) # Warn if --editable wasn't passed. - if pkg_requirement.is_vcs and not pkg_requirement.editable and not PIPENV_RESOLVE_VCS: + if pkg_requirement.is_vcs and not pkg_requirement.editable and not project.s.PIPENV_RESOLVE_VCS: sp.write_err( "{}: You installed a VCS dependency in non-editable mode. " "This will work fine, but sub-dependencies will not be resolved by {}." @@ -2183,6 +2132,7 @@ def do_install( if pip_shims_module: os.environ["PIP_SHIMS_BASE_MODULE"] = pip_shims_module do_init( + project, dev=dev, system=system, allow_global=system, @@ -2197,6 +2147,7 @@ def do_install( def do_uninstall( + project, packages=False, editable_packages=False, three=None, @@ -2209,17 +2160,16 @@ def do_uninstall( pypi_mirror=None, ctx=None ): - from .environments import PIPENV_USE_SYSTEM from .vendor.packaging.utils import canonicalize_name from .vendor.requirementslib.models.requirements import Requirement # Automatically use an activated virtualenv. - if PIPENV_USE_SYSTEM: + if project.s.PIPENV_USE_SYSTEM: system = True # Ensure that virtualenv is available. # TODO: We probably shouldn't ensure a project exists if the outcome will be to just # install things in order to remove them... maybe tell the user to install first? - ensure_project(three=three, python=python, pypi_mirror=pypi_mirror) + ensure_project(project, three=three, python=python, pypi_mirror=pypi_mirror) # Un-install all dependencies, if --all was provided. if not any([packages, editable_packages, all_dev, all]): raise exceptions.PipenvUsageError("No package provided!", ctx=ctx) @@ -2260,7 +2210,7 @@ def do_uninstall( bad_pkgs = get_canonical_names(BAD_PACKAGES) ignored_packages = bad_pkgs & set(list(package_map.keys())) for ignored_pkg in ignored_packages: - if environments.is_verbose(): + if project.s.is_verbose(): click.echo(f"Ignoring {ignored_pkg}.", err=True) package_names.discard(package_map[ignored_pkg]) @@ -2275,7 +2225,7 @@ def do_uninstall( )), bold=True ) ) - do_purge(bare=False, allow_global=system) + do_purge(project, bare=False, allow_global=system) sys.exit(0) selected_pkg_map = { @@ -2296,9 +2246,9 @@ def do_uninstall( if package_name in packages_to_remove: with project.environment.activated(): if pip_path is None: - pip_path = which_pip(allow_global=system) + pip_path = which_pip(project, allow_global=system) cmd = [pip_path, "uninstall", package_name, "-y"] - c = run_command(cmd) + c = run_command(cmd, is_verbose=project.s.is_verbose()) click.echo(crayons.cyan(c.stdout)) if c.returncode != 0: failure = True @@ -2339,18 +2289,18 @@ def do_uninstall( if in_packages: project.remove_package_from_pipfile(package_name, dev=False) if lock: - do_lock(system=system, keep_outdated=keep_outdated, pypi_mirror=pypi_mirror) + do_lock(project, system=system, keep_outdated=keep_outdated, pypi_mirror=pypi_mirror) sys.exit(int(failure)) -def do_shell(three=None, python=False, fancy=False, shell_args=None, pypi_mirror=None): +def do_shell(project, three=None, python=False, fancy=False, shell_args=None, pypi_mirror=None): # Ensure that virtualenv is available. ensure_project( - three=three, python=python, validate=False, pypi_mirror=pypi_mirror, + project, three=three, python=python, validate=False, pypi_mirror=pypi_mirror, ) # Support shell compatibility mode. - if PIPENV_SHELL_FANCY: + if project.s.PIPENV_SHELL_FANCY: fancy = True from .shells import choose_shell @@ -2386,9 +2336,9 @@ def do_shell(three=None, python=False, fancy=False, shell_args=None, pypi_mirror shell.fork(*fork_args) -def _inline_activate_virtualenv(): +def _inline_activate_virtualenv(project): try: - activate_this = which("activate_this.py") + activate_this = project._which("activate_this.py") if not activate_this or not os.path.exists(activate_this): raise exceptions.VirtualenvActivationException() with open(activate_this) as f: @@ -2405,7 +2355,7 @@ def _inline_activate_virtualenv(): ) -def _inline_activate_venv(): +def _inline_activate_venv(project): """Built-in venv doesn't have activate_this.py, but doesn't need it anyway. As long as we find the correct executable, built-in venv sets up the @@ -2423,21 +2373,22 @@ def _inline_activate_venv(): os.environ["PATH"] = os.pathsep.join(components) -def inline_activate_virtual_environment(): +def inline_activate_virtual_environment(project): root = project.virtualenv_location if os.path.exists(os.path.join(root, "pyvenv.cfg")): - _inline_activate_venv() + _inline_activate_venv(project) else: - _inline_activate_virtualenv() + _inline_activate_virtualenv(project) if "VIRTUAL_ENV" not in os.environ: os.environ["VIRTUAL_ENV"] = vistir.misc.fs_str(root) -def _launch_windows_subprocess(script): +def _launch_windows_subprocess(script, path): import subprocess + command = system_which(script.command, path=path) - command = system_which(script.command) options = {"universal_newlines": True} + script.cmd_args[1:] = [expandvars(arg) for arg in script.args] # Command not found, maybe this is a shell built-in? if not command: @@ -2456,14 +2407,14 @@ def _launch_windows_subprocess(script): return subprocess.Popen(script.cmdify(), shell=True, **options) -def do_run_nt(script): - p = _launch_windows_subprocess(script) +def do_run_nt(project, script, path): + p = _launch_windows_subprocess(script, path) p.communicate() sys.exit(p.returncode) -def do_run_posix(script, command): - command_path = system_which(script.command) +def do_run_posix(project, script, command, path): + command_path = system_which(script.command, path=path) if not command_path: if project.has_script(command): click.echo( @@ -2493,7 +2444,7 @@ def do_run_posix(script, command): ) -def do_run(command, args, three=None, python=False, pypi_mirror=None): +def do_run(project, command, args, three=None, python=False, pypi_mirror=None): """Attempt to run command either pulling from project or interpreting as executable. Args are appended to the command in [scripts] section of project if found. @@ -2502,15 +2453,19 @@ def do_run(command, args, three=None, python=False, pypi_mirror=None): # Ensure that virtualenv is available. ensure_project( - three=three, python=python, validate=False, pypi_mirror=pypi_mirror, + project, three=three, python=python, validate=False, pypi_mirror=pypi_mirror, ) - load_dot_env() + load_dot_env(project) previous_pip_shims_module = os.environ.pop("PIP_SHIMS_BASE_MODULE", None) - # Activate virtualenv under the current interpreter's environment - inline_activate_virtual_environment() + path = os.getenv('PATH', '') + if project.virtualenv_location: + new_path = os.path.join(project.virtualenv_location, 'Scripts' if os.name == 'nt' else 'bin') + paths = path.split(os.pathsep) + paths.insert(0, new_path) + path = os.pathsep.join(paths) # Set an environment variable, so we know we're in the environment. # Only set PIPENV_ACTIVE after finishing reading virtualenv_location @@ -2524,13 +2479,13 @@ def do_run(command, args, three=None, python=False, pypi_mirror=None): try: script = project.build_script(command, args) cmd_string = ' '.join([script.command] + script.args) - if environments.is_verbose(): + if project.s.is_verbose(): click.echo(crayons.normal(f"$ {cmd_string}"), err=True) except ScriptEmptyError: click.echo("Can't run script {0!r}-it's empty?", err=True) - run_args = [script] - run_kwargs = {} - if os.name == "nt": + run_args = [project, script] + run_kwargs = {'path': path} + if os.name == "nt" or environments.PIPENV_IS_CI: run_fn = do_run_nt else: run_fn = do_run_posix @@ -2546,6 +2501,7 @@ def do_run(command, args, three=None, python=False, pypi_mirror=None): def do_check( + project, three=None, python=False, system=False, @@ -2564,6 +2520,7 @@ def do_check( if not system: # Ensure that virtualenv is available. ensure_project( + project, three=three, python=python, validate=False, @@ -2581,7 +2538,7 @@ def do_check( except ValueError: pass if deps_required: - if not quiet and not environments.is_quiet(): + if not quiet and not project.s.is_quiet(): click.echo( crayons.normal( "The following dependencies appear unused, and may be safe for removal:" @@ -2592,14 +2549,14 @@ def do_check( sys.exit(1) else: sys.exit(0) - if not quiet and not environments.is_quiet(): + if not quiet and not project.s.is_quiet(): click.echo(crayons.normal(decode_for_output("Checking PEP 508 requirements..."), bold=True)) pep508checker_path = pep508checker.__file__.rstrip("cdo") safety_path = os.path.join( os.path.dirname(os.path.abspath(__file__)), "patched", "safety" ) if not system: - python = which("python") + python = project._which("python") else: python = first(system_which(p) for p in ("python", "python3", "python2")) if not python: @@ -2608,7 +2565,7 @@ def do_check( _cmd = [vistir.compat.Path(python).as_posix()] # Run the PEP 508 checker in the virtualenv. cmd = _cmd + [vistir.compat.Path(pep508checker_path).as_posix()] - c = run_command(cmd) + c = run_command(cmd, is_verbose=project.s.is_verbose()) if c.returncode is not None: try: results = simplejson.loads(c.stdout.strip()) @@ -2642,9 +2599,9 @@ def do_check( click.echo(crayons.red("Failed!"), err=True) sys.exit(1) else: - if not quiet and not environments.is_quiet(): + if not quiet and not project.s.is_quiet(): click.echo(crayons.green("Passed!")) - if not quiet and not environments.is_quiet(): + if not quiet and not project.s.is_quiet(): click.echo(crayons.normal( decode_for_output("Checking installed package safety..."), bold=True) ) @@ -2652,7 +2609,7 @@ def do_check( if not isinstance(ignore, (tuple, list)): ignore = [ignore] ignored = [["--ignore", cve] for cve in ignore] - if not quiet and not environments.is_quiet(): + if not quiet and not project.s.is_quiet(): click.echo( crayons.normal( "Notice: Ignoring CVE(s) {}".format(crayons.yellow(", ".join(ignore))) @@ -2668,15 +2625,15 @@ def do_check( cmd = _cmd + [safety_path, "check", f"--{switch}"] if db: - if not quiet and not environments.is_quiet(): + if not quiet and not project.s.is_quiet(): click.echo(crayons.normal(f"Using local database {db}")) cmd.append(f"--db={db}") - elif key or PIPENV_PYUP_API_KEY: - cmd = cmd + [f"--key={key or PIPENV_PYUP_API_KEY}"] + elif key or project.s.PIPENV_PYUP_API_KEY: + cmd = cmd + [f"--key={key or project.s.PIPENV_PYUP_API_KEY}"] if ignored: for cve in ignored: cmd += cve - c = run_command(cmd, catch_exceptions=False) + c = run_command(cmd, catch_exceptions=False, is_verbose=project.s.is_verbose()) if output == "default": try: results = simplejson.loads(c.stdout) @@ -2705,12 +2662,12 @@ def do_check( sys.exit(c.returncode) -def do_graph(bare=False, json=False, json_tree=False, reverse=False): +def do_graph(project, bare=False, json=False, json_tree=False, reverse=False): from pipenv.vendor import pipdeptree from pipenv.vendor.vistir.compat import JSONDecodeError pipdeptree_path = pipdeptree.__file__.rstrip("cdo") try: - python_path = which("python") + python_path = project._which("python") except AttributeError: click.echo( "{}: {}".format( @@ -2780,7 +2737,7 @@ def do_graph(bare=False, json=False, json_tree=False, reverse=False): cmd_args = [python_path, pipdeptree_path, "-l"] if flag: cmd_args.append(flag) - c = run_command(cmd_args) + c = run_command(cmd_args, is_verbose=project.s.is_verbose()) # Run dep-tree. if not bare: if json: @@ -2844,7 +2801,7 @@ def traverse(obj): def do_sync( - ctx, + project, dev=False, three=None, python=None, @@ -2864,6 +2821,7 @@ def do_sync( # Ensure that virtualenv is available if not system. ensure_project( + project, three=three, python=python, validate=False, @@ -2876,6 +2834,7 @@ def do_sync( suffix="-requirements", prefix="pipenv-" ) do_init( + project, dev=dev, allow_global=system, concurrent=(not sequential), @@ -2890,20 +2849,20 @@ def do_sync( def do_clean( - ctx, three=None, python=None, dry_run=False, bare=False, pypi_mirror=None, + project, three=None, python=None, dry_run=False, bare=False, pypi_mirror=None, system=False ): # Ensure that virtualenv is available. from packaging.utils import canonicalize_name - ensure_project(three=three, python=python, validate=False, pypi_mirror=pypi_mirror) - ensure_lockfile(pypi_mirror=pypi_mirror) + ensure_project(project, three=three, python=python, validate=False, pypi_mirror=pypi_mirror) + ensure_lockfile(project, pypi_mirror=pypi_mirror) # Make sure that the virtualenv's site packages are configured correctly # otherwise we may end up removing from the global site packages directory installed_package_names = project.installed_package_names.copy() # Remove known "bad packages" from the list. for bad_package in BAD_PACKAGES: if canonicalize_name(bad_package) in installed_package_names: - if environments.is_verbose(): + if project.s.is_verbose(): click.echo(f"Ignoring {bad_package}.", err=True) installed_package_names.remove(canonicalize_name(bad_package)) # Intelligently detect if --dev should be used or not. @@ -2914,7 +2873,7 @@ def do_clean( if used_package in installed_package_names: installed_package_names.remove(used_package) failure = False - cmd = [which_pip(allow_global=system), "uninstall", "-y", "-qq"] + cmd = [which_pip(project, allow_global=system), "uninstall", "-y", "-qq"] for apparent_bad_package in installed_package_names: if dry_run and not bare: click.echo(apparent_bad_package) @@ -2926,8 +2885,8 @@ def do_clean( ) ) # Uninstall the package. - cmd = [which_pip(), "uninstall", apparent_bad_package, "-y"] - c = run_command(cmd) + cmd = [which_pip(project), "uninstall", apparent_bad_package, "-y"] + c = run_command(cmd, is_verbose=project.s.is_verbose()) if c.returncode != 0: failure = True sys.exit(int(failure)) diff --git a/pipenv/environment.py b/pipenv/environment.py index aa0c120fc0..b07ef84205 100644 --- a/pipenv/environment.py +++ b/pipenv/environment.py @@ -1,5 +1,6 @@ import contextlib import importlib +import itertools import json import operator import os @@ -8,24 +9,27 @@ from sysconfig import get_paths, get_python_version -import itertools import pkg_resources import pipenv +from pipenv.environments import is_type_checking +from pipenv.utils import make_posix, normalize_path, subprocess_run +from pipenv.vendor import vistir from pipenv.vendor.cached_property import cached_property from pipenv.vendor.packaging.utils import canonicalize_name -from pipenv.vendor import vistir -from pipenv.utils import normalize_path, make_posix, subprocess_run +if is_type_checking(): + from types import ModuleType + from typing import ( + ContextManager, Dict, Generator, List, Optional, Set, Union + ) -if False: import pip_shims.shims import tomlkit - from typing import ContextManager, Dict, Generator, List, Optional, Set, Union - from types import ModuleType - from pipenv.project import TSource, TPipfile, Project + + from pipenv.project import Project, TPipfile, TSource from pipenv.vendor.packaging.version import Version BASE_WORKING_SET = pkg_resources.WorkingSet(sys.path) @@ -82,14 +86,6 @@ def safe_import(self, name): if dist: dist.activate() module = importlib.import_module(name) - if name in sys.modules: - try: - importlib.reload(module) - importlib.reload(sys.modules[name]) - except TypeError: - del sys.modules[name] - sys.modules[name] = self._modules[name] - return self._modules[name] return module @classmethod @@ -544,7 +540,6 @@ def get_distributions(self): :rtype: iterator """ - pkg_resources = self.safe_import("pkg_resources") libdirs = self.base_paths["libdirs"].split(os.pathsep) dists = (pkg_resources.find_distributions(libdir) for libdir in libdirs) yield from itertools.chain.from_iterable(dists) @@ -576,7 +571,7 @@ def locate_dist(self, dist): def dist_is_in_project(self, dist): # type: (pkg_resources.Distribution) -> bool """Determine whether the supplied distribution is in the environment.""" - from .project import _normalized + from .environments import normalize_pipfile_path as _normalized prefixes = [ _normalized(prefix) for prefix in self.base_paths["libdirs"].split(os.pathsep) if _normalized(prefix).startswith(_normalized(self.prefix.as_posix())) @@ -600,15 +595,12 @@ def get_installed_packages(self): @contextlib.contextmanager def get_finder(self, pre=False): # type: (bool) -> ContextManager[pip_shims.shims.PackageFinder] - from .vendor.pip_shims.shims import ( - InstallCommand, get_package_finder - ) - from .environments import PIPENV_CACHE_DIR + from .vendor.pip_shims.shims import InstallCommand, get_package_finder pip_command = InstallCommand() pip_args = self._modules["pipenv"].utils.prepare_pip_source_args(self.sources) pip_options, _ = pip_command.parser.parse_args(pip_args) - pip_options.cache_dir = PIPENV_CACHE_DIR + pip_options.cache_dir = self.project.s.PIPENV_CACHE_DIR pip_options.pre = self.pipfile.get("pre", pre) with pip_command._build_session(pip_options) as session: finder = get_package_finder(install_cmd=pip_command, options=pip_options, session=session) @@ -616,7 +608,7 @@ def get_finder(self, pre=False): def get_package_info(self, pre=False): # type: (bool) -> Generator[pkg_resources.Distribution, None, None] - from .vendor.pip_shims.shims import pip_version, parse_version + from .vendor.pip_shims.shims import parse_version, pip_version dependency_links = [] packages = self.get_installed_packages() # This code is borrowed from pip's current implementation @@ -685,7 +677,7 @@ def _get_requirements_for_package(cls, node, key_tree, parent=None, chain=None): return d def get_package_requirements(self, pkg=None): - from .vendor.pipdeptree import flatten, PackageDAG + from .vendor.pipdeptree import PackageDAG, flatten packages = self.get_installed_packages() if pkg: @@ -716,7 +708,7 @@ def reverse_dependency(cls, node): yield new_node def reverse_dependencies(self): - from vistir.misc import unnest, chunked + from vistir.misc import chunked, unnest rdeps = {} for req in self.get_package_requirements(): for d in self.reverse_dependency(req): @@ -877,12 +869,11 @@ def activated(self, include_extras=True, extra_dists=None): ]) os.environ["PYTHONIOENCODING"] = vistir.compat.fs_str("utf-8") os.environ["PYTHONDONTWRITEBYTECODE"] = vistir.compat.fs_str("1") - from .environments import PIPENV_USE_SYSTEM if self.is_venv: os.environ["PYTHONPATH"] = self.base_paths["PYTHONPATH"] os.environ["VIRTUAL_ENV"] = vistir.compat.fs_str(prefix) else: - if not PIPENV_USE_SYSTEM and not os.environ.get("VIRTUAL_ENV"): + if not self.project.s.PIPENV_USE_SYSTEM and not os.environ.get("VIRTUAL_ENV"): os.environ["PYTHONPATH"] = self.base_paths["PYTHONPATH"] os.environ.pop("PYTHONHOME", None) sys.path = self.sys_path @@ -907,7 +898,6 @@ def activated(self, include_extras=True, extra_dists=None): finally: sys.path = original_path sys.prefix = original_prefix - importlib.reload(pkg_resources) @cached_property def finders(self): diff --git a/pipenv/environments.py b/pipenv/environments.py index 4834314361..1471e4502c 100644 --- a/pipenv/environments.py +++ b/pipenv/environments.py @@ -1,7 +1,11 @@ +import glob import os +import pathlib +import re import sys from appdirs import user_cache_dir +from vistir.path import normalize_drive from pipenv._compat import fix_utf8 from pipenv.vendor.vistir.misc import _isatty, fs_str @@ -73,21 +77,31 @@ def get_from_env(arg, prefix="PIPENV", check_for_negation=True): return None -PIPENV_IS_CI = bool("CI" in os.environ or "TF_BUILD" in os.environ) +def normalize_pipfile_path(p): + if p is None: + return None + loc = pathlib.Path(p) + try: + loc = loc.resolve() + except OSError: + loc = loc.absolute() + # Recase the path properly on Windows. From https://stackoverflow.com/a/35229734/5043728 + if os.name == 'nt': + matches = glob.glob(re.sub(r'([^:/\\])(?=[/\\]|$)', r'[\1]', str(loc))) + path_str = matches and matches[0] or str(loc) + else: + path_str = str(loc) + return normalize_drive(os.path.abspath(path_str)) + # HACK: Prevent invalid shebangs with Homebrew-installed Python: # https://bugs.python.org/issue22490 os.environ.pop("__PYVENV_LAUNCHER__", None) - # Load patched pip instead of system pip os.environ["PIP_SHIMS_BASE_MODULE"] = fs_str("pipenv.patched.notpip") - -PIPENV_CACHE_DIR = os.environ.get("PIPENV_CACHE_DIR", user_cache_dir("pipenv")) -"""Location for Pipenv to store it's package cache. - -Default is to use appdir's user cache directory. -""" - +# Internal, to tell whether the command line session is interactive. +SESSION_IS_INTERACTIVE = _isatty(sys.stdout) +PIPENV_IS_CI = bool("CI" in os.environ or "TF_BUILD" in os.environ) PIPENV_COLORBLIND = bool(os.environ.get("PIPENV_COLORBLIND")) """If set, disable terminal colors. @@ -95,272 +109,289 @@ def get_from_env(arg, prefix="PIPENV", check_for_negation=True): to show colors. """ -# Tells Pipenv which Python to default to, when none is provided. -PIPENV_DEFAULT_PYTHON_VERSION = os.environ.get("PIPENV_DEFAULT_PYTHON_VERSION") -"""Use this Python version when creating new virtual environments by default. +PIPENV_HIDE_EMOJIS = ( + os.environ.get("PIPENV_HIDE_EMOJIS") is None + and (os.name == "nt" or PIPENV_IS_CI) + or _is_env_truthy("PIPENV_HIDE_EMOJIS") +) +"""Disable emojis in output. -This can be set to a version string, e.g. ``3.6``, or a path. Default is to use -whatever Python Pipenv is installed under (i.e. ``sys.executable``). Command -line flags (e.g. ``--python``, ``--three``, and ``--two``) are prioritized over -this configuration. +Default is to show emojis. This is automatically set on Windows. """ -PIPENV_DONT_LOAD_ENV = bool(os.environ.get("PIPENV_DONT_LOAD_ENV")) -"""If set, Pipenv does not load the ``.env`` file. -Default is to load ``.env`` for ``run`` and ``shell`` commands. -""" +class Setting: + def __init__(self) -> None: + self.USING_DEFAULT_PYTHON = True + self.initialize() -PIPENV_DONT_USE_PYENV = bool(os.environ.get("PIPENV_DONT_USE_PYENV")) -"""If set, Pipenv does not attempt to install Python with pyenv. + def initialize(self): -Default is to install Python automatically via pyenv when needed, if possible. -""" + self.PIPENV_CACHE_DIR = os.environ.get("PIPENV_CACHE_DIR", user_cache_dir("pipenv")) + """Location for Pipenv to store it's package cache. -PIPENV_DONT_USE_ASDF = bool(os.environ.get("PIPENV_DONT_USE_ASDF")) -"""If set, Pipenv does not attempt to install Python with asdf. + Default is to use appdir's user cache directory. + """ -Default is to install Python automatically via asdf when needed, if possible. -""" + # Tells Pipenv which Python to default to, when none is provided. + self.PIPENV_DEFAULT_PYTHON_VERSION = os.environ.get("PIPENV_DEFAULT_PYTHON_VERSION") + """Use this Python version when creating new virtual environments by default. -PIPENV_DOTENV_LOCATION = os.environ.get("PIPENV_DOTENV_LOCATION") -"""If set, Pipenv loads the ``.env`` file at the specified location. + This can be set to a version string, e.g. ``3.6``, or a path. Default is to use + whatever Python Pipenv is installed under (i.e. ``sys.executable``). Command + line flags (e.g. ``--python``, ``--three``, and ``--two``) are prioritized over + this configuration. + """ -Default is to load ``.env`` from the project root, if found. -""" + self.PIPENV_DONT_LOAD_ENV = bool(os.environ.get("PIPENV_DONT_LOAD_ENV")) + """If set, Pipenv does not load the ``.env`` file. -PIPENV_EMULATOR = os.environ.get("PIPENV_EMULATOR", "") -"""If set, the terminal emulator's name for ``pipenv shell`` to use. + Default is to load ``.env`` for ``run`` and ``shell`` commands. + """ -Default is to detect emulators automatically. This should be set if your -emulator, e.g. Cmder, cannot be detected correctly. -""" + self.PIPENV_DONT_USE_PYENV = bool(os.environ.get("PIPENV_DONT_USE_PYENV")) + """If set, Pipenv does not attempt to install Python with pyenv. -PIPENV_HIDE_EMOJIS = ( - os.environ.get("PIPENV_HIDE_EMOJIS") is None - and (os.name == "nt" or PIPENV_IS_CI) - or _is_env_truthy("PIPENV_HIDE_EMOJIS") -) -"""Disable emojis in output. + Default is to install Python automatically via pyenv when needed, if possible. + """ -Default is to show emojis. This is automatically set on Windows. -""" + self.PIPENV_DONT_USE_ASDF = bool(os.environ.get("PIPENV_DONT_USE_ASDF")) + """If set, Pipenv does not attempt to install Python with asdf. -PIPENV_IGNORE_VIRTUALENVS = bool(os.environ.get("PIPENV_IGNORE_VIRTUALENVS")) -"""If set, Pipenv will always assign a virtual environment for this project. + Default is to install Python automatically via asdf when needed, if possible. + """ -By default, Pipenv tries to detect whether it is run inside a virtual -environment, and reuses it if possible. This is usually the desired behavior, -and enables the user to use any user-built environments with Pipenv. -""" + self.PIPENV_DOTENV_LOCATION = os.environ.get("PIPENV_DOTENV_LOCATION") + """If set, Pipenv loads the ``.env`` file at the specified location. -PIPENV_INSTALL_TIMEOUT = int(os.environ.get("PIPENV_INSTALL_TIMEOUT", 60 * 15)) -"""Max number of seconds to wait for package installation. + Default is to load ``.env`` from the project root, if found. + """ -Defaults to 900 (15 minutes), a very long arbitrary time. -""" + self.PIPENV_EMULATOR = os.environ.get("PIPENV_EMULATOR", "") + """If set, the terminal emulator's name for ``pipenv shell`` to use. -# NOTE: +1 because of a temporary bug in Pipenv. -PIPENV_MAX_DEPTH = int(os.environ.get("PIPENV_MAX_DEPTH", "3")) + 1 -"""Maximum number of directories to recursively search for a Pipfile. + Default is to detect emulators automatically. This should be set if your + emulator, e.g. Cmder, cannot be detected correctly. + """ -Default is 3. See also ``PIPENV_NO_INHERIT``. -""" + self.PIPENV_IGNORE_VIRTUALENVS = bool(os.environ.get("PIPENV_IGNORE_VIRTUALENVS")) + """If set, Pipenv will always assign a virtual environment for this project. -PIPENV_MAX_RETRIES = int( - os.environ.get("PIPENV_MAX_RETRIES", "1" if PIPENV_IS_CI else "0") -) -"""Specify how many retries Pipenv should attempt for network requests. + By default, Pipenv tries to detect whether it is run inside a virtual + environment, and reuses it if possible. This is usually the desired behavior, + and enables the user to use any user-built environments with Pipenv. + """ -Default is 0. Automatically set to 1 on CI environments for robust testing. -""" + self.PIPENV_INSTALL_TIMEOUT = int(os.environ.get("PIPENV_INSTALL_TIMEOUT", 60 * 15)) + """Max number of seconds to wait for package installation. -PIPENV_MAX_ROUNDS = int(os.environ.get("PIPENV_MAX_ROUNDS", "16")) -"""Tells Pipenv how many rounds of resolving to do for Pip-Tools. + Defaults to 900 (15 minutes), a very long arbitrary time. + """ -Default is 16, an arbitrary number that works most of the time. -""" + # NOTE: +1 because of a temporary bug in Pipenv. + self.PIPENV_MAX_DEPTH = int(os.environ.get("PIPENV_MAX_DEPTH", "3")) + 1 + """Maximum number of directories to recursively search for a Pipfile. -PIPENV_MAX_SUBPROCESS = int(os.environ.get("PIPENV_MAX_SUBPROCESS", "8")) -"""How many subprocesses should Pipenv use when installing. + Default is 3. See also ``PIPENV_NO_INHERIT``. + """ -Default is 16, an arbitrary number that seems to work. -""" + self.PIPENV_MAX_RETRIES = int( + os.environ.get("PIPENV_MAX_RETRIES", "1" if PIPENV_IS_CI else "0") + ) + """Specify how many retries Pipenv should attempt for network requests. -PIPENV_NO_INHERIT = "PIPENV_NO_INHERIT" in os.environ -"""Tell Pipenv not to inherit parent directories. + Default is 0. Automatically set to 1 on CI environments for robust testing. + """ -This is useful for deployment to avoid using the wrong current directory. -Overwrites ``PIPENV_MAX_DEPTH``. -""" -if PIPENV_NO_INHERIT: - PIPENV_MAX_DEPTH = 2 + self.PIPENV_MAX_ROUNDS = int(os.environ.get("PIPENV_MAX_ROUNDS", "16")) + """Tells Pipenv how many rounds of resolving to do for Pip-Tools. -PIPENV_NOSPIN = bool(os.environ.get("PIPENV_NOSPIN")) -"""If set, disable terminal spinner. + Default is 16, an arbitrary number that works most of the time. + """ -This can make the logs cleaner. Automatically set on Windows, and in CI -environments. -""" -if PIPENV_IS_CI: - PIPENV_NOSPIN = True + self.PIPENV_MAX_SUBPROCESS = int(os.environ.get("PIPENV_MAX_SUBPROCESS", "8")) + """How many subprocesses should Pipenv use when installing. -PIPENV_SPINNER = "dots" if not os.name == "nt" else "bouncingBar" -PIPENV_SPINNER = os.environ.get("PIPENV_SPINNER", PIPENV_SPINNER) -"""Sets the default spinner type. + Default is 16, an arbitrary number that seems to work. + """ -Spinners are identical to the ``node.js`` spinners and can be found at -https://github.com/sindresorhus/cli-spinners -""" + self.PIPENV_NO_INHERIT = "PIPENV_NO_INHERIT" in os.environ + """Tell Pipenv not to inherit parent directories. -PIPENV_PIPFILE = os.environ.get("PIPENV_PIPFILE") -"""If set, this specifies a custom Pipfile location. + This is useful for deployment to avoid using the wrong current directory. + Overwrites ``PIPENV_MAX_DEPTH``. + """ + if self.PIPENV_NO_INHERIT: + self.PIPENV_MAX_DEPTH = 2 -When running pipenv from a location other than the same directory where the -Pipfile is located, instruct pipenv to find the Pipfile in the location -specified by this environment variable. + self.PIPENV_NOSPIN = bool(os.environ.get("PIPENV_NOSPIN")) + """If set, disable terminal spinner. -Default is to find Pipfile automatically in the current and parent directories. -See also ``PIPENV_MAX_DEPTH``. -""" + This can make the logs cleaner. Automatically set on Windows, and in CI + environments. + """ + if PIPENV_IS_CI: + self.PIPENV_NOSPIN = True -PIPENV_PYPI_MIRROR = os.environ.get("PIPENV_PYPI_MIRROR") -"""If set, tells pipenv to override PyPI index urls with a mirror. + pipenv_spinner = "dots" if not os.name == "nt" else "bouncingBar" + self.PIPENV_SPINNER = os.environ.get("PIPENV_SPINNER", pipenv_spinner) + """Sets the default spinner type. -Default is to not mirror PyPI, i.e. use the real one, pypi.org. The -``--pypi-mirror`` command line flag overwrites this. -""" + Spinners are identical to the ``node.js`` spinners and can be found at + https://github.com/sindresorhus/cli-spinners + """ -PIPENV_QUIET = bool(os.environ.get("PIPENV_QUIET")) -"""If set, makes Pipenv quieter. + pipenv_pipfile = os.environ.get("PIPENV_PIPFILE") + if pipenv_pipfile: + if not os.path.isfile(pipenv_pipfile): + raise RuntimeError("Given PIPENV_PIPFILE is not found!") -Default is unset, for normal verbosity. ``PIPENV_VERBOSE`` overrides this. -""" + else: + pipenv_pipfile = normalize_pipfile_path(pipenv_pipfile) + # Overwrite environment variable so that subprocesses can get the correct path. + # See https://github.com/pypa/pipenv/issues/3584 + os.environ['PIPENV_PIPFILE'] = pipenv_pipfile + self.PIPENV_PIPFILE = pipenv_pipfile + """If set, this specifies a custom Pipfile location. -PIPENV_SHELL = os.environ.get("PIPENV_SHELL") -"""An absolute path to the preferred shell for ``pipenv shell``. + When running pipenv from a location other than the same directory where the + Pipfile is located, instruct pipenv to find the Pipfile in the location + specified by this environment variable. -Default is to detect automatically what shell is currently in use. -""" -# Hack because PIPENV_SHELL is actually something else. Internally this -# variable is called PIPENV_SHELL_EXPLICIT instead. -PIPENV_SHELL_EXPLICIT = PIPENV_SHELL -del PIPENV_SHELL + Default is to find Pipfile automatically in the current and parent directories. + See also ``PIPENV_MAX_DEPTH``. + """ -PIPENV_SHELL_FANCY = bool(os.environ.get("PIPENV_SHELL_FANCY")) -"""If set, always use fancy mode when invoking ``pipenv shell``. + self.PIPENV_PYPI_MIRROR = os.environ.get("PIPENV_PYPI_MIRROR") + """If set, tells pipenv to override PyPI index urls with a mirror. -Default is to use the compatibility shell if possible. -""" + Default is to not mirror PyPI, i.e. use the real one, pypi.org. The + ``--pypi-mirror`` command line flag overwrites this. + """ -PIPENV_TIMEOUT = int(os.environ.get("PIPENV_TIMEOUT", 120)) -"""Max number of seconds Pipenv will wait for virtualenv creation to complete. + self.PIPENV_QUIET = bool(os.environ.get("PIPENV_QUIET")) + """If set, makes Pipenv quieter. -Default is 120 seconds, an arbitrary number that seems to work. -""" + Default is unset, for normal verbosity. ``PIPENV_VERBOSE`` overrides this. + """ -PIPENV_VENV_IN_PROJECT = bool(os.environ.get("PIPENV_VENV_IN_PROJECT")) -"""If set, creates ``.venv`` in your project directory. + self.PIPENV_SHELL_EXPLICIT = os.environ.get("PIPENV_SHELL") + """An absolute path to the preferred shell for ``pipenv shell``. -Default is to create new virtual environments in a global location. -""" + Default is to detect automatically what shell is currently in use. + """ + # Hack because PIPENV_SHELL is actually something else. Internally this + # variable is called PIPENV_SHELL_EXPLICIT instead. -PIPENV_VERBOSE = bool(os.environ.get("PIPENV_VERBOSE")) -"""If set, makes Pipenv more wordy. + self.PIPENV_SHELL_FANCY = bool(os.environ.get("PIPENV_SHELL_FANCY")) + """If set, always use fancy mode when invoking ``pipenv shell``. -Default is unset, for normal verbosity. This takes precedence over -``PIPENV_QUIET``. -""" + Default is to use the compatibility shell if possible. + """ -PIPENV_YES = bool(os.environ.get("PIPENV_YES")) -"""If set, Pipenv automatically assumes "yes" at all prompts. + self.PIPENV_TIMEOUT = int(os.environ.get("PIPENV_TIMEOUT", 120)) + """Max number of seconds Pipenv will wait for virtualenv creation to complete. -Default is to prompt the user for an answer if the current command line session -if interactive. -""" + Default is 120 seconds, an arbitrary number that seems to work. + """ -PIPENV_SKIP_LOCK = False -"""If set, Pipenv won't lock dependencies automatically. + self.PIPENV_VENV_IN_PROJECT = bool(os.environ.get("PIPENV_VENV_IN_PROJECT")) + """If set, creates ``.venv`` in your project directory. -This might be desirable if a project has large number of dependencies, -because locking is an inherently slow operation. + Default is to create new virtual environments in a global location. + """ -Default is to lock dependencies and update ``Pipfile.lock`` on each run. + self.PIPENV_VERBOSE = bool(os.environ.get("PIPENV_VERBOSE")) + """If set, makes Pipenv more wordy. -NOTE: This only affects the ``install`` and ``uninstall`` commands. -""" + Default is unset, for normal verbosity. This takes precedence over + ``PIPENV_QUIET``. + """ -PIP_EXISTS_ACTION = os.environ.get("PIP_EXISTS_ACTION", "w") -"""Specifies the value for pip's --exists-action option + self.PIPENV_YES = bool(os.environ.get("PIPENV_YES")) + """If set, Pipenv automatically assumes "yes" at all prompts. -Defaults to ``(w)ipe`` -""" + Default is to prompt the user for an answer if the current command line session + if interactive. + """ -PIPENV_RESOLVE_VCS = ( - os.environ.get("PIPENV_RESOLVE_VCS") is None - or _is_env_truthy("PIPENV_RESOLVE_VCS") -) + self.PIPENV_SKIP_LOCK = False + """If set, Pipenv won't lock dependencies automatically. -"""Tells Pipenv whether to resolve all VCS dependencies in full. + This might be desirable if a project has large number of dependencies, + because locking is an inherently slow operation. -As of Pipenv 2018.11.26, only editable VCS dependencies were resolved in full. -To retain this behavior and avoid handling any conflicts that arise from the new -approach, you may set this to '0', 'off', or 'false'. -""" + Default is to lock dependencies and update ``Pipfile.lock`` on each run. -PIPENV_PYUP_API_KEY = os.environ.get( - "PIPENV_PYUP_API_KEY", None -) + NOTE: This only affects the ``install`` and ``uninstall`` commands. + """ -# Internal, support running in a different Python from sys.executable. -PIPENV_PYTHON = os.environ.get("PIPENV_PYTHON") + self.PIP_EXISTS_ACTION = os.environ.get("PIP_EXISTS_ACTION", "w") + """Specifies the value for pip's --exists-action option -# Internal, overwrite all index funcitonality. -PIPENV_TEST_INDEX = os.environ.get("PIPENV_TEST_INDEX") + Defaults to ``(w)ipe`` + """ -# Internal, tells Pipenv about the surrounding environment. -PIPENV_USE_SYSTEM = False -PIPENV_VIRTUALENV = None -if "PIPENV_ACTIVE" not in os.environ and not PIPENV_IGNORE_VIRTUALENVS: - PIPENV_VIRTUALENV = os.environ.get("VIRTUAL_ENV") - PIPENV_USE_SYSTEM = bool(PIPENV_VIRTUALENV) + self.PIPENV_RESOLVE_VCS = ( + os.environ.get("PIPENV_RESOLVE_VCS") is None + or _is_env_truthy("PIPENV_RESOLVE_VCS") + ) -# Internal, tells Pipenv to skip case-checking (slow internet connections). -# This is currently always set to True for performance reasons. -PIPENV_SKIP_VALIDATION = True + """Tells Pipenv whether to resolve all VCS dependencies in full. -# Internal, the default shell to use if shell detection fails. -PIPENV_SHELL = ( - os.environ.get("SHELL") - or os.environ.get("PYENV_SHELL") - or os.environ.get("COMSPEC") -) + As of Pipenv 2018.11.26, only editable VCS dependencies were resolved in full. + To retain this behavior and avoid handling any conflicts that arise from the new + approach, you may set this to '0', 'off', or 'false'. + """ -# Internal, to tell whether the command line session is interactive. -SESSION_IS_INTERACTIVE = _isatty(sys.stdout) + self.PIPENV_PYUP_API_KEY = os.environ.get( + "PIPENV_PYUP_API_KEY", None + ) -# Internal, consolidated verbosity representation as an integer. The default -# level is 0, increased for wordiness and decreased for terseness. -PIPENV_VERBOSITY = os.environ.get("PIPENV_VERBOSITY", "") -try: - PIPENV_VERBOSITY = int(PIPENV_VERBOSITY) -except (ValueError, TypeError): - if PIPENV_VERBOSE: - PIPENV_VERBOSITY = 1 - elif PIPENV_QUIET: - PIPENV_VERBOSITY = -1 - else: - PIPENV_VERBOSITY = 0 -del PIPENV_QUIET -del PIPENV_VERBOSE + # Internal, support running in a different Python from sys.executable. + self.PIPENV_PYTHON = os.environ.get("PIPENV_PYTHON") + + # Internal, overwrite all index funcitonality. + self.PIPENV_TEST_INDEX = os.environ.get("PIPENV_TEST_INDEX") + + # Internal, tells Pipenv about the surrounding environment. + self.PIPENV_USE_SYSTEM = False + self.PIPENV_VIRTUALENV = None + if "PIPENV_ACTIVE" not in os.environ and not self.PIPENV_IGNORE_VIRTUALENVS: + self.PIPENV_VIRTUALENV = os.environ.get("VIRTUAL_ENV") + self.PIPENV_USE_SYSTEM = bool(self.PIPENV_VIRTUALENV) + + # Internal, tells Pipenv to skip case-checking (slow internet connections). + # This is currently always set to True for performance reasons. + self.PIPENV_SKIP_VALIDATION = True + # Internal, the default shell to use if shell detection fails. + self.PIPENV_SHELL = ( + os.environ.get("SHELL") + or os.environ.get("PYENV_SHELL") + or os.environ.get("COMSPEC") + ) -def is_verbose(threshold=1): - return PIPENV_VERBOSITY >= threshold + # Internal, consolidated verbosity representation as an integer. The default + # level is 0, increased for wordiness and decreased for terseness. + verbosity = os.environ.get("PIPENV_VERBOSITY", "") + try: + self.PIPENV_VERBOSITY = int(verbosity) + except (ValueError, TypeError): + if self.PIPENV_VERBOSE: + self.PIPENV_VERBOSITY = 1 + elif self.PIPENV_QUIET: + self.PIPENV_VERBOSITY = -1 + else: + self.PIPENV_VERBOSITY = 0 + del self.PIPENV_QUIET + del self.PIPENV_VERBOSE + def is_verbose(self, threshold=1): + return self.PIPENV_VERBOSITY >= threshold -def is_quiet(threshold=-1): - return PIPENV_VERBOSITY <= threshold + def is_quiet(self, threshold=-1): + return self.PIPENV_VERBOSITY <= threshold def is_using_venv(): @@ -389,11 +420,6 @@ def is_in_virtualenv(): return virtual_env and not (pipenv_active or ignore_virtualenvs) -PIPENV_SPINNER_FAIL_TEXT = fix_utf8("✘ {0}") if not PIPENV_HIDE_EMOJIS else ("{0}") - -PIPENV_SPINNER_OK_TEXT = fix_utf8("✔ {0}") if not PIPENV_HIDE_EMOJIS else ("{0}") - - def is_type_checking(): try: from typing import TYPE_CHECKING @@ -403,3 +429,5 @@ def is_type_checking(): MYPY_RUNNING = is_type_checking() +PIPENV_SPINNER_FAIL_TEXT = fix_utf8("✘ {0}") if not PIPENV_HIDE_EMOJIS else "{0}" +PIPENV_SPINNER_OK_TEXT = fix_utf8("✔ {0}") if not PIPENV_HIDE_EMOJIS else "{0}" diff --git a/pipenv/exceptions.py b/pipenv/exceptions.py index 08f56cbfda..144f75e6eb 100644 --- a/pipenv/exceptions.py +++ b/pipenv/exceptions.py @@ -39,7 +39,7 @@ def handle_exception(exc_type, exception, traceback, hook=sys.excepthook): - if environments.is_verbose() or not issubclass(exc_type, ClickException): + if environments.Setting().is_verbose() or not issubclass(exc_type, ClickException): hook(exc_type, exception, traceback) else: tb = format_tb(traceback, limit=-6) @@ -258,17 +258,6 @@ def __init__(self, option_name="system", message=None, ctx=None, **kwargs): super().__init__(option_name, message=message, ctx=ctx, extra=extra, **kwargs) -class PipfileException(PipenvFileError): - def __init__(self, hint=None, **kwargs): - from .core import project - - if not hint: - hint = "{} {}".format(crayons.red("ERROR (PACKAGE NOT INSTALLED):"), hint) - filename = project.pipfile_location - extra = kwargs.pop("extra", []) - PipenvFileError.__init__(self, filename, hint, extra=extra, **kwargs) - - class SetupException(PipenvException): def __init__(self, message=None, **kwargs): PipenvException.__init__(self, message, **kwargs) diff --git a/pipenv/help.py b/pipenv/help.py index 6d2f62d90d..2882bca177 100644 --- a/pipenv/help.py +++ b/pipenv/help.py @@ -4,7 +4,6 @@ import pipenv -from pipenv.core import project from pipenv.pep508checker import lookup from pipenv.vendor import pythonfinder @@ -16,7 +15,7 @@ def print_utf(line): print(line.encode("utf-8")) -def get_pipenv_diagnostics(): +def get_pipenv_diagnostics(project): print("
$ pipenv --support") print("") print(f"Pipenv version: `{pipenv.__version__!r}`") @@ -82,4 +81,5 @@ def get_pipenv_diagnostics(): if __name__ == "__main__": - get_pipenv_diagnostics() + from pipenv.project import Project + get_pipenv_diagnostics(Project()) diff --git a/pipenv/installers.py b/pipenv/installers.py index 9fdabe5676..0b069d2633 100644 --- a/pipenv/installers.py +++ b/pipenv/installers.py @@ -3,8 +3,6 @@ import re from abc import ABCMeta, abstractmethod - -from pipenv.environments import PIPENV_INSTALL_TIMEOUT from pipenv.vendor import attr from pipenv.utils import find_windows_executable, subprocess_run @@ -65,9 +63,9 @@ def __init__(self, desc, c): class Installer(metaclass=ABCMeta): - def __init__(self): + def __init__(self, project): self.cmd = self._find_installer() - super().__init__() + self.project = project def __str__(self): return self.__class__.__name__ @@ -187,7 +185,7 @@ def install(self, version): """ c = self._run( 'install', '-s', str(version), - timeout=PIPENV_INSTALL_TIMEOUT, + timeout=self.project.s.PIPENV_INSTALL_TIMEOUT, ) return c @@ -216,6 +214,6 @@ def install(self, version): """ c = self._run( 'install', 'python', str(version), - timeout=PIPENV_INSTALL_TIMEOUT, + timeout=self.project.s.PIPENV_INSTALL_TIMEOUT, ) return c diff --git a/pipenv/project.py b/pipenv/project.py index 47f72ea0d2..abca0ea374 100644 --- a/pipenv/project.py +++ b/pipenv/project.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import base64 import fnmatch -import glob import hashlib import io import json @@ -11,34 +10,33 @@ import sys import urllib.parse +import pipfile +import pipfile.api import toml import tomlkit import vistir -import pipfile -import pipfile.api - -from pipenv.vendor.cached_property import cached_property - from pipenv.cmdparse import Script +from pipenv.core import system_which from pipenv.environment import Environment -from pipenv.environments import ( - PIPENV_DEFAULT_PYTHON_VERSION, PIPENV_IGNORE_VIRTUALENVS, PIPENV_MAX_DEPTH, - PIPENV_PIPFILE, PIPENV_PYTHON, PIPENV_TEST_INDEX, PIPENV_VENV_IN_PROJECT, - PIPENV_USE_SYSTEM, is_in_virtualenv, is_type_checking, is_using_venv -) -from pipenv.vendor.requirementslib.models.utils import get_default_pyproject_backend +from pipenv.environments import Setting, is_type_checking, is_in_virtualenv, normalize_pipfile_path from pipenv.utils import ( cleanup_toml, convert_toml_outline_tables, find_requirements, - get_canonical_names, get_url_name, get_workon_home, is_editable, - is_installable_file, is_star, is_valid_url, is_virtual_environment, - looks_like_dir, normalize_drive, pep423_name, proper_case, python_version, - safe_expandvars, get_pipenv_dist + find_windows_executable, get_canonical_names, get_pipenv_dist, get_url_name, + get_workon_home, is_editable, is_installable_file, is_star, is_valid_url, + is_virtual_environment, looks_like_dir, pep423_name, + proper_case, python_version, safe_expandvars ) +from pipenv.vendor.cached_property import cached_property +from pipenv.vendor.requirementslib.models.utils import ( + get_default_pyproject_backend +) + if is_type_checking(): - import pkg_resources from typing import Dict, List, Optional, Set, Text, Tuple, Union + + import pkg_resources TSource = Dict[Text, Union[Text, bool]] TPackageEntry = Dict[str, Union[bool, str, List[str]]] TPackage = Dict[str, TPackageEntry] @@ -47,24 +45,7 @@ TPipfile = Dict[str, Union[TPackage, TScripts, TPipenv, List[TSource]]] -def _normalized(p): - if p is None: - return None - loc = vistir.compat.Path(p) - try: - loc = loc.resolve() - except OSError: - loc = loc.absolute() - # Recase the path properly on Windows. From https://stackoverflow.com/a/35229734/5043728 - if os.name == 'nt': - matches = glob.glob(re.sub(r'([^:/\\])(?=[/\\]|$)', r'[\1]', str(loc))) - path_str = matches and matches[0] or str(loc) - else: - path_str = str(loc) - return normalize_drive(os.path.abspath(path_str)) - - -DEFAULT_NEWLINES = u"\n" +DEFAULT_NEWLINES = "\n" class _LockFileEncoder(json.JSONEncoder): @@ -100,47 +81,21 @@ def preferred_newlines(f): return DEFAULT_NEWLINES -if PIPENV_PIPFILE: - if not os.path.isfile(PIPENV_PIPFILE): - raise RuntimeError("Given PIPENV_PIPFILE is not found!") - - else: - PIPENV_PIPFILE = _normalized(PIPENV_PIPFILE) - # Overwrite environment variable so that subprocesses can get the correct path. - # See https://github.com/pypa/pipenv/issues/3584 - os.environ['PIPENV_PIPFILE'] = PIPENV_PIPFILE # (path, file contents) => TOMLFile # keeps track of pipfiles that we've seen so we do not need to re-parse 'em _pipfile_cache = {} -if PIPENV_TEST_INDEX: - DEFAULT_SOURCE = { - u"url": PIPENV_TEST_INDEX, - u"verify_ssl": True, - u"name": u"custom", - } -else: - DEFAULT_SOURCE = { - u"url": u"https://pypi.org/simple", - u"verify_ssl": True, - u"name": u"pypi", - } - -pipfile.api.DEFAULT_SOURCE = DEFAULT_SOURCE - - class SourceNotFound(KeyError): pass -class Project(object): +class Project: """docstring for Project""" _lockfile_encoder = _LockFileEncoder() - def __init__(self, which=None, python_version=None, chdir=True): - super(Project, self).__init__() + def __init__(self, python_version=None, chdir=True): self._name = None self._virtualenv_location = None self._download_location = None @@ -151,11 +106,25 @@ def __init__(self, which=None, python_version=None, chdir=True): self._requirements_location = None self._original_dir = os.path.abspath(os.curdir) self._environment = None - self._which = which self._build_system = { "requires": ["setuptools", "wheel"] } self.python_version = python_version + self.s = Setting() + if self.s.PIPENV_TEST_INDEX: + self.default_source = { + u"url": self.s.PIPENV_TEST_INDEX, + u"verify_ssl": True, + u"name": u"custom", + } + else: + self.default_source = { + u"url": u"https://pypi.org/simple", + u"verify_ssl": True, + u"name": u"pypi", + } + pipfile.api.DEFAULT_SOURCE = self.default_source + # Hack to skip this during pipenv run, or -r. if ("run" not in sys.argv) and chdir: try: @@ -260,7 +229,7 @@ def requirements_exists(self): def is_venv_in_project(self): # type: () -> bool - return PIPENV_VENV_IN_PROJECT or ( + return self.s.PIPENV_VENV_IN_PROJECT or ( self.project_directory and os.path.isdir(os.path.join(self.project_directory, ".venv")) ) @@ -349,15 +318,14 @@ def pipfile_package_names(self): def get_environment(self, allow_global=False): # type: (bool) -> Environment - is_venv = is_in_virtualenv() or is_using_venv() - use_system = os.getenv('PIPENV_USE_SYSTEM') == '1' - if use_system or allow_global and not is_venv: + is_venv = is_in_virtualenv() + if allow_global and not is_venv: prefix = sys.prefix python = sys.executable else: prefix = self.virtualenv_location python = None - sources = self.sources if self.sources else [DEFAULT_SOURCE] + sources = self.sources if self.sources else [self.default_source] environment = Environment( prefix=prefix, python=python, is_venv=is_venv, sources=sources, pipfile=self.parsed_pipfile, project=self @@ -373,7 +341,7 @@ def get_environment(self, allow_global=False): def environment(self): # type: () -> Environment if not self._environment: - allow_global = os.environ.get("PIPENV_USE_SYSTEM", PIPENV_USE_SYSTEM) + allow_global = self.s.PIPENV_USE_SYSTEM self._environment = self.get_environment(allow_global=allow_global) return self._environment @@ -445,11 +413,11 @@ def virtualenv_name(self): # type: () -> str sanitized, encoded_hash = self._get_virtualenv_hash(self.name) suffix = "" - if PIPENV_PYTHON: - if os.path.isabs(PIPENV_PYTHON): - suffix = "-{0}".format(os.path.basename(PIPENV_PYTHON)) + if self.s.PIPENV_PYTHON: + if os.path.isabs(self.s.PIPENV_PYTHON): + suffix = "-{0}".format(os.path.basename(self.s.PIPENV_PYTHON)) else: - suffix = "-{0}".format(PIPENV_PYTHON) + suffix = "-{0}".format(self.s.PIPENV_PYTHON) # If the pipfile was located at '/home/user/MY_PROJECT/Pipfile', # the name of its virtualenv will be 'my-project-wyUfYPqE' @@ -462,7 +430,7 @@ def virtualenv_location(self): virtualenv_env = os.getenv("VIRTUAL_ENV") if ( "PIPENV_ACTIVE" not in os.environ - and not PIPENV_IGNORE_VIRTUALENVS and virtualenv_env + and not self.s.PIPENV_IGNORE_VIRTUALENVS and virtualenv_env ): return virtualenv_env @@ -516,15 +484,15 @@ def register_proper_name(self, name): @property def pipfile_location(self): # type: () -> str - if PIPENV_PIPFILE: - return PIPENV_PIPFILE + if self.s.PIPENV_PIPFILE: + return self.s.PIPENV_PIPFILE if self._pipfile_location is None: try: - loc = pipfile.Pipfile.find(max_depth=PIPENV_MAX_DEPTH) + loc = pipfile.Pipfile.find(max_depth=self.s.PIPENV_MAX_DEPTH) except RuntimeError: loc = "Pipfile" - self._pipfile_location = _normalized(loc) + self._pipfile_location = normalize_pipfile_path(loc) return self._pipfile_location @property @@ -532,7 +500,7 @@ def requirements_location(self): # type: () -> Optional[str] if self._requirements_location is None: try: - loc = find_requirements(max_depth=PIPENV_MAX_DEPTH) + loc = find_requirements(max_depth=self.s.PIPENV_MAX_DEPTH) except RuntimeError: loc = None self._requirements_location = loc @@ -652,7 +620,8 @@ def _lockfile(self): @property def _pipfile(self): - from .vendor.requirementslib.models.pipfile import Pipfile as ReqLibPipfile + from .vendor.requirementslib.models.pipfile import \ + Pipfile as ReqLibPipfile pf = ReqLibPipfile.load(self.pipfile_location) return pf @@ -735,10 +704,11 @@ def pipfile_is_empty(self): def create_pipfile(self, python=None): """Creates the Pipfile, filled with juicy defaults.""" from .vendor.pip_shims.shims import InstallCommand + # Inherit the pip's index configuration of install command. command = InstallCommand() indexes = command.cmd_opts.get_option("--extra-index-url").default - sources = [DEFAULT_SOURCE] + sources = [self.default_source] for i, index in enumerate(indexes): if not index: continue @@ -762,7 +732,7 @@ def create_pipfile(self, python=None): required_python = self.which("python", self.virtualenv_location) else: required_python = self.which("python") - version = python_version(required_python) or PIPENV_DEFAULT_PYTHON_VERSION + version = python_version(required_python) or self.s.PIPENV_DEFAULT_PYTHON_VERSION if version and len(version.split(".")) > 2: data[u"requires"] = {"python_version": ".".join(version.split(".")[:2])} self.write_toml(data) @@ -780,7 +750,8 @@ def populate_source(cls, source): return source def get_or_create_lockfile(self, from_pipfile=False): - from pipenv.vendor.requirementslib.models.lockfile import Lockfile as Req_Lockfile + from pipenv.vendor.requirementslib.models.lockfile import \ + Lockfile as Req_Lockfile lockfile = None if from_pipfile and self.pipfile_exists: lockfile_dict = { @@ -886,7 +857,7 @@ def write_lockfile(self, content): @property def pipfile_sources(self): if self.pipfile_is_empty or "source" not in self.parsed_pipfile: - return [DEFAULT_SOURCE] + return [self.default_source] # We need to make copies of the source info so we don't # accidentally modify the cache. See #2100 where values are # written after the os.path.expandvars() call. @@ -1141,3 +1112,29 @@ def which(self, search, as_path=True): if as_path: result = str(result.path) return result + + def _which(self, command, location=None, allow_global=False): + if not allow_global and location is None: + if self.virtualenv_exists: + location = self.virtualenv_location + else: + location = os.environ.get("VIRTUAL_ENV", None) + if not (location and os.path.exists(location)) and not allow_global: + raise RuntimeError("location not created nor specified") + + version_str = "python{}".format(".".join([str(v) for v in sys.version_info[:2]])) + is_python = command in ("python", os.path.basename(sys.executable), version_str) + if not allow_global: + if os.name == "nt": + p = find_windows_executable(os.path.join(location, "Scripts"), command) + else: + p = os.path.join(location, "bin", command) + else: + if is_python: + p = sys.executable + if not os.path.exists(p): + if is_python: + p = sys.executable or system_which("python") + else: + p = system_which(command) + return p diff --git a/pipenv/resolver.py b/pipenv/resolver.py index a3e75c13cf..5097bd6f66 100644 --- a/pipenv/resolver.py +++ b/pipenv/resolver.py @@ -510,7 +510,6 @@ def validate_constraints(self): :raises: :exc:`pipenv.exceptions.DependencyConflict` if the constraints dont exist """ from pipenv.exceptions import DependencyConflict - from pipenv.environments import is_verbose constraints = self.get_constraints() pinned_version = self.updated_version @@ -520,7 +519,7 @@ def validate_constraints(self): if pinned_version and not constraint.req.specifier.contains( str(pinned_version), prereleases=True ): - if is_verbose(): + if self.project.s.is_verbose(): print(f"Tried constraint: {constraint!r}", file=sys.stderr) msg = ( "Cannot resolve conflicting version {}{} while {}{} is " @@ -688,7 +687,8 @@ def resolve(packages, pre, project, sources, clear, system, requirements_dir=Non req_dir=requirements_dir ) - from pipenv.core import project + from pipenv.project import Project + project = Project() sources = ( replace_pypi_sources(project.pipfile_sources, pypi_mirror_source) if pypi_mirror_source diff --git a/pipenv/shells.py b/pipenv/shells.py index 4ea9fbd33a..ed8cb9ad6f 100644 --- a/pipenv/shells.py +++ b/pipenv/shells.py @@ -6,7 +6,6 @@ import subprocess import sys -from pipenv.environments import PIPENV_EMULATOR, PIPENV_SHELL, PIPENV_SHELL_EXPLICIT from pipenv.vendor import shellingham from pipenv.vendor.vistir.compat import Path, get_terminal_size from pipenv.vendor.vistir.contextmanagers import temp_environ @@ -19,14 +18,14 @@ def _build_info(value): return (os.path.splitext(os.path.basename(value))[0], value) -def detect_info(): - if PIPENV_SHELL_EXPLICIT: - return _build_info(PIPENV_SHELL_EXPLICIT) +def detect_info(project): + if project.s.PIPENV_SHELL_EXPLICIT: + return _build_info(project.s.PIPENV_SHELL_EXPLICIT) try: return shellingham.detect_shell() except (shellingham.ShellDetectionFailure, TypeError): - if PIPENV_SHELL: - return _build_info(PIPENV_SHELL) + if project.s.PIPENV_SHELL: + return _build_info(project.s.PIPENV_SHELL) raise ShellDetectionFailure @@ -232,9 +231,9 @@ def _detect_emulator(): return ",".join(keys) -def choose_shell(): - emulator = PIPENV_EMULATOR.lower() or _detect_emulator() - type_, command = detect_info() +def choose_shell(project): + emulator = project.PIPENV_EMULATOR.lower() or _detect_emulator() + type_, command = project.detect_info() shell_types = SHELL_LOOKUP[type_] for key in emulator.split(","): key = key.strip().lower() diff --git a/pipenv/utils.py b/pipenv/utils.py index e5d947c576..beda5b8fc5 100644 --- a/pipenv/utils.py +++ b/pipenv/utils.py @@ -1,4 +1,3 @@ -import subprocess import contextlib import errno import logging @@ -8,33 +7,43 @@ import shutil import signal import stat +import subprocess import sys import warnings + from contextlib import contextmanager from distutils.spawn import find_executable from urllib.parse import urlparse -import toml -from click import echo as click_echo - import crayons import parse +import toml import tomlkit +from click import echo as click_echo + from pipenv import environments from pipenv._compat import DEFAULT_ENCODING -from pipenv.exceptions import PipenvCmdError, PipenvUsageError, RequirementError, ResolutionFailure +from pipenv.exceptions import ( + PipenvCmdError, PipenvUsageError, RequirementError, ResolutionFailure +) from pipenv.pep508checker import lookup from pipenv.vendor.packaging.markers import Marker from pipenv.vendor.urllib3 import util as urllib3_util -from pipenv.vendor.vistir.compat import Mapping, ResourceWarning, Sequence, Set, lru_cache +from pipenv.vendor.vistir.compat import ( + Mapping, ResourceWarning, Sequence, Set, lru_cache +) from pipenv.vendor.vistir.misc import fs_str, run + if environments.MYPY_RUNNING: - from typing import Tuple, Dict, Any, List, Union, Optional, Text - from pipenv.vendor.requirementslib.models.requirements import Requirement, Line - from pipenv.vendor.requirementslib.models.pipfile import Pipfile + from typing import Any, Dict, List, Optional, Text, Tuple, Union + from pipenv.project import Project, TSource + from pipenv.vendor.requirementslib.models.pipfile import Pipfile + from pipenv.vendor.requirementslib.models.requirements import ( + Line, Requirement + ) logging.basicConfig(level=logging.ERROR) @@ -46,7 +55,7 @@ requests_session = None # type: ignore -def _get_requests_session(): +def _get_requests_session(max_retries=1): """Load requests lazily.""" global requests_session if requests_session is not None: @@ -54,9 +63,7 @@ def _get_requests_session(): import requests requests_session = requests.Session() - adapter = requests.adapters.HTTPAdapter( - max_retries=environments.PIPENV_MAX_RETRIES - ) + adapter = requests.adapters.HTTPAdapter(max_retries=max_retries) requests_session.mount("https://pypi.org/pypi", adapter) return requests_session @@ -119,7 +126,7 @@ def convert_toml_table(section): return parsed -def run_command(cmd, *args, **kwargs): +def run_command(cmd, *args, is_verbose=False, **kwargs): """ Take an input command and run it, handling exceptions and error codes and returning its stdout and stderr. @@ -142,10 +149,10 @@ def run_command(cmd, *args, **kwargs): kwargs["env"] = os.environ.copy() kwargs["env"]["PYTHONIOENCODING"] = "UTF-8" command = [cmd.command, *cmd.args] - if environments.is_verbose(): + if is_verbose: click_echo(f"Running command: $ {cmd.cmdify()}") c = subprocess_run(command, *args, **kwargs) - if environments.is_verbose(): + if is_verbose: click_echo("Command output: {}".format( crayons.cyan(decode_for_output(c.stdout)) ), err=True) @@ -273,11 +280,9 @@ def prepare_pip_source_args(sources, pip_args=None): return pip_args -def get_project_index(index=None, trusted_hosts=None, project=None): +def get_project_index(project, index=None, trusted_hosts=None): # type: (Optional[Union[str, TSource]], Optional[List[str]], Optional[Project]) -> TSource from .project import SourceNotFound - if not project: - from .core import project if trusted_hosts is None: trusted_hosts = [] if isinstance(index, Mapping): @@ -293,23 +298,21 @@ def get_project_index(index=None, trusted_hosts=None, project=None): def get_source_list( + project, # type: Project index=None, # type: Optional[Union[str, TSource]] extra_indexes=None, # type: Optional[List[str]] trusted_hosts=None, # type: Optional[List[str]] pypi_mirror=None, # type: Optional[str] - project=None, # type: Optional[Project] ): # type: (...) -> List[TSource] sources = [] # type: List[TSource] - if not project: - from .core import project if index: - sources.append(get_project_index(index)) + sources.append(get_project_index(project, index)) if extra_indexes: if isinstance(extra_indexes, str): extra_indexes = [extra_indexes] for source in extra_indexes: - extra_src = get_project_index(source) + extra_src = get_project_index(project, source) if not sources or extra_src["url"] != sources[0]["url"]: sources.append(extra_src) else: @@ -326,10 +329,8 @@ def get_source_list( return sources -def get_indexes_from_requirement(req, project=None, index=None, extra_indexes=None, trusted_hosts=None, pypi_mirror=None): - # type: (Requirement, Optional[Project], Optional[Text], Optional[List[Text]], Optional[List[Text]], Optional[Text]) -> Tuple[TSource, List[TSource], List[Text]] - if not project: - from .core import project +def get_indexes_from_requirement(req, project, index=None, extra_indexes=None, trusted_hosts=None, pypi_mirror=None): + # type: (Requirement, Project, Optional[Text], Optional[List[Text]], Optional[List[Text]], Optional[Text]) -> Tuple[TSource, List[TSource], List[Text]] index_sources = [] # type: List[TSource] if not trusted_hosts: trusted_hosts = [] # type: List[Text] @@ -347,7 +348,7 @@ def get_indexes_from_requirement(req, project=None, index=None, extra_indexes=No indexes.extend(project_indexes) if len(indexes) > 1: index, extra_indexes = indexes[0], indexes[1:] - index_sources = get_source_list(index=index, extra_indexes=extra_indexes, trusted_hosts=trusted_hosts, pypi_mirror=pypi_mirror, project=project) + index_sources = get_source_list(project, index=index, extra_indexes=extra_indexes, trusted_hosts=trusted_hosts, pypi_mirror=pypi_mirror) if len(index_sources) > 1: index_source, extra_index_sources = index_sources[0], index_sources[1:] else: @@ -373,9 +374,9 @@ def __init__( markers_lookup=None, skipped=None, clear=False, pre=False ): from pipenv.patched.piptools import logging as piptools_logging - if environments.is_verbose(): + if project.s.is_verbose(): logging.log.verbose = True - piptools_logging.log.verbosity = environments.PIPENV_VERBOSITY + piptools_logging.log.verbosity = project.s.PIPENV_VERBOSITY self.initial_constraints = constraints self.req_dir = req_dir self.project = project @@ -452,7 +453,7 @@ def get_metadata( # rest of the dependencies, while adding the files/vcs deps/paths themselves # to the lockfile directly constraint_update, lockfile_update = cls.get_deps_from_req( - req, resolver=transient_resolver + req, resolver=transient_resolver, resolve_vcs=project.s.PIPENV_RESOLVE_VCS ) constraints |= constraint_update skipped.update(lockfile_update) @@ -518,12 +519,15 @@ def get_deps_from_line(cls, line): return cls.get_deps_from_req(req) @classmethod - def get_deps_from_req(cls, req, resolver=None): - # type: (Requirement, Optional["Resolver"]) -> Tuple[Set[str], Dict[str, Dict[str, Union[str, bool, List[str]]]]] + def get_deps_from_req(cls, req, resolver=None, resolve_vcs=True): + # type: (Requirement, Optional["Resolver"], bool) -> Tuple[Set[str], Dict[str, Dict[str, Union[str, bool, List[str]]]]] from .patched.piptools.exceptions import NoCandidateFound - from .vendor.requirementslib.models.utils import _requirement_to_str_lowercase_name from .vendor.requirementslib.models.requirements import Requirement + from .vendor.requirementslib.models.utils import ( + _requirement_to_str_lowercase_name + ) from .vendor.requirementslib.utils import is_installable_dir + # TODO: this is way too complex, refactor this constraints = set() # type: Set[str] locked_deps = dict() # type: Dict[str, Dict[str, Union[str, bool, List[str]]]] @@ -547,7 +551,7 @@ def get_deps_from_req(cls, req, resolver=None): requirements = [] # Allow users to toggle resolution off for non-editable VCS packages # but leave it on for local, installable folders on the filesystem - if environments.PIPENV_RESOLVE_VCS or ( + if resolve_vcs or ( req.editable or parsed_line.is_wheel or ( req.is_file_or_url and parsed_line.is_local and is_installable_dir(parsed_line.path) @@ -630,9 +634,9 @@ def get_deps_from_req(cls, req, resolver=None): def create( cls, deps, # type: List[str] + project, # type: Project index_lookup=None, # type: Dict[str, str] markers_lookup=None, # type: Dict[str, str] - project=None, # type: Project sources=None, # type: List[str] req_dir=None, # type: str clear=False, # type: bool @@ -646,9 +650,6 @@ def create( index_lookup = {} if markers_lookup is None: markers_lookup = {} - if project is None: - from pipenv.core import project - project = project if sources is None: sources = project.sources constraints, skipped, index_lookup, markers_lookup = cls.get_metadata( @@ -661,11 +662,9 @@ def create( ) @classmethod - def from_pipfile(cls, project=None, pipfile=None, dev=False, pre=False, clear=False): + def from_pipfile(cls, project, pipfile=None, dev=False, pre=False, clear=False): # type: (Optional[Project], Optional[Pipfile], bool, bool, bool) -> "Resolver" from pipenv.vendor.vistir.path import create_tracked_tempdir - if not project: - from pipenv.core import project if not pipfile: pipfile = project._pipfile req_dir = create_tracked_tempdir(suffix="-requirements", prefix="pipenv-") @@ -699,7 +698,7 @@ def prepare_pip_args(self, use_pep517=False, build_isolation=True): pip_args.append("--no-build-isolation") if self.pre: pip_args.append("--pre") - pip_args.extend(["--cache-dir", environments.PIPENV_CACHE_DIR]) + pip_args.extend(["--cache-dir", self.project.s.PIPENV_CACHE_DIR]) return pip_args @property @@ -745,7 +744,7 @@ def constraint_file(self): def pip_options(self): if self._pip_options is None: pip_options, _ = self.pip_command.parser.parse_args(self.pip_args) - pip_options.cache_dir = environments.PIPENV_CACHE_DIR + pip_options.cache_dir = self.project.s.PIPENV_CACHE_DIR pip_options.no_python_version_warning = True pip_options.no_input = True pip_options.progress_bar = "off" @@ -757,10 +756,6 @@ def pip_options(self): def session(self): if self._session is None: self._session = self.pip_command._build_session(self.pip_options) - # if environments.is_verbose(): - # click_echo( - # crayons.cyan("Using pip: {0}".format(" ".join(self.pip_args))), err=True - # ) return self._session @property @@ -790,11 +785,12 @@ def parsed_constraints(self): return self._parsed_constraints def get_resolver(self, clear=False, pre=False): - from pipenv.patched.piptools.resolver import Resolver as PiptoolsResolver from pipenv.patched.piptools.cache import DependencyCache + from pipenv.patched.piptools.resolver import \ + Resolver as PiptoolsResolver self._resolver = PiptoolsResolver( constraints=self.parsed_constraints, repository=self.repository, - cache=DependencyCache(environments.PIPENV_CACHE_DIR), clear_caches=clear, + cache=DependencyCache(self.project.s.PIPENV_CACHE_DIR), clear_caches=clear, # TODO: allow users to toggle the 'allow unsafe' flag to resolve setuptools? prereleases=pre, allow_unsafe=False ) @@ -806,15 +802,16 @@ def resolver(self): return self._resolver def resolve(self): + from pipenv.patched.piptools.cache import CorruptCacheError + from pipenv.patched.piptools.exceptions import NoCandidateFound from pipenv.vendor.pip_shims.shims import DistributionNotFound from pipenv.vendor.requests.exceptions import HTTPError - from pipenv.patched.piptools.exceptions import NoCandidateFound - from pipenv.patched.piptools.cache import CorruptCacheError + from .exceptions import CacheError, ResolutionFailure with temp_environ(): os.environ["PIP_NO_USE_PEP517"] = "" try: - results = self.resolver.resolve(max_rounds=environments.PIPENV_MAX_ROUNDS) + results = self.resolver.resolve(max_rounds=self.project.s.PIPENV_MAX_ROUNDS) except CorruptCacheError as e: if environments.PIPENV_IS_CI or self.clear: if self._retry_attempts < 3: @@ -888,7 +885,7 @@ def collect_hashes(self, ireq): for source in self.sources ): pkg_url = f"https://pypi.org/pypi/{ireq.name}/json" - session = _get_requests_session() + session = _get_requests_session(self.project.s.PIPENV_MAX_RETRIES) try: # Grab the hashes from the new warehouse API. r = session.get(pkg_url, timeout=10) @@ -906,7 +903,7 @@ def collect_hashes(self, ireq): collected_hashes.append(release["digests"]["sha256"]) collected_hashes = self.prepend_hash_types(collected_hashes) except (ValueError, KeyError, ConnectionError): - if environments.is_verbose(): + if self.project.s.is_verbose(): click_echo( "{}: Error generating hash for {}".format( crayons.red("Warning", bold=True), ireq.name @@ -1000,7 +997,9 @@ def _clean_skipped_result(self, req, value): return req.name, entry def clean_results(self): - from pipenv.vendor.requirementslib.models.requirements import Requirement + from pipenv.vendor.requirementslib.models.requirements import ( + Requirement + ) reqs = [(Requirement.from_ireq(ireq), ireq) for ireq in self.resolved_tree] results = {} for req, ireq in reqs: @@ -1097,7 +1096,7 @@ def actually_resolve_deps( with warnings.catch_warnings(record=True) as warning_list: resolver = Resolver.create( - deps, index_lookup, markers_lookup, project, sources, req_dir, clear, pre + deps, project, index_lookup, markers_lookup, sources, req_dir, clear, pre ) resolver.resolve() hashes = resolver.resolve_hashes() @@ -1110,13 +1109,13 @@ def actually_resolve_deps( @contextlib.contextmanager -def create_spinner(text, nospin=None, spinner_name=None): +def create_spinner(text, setting, nospin=None, spinner_name=None): from .vendor.vistir import spin from .vendor.vistir.misc import fs_str if not spinner_name: - spinner_name = environments.PIPENV_SPINNER + spinner_name = setting.PIPENV_SPINNER if nospin is None: - nospin = environments.PIPENV_NOSPIN + nospin = setting.PIPENV_NOSPIN with spin.create_spinner( spinner_name=spinner_name, start_text=fs_str(text), @@ -1125,16 +1124,17 @@ def create_spinner(text, nospin=None, spinner_name=None): yield sp -def resolve(cmd, sp): +def resolve(cmd, sp, project): + from ._compat import decode_output from .cmdparse import Script from .vendor.vistir.misc import echo - from ._compat import decode_output c = subprocess_run(Script.parse(cmd).cmd_args, block=False, env=os.environ.copy()) + is_verbose = project.s.is_verbose() err = "" for line in iter(c.stderr.readline, ""): line = decode_output(line) err += line - if environments.is_verbose() and line.rstrip(): + if is_verbose and line.rstrip(): sp.hide_and_write(line.rstrip()) c.wait() @@ -1145,10 +1145,10 @@ def resolve(cmd, sp): "Locking Failed!" )) echo(out.strip(), err=True) - if not environments.is_verbose(): + if not is_verbose: echo(err, err=True) sys.exit(returncode) - if environments.is_verbose(): + if is_verbose: echo(out.strip(), err=True) return subprocess.CompletedProcess(c.args, returncode, out, err) @@ -1246,12 +1246,13 @@ def venv_resolve_deps( :rtype: None """ - from .vendor.vistir.misc import fs_str - from .vendor.vistir.compat import Path, JSONDecodeError, NamedTemporaryFile - from .vendor.vistir.path import create_tracked_tempdir + import json + from . import resolver from ._compat import decode_for_output - import json + from .vendor.vistir.compat import JSONDecodeError, NamedTemporaryFile, Path + from .vendor.vistir.misc import fs_str + from .vendor.vistir.path import create_tracked_tempdir results = [] pipfile_section = "dev-packages" if dev else "packages" @@ -1287,7 +1288,7 @@ def venv_resolve_deps( os.environ.update({fs_str(k): fs_str(val) for k, val in os.environ.items()}) if pypi_mirror: os.environ["PIPENV_PYPI_MIRROR"] = str(pypi_mirror) - os.environ["PIPENV_VERBOSITY"] = str(environments.PIPENV_VERBOSITY) + os.environ["PIPENV_VERBOSITY"] = str(project.s.PIPENV_VERBOSITY) os.environ["PIPENV_REQ_DIR"] = fs_str(req_dir) os.environ["PIP_NO_INPUT"] = fs_str("1") pipenv_site_dir = get_pipenv_sitedir() @@ -1297,7 +1298,7 @@ def venv_resolve_deps( os.environ.pop("PIPENV_SITE_DIR", None) if keep_outdated: os.environ["PIPENV_KEEP_OUTDATED"] = fs_str("1") - with create_spinner(text=decode_for_output("Locking...")) as sp: + with create_spinner(text=decode_for_output("Locking..."), setting=project.s) as sp: # This conversion is somewhat slow on local and file-type requirements since # we now download those requirements / make temporary folders to perform # dependency resolution on them, so we are including this step inside the @@ -1309,11 +1310,11 @@ def venv_resolve_deps( constraints = set(deps) os.environ["PIPENV_PACKAGES"] = str("\n".join(constraints)) sp.write(decode_for_output("Resolving dependencies...")) - c = resolve(cmd, sp) + c = resolve(cmd, sp, project=project) results = c.stdout.strip() if c.returncode == 0: sp.green.ok(environments.PIPENV_SPINNER_OK_TEXT.format("Success!")) - if not environments.is_verbose() and c.stderr.strip(): + if not project.s.is_verbose() and c.stderr.strip(): click_echo(crayons.yellow(f"Warning: {c.stderr.strip()}"), err=True) else: sp.red.fail(environments.PIPENV_SPINNER_FAIL_TEXT.format("Locking Failed!")) @@ -1494,9 +1495,9 @@ def is_editable(pipfile_entry): def is_installable_file(path): """Determine if a path can potentially be installed""" - from .vendor.pip_shims.shims import is_installable_dir, is_archive_file - from .patched.notpip._internal.utils.packaging import specifiers from ._compat import Path + from .patched.notpip._internal.utils.packaging import specifiers + from .vendor.pip_shims.shims import is_archive_file, is_installable_dir if hasattr(path, "keys") and any( key for key in path.keys() if key in ["file", "path"] @@ -1699,8 +1700,9 @@ def temp_path(): def load_path(python): - from pathlib import Path import json + + from pathlib import Path python = Path(python).as_posix() json_dump_commmand = '"import json, sys; print(json.dumps(sys.path));"' c = subprocess_run([python, "-c", json_dump_commmand]) @@ -1734,9 +1736,9 @@ def create_mirror_source(url): } -def download_file(url, filename): +def download_file(url, filename, max_retries=1): """Downloads file from url to a path with filename""" - r = _get_requests_session().get(url, stream=True) + r = _get_requests_session(max_retries).get(url, stream=True) if not r.ok: raise OSError("Unable to download file") @@ -1861,7 +1863,10 @@ def get_vcs_deps( if requirement.is_vcs: try: with temp_path(), locked_repository(requirement) as repo: - from pipenv.vendor.requirementslib.models.requirements import Requirement + from pipenv.vendor.requirementslib.models.requirements import ( + Requirement + ) + # from distutils.sysconfig import get_python_lib # sys.path = [repo.checkout_directory, "", ".", get_python_lib(plat_specific=0)] commit_hash = repo.get_commit_hash() diff --git a/tests/conftest.py b/tests/conftest.py index e69de29bb2..fc75b6b572 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -0,0 +1,7 @@ +import pytest + + +@pytest.fixture() +def project(): + from pipenv.project import Project + return Project() diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 3569aff385..3cbd6574ad 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,9 +1,11 @@ import errno +import functools import json import logging import os -from pipenv.utils import subprocess_run +import shlex import shutil +import traceback import sys import warnings @@ -12,8 +14,14 @@ import pytest import requests +from click.testing import CliRunner +from pytest_pypi.app import prepare_fixtures +from pytest_pypi.app import prepare_packages as prepare_pypi_packages + from pipenv._compat import Path +from pipenv.cli import cli from pipenv.exceptions import VirtualenvActivationException +from pipenv.utils import subprocess_run from pipenv.vendor import toml, tomlkit from pipenv.vendor.vistir.compat import ( FileNotFoundError, PermissionError, ResourceWarning, TemporaryDirectory, @@ -24,12 +32,11 @@ from pipenv.vendor.vistir.path import ( create_tracked_tempdir, handle_remove_readonly, mkdir_p ) -from pytest_pypi.app import prepare_fixtures -from pytest_pypi.app import prepare_packages as prepare_pypi_packages log = logging.getLogger(__name__) warnings.simplefilter("default", category=ResourceWarning) +cli_runner = CliRunner(mix_stderr=False) HAS_WARNED_GITHUB = False @@ -142,10 +149,7 @@ def pathlib_tmpdir(request, tmpdir): def _create_tracked_dir(): - tmp_location = os.environ.get("TEMP", os.environ.get("TMP")) temp_args = {"prefix": "pipenv-", "suffix": "-test"} - if tmp_location is not None: - temp_args["dir"] = tmp_location temp_path = create_tracked_tempdir(**temp_args) return temp_path @@ -298,27 +302,29 @@ def get_url(cls, pkg=None, filename=None): class _PipenvInstance: """An instance of a Pipenv Project...""" def __init__( - self, pypi=None, pipfile=True, chdir=False, path=None, home_dir=None, + self, pypi=None, pipfile=True, chdir=True, path=None, capfd=None, venv_root=None, ignore_virtualenvs=True, venv_in_project=True, name=None ): self.index_url = os.getenv("PIPENV_TEST_INDEX") self.pypi = None + self.env = {} + self.capfd = capfd if pypi: self.pypi = pypi.url elif self.index_url is not None: self.pypi, _, _ = self.index_url.rpartition("/") if self.index_url else "" self.index = os.getenv("PIPENV_PYPI_INDEX") - os.environ["PYTHONWARNINGS"] = "ignore:DEPRECATION" + self.env["PYTHONWARNINGS"] = "ignore:DEPRECATION" if ignore_virtualenvs: - os.environ["PIPENV_IGNORE_VIRTUALENVS"] = fs_str("1") + self.env["PIPENV_IGNORE_VIRTUALENVS"] = fs_str("1") if venv_root: - os.environ["VIRTUAL_ENV"] = venv_root + self.env["VIRTUAL_ENV"] = venv_root if venv_in_project: - os.environ["PIPENV_VENV_IN_PROJECT"] = fs_str("1") + self.env["PIPENV_VENV_IN_PROJECT"] = fs_str("1") else: - os.environ.pop("PIPENV_VENV_IN_PROJECT", None) + self.env.pop("PIPENV_VENV_IN_PROJECT", None) - self.original_dir = os.path.abspath(os.curdir) + self.original_dir = Path(__file__).parent.parent.parent path = path if path else os.environ.get("PIPENV_PROJECT_DIR", None) if name is not None: path = Path(os.environ["HOME"]) / "projects" / name @@ -346,7 +352,7 @@ def __init__( self.chdir = chdir if self.pypi and "PIPENV_PYPI_URL" not in os.environ: - os.environ['PIPENV_PYPI_URL'] = fs_str(f'{self.pypi}') + self.env['PIPENV_PYPI_URL'] = fs_str(f'{self.pypi}') # os.environ['PIPENV_PYPI_URL'] = fs_str('{0}'.format(self.pypi.url)) # os.environ['PIPENV_TEST_INDEX'] = fs_str('{0}/simple'.format(self.pypi.url)) @@ -382,27 +388,28 @@ def pipenv(self, cmd, block=True): # a bit of a hack to make sure the virtualenv is created with TemporaryDirectory(prefix='pipenv-', suffix='-cache') as tempdir: - os.environ['PIPENV_CACHE_DIR'] = fs_str(tempdir.name) - c = subprocess_run( - f'pipenv {cmd}', block=block, shell=True, - cwd=os.path.abspath(self.path), env=os.environ.copy() - ) - if 'PIPENV_CACHE_DIR' in os.environ: - del os.environ['PIPENV_CACHE_DIR'] - - if 'PIPENV_PIPFILE' in os.environ: - del os.environ['PIPENV_PIPFILE'] - + cmd_args = shlex.split(cmd) + env = {**self.env, **{'PIPENV_CACHE_DIR': tempdir.name}} + self.capfd.readouterr() + r = cli_runner.invoke(cli, cmd_args, env=env) + r.returncode = r.exit_code # Pretty output for failing tests. + out, err = self.capfd.readouterr() + if out: + r.stdout_bytes = r.stdout_bytes + out + if err: + r.stderr_bytes = r.stderr_bytes + err if block: print(f'$ pipenv {cmd}') - print(c.stdout) - print(c.stderr, file=sys.stderr) - if c.returncode != 0: + print(r.stdout) + print(r.stderr, file=sys.stderr) + if r.exception: + print(''.join(traceback.format_exception(*r.exc_info)), file=sys.stderr) + if r.returncode != 0: print("Command failed...") # Where the action happens. - return c + return r @property def pipfile(self): @@ -447,7 +454,7 @@ def finalize(): @pytest.fixture() -def PipenvInstance(pip_src_dir, monkeypatch, pypi): +def PipenvInstance(pip_src_dir, monkeypatch, pypi, capfdbinary): with temp_environ(), monkeypatch.context() as m: m.setattr(shutil, "rmtree", _rmtree_func) original_umask = os.umask(0o007) @@ -461,13 +468,13 @@ def PipenvInstance(pip_src_dir, monkeypatch, pypi): warnings.simplefilter("ignore", category=ResourceWarning) warnings.filterwarnings("ignore", category=ResourceWarning, message="unclosed.*") try: - yield _PipenvInstance + yield functools.partial(_PipenvInstance, capfd=capfdbinary) finally: os.umask(original_umask) @pytest.fixture() -def PipenvInstance_NoPyPI(monkeypatch, pip_src_dir, pypi): +def PipenvInstance_NoPyPI(monkeypatch, pip_src_dir, pypi, capfdbinary): with temp_environ(), monkeypatch.context() as m: m.setattr(shutil, "rmtree", _rmtree_func) original_umask = os.umask(0o007) @@ -479,7 +486,7 @@ def PipenvInstance_NoPyPI(monkeypatch, pip_src_dir, pypi): warnings.simplefilter("ignore", category=ResourceWarning) warnings.filterwarnings("ignore", category=ResourceWarning, message="unclosed.*") try: - yield _PipenvInstance + yield functools.partial(_PipenvInstance, capfd=capfdbinary) finally: os.umask(original_umask) @@ -495,7 +502,7 @@ def __init__(self, name="venv", base_dir=None): base_dir = Path(_create_tracked_dir()) self.base_dir = base_dir self.name = name - self.path = base_dir / name + self.path = (base_dir / name).resolve() def __enter__(self): self._old_environ = os.environ.copy() @@ -526,17 +533,14 @@ def activate(self): code = compile(f.read(), str(activate_this), "exec") exec(code, dict(__file__=str(activate_this))) os.environ["VIRTUAL_ENV"] = str(self.path) - try: - return self.path.absolute().resolve() - except OSError: - return self.path.absolute() + return self.path else: raise VirtualenvActivationException("Can't find the activate_this.py script.") @pytest.fixture() def virtualenv(vistir_tmpdir): - with temp_environ(), VirtualEnv(base_dir=vistir_tmpdir) as venv: + with VirtualEnv(base_dir=vistir_tmpdir) as venv: yield venv diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py index 5c90c70faa..d3b3ed1287 100644 --- a/tests/integration/test_cli.py +++ b/tests/integration/test_cli.py @@ -8,7 +8,7 @@ from flaky import flaky -from pipenv.utils import normalize_drive +from pipenv.utils import normalize_drive, subprocess_run @pytest.mark.cli @@ -217,8 +217,8 @@ def test_help(PipenvInstance): @pytest.mark.cli def test_man(PipenvInstance): - with PipenvInstance() as p: - c = p.pipenv('--man') + with PipenvInstance(): + c = subprocess_run(["pipenv", "--man"]) assert c.returncode == 0, c.stderr diff --git a/tests/integration/test_install_markers.py b/tests/integration/test_install_markers.py index f212bc1b9b..96afc37be3 100644 --- a/tests/integration/test_install_markers.py +++ b/tests/integration/test_install_markers.py @@ -1,5 +1,4 @@ import os -import sys import pytest diff --git a/tests/integration/test_lock.py b/tests/integration/test_lock.py index 45686f67e5..84a423fb2d 100644 --- a/tests/integration/test_lock.py +++ b/tests/integration/test_lock.py @@ -646,16 +646,11 @@ def test_lock_with_incomplete_source(PipenvInstance): @pytest.mark.lock @pytest.mark.install -def test_lock_no_warnings(PipenvInstance): +def test_lock_no_warnings(PipenvInstance, recwarn): with PipenvInstance(chdir=True) as p: - os.environ["PYTHONWARNINGS"] = "once" c = p.pipenv("install six") assert c.returncode == 0 - c = p.pipenv('run python -c "import warnings; warnings.warn(\\"This is a warning\\", DeprecationWarning); print(\\"hello\\")"') - assert c.returncode == 0 - assert "Warning" in c.stderr - assert "Warning" not in c.stdout - assert "hello" in c.stdout + assert len(recwarn) == 0 @pytest.mark.lock diff --git a/tests/integration/test_project.py b/tests/integration/test_project.py index 4a4b12ebf3..1ad60a2a13 100644 --- a/tests/integration/test_project.py +++ b/tests/integration/test_project.py @@ -1,11 +1,13 @@ import os import tarfile +from pathlib import Path + import pytest from pipenv.patched import pipfile from pipenv.project import Project -from pipenv.utils import subprocess_run, temp_environ +from pipenv.utils import temp_environ from pipenv.vendor.vistir.path import is_in_path, normalize_path @@ -170,38 +172,26 @@ def test_include_editable_packages(PipenvInstance, testsroot, pathlib_tmpdir): @pytest.mark.virtualenv def test_run_in_virtualenv_with_global_context(PipenvInstance, virtualenv): with PipenvInstance(chdir=True, venv_root=virtualenv.as_posix(), ignore_virtualenvs=False, venv_in_project=False) as p: - c = subprocess_run( - ["pipenv", "run", "pip", "freeze"], cwd=os.path.abspath(p.path), - env=os.environ.copy() - ) + c = p.pipenv("run pip freeze") assert c.returncode == 0, (c.stdout, c.stderr) assert 'Creating a virtualenv' not in c.stderr, c.stderr project = Project() - assert project.virtualenv_location == virtualenv.as_posix(), ( - project.virtualenv_location, virtualenv.as_posix() - ) - c = subprocess_run( - ["pipenv", "run", "pip", "install", "-i", p.index_url, "click"], - cwd=os.path.abspath(p.path), - env=os.environ.copy() + assert Path(project.virtualenv_location).resolve() == Path(virtualenv), ( + project.virtualenv_location, str(virtualenv) ) + + c = p.pipenv(f"run pip install -i {p.index_url} click") assert c.returncode == 0, (c.stdout, c.stderr) assert "Courtesy Notice" in c.stderr, (c.stdout, c.stderr) - c = subprocess_run( - ["pipenv", "install", "-i", p.index_url, "six"], - cwd=os.path.abspath(p.path), env=os.environ.copy() - ) + + c = p.pipenv("install six") assert c.returncode == 0, (c.stdout, c.stderr) - c = subprocess_run( - ['pipenv', 'run', 'python', '-c', 'import click;print(click.__file__)'], - cwd=os.path.abspath(p.path), env=os.environ.copy() - ) + + c = p.pipenv("run python -c 'import click;print(click.__file__)'") assert c.returncode == 0, (c.stdout, c.stderr) assert is_in_path(c.stdout.strip(), str(virtualenv)), (c.stdout.strip(), str(virtualenv)) - c = subprocess_run( - ["pipenv", "clean", "--dry-run"], cwd=os.path.abspath(p.path), - env=os.environ.copy() - ) + + c = p.pipenv("clean --dry-run") assert c.returncode == 0, (c.stdout, c.stderr) assert "click" in c.stdout, c.stdout @@ -227,6 +217,7 @@ def test_run_in_virtualenv(PipenvInstance): assert c.returncode == 0 assert "click" in c.stdout + @pytest.mark.project @pytest.mark.sources def test_no_sources_in_pipfile(PipenvInstance): @@ -235,7 +226,7 @@ def test_no_sources_in_pipfile(PipenvInstance): contents = """ [packages] pytest = "*" - """.format(os.environ['PIPENV_TEST_INDEX']).strip() + """.strip() f.write(contents) c = p.pipenv('install --skip-lock') assert c.returncode == 0 diff --git a/tests/integration/test_run.py b/tests/integration/test_run.py index 22b29788b1..3059f88c7f 100644 --- a/tests/integration/test_run.py +++ b/tests/integration/test_run.py @@ -3,17 +3,16 @@ import pytest from pipenv.project import Project -from pipenv.utils import temp_environ +from pipenv.utils import subprocess_run, temp_environ @pytest.mark.run @pytest.mark.dotenv def test_env(PipenvInstance): with PipenvInstance(pipfile=False, chdir=True) as p: - with open('.env', 'w') as f: - f.write('HELLO=WORLD') - - c = p.pipenv('run python -c "import os; print(os.environ[\'HELLO\'])"') + with open(os.path.join(p.path, ".env"), "w") as f: + f.write("HELLO=WORLD") + c = subprocess_run(['pipenv', 'run', 'python', '-c', "import os; print(os.environ['HELLO'])"], env=p.env) assert c.returncode == 0 assert 'WORLD' in c.stdout @@ -35,18 +34,16 @@ def test_scripts(PipenvInstance): f.write('scriptwithenv = "echo $HELLO"\n') c = p.pipenv('install') assert c.returncode == 0 - c = p.pipenv('run printfoo') assert c.returncode == 0 - assert c.stdout == 'foo\n' - assert c.stderr == '' + assert c.stdout.splitlines()[0] == 'foo' + assert not c.stderr.strip() c = p.pipenv('run notfoundscript') - assert c.returncode == 1 + assert c.returncode != 0 assert c.stdout == '' if os.name != 'nt': # TODO: Implement this message for Windows. - assert 'Error' in c.stderr - assert 'randomthingtotally (from notfoundscript)' in c.stderr + assert 'not found' in c.stderr project = Project() diff --git a/tests/unit/test_core.py b/tests/unit/test_core.py index d7981a4786..f20e00718e 100644 --- a/tests/unit/test_core.py +++ b/tests/unit/test_core.py @@ -1,6 +1,5 @@ import os -from unittest import mock import pytest from pipenv._compat import TemporaryDirectory @@ -8,19 +7,19 @@ from pipenv.utils import temp_environ -@mock.patch('pipenv.environments.PIPENV_VIRTUALENV', 'totallyrealenv') -@mock.patch('pipenv.environments.PIPENV_VERBOSITY', -1) @pytest.mark.core -def test_suppress_nested_venv_warning(capsys): +def test_suppress_nested_venv_warning(capsys, project): # Capture the stderr of warn_in_virtualenv to test for the presence of the # courtesy notice. - warn_in_virtualenv() + project.s.PIPENV_VIRTUALENV = 'totallyrealenv' + project.s.PIPENV_VERBOSITY = -1 + warn_in_virtualenv(project) output, err = capsys.readouterr() assert 'Courtesy Notice' not in err @pytest.mark.core -def test_load_dot_env_from_environment_variable_location(monkeypatch, capsys): +def test_load_dot_env_from_environment_variable_location(monkeypatch, capsys, project): with temp_environ(), monkeypatch.context() as m, TemporaryDirectory(prefix='pipenv-', suffix='') as tempdir: if os.name == "nt": import click @@ -31,14 +30,13 @@ def test_load_dot_env_from_environment_variable_location(monkeypatch, capsys): with open(dotenv_path, 'w') as f: f.write(f'{key}={val}') - m.setenv("PIPENV_DOTENV_LOCATION", str(dotenv_path)) - m.setattr("pipenv.environments.PIPENV_DOTENV_LOCATION", str(dotenv_path)) - load_dot_env() + project.s.PIPENV_DOTENV_LOCATION = str(dotenv_path) + load_dot_env(project) assert os.environ[key] == val @pytest.mark.core -def test_doesnt_load_dot_env_if_disabled(monkeypatch, capsys): +def test_doesnt_load_dot_env_if_disabled(monkeypatch, capsys, project): with temp_environ(), monkeypatch.context() as m, TemporaryDirectory(prefix='pipenv-', suffix='') as tempdir: if os.name == "nt": import click @@ -49,26 +47,24 @@ def test_doesnt_load_dot_env_if_disabled(monkeypatch, capsys): with open(dotenv_path, 'w') as f: f.write(f'{key}={val}') - m.setenv("PIPENV_DOTENV_LOCATION", str(dotenv_path)) - m.setattr("pipenv.environments.PIPENV_DOTENV_LOCATION", str(dotenv_path)) - m.setattr("pipenv.environments.PIPENV_DONT_LOAD_ENV", True) - load_dot_env() + project.s.PIPENV_DOTENV_LOCATION = str(dotenv_path) + project.s.PIPENV_DONT_LOAD_ENV = True + load_dot_env(project) assert key not in os.environ - m.setattr("pipenv.environments.PIPENV_DONT_LOAD_ENV", False) - load_dot_env() + project.s.PIPENV_DONT_LOAD_ENV = False + load_dot_env(project) assert key in os.environ @pytest.mark.core -def test_load_dot_env_warns_if_file_doesnt_exist(monkeypatch, capsys): +def test_load_dot_env_warns_if_file_doesnt_exist(monkeypatch, capsys, project): with temp_environ(), monkeypatch.context() as m, TemporaryDirectory(prefix='pipenv-', suffix='') as tempdir: if os.name == "nt": import click is_console = False m.setattr(click._winconsole, "_is_console", lambda x: is_console) dotenv_path = os.path.join(tempdir.name, 'does-not-exist.env') - m.setenv("PIPENV_DOTENV_LOCATION", str(dotenv_path)) - m.setattr("pipenv.environments.PIPENV_DOTENV_LOCATION", str(dotenv_path)) - load_dot_env() + project.s.PIPENV_DOTENV_LOCATION = str(dotenv_path) + load_dot_env(project) output, err = capsys.readouterr() assert 'Warning' in err diff --git a/tests/unit/test_help.py b/tests/unit/test_help.py index 5179e4d3d1..812925f759 100644 --- a/tests/unit/test_help.py +++ b/tests/unit/test_help.py @@ -13,18 +13,3 @@ def test_help(): stderr=subprocess.STDOUT, env=os.environ.copy(), ) assert output - - -@pytest.mark.cli -@pytest.mark.help -def test_count_of_description_pre_option(): - test_command = 'pipenv install --help' - test_line = '--pre Allow pre-releases.' - out = subprocess.Popen(test_command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - stdout, _ = out.communicate() - lines = stdout.decode().split('\n') - count = 0 - for line in lines: - if line.strip().split() == test_line.split(): - count += 1 - assert count == 1