Skip to content

Commit bdac02f

Browse files
authored
refactor: make DependencyManagers objects of base class (#377)
1 parent b73f7af commit bdac02f

File tree

11 files changed

+92
-135
lines changed

11 files changed

+92
-135
lines changed

.github/workflows/test.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ jobs:
1616
fail-fast: false
1717
matrix:
1818
python-version: ["3.10", "3.14"]
19-
runs-on: [ubuntu-latest]
2019
steps:
2120
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
2221

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1 @@
1-
from twyn.dependency_managers.managers.npm_dependency_manager import NpmDependencyManager
2-
from twyn.dependency_managers.managers.pypi_dependency_manager import PypiDependencyManager
3-
from twyn.dependency_managers.utils import (
4-
PACKAGE_ECOSYSTEMS,
5-
get_dependency_manager_from_file,
6-
get_dependency_manager_from_name,
7-
)
81

9-
__all__ = [
10-
"NpmDependencyManager",
11-
"PypiDependencyManager",
12-
"get_dependency_manager_from_file",
13-
"get_dependency_manager_from_name",
14-
"PACKAGE_ECOSYSTEMS",
15-
]
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from dataclasses import dataclass
2+
from pathlib import Path
3+
4+
from twyn.dependency_managers.exceptions import NoMatchingDependencyManagerError
5+
from twyn.dependency_parser.parsers.constants import (
6+
PACKAGE_LOCK_JSON,
7+
POETRY_LOCK,
8+
REQUIREMENTS_TXT,
9+
UV_LOCK,
10+
YARN_LOCK,
11+
)
12+
from twyn.trusted_packages.references.base import AbstractPackageReference
13+
from twyn.trusted_packages.references.top_npm_reference import TopNpmReference
14+
from twyn.trusted_packages.references.top_pypi_reference import TopPyPiReference
15+
16+
17+
@dataclass
18+
class DependencyManager:
19+
"""Base class for all `DependencyManagers`.
20+
21+
It acts as a repository, linking programming languages with trusted packages sources and dependency file names.
22+
"""
23+
24+
name: str
25+
"""Name identifier for the package ecosystem."""
26+
27+
trusted_packages_source: type[AbstractPackageReference]
28+
"""Reference class for trusted packages source."""
29+
30+
dependency_files: set[str]
31+
"""Set of supported dependency file names."""
32+
33+
def matches_dependency_file(self, dependency_file: str) -> bool:
34+
"""Check if this manager can handle the given dependency file."""
35+
return Path(dependency_file).name in self.dependency_files
36+
37+
def get_alternative_source(self, sources: dict[str, str]) -> str | None:
38+
"""Get alternative source URL for this ecosystem from sources dict."""
39+
return sources.get(self.name)
40+
41+
42+
npm_dependency_manager = DependencyManager(
43+
name="npm",
44+
trusted_packages_source=TopNpmReference,
45+
dependency_files={PACKAGE_LOCK_JSON, YARN_LOCK},
46+
)
47+
pypi_dependency_manager = DependencyManager(
48+
name="pypi",
49+
trusted_packages_source=TopPyPiReference,
50+
dependency_files={UV_LOCK, POETRY_LOCK, REQUIREMENTS_TXT},
51+
)
52+
53+
DEPENDENCY_MANAGERS: list[DependencyManager] = [pypi_dependency_manager, npm_dependency_manager]
54+
"""List of available dependency manager classes."""
55+
56+
PACKAGE_ECOSYSTEMS = {x.name for x in DEPENDENCY_MANAGERS}
57+
"""Set of package ecosystem names from available dependency managers."""
58+
59+
60+
def get_dependency_manager_from_file(dependency_file: str) -> DependencyManager:
61+
"""Get dependency manager that can handle the given file."""
62+
for manager in DEPENDENCY_MANAGERS:
63+
if manager.matches_dependency_file(dependency_file):
64+
return manager
65+
raise NoMatchingDependencyManagerError
66+
67+
68+
def get_dependency_manager_from_name(name: str) -> DependencyManager:
69+
"""Get dependency manager by ecosystem name."""
70+
for manager in DEPENDENCY_MANAGERS:
71+
if manager.name == name:
72+
return manager
73+
raise NoMatchingDependencyManagerError

src/twyn/dependency_managers/managers/__init__.py

Whitespace-only changes.

src/twyn/dependency_managers/managers/base.py

Lines changed: 0 additions & 36 deletions
This file was deleted.

src/twyn/dependency_managers/managers/npm_dependency_manager.py

Lines changed: 0 additions & 15 deletions
This file was deleted.

src/twyn/dependency_managers/managers/pypi_dependency_manager.py

Lines changed: 0 additions & 15 deletions
This file was deleted.

src/twyn/dependency_managers/utils.py

Lines changed: 0 additions & 29 deletions
This file was deleted.

src/twyn/main.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99
)
1010
from twyn.config.config_handler import ConfigHandler, TwynConfiguration
1111
from twyn.config.exceptions import InvalidSelectorMethodError
12-
from twyn.dependency_managers.managers.base import BaseDependencyManager
13-
from twyn.dependency_managers.utils import (
12+
from twyn.dependency_managers.managers import (
1413
PACKAGE_ECOSYSTEMS,
1514
get_dependency_manager_from_file,
1615
get_dependency_manager_from_name,
@@ -172,9 +171,10 @@ def _analyze_packages_from_source(
172171
typos_by_file = TyposquatCheckResults()
173172

174173
dependency_managers = _get_dependency_managers_and_parsers_mapping(dependency_files)
175-
for dependency_manager, parsers in dependency_managers.items():
176-
source = dependency_manager.get_alternative_source({"pypi": pypi_source, "npm": npm_source})
177-
top_package_reference = dependency_manager.trusted_packages_source(source, maybe_cache_handler)
174+
for ecosystem_name, parsers in dependency_managers.items():
175+
manager = get_dependency_manager_from_name(ecosystem_name)
176+
source = manager.get_alternative_source({"pypi": pypi_source, "npm": npm_source})
177+
top_package_reference = manager.trusted_packages_source(source, maybe_cache_handler)
178178

179179
packages_from_source = top_package_reference.get_packages()
180180
trusted_packages = TrustedPackages(
@@ -262,9 +262,9 @@ def _get_selector_method(selector_method: str) -> SelectorMethod:
262262

263263
def _get_dependency_managers_and_parsers_mapping(
264264
dependency_files: set[str] | None,
265-
) -> dict[type[BaseDependencyManager], list[AbstractParser]]:
265+
) -> dict[str, list[AbstractParser]]:
266266
"""Return a dictionary, grouping all files to parse by their DependencyManager."""
267-
dependency_managers: dict[type[BaseDependencyManager], list[AbstractParser]] = {}
267+
dependency_managers: dict[str, list[AbstractParser]] = {}
268268

269269
# No dependencies introduced via the CLI, so the dependecy file was either given or will be auto-detected
270270
dependency_selector = DependencySelector(dependency_files)
@@ -273,9 +273,9 @@ def _get_dependency_managers_and_parsers_mapping(
273273
for parser in dependency_parsers:
274274
manager = get_dependency_manager_from_file(parser.file_path)
275275

276-
if manager not in dependency_managers:
277-
dependency_managers[manager] = []
278-
dependency_managers[manager].append(parser)
276+
if manager.name not in dependency_managers:
277+
dependency_managers[manager.name] = []
278+
dependency_managers[manager.name].append(parser)
279279
return dependency_managers
280280

281281

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,18 @@
11
from unittest.mock import Mock
22

3-
from twyn.dependency_managers.managers.base import BaseDependencyManager
3+
from twyn.dependency_managers.managers import DependencyManager
44

5-
6-
class DummyManager(BaseDependencyManager):
7-
name = "pypi"
8-
trusted_packages_source = Mock()
9-
dependency_files = {"requirements.txt", "poetry.lock"}
5+
manager = DependencyManager(
6+
name="pypi", trusted_packages_source=Mock(), dependency_files={"requirements.txt", "poetry.lock"}
7+
)
108

119

1210
class TestDependencyManager:
1311
def test_matches_dependency_file(self) -> None:
14-
assert DummyManager.matches_dependency_file("requirements.txt")
15-
assert DummyManager.matches_dependency_file("/some/path/poetry.lock")
16-
assert not DummyManager.matches_dependency_file("setup.py")
17-
18-
def test_matches_language_name(self) -> None:
19-
assert DummyManager.matches_ecosystem_name("pypi")
20-
assert not DummyManager.matches_ecosystem_name("npm")
12+
assert manager.matches_dependency_file("requirements.txt")
13+
assert manager.matches_dependency_file("/some/path/poetry.lock")
14+
assert not manager.matches_dependency_file("setup.py")
2115

2216
def test_get_alternative_source_none(self) -> None:
2317
sources = {"npm": "npmjs.com"}
24-
assert DummyManager.get_alternative_source(sources) is None
18+
assert manager.get_alternative_source(sources) is None

0 commit comments

Comments
 (0)