Skip to content

Commit 94ca571

Browse files
committed
LocalPackageResolver: Resolve packages using _first_ match in sys.path
When LocalPackageResolver is used to look up packages in the _current_ Python environment, it should take care to use the same package that Python itself would end up using when importing a package of that name. Until now, the way we have used importlib_metadata (specifically the way we iterate over MetadataPathFinder().find_distributions()), if a packages occurs multiple times in sys.path, we would end up using the _last_ instance, whereas Python would use the _first_. Fix our iteration to be in line with Python.
1 parent 8ace2c1 commit 94ca571

File tree

2 files changed

+25
-1
lines changed

2 files changed

+25
-1
lines changed

fawltydeps/packages.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -261,15 +261,24 @@ def packages(self) -> Dict[str, Package]:
261261
# that map to zero import names.
262262
context = DistributionFinder.Context(path=paths) # type: ignore
263263
for dist in MetadataPathFinder().find_distributions(context): # type: ignore
264+
normalized_name = Package.normalize_name(dist.name)
264265
parent_dir = dist.locate_file("")
266+
if normalized_name in ret:
267+
# We already found another instance of this package earlier in
268+
# the given paths. Assume that the earlier package is what
269+
# Python's import machinery will choose, and that this later
270+
# package is not interesting
271+
logger.debug(f"Skip {dist.name} {dist.version} under {parent_dir}")
272+
continue
273+
265274
logger.debug(f"Found {dist.name} {dist.version} under {parent_dir}")
266275
imports = set(
267276
_top_level_declared(dist) # type: ignore
268277
or _top_level_inferred(dist) # type: ignore
269278
)
270279
description = self.description_template.format(path=parent_dir)
271280
package = Package(dist.name, {description: imports})
272-
ret[Package.normalize_name(dist.name)] = package
281+
ret[normalized_name] = package
273282

274283
return ret
275284

tests/test_local_env.py

+15
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,21 @@ def test_local_env__current_venv__contains_prepared_packages(isolate_default_res
121121
assert package_name in lpl.packages
122122

123123

124+
def test_local_env__prefers_first_package_found_in_sys_path(isolate_default_resolver):
125+
# Add the same package twice, The one that ends up _first_ in sys.path is
126+
# the one that Python would end up importing, and it is therefore also the
127+
# one that we should resolve to.
128+
129+
site_dir1 = isolate_default_resolver({"other": {"other"}})
130+
site_dir2 = isolate_default_resolver({"other": {"other"}})
131+
assert site_dir1 != site_dir2
132+
assert sys.path[0] == str(site_dir2)
133+
actual = LocalPackageResolver().lookup_packages({"other"})
134+
assert actual == {
135+
"other": Package("other", {f"Python env at {site_dir2}": {"other"}}),
136+
}
137+
138+
124139
def test_resolve_dependencies__in_empty_venv__reverts_to_id_mapping(tmp_path):
125140
venv.create(tmp_path, with_pip=False)
126141
id_mapping = IdentityMapping()

0 commit comments

Comments
 (0)