Skip to content

Commit 8ace2c1

Browse files
committed
packages: Replace DependenciesMapping with string descriptions
The only purpose of the DependenciesMapping enum is to differentiate the various types of mappings stored inside a Package object, e.g. allowing a Package object like this to be created: Package( "foo", { DependenciesMapping.LOCAL_ENV: {"bar", "baz"}, DependenciesMapping.IDENTITY: {"foo"}, } ) However, the way we are currently structuring our resolver stack, we will never mix mappings from _different_ resolvers inside a single Package object. What we _do_ see, however, are Package objects coming from a single resolver that nonetheless contain mappings that ultimately come from different sources. This could be: - User-defined mappings spread across multiple files, where a single package gets imports contributed from different files. - Soon, we will be accepting multiple --pyenv options which could also allow a single package to appear in multiple environments, with different imports being contributed from different environments. It would be very useful for debugging if the Package.mappings dict allowed for more specific information about the source of a given mapping. By converting the keys in this dict from DependenciesMapping enum values into free-form text strings, we can allow for this extra information, consider for example the following Package objects in the future: Package( "foo", { "User-defined mapping from mappings.toml": {"foo"}, "User-defined mapping from pyproject.toml": {"bar", "baz"}, } ) or Package( "foo", { "Python env at .venv/lib/python3.7/site-packages": {"foo", "bar"}, "Python env at __pypackages__/3.10/lib": {"foo", "baz"}, } ) This commit removes the DependenciesMapping enum, and uses text strings for the various types of mappings instead: - UserDefinedMapping: USER_DEFINED -> "User-defined mapping" (to be made more precise about the mapping source in a future commit) - LocalPackageResolver: LOCAL_ENV -> "Python env at {site_packages}" ("{site_packages}" is replaced by the dir containing the package) - TemporaryPipInstallResolver: LOCAL_ENV -> "Temporarily pip-installed" - IdentityMapping: IDENTITY -> "Identity mapping"
1 parent 11ef514 commit 8ace2c1

File tree

6 files changed

+81
-67
lines changed

6 files changed

+81
-67
lines changed

fawltydeps/packages.py

+18-21
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from abc import ABC, abstractmethod
99
from contextlib import contextmanager
1010
from dataclasses import dataclass, field
11-
from enum import Enum
1211
from itertools import chain
1312
from pathlib import Path
1413
from typing import Dict, Iterable, Iterator, List, Optional, Set
@@ -37,14 +36,6 @@
3736
logger = logging.getLogger(__name__)
3837

3938

40-
class DependenciesMapping(str, Enum):
41-
"""Types of dependency and imports mapping"""
42-
43-
IDENTITY = "identity"
44-
LOCAL_ENV = "local_env"
45-
USER_DEFINED = "user_defined"
46-
47-
4839
@dataclass
4940
class Package:
5041
"""Encapsulate an installable Python package.
@@ -55,7 +46,7 @@ class Package:
5546
"""
5647

5748
package_name: str
58-
mappings: Dict[DependenciesMapping, Set[str]] = field(default_factory=dict)
49+
mappings: Dict[str, Set[str]] = field(default_factory=dict)
5950
import_names: Set[str] = field(default_factory=set)
6051

6152
def __post_init__(self) -> None:
@@ -79,15 +70,14 @@ def normalize_name(package_name: str) -> str:
7970
"""
8071
return package_name.lower().replace("-", "_")
8172

82-
def add_import_names(
83-
self, *import_names: str, mapping: DependenciesMapping
84-
) -> None:
73+
def add_import_names(self, *import_names: str, description: str) -> None:
8574
"""Add import names provided by this package.
8675
87-
Import names must be associated with a DependenciesMapping enum value,
88-
as keeping track of this is extremely helpful when debugging.
76+
Import names must be associated with a description that provides some
77+
details about its source (keeping track of this is extremely helpful
78+
when debugging).
8979
"""
90-
self.mappings.setdefault(mapping, set()).update(import_names)
80+
self.mappings.setdefault(description, set()).update(import_names)
9181
self.import_names.update(import_names)
9282

9383
def is_used(self, imported_names: Iterable[str]) -> bool:
@@ -177,7 +167,7 @@ def _custom_mappings() -> Iterator[CustomMapping]:
177167
return {
178168
name: Package(
179169
name,
180-
{DependenciesMapping.USER_DEFINED: set(imports)},
170+
{"User-defined mapping": set(imports)},
181171
)
182172
for name, imports in custom_mapping.items()
183173
}
@@ -194,7 +184,11 @@ def lookup_packages(self, package_names: Set[str]) -> Dict[str, Package]:
194184
class LocalPackageResolver(BasePackageResolver):
195185
"""Lookup imports exposed by packages installed in a Python environment."""
196186

197-
def __init__(self, pyenv_path: Optional[Path] = None) -> None:
187+
def __init__(
188+
self,
189+
pyenv_path: Optional[Path] = None,
190+
description_template: str = "Python env at {path}",
191+
) -> None:
198192
"""Lookup packages installed in the given virtualenv.
199193
200194
Default to the current python environment if `pyenv_path` is not given
@@ -203,6 +197,7 @@ def __init__(self, pyenv_path: Optional[Path] = None) -> None:
203197
Use importlib_metadata to look up the mapping between packages and their
204198
provided import names.
205199
"""
200+
self.description_template = description_template
206201
if pyenv_path is not None:
207202
self.pyenv_path = self.determine_package_dir(pyenv_path)
208203
if self.pyenv_path is None:
@@ -272,7 +267,8 @@ def packages(self) -> Dict[str, Package]:
272267
_top_level_declared(dist) # type: ignore
273268
or _top_level_inferred(dist) # type: ignore
274269
)
275-
package = Package(dist.name, {DependenciesMapping.LOCAL_ENV: imports})
270+
description = self.description_template.format(path=parent_dir)
271+
package = Package(dist.name, {description: imports})
276272
ret[Package.normalize_name(dist.name)] = package
277273

278274
return ret
@@ -338,7 +334,8 @@ def lookup_packages(self, package_names: Set[str]) -> Dict[str, Package]:
338334
"""
339335
logger.info("Installing dependencies into a new temporary Python environment.")
340336
with self.temp_installed_requirements(sorted(package_names)) as venv_dir:
341-
local_resolver = LocalPackageResolver(venv_dir)
337+
description_template = "Temporarily pip-installed"
338+
local_resolver = LocalPackageResolver(venv_dir, description_template)
342339
return local_resolver.lookup_packages(package_names)
343340

344341

@@ -354,7 +351,7 @@ def lookup_package(package_name: str) -> Package:
354351
"""Convert a package name into a Package with the same import name."""
355352
ret = Package(package_name)
356353
import_name = Package.normalize_name(package_name)
357-
ret.add_import_names(import_name, mapping=DependenciesMapping.IDENTITY)
354+
ret.add_import_names(import_name, description="Identity mapping")
358355
logger.info(
359356
f"{package_name!r} was not resolved. "
360357
f"Assuming it can be imported as {import_name!r}."

tests/test_cmdline.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ def test_check_json__simple_project__can_report_both_undeclared_and_unused(
546546
"resolved_deps": {
547547
"pandas": {
548548
"package_name": "pandas",
549-
"mappings": {"identity": ["pandas"]},
549+
"mappings": {"Identity mapping": ["pandas"]},
550550
}
551551
},
552552
"undeclared_deps": [

tests/test_local_env.py

+9-9
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import pytest
66

77
from fawltydeps.packages import (
8-
DependenciesMapping,
98
IdentityMapping,
109
LocalPackageResolver,
1110
Package,
@@ -96,13 +95,14 @@ def test_local_env__default_venv__contains_pip_and_setuptools(tmp_path):
9695
"pip": {"pip"},
9796
"setuptools": {"setuptools", "pkg_resources"},
9897
}
98+
package_dir = tmp_path / f"lib/python{major}.{minor}/site-packages"
9999
for package_name, import_names in expect.items():
100100
assert package_name in lpl.packages
101101
p = lpl.packages[package_name]
102102
assert package_name == p.package_name
103103
assert len(p.mappings) == 1
104-
assert DependenciesMapping.LOCAL_ENV in p.mappings
105-
assert import_names.issubset(p.mappings[DependenciesMapping.LOCAL_ENV])
104+
assert f"Python env at {package_dir}" in p.mappings
105+
assert import_names.issubset(p.mappings[f"Python env at {package_dir}"])
106106

107107

108108
def test_local_env__current_venv__contains_prepared_packages(isolate_default_resolver):
@@ -129,7 +129,7 @@ def test_resolve_dependencies__in_empty_venv__reverts_to_id_mapping(tmp_path):
129129

130130

131131
def test_resolve_dependencies__in_fake_venv__returns_local_and_id_deps(fake_venv):
132-
venv_dir, _ = fake_venv(
132+
venv_dir, site_packages = fake_venv(
133133
{
134134
"pip": {"pip"},
135135
"setuptools": {"setuptools", "pkg_resources"},
@@ -138,9 +138,9 @@ def test_resolve_dependencies__in_fake_venv__returns_local_and_id_deps(fake_venv
138138
)
139139
actual = resolve_dependencies(["PIP", "pandas", "empty-pkg"], pyenv_path=venv_dir)
140140
assert actual == {
141-
"PIP": Package("pip", {DependenciesMapping.LOCAL_ENV: {"pip"}}),
142-
"pandas": Package("pandas", {DependenciesMapping.IDENTITY: {"pandas"}}),
143-
"empty-pkg": Package("empty_pkg", {DependenciesMapping.LOCAL_ENV: set()}),
141+
"PIP": Package("pip", {f"Python env at {site_packages}": {"pip"}}),
142+
"pandas": Package("pandas", {"Identity mapping": {"pandas"}}),
143+
"empty-pkg": Package("empty_pkg", {f"Python env at {site_packages}": set()}),
144144
}
145145

146146

@@ -155,6 +155,6 @@ def test_on_installed_venv__returns_local_deps(request, monkeypatch):
155155
["leftpadx", "click"], pyenv_path=None, install_deps=True
156156
)
157157
assert actual == {
158-
"leftpadx": Package("leftpadx", {DependenciesMapping.LOCAL_ENV: {"leftpad"}}),
159-
"click": Package("click", {DependenciesMapping.LOCAL_ENV: {"click"}}),
158+
"leftpadx": Package("leftpadx", {"Temporarily pip-installed": {"leftpad"}}),
159+
"click": Package("click", {"Temporarily pip-installed": {"click"}}),
160160
}

tests/test_packages.py

+24-13
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import pytest
77

88
from fawltydeps.packages import (
9-
DependenciesMapping,
109
IdentityMapping,
1110
LocalPackageResolver,
1211
Package,
@@ -15,7 +14,12 @@
1514
)
1615
from fawltydeps.types import UnparseablePathException
1716

18-
from .utils import SAMPLE_PROJECTS_DIR, default_sys_path_env_for_tests, test_vectors
17+
from .utils import (
18+
SAMPLE_PROJECTS_DIR,
19+
default_sys_path_env_for_tests,
20+
expand_package_mappings_placeholders,
21+
test_vectors,
22+
)
1923

2024

2125
def test_package__empty_package__matches_nothing():
@@ -121,7 +125,7 @@ def test_package__local_env_mapping(
121125
package_name, import_names, matching_imports, non_matching_imports
122126
):
123127
p = Package(package_name)
124-
p.add_import_names(*import_names, mapping=DependenciesMapping.LOCAL_ENV)
128+
p.add_import_names(*import_names, description="Python env at /some/path")
125129
assert p.package_name == package_name # package name is not normalized
126130
assert p.is_used(matching_imports)
127131
assert not p.is_used(non_matching_imports)
@@ -131,7 +135,7 @@ def test_package__both_mappings():
131135
id_mapping = IdentityMapping()
132136
p = id_mapping.lookup_package("FooBar")
133137
import_names = ["foo", "bar", "baz"]
134-
p.add_import_names(*import_names, mapping=DependenciesMapping.LOCAL_ENV)
138+
p.add_import_names(*import_names, description="Python env at /some/path")
135139
assert p.package_name == "FooBar" # package name is not normalized
136140
assert p.is_used(["foobar"]) # but identity-mapped import name _is_.
137141
assert p.is_used(["foo"])
@@ -140,8 +144,8 @@ def test_package__both_mappings():
140144
assert not p.is_used(["fooba"])
141145
assert not p.is_used(["foobarbaz"])
142146
assert p.mappings == {
143-
DependenciesMapping.IDENTITY: {"foobar"},
144-
DependenciesMapping.LOCAL_ENV: {"foo", "bar", "baz"},
147+
"Identity mapping": {"foobar"},
148+
"Python env at /some/path": {"foo", "bar", "baz"},
145149
}
146150
assert p.import_names == {"foobar", "foo", "bar", "baz"}
147151

@@ -280,19 +284,26 @@ def test_LocalPackageResolver_lookup_packages(
280284
@pytest.mark.parametrize("vector", [pytest.param(v, id=v.id) for v in test_vectors])
281285
def test_resolve_dependencies(vector, isolate_default_resolver):
282286
dep_names = [dd.name for dd in vector.declared_deps]
283-
isolate_default_resolver(default_sys_path_env_for_tests)
284-
assert resolve_dependencies(dep_names) == vector.expect_resolved_deps
287+
site_packages = isolate_default_resolver(default_sys_path_env_for_tests)
288+
expected = expand_package_mappings_placeholders(
289+
vector.expect_resolved_deps,
290+
site_packages=site_packages,
291+
)
292+
assert resolve_dependencies(dep_names) == expected
285293

286294

287295
def test_resolve_dependencies__informs_once_when_id_mapping_is_used(
288296
caplog, isolate_default_resolver
289297
):
290298
dep_names = ["some-foo", "pip", "some-foo"]
291-
isolate_default_resolver(default_sys_path_env_for_tests)
292-
expect = {
293-
"pip": Package("pip", {DependenciesMapping.LOCAL_ENV: {"pip"}}),
294-
"some-foo": Package("some-foo", {DependenciesMapping.IDENTITY: {"some_foo"}}),
295-
}
299+
site_packages = isolate_default_resolver(default_sys_path_env_for_tests)
300+
expect = expand_package_mappings_placeholders(
301+
{
302+
"pip": Package("pip", {"Python env at {site_packages}": {"pip"}}),
303+
"some-foo": Package("some-foo", {"Identity mapping": {"some_foo"}}),
304+
},
305+
site_packages=site_packages,
306+
)
296307
expect_log = [
297308
(
298309
"fawltydeps.packages",

tests/test_resolver.py

+10-17
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,9 @@
66
from hypothesis import HealthCheck, given, settings
77
from hypothesis import strategies as st
88

9-
from fawltydeps.packages import (
10-
DependenciesMapping,
11-
Package,
12-
UserDefinedMapping,
13-
resolve_dependencies,
14-
)
9+
from fawltydeps.packages import Package, UserDefinedMapping, resolve_dependencies
1510

16-
from .utils import default_sys_path_env_for_tests
11+
from .utils import default_sys_path_env_for_tests, expand_package_mappings_placeholders
1712

1813
# The deps in each category should be disjoint
1914
other_deps = ["pandas", "numpy", "other"]
@@ -69,7 +64,7 @@ def generate_expected_resolved_deps(
6964
user_defined_deps: Optional[List[str]] = None,
7065
user_mapping_file: Optional[Path] = None,
7166
user_mapping_from_config: Optional[Dict[str, List[str]]] = None,
72-
):
67+
) -> Dict[str, Package]:
7368
"""
7469
Returns a dict of resolved packages.
7570
@@ -81,10 +76,7 @@ def generate_expected_resolved_deps(
8176
if locally_installed_deps:
8277
ret.update(
8378
{
84-
dep: Package(
85-
Package.normalize_name(dep),
86-
{DependenciesMapping.LOCAL_ENV: set(imports)},
87-
)
79+
dep: Package(dep, {"Python env at {site_packages}": set(imports)})
8880
for dep, imports in locally_installed_deps.items()
8981
}
9082
)
@@ -97,10 +89,7 @@ def generate_expected_resolved_deps(
9789
ret.update(resolved_packages)
9890
if other_deps:
9991
ret.update(
100-
{
101-
dep: Package(dep, {DependenciesMapping.IDENTITY: {dep}})
102-
for dep in other_deps
103-
}
92+
{dep: Package(dep, {"Identity mapping": {dep}}) for dep in other_deps}
10493
)
10594
return ret
10695

@@ -157,7 +146,11 @@ def test_resolve_dependencies__generates_expected_mappings(
157146
other_deps=other_deps,
158147
)
159148

160-
isolate_default_resolver(installed_deps)
149+
site_packages = isolate_default_resolver(installed_deps)
150+
expected = expand_package_mappings_placeholders(
151+
expected,
152+
site_packages=site_packages,
153+
)
161154
obtained = resolve_dependencies(
162155
dep_names,
163156
custom_mapping_files=set([custom_mapping_file])

tests/utils.py

+19-6
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
import io
44
import logging
55
import subprocess
6-
from dataclasses import dataclass, field
6+
from dataclasses import dataclass, field, replace
77
from pathlib import Path
88
from typing import Any, Dict, Iterable, List, Optional, Tuple
99

1010
from fawltydeps.main import main
11-
from fawltydeps.packages import DependenciesMapping, Package
11+
from fawltydeps.packages import Package
1212
from fawltydeps.types import (
1313
DeclaredDependency,
1414
Location,
@@ -54,13 +54,26 @@ def resolved_factory(*deps: str) -> Dict[str, Package]:
5454
def make_package(dep: str) -> Package:
5555
imports = default_sys_path_env_for_tests.get(dep, None)
5656
if imports is not None: # exists in local env
57-
return Package(dep, {DependenciesMapping.LOCAL_ENV: imports})
57+
return Package(dep, {"Python env at {site_packages}": imports})
5858
# fall back to identity mapping
59-
return Package(dep, {DependenciesMapping.IDENTITY: {dep}})
59+
return Package(dep, {"Identity mapping": {dep}})
6060

6161
return {dep: make_package(dep) for dep in deps}
6262

6363

64+
def expand_package_mappings_placeholders(
65+
resolved_deps: Dict[str, Package], **kwargs
66+
) -> Dict[str, Package]:
67+
def expand_one(package: Package) -> Package:
68+
mappings = {
69+
description.format(**kwargs): imports
70+
for description, imports in package.mappings.items()
71+
}
72+
return replace(package, mappings=mappings)
73+
74+
return {name: expand_one(package) for name, package in resolved_deps.items()}
75+
76+
6477
def undeclared_factory(*deps: str) -> List[UndeclaredDependency]:
6578
return [UndeclaredDependency(dep, [Location("<stdin>")]) for dep in deps]
6679

@@ -251,8 +264,8 @@ class FDTestVector: # pylint: disable=too-many-instance-attributes
251264
DeclaredDependency(name="pip", source=Location(Path("requirements2.txt"))),
252265
],
253266
expect_resolved_deps={
254-
"Pip": Package("pip", {DependenciesMapping.LOCAL_ENV: {"pip"}}),
255-
"pip": Package("pip", {DependenciesMapping.LOCAL_ENV: {"pip"}}),
267+
"Pip": Package("pip", {"Python env at {site_packages}": {"pip"}}),
268+
"pip": Package("pip", {"Python env at {site_packages}": {"pip"}}),
256269
},
257270
expect_unused_deps=[
258271
UnusedDependency("Pip", [Location(Path("requirements1.txt"))]),

0 commit comments

Comments
 (0)