diff --git a/piptools/_compat/pip_compat.py b/piptools/_compat/pip_compat.py index 7fea2b8f4..3ac653a27 100644 --- a/piptools/_compat/pip_compat.py +++ b/piptools/_compat/pip_compat.py @@ -1,5 +1,5 @@ import optparse -from typing import Iterator, Optional +from typing import Callable, Iterable, Iterator, Optional, cast import pip from pip._internal.index.package_finder import PackageFinder @@ -8,6 +8,7 @@ from pip._internal.req import parse_requirements as _parse_requirements from pip._internal.req.constructors import install_req_from_parsed_requirement from pip._vendor.packaging.version import parse as parse_version +from pip._vendor.pkg_resources import Requirement PIP_VERSION = tuple(map(int, parse_version(pip.__version__).base_version.split("."))) @@ -15,6 +16,9 @@ __all__ = [ "get_build_tracker", "update_env_context_manager", + "dist_requires", + "uses_pkg_resources", + "Distribution", ] @@ -42,3 +46,47 @@ def parse_requirements( get_build_tracker, update_env_context_manager, ) + + +# The Distribution interface has changed between pkg_resources and +# importlib.metadata, so this compat layer allows for a consistent access +# pattern. In pip 22.1, importlib.metdata became the default on Python 3.11 +# (and later), but is overrideable. `select_backend` returns what's being used. + + +def _uses_pkg_resources() -> bool: + + if PIP_VERSION[:2] < (22, 1): + return True + else: + from pip._internal.metadata import select_backend + from pip._internal.metadata.pkg_resources import Distribution as _Dist + + return select_backend().Distribution is _Dist + + +uses_pkg_resources = _uses_pkg_resources() + +if uses_pkg_resources: + from operator import methodcaller + + from pip._vendor.pkg_resources import Distribution + + dist_requires = cast( + Callable[[Distribution], Iterable[Requirement]], methodcaller("requires") + ) +else: + from pip._internal.metadata import select_backend + + Distribution = select_backend().Distribution + + def dist_requires(dist: "Distribution") -> Iterable[Requirement]: + """Mimics pkg_resources.Distribution.requires for the case of no + extras. This doesn't fulfill that API's `extras` parameter but + satisfies the needs of pip-tools.""" + reqs = (Requirement.parse(req) for req in (dist.requires or ())) + return [ + req + for req in reqs + if not req.marker or req.marker.evaluate({"extra": None}) + ] diff --git a/piptools/scripts/sync.py b/piptools/scripts/sync.py index f5fffd0a6..02ed847fb 100755 --- a/piptools/scripts/sync.py +++ b/piptools/scripts/sync.py @@ -10,10 +10,10 @@ from pip._internal.commands.install import InstallCommand from pip._internal.index.package_finder import PackageFinder from pip._internal.metadata import get_environment -from pip._vendor.pkg_resources import Distribution from .. import sync from .._compat import IS_CLICK_VER_8_PLUS, parse_requirements +from .._compat.pip_compat import Distribution from ..exceptions import PipToolsError from ..logging import log from ..repositories import PyPIRepository @@ -275,7 +275,6 @@ def _get_installed_distributions( paths: Optional[List[str]] = None, ) -> List[Distribution]: """Return a list of installed Distribution objects.""" - from pip._internal.metadata.pkg_resources import Distribution as _Dist env = get_environment(paths) dists = env.iter_installed_distributions( @@ -283,4 +282,4 @@ def _get_installed_distributions( user_only=user_only, skip=[], ) - return [cast(_Dist, dist)._dist for dist in dists] + return [cast(Distribution, dist)._dist for dist in dists] diff --git a/piptools/sync.py b/piptools/sync.py index ddf9808e6..336b88348 100644 --- a/piptools/sync.py +++ b/piptools/sync.py @@ -19,8 +19,8 @@ from pip._internal.commands.freeze import DEV_PKGS from pip._internal.req import InstallRequirement from pip._internal.utils.compat import stdlib_pkgs -from pip._vendor.pkg_resources import Distribution +from ._compat.pip_compat import Distribution, dist_requires from .exceptions import IncompatibleRequirements from .logging import log from .utils import ( @@ -69,7 +69,7 @@ def dependency_tree( dependencies.add(key) - for dep_specifier in v.requires(): + for dep_specifier in dist_requires(v): dep_name = key_from_req(dep_specifier) if dep_name in installed_keys: dep = installed_keys[dep_name] diff --git a/tests/conftest.py b/tests/conftest.py index 09750512e..3e59b86c6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,6 +19,7 @@ from pip._vendor.packaging.version import Version from pip._vendor.pkg_resources import Requirement +from piptools._compat.pip_compat import uses_pkg_resources from piptools.cache import DependencyCache from piptools.exceptions import NoCandidateFound from piptools.repositories import PyPIRepository @@ -106,6 +107,7 @@ class FakeInstalledDistribution: def __init__(self, line, deps=None): if deps is None: deps = [] + self.dep_strs = deps self.deps = [Requirement.parse(d) for d in deps] self.req = Requirement.parse(line) @@ -115,8 +117,18 @@ def __init__(self, line, deps=None): self.version = line.split("==")[1] - def requires(self): - return self.deps + # The Distribution interface has changed between pkg_resources and + # importlib.metadata. + if uses_pkg_resources: + + def requires(self): + return self.deps + + else: + + @property + def requires(self): + return self.dep_strs def pytest_collection_modifyitems(config, items):