Skip to content

Commit 3f1cdd3

Browse files
committed
Basic abstraction one actual migration
1 parent ec89cad commit 3f1cdd3

File tree

7 files changed

+281
-161
lines changed

7 files changed

+281
-161
lines changed
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from pip._internal.utils.compat import lru_cache
2+
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
3+
4+
if MYPY_CHECK_RUNNING:
5+
from typing import List, Optional
6+
7+
from .base import BaseEnvironment
8+
9+
10+
@lru_cache(maxsize=None)
11+
def get_environment(paths=None):
12+
# type: (Optional[List[str]]) -> BaseEnvironment
13+
from .pkg_resources import Environment
14+
if paths is None:
15+
return Environment.default()
16+
return Environment.from_paths(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.misc import stdlib_pkgs # TODO: Move definition here.
6+
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
7+
8+
if MYPY_CHECK_RUNNING:
9+
from typing import Container, Iterator, List, Optional
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: (List[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, List, Optional
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: (List[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/self_outdated_check.py

+4-10
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,14 @@
1212

1313
from pip._internal.index.collector import LinkCollector
1414
from pip._internal.index.package_finder import PackageFinder
15+
from pip._internal.metadata import get_environment
1516
from pip._internal.models.selection_prefs import SelectionPreferences
1617
from pip._internal.utils.filesystem import (
1718
adjacent_tmp_file,
1819
check_path_owner,
1920
replace,
2021
)
21-
from pip._internal.utils.misc import (
22-
ensure_dir,
23-
get_distribution,
24-
get_installed_version,
25-
)
26-
from pip._internal.utils.packaging import get_installer
22+
from pip._internal.utils.misc import ensure_dir, get_installed_version
2723
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
2824

2925
if MYPY_CHECK_RUNNING:
@@ -113,10 +109,8 @@ def was_installed_by_pip(pkg):
113109
This is used not to display the upgrade message when pip is in fact
114110
installed by system package manager, such as dnf on Fedora.
115111
"""
116-
dist = get_distribution(pkg)
117-
if not dist:
118-
return False
119-
return "pip" == get_installer(dist)
112+
dist = get_environment().get_installed_distribution(pkg)
113+
return dist is not None and "pip" == dist.installer
120114

121115

122116
def pip_self_version_check(session, options):

src/pip/_internal/utils/misc.py

+16-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

@@ -441,78 +444,22 @@ def get_installed_distributions(
441444
If ``paths`` is set, only report the distributions present at the
442445
specified list of locations.
443446
"""
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)
447+
iterator = get_environment(paths).iter_installed_distributions(
448+
local_only=local_only,
449+
skip=skip,
450+
include_editables=include_editables,
451+
editables_only=editables_only,
452+
user_only=user_only,
453+
)
454+
return [cast("PipMetadataDistribution", dist)._dist for dist in iterator]
493455

494456

495457
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.
458+
dist = get_environment().get_installed_distribution(req_name)
510459
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)
460+
return None
461+
casted = cast("PipMetadataDistribution", dist)
462+
return casted._dist
516463

517464

518465
def egg_link_path(dist):

tests/unit/test_self_check_outdated.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ def get_metadata_lines(self, name):
5757
raise NotImplementedError('nope')
5858

5959

60+
class MockEnvironment(object):
61+
def __init__(self, installer):
62+
self.installer = installer
63+
64+
def get_installed_distribution(self, name):
65+
return MockDistribution(self.installer)
66+
67+
6068
def _options():
6169
''' Some default options that we pass to
6270
self_outdated_check.pip_self_version_check '''
@@ -97,8 +105,8 @@ def test_pip_self_version_check(monkeypatch, stored_time, installed_ver,
97105
pretend.call_recorder(lambda *a, **kw: None))
98106
monkeypatch.setattr(logger, 'debug',
99107
pretend.call_recorder(lambda s, exc_info=None: None))
100-
monkeypatch.setattr(self_outdated_check, 'get_distribution',
101-
lambda name: MockDistribution(installer))
108+
monkeypatch.setattr(self_outdated_check, 'get_environment',
109+
lambda: MockEnvironment(installer))
102110

103111
fake_state = pretend.stub(
104112
state={"last_check": stored_time, 'pypi_version': installed_ver},

0 commit comments

Comments
 (0)