Skip to content

Commit adec5e8

Browse files
committed
refactor(dependency): use get_packages_from_distribution
1 parent 3301a3a commit adec5e8

2 files changed

Lines changed: 15 additions & 126 deletions

File tree

python/deptry/dependency.py

Lines changed: 6 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
from __future__ import annotations
22

33
import logging
4-
import re
5-
from contextlib import suppress
64
from typing import TYPE_CHECKING
75

8-
from deptry.compat import importlib_metadata
6+
from deptry.distribution import get_packages_from_distribution
97

108
if TYPE_CHECKING:
119
from collections.abc import Sequence
12-
from importlib.metadata import Distribution
1310
from pathlib import Path
1411

1512

@@ -22,7 +19,6 @@ class Dependency:
2219
name (str): The name of the dependency.
2320
definition_file (Path): The path to the file defining the dependency, e.g. 'pyproject.toml'.
2421
and that can be used to create a variant of the package with a set of extra functionalities.
25-
found (bool): Indicates if the dependency has been found in the environment.
2622
top_levels (set[str]): The top-level module names associated with the dependency.
2723
"""
2824

@@ -32,16 +28,11 @@ def __init__(
3228
definition_file: Path,
3329
module_names: Sequence[str] | None = None,
3430
) -> None:
35-
distribution = self.find_distribution(name)
36-
3731
self.name = name
3832
self.definition_file = definition_file
39-
self.found = distribution is not None
40-
self.top_levels = self._get_top_levels(name, distribution, module_names)
33+
self.top_levels = self._get_top_levels(name, module_names)
4134

42-
def _get_top_levels(
43-
self, name: str, distribution: Distribution | None, module_names: Sequence[str] | None
44-
) -> set[str]:
35+
def _get_top_levels(self, name: str, module_names: Sequence[str] | None) -> set[str]:
4536
"""
4637
Get the top-level module names for a dependency. They are searched for in the following order:
4738
1. If `module_names` is defined, simply use those as the top-level modules.
@@ -50,22 +41,16 @@ def _get_top_levels(
5041
5142
Args:
5243
name: The name of the dependency.
53-
distribution: The metadata distribution of the package.
5444
module_names: If this is given, use these as the top-level modules instead of
5545
searching for them in the metadata.
5646
"""
5747
if module_names is not None:
5848
return set(module_names)
5949

60-
if distribution is not None:
61-
with suppress(FileNotFoundError):
62-
return self._get_top_level_module_names_from_top_level_txt(distribution)
63-
64-
with suppress(FileNotFoundError):
65-
return self._get_top_level_module_names_from_record_file(distribution)
50+
if distributions := get_packages_from_distribution(self.name):
51+
return distributions
6652

67-
# No metadata or other configuration has been found. As a fallback
68-
# we'll guess the name.
53+
# No metadata or other configuration has been found. As a fallback we'll guess the name.
6954
module_name = name.replace("-", "_").lower()
7055
logging.warning(
7156
"Assuming the corresponding module name of package %r is %r. Install the package or configure a"
@@ -80,56 +65,3 @@ def __repr__(self) -> str:
8065

8166
def __str__(self) -> str:
8267
return f"Dependency '{self.name}' with top-levels: {self.top_levels}."
83-
84-
@staticmethod
85-
def find_distribution(name: str) -> Distribution | None:
86-
try:
87-
return importlib_metadata.distribution(name)
88-
except importlib_metadata.PackageNotFoundError:
89-
return None
90-
91-
@staticmethod
92-
def _get_top_level_module_names_from_top_level_txt(distribution: Distribution) -> set[str]:
93-
"""
94-
top-level.txt is a metadata file added by setuptools that looks as follows:
95-
96-
610faff656c4cfcbb4a3__mypyc
97-
_black_version
98-
black
99-
blackd
100-
blib2to3
101-
102-
This function extracts these names, if a top-level.txt file exists.
103-
"""
104-
metadata_top_levels = distribution.read_text("top_level.txt")
105-
if metadata_top_levels is None:
106-
raise FileNotFoundError("top_level.txt")
107-
108-
return {x for x in metadata_top_levels.splitlines() if x}
109-
110-
@staticmethod
111-
def _get_top_level_module_names_from_record_file(distribution: Distribution) -> set[str]:
112-
"""
113-
Get the top-level module names from the RECORD file, whose contents usually look as follows:
114-
115-
...
116-
../../../bin/black,sha256=<HASH>,247
117-
__pycache__/_black_version.cpython-311.pyc,,
118-
_black_version.py,sha256=<HASH>,19
119-
black/trans.cpython-39-darwin.so,sha256=<HASH>
120-
black/trans.py,sha256=<HASH>
121-
blackd/__init__.py,sha256=<HASH>
122-
blackd/__main__.py,sha256=<HASH>
123-
...
124-
125-
So if no file top-level.txt is provided, we can try and extract top-levels from this file, in
126-
this case _black_version, black, and blackd.
127-
"""
128-
metadata_records = distribution.read_text("RECORD")
129-
130-
if metadata_records is None:
131-
raise FileNotFoundError("RECORD")
132-
133-
matches = re.finditer(r"^(?!__)([a-zA-Z0-9-_]+)(?:/|\.py,)", metadata_records, re.MULTILINE)
134-
135-
return {x.group(1) for x in matches}

tests/unit/test_dependency.py

Lines changed: 9 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
from importlib.metadata import PackageNotFoundError
43
from pathlib import Path
54
from unittest.mock import patch
65

@@ -21,80 +20,38 @@ def test_create_default_top_level_if_metadata_not_found() -> None:
2120
assert dependency.top_levels == {"foo_bar"}
2221

2322

24-
def test_read_top_level_from_top_level_txt() -> None:
23+
def test_get_top_levels_from_distribution() -> None:
2524
"""
26-
Read the top-levels.txt file
25+
Get the packages from distribution.
2726
"""
2827

29-
class MockDistribution:
30-
def __init__(self) -> None:
31-
pass
32-
33-
def read_text(self, file_name: str) -> str:
34-
return "foo\nbar"
35-
36-
with patch("deptry.dependency.metadata.distribution") as mock:
37-
mock.return_value = MockDistribution()
28+
with patch("deptry.dependency.get_packages_from_distribution", return_value={"foo", "bar"}):
3829
dependency = Dependency("Foo-bar", Path("pyproject.toml"))
3930

4031
assert dependency.name == "Foo-bar"
4132
assert dependency.definition_file == Path("pyproject.toml")
4233
assert dependency.top_levels == {"foo", "bar"}
4334

4435

45-
def test_read_top_level_from_record() -> None:
46-
"""
47-
Verify that if top-level.txt not found, an attempt is made to extract top-level module names from
48-
the metadata RECORD file.
49-
"""
50-
51-
class MockDistribution:
52-
def __init__(self) -> None:
53-
pass
54-
55-
def read_text(self, file_name: str) -> str | None:
56-
if file_name == "RECORD":
57-
return """\
58-
../../../bin/black,sha256=<HASH>,247
59-
__pycache__/_black_version.cpython-311.pyc,,
60-
_black_version.py,sha256=<HASH>,19
61-
black/trans.cpython-39-darwin.so,sha256=<HASH>
62-
black/trans.py,sha256=<HASH>
63-
blackd/__init__.py,sha256=<HASH>
64-
blackd/__main__.py,sha256=<HASH>
65-
"""
66-
return None
67-
68-
with patch("deptry.dependency.metadata.distribution") as mock:
69-
mock.return_value = MockDistribution()
70-
dependency = Dependency("Foo-bar", Path("pyproject.toml"))
71-
72-
assert dependency.name == "Foo-bar"
73-
assert dependency.definition_file == Path("pyproject.toml")
74-
assert dependency.top_levels == {"_black_version", "black", "blackd"}
75-
76-
77-
def test_read_top_level_from_predefined() -> None:
36+
def test_get_top_levels_from_predefined() -> None:
7837
"""
79-
Verify that if there are predefined top-level module names it takes
80-
precedence over other lookup methods.
38+
Verify that if there are predefined top-level module names it take precedence over other lookup methods.
8139
"""
82-
with patch("deptry.dependency.metadata.distribution") as mock:
40+
with patch("deptry.dependency.get_packages_from_distribution") as mock:
8341
dependency = Dependency("Foo-bar", Path("pyproject.toml"), module_names=["foo"])
8442

8543
assert dependency.name == "Foo-bar"
8644
assert dependency.definition_file == Path("pyproject.toml")
8745
assert dependency.top_levels == {"foo"}
88-
mock.return_value.read_text.assert_not_called()
46+
mock.assert_not_called()
8947

9048

91-
def test_not_predefined_and_not_installed() -> None:
49+
def test_get_top_levels_fallback() -> None:
9250
"""
9351
Use the fallback option of translating the package name.
9452
"""
9553

96-
with patch("deptry.dependency.metadata.distribution") as mock:
97-
mock.side_effect = PackageNotFoundError
54+
with patch("deptry.dependency.get_packages_from_distribution", return_value=None):
9855
dependency = Dependency("Foo-bar", Path("pyproject.toml"))
9956

10057
assert dependency.name == "Foo-bar"

0 commit comments

Comments
 (0)