summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.mailmap1
-rw-r--r--AUTHORS.txt2
-rw-r--r--NEWS.rst10
-rw-r--r--news/9185.feature.rst2
-rw-r--r--news/9203.bugfix.rst2
-rw-r--r--news/9232.bugfix.rst2
-rw-r--r--src/pip/_internal/cli/base_command.py14
-rw-r--r--src/pip/_internal/cli/cmdoptions.py8
-rw-r--r--src/pip/_internal/commands/download.py1
-rw-r--r--src/pip/_internal/commands/install.py2
-rw-r--r--src/pip/_internal/commands/wheel.py1
-rw-r--r--src/pip/_internal/resolution/resolvelib/base.py32
-rw-r--r--src/pip/_internal/resolution/resolvelib/candidates.py28
-rw-r--r--src/pip/_internal/resolution/resolvelib/factory.py11
-rw-r--r--src/pip/_internal/resolution/resolvelib/provider.py63
-rw-r--r--src/pip/_internal/resolution/resolvelib/requirements.py19
16 files changed, 187 insertions, 11 deletions
diff --git a/.mailmap b/.mailmap
index 63d292348..29f9ec039 100644
--- a/.mailmap
+++ b/.mailmap
@@ -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
diff --git a/NEWS.rst b/NEWS.rst
index feff15919..71071a4c0 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -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