diff --git a/news/3577.feature.rst b/news/3577.feature.rst new file mode 100644 index 0000000000..7944c09878 --- /dev/null +++ b/news/3577.feature.rst @@ -0,0 +1 @@ +Added a new environment variable, ``PIPENV_RESOLVE_VCS``, to toggle dependency resolution off for non-editable VCS, file, and URL based dependencies. diff --git a/news/3647.bugfix.rst b/news/3647.bugfix.rst new file mode 100644 index 0000000000..cb64edc1bc --- /dev/null +++ b/news/3647.bugfix.rst @@ -0,0 +1 @@ +Pipenv will no longer inadvertently set ``editable=True`` on all vcs dependencies. diff --git a/news/3656.bugfix.rst b/news/3656.bugfix.rst new file mode 100644 index 0000000000..58df202019 --- /dev/null +++ b/news/3656.bugfix.rst @@ -0,0 +1,2 @@ +The ``--keep-outdated`` argument to ``pipenv install`` and ``pipenv lock`` will now drop specifier constraints when encountering editable dependencies. +- In addition, ``--keep-outdated`` will retain specifiers that would otherwise be dropped from any entries that have not been updated. diff --git a/pipenv/core.py b/pipenv/core.py index 7b00eb5916..9b4fa5a446 100644 --- a/pipenv/core.py +++ b/pipenv/core.py @@ -1394,6 +1394,8 @@ def pip_install( src_dir = os.environ["PIP_SRC"] src = ["--src", os.environ["PIP_SRC"]] if not requirement.editable: + # Leave this off becauase old lockfiles don't have all deps included + # TODO: When can it be turned back on? no_deps = False if src_dir is not None: @@ -1412,7 +1414,7 @@ def pip_install( prefix="pipenv-", suffix="-requirement.txt", dir=requirements_dir, delete=False ) - line = "-e" if requirement.editable else "" + line = "-e " if requirement.editable else "" if requirement.editable or requirement.name is not None: name = requirement.name if requirement.extras: @@ -1640,7 +1642,6 @@ def system_which(command, mult=False): return result - def format_help(help): """Formats the help string.""" help = help.replace("Options:", str(crayons.normal("Options:", bold=True))) @@ -1784,8 +1785,8 @@ def do_py(system=False): ), err=True, ) - return - + return + try: click.echo(which("python", allow_global=system)) except AttributeError: diff --git a/pipenv/environments.py b/pipenv/environments.py index 34aef2bce7..e7e64d31a1 100644 --- a/pipenv/environments.py +++ b/pipenv/environments.py @@ -236,6 +236,15 @@ def _is_env_truthy(name): NOTE: This only affects the ``install`` and ``uninstall`` commands. """ +PIPENV_RESOLVE_VCS = _is_env_truthy(os.environ.get("PIPENV_RESOLVE_VCS", 'true')) +"""Tells Pipenv whether to resolve all VCS dependencies in full. + +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'. +""" + + PIPENV_PYUP_API_KEY = os.environ.get( "PIPENV_PYUP_API_KEY", "1ab8d58f-5122e025-83674263-bc1e79e0" ) diff --git a/pipenv/patched/piptools/utils.py b/pipenv/patched/piptools/utils.py index 88755431f7..fb846cc4a6 100644 --- a/pipenv/patched/piptools/utils.py +++ b/pipenv/patched/piptools/utils.py @@ -70,7 +70,7 @@ def clean_requires_python(candidates): all_candidates = [] py_version = parse_version(os.environ.get('PIP_PYTHON_VERSION', '.'.join(map(str, sys.version_info[:3])))) for c in candidates: - if c.requires_python: + if getattr(c, "requires_python", None): # Old specifications had people setting this to single digits # which is effectively the same as '>=digit, str return self.entry.specifiers @property @@ -373,7 +390,7 @@ def original_version(self): return None def validate_specifiers(self): - if self.is_in_pipfile: + if self.is_in_pipfile and not self.pipfile_entry.editable: return self.pipfile_entry.requirement.specifier.contains(self.updated_version) return True @@ -550,8 +567,11 @@ def validate_constraints(self): constraint.check_if_exists(False) except Exception: from pipenv.exceptions import DependencyConflict + from pipenv.environments import is_verbose + if is_verbose(): + print("Tried constraint: {0!r}".format(constraint), file=sys.stderr) msg = ( - "Cannot resolve conflicting version {0}{1} while {1}{2} is " + "Cannot resolve conflicting version {0}{1} while {2}{3} is " "locked.".format( self.name, self.updated_specifier, self.old_name, self.old_specifiers ) @@ -624,6 +644,7 @@ def clean_results(results, resolver, project, dev=False): def clean_outdated(results, resolver, project, dev=False): from pipenv.vendor.requirementslib.models.requirements import Requirement + from pipenv.environments import is_verbose if not project.lockfile_exists: return results lockfile = project.lockfile_content diff --git a/pipenv/utils.py b/pipenv/utils.py index b73b7fa737..78c5283c8c 100644 --- a/pipenv/utils.py +++ b/pipenv/utils.py @@ -31,7 +31,9 @@ import parse from . import environments -from .exceptions import PipenvUsageError, ResolutionFailure, RequirementError, PipenvCmdError +from .exceptions import ( + PipenvUsageError, RequirementError, PipenvCmdError, ResolutionFailure +) from .pep508checker import lookup from .vendor.urllib3 import util as urllib3_util @@ -398,7 +400,6 @@ def parse_line( ): # type: (...) -> Tuple[Requirement, Dict[str, str], Dict[str, str]] from .vendor.requirementslib.models.requirements import Requirement - from .exceptions import ResolutionFailure if index_lookup is None: index_lookup = {} if markers_lookup is None: @@ -444,6 +445,7 @@ 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]]]]] from .vendor.requirementslib.models.utils import _requirement_to_str_lowercase_name from .vendor.requirementslib.models.requirements import Requirement + from requirementslib.utils import is_installable_dir constraints = set() # type: Set[str] locked_deps = dict() # type: Dict[str, Dict[str, Union[str, bool, List[str]]]] if (req.is_file_or_url or req.is_vcs) and not req.is_wheel: @@ -463,7 +465,16 @@ def get_deps_from_req(cls, req, resolver=None): setup_info = req.req.setup_info setup_info.get_info() locked_deps[pep423_name(name)] = entry - requirements = [v for v in getattr(setup_info, "requires", {}).values()] + 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 ( + 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) + ) + ): + requirements = [v for v in getattr(setup_info, "requires", {}).values()] for r in requirements: if getattr(r, "url", None) and not getattr(r, "editable", False): if r is not None: @@ -1797,13 +1808,15 @@ def clean_resolved_dep(dep, is_top_level=False, pipfile_entry=None): # If a package is **PRESENT** in the pipfile but has no markers, make sure we # **NEVER** include markers in the lockfile - if "markers" in dep: + if "markers" in dep and dep.get("markers", "").strip(): # First, handle the case where there is no top level dependency in the pipfile if not is_top_level: - try: - lockfile["markers"] = translate_markers(dep)["markers"] - except TypeError: - pass + translated = translate_markers(dep).get("markers", "").strip() + if translated: + try: + lockfile["markers"] = translated + except TypeError: + pass # otherwise make sure we are prioritizing whatever the pipfile says about the markers # If the pipfile says nothing, then we should put nothing in the lockfile else: diff --git a/tasks/vendoring/patches/patched/piptools.patch b/tasks/vendoring/patches/patched/piptools.patch index 14c3354ce1..c220eb4bed 100644 --- a/tasks/vendoring/patches/patched/piptools.patch +++ b/tasks/vendoring/patches/patched/piptools.patch @@ -608,7 +608,7 @@ index 9b4b4c2..8875543 100644 + all_candidates = [] + py_version = parse_version(os.environ.get('PIP_PYTHON_VERSION', '.'.join(map(str, sys.version_info[:3])))) + for c in candidates: -+ if c.requires_python: ++ if getattr(c, "requires_python", None): + # Old specifications had people setting this to single digits + # which is effectively the same as '>=digit,= '2.7'" + + @pytest.mark.lock @pytest.mark.keep_outdated def test_keep_outdated_doesnt_update_satisfied_constraints(PipenvInstance, pypi):