From d17b6891e009b21254c0f92eef7d48e184a3f8f3 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Thu, 22 Jul 2021 15:55:20 +0800 Subject: [PATCH] Remove get_installed_distributions usages The function itself is kept for now because it's currently used to test the pip.metadata subpackage... --- src/pip/_internal/cli/autocompletion.py | 10 ++-- src/pip/_internal/commands/list.py | 2 +- src/pip/_internal/metadata/base.py | 8 ++- src/pip/_internal/metadata/pkg_resources.py | 19 +++++- src/pip/_internal/operations/check.py | 58 ++++++++++--------- .../resolution/resolvelib/factory.py | 34 +++++------ .../resolution/resolvelib/resolver.py | 6 +- tests/unit/test_check.py | 26 --------- tests/unit/test_metadata.py | 2 +- 9 files changed, 79 insertions(+), 86 deletions(-) delete mode 100644 tests/unit/test_check.py diff --git a/src/pip/_internal/cli/autocompletion.py b/src/pip/_internal/cli/autocompletion.py index 2018ba2d463..6cf78852b9d 100644 --- a/src/pip/_internal/cli/autocompletion.py +++ b/src/pip/_internal/cli/autocompletion.py @@ -9,7 +9,7 @@ from pip._internal.cli.main_parser import create_main_parser from pip._internal.commands import commands_dict, create_command -from pip._internal.utils.misc import get_installed_distributions +from pip._internal.metadata import get_default_environment def autocomplete() -> None: @@ -45,11 +45,13 @@ def autocomplete() -> None: "uninstall", ] if should_list_installed: + env = get_default_environment() lc = current.lower() installed = [ - dist.key - for dist in get_installed_distributions(local_only=True) - if dist.key.startswith(lc) and dist.key not in cwords[1:] + dist.canonical_name + for dist in env.iter_installed_distributions(local_only=True) + if dist.canonical_name.startswith(lc) + and dist.canonical_name not in cwords[1:] ] # if there are no dists installed, fall back to option completion if installed: diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index db886dacdb5..d32df10123d 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -199,7 +199,7 @@ def get_not_required(self, packages, options): dep_keys = { canonicalize_name(dep.name) for dist in packages - for dep in dist.iter_dependencies() + for dep in (dist.iter_dependencies() or ()) } # Create a set to remove duplicate packages, and cast it to a list diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index 38c8bdab8fd..b9c432d7b93 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -25,6 +25,8 @@ if TYPE_CHECKING: from typing import Protocol + + from pip._vendor.packaging.utils import NormalizedName else: Protocol = object @@ -59,7 +61,7 @@ def location(self) -> Optional[str]: raise NotImplementedError() @property - def canonical_name(self) -> str: + def canonical_name(self) -> "NormalizedName": raise NotImplementedError() @property @@ -108,6 +110,10 @@ def local(self) -> bool: def in_usersite(self) -> bool: raise NotImplementedError() + @property + def in_site_packages(self) -> bool: + raise NotImplementedError() + def read_text(self, name: str) -> str: """Read a file in the .dist-info (or .egg-info) directory. diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index 86a936abc0a..0dab7e989a3 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -1,7 +1,15 @@ import email.message import logging import zipfile -from typing import Collection, Iterable, Iterator, List, NamedTuple, Optional +from typing import ( + TYPE_CHECKING, + Collection, + Iterable, + Iterator, + List, + NamedTuple, + Optional, +) from pip._vendor import pkg_resources from pip._vendor.packaging.requirements import Requirement @@ -14,6 +22,9 @@ from .base import BaseDistribution, BaseEntryPoint, BaseEnvironment, DistributionVersion +if TYPE_CHECKING: + from pip._vendor.packaging.utils import NormalizedName + logger = logging.getLogger(__name__) @@ -38,7 +49,7 @@ def location(self) -> Optional[str]: return self._dist.location @property - def canonical_name(self) -> str: + def canonical_name(self) -> "NormalizedName": return canonicalize_name(self._dist.project_name) @property @@ -61,6 +72,10 @@ def local(self) -> bool: def in_usersite(self) -> bool: return misc.dist_in_usersite(self._dist) + @property + def in_site_packages(self) -> bool: + return misc.dist_in_site_packages(self._dist) + def read_text(self, name: str) -> str: if not self._dist.has_metadata(name): raise FileNotFoundError(name) diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index 5699c0b91ee..ea9c8a712c8 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -2,49 +2,50 @@ """ import logging -from collections import namedtuple -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Callable, Dict, List, NamedTuple, Optional, Set, Tuple +from pip._vendor.packaging.requirements import Requirement from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.pkg_resources import RequirementParseError from pip._internal.distributions import make_distribution_for_install_requirement +from pip._internal.metadata import get_default_environment +from pip._internal.metadata.base import DistributionVersion 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__) + +class PackageDetails(NamedTuple): + version: DistributionVersion + dependencies: List[Requirement] + + # Shorthands -PackageSet = Dict['NormalizedName', 'PackageDetails'] -Missing = Tuple[str, Any] -Conflicting = Tuple[str, str, Any] +PackageSet = Dict['NormalizedName', PackageDetails] +Missing = Tuple['NormalizedName', Requirement] +Conflicting = Tuple['NormalizedName', DistributionVersion, Requirement] 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: Any) -> Tuple["PackageSet", bool]: - """Converts a list of distributions into a PackageSet. - """ - # Default to using all packages installed on the system - if kwargs == {}: - kwargs = {"local_only": False, "skip": ()} +def create_package_set_from_installed() -> Tuple[PackageSet, bool]: + """Converts a list of distributions into a PackageSet.""" package_set = {} problems = False - for dist in get_installed_distributions(**kwargs): - name = canonicalize_name(dist.project_name) + env = get_default_environment() + for dist in env.iter_installed_distributions(local_only=False, skip=()): + name = dist.canonical_name try: - package_set[name] = PackageDetails(dist.version, dist.requires()) - except (OSError, RequirementParseError) as e: - # Don't crash on unreadable or broken metadata + dependencies = list(dist.iter_dependencies()) + package_set[name] = PackageDetails(dist.version, dependencies) + except (OSError, ValueError) as e: + # Don't crash on unreadable or broken metadata. logger.warning("Error parsing requirements for %s: %s", name, e) problems = True return package_set, problems @@ -69,8 +70,8 @@ def check_package_set(package_set, should_ignore=None): if should_ignore and should_ignore(package_name): continue - for req in package_detail.requires: - name = canonicalize_name(req.project_name) + for req in package_detail.dependencies: + name = canonicalize_name(req.name) # Check if it's missing if name not in package_set: @@ -82,7 +83,7 @@ def check_package_set(package_set, should_ignore=None): continue # Check if there's a conflict - version = package_set[name].version # type: str + version = package_set[name].version if not req.specifier.contains(version, prereleases=True): conflicting_deps.add((name, version, req)) @@ -129,8 +130,11 @@ def _simulate_installation_of(to_install, package_set): dist = abstract_dist.get_pkg_resources_distribution() assert dist is not None - name = canonicalize_name(dist.key) - package_set[name] = PackageDetails(dist.version, dist.requires()) + name = dist.canonical_name + package_set[name] = PackageDetails( + dist.version, + list(dist.iter_dependencies()), + ) installed.add(name) @@ -145,7 +149,7 @@ def _create_whitelist(would_be_installed, package_set): if package_name in packages_affected: continue - for req in package_set[package_name].requires: + for req in package_set[package_name].dependencies: if canonicalize_name(req.name) in packages_affected: packages_affected.add(package_name) break diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index f1b750e252a..cde51b7d6e0 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -22,7 +22,6 @@ from pip._vendor.packaging.requirements import Requirement as PackagingRequirement from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.utils import NormalizedName, canonicalize_name -from pip._vendor.pkg_resources import Distribution from pip._vendor.resolvelib import ResolutionImpossible from pip._internal.cache import CacheEntry, WheelCache @@ -35,6 +34,7 @@ UnsupportedWheel, ) from pip._internal.index.package_finder import PackageFinder +from pip._internal.metadata import BaseDistribution, get_default_environment from pip._internal.models.link import Link from pip._internal.models.wheel import Wheel from pip._internal.operations.prepare import RequirementPreparer @@ -46,11 +46,6 @@ from pip._internal.resolution.base import InstallRequirementProvider from pip._internal.utils.compatibility_tags import get_supported from pip._internal.utils.hashes import Hashes -from pip._internal.utils.misc import ( - dist_in_site_packages, - dist_in_usersite, - get_installed_distributions, -) from pip._internal.utils.virtualenv import running_under_virtualenv from .base import Candidate, CandidateVersion, Constraint, Requirement @@ -122,9 +117,10 @@ def __init__( ] = {} if not ignore_installed: + env = get_default_environment() self._installed_dists = { - canonicalize_name(dist.project_name): dist - for dist in get_installed_distributions(local_only=False) + dist.canonical_name: dist + for dist in env.iter_installed_distributions(local_only=False) } else: self._installed_dists = {} @@ -155,15 +151,15 @@ def _make_extras_candidate( def _make_candidate_from_dist( self, - dist: Distribution, + dist: BaseDistribution, extras: FrozenSet[str], template: InstallRequirement, ) -> Candidate: try: - base = self._installed_candidate_cache[dist.key] + base = self._installed_candidate_cache[dist.canonical_name] except KeyError: base = AlreadyInstalledCandidate(dist, template, factory=self) - self._installed_candidate_cache[dist.key] = base + self._installed_candidate_cache[dist.canonical_name] = base if not extras: return base return self._make_extras_candidate(base, extras) @@ -520,7 +516,7 @@ def get_wheel_cache_entry( supported_tags=get_supported(), ) - def get_dist_to_uninstall(self, candidate: Candidate) -> Optional[Distribution]: + def get_dist_to_uninstall(self, candidate: Candidate) -> Optional[BaseDistribution]: # TODO: Are there more cases this needs to return True? Editable? dist = self._installed_dists.get(candidate.project_name) if dist is None: # Not installed, no uninstallation required. @@ -533,21 +529,19 @@ def get_dist_to_uninstall(self, candidate: Candidate) -> Optional[Distribution]: return dist # We're installing into user site. Remove the user site installation. - if dist_in_usersite(dist): + if dist.in_usersite: return dist # We're installing into user site, but the installed incompatible # package is in global site. We can't uninstall that, and would let # the new user installation to "shadow" it. But shadowing won't work # in virtual environments, so we error out. - if running_under_virtualenv() and dist_in_site_packages(dist): - raise InstallationError( - "Will not install to the user site because it will " - "lack sys.path precedence to {} in {}".format( - dist.project_name, - dist.location, - ) + if running_under_virtualenv() and dist.in_site_packages: + message = ( + f"Will not install to the user site because it will lack " + f"sys.path precedence to {dist.raw_name} in {dist.location}" ) + raise InstallationError(message) return None def _report_requires_python_error( diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 4648052d454..6de6636a0d7 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, cast from pip._vendor.packaging.utils import canonicalize_name -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.structs import DirectedGraph @@ -22,7 +21,6 @@ ) from pip._internal.utils.deprecation import deprecated from pip._internal.utils.filetypes import is_archive_file -from pip._internal.utils.misc import dist_is_editable from .base import Candidate, Requirement from .factory import Factory @@ -119,10 +117,10 @@ def resolve( elif self.factory.force_reinstall: # The --force-reinstall flag is set -- reinstall. ireq.should_reinstall = True - elif parse_version(installed_dist.version) != candidate.version: + elif 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): + elif candidate.is_editable or installed_dist.editable: # The incoming distribution is editable, or different in # editable-ness to installation -- reinstall. ireq.should_reinstall = True diff --git a/tests/unit/test_check.py b/tests/unit/test_check.py deleted file mode 100644 index 5f1c6d119d0..00000000000 --- a/tests/unit/test_check.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Unit Tests for pip's dependency checking logic -""" - -from unittest import mock - -from pip._internal.operations import check - - -class TestInstalledDistributionsCall: - - def test_passes_correct_default_kwargs(self, monkeypatch): - my_mock = mock.MagicMock(return_value=[]) - monkeypatch.setattr(check, "get_installed_distributions", my_mock) - - check.create_package_set_from_installed() - - my_mock.assert_called_with(local_only=False, skip=()) - - def test_passes_any_given_kwargs(self, monkeypatch): - my_mock = mock.MagicMock(return_value=[]) - monkeypatch.setattr(check, "get_installed_distributions", my_mock) - - obj = object() - check.create_package_set_from_installed(hi=obj) - - my_mock.assert_called_with(hi=obj) diff --git a/tests/unit/test_metadata.py b/tests/unit/test_metadata.py index a87a717c137..325ce009411 100644 --- a/tests/unit/test_metadata.py +++ b/tests/unit/test_metadata.py @@ -1,8 +1,8 @@ import logging from unittest.mock import patch -from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, ArchiveInfo from pip._internal.metadata import BaseDistribution +from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, ArchiveInfo @patch.object(BaseDistribution, "read_text", side_effect=FileNotFoundError)