diff --git a/src/pip/_internal/distributions/__init__.py b/src/pip/_internal/distributions/__init__.py new file mode 100644 index 000000000..92ea47336 --- /dev/null +++ b/src/pip/_internal/distributions/__init__.py @@ -0,0 +1,23 @@ +from pip._internal.distributions.source import SourceDistribution +from pip._internal.distributions.wheel import WheelDistribution + +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from pip._internal.distributions.base import AbstractDistribution + from pip._internal.req.req_install import InstallRequirement + + +def make_abstract_dist(install_req): + # type: (InstallRequirement) -> AbstractDistribution + """Returns a Distribution for the given InstallRequirement + """ + # If it's not an editable, is a wheel, it's a WheelDistribution + if install_req.editable: + return SourceDistribution(install_req) + + if install_req.link and install_req.is_wheel: + return WheelDistribution(install_req) + + # Otherwise, a SourceDistribution + return SourceDistribution(install_req) diff --git a/src/pip/_internal/distributions/base.py b/src/pip/_internal/distributions/base.py new file mode 100644 index 000000000..c0669fd88 --- /dev/null +++ b/src/pip/_internal/distributions/base.py @@ -0,0 +1,19 @@ +import abc + +from pip._vendor.six import add_metaclass + + +@add_metaclass(abc.ABCMeta) +class AbstractDistribution(object): + + def __init__(self, req): + super(AbstractDistribution, self).__init__() + self.req = req + + @abc.abstractmethod + def dist(self): + raise NotImplementedError() + + @abc.abstractmethod + def prep_for_dist(self, finder, build_isolation): + raise NotImplementedError() diff --git a/src/pip/_internal/distributions/installed.py b/src/pip/_internal/distributions/installed.py new file mode 100644 index 000000000..79fdb735d --- /dev/null +++ b/src/pip/_internal/distributions/installed.py @@ -0,0 +1,10 @@ +from pip._internal.distributions.base import AbstractDistribution + + +class InstalledDistribution(AbstractDistribution): + + def dist(self): + return self.req.satisfied_by + + def prep_for_dist(self, finder, build_isolation): + pass diff --git a/src/pip/_internal/distributions/source.py b/src/pip/_internal/distributions/source.py new file mode 100644 index 000000000..e2206ef80 --- /dev/null +++ b/src/pip/_internal/distributions/source.py @@ -0,0 +1,70 @@ +import logging + +from pip._internal.build_env import BuildEnvironment +from pip._internal.distributions.base import AbstractDistribution +from pip._internal.exceptions import InstallationError + +logger = logging.getLogger(__name__) + + +class SourceDistribution(AbstractDistribution): + + def dist(self): + return self.req.get_dist() + + def prep_for_dist(self, finder, build_isolation): + # Prepare for building. We need to: + # 1. Load pyproject.toml (if it exists) + # 2. Set up the build environment + + self.req.load_pyproject_toml() + should_isolate = self.req.use_pep517 and build_isolation + + def _raise_conflicts(conflicting_with, conflicting_reqs): + raise InstallationError( + "Some build dependencies for %s conflict with %s: %s." % ( + self.req, conflicting_with, ', '.join( + '%s is incompatible with %s' % (installed, wanted) + for installed, wanted in sorted(conflicting)))) + + if should_isolate: + # Isolate in a BuildEnvironment and install the build-time + # requirements. + self.req.build_env = BuildEnvironment() + self.req.build_env.install_requirements( + finder, self.req.pyproject_requires, 'overlay', + "Installing build dependencies" + ) + conflicting, missing = self.req.build_env.check_requirements( + self.req.requirements_to_check + ) + if conflicting: + _raise_conflicts("PEP 517/518 supported requirements", + conflicting) + if missing: + logger.warning( + "Missing build requirements in pyproject.toml for %s.", + self.req, + ) + logger.warning( + "The project does not specify a build backend, and " + "pip cannot fall back to setuptools without %s.", + " and ".join(map(repr, sorted(missing))) + ) + # Install any extra build dependencies that the backend requests. + # This must be done in a second pass, as the pyproject.toml + # dependencies must be installed before we can call the backend. + with self.req.build_env: + # We need to have the env active when calling the hook. + self.req.spin_message = "Getting requirements to build wheel" + reqs = self.req.pep517_backend.get_requires_for_build_wheel() + conflicting, missing = self.req.build_env.check_requirements(reqs) + if conflicting: + _raise_conflicts("the backend dependencies", conflicting) + self.req.build_env.install_requirements( + finder, missing, 'normal', + "Installing backend dependencies" + ) + + self.req.prepare_metadata() + self.req.assert_source_matches_version() diff --git a/src/pip/_internal/distributions/wheel.py b/src/pip/_internal/distributions/wheel.py new file mode 100644 index 000000000..7b089b01e --- /dev/null +++ b/src/pip/_internal/distributions/wheel.py @@ -0,0 +1,13 @@ +from pip._vendor import pkg_resources + +from pip._internal.distributions.base import AbstractDistribution + + +class WheelDistribution(AbstractDistribution): + + def dist(self): + return list(pkg_resources.find_distributions( + self.req.source_dir))[0] + + def prep_for_dist(self, finder, build_isolation): + pass diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 7832ae841..08347b53e 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -36,6 +36,7 @@ if MYPY_CHECK_RUNNING: from pip._vendor import pkg_resources from pip._internal.cache import WheelCache + from pip._internal.distributions import AbstractDistribution from pip._internal.download import PipSession from pip._internal.index import PackageFinder from pip._internal.operations.prepare import RequirementPreparer @@ -276,7 +277,7 @@ class Resolver(object): return None def _get_abstract_dist_for(self, req): - # type: (InstallRequirement) -> DistAbstraction + # type: (InstallRequirement) -> AbstractDistribution """Takes a InstallRequirement and returns a single AbstractDist \ representing a prepared variant of the same. """ diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index 920df5d48..f8653c894 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -7,7 +7,7 @@ from collections import namedtuple from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.pkg_resources import RequirementParseError -from pip._internal.operations.prepare import make_abstract_dist +from pip._internal.distributions import make_abstract_dist from pip._internal.utils.misc import get_installed_distributions from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 9733237ee..3a4a70699 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -4,9 +4,10 @@ import logging import os -from pip._vendor import pkg_resources, requests +from pip._vendor import requests -from pip._internal.build_env import BuildEnvironment +from pip._internal.distributions import make_abstract_dist +from pip._internal.distributions.installed import InstalledDistribution from pip._internal.download import ( is_dir_url, is_file_url, is_vcs_url, unpack_url, url_to_path, ) @@ -21,8 +22,9 @@ from pip._internal.utils.misc import display_path, normalize_path from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, Optional + from typing import Optional + from pip._internal.distributions import AbstractDistribution from pip._internal.download import PipSession from pip._internal.index import PackageFinder from pip._internal.req.req_install import InstallRequirement @@ -31,159 +33,6 @@ if MYPY_CHECK_RUNNING: logger = logging.getLogger(__name__) -def make_abstract_dist(req): - # type: (InstallRequirement) -> DistAbstraction - """Factory to make an abstract dist object. - - Preconditions: Either an editable req with a source_dir, or satisfied_by or - a wheel link, or a non-editable req with a source_dir. - - :return: A concrete DistAbstraction. - """ - if req.editable: - return IsSDist(req) - elif req.link and req.link.is_wheel: - return IsWheel(req) - else: - return IsSDist(req) - - -class DistAbstraction(object): - """Abstracts out the wheel vs non-wheel Resolver.resolve() logic. - - The requirements for anything installable are as follows: - - we must be able to determine the requirement name - (or we can't correctly handle the non-upgrade case). - - we must be able to generate a list of run-time dependencies - without installing any additional packages (or we would - have to either burn time by doing temporary isolated installs - or alternatively violate pips 'don't start installing unless - all requirements are available' rule - neither of which are - desirable). - - for packages with setup requirements, we must also be able - to determine their requirements without installing additional - packages (for the same reason as run-time dependencies) - - we must be able to create a Distribution object exposing the - above metadata. - """ - - def __init__(self, req): - # type: (InstallRequirement) -> None - self.req = req # type: InstallRequirement - - def dist(self): - # type: () -> Any - """Return a setuptools Dist object.""" - raise NotImplementedError - - def prep_for_dist(self, finder, build_isolation): - # type: (PackageFinder, bool) -> Any - """Ensure that we can get a Dist for this requirement.""" - raise NotImplementedError - - -class IsWheel(DistAbstraction): - - def dist(self): - # type: () -> pkg_resources.Distribution - return list(pkg_resources.find_distributions( - self.req.source_dir))[0] - - def prep_for_dist(self, finder, build_isolation): - # type: (PackageFinder, bool) -> Any - # FIXME:https://github.com/pypa/pip/issues/1112 - pass - - -class IsSDist(DistAbstraction): - - def dist(self): - return self.req.get_dist() - - def _raise_conflicts(self, conflicting_with, conflicting_reqs): - conflict_messages = [ - '%s is incompatible with %s' % (installed, wanted) - for installed, wanted in sorted(conflicting_reqs) - ] - raise InstallationError( - "Some build dependencies for %s conflict with %s: %s." % ( - self.req, conflicting_with, ', '.join(conflict_messages)) - ) - - def install_backend_dependencies(self, finder): - # type: (PackageFinder) -> None - """ - Install any extra build dependencies that the backend requests. - - :param finder: a PackageFinder object. - """ - req = self.req - with req.build_env: - # We need to have the env active when calling the hook. - req.spin_message = "Getting requirements to build wheel" - reqs = req.pep517_backend.get_requires_for_build_wheel() - conflicting, missing = req.build_env.check_requirements(reqs) - if conflicting: - self._raise_conflicts("the backend dependencies", conflicting) - req.build_env.install_requirements( - finder, missing, 'normal', - "Installing backend dependencies" - ) - - def prep_for_dist(self, finder, build_isolation): - # type: (PackageFinder, bool) -> None - # Prepare for building. We need to: - # 1. Load pyproject.toml (if it exists) - # 2. Set up the build environment - - self.req.load_pyproject_toml() - should_isolate = self.req.use_pep517 and build_isolation - - if should_isolate: - # Isolate in a BuildEnvironment and install the build-time - # requirements. - self.req.build_env = BuildEnvironment() - self.req.build_env.install_requirements( - finder, self.req.pyproject_requires, 'overlay', - "Installing build dependencies" - ) - conflicting, missing = self.req.build_env.check_requirements( - self.req.requirements_to_check - ) - if conflicting: - self._raise_conflicts("PEP 517/518 supported requirements", - conflicting) - if missing: - logger.warning( - "Missing build requirements in pyproject.toml for %s.", - self.req, - ) - logger.warning( - "The project does not specify a build backend, and " - "pip cannot fall back to setuptools without %s.", - " and ".join(map(repr, sorted(missing))) - ) - - # Install any extra build dependencies that the backend requests. - # This must be done in a second pass, as the pyproject.toml - # dependencies must be installed before we can call the backend. - self.install_backend_dependencies(finder=finder) - - self.req.prepare_metadata() - self.req.assert_source_matches_version() - - -class Installed(DistAbstraction): - - def dist(self): - # type: () -> pkg_resources.Distribution - return self.req.satisfied_by - - def prep_for_dist(self, finder, build_isolation): - # type: (PackageFinder, bool) -> Any - pass - - class RequirementPreparer(object): """Prepares a Requirement """ @@ -249,7 +98,7 @@ class RequirementPreparer(object): upgrade_allowed, # type: bool require_hashes # type: bool ): - # type: (...) -> DistAbstraction + # type: (...) -> AbstractDistribution """Prepare a requirement that would be obtained from req.link """ # TODO: Breakup into smaller functions @@ -374,7 +223,7 @@ class RequirementPreparer(object): use_user_site, # type: bool finder # type: PackageFinder ): - # type: (...) -> DistAbstraction + # type: (...) -> AbstractDistribution """Prepare an editable requirement """ assert req.editable, "cannot prepare a non-editable req as editable" @@ -401,8 +250,13 @@ class RequirementPreparer(object): return abstract_dist - def prepare_installed_requirement(self, req, require_hashes, skip_reason): - # type: (InstallRequirement, bool, Optional[str]) -> DistAbstraction + def prepare_installed_requirement( + self, + req, # type: InstallRequirement + require_hashes, # type: bool + skip_reason # type: str + ): + # type: (...) -> AbstractDistribution """Prepare an already-installed requirement """ assert req.satisfied_by, "req should have been satisfied but isn't" @@ -422,6 +276,6 @@ class RequirementPreparer(object): 'completely repeatable environment, install into an ' 'empty virtualenv.' ) - abstract_dist = Installed(req) + abstract_dist = InstalledDistribution(req) return abstract_dist