Skip to content

Commit 4c7bbdb

Browse files
authored
Merge pull request #8702 from uranusjr/get-distribution-looks-for-all
2 parents 2e4d748 + 4fce2ea commit 4c7bbdb

File tree

4 files changed

+86
-19
lines changed

4 files changed

+86
-19
lines changed

news/8695.bugfix

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix regression that distributions in system site-packages are not correctly
2+
found when a virtual environment is configured with ``system-site-packages``
3+
on.

src/pip/_internal/commands/search.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ def print_results(hits, name_column_width=None, terminal_width=None):
140140
write_output(line)
141141
if name in installed_packages:
142142
dist = get_distribution(name)
143+
assert dist is not None
143144
with indent_log():
144145
if dist.version == latest:
145146
write_output('INSTALLED: %s (latest)', dist.version)

src/pip/_internal/utils/misc.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -483,22 +483,39 @@ def user_test(d):
483483
]
484484

485485

486-
def search_distribution(req_name):
486+
def _search_distribution(req_name):
487+
# type: (str) -> Optional[Distribution]
488+
"""Find a distribution matching the ``req_name`` in the environment.
487489
490+
This searches from *all* distributions available in the environment, to
491+
match the behavior of ``pkg_resources.get_distribution()``.
492+
"""
488493
# Canonicalize the name before searching in the list of
489494
# installed distributions and also while creating the package
490495
# dictionary to get the Distribution object
491496
req_name = canonicalize_name(req_name)
492-
packages = get_installed_distributions(skip=())
497+
packages = get_installed_distributions(
498+
local_only=False,
499+
skip=(),
500+
include_editables=True,
501+
editables_only=False,
502+
user_only=False,
503+
paths=None,
504+
)
493505
pkg_dict = {canonicalize_name(p.key): p for p in packages}
494506
return pkg_dict.get(req_name)
495507

496508

497509
def get_distribution(req_name):
498-
"""Given a requirement name, return the installed Distribution object"""
510+
# type: (str) -> Optional[Distribution]
511+
"""Given a requirement name, return the installed Distribution object.
512+
513+
This searches from *all* distributions available in the environment, to
514+
match the behavior of ``pkg_resources.get_distribution()``.
515+
"""
499516

500517
# Search the distribution by looking through the working set
501-
dist = search_distribution(req_name)
518+
dist = _search_distribution(req_name)
502519

503520
# If distribution could not be found, call working_set.require
504521
# to update the working set, and try to find the distribution
@@ -514,7 +531,7 @@ def get_distribution(req_name):
514531
pkg_resources.working_set.require(req_name)
515532
except pkg_resources.DistributionNotFound:
516533
return None
517-
return search_distribution(req_name)
534+
return _search_distribution(req_name)
518535

519536

520537
def egg_link_path(dist):

tests/unit/test_utils.py

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
66
"""
77
import codecs
8+
import itertools
89
import os
910
import shutil
1011
import stat
@@ -34,6 +35,7 @@
3435
build_url_from_netloc,
3536
egg_link_path,
3637
format_size,
38+
get_distribution,
3739
get_installed_distributions,
3840
get_prog,
3941
hide_url,
@@ -192,26 +194,30 @@ def test_noegglink_in_sitepkgs_venv_global(self):
192194
@patch('pip._internal.utils.misc.dist_in_usersite')
193195
@patch('pip._internal.utils.misc.dist_is_local')
194196
@patch('pip._internal.utils.misc.dist_is_editable')
195-
class Tests_get_installed_distributions:
196-
"""test util.get_installed_distributions"""
197-
198-
workingset = [
199-
Mock(test_name="global"),
200-
Mock(test_name="editable"),
201-
Mock(test_name="normal"),
202-
Mock(test_name="user"),
203-
]
204-
205-
workingset_stdlib = [
197+
class TestsGetDistributions(object):
198+
"""Test get_installed_distributions() and get_distribution().
199+
"""
200+
class MockWorkingSet(list):
201+
def require(self, name):
202+
pass
203+
204+
workingset = MockWorkingSet((
205+
Mock(test_name="global", key="global"),
206+
Mock(test_name="editable", key="editable"),
207+
Mock(test_name="normal", key="normal"),
208+
Mock(test_name="user", key="user"),
209+
))
210+
211+
workingset_stdlib = MockWorkingSet((
206212
Mock(test_name='normal', key='argparse'),
207213
Mock(test_name='normal', key='wsgiref')
208-
]
214+
))
209215

210-
workingset_freeze = [
216+
workingset_freeze = MockWorkingSet((
211217
Mock(test_name='normal', key='pip'),
212218
Mock(test_name='normal', key='setuptools'),
213219
Mock(test_name='normal', key='distribute')
214-
]
220+
))
215221

216222
def dist_is_editable(self, dist):
217223
return dist.test_name == "editable"
@@ -287,6 +293,46 @@ def test_freeze_excludes(self, mock_dist_is_editable,
287293
skip=('setuptools', 'pip', 'distribute'))
288294
assert len(dists) == 0
289295

296+
@pytest.mark.parametrize(
297+
"working_set, req_name",
298+
itertools.chain(
299+
itertools.product([workingset], (d.key for d in workingset)),
300+
itertools.product(
301+
[workingset_stdlib], (d.key for d in workingset_stdlib),
302+
),
303+
),
304+
)
305+
def test_get_distribution(
306+
self,
307+
mock_dist_is_editable,
308+
mock_dist_is_local,
309+
mock_dist_in_usersite,
310+
working_set,
311+
req_name,
312+
):
313+
"""Ensure get_distribution() finds all kinds of distributions.
314+
"""
315+
mock_dist_is_editable.side_effect = self.dist_is_editable
316+
mock_dist_is_local.side_effect = self.dist_is_local
317+
mock_dist_in_usersite.side_effect = self.dist_in_usersite
318+
with patch("pip._vendor.pkg_resources.working_set", working_set):
319+
dist = get_distribution(req_name)
320+
assert dist is not None
321+
assert dist.key == req_name
322+
323+
@patch('pip._vendor.pkg_resources.working_set', workingset)
324+
def test_get_distribution_nonexist(
325+
self,
326+
mock_dist_is_editable,
327+
mock_dist_is_local,
328+
mock_dist_in_usersite,
329+
):
330+
mock_dist_is_editable.side_effect = self.dist_is_editable
331+
mock_dist_is_local.side_effect = self.dist_is_local
332+
mock_dist_in_usersite.side_effect = self.dist_in_usersite
333+
dist = get_distribution("non-exist")
334+
assert dist is None
335+
290336

291337
def test_rmtree_errorhandler_nonexistent_directory(tmpdir):
292338
"""

0 commit comments

Comments
 (0)