diff options
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 @@ -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) |