123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450 |
- import logging
- import sys
- from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
- from pip._vendor.packaging.utils import canonicalize_name
- from pip._vendor.packaging.version import Version
- from pip._internal.req.constructors import (
- install_req_from_editable,
- install_req_from_line,
- )
- from pip._internal.req.req_install import InstallRequirement
- from pip._internal.utils.misc import normalize_version_info
- from pip._internal.utils.packaging import get_requires_python
- from pip._internal.utils.typing import MYPY_CHECK_RUNNING
- from .base import Candidate, format_name
- if MYPY_CHECK_RUNNING:
- from typing import Any, Optional, Sequence, Set, Tuple, Union
- from pip._vendor.packaging.version import _BaseVersion
- from pip._vendor.pkg_resources import Distribution
- from pip._internal.distributions import AbstractDistribution
- from pip._internal.models.link import Link
- from .base import Requirement
- from .factory import Factory
- BaseCandidate = Union[
- "AlreadyInstalledCandidate",
- "EditableCandidate",
- "LinkCandidate",
- ]
- logger = logging.getLogger(__name__)
- def make_install_req_from_link(link, parent):
- # type: (Link, InstallRequirement) -> InstallRequirement
- assert not parent.editable, "parent is editable"
- return install_req_from_line(
- link.url,
- comes_from=parent.comes_from,
- use_pep517=parent.use_pep517,
- isolated=parent.isolated,
- constraint=parent.constraint,
- options=dict(
- install_options=parent.install_options,
- global_options=parent.global_options,
- hashes=parent.hash_options
- ),
- )
- def make_install_req_from_editable(link, parent):
- # type: (Link, InstallRequirement) -> InstallRequirement
- assert parent.editable, "parent not editable"
- return install_req_from_editable(
- link.url,
- comes_from=parent.comes_from,
- use_pep517=parent.use_pep517,
- isolated=parent.isolated,
- constraint=parent.constraint,
- options=dict(
- install_options=parent.install_options,
- global_options=parent.global_options,
- hashes=parent.hash_options
- ),
- )
- def make_install_req_from_dist(dist, parent):
- # type: (Distribution, InstallRequirement) -> InstallRequirement
- ireq = install_req_from_line(
- "{}=={}".format(
- canonicalize_name(dist.project_name),
- dist.parsed_version,
- ),
- comes_from=parent.comes_from,
- use_pep517=parent.use_pep517,
- isolated=parent.isolated,
- constraint=parent.constraint,
- options=dict(
- install_options=parent.install_options,
- global_options=parent.global_options,
- hashes=parent.hash_options
- ),
- )
- ireq.satisfied_by = dist
- return ireq
- class _InstallRequirementBackedCandidate(Candidate):
- def __init__(
- self,
- link, # type: Link
- ireq, # type: InstallRequirement
- factory, # type: Factory
- name=None, # type: Optional[str]
- version=None, # type: Optional[_BaseVersion]
- ):
- # type: (...) -> None
- self.link = link
- self._factory = factory
- self._ireq = ireq
- self._name = name
- self._version = version
- self._dist = None # type: Optional[Distribution]
- def __repr__(self):
- # type: () -> str
- return "{class_name}({link!r})".format(
- class_name=self.__class__.__name__,
- link=str(self.link),
- )
- def __eq__(self, other):
- # type: (Any) -> bool
- if isinstance(other, self.__class__):
- return self.link == other.link
- return False
- # Needed for Python 2, which does not implement this by default
- def __ne__(self, other):
- # type: (Any) -> bool
- return not self.__eq__(other)
- @property
- def name(self):
- # type: () -> str
- """The normalised name of the project the candidate refers to"""
- if self._name is None:
- self._name = canonicalize_name(self.dist.project_name)
- return self._name
- @property
- def version(self):
- # type: () -> _BaseVersion
- if self._version is None:
- self._version = self.dist.parsed_version
- return self._version
- def _prepare_abstract_distribution(self):
- # type: () -> AbstractDistribution
- raise NotImplementedError("Override in subclass")
- def _prepare(self):
- # type: () -> None
- if self._dist is not None:
- return
- abstract_dist = self._prepare_abstract_distribution()
- self._dist = abstract_dist.get_pkg_resources_distribution()
- assert self._dist is not None, "Distribution already installed"
- # TODO: Abort cleanly here, as the resolution has been
- # based on the wrong name/version until now, and
- # so is wrong.
- # TODO: (Longer term) Rather than abort, reject this candidate
- # and backtrack. This would need resolvelib support.
- # These should be "proper" errors, not just asserts, as they
- # can result from user errors like a requirement "foo @ URL"
- # when the project at URL has a name of "bar" in its metadata.
- assert (
- self._name is None or
- self._name == canonicalize_name(self._dist.project_name)
- ), "Name mismatch: {!r} vs {!r}".format(
- self._name, canonicalize_name(self._dist.project_name),
- )
- assert (
- self._version is None or
- self._version == self._dist.parsed_version
- ), "Version mismatch: {!r} vs {!r}".format(
- self._version, self._dist.parsed_version,
- )
- @property
- def dist(self):
- # type: () -> Distribution
- self._prepare()
- return self._dist
- def _get_requires_python_specifier(self):
- # type: () -> Optional[SpecifierSet]
- requires_python = get_requires_python(self.dist)
- if requires_python is None:
- return None
- try:
- spec = SpecifierSet(requires_python)
- except InvalidSpecifier as e:
- logger.warning(
- "Package %r has an invalid Requires-Python: %s", self.name, e,
- )
- return None
- return spec
- def get_dependencies(self):
- # type: () -> Sequence[Requirement]
- deps = [
- self._factory.make_requirement_from_spec(str(r), self._ireq)
- for r in self.dist.requires()
- ]
- python_dep = self._factory.make_requires_python_requirement(
- self._get_requires_python_specifier(),
- )
- if python_dep:
- deps.append(python_dep)
- return deps
- def get_install_requirement(self):
- # type: () -> Optional[InstallRequirement]
- self._prepare()
- return self._ireq
- class LinkCandidate(_InstallRequirementBackedCandidate):
- def __init__(
- self,
- link, # type: Link
- parent, # type: InstallRequirement
- factory, # type: Factory
- name=None, # type: Optional[str]
- version=None, # type: Optional[_BaseVersion]
- ):
- # type: (...) -> None
- super(LinkCandidate, self).__init__(
- link=link,
- ireq=make_install_req_from_link(link, parent),
- factory=factory,
- name=name,
- version=version,
- )
- def _prepare_abstract_distribution(self):
- # type: () -> AbstractDistribution
- return self._factory.preparer.prepare_linked_requirement(self._ireq)
- class EditableCandidate(_InstallRequirementBackedCandidate):
- def __init__(
- self,
- link, # type: Link
- parent, # type: InstallRequirement
- factory, # type: Factory
- name=None, # type: Optional[str]
- version=None, # type: Optional[_BaseVersion]
- ):
- # type: (...) -> None
- super(EditableCandidate, self).__init__(
- link=link,
- ireq=make_install_req_from_editable(link, parent),
- factory=factory,
- name=name,
- version=version,
- )
- def _prepare_abstract_distribution(self):
- # type: () -> AbstractDistribution
- return self._factory.preparer.prepare_editable_requirement(self._ireq)
- class AlreadyInstalledCandidate(Candidate):
- def __init__(
- self,
- dist, # type: Distribution
- parent, # type: InstallRequirement
- factory, # type: Factory
- ):
- # type: (...) -> None
- self.dist = dist
- self._ireq = make_install_req_from_dist(dist, parent)
- self._factory = factory
- # This is just logging some messages, so we can do it eagerly.
- # The returned dist would be exactly the same as self.dist because we
- # set satisfied_by in make_install_req_from_dist.
- # TODO: Supply reason based on force_reinstall and upgrade_strategy.
- skip_reason = "already satisfied"
- factory.preparer.prepare_installed_requirement(self._ireq, skip_reason)
- def __repr__(self):
- # type: () -> str
- return "{class_name}({distribution!r})".format(
- class_name=self.__class__.__name__,
- distribution=self.dist,
- )
- def __eq__(self, other):
- # type: (Any) -> bool
- if isinstance(other, self.__class__):
- return self.name == other.name and self.version == other.version
- return False
- # Needed for Python 2, which does not implement this by default
- def __ne__(self, other):
- # type: (Any) -> bool
- return not self.__eq__(other)
- @property
- def name(self):
- # type: () -> str
- return canonicalize_name(self.dist.project_name)
- @property
- def version(self):
- # type: () -> _BaseVersion
- return self.dist.parsed_version
- def get_dependencies(self):
- # type: () -> Sequence[Requirement]
- return [
- self._factory.make_requirement_from_spec(str(r), self._ireq)
- for r in self.dist.requires()
- ]
- def get_install_requirement(self):
- # type: () -> Optional[InstallRequirement]
- return None
- class ExtrasCandidate(Candidate):
- """A candidate that has 'extras', indicating additional dependencies.
- Requirements can be for a project with dependencies, something like
- foo[extra]. The extras don't affect the project/version being installed
- directly, but indicate that we need additional dependencies. We model that
- by having an artificial ExtrasCandidate that wraps the "base" candidate.
- The ExtrasCandidate differs from the base in the following ways:
- 1. It has a unique name, of the form foo[extra]. This causes the resolver
- to treat it as a separate node in the dependency graph.
- 2. When we're getting the candidate's dependencies,
- a) We specify that we want the extra dependencies as well.
- b) We add a dependency on the base candidate (matching the name and
- version). See below for why this is needed.
- 3. We return None for the underlying InstallRequirement, as the base
- candidate will provide it, and we don't want to end up with duplicates.
- The dependency on the base candidate is needed so that the resolver can't
- decide that it should recommend foo[extra1] version 1.0 and foo[extra2]
- version 2.0. Having those candidates depend on foo=1.0 and foo=2.0
- respectively forces the resolver to recognise that this is a conflict.
- """
- def __init__(
- self,
- base, # type: BaseCandidate
- extras, # type: Set[str]
- ):
- # type: (...) -> None
- self.base = base
- self.extras = extras
- def __repr__(self):
- # type: () -> str
- return "{class_name}(base={base!r}, extras={extras!r})".format(
- class_name=self.__class__.__name__,
- base=self.base,
- extras=self.extras,
- )
- def __eq__(self, other):
- # type: (Any) -> bool
- if isinstance(other, self.__class__):
- return self.base == other.base and self.extras == other.extras
- return False
- # Needed for Python 2, which does not implement this by default
- def __ne__(self, other):
- # type: (Any) -> bool
- return not self.__eq__(other)
- @property
- def name(self):
- # type: () -> str
- """The normalised name of the project the candidate refers to"""
- return format_name(self.base.name, self.extras)
- @property
- def version(self):
- # type: () -> _BaseVersion
- return self.base.version
- def get_dependencies(self):
- # type: () -> Sequence[Requirement]
- factory = self.base._factory
- # The user may have specified extras that the candidate doesn't
- # support. We ignore any unsupported extras here.
- valid_extras = self.extras.intersection(self.base.dist.extras)
- invalid_extras = self.extras.difference(self.base.dist.extras)
- if invalid_extras:
- logger.warning(
- "Invalid extras specified in %s: %s",
- self.name,
- ','.join(sorted(invalid_extras))
- )
- deps = [
- factory.make_requirement_from_spec(str(r), self.base._ireq)
- for r in self.base.dist.requires(valid_extras)
- ]
- # Add a dependency on the exact base.
- # (See note 2b in the class docstring)
- spec = "{}=={}".format(self.base.name, self.base.version)
- deps.append(factory.make_requirement_from_spec(spec, self.base._ireq))
- return deps
- def get_install_requirement(self):
- # type: () -> Optional[InstallRequirement]
- # We don't return anything here, because we always
- # depend on the base candidate, and we'll get the
- # install requirement from that.
- return None
- class RequiresPythonCandidate(Candidate):
- def __init__(self, py_version_info):
- # type: (Optional[Tuple[int, ...]]) -> None
- if py_version_info is not None:
- version_info = normalize_version_info(py_version_info)
- else:
- version_info = sys.version_info[:3]
- self._version = Version(".".join(str(c) for c in version_info))
- # We don't need to implement __eq__() and __ne__() since there is always
- # only one RequiresPythonCandidate in a resolution, i.e. the host Python.
- # The built-in object.__eq__() and object.__ne__() do exactly what we want.
- @property
- def name(self):
- # type: () -> str
- # Avoid conflicting with the PyPI package "Python".
- return "<Python fom Requires-Python>"
- @property
- def version(self):
- # type: () -> _BaseVersion
- return self._version
- def get_dependencies(self):
- # type: () -> Sequence[Requirement]
- return []
- def get_install_requirement(self):
- # type: () -> Optional[InstallRequirement]
- return None
|