Skip to content

Commit 1fa6ebd

Browse files
committed
Basic abstraction one actual migration
1 parent 0a174b9 commit 1fa6ebd

File tree

5 files changed

+282
-149
lines changed

5 files changed

+282
-149
lines changed
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from pip._internal.utils.compat import lru_cache
2+
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
3+
4+
from .base import BaseDistribution, BaseEnvironment
5+
6+
if MYPY_CHECK_RUNNING:
7+
from typing import Optional, Sequence, Tuple
8+
9+
10+
__all__ = ["BaseDistribution", "BaseEnvironment", "get_environment"]
11+
12+
13+
@lru_cache(maxsize=None)
14+
def _get_environment(paths):
15+
# type: (Optional[Tuple[str, ...]]) -> BaseEnvironment
16+
from .pkg_resources import Environment
17+
if paths is None:
18+
return Environment.default()
19+
return Environment.from_paths(paths)
20+
21+
22+
def get_environment(paths=None):
23+
# type: (Optional[Sequence[str]]) -> BaseEnvironment
24+
# Convert input to tuple since lru_cache() requires hashable arguments.
25+
if paths is not None:
26+
paths = tuple(paths)
27+
return _get_environment(paths)

src/pip/_internal/metadata/base.py

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import abc
2+
3+
from pip._vendor.six import add_metaclass
4+
5+
from pip._internal.utils.compat import stdlib_pkgs
6+
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
7+
8+
if MYPY_CHECK_RUNNING:
9+
from typing import Container, Iterator, Optional, Sequence
10+
11+
12+
@add_metaclass(abc.ABCMeta)
13+
class BaseDistribution(object):
14+
@property
15+
def canonical_name(self):
16+
# type: () -> str
17+
raise NotImplementedError()
18+
19+
@property
20+
def installer(self):
21+
# type: () -> str
22+
raise NotImplementedError()
23+
24+
@property
25+
def editable(self):
26+
# type: () -> bool
27+
raise NotImplementedError()
28+
29+
@property
30+
def local(self):
31+
# type: () -> bool
32+
raise NotImplementedError()
33+
34+
@property
35+
def in_usersite(self):
36+
# type: () -> bool
37+
raise NotImplementedError()
38+
39+
40+
@add_metaclass(abc.ABCMeta)
41+
class BaseEnvironment(object):
42+
"""An environment containing distributions to introspect.
43+
"""
44+
@classmethod
45+
def default(cls):
46+
# type: () -> BaseEnvironment
47+
raise NotImplementedError()
48+
49+
@classmethod
50+
def from_paths(cls, paths):
51+
# type: (Sequence[str]) -> BaseEnvironment
52+
raise NotImplementedError()
53+
54+
def iter_distributions(self):
55+
# type: () -> Iterator[BaseDistribution]
56+
raise NotImplementedError()
57+
58+
def iter_installed_distributions(
59+
self,
60+
local_only=True, # type: bool
61+
skip=stdlib_pkgs, # type: Container[str]
62+
include_editables=True, # type: bool
63+
editables_only=False, # type: bool
64+
user_only=False, # type: bool
65+
):
66+
# type: (...) -> Iterator[BaseDistribution]
67+
it = self.iter_distributions()
68+
if local_only:
69+
it = (d for d in it if d.local)
70+
if not include_editables:
71+
it = (d for d in it if not d.editable)
72+
if editables_only:
73+
it = (d for d in it if d.editable)
74+
if user_only:
75+
it = (d for d in it if d.in_usersite)
76+
return (d for d in it if d.canonical_name not in skip)
77+
78+
def get_installed_distribution(self, name):
79+
# type: (str) -> Optional[BaseDistribution]
80+
raise NotImplementedError()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from pip._vendor import pkg_resources
2+
from pip._vendor.packaging.utils import canonicalize_name
3+
4+
from pip._internal.utils.misc import (
5+
dist_in_usersite,
6+
dist_is_editable,
7+
dist_is_local,
8+
)
9+
from pip._internal.utils.packaging import get_installer
10+
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
11+
12+
from .base import BaseDistribution, BaseEnvironment
13+
14+
if MYPY_CHECK_RUNNING:
15+
from typing import Iterator, Optional, Sequence
16+
17+
18+
class Distribution(BaseDistribution):
19+
def __init__(self, dist):
20+
# type: (pkg_resources.Distribution) -> None
21+
self._dist = dist
22+
23+
@property
24+
def canonical_name(self):
25+
# type: () -> str
26+
return canonicalize_name(self._dist.project_name)
27+
28+
@property
29+
def installer(self):
30+
# type: () -> str
31+
return get_installer(self._dist)
32+
33+
@property
34+
def editable(self):
35+
# type: () -> bool
36+
return dist_is_editable(self._dist)
37+
38+
@property
39+
def local(self):
40+
# type: () -> bool
41+
return dist_is_local(self._dist)
42+
43+
@property
44+
def in_usersite(self):
45+
# type: () -> bool
46+
return dist_in_usersite(self._dist)
47+
48+
49+
class Environment(BaseEnvironment):
50+
def __init__(self, ws):
51+
# type: (pkg_resources.WorkingSet) -> None
52+
self._ws = ws
53+
54+
@classmethod
55+
def default(cls):
56+
# type: () -> BaseEnvironment
57+
return cls(pkg_resources.working_set)
58+
59+
@classmethod
60+
def from_paths(cls, paths):
61+
# type: (Sequence[str]) -> BaseEnvironment
62+
return cls(pkg_resources.WorkingSet(paths))
63+
64+
def iter_distributions(self):
65+
# type: () -> Iterator[BaseDistribution]
66+
for dist in self._ws:
67+
yield Distribution(dist)
68+
69+
def _get_installed_distribution_from_cache(self, name):
70+
# type: (str) -> Optional[BaseDistribution]
71+
name = canonicalize_name(name)
72+
for dist in self.iter_installed_distributions():
73+
if dist.canonical_name == name:
74+
return dist
75+
return None
76+
77+
def get_installed_distribution(self, name):
78+
# type: (str) -> Optional[BaseDistribution]
79+
dist = self._get_installed_distribution_from_cache(name)
80+
if dist:
81+
return dist
82+
# If distribution could not be found, call WorkingSet.require()
83+
# to update the working set, and try to find the distribution again.
84+
# This might happen for e.g. when you install a package twice, once
85+
# using setup.py develop and again using setup.py install. Now when
86+
# pip uninstall is run twice, the package gets removed from the
87+
# working set in the first uninstall, so we have to populate the
88+
# working set again so that pip knows about it and the packages gets
89+
# picked up and is successfully uninstalled the second time too.
90+
try:
91+
self._ws.require(name)
92+
except pkg_resources.DistributionNotFound:
93+
return None
94+
return self._get_installed_distribution_from_cache(name)

src/pip/_internal/utils/misc.py

+20-69
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from collections import deque
1919

2020
from pip._vendor import pkg_resources
21-
from pip._vendor.packaging.utils import canonicalize_name
2221
# NOTE: retrying is not annotated in typeshed as on 2017-07-17, which is
2322
# why we ignore the type on this import.
2423
from pip._vendor.retrying import retry # type: ignore
@@ -34,6 +33,7 @@
3433
site_packages,
3534
user_site,
3635
)
36+
from pip._internal.metadata import get_environment
3737
from pip._internal.utils.compat import (
3838
WINDOWS,
3939
expanduser,
@@ -58,6 +58,9 @@
5858
)
5959
from pip._vendor.pkg_resources import Distribution
6060

61+
from pip._internal.metadata import pkg_resources as metadata_pkg_resources
62+
63+
PipMetadataDistribution = metadata_pkg_resources.Distribution
6164
VersionInfo = Tuple[int, int, int]
6265

6366

@@ -413,6 +416,8 @@ def dist_is_editable(dist):
413416
return False
414417

415418

419+
# TODO: Refactor the call site to make it call get_environment() directly,
420+
# and operate on the wrapper PipMetadataDistribution instead.
416421
def get_installed_distributions(
417422
local_only=True, # type: bool
418423
skip=stdlib_pkgs, # type: Container[str]
@@ -441,78 +446,24 @@ def get_installed_distributions(
441446
If ``paths`` is set, only report the distributions present at the
442447
specified list of locations.
443448
"""
444-
if paths:
445-
working_set = pkg_resources.WorkingSet(paths)
446-
else:
447-
working_set = pkg_resources.working_set
448-
449-
if local_only:
450-
local_test = dist_is_local
451-
else:
452-
def local_test(d):
453-
return True
454-
455-
if include_editables:
456-
def editable_test(d):
457-
return True
458-
else:
459-
def editable_test(d):
460-
return not dist_is_editable(d)
461-
462-
if editables_only:
463-
def editables_only_test(d):
464-
return dist_is_editable(d)
465-
else:
466-
def editables_only_test(d):
467-
return True
468-
469-
if user_only:
470-
user_test = dist_in_usersite
471-
else:
472-
def user_test(d):
473-
return True
474-
475-
return [d for d in working_set
476-
if local_test(d) and
477-
d.key not in skip and
478-
editable_test(d) and
479-
editables_only_test(d) and
480-
user_test(d)
481-
]
482-
483-
484-
def search_distribution(req_name):
485-
486-
# Canonicalize the name before searching in the list of
487-
# installed distributions and also while creating the package
488-
# dictionary to get the Distribution object
489-
req_name = canonicalize_name(req_name)
490-
packages = get_installed_distributions(skip=())
491-
pkg_dict = {canonicalize_name(p.key): p for p in packages}
492-
return pkg_dict.get(req_name)
449+
iterator = get_environment(paths).iter_installed_distributions(
450+
local_only=local_only,
451+
skip=skip,
452+
include_editables=include_editables,
453+
editables_only=editables_only,
454+
user_only=user_only,
455+
)
456+
return [cast("PipMetadataDistribution", dist)._dist for dist in iterator]
493457

494458

459+
# TODO: Refactor the call site to make it call get_environment() directly,
460+
# and operate on the wrapper PipMetadataDistribution instead.
495461
def get_distribution(req_name):
496-
"""Given a requirement name, return the installed Distribution object"""
497-
498-
# Search the distribution by looking through the working set
499-
dist = search_distribution(req_name)
500-
501-
# If distribution could not be found, call working_set.require
502-
# to update the working set, and try to find the distribution
503-
# again.
504-
# This might happen for e.g. when you install a package
505-
# twice, once using setup.py develop and again using setup.py install.
506-
# Now when run pip uninstall twice, the package gets removed
507-
# from the working set in the first uninstall, so we have to populate
508-
# the working set again so that pip knows about it and the packages
509-
# gets picked up and is successfully uninstalled the second time too.
462+
dist = get_environment().get_installed_distribution(req_name)
510463
if not dist:
511-
try:
512-
pkg_resources.working_set.require(req_name)
513-
except pkg_resources.DistributionNotFound:
514-
return None
515-
return search_distribution(req_name)
464+
return None
465+
casted = cast("PipMetadataDistribution", dist)
466+
return casted._dist
516467

517468

518469
def egg_link_path(dist):

0 commit comments

Comments
 (0)