summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/html/index.md48
-rw-r--r--docs/html/index.rst63
-rw-r--r--news/9541.bugfix.rst1
-rw-r--r--news/9e768673-6079-491e-bbe0-d1593952f1c7.trivial.rst0
-rw-r--r--setup.cfg17
-rw-r--r--src/pip/_internal/commands/debug.py4
-rw-r--r--src/pip/_internal/commands/install.py2
-rw-r--r--src/pip/_internal/locations/_sysconfig.py7
-rw-r--r--src/pip/_internal/metadata/base.py8
-rw-r--r--src/pip/_internal/metadata/pkg_resources.py8
-rw-r--r--src/pip/_internal/models/candidate.py3
-rw-r--r--src/pip/_internal/network/session.py2
-rw-r--r--src/pip/_internal/operations/check.py24
-rw-r--r--src/pip/_internal/req/constructors.py8
-rw-r--r--src/pip/_internal/req/req_install.py2
-rw-r--r--src/pip/_internal/req/req_set.py6
-rw-r--r--src/pip/_internal/resolution/resolvelib/base.py13
-rw-r--r--src/pip/_internal/resolution/resolvelib/candidates.py46
-rw-r--r--src/pip/_internal/resolution/resolvelib/factory.py70
-rw-r--r--src/pip/_internal/resolution/resolvelib/requirements.py21
-rw-r--r--src/pip/_internal/resolution/resolvelib/resolver.py16
-rw-r--r--src/pip/_internal/wheel_builder.py10
-rw-r--r--tests/functional/test_new_resolver_errors.py30
23 files changed, 231 insertions, 178 deletions
diff --git a/docs/html/index.md b/docs/html/index.md
new file mode 100644
index 000000000..351d8f79f
--- /dev/null
+++ b/docs/html/index.md
@@ -0,0 +1,48 @@
+---
+hide-toc: true
+---
+
+# pip
+
+pip is the [package installer for Python][recommended]. You can use it to
+install packages from the [Python Package Index][pypi] and other indexes.
+
+```{toctree}
+:hidden:
+
+quickstart
+installing
+user_guide
+reference/index
+```
+
+```{toctree}
+:caption: Project
+:hidden:
+
+development/index
+ux_research_design
+news
+Code of Conduct <https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md>
+GitHub <https://github.com/pypa/pip>
+```
+
+If you want to learn about how to use pip, check out the following resources:
+
+- [Quickstart](quickstart)
+- [Python Packaging User Guide](https://packaging.python.org)
+
+If you find bugs, need help, or want to talk to the developers, use our mailing
+lists or chat rooms:
+
+- [GitHub Issues][issue-tracker]
+- [Discourse channel][packaging-discourse]
+- [User IRC][irc-pypa]
+- [Development IRC][irc-pypa-dev]
+
+[recommended]: https://packaging.python.org/guides/tool-recommendations/
+[pypi]: https://pypi.org/
+[issue-tracker]: https://github.com/pypa/pip/issues/
+[packaging-discourse]: https://discuss.python.org/c/packaging/14
+[irc-pypa]: https://webchat.freenode.net/#pypa
+[irc-pypa-dev]: https://webchat.freenode.net/#pypa-dev
diff --git a/docs/html/index.rst b/docs/html/index.rst
deleted file mode 100644
index b92a23e02..000000000
--- a/docs/html/index.rst
+++ /dev/null
@@ -1,63 +0,0 @@
-==================================
-pip - The Python Package Installer
-==================================
-
-pip is the `package installer`_ for Python. You can use pip to install packages from the `Python Package Index`_ and other indexes.
-
-Please take a look at our documentation for how to install and use pip:
-
-.. toctree::
- :maxdepth: 1
-
- quickstart
- installing
- user_guide
- reference/index
- development/index
- ux_research_design
- news
-
-.. warning::
-
- In pip 20.3, we've `made a big improvement to the heart of pip`_;
- :ref:`Resolver changes 2020`. We want your input, so `sign up for
- our user experience research studies`_ to help us do it right.
-
-.. warning::
-
- pip 21.0, in January 2021, removed Python 2 support, per pip's
- :ref:`Python 2 Support` policy. Please migrate to Python 3.
-
-If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms:
-
-* `Issue tracking`_
-* `Discourse channel`_
-* `User IRC`_
-
-If you want to get involved, head over to GitHub to get the source code, and feel free to jump on the developer mailing lists and chat rooms:
-
-* `GitHub page`_
-* `Development mailing list`_
-* `Development IRC`_
-
-
-Code of Conduct
-===============
-
-Everyone interacting in the pip project's codebases, issue trackers, chat
-rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_.
-
-.. _package installer: https://packaging.python.org/guides/tool-recommendations/
-.. _Python Package Index: https://pypi.org
-.. _made a big improvement to the heart of pip: https://pyfound.blogspot.com/2020/11/pip-20-3-new-resolver.html
-.. _sign up for our user experience research studies: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html
-.. _Installation: https://pip.pypa.io/en/stable/installing.html
-.. _Documentation: https://pip.pypa.io/en/stable/
-.. _Changelog: https://pip.pypa.io/en/stable/news.html
-.. _GitHub page: https://github.com/pypa/pip
-.. _Issue tracking: https://github.com/pypa/pip/issues
-.. _Discourse channel: https://discuss.python.org/c/packaging
-.. _Development mailing list: https://mail.python.org/mailman3/lists/distutils-sig.python.org/
-.. _User IRC: https://webchat.freenode.net/?channels=%23pypa
-.. _Development IRC: https://webchat.freenode.net/?channels=%23pypa-dev
-.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md
diff --git a/news/9541.bugfix.rst b/news/9541.bugfix.rst
new file mode 100644
index 000000000..88180198c
--- /dev/null
+++ b/news/9541.bugfix.rst
@@ -0,0 +1 @@
+Fix incorrect reporting on ``Requires-Python`` conflicts.
diff --git a/news/9e768673-6079-491e-bbe0-d1593952f1c7.trivial.rst b/news/9e768673-6079-491e-bbe0-d1593952f1c7.trivial.rst
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/news/9e768673-6079-491e-bbe0-d1593952f1c7.trivial.rst
diff --git a/setup.cfg b/setup.cfg
index 1d851d949..7746f08c9 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -36,10 +36,23 @@ disallow_untyped_defs = True
disallow_any_generics = True
warn_unused_ignores = True
-[mypy-pip/_vendor/*]
-follow_imports = skip
+[mypy-pip._vendor.*]
ignore_errors = True
+# These vendored libraries use runtime magic to populate things and don't sit
+# well with static typing out of the box. Eventually we should provide correct
+# typing information for their public interface and remove these configs.
+[mypy-pip._vendor.colorama]
+follow_imports = skip
+[mypy-pip._vendor.pkg_resources]
+follow_imports = skip
+[mypy-pip._vendor.progress.*]
+follow_imports = skip
+[mypy-pip._vendor.requests.*]
+follow_imports = skip
+[mypy-pip._vendor.retrying]
+follow_imports = skip
+
[tool:pytest]
addopts = --ignore src/pip/_vendor --ignore tests/tests_cache -r aR
markers =
diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py
index 480c0444d..ead5119a2 100644
--- a/src/pip/_internal/commands/debug.py
+++ b/src/pip/_internal/commands/debug.py
@@ -4,7 +4,7 @@ import os
import sys
from optparse import Values
from types import ModuleType
-from typing import Dict, List, Optional
+from typing import Any, Dict, List, Optional
import pip._vendor
from pip._vendor.certifi import where
@@ -24,7 +24,7 @@ logger = logging.getLogger(__name__)
def show_value(name, value):
- # type: (str, Optional[str]) -> None
+ # type: (str, Any) -> None
logger.info('%s: %s', name, value)
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index c4273eda9..dc637d876 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -49,7 +49,7 @@ def get_check_binary_allowed(format_control):
# type: (FormatControl) -> BinaryAllowedPredicate
def check_binary_allowed(req):
# type: (InstallRequirement) -> bool
- canonical_name = canonicalize_name(req.name)
+ canonical_name = canonicalize_name(req.name or "")
allowed_formats = format_control.get_allowed_formats(canonical_name)
return "binary" in allowed_formats
diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py
index 93e8d40b1..e4d66d25d 100644
--- a/src/pip/_internal/locations/_sysconfig.py
+++ b/src/pip/_internal/locations/_sysconfig.py
@@ -136,13 +136,6 @@ def get_scheme(
python_xy = f"python{get_major_minor_version()}"
paths["include"] = os.path.join(base, "include", "site", python_xy)
- # Special user scripts path on Windows for compatibility to distutils.
- # See ``distutils.commands.install.INSTALL_SCHEMES["nt_user"]["scripts"]``.
- if scheme_name == "nt_user":
- base = variables.get("userbase", sys.prefix)
- python_xy = f"Python{sys.version_info.major}{sys.version_info.minor}"
- paths["scripts"] = os.path.join(base, python_xy, "Scripts")
-
scheme = Scheme(
platlib=paths["platlib"],
purelib=paths["purelib"],
diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py
index 100168b6e..37f9a8232 100644
--- a/src/pip/_internal/metadata/base.py
+++ b/src/pip/_internal/metadata/base.py
@@ -1,11 +1,13 @@
import logging
import re
-from typing import Container, Iterator, List, Optional
+from typing import Container, Iterator, List, Optional, Union
-from pip._vendor.packaging.version import _BaseVersion
+from pip._vendor.packaging.version import LegacyVersion, Version
from pip._internal.utils.misc import stdlib_pkgs # TODO: Move definition here.
+DistributionVersion = Union[LegacyVersion, Version]
+
logger = logging.getLogger(__name__)
@@ -34,7 +36,7 @@ class BaseDistribution:
@property
def version(self):
- # type: () -> _BaseVersion
+ # type: () -> DistributionVersion
raise NotImplementedError()
@property
diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py
index 5cd9eaee6..f39a39ebe 100644
--- a/src/pip/_internal/metadata/pkg_resources.py
+++ b/src/pip/_internal/metadata/pkg_resources.py
@@ -3,13 +3,13 @@ from typing import Iterator, List, Optional
from pip._vendor import pkg_resources
from pip._vendor.packaging.utils import canonicalize_name
-from pip._vendor.packaging.version import _BaseVersion
+from pip._vendor.packaging.version import parse as parse_version
from pip._internal.utils import misc # TODO: Move definition here.
from pip._internal.utils.packaging import get_installer
from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel
-from .base import BaseDistribution, BaseEnvironment
+from .base import BaseDistribution, BaseEnvironment, DistributionVersion
class Distribution(BaseDistribution):
@@ -44,8 +44,8 @@ class Distribution(BaseDistribution):
@property
def version(self):
- # type: () -> _BaseVersion
- return self._dist.parsed_version
+ # type: () -> DistributionVersion
+ return parse_version(self._dist.version)
@property
def installer(self):
diff --git a/src/pip/_internal/models/candidate.py b/src/pip/_internal/models/candidate.py
index 10a144620..3b91704a2 100644
--- a/src/pip/_internal/models/candidate.py
+++ b/src/pip/_internal/models/candidate.py
@@ -1,4 +1,3 @@
-from pip._vendor.packaging.version import _BaseVersion
from pip._vendor.packaging.version import parse as parse_version
from pip._internal.models.link import Link
@@ -14,7 +13,7 @@ class InstallationCandidate(KeyBasedCompareMixin):
def __init__(self, name, version, link):
# type: (str, str, Link) -> None
self.name = name
- self.version = parse_version(version) # type: _BaseVersion
+ self.version = parse_version(version)
self.link = link
super().__init__(
diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py
index b42d06bc3..4af800f12 100644
--- a/src/pip/_internal/network/session.py
+++ b/src/pip/_internal/network/session.py
@@ -295,7 +295,7 @@ class PipSession(requests.Session):
# Add a small amount of back off between failed requests in
# order to prevent hammering the service.
backoff_factor=0.25,
- )
+ ) # type: ignore
# Our Insecure HTTPAdapter disables HTTPS validation. It does not
# support caching so we'll use it for all http:// URLs.
diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py
index 224633561..5699c0b91 100644
--- a/src/pip/_internal/operations/check.py
+++ b/src/pip/_internal/operations/check.py
@@ -3,7 +3,7 @@
import logging
from collections import namedtuple
-from typing import Any, Callable, Dict, List, Optional, Set, Tuple
+from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.pkg_resources import RequirementParseError
@@ -12,23 +12,25 @@ from pip._internal.distributions import make_distribution_for_install_requiremen
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.misc import get_installed_distributions
+if TYPE_CHECKING:
+ from pip._vendor.packaging.utils import NormalizedName
+
logger = logging.getLogger(__name__)
# Shorthands
-PackageSet = Dict[str, 'PackageDetails']
+PackageSet = Dict['NormalizedName', 'PackageDetails']
Missing = Tuple[str, Any]
Conflicting = Tuple[str, str, Any]
-MissingDict = Dict[str, List[Missing]]
-ConflictingDict = Dict[str, List[Conflicting]]
+MissingDict = Dict['NormalizedName', List[Missing]]
+ConflictingDict = Dict['NormalizedName', List[Conflicting]]
CheckResult = Tuple[MissingDict, ConflictingDict]
ConflictDetails = Tuple[PackageSet, CheckResult]
PackageDetails = namedtuple('PackageDetails', ['version', 'requires'])
-def create_package_set_from_installed(**kwargs):
- # type: (**Any) -> Tuple[PackageSet, bool]
+def create_package_set_from_installed(**kwargs: Any) -> Tuple["PackageSet", bool]:
"""Converts a list of distributions into a PackageSet.
"""
# Default to using all packages installed on the system
@@ -59,7 +61,7 @@ def check_package_set(package_set, should_ignore=None):
missing = {}
conflicting = {}
- for package_name in package_set:
+ for package_name, package_detail in package_set.items():
# Info about dependencies of package_name
missing_deps = set() # type: Set[Missing]
conflicting_deps = set() # type: Set[Conflicting]
@@ -67,8 +69,8 @@ def check_package_set(package_set, should_ignore=None):
if should_ignore and should_ignore(package_name):
continue
- for req in package_set[package_name].requires:
- name = canonicalize_name(req.project_name) # type: str
+ for req in package_detail.requires:
+ name = canonicalize_name(req.project_name)
# Check if it's missing
if name not in package_set:
@@ -114,7 +116,7 @@ def check_install_conflicts(to_install):
def _simulate_installation_of(to_install, package_set):
- # type: (List[InstallRequirement], PackageSet) -> Set[str]
+ # type: (List[InstallRequirement], PackageSet) -> Set[NormalizedName]
"""Computes the version of packages after installing to_install.
"""
@@ -136,7 +138,7 @@ def _simulate_installation_of(to_install, package_set):
def _create_whitelist(would_be_installed, package_set):
- # type: (Set[str], PackageSet) -> Set[str]
+ # type: (Set[NormalizedName], PackageSet) -> Set[NormalizedName]
packages_affected = set(would_be_installed)
for package_name in package_set:
diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py
index b279bccbc..a659b4d6e 100644
--- a/src/pip/_internal/req/constructors.py
+++ b/src/pip/_internal/req/constructors.py
@@ -182,7 +182,7 @@ def parse_req_from_editable(editable_req):
if name is not None:
try:
- req = Requirement(name)
+ req = Requirement(name) # type: Optional[Requirement]
except InvalidRequirement:
raise InstallationError(f"Invalid requirement: '{name}'")
else:
@@ -335,7 +335,7 @@ def parse_req_from_line(name, line_source):
return text
return f'{text} (from {line_source})'
- if req_as_string is not None:
+ def _parse_req_string(req_as_string: str) -> Requirement:
try:
req = Requirement(req_as_string)
except InvalidRequirement:
@@ -363,6 +363,10 @@ def parse_req_from_line(name, line_source):
if spec_str.endswith(']'):
msg = f"Extras after version '{spec_str}'."
raise InstallationError(msg)
+ return req
+
+ if req_as_string is not None:
+ req = _parse_req_string(req_as_string) # type: Optional[Requirement]
else:
req = None
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
index 9419eec49..7f4e974cc 100644
--- a/src/pip/_internal/req/req_install.py
+++ b/src/pip/_internal/req/req_install.py
@@ -349,7 +349,7 @@ class InstallRequirement:
# When parallel builds are enabled, add a UUID to the build directory
# name so multiple builds do not interfere with each other.
- dir_name = canonicalize_name(self.name)
+ dir_name = canonicalize_name(self.name) # type: str
if parallel_builds:
dir_name = f"{dir_name}_{uuid.uuid4().hex}"
diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py
index 7ff137b73..59c584355 100644
--- a/src/pip/_internal/req/req_set.py
+++ b/src/pip/_internal/req/req_set.py
@@ -28,7 +28,7 @@ class RequirementSet:
# type: () -> str
requirements = sorted(
(req for req in self.requirements.values() if not req.comes_from),
- key=lambda req: canonicalize_name(req.name),
+ key=lambda req: canonicalize_name(req.name or ""),
)
return ' '.join(str(req.req) for req in requirements)
@@ -36,7 +36,7 @@ class RequirementSet:
# type: () -> str
requirements = sorted(
self.requirements.values(),
- key=lambda req: canonicalize_name(req.name),
+ key=lambda req: canonicalize_name(req.name or ""),
)
format_string = '<{classname} object; {count} requirement(s): {reqs}>'
@@ -122,6 +122,8 @@ class RequirementSet:
existing_req and
not existing_req.constraint and
existing_req.extras == install_req.extras and
+ existing_req.req and
+ install_req.req and
existing_req.req.specifier != install_req.req.specifier
)
if has_conflicting_requirement:
diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py
index 81fee9b9e..0295b0ed8 100644
--- a/src/pip/_internal/resolution/resolvelib/base.py
+++ b/src/pip/_internal/resolution/resolvelib/base.py
@@ -1,14 +1,15 @@
-from typing import FrozenSet, Iterable, Optional, Tuple
+from typing import FrozenSet, Iterable, Optional, Tuple, Union
from pip._vendor.packaging.specifiers import SpecifierSet
-from pip._vendor.packaging.utils import canonicalize_name
-from pip._vendor.packaging.version import _BaseVersion
+from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
+from pip._vendor.packaging.version import LegacyVersion, Version
from pip._internal.models.link import Link
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.hashes import Hashes
CandidateLookup = Tuple[Optional["Candidate"], Optional[InstallRequirement]]
+CandidateVersion = Union[LegacyVersion, Version]
def format_name(project, extras):
@@ -62,7 +63,7 @@ class Constraint:
class Requirement:
@property
def project_name(self):
- # type: () -> str
+ # type: () -> NormalizedName
"""The "project name" of a requirement.
This is different from ``name`` if this requirement contains extras,
@@ -97,7 +98,7 @@ class Requirement:
class Candidate:
@property
def project_name(self):
- # type: () -> str
+ # type: () -> NormalizedName
"""The "project name" of the candidate.
This is different from ``name`` if this candidate contains extras,
@@ -118,7 +119,7 @@ class Candidate:
@property
def version(self):
- # type: () -> _BaseVersion
+ # type: () -> CandidateVersion
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 035e118d0..184884cbd 100644
--- a/src/pip/_internal/resolution/resolvelib/candidates.py
+++ b/src/pip/_internal/resolution/resolvelib/candidates.py
@@ -1,10 +1,11 @@
import logging
import sys
-from typing import TYPE_CHECKING, Any, FrozenSet, Iterable, Optional, Tuple, Union
+from typing import TYPE_CHECKING, Any, FrozenSet, Iterable, Optional, Tuple, Union, cast
from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
-from pip._vendor.packaging.utils import canonicalize_name
-from pip._vendor.packaging.version import Version, _BaseVersion
+from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
+from pip._vendor.packaging.version import Version
+from pip._vendor.packaging.version import parse as parse_version
from pip._vendor.pkg_resources import Distribution
from pip._internal.exceptions import HashError, MetadataInconsistent
@@ -18,7 +19,7 @@ from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.misc import dist_is_editable, normalize_version_info
from pip._internal.utils.packaging import get_requires_python
-from .base import Candidate, Requirement, format_name
+from .base import Candidate, CandidateVersion, Requirement, format_name
if TYPE_CHECKING:
from .factory import Factory
@@ -125,8 +126,8 @@ class _InstallRequirementBackedCandidate(Candidate):
source_link, # type: Link
ireq, # type: InstallRequirement
factory, # type: Factory
- name=None, # type: Optional[str]
- version=None, # type: Optional[_BaseVersion]
+ name=None, # type: Optional[NormalizedName]
+ version=None, # type: Optional[CandidateVersion]
):
# type: (...) -> None
self._link = link
@@ -165,7 +166,7 @@ class _InstallRequirementBackedCandidate(Candidate):
@property
def project_name(self):
- # type: () -> str
+ # type: () -> NormalizedName
"""The normalised name of the project the candidate refers to"""
if self._name is None:
self._name = canonicalize_name(self.dist.project_name)
@@ -178,9 +179,9 @@ class _InstallRequirementBackedCandidate(Candidate):
@property
def version(self):
- # type: () -> _BaseVersion
+ # type: () -> CandidateVersion
if self._version is None:
- self._version = self.dist.parsed_version
+ self._version = parse_version(self.dist.version)
return self._version
def format_for_error(self):
@@ -206,7 +207,8 @@ class _InstallRequirementBackedCandidate(Candidate):
self._name,
dist.project_name,
)
- if self._version is not None and self._version != dist.parsed_version:
+ parsed_version = parse_version(dist.version)
+ if self._version is not None and self._version != parsed_version:
raise MetadataInconsistent(
self._ireq,
"version",
@@ -260,8 +262,8 @@ class LinkCandidate(_InstallRequirementBackedCandidate):
link, # type: Link
template, # type: InstallRequirement
factory, # type: Factory
- name=None, # type: Optional[str]
- version=None, # type: Optional[_BaseVersion]
+ name=None, # type: Optional[NormalizedName]
+ version=None, # type: Optional[CandidateVersion]
):
# type: (...) -> None
source_link = link
@@ -313,8 +315,8 @@ class EditableCandidate(_InstallRequirementBackedCandidate):
link, # type: Link
template, # type: InstallRequirement
factory, # type: Factory
- name=None, # type: Optional[str]
- version=None, # type: Optional[_BaseVersion]
+ name=None, # type: Optional[NormalizedName]
+ version=None, # type: Optional[CandidateVersion]
):
# type: (...) -> None
super().__init__(
@@ -376,7 +378,7 @@ class AlreadyInstalledCandidate(Candidate):
@property
def project_name(self):
- # type: () -> str
+ # type: () -> NormalizedName
return canonicalize_name(self.dist.project_name)
@property
@@ -386,8 +388,8 @@ class AlreadyInstalledCandidate(Candidate):
@property
def version(self):
- # type: () -> _BaseVersion
- return self.dist.parsed_version
+ # type: () -> CandidateVersion
+ return parse_version(self.dist.version)
@property
def is_editable(self):
@@ -469,7 +471,7 @@ class ExtrasCandidate(Candidate):
@property
def project_name(self):
- # type: () -> str
+ # type: () -> NormalizedName
return self.base.project_name
@property
@@ -480,7 +482,7 @@ class ExtrasCandidate(Candidate):
@property
def version(self):
- # type: () -> _BaseVersion
+ # type: () -> CandidateVersion
return self.base.version
def format_for_error(self):
@@ -563,9 +565,9 @@ class RequiresPythonCandidate(Candidate):
@property
def project_name(self):
- # type: () -> str
+ # type: () -> NormalizedName
# Avoid conflicting with the PyPI package "Python".
- return "<Python from Requires-Python>"
+ return cast(NormalizedName, "<Python from Requires-Python>")
@property
def name(self):
@@ -574,7 +576,7 @@ class RequiresPythonCandidate(Candidate):
@property
def version(self):
- # type: () -> _BaseVersion
+ # type: () -> CandidateVersion
return self._version
def format_for_error(self):
diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py
index fbf04fa4f..aa6c4781d 100644
--- a/src/pip/_internal/resolution/resolvelib/factory.py
+++ b/src/pip/_internal/resolution/resolvelib/factory.py
@@ -1,6 +1,7 @@
import functools
import logging
from typing import (
+ TYPE_CHECKING,
Dict,
FrozenSet,
Iterable,
@@ -14,8 +15,7 @@ from typing import (
)
from pip._vendor.packaging.specifiers import SpecifierSet
-from pip._vendor.packaging.utils import canonicalize_name
-from pip._vendor.packaging.version import _BaseVersion
+from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._vendor.pkg_resources import Distribution
from pip._vendor.resolvelib import ResolutionImpossible
@@ -43,7 +43,7 @@ from pip._internal.utils.misc import (
)
from pip._internal.utils.virtualenv import running_under_virtualenv
-from .base import Candidate, Constraint, Requirement
+from .base import Candidate, CandidateVersion, Constraint, Requirement
from .candidates import (
AlreadyInstalledCandidate,
BaseCandidate,
@@ -60,6 +60,14 @@ from .requirements import (
UnsatisfiableRequirement,
)
+if TYPE_CHECKING:
+ from typing import Protocol
+
+ class ConflictCause(Protocol):
+ requirement: RequiresPythonRequirement
+ parent: Candidate
+
+
logger = logging.getLogger(__name__)
C = TypeVar("C")
@@ -130,8 +138,8 @@ class Factory:
link, # type: Link
extras, # type: FrozenSet[str]
template, # type: InstallRequirement
- name, # type: Optional[str]
- version, # type: Optional[_BaseVersion]
+ name, # type: Optional[NormalizedName]
+ version, # type: Optional[CandidateVersion]
):
# type: (...) -> Optional[Candidate]
# TODO: Check already installed candidate, and use it if the link and
@@ -193,10 +201,12 @@ class Factory:
# all of them.
# Hopefully the Project model can correct this mismatch in the future.
template = ireqs[0]
+ assert template.req, "Candidates found on index must be PEP 508"
name = canonicalize_name(template.req.name)
extras = frozenset() # type: FrozenSet[str]
for ireq in ireqs:
+ assert ireq.req, "Candidates found on index must be PEP 508"
specifier &= ireq.req.specifier
hashes &= ireq.hashes(trust_internet=False)
extras |= frozenset(ireq.extras)
@@ -359,7 +369,7 @@ class Factory:
def get_dist_to_uninstall(self, candidate):
# type: (Candidate) -> Optional[Distribution]
# TODO: Are there more cases this needs to return True? Editable?
- dist = self._installed_dists.get(candidate.name)
+ dist = self._installed_dists.get(candidate.project_name)
if dist is None: # Not installed, no uninstallation required.
return None
@@ -387,21 +397,25 @@ class Factory:
)
return None
- def _report_requires_python_error(
- self,
- requirement, # type: RequiresPythonRequirement
- template, # type: Candidate
- ):
- # type: (...) -> UnsupportedPythonVersion
- message_format = (
- "Package {package!r} requires a different Python: "
- "{version} not in {specifier!r}"
- )
- message = message_format.format(
- package=template.name,
- version=self._python_candidate.version,
- specifier=str(requirement.specifier),
- )
+ def _report_requires_python_error(self, causes):
+ # type: (Sequence[ConflictCause]) -> UnsupportedPythonVersion
+ assert causes, "Requires-Python error reported with no cause"
+
+ version = self._python_candidate.version
+
+ if len(causes) == 1:
+ specifier = str(causes[0].requirement.specifier)
+ message = (
+ f"Package {causes[0].parent.name!r} requires a different "
+ f"Python: {version} not in {specifier!r}"
+ )
+ return UnsupportedPythonVersion(message)
+
+ message = f"Packages require a different Python. {version} not in:"
+ for cause in causes:
+ package = cause.parent.format_for_error()
+ specifier = str(cause.requirement.specifier)
+ message += f"\n{specifier!r} (required by {package})"
return UnsupportedPythonVersion(message)
def _report_single_requirement_conflict(self, req, parent):
@@ -434,12 +448,14 @@ class Factory:
# If one of the things we can't solve is "we need Python X.Y",
# that is what we report.
- for cause in e.causes:
- if isinstance(cause.requirement, RequiresPythonRequirement):
- return self._report_requires_python_error(
- cause.requirement,
- cause.parent,
- )
+ requires_python_causes = [
+ cause
+ for cause in e.causes
+ if isinstance(cause.requirement, RequiresPythonRequirement)
+ and not cause.requirement.is_satisfied_by(self._python_candidate)
+ ]
+ if requires_python_causes:
+ return self._report_requires_python_error(requires_python_causes)
# Otherwise, we have a set of causes which can't all be satisfied
# at once.
diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py
index 70ad86af9..b4cb57140 100644
--- a/src/pip/_internal/resolution/resolvelib/requirements.py
+++ b/src/pip/_internal/resolution/resolvelib/requirements.py
@@ -1,5 +1,5 @@
from pip._vendor.packaging.specifiers import SpecifierSet
-from pip._vendor.packaging.utils import canonicalize_name
+from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
from pip._internal.req.req_install import InstallRequirement
@@ -24,7 +24,7 @@ class ExplicitRequirement(Requirement):
@property
def project_name(self):
- # type: () -> str
+ # type: () -> NormalizedName
# No need to canonicalise - the candidate did this
return self.candidate.project_name
@@ -67,7 +67,8 @@ class SpecifierRequirement(Requirement):
@property
def project_name(self):
- # type: () -> str
+ # type: () -> NormalizedName
+ assert self._ireq.req, "Specifier-backed ireq is always PEP 508"
return canonicalize_name(self._ireq.req.name)
@property
@@ -96,14 +97,14 @@ class SpecifierRequirement(Requirement):
def is_satisfied_by(self, candidate):
# type: (Candidate) -> bool
- assert (
- candidate.name == self.name
- ), "Internal issue: Candidate is not for this requirement " " {} vs {}".format(
- candidate.name, self.name
+ assert candidate.name == self.name, (
+ f"Internal issue: Candidate is not for this requirement "
+ f"{candidate.name} vs {self.name}"
)
# We can safely always allow prereleases here since PackageFinder
# already implements the prerelease logic, and would have filtered out
# prerelease candidates if the user does not expect them.
+ assert self._ireq.req, "Specifier-backed ireq is always PEP 508"
spec = self._ireq.req.specifier
return spec.contains(candidate.version, prereleases=True)
@@ -129,7 +130,7 @@ class RequiresPythonRequirement(Requirement):
@property
def project_name(self):
- # type: () -> str
+ # type: () -> NormalizedName
return self._candidate.project_name
@property
@@ -160,7 +161,7 @@ class UnsatisfiableRequirement(Requirement):
"""A requirement that cannot be satisfied."""
def __init__(self, name):
- # type: (str) -> None
+ # type: (NormalizedName) -> None
self._name = name
def __str__(self):
@@ -176,7 +177,7 @@ class UnsatisfiableRequirement(Requirement):
@property
def project_name(self):
- # type: () -> str
+ # type: () -> NormalizedName
return self._name
@property
diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py
index eba441091..4f8a53d0d 100644
--- a/src/pip/_internal/resolution/resolvelib/resolver.py
+++ b/src/pip/_internal/resolution/resolvelib/resolver.py
@@ -4,7 +4,8 @@ import os
from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple
from pip._vendor.packaging.utils import canonicalize_name
-from pip._vendor.resolvelib import ResolutionImpossible
+from pip._vendor.packaging.version import parse as parse_version
+from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible
from pip._vendor.resolvelib import Resolver as RLResolver
from pip._vendor.resolvelib.resolvers import Result
@@ -31,7 +32,7 @@ from .base import Constraint
from .factory import Factory
if TYPE_CHECKING:
- from pip._vendor.resolvelib.structs import Graph
+ from pip._vendor.resolvelib.structs import DirectedGraph
logger = logging.getLogger(__name__)
@@ -85,6 +86,7 @@ class Resolver(BaseResolver):
raise InstallationError(problem)
if not req.match_markers():
continue
+ assert req.name, "Constraint must be named"
name = canonicalize_name(req.name)
if name in constraints:
constraints[name] &= req
@@ -109,14 +111,14 @@ class Resolver(BaseResolver):
user_requested=user_requested,
)
if "PIP_RESOLVER_DEBUG" in os.environ:
- reporter = PipDebuggingReporter()
+ reporter = PipDebuggingReporter() # type: BaseReporter
else:
reporter = PipReporter()
resolver = RLResolver(provider, reporter)
try:
try_to_avoid_resolution_too_deep = 2000000
- self._result = resolver.resolve(
+ result = self._result = resolver.resolve(
requirements, max_rounds=try_to_avoid_resolution_too_deep
)
@@ -125,7 +127,7 @@ class Resolver(BaseResolver):
raise error from e
req_set = RequirementSet(check_supported_wheels=check_supported_wheels)
- for candidate in self._result.mapping.values():
+ for candidate in result.mapping.values():
ireq = candidate.get_install_requirement()
if ireq is None:
continue
@@ -139,7 +141,7 @@ class Resolver(BaseResolver):
elif self.factory.force_reinstall:
# The --force-reinstall flag is set -- reinstall.
ireq.should_reinstall = True
- elif installed_dist.parsed_version != candidate.version:
+ elif parse_version(installed_dist.version) != candidate.version:
# The installation is different in version -- reinstall.
ireq.should_reinstall = True
elif candidate.is_editable or dist_is_editable(installed_dist):
@@ -234,7 +236,7 @@ class Resolver(BaseResolver):
def get_topological_weights(graph, expected_node_count):
- # type: (Graph, int) -> Dict[Optional[str], int]
+ # type: (DirectedGraph, int) -> Dict[Optional[str], int]
"""Assign weights to each node based on how "deep" they are.
This implementation may change at any point in the future without prior
diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py
index d6314714a..90966d9ed 100644
--- a/src/pip/_internal/wheel_builder.py
+++ b/src/pip/_internal/wheel_builder.py
@@ -167,7 +167,7 @@ def _always_true(_):
def _verify_one(req, wheel_path):
# type: (InstallRequirement, str) -> None
- canonical_name = canonicalize_name(req.name)
+ canonical_name = canonicalize_name(req.name or "")
w = Wheel(os.path.basename(wheel_path))
if canonicalize_name(w.name) != canonical_name:
raise InvalidWheelFilename(
@@ -175,10 +175,11 @@ def _verify_one(req, wheel_path):
"got {!r}".format(canonical_name, w.name),
)
dist = get_wheel_distribution(wheel_path, canonical_name)
- if canonicalize_version(dist.version) != canonicalize_version(w.version):
+ dist_verstr = str(dist.version)
+ if canonicalize_version(dist_verstr) != canonicalize_version(w.version):
raise InvalidWheelFilename(
"Wheel has unexpected file name: expected {!r}, "
- "got {!r}".format(str(dist.version), w.version),
+ "got {!r}".format(dist_verstr, w.version),
)
metadata_version_value = dist.metadata_version
if metadata_version_value is None:
@@ -192,7 +193,7 @@ def _verify_one(req, wheel_path):
and not isinstance(dist.version, Version)):
raise UnsupportedWheel(
"Metadata 1.2 mandates PEP 440 version, "
- "but {!r} is not".format(str(dist.version))
+ "but {!r} is not".format(dist_verstr)
)
@@ -242,6 +243,7 @@ def _build_one_inside_env(
assert req.name
if req.use_pep517:
assert req.metadata_directory
+ assert req.pep517_backend
wheel_path = build_wheel_pep517(
name=req.name,
backend=req.pep517_backend,
diff --git a/tests/functional/test_new_resolver_errors.py b/tests/functional/test_new_resolver_errors.py
index e263f4206..b4d63a996 100644
--- a/tests/functional/test_new_resolver_errors.py
+++ b/tests/functional/test_new_resolver_errors.py
@@ -1,4 +1,6 @@
-from tests.lib import create_basic_wheel_for_package
+import sys
+
+from tests.lib import create_basic_wheel_for_package, create_test_package_with_setup
def test_new_resolver_conflict_requirements_file(tmpdir, script):
@@ -45,3 +47,29 @@ def test_new_resolver_conflict_constraints_file(tmpdir, script):
message = "The user requested (constraint) pkg!=1.0"
assert message in result.stdout, str(result)
+
+
+def test_new_resolver_requires_python_error(script):
+ compatible_python = ">={0.major}.{0.minor}".format(sys.version_info)
+ incompatible_python = "<{0.major}.{0.minor}".format(sys.version_info)
+
+ pkga = create_test_package_with_setup(
+ script,
+ name="pkga",
+ version="1.0",
+ python_requires=compatible_python,
+ )
+ pkgb = create_test_package_with_setup(
+ script,
+ name="pkgb",
+ version="1.0",
+ python_requires=incompatible_python,
+ )
+
+ # This always fails because pkgb can never be satisfied.
+ result = script.pip("install", "--no-index", pkga, pkgb, expect_error=True)
+
+ # The error message should mention the Requires-Python: value causing the
+ # conflict, not the compatible one.
+ assert incompatible_python in result.stderr, str(result)
+ assert compatible_python not in result.stderr, str(result)