From 8d40580e76f9e8b11cea15a528c1e42935c81c12 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Tue, 19 Jan 2021 04:19:32 +0800 Subject: [PATCH] Move dist-related logic from misc into metadata --- src/pip/_internal/metadata/base.py | 13 +++ src/pip/_internal/metadata/pkg_resources.py | 34 +++++- src/pip/_internal/utils/misc.py | 120 ++++---------------- 3 files changed, 65 insertions(+), 102 deletions(-) diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index df6326d07e3..c188dcb9c1f 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -49,10 +49,12 @@ def from_paths(cls, paths): def get_distribution(self, name): # type: (str) -> Optional[BaseDistribution] + """Given a requirement name, return the installed distributions.""" raise NotImplementedError() def iter_distributions(self): # type: () -> Iterator[BaseDistribution] + """Iterate through installed distributions.""" raise NotImplementedError() def iter_installed_distributions( @@ -64,6 +66,17 @@ def iter_installed_distributions( user_only=False, # type: bool ): # type: (...) -> Iterator[BaseDistribution] + """Return a list of installed distributions. + + :param local_only: If True (default), only return installations + local to the current virtualenv, if in a virtualenv. + :param skip: An iterable of canonicalized project names to ignore; + defaults to ``stdlib_pkgs``. + :param include_editables: If False, don't report editables. + :param editables_only: If True, only report editables. + :param user_only: If True, only report installations in the user + site directory. + """ it = self.iter_distributions() if local_only: it = (d for d in it if d.local) diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index 3507b47ddc0..63c0a451c78 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -61,14 +61,42 @@ def from_paths(cls, paths): # type: (List[str]) -> BaseEnvironment return cls(pkg_resources.WorkingSet(paths)) + def _search_distribution(self, name): + """Find a distribution matching the ``name`` in the environment. + + This searches from *all* distributions available in the environment, to + match the behavior of ``pkg_resources.get_distribution()``. + """ + # type: (str) -> Optional[BaseDistribution] + canonical_name = canonicalize_name(name) + for dist in self.iter_distributions(): + if dist.canonical_name == canonical_name: + return dist + return None + def get_distribution(self, name): # type: (str) -> Optional[BaseDistribution] - req = pkg_resources.Requirement(name) + + # Search the distribution by looking through the working set. + dist = self._search_distribution(name) + if dist: + return dist + + # If distribution could not be found, call working_set.require to + # update the working set, and try to find the distribution again. + # This might happen for e.g. when you install a package twice, once + # using setup.py develop and again using setup.py install. Now when + # running pip uninstall twice, the package gets removed from the + # working set in the first uninstall, so we have to populate the + # working set again so that pip knows about it and the packages gets + # picked up and is successfully uninstalled the second time too. try: - dist = self._ws.find(req) + # We didn't pass in any version specifiers, so this can never + # raise pkg_resources.VersionConflict. + self._ws.require(name) except pkg_resources.DistributionNotFound: return None - return Distribution(dist) + return self._search_distribution(name) def iter_distributions(self): # type: () -> Iterator[BaseDistribution] diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index a3bd49b9139..0cee9156574 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -18,7 +18,6 @@ from itertools import filterfalse, tee, zip_longest from pip._vendor import pkg_resources -from pip._vendor.packaging.utils import canonicalize_name # NOTE: retrying is not annotated in typeshed as on 2017-07-17, which is # why we ignore the type on this import. @@ -402,86 +401,20 @@ def get_installed_distributions( paths=None # type: Optional[List[str]] ): # type: (...) -> List[Distribution] - """ - Return a list of installed Distribution objects. - - If ``local_only`` is True (default), only return installations - local to the current virtualenv, if in a virtualenv. - - ``skip`` argument is an iterable of lower-case project names to - ignore; defaults to stdlib_pkgs - - If ``include_editables`` is False, don't report editables. - - If ``editables_only`` is True , only report editables. - - If ``user_only`` is True , only report installations in the user - site directory. - - If ``paths`` is set, only report the distributions present at the - specified list of locations. - """ - if paths: - working_set = pkg_resources.WorkingSet(paths) - else: - working_set = pkg_resources.working_set - - if local_only: - local_test = dist_is_local - else: - def local_test(d): - return True - - if include_editables: - def editable_test(d): - return True - else: - def editable_test(d): - return not dist_is_editable(d) - - if editables_only: - def editables_only_test(d): - return dist_is_editable(d) - else: - def editables_only_test(d): - return True - - if user_only: - user_test = dist_in_usersite - else: - def user_test(d): - return True - - return [d for d in working_set - if local_test(d) and - d.key not in skip and - editable_test(d) and - editables_only_test(d) and - user_test(d) - ] - - -def _search_distribution(req_name): - # type: (str) -> Optional[Distribution] - """Find a distribution matching the ``req_name`` in the environment. - - This searches from *all* distributions available in the environment, to - match the behavior of ``pkg_resources.get_distribution()``. - """ - # Canonicalize the name before searching in the list of - # installed distributions and also while creating the package - # dictionary to get the Distribution object - req_name = canonicalize_name(req_name) - packages = get_installed_distributions( - local_only=False, - skip=(), - include_editables=True, - editables_only=False, - user_only=False, - paths=None, + """Return a list of installed Distribution objects. + + Left for compatibility until direct pkg_resources uses are refactored out. + """ + from pip._internal.metadata import get_environment + from pip._internal.metadata.pkg_resources import Distribution as _Dist + dists = get_environment(paths).iter_installed_distributions( + local_only=local_only, + skip=skip, + include_editables=include_editables, + editables_only=editables_only, + user_only=user_only, ) - pkg_dict = {canonicalize_name(p.key): p for p in packages} - return pkg_dict.get(req_name) + return [cast(_Dist, dist)._dist for dist in dists] def get_distribution(req_name): @@ -490,26 +423,15 @@ def get_distribution(req_name): This searches from *all* distributions available in the environment, to match the behavior of ``pkg_resources.get_distribution()``. - """ - # Search the distribution by looking through the working set - dist = _search_distribution(req_name) - - # If distribution could not be found, call working_set.require - # to update the working set, and try to find the distribution - # again. - # This might happen for e.g. when you install a package - # twice, once using setup.py develop and again using setup.py install. - # Now when run pip uninstall twice, the package gets removed - # from the working set in the first uninstall, so we have to populate - # the working set again so that pip knows about it and the packages - # gets picked up and is successfully uninstalled the second time too. - if not dist: - try: - pkg_resources.working_set.require(req_name) - except pkg_resources.DistributionNotFound: - return None - return _search_distribution(req_name) + Left for compatibility until direct pkg_resources uses are refactored out. + """ + from pip._internal.metadata import get_environment + from pip._internal.metadata.pkg_resources import Distribution as _Dist + dist = get_environment().get_distribution(req_name) + if dist is None: + return None + return cast(_Dist, dist)._dist def egg_link_path(dist):