Skip to content

Commit 0580d66

Browse files
committed
package.UserDefinedMapping: Include source of mapping in Package objects
When accumulating a user-defined Package object from multiple files (e.g. a [tool.fawltydeps.custom_mapping] section in pyproject.toml together with a --custom-mapping-file=mapping.toml), include the ultimate source of the mapping into the .debug_info member of the Package object. This makes it easier to debug the source of these user-defined meppings. In other words, given a mapping.toml with: apache-airflow = ["airflow"] and a pyproject.toml with: [tool.fawltydeps.custom_mapping] apache-airflow = ["foo", "bar"] we used to generate this Package object: Package( package_name="apache_airflow", import_names={"airflow", "foo", "bar"}, resolved_with=UserDefinedMapping, debug_info=None, ) but with this commit we instead get: Package( package_name="apache_airflow", import_names={"airflow", "foo", "bar"}, resolved_with=UserDefinedMapping, debug_info={ "mapping.toml": {"airflow}, "from settings": {"foo", "bar"}, }, ) Also refactor the accumulate_mappings() helper out of the UserDefinedMapping class as we will soon start to use it from LocalPackageResolver as well.
1 parent 045fa2e commit 0580d66

File tree

3 files changed

+64
-45
lines changed

3 files changed

+64
-45
lines changed

fawltydeps/packages.py

+39-27
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@
88
from abc import ABC, abstractmethod
99
from contextlib import contextmanager
1010
from dataclasses import dataclass, replace
11-
from itertools import chain
1211
from pathlib import Path
13-
from typing import Dict, Iterable, Iterator, List, Optional, Set, Type, Union
12+
from typing import Dict, Iterable, Iterator, List, Optional, Set, Tuple, Type, Union
1413

1514
# importlib_metadata is gradually graduating into the importlib.metadata stdlib
1615
# module, however we rely on internal functions and recent (and upcoming)
@@ -89,6 +88,40 @@ def lookup_packages(self, package_names: Set[str]) -> Dict[str, Package]:
8988
raise NotImplementedError
9089

9190

91+
def accumulate_mappings(
92+
resolved_with: Type[BasePackageResolver],
93+
custom_mappings: Iterable[Tuple[CustomMapping, str]],
94+
) -> Dict[str, Package]:
95+
"""Merge CustomMappings (w/associated descriptions) into a dict of Packages.
96+
97+
Each resulting package object maps a (normalized) package name to a mapping
98+
dict where the provided imports are keyed by their associated description.
99+
The keys in the returned dict are also normalized package names.
100+
"""
101+
result: Dict[str, Package] = {}
102+
for custom_mapping, debug_key in custom_mappings:
103+
for name, imports in custom_mapping.items():
104+
normalized_name = Package.normalize_name(name)
105+
if normalized_name not in result: # create new Package instance
106+
result[normalized_name] = Package(
107+
package_name=normalized_name,
108+
import_names=set(imports),
109+
resolved_with=resolved_with,
110+
debug_info={debug_key: set(imports)},
111+
)
112+
else: # replace existing Package instance with "augmented" version
113+
prev = result[normalized_name]
114+
debug_info = prev.debug_info
115+
assert isinstance(debug_info, dict)
116+
debug_info.setdefault(debug_key, set()).update(imports)
117+
result[normalized_name] = replace(
118+
prev,
119+
import_names=set.union(prev.import_names, imports),
120+
debug_info=debug_info,
121+
)
122+
return result
123+
124+
92125
class UserDefinedMapping(BasePackageResolver):
93126
"""Use user-defined mapping loaded from a toml file"""
94127

@@ -107,22 +140,6 @@ def __init__(
107140
# We enumerate packages declared in the mapping _once_ and cache the result here:
108141
self._packages: Optional[Dict[str, Package]] = None
109142

110-
@staticmethod
111-
def accumulate_mappings(custom_mappings: Iterable[CustomMapping]) -> CustomMapping:
112-
"""Merge mapping dictionaries and normalise key (package) names."""
113-
result: CustomMapping = {}
114-
for name, imports in chain.from_iterable(cm.items() for cm in custom_mappings):
115-
normalised_name = Package.normalize_name(name)
116-
if normalised_name in result:
117-
logger.info(
118-
"Mapping for %s already found. Import names "
119-
"from the second mapping are appended to ones "
120-
"found in the first mapping.",
121-
normalised_name,
122-
)
123-
result.setdefault(normalised_name, []).extend(imports)
124-
return result
125-
126143
@property
127144
@calculated_once
128145
def packages(self) -> Dict[str, Package]:
@@ -138,23 +155,18 @@ def packages(self) -> Dict[str, Package]:
138155
the remainder of this object's life in _packages.
139156
"""
140157

141-
def _custom_mappings() -> Iterator[CustomMapping]:
158+
def _custom_mappings() -> Iterator[Tuple[CustomMapping, str]]:
142159
if self.custom_mapping is not None:
143160
logger.debug("Applying user-defined mapping from settings.")
144-
yield self.custom_mapping
161+
yield self.custom_mapping, "from settings"
145162

146163
if self.mapping_paths is not None:
147164
for path in self.mapping_paths:
148165
logger.debug(f"Loading user-defined mapping from {path}")
149166
with open(path, "rb") as mapping_file:
150-
yield tomllib.load(mapping_file)
167+
yield tomllib.load(mapping_file), str(path)
151168

152-
custom_mapping = self.accumulate_mappings(_custom_mappings())
153-
154-
return {
155-
name: Package(name, set(imports), UserDefinedMapping)
156-
for name, imports in custom_mapping.items()
157-
}
169+
return accumulate_mappings(self.__class__, _custom_mappings())
158170

159171
def lookup_packages(self, package_names: Set[str]) -> Dict[str, Package]:
160172
"""Convert package names to locally available Package objects."""

tests/test_packages.py

+18-9
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,12 @@ def test_package__local_env_mapping(
141141
"""
142142
],
143143
None,
144-
{"apache_airflow": {"airflow"}, "attrs": {"attr", "attrs"}},
144+
{
145+
"apache_airflow": Package(
146+
"apache_airflow", {"airflow"}, UserDefinedMapping
147+
),
148+
"attrs": Package("attrs", {"attr", "attrs"}, UserDefinedMapping),
149+
},
145150
id="well_formated_input_file__parses_correctly",
146151
),
147152
pytest.param(
@@ -157,9 +162,11 @@ def test_package__local_env_mapping(
157162
],
158163
None,
159164
{
160-
"apache_airflow": {"airflow", "baz"},
161-
"attrs": {"attr", "attrs"},
162-
"foo": {"bar"},
165+
"apache_airflow": Package(
166+
"apache_airflow", {"airflow", "baz"}, UserDefinedMapping
167+
),
168+
"attrs": Package("attrs", {"attr", "attrs"}, UserDefinedMapping),
169+
"foo": Package("foo", {"bar"}, UserDefinedMapping),
163170
},
164171
id="well_formated_input_2files__parses_correctly",
165172
),
@@ -176,9 +183,11 @@ def test_package__local_env_mapping(
176183
],
177184
{"apache-airflow": ["unicorn"]},
178185
{
179-
"apache_airflow": {"airflow", "baz", "unicorn"},
180-
"attrs": {"attr", "attrs"},
181-
"foo": {"bar"},
186+
"apache_airflow": Package(
187+
"apache_airflow", {"airflow", "baz", "unicorn"}, UserDefinedMapping
188+
),
189+
"attrs": Package("attrs", {"attr", "attrs"}, UserDefinedMapping),
190+
"foo": Package("foo", {"bar"}, UserDefinedMapping),
182191
},
183192
id="well_formated_input_2files_and_config__parses_correctly",
184193
),
@@ -199,8 +208,8 @@ def test_user_defined_mapping__well_formated_input_file__parses_correctly(
199208
udm = UserDefinedMapping(
200209
mapping_paths=custom_mapping_files, custom_mapping=custom_mapping
201210
)
202-
mapped_packages = {k: v.import_names for k, v in udm.packages.items()}
203-
assert mapped_packages == expect
211+
actual = ignore_package_debug_info(udm.packages)
212+
assert actual == expect
204213

205214

206215
def test_user_defined_mapping__input_is_no_file__raises_unparsable_path_exeption():

tests/test_resolver.py

+7-9
Original file line numberDiff line numberDiff line change
@@ -151,14 +151,12 @@ def test_resolve_dependencies__generates_expected_mappings(
151151
)
152152

153153
isolate_default_resolver(installed_deps)
154-
actual = ignore_package_debug_info(
155-
resolve_dependencies(
156-
dep_names,
157-
custom_mapping_files=set([custom_mapping_file])
158-
if custom_mapping_file
159-
else None,
160-
custom_mapping=user_config_mapping,
161-
)
154+
actual = resolve_dependencies(
155+
dep_names,
156+
custom_mapping_files=set([custom_mapping_file])
157+
if custom_mapping_file
158+
else None,
159+
custom_mapping=user_config_mapping,
162160
)
163161

164-
assert actual == expected
162+
assert ignore_package_debug_info(actual) == ignore_package_debug_info(expected)

0 commit comments

Comments
 (0)