Skip to content

Commit 6591922

Browse files
committed
feat: allow to set ecosystem sources via the cli
1 parent 5f2efaf commit 6591922

File tree

5 files changed

+159
-11
lines changed

5 files changed

+159
-11
lines changed

src/twyn/cli.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,16 @@ def entry_point() -> None:
111111
default=False,
112112
help="Recursively look for files when trying to locate them automatically. Ignored if --dependency-file is given.",
113113
)
114+
@click.option(
115+
"--pypi-source",
116+
type=str,
117+
help="Alternative PyPI source URL to use for fetching trusted packages.",
118+
)
119+
@click.option(
120+
"--npm-source",
121+
type=str,
122+
help="Alternative npm source URL to use for fetching trusted packages.",
123+
)
114124
def run( # noqa: C901
115125
config: str,
116126
dependency_file: tuple[str],
@@ -123,6 +133,8 @@ def run( # noqa: C901
123133
json: bool,
124134
package_ecosystem: Optional[str],
125135
recursive: bool,
136+
pypi_source: Optional[str],
137+
npm_source: Optional[str],
126138
) -> int:
127139
if vv:
128140
logger.setLevel(logging.DEBUG)
@@ -149,6 +161,8 @@ def run( # noqa: C901
149161
load_config_from_file=True,
150162
package_ecosystem=package_ecosystem,
151163
recursive=recursive,
164+
pypi_source=pypi_source,
165+
npm_source=npm_source,
152166
)
153167
except TwynError as e:
154168
raise CliError(str(e)) from e

src/twyn/config/config_handler.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,15 @@ class ConfigHandler:
6262
def __init__(self, file_handler: Optional[FileHandler] = None) -> None:
6363
self.file_handler = file_handler
6464

65-
def resolve_config(
65+
def resolve_config( # noqa: C901, PLR0912
6666
self,
6767
selector_method: Optional[str] = None,
6868
dependency_files: Optional[set[str]] = None,
6969
use_cache: Optional[bool] = None,
7070
package_ecosystem: Optional[PackageEcosystems] = None,
7171
recursive: Optional[bool] = None,
72+
pypi_source: Optional[str] = None,
73+
npm_source: Optional[str] = None,
7274
) -> TwynConfiguration:
7375
"""Resolve the configuration for Twyn.
7476
@@ -101,18 +103,34 @@ def resolve_config(
101103
final_use_cache = DEFAULT_USE_CACHE
102104

103105
if recursive is not None:
104-
final_recursive = use_cache
106+
final_recursive = recursive
105107
elif read_config.recursive is not None:
106108
final_recursive = read_config.recursive
107109
else:
108110
final_recursive = DEFAULT_RECURSIVE
109111

112+
# Determine final pypi_source from CLI, config file, or default
113+
if pypi_source is not None:
114+
final_pypi_source = pypi_source
115+
elif read_config.pypi_source is not None:
116+
final_pypi_source = read_config.pypi_source
117+
else:
118+
final_pypi_source = None
119+
120+
# Determine final npm_source from CLI, config file, or default
121+
if npm_source is not None:
122+
final_npm_source = npm_source
123+
elif read_config.npm_source is not None:
124+
final_npm_source = read_config.npm_source
125+
else:
126+
final_npm_source = None
127+
110128
return TwynConfiguration(
111129
dependency_files=dependency_files or read_config.dependency_files or set(),
112130
selector_method=final_selector_method,
113131
allowlist=read_config.allowlist,
114-
pypi_source=read_config.pypi_source,
115-
npm_source=read_config.npm_source,
132+
pypi_source=final_pypi_source,
133+
npm_source=final_npm_source,
116134
use_cache=final_use_cache,
117135
package_ecosystem=package_ecosystem or read_config.package_ecosystem,
118136
recursive=final_recursive,

src/twyn/main.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ def check_dependencies(
4444
load_config_from_file: bool = False,
4545
package_ecosystem: Optional[PackageEcosystems] = None,
4646
recursive: Optional[bool] = None,
47+
pypi_source: Optional[str] = None,
48+
npm_source: Optional[str] = None,
4749
) -> TyposquatCheckResults:
4850
"""
4951
Check if the provided dependencies are potential typosquats of trusted packages.
@@ -72,6 +74,8 @@ def check_dependencies(
7274
use_cache=use_cache,
7375
package_ecosystem=package_ecosystem,
7476
recursive=recursive,
77+
pypi_source=pypi_source,
78+
npm_source=npm_source,
7579
)
7680
maybe_cache_handler = CacheHandler() if config.use_cache else None
7781
selector_method_obj = _get_selector_method(config.selector_method)
@@ -284,6 +288,8 @@ def _get_config(
284288
use_cache: Optional[bool],
285289
package_ecosystem: Optional[PackageEcosystems],
286290
recursive: Optional[bool],
291+
pypi_source: Optional[str],
292+
npm_source: Optional[str],
287293
) -> TwynConfiguration:
288294
"""Given the arguments passed to the main function and the configuration loaded from the config file (if any), return a config object."""
289295
if load_config_from_file:
@@ -296,4 +302,6 @@ def _get_config(
296302
use_cache=use_cache,
297303
package_ecosystem=package_ecosystem,
298304
recursive=recursive,
305+
pypi_source=pypi_source,
306+
npm_source=npm_source,
299307
)

tests/main/test_cli.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ def test_click_arguments_dependency_file(self, mock_check_dependencies: Mock) ->
9696
load_config_from_file=True,
9797
package_ecosystem=None,
9898
recursive=False,
99+
pypi_source=None,
100+
npm_source=None,
99101
)
100102
]
101103

@@ -121,6 +123,8 @@ def test_click_arguments_dependency_file_in_different_path(self, mock_check_depe
121123
load_config_from_file=True,
122124
package_ecosystem=None,
123125
recursive=False,
126+
pypi_source=None,
127+
npm_source=None,
124128
)
125129
]
126130

@@ -146,6 +150,8 @@ def test_package_ecosystem_option(self, mock_check_dependencies: Mock) -> None:
146150
load_config_from_file=True,
147151
package_ecosystem="pypi",
148152
recursive=False,
153+
pypi_source=None,
154+
npm_source=None,
149155
)
150156
]
151157

@@ -170,6 +176,8 @@ def test_click_arguments_single_dependency_cli(self, mock_check_dependencies: Mo
170176
load_config_from_file=True,
171177
package_ecosystem=None,
172178
recursive=False,
179+
pypi_source=None,
180+
npm_source=None,
173181
)
174182
]
175183

@@ -206,6 +214,8 @@ def test_click_arguments_multiple_dependencies(self, mock_check_dependencies: Mo
206214
load_config_from_file=True,
207215
package_ecosystem=None,
208216
recursive=False,
217+
pypi_source=None,
218+
npm_source=None,
209219
)
210220
]
211221

@@ -235,6 +245,8 @@ def test_recursive(self, mock_check_dependencies: Mock) -> None:
235245
load_config_from_file=True,
236246
package_ecosystem=None,
237247
recursive=True,
248+
pypi_source=None,
249+
npm_source=None,
238250
)
239251
assert mock_check_dependencies.call_args_list[0] == call_args
240252
assert mock_check_dependencies.call_args_list[1] == call_args
@@ -255,6 +267,8 @@ def test_click_arguments_default(self, mock_check_dependencies: Mock) -> None:
255267
load_config_from_file=True,
256268
package_ecosystem=None,
257269
recursive=False,
270+
pypi_source=None,
271+
npm_source=None,
258272
)
259273
]
260274

@@ -387,3 +401,89 @@ def test_unhandled_exception_is_caught_and_wrapped_in_cli_error(
387401
assert isinstance(result.exception, SystemExit)
388402
# Check that the generic error message was logged
389403
assert "Unhandled exception occured." in caplog.text
404+
405+
@patch("twyn.cli.check_dependencies")
406+
def test_pypi_source_option(self, mock_check_dependencies: Mock) -> None:
407+
"""Test that --pypi-source option is passed correctly."""
408+
runner = CliRunner()
409+
runner.invoke(
410+
cli.run,
411+
[
412+
"--pypi-source",
413+
"https://custom-pypi.org/",
414+
],
415+
)
416+
417+
assert mock_check_dependencies.call_args_list == [
418+
call(
419+
config_file=None,
420+
dependency_files=None,
421+
dependencies=None,
422+
selector_method=None,
423+
use_cache=None,
424+
show_progress_bar=True,
425+
load_config_from_file=True,
426+
package_ecosystem=None,
427+
recursive=False,
428+
pypi_source="https://custom-pypi.org/",
429+
npm_source=None,
430+
)
431+
]
432+
433+
@patch("twyn.cli.check_dependencies")
434+
def test_npm_source_option(self, mock_check_dependencies: Mock) -> None:
435+
"""Test that --npm-source option is passed correctly."""
436+
runner = CliRunner()
437+
runner.invoke(
438+
cli.run,
439+
[
440+
"--npm-source",
441+
"https://custom-npm.org/",
442+
],
443+
)
444+
445+
assert mock_check_dependencies.call_args_list == [
446+
call(
447+
config_file=None,
448+
dependency_files=None,
449+
dependencies=None,
450+
selector_method=None,
451+
use_cache=None,
452+
show_progress_bar=True,
453+
load_config_from_file=True,
454+
package_ecosystem=None,
455+
recursive=False,
456+
pypi_source=None,
457+
npm_source="https://custom-npm.org/",
458+
)
459+
]
460+
461+
@patch("twyn.cli.check_dependencies")
462+
def test_both_source_options(self, mock_check_dependencies: Mock) -> None:
463+
"""Test that both --pypi-source and --npm-source options work together."""
464+
runner = CliRunner()
465+
runner.invoke(
466+
cli.run,
467+
[
468+
"--pypi-source",
469+
"https://custom-pypi.org/",
470+
"--npm-source",
471+
"https://custom-npm.org/",
472+
],
473+
)
474+
475+
assert mock_check_dependencies.call_args_list == [
476+
call(
477+
config_file=None,
478+
dependency_files=None,
479+
dependencies=None,
480+
selector_method=None,
481+
use_cache=None,
482+
show_progress_bar=True,
483+
load_config_from_file=True,
484+
package_ecosystem=None,
485+
recursive=False,
486+
pypi_source="https://custom-pypi.org/",
487+
npm_source="https://custom-npm.org/",
488+
)
489+
]

tests/main/test_main.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,12 @@
1717
from twyn.main import (
1818
check_dependencies,
1919
)
20-
from twyn.trusted_packages import TopPyPiReference
2120
from twyn.trusted_packages.exceptions import InvalidArgumentsError
2221
from twyn.trusted_packages.models import (
2322
TyposquatCheckResultEntry,
2423
TyposquatCheckResultFromSource,
2524
TyposquatCheckResults,
2625
)
27-
from twyn.trusted_packages.references.top_npm_reference import TopNpmReference
2826

2927
from tests.conftest import create_tmp_file, patch_npm_packages_download
3028

@@ -39,9 +37,10 @@ class TestCheckDependencies:
3937
"selector_method": "first-letter",
4038
"dependency_file": {"requirements.txt"},
4139
"use_cache": True,
42-
"recurisve": True,
40+
"recursive": True,
4341
"pypi_source": "pypi",
4442
"npm_source": "npm",
43+
"package_ecosystem": "pypi",
4544
},
4645
{
4746
"selector_method": "nearby-letter",
@@ -51,6 +50,7 @@ class TestCheckDependencies:
5150
"recursive": False,
5251
"pypi_source": "a",
5352
"npm_source": "a",
53+
"package_ecosystem": "npm",
5454
},
5555
TwynConfiguration(
5656
dependency_files={"requirements.txt"},
@@ -73,6 +73,7 @@ class TestCheckDependencies:
7373
"recursive": True,
7474
"pypi_source": "pypi",
7575
"npm_source": "npm",
76+
"package_ecosystem": "pypi",
7677
},
7778
TwynConfiguration(
7879
dependency_files={"poetry.lock"},
@@ -92,10 +93,10 @@ class TestCheckDependencies:
9293
dependency_files=set(),
9394
selector_method="all",
9495
allowlist=set(),
95-
pypi_source=TopPyPiReference.DEFAULT_SOURCE,
96-
npm_source=TopNpmReference.DEFAULT_SOURCE,
96+
pypi_source=None,
97+
npm_source=None,
9798
use_cache=True,
98-
package_ecosystem="pypi",
99+
package_ecosystem=None,
99100
recursive=False,
100101
),
101102
), # Fallback values
@@ -123,12 +124,19 @@ def test_options_priorities_assignation(
123124
selector_method=cli_config.get("selector_method"),
124125
dependency_files=cli_config.get("dependency_file", set()),
125126
use_cache=cli_config.get("use_cache"),
127+
pypi_source=cli_config.get("pypi_source"),
128+
npm_source=cli_config.get("npm_source"),
129+
recursive=cli_config.get("recursive"),
130+
package_ecosystem=cli_config.get("package_ecosystem"),
126131
)
127132

128133
assert resolved.dependency_files == expected_resolved_config.dependency_files
129134
assert resolved.selector_method == expected_resolved_config.selector_method
130135
assert resolved.allowlist == expected_resolved_config.allowlist
131-
assert resolved.use_cache == expected_resolved_config.use_cache
136+
assert resolved.package_ecosystem == expected_resolved_config.package_ecosystem
137+
assert resolved.recursive == expected_resolved_config.recursive
138+
assert resolved.npm_source == expected_resolved_config.npm_source
139+
assert resolved.pypi_source == expected_resolved_config.pypi_source
132140

133141
@patch("twyn.trusted_packages.TopPyPiReference.get_packages")
134142
def test_check_dependencies_detects_typosquats_from_file(

0 commit comments

Comments
 (0)