diff options
-rw-r--r-- | .mailmap | 1 | ||||
-rw-r--r-- | AUTHORS.txt | 2 | ||||
-rw-r--r-- | NEWS.rst | 10 | ||||
-rw-r--r-- | news/9185.feature.rst | 2 | ||||
-rw-r--r-- | news/9203.bugfix.rst | 2 | ||||
-rw-r--r-- | news/9232.bugfix.rst | 2 | ||||
-rw-r--r-- | src/pip/_internal/cli/base_command.py | 14 | ||||
-rw-r--r-- | src/pip/_internal/cli/cmdoptions.py | 8 | ||||
-rw-r--r-- | src/pip/_internal/commands/download.py | 1 | ||||
-rw-r--r-- | src/pip/_internal/commands/install.py | 2 | ||||
-rw-r--r-- | src/pip/_internal/commands/wheel.py | 1 | ||||
-rw-r--r-- | src/pip/_internal/resolution/resolvelib/base.py | 32 | ||||
-rw-r--r-- | src/pip/_internal/resolution/resolvelib/candidates.py | 28 | ||||
-rw-r--r-- | src/pip/_internal/resolution/resolvelib/factory.py | 11 | ||||
-rw-r--r-- | src/pip/_internal/resolution/resolvelib/provider.py | 63 | ||||
-rw-r--r-- | src/pip/_internal/resolution/resolvelib/requirements.py | 19 |
16 files changed, 187 insertions, 11 deletions
@@ -20,7 +20,6 @@ Dustin Ingram <di@di.codes> <di@users.noreply.gi Endoh Takanao <djmchl@gmail.com> Erik M. Bray <embray@stsci.edu> Gabriel de Perthuis <g2p.code@gmail.com> -Geoffrey Lehée <geoffrey@lehee.name> Hsiaoming Yang <lepture@me.com> Igor Kuzmitshov <kuzmiigo@gmail.com> <igor@qubit.com> Ilya Baryshev <baryshev@gmail.com> diff --git a/AUTHORS.txt b/AUTHORS.txt index 7ab324ce4..5b53db6c2 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -218,7 +218,6 @@ Gabriel Curio Gabriel de Perthuis Garry Polley gdanielson -Geoffrey Lehée Geoffrey Sneddon George Song Georgi Valkov @@ -551,6 +550,7 @@ Tony Zhaocheng Tan TonyBeswick toonarmycaptain Toshio Kuratomi +toxinu Travis Swicegood Tzu-ping Chung Valentin Haenel @@ -9,6 +9,16 @@ .. towncrier release notes start +20.3.1 (2020-12-03) +=================== + +Deprecations and Removals +------------------------- + +- The --build-dir option has been restored as a no-op, to soften the transition + for tools that still used it. (`#9193 <https://github.com/pypa/pip/issues/9193>`_) + + 20.3 (2020-11-30) ================= diff --git a/news/9185.feature.rst b/news/9185.feature.rst new file mode 100644 index 000000000..a9d9ae718 --- /dev/null +++ b/news/9185.feature.rst @@ -0,0 +1,2 @@ +New resolver: Resolve direct and pinned (``==`` or ``===``) requirements first +to improve resolver performance. diff --git a/news/9203.bugfix.rst b/news/9203.bugfix.rst new file mode 100644 index 000000000..29b39d66c --- /dev/null +++ b/news/9203.bugfix.rst @@ -0,0 +1,2 @@ +New resolver: Correctly implement PEP 592. Do not return yanked versions from +an index, unless the version range can only be satisfied by yanked candidates. diff --git a/news/9232.bugfix.rst b/news/9232.bugfix.rst new file mode 100644 index 000000000..2d50d1ce4 --- /dev/null +++ b/news/9232.bugfix.rst @@ -0,0 +1,2 @@ +New resolver: Make constraints also apply to package variants with extras, so +the resolver correctly avoids backtracking on them. diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index c1522d639..7f05efb85 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -199,6 +199,20 @@ class Command(CommandContextMixIn): ) options.cache_dir = None + if getattr(options, "build_dir", None): + deprecated( + reason=( + "The -b/--build/--build-dir/--build-directory " + "option is deprecated and has no effect anymore." + ), + replacement=( + "use the TMPDIR/TEMP/TMP environment variable, " + "possibly combined with --no-clean" + ), + gone_in="21.1", + issue=8333, + ) + if '2020-resolver' in options.features_enabled and not PY2: logger.warning( "--use-feature=2020-resolver no longer has any effect, " diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 07d612a6f..3543ed48b 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -695,6 +695,14 @@ no_deps = partial( help="Don't install package dependencies.", ) # type: Callable[..., Option] +build_dir = partial( + PipOption, + '-b', '--build', '--build-dir', '--build-directory', + dest='build_dir', + type='path', + metavar='dir', + help=SUPPRESS_HELP, +) # type: Callable[..., Option] ignore_requires_python = partial( Option, diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index a2d3bf7d9..7405870ae 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -43,6 +43,7 @@ class DownloadCommand(RequirementCommand): # type: () -> None self.cmd_opts.add_option(cmdoptions.constraints()) self.cmd_opts.add_option(cmdoptions.requirements()) + self.cmd_opts.add_option(cmdoptions.build_dir()) self.cmd_opts.add_option(cmdoptions.no_deps()) self.cmd_opts.add_option(cmdoptions.global_options()) self.cmd_opts.add_option(cmdoptions.no_binary()) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 38f9f063d..a4e10f260 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -129,6 +129,8 @@ class InstallCommand(RequirementCommand): help="Installation prefix where lib, bin and other top-level " "folders are placed") + self.cmd_opts.add_option(cmdoptions.build_dir()) + self.cmd_opts.add_option(cmdoptions.src()) self.cmd_opts.add_option( diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 2d654338d..39fd2bf81 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -78,6 +78,7 @@ class WheelCommand(RequirementCommand): self.cmd_opts.add_option(cmdoptions.src()) self.cmd_opts.add_option(cmdoptions.ignore_requires_python()) self.cmd_opts.add_option(cmdoptions.no_deps()) + self.cmd_opts.add_option(cmdoptions.build_dir()) self.cmd_opts.add_option(cmdoptions.progress_bar()) self.cmd_opts.add_option( diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index e2edbe9f4..7eb8a178e 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -68,8 +68,24 @@ class Constraint(object): class Requirement(object): @property + def project_name(self): + # type: () -> str + """The "project name" of a requirement. + + This is different from ``name`` if this requirement contains extras, + in which case ``name`` would contain the ``[...]`` part, while this + refers to the name of the project. + """ + raise NotImplementedError("Subclass should override") + + @property def name(self): # type: () -> str + """The name identifying this requirement in the resolver. + + This is different from ``project_name`` if this requirement contains + extras, where ``project_name`` would not contain the ``[...]`` part. + """ raise NotImplementedError("Subclass should override") def is_satisfied_by(self, candidate): @@ -87,8 +103,24 @@ class Requirement(object): class Candidate(object): @property + def project_name(self): + # type: () -> str + """The "project name" of the candidate. + + This is different from ``name`` if this candidate contains extras, + in which case ``name`` would contain the ``[...]`` part, while this + refers to the name of the project. + """ + raise NotImplementedError("Override in subclass") + + @property def name(self): # type: () -> str + """The name identifying this candidate in the resolver. + + This is different from ``project_name`` if this candidate contains + extras, where ``project_name`` would not contain the ``[...]`` part. + """ raise NotImplementedError("Override in subclass") @property diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 1fc2ff479..cd1f18870 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -175,7 +175,7 @@ class _InstallRequirementBackedCandidate(Candidate): return self._source_link @property - def name(self): + def project_name(self): # type: () -> str """The normalised name of the project the candidate refers to""" if self._name is None: @@ -183,6 +183,11 @@ class _InstallRequirementBackedCandidate(Candidate): return self._name @property + def name(self): + # type: () -> str + return self.project_name + + @property def version(self): # type: () -> _BaseVersion if self._version is None: @@ -390,11 +395,16 @@ class AlreadyInstalledCandidate(Candidate): return not self.__eq__(other) @property - def name(self): + def project_name(self): # type: () -> str return canonicalize_name(self.dist.project_name) @property + def name(self): + # type: () -> str + return self.project_name + + @property def version(self): # type: () -> _BaseVersion return self.dist.parsed_version @@ -482,10 +492,15 @@ class ExtrasCandidate(Candidate): return not self.__eq__(other) @property + def project_name(self): + # type: () -> str + return self.base.project_name + + @property def name(self): # type: () -> str """The normalised name of the project the candidate refers to""" - return format_name(self.base.name, self.extras) + return format_name(self.base.project_name, self.extras) @property def version(self): @@ -572,12 +587,17 @@ class RequiresPythonCandidate(Candidate): return "Python {}".format(self._version) @property - def name(self): + def project_name(self): # type: () -> str # Avoid conflicting with the PyPI package "Python". return "<Python from Requires-Python>" @property + def name(self): + # type: () -> str + return self.project_name + + @property def version(self): # type: () -> _BaseVersion return self._version diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index f4177d981..c723d343b 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -193,8 +193,17 @@ class Factory(object): specifier=specifier, hashes=hashes, ) + icans = list(result.iter_applicable()) + + # PEP 592: Yanked releases must be ignored unless only yanked + # releases can satisfy the version range. So if this is false, + # all yanked icans need to be skipped. + all_yanked = all(ican.link.is_yanked for ican in icans) + # PackageFinder returns earlier versions first, so we reverse. - for ican in reversed(list(result.iter_applicable())): + for ican in reversed(icans): + if not all_yanked and ican.link.is_yanked: + continue yield self._make_candidate_from_link( link=ican.link, extras=extras, diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index c0e6b60d9..3883135f1 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -30,6 +30,16 @@ if MYPY_CHECK_RUNNING: class PipProvider(AbstractProvider): + """Pip's provider implementation for resolvelib. + + :params constraints: A mapping of constraints specified by the user. Keys + are canonicalized project names. + :params ignore_dependencies: Whether the user specified ``--no-deps``. + :params upgrade_strategy: The user-specified upgrade strategy. + :params user_requested: A set of canonicalized package names that the user + supplied for pip to install/upgrade. + """ + def __init__( self, factory, # type: Factory @@ -56,15 +66,64 @@ class PipProvider(AbstractProvider): information # type: Sequence[Tuple[Requirement, Candidate]] ): # type: (...) -> Any + """Produce a sort key for given requirement based on preference. + + The lower the return value is, the more preferred this group of + arguments is. + + Currently pip considers the followings in order: + + * Prefer if any of the known requirements points to an explicit URL. + * If equal, prefer if any requirements contain ``===`` and ``==``. + * If equal, prefer if requirements include version constraints, e.g. + ``>=`` and ``<``. + * If equal, prefer user-specified (non-transitive) requirements. + * If equal, order alphabetically for consistency (helps debuggability). + """ + + def _get_restrictive_rating(requirements): + # type: (Iterable[Requirement]) -> int + """Rate how restrictive a set of requirements are. + + ``Requirement.get_candidate_lookup()`` returns a 2-tuple for + lookup. The first element is ``Optional[Candidate]`` and the + second ``Optional[InstallRequirement]``. + + * If the requirement is an explicit one, the explicitly-required + candidate is returned as the first element. + * If the requirement is based on a PEP 508 specifier, the backing + ``InstallRequirement`` is returned as the second element. + + We use the first element to check whether there is an explicit + requirement, and the second for equality operator. + """ + lookups = (r.get_candidate_lookup() for r in requirements) + cands, ireqs = zip(*lookups) + if any(cand is not None for cand in cands): + return 0 + spec_sets = (ireq.specifier for ireq in ireqs if ireq) + operators = [ + specifier.operator + for spec_set in spec_sets + for specifier in spec_set + ] + if any(op in ("==", "===") for op in operators): + return 1 + if operators: + return 2 + # A "bare" requirement without any version requirements. + return 3 + + restrictive = _get_restrictive_rating(req for req, _ in information) transitive = all(parent is not None for _, parent in information) key = next(iter(candidates)).name if candidates else "" - return (transitive, key) + return (restrictive, transitive, key) def find_matches(self, requirements): # type: (Sequence[Requirement]) -> Iterable[Candidate] if not requirements: return [] - name = requirements[0].name + name = requirements[0].project_name def _eligible_for_upgrade(name): # type: (str) -> bool diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 25cddceaf..d926d0a06 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -29,6 +29,12 @@ class ExplicitRequirement(Requirement): ) @property + def project_name(self): + # type: () -> str + # No need to canonicalise - the candidate did this + return self.candidate.project_name + + @property def name(self): # type: () -> str # No need to canonicalise - the candidate did this @@ -66,10 +72,14 @@ class SpecifierRequirement(Requirement): ) @property + def project_name(self): + # type: () -> str + return canonicalize_name(self._ireq.req.name) + + @property def name(self): # type: () -> str - canonical_name = canonicalize_name(self._ireq.req.name) - return format_name(canonical_name, self._extras) + return format_name(self.project_name, self._extras) def format_for_error(self): # type: () -> str @@ -122,6 +132,11 @@ class RequiresPythonRequirement(Requirement): ) @property + def project_name(self): + # type: () -> str + return self._candidate.project_name + + @property def name(self): # type: () -> str return self._candidate.name |