Skip to content

Commit e2ba50a

Browse files
committed
feat: Accept a source for every dependency manager in the config
BREAKING CHANGE
1 parent fb634a6 commit e2ba50a

File tree

8 files changed

+66
-64
lines changed

8 files changed

+66
-64
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,8 @@ dependency_file="/my/path/requirements.txt" # it can be either a string or a lis
222222
selector_method="first_letter"
223223
logging_level="debug"
224224
allowlist=["my_package"]
225-
source="https://mirror-with-trusted-dependencies.com/file.json"
225+
pypi_source="https://mirror-with-trusted-dependencies.com/file-pypi.json"
226+
npm_source="https://mirror-with-trusted-dependencies.com/file-npm.json"
226227
```
227228

228229
The file format for each reference is as follows:

src/twyn/config/config_handler.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ class TwynConfiguration:
3535
dependency_files: set[str]
3636
selector_method: str
3737
allowlist: set[str]
38-
source: Optional[str]
38+
pypi_source: Optional[str]
39+
npm_source: Optional[str]
3940
use_cache: bool
4041
package_ecosystem: Optional[PackageEcosystems]
4142
recursive: Optional[bool]
@@ -48,7 +49,8 @@ class ReadTwynConfiguration:
4849
dependency_files: Optional[set[str]] = field(default_factory=set)
4950
selector_method: Optional[str] = None
5051
allowlist: set[str] = field(default_factory=set)
51-
source: Optional[str] = None
52+
pypi_source: Optional[str] = None
53+
npm_source: Optional[str] = None
5254
use_cache: Optional[bool] = None
5355
package_ecosystem: Optional[PackageEcosystems] = None
5456
recursive: Optional[bool] = None
@@ -91,7 +93,6 @@ def resolve_config(
9193
raise InvalidSelectorMethodError(
9294
f"Invalid selector_method '{final_selector_method}'. Must be one of: {valid_methods}"
9395
)
94-
9596
if use_cache is not None:
9697
final_use_cache = use_cache
9798
elif read_config.use_cache is not None:
@@ -110,7 +111,8 @@ def resolve_config(
110111
dependency_files=dependency_files or read_config.dependency_files or set(),
111112
selector_method=final_selector_method,
112113
allowlist=read_config.allowlist,
113-
source=read_config.source,
114+
pypi_source=read_config.pypi_source,
115+
npm_source=read_config.npm_source,
114116
use_cache=final_use_cache,
115117
package_ecosystem=package_ecosystem or read_config.package_ecosystem,
116118
recursive=final_recursive,
@@ -158,7 +160,8 @@ def _get_read_config(self, toml: TOMLDocument) -> ReadTwynConfiguration:
158160
dependency_files=dependency_file,
159161
selector_method=twyn_config_data.get("selector_method"),
160162
allowlist=allowlist,
161-
source=twyn_config_data.get("source"),
163+
pypi_source=twyn_config_data.get("pypi_source"),
164+
npm_source=twyn_config_data.get("npm_source"),
162165
use_cache=twyn_config_data.get("use_cache"),
163166
package_ecosystem=twyn_config_data.get("package_ecosystem"),
164167
recursive=twyn_config_data.get("recursive"),

src/twyn/dependency_managers/dependency_manager.py

Lines changed: 3 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,7 @@
1-
from dataclasses import dataclass
2-
from pathlib import Path
3-
41
from twyn.dependency_managers.exceptions import NoMatchingDependencyManagerError
5-
from twyn.dependency_parser.parsers.constants import PACKAGE_LOCK_JSON, POETRY_LOCK, REQUIREMENTS_TXT, UV_LOCK
6-
from twyn.trusted_packages.references import AbstractPackageReference, TopNpmReference, TopPyPiReference
7-
8-
9-
@dataclass
10-
class BaseDependencyManager:
11-
"""Base class for all `DependencyManagers`.
12-
13-
It acts as a repository, linking programming languages with trusted packages sources and dependency file names.
14-
"""
15-
16-
name: str
17-
trusted_packages_source: type[AbstractPackageReference]
18-
dependency_files: set[str]
19-
20-
@classmethod
21-
def matches_dependency_file(cls, dependency_file: str) -> bool:
22-
return Path(dependency_file).name in cls.dependency_files
23-
24-
@classmethod
25-
def matches_language_name(cls, name: str) -> bool:
26-
return cls.name == Path(name).name.lower()
27-
28-
29-
@dataclass
30-
class PypiDependencyManager(BaseDependencyManager):
31-
name = "pypi"
32-
trusted_packages_source = TopPyPiReference
33-
dependency_files = {UV_LOCK, POETRY_LOCK, REQUIREMENTS_TXT}
34-
35-
36-
@dataclass
37-
class NpmDependencyManager(BaseDependencyManager):
38-
name = "npm"
39-
trusted_packages_source = TopNpmReference
40-
dependency_files = {PACKAGE_LOCK_JSON}
41-
2+
from twyn.dependency_managers.managers.base import BaseDependencyManager
3+
from twyn.dependency_managers.managers.npm_dependency_manager import NpmDependencyManager
4+
from twyn.dependency_managers.managers.pypi_dependency_manager import PypiDependencyManager
425

436
DEPENDENCY_MANAGERS: list[type[BaseDependencyManager]] = [PypiDependencyManager, NpmDependencyManager]
447
PACKAGE_ECOSYSTEMS = {x.name for x in DEPENDENCY_MANAGERS}

src/twyn/dependency_managers/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@
33

44
class NoMatchingDependencyManagerError(TwynError):
55
"""Error for when a DependencyManger cannot be retrieved based on the provided arguments."""
6+
7+
8+
class MultipleSourcesError(TwynError):
9+
"""Error for when more than one alternative source matches the configuration."""

src/twyn/dependency_managers/managers/base.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from dataclasses import dataclass
22
from pathlib import Path
3+
from typing import Optional
34

5+
from twyn.dependency_managers.exceptions import MultipleSourcesError
46
from twyn.trusted_packages.references.base import AbstractPackageReference
57

68

@@ -22,3 +24,11 @@ def matches_dependency_file(cls, dependency_file: str) -> bool:
2224
@classmethod
2325
def matches_language_name(cls, name: str) -> bool:
2426
return cls.name == Path(name).name.lower()
27+
28+
@classmethod
29+
def get_alternative_source(cls, sources: dict[str, str]) -> Optional[str]:
30+
match = [x for x in sources if x == cls.name]
31+
if len(match) > 1:
32+
raise MultipleSourcesError
33+
34+
return match[0] if match else None

src/twyn/main.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ def check_dependencies(
7979
if dependencies: # Dependencies where input manually, will not read dependency files.
8080
return _analyze_dependencies_from_input(
8181
selector_method=selector_method_obj,
82-
source=config.source,
82+
pypi_source=config.pypi_source,
83+
npm_source=config.npm_source,
8384
maybe_cache_handler=maybe_cache_handler,
8485
allowlist=config.allowlist,
8586
show_progress_bar=show_progress_bar,
@@ -100,7 +101,8 @@ def check_dependencies(
100101

101102
return _analyze_packages_from_source(
102103
selector_method=selector_method_obj,
103-
source=config.source,
104+
pypi_source=config.pypi_source,
105+
npm_source=config.npm_source,
104106
maybe_cache_handler=maybe_cache_handler,
105107
allowlist=config.allowlist,
106108
show_progress_bar=show_progress_bar,
@@ -111,7 +113,8 @@ def check_dependencies(
111113
def _analyze_dependencies_from_input(
112114
package_ecosystem: Optional[PackageEcosystems],
113115
selector_method: SelectorMethod,
114-
source: Optional[str],
116+
pypi_source: Optional[str],
117+
npm_source: Optional[str],
115118
maybe_cache_handler: Optional[CacheHandler],
116119
dependencies: set[str],
117120
allowlist: set[str],
@@ -127,6 +130,7 @@ def _analyze_dependencies_from_input(
127130
raise InvalidArgumentsError("Not a valid `package_ecosystem`.")
128131

129132
dependency_manager = get_dependency_manager_from_name(package_ecosystem)
133+
source = dependency_manager.get_alternative_source({"pypi": pypi_source, "npm": npm_source})
130134
top_package_reference = dependency_manager.trusted_packages_source(source, maybe_cache_handler)
131135
trusted_packages = TrustedPackages(
132136
names=top_package_reference.get_packages(),
@@ -154,7 +158,8 @@ def _analyze_packages_from_source(
154158
selector_method: SelectorMethod,
155159
show_progress_bar: bool,
156160
dependency_files: Optional[set[str]],
157-
source: Optional[str],
161+
pypi_source: Optional[str],
162+
npm_source: Optional[str],
158163
maybe_cache_handler: Optional[CacheHandler],
159164
) -> TyposquatCheckResults:
160165
"""Analyze dependencies from a dependencies file.
@@ -165,6 +170,7 @@ def _analyze_packages_from_source(
165170

166171
dependency_managers = _get_dependency_managers_and_parsers_mapping(dependency_files)
167172
for dependency_manager, parsers in dependency_managers.items():
173+
source = dependency_manager.get_alternative_source({"pypi": pypi_source, "npm": npm_source})
168174
top_package_reference = dependency_manager.trusted_packages_source(source, maybe_cache_handler)
169175

170176
packages_from_source = top_package_reference.get_packages()

tests/config/test_config_handler.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ def test_no_enforce_file_on_non_existent_file(self, mock_is_file: Mock) -> None:
3535
dependency_files=set(),
3636
selector_method="all",
3737
allowlist=set(),
38-
source=None,
38+
pypi_source=None,
39+
npm_source=None,
3940
use_cache=True,
4041
package_ecosystem=None,
4142
recursive=False,
@@ -61,7 +62,8 @@ def test_get_twyn_data_from_file(self, pyproject_toml_file: Path) -> None:
6162
dependency_files={"my_file.txt", "my_other_file.txt"},
6263
selector_method="all",
6364
allowlist={"boto4", "boto2"},
64-
source=None,
65+
pypi_source=None,
66+
npm_source=None,
6567
use_cache=False,
6668
)
6769

@@ -138,7 +140,8 @@ def test_no_load_config_from_cache(self, pyproject_toml_file: Path) -> None:
138140
assert config.dependency_files == set()
139141
assert config.use_cache is True
140142
assert config.selector_method == DEFAULT_SELECTOR_METHOD
141-
assert config.source is None
143+
assert config.pypi_source is None
144+
assert config.npm_source is None
142145

143146
def test_cannot_write_if_file_not_configured(self) -> None:
144147
with pytest.raises(ConfigFileNotConfiguredError, match="write operation"):

tests/main/test_main.py

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
TyposquatCheckResultFromSource,
2525
TyposquatCheckResults,
2626
)
27+
from twyn.trusted_packages.references.top_npm_reference import TopNpmReference
2728

2829
from tests.conftest import create_tmp_file, patch_npm_packages_download
2930

@@ -38,22 +39,25 @@ class TestCheckDependencies:
3839
"selector_method": "first-letter",
3940
"dependency_file": {"requirements.txt"},
4041
"use_cache": True,
41-
"pypi_reference": "https://myurl.com",
4242
"recurisve": True,
43+
"pypi_source": "pypi",
44+
"npm_source": "npm",
4345
},
4446
{
4547
"selector_method": "nearby-letter",
4648
"dependency_file": ["poetry.lock"],
4749
"allowlist": ["boto4", "boto2"], # There is no allowlist option in the cli
4850
"use_cache": False,
49-
"pypi_reference": "https://mysecondurl.com",
5051
"recursive": False,
52+
"pypi_source": "a",
53+
"npm_source": "a",
5154
},
5255
TwynConfiguration(
5356
dependency_files={"requirements.txt"},
5457
selector_method="first-letter",
5558
allowlist={"boto4", "boto2"},
56-
source=TopPyPiReference.DEFAULT_SOURCE,
59+
pypi_source="pypi",
60+
npm_source="npm",
5761
use_cache=True,
5862
package_ecosystem="pypi",
5963
recursive=True,
@@ -66,14 +70,16 @@ class TestCheckDependencies:
6670
"dependency_file": ["poetry.lock"],
6771
"allowlist": ["boto4", "boto2"],
6872
"use_cache": False,
69-
"pypi_reference": "https://mysecondurl.com",
7073
"recursive": True,
74+
"pypi_source": "pypi",
75+
"npm_source": "npm",
7176
},
7277
TwynConfiguration(
7378
dependency_files={"poetry.lock"},
7479
selector_method="nearby-letter",
7580
allowlist={"boto4", "boto2"},
76-
source=TopPyPiReference.DEFAULT_SOURCE,
81+
pypi_source="pypi",
82+
npm_source="npm",
7783
use_cache=False,
7884
package_ecosystem="pypi",
7985
recursive=True,
@@ -86,7 +92,8 @@ class TestCheckDependencies:
8692
dependency_files=set(),
8793
selector_method="all",
8894
allowlist=set(),
89-
source=TopPyPiReference.DEFAULT_SOURCE,
95+
pypi_source=TopPyPiReference.DEFAULT_SOURCE,
96+
npm_source=TopNpmReference.DEFAULT_SOURCE,
9097
use_cache=True,
9198
package_ecosystem="pypi",
9299
recursive=False,
@@ -184,7 +191,8 @@ def test_check_dependencies_detects_typosquats_and_autodetects_file(
184191
dependency_files={str(uv_lock_file_with_typo)},
185192
selector_method="all",
186193
allowlist=set(),
187-
source=None,
194+
pypi_source=None,
195+
npm_source=None,
188196
use_cache=False,
189197
package_ecosystem=None,
190198
recursive=False,
@@ -219,7 +227,8 @@ def test_check_dependencies_detects_typosquats_and_autodetects_file_and_language
219227
dependency_files={str(uv_lock_file_with_typo)},
220228
selector_method="all",
221229
allowlist=set(),
222-
source=None,
230+
pypi_source=None,
231+
npm_source=None,
223232
use_cache=False,
224233
package_ecosystem="pypi",
225234
recursive=False,
@@ -439,7 +448,8 @@ def test_check_dependencies_ignores_package_in_allowlist(
439448
dependency_files={str(uv_lock_file_with_typo)},
440449
selector_method="first-letter",
441450
use_cache=True,
442-
source=None,
451+
pypi_source=None,
452+
npm_source=None,
443453
package_ecosystem=None,
444454
recursive=False,
445455
)
@@ -473,7 +483,8 @@ def test_track_is_disabled_by_default_when_used_as_package(
473483
dependency_files={str(uv_lock_file)},
474484
selector_method="all",
475485
allowlist=set(),
476-
source=None,
486+
pypi_source=None,
487+
npm_source=None,
477488
use_cache=False,
478489
package_ecosystem=None,
479490
recursive=False,
@@ -490,7 +501,8 @@ def test_track_is_shown_when_enabled(self, mock_config: Mock, mock_get_packages:
490501
dependency_files={str(uv_lock_file)},
491502
selector_method="all",
492503
allowlist=set(),
493-
source=None,
504+
pypi_source=None,
505+
npm_source=None,
494506
use_cache=False,
495507
package_ecosystem=None,
496508
recursive=False,

0 commit comments

Comments
 (0)