Skip to content

Commit 1e62075

Browse files
author
Sergio Castillo
committed
feat: configurable pypi-reference
1 parent 1176134 commit 1e62075

File tree

9 files changed

+39
-30
lines changed

9 files changed

+39
-30
lines changed

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,15 @@ twyn run --config <file>
131131
All the configurations available through the command line are also supported in the config file.
132132

133133
```toml
134-
[tool.twyn]
135-
dependency_file="/my/path/requirements.txt"
136-
selector_method="first_letter"
137-
logging_level="debug"
138-
allowlist=["my_package"]
134+
[tool.twyn]
135+
dependency_file="/my/path/requirements.txt"
136+
selector_method="first_letter"
137+
logging_level="debug"
138+
allowlist=["my_package"]
139+
pypi_reference="https://mirror-with-trusted-dependencies.com/file.json"
139140
```
141+
142+
> [!WARNING]
143+
> `twyn` will have a default reference URL for every source of trusted packages that is configurable.
144+
> If you want to protect yourself against spoofing attacks, it is recommended to set your own
145+
> reference url.

src/twyn/base/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
if TYPE_CHECKING:
1010
from twyn.dependency_parser.abstract_parser import AbstractParser
1111

12+
1213
SELECTOR_METHOD_MAPPING: dict[str, type[selectors.AbstractSelector]] = {
1314
"first-letter": selectors.FirstLetterExact,
1415
"nearby-letter": selectors.FirstLetterNearbyInKeyboard,
@@ -23,6 +24,7 @@
2324
DEFAULT_SELECTOR_METHOD = "all"
2425
DEFAULT_DEPENDENCY_FILE = "requirements.txt"
2526
DEFAULT_PROJECT_TOML_FILE = "pyproject.toml"
27+
DEFAULT_TOP_PYPI_PACKAGES = "https://hugovk.github.io/top-pypi-packages/top-pypi-packages-30-days.min.json"
2628

2729

2830
class AvailableLoggingLevels(enum.Enum):

src/twyn/core/config_handler.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from twyn.base.constants import (
1111
DEFAULT_PROJECT_TOML_FILE,
1212
DEFAULT_SELECTOR_METHOD,
13+
DEFAULT_TOP_PYPI_PACKAGES,
1314
AvailableLoggingLevels,
1415
)
1516
from twyn.core.exceptions import (
@@ -29,6 +30,7 @@ class TwynConfiguration:
2930
selector_method: str
3031
logging_level: AvailableLoggingLevels
3132
allowlist: set[str]
33+
pypi_reference: str
3234

3335

3436
@dataclass(frozen=True)
@@ -39,6 +41,7 @@ class ReadTwynConfiguration:
3941
selector_method: Optional[str]
4042
logging_level: Optional[AvailableLoggingLevels]
4143
allowlist: set[str]
44+
pypi_reference: Optional[str]
4245

4346

4447
class ConfigHandler:
@@ -71,6 +74,7 @@ def resolve_config(
7174
selector_method=selector_method or twyn_config_data.get("selector_method", DEFAULT_SELECTOR_METHOD),
7275
logging_level=_get_logging_level(verbosity, twyn_config_data.get("logging_level")),
7376
allowlist=set(twyn_config_data.get("allowlist", set())),
77+
pypi_reference=twyn_config_data.get("pypi_reference", DEFAULT_TOP_PYPI_PACKAGES),
7478
)
7579

7680
def add_package_to_allowlist(self, package_name: str) -> None:
@@ -85,6 +89,7 @@ def add_package_to_allowlist(self, package_name: str) -> None:
8589
selector_method=config.selector_method,
8690
logging_level=config.logging_level,
8791
allowlist=config.allowlist | {package_name},
92+
pypi_reference=config.pypi_reference,
8893
)
8994
self._write_config(toml, new_config)
9095
logger.info(f"Package '{package_name}' successfully added to allowlist")
@@ -101,6 +106,7 @@ def remove_package_from_allowlist(self, package_name: str) -> None:
101106
selector_method=config.selector_method,
102107
logging_level=config.logging_level,
103108
allowlist=config.allowlist - {package_name},
109+
pypi_reference=config.pypi_reference,
104110
)
105111
self._write_config(toml, new_config)
106112
logger.info(f"Package '{package_name}' successfully removed from allowlist")
@@ -113,6 +119,7 @@ def _get_read_config(self, toml: TOMLDocument) -> ReadTwynConfiguration:
113119
selector_method=twyn_config_data.get("selector_method"),
114120
logging_level=twyn_config_data.get("logging_level"),
115121
allowlist=set(twyn_config_data.get("allowlist", set())),
122+
pypi_reference=twyn_config_data.get("pypi_reference"),
116123
)
117124

118125
def _write_config(self, toml: TOMLDocument, config: ReadTwynConfiguration) -> None:

src/twyn/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def check_dependencies(
4141
_set_logging_level(config.logging_level)
4242

4343
trusted_packages = TrustedPackages(
44-
names=TopPyPiReference().get_packages(),
44+
names=TopPyPiReference(source=config.pypi_reference).get_packages(),
4545
algorithm=EditDistance(),
4646
selector=get_candidate_selector(config.selector_method),
4747
threshold_class=SimilarityThreshold,

src/twyn/trusted_packages/constants.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
from typing import NewType
2-
3-
Url = NewType("Url", str)
4-
5-
TOP_PYPI_PACKAGES = Url(
6-
"https://hugovk.github.io/top-pypi-packages/top-pypi-packages-30-days.min.json"
7-
)
8-
91

102
ADJACENCY_MATRIX = {
113
"1": ["2", "q", "w"],

src/twyn/trusted_packages/references.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import requests
66

7-
from twyn.trusted_packages.constants import TOP_PYPI_PACKAGES, Url
87
from twyn.trusted_packages.exceptions import (
98
EmptyPackagesListError,
109
InvalidJSONError,
@@ -17,8 +16,8 @@
1716
class AbstractPackageReference(ABC):
1817
"""Represents a reference to retrieve trusted packages from."""
1918

20-
def __init__(self, *args: Any, **kwargs: Any) -> None:
21-
super().__init__(*args, **kwargs)
19+
def __init__(self, source: str) -> None:
20+
self.source = source
2221

2322
@abstractmethod
2423
def get_packages(self) -> set[str]:
@@ -28,12 +27,6 @@ def get_packages(self) -> set[str]:
2827
class TopPyPiReference(AbstractPackageReference):
2928
"""Top PyPi packages retrieved from an online source."""
3029

31-
def __init__(
32-
self, source: Url = TOP_PYPI_PACKAGES, *args: Any, **kwargs: Any
33-
) -> None:
34-
super().__init__(*args, **kwargs)
35-
self.source = source
36-
3730
def get_packages(self) -> set[str]:
3831
"""Download and parse online source of top Python Package Index packages."""
3932
packages_info = self._download()
@@ -47,9 +40,7 @@ def _download(self) -> dict[str, Any]:
4740
except requests.exceptions.JSONDecodeError as err:
4841
raise InvalidJSONError from err
4942

50-
logger.debug(
51-
f"Successfully downloaded trusted packages list from {self.source}"
52-
)
43+
logger.debug(f"Successfully downloaded trusted packages list from {self.source}")
5344

5445
return packages_json
5546

tests/core/test_config_handler.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import pytest
66
from tomlkit import TOMLDocument, dumps, parse
7-
from twyn.base.constants import AvailableLoggingLevels
7+
from twyn.base.constants import DEFAULT_TOP_PYPI_PACKAGES, AvailableLoggingLevels
88
from twyn.core.config_handler import ConfigHandler, ReadTwynConfiguration, TwynConfiguration
99
from twyn.core.exceptions import (
1010
AllowlistPackageAlreadyExistsError,
@@ -30,7 +30,11 @@ def test_no_enforce_file_on_non_existent_file(self, mock_is_file):
3030
config = ConfigHandler(enforce_file=False).resolve_config()
3131

3232
assert config == TwynConfiguration(
33-
dependency_file=None, selector_method="all", logging_level=AvailableLoggingLevels.warning, allowlist=set()
33+
dependency_file=None,
34+
selector_method="all",
35+
logging_level=AvailableLoggingLevels.warning,
36+
allowlist=set(),
37+
pypi_reference=DEFAULT_TOP_PYPI_PACKAGES,
3438
)
3539

3640
def test_config_raises_for_unknown_file(self):
@@ -54,6 +58,7 @@ def test_get_twyn_data_from_file(self, pyproject_toml_file):
5458
selector_method="my_selector",
5559
logging_level="debug",
5660
allowlist={"boto4", "boto2"},
61+
pypi_reference=None,
5762
)
5863

5964
def test_write_toml(self, pyproject_toml_file):
@@ -91,6 +96,7 @@ def test_write_toml(self, pyproject_toml_file):
9196
"selector_method": "my_selector",
9297
"logging_level": "debug",
9398
"allowlist": {},
99+
"pypi_reference": DEFAULT_TOP_PYPI_PACKAGES,
94100
},
95101
}
96102
}

tests/main/test_main.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from unittest.mock import patch
22

33
import pytest
4-
from twyn.base.constants import AvailableLoggingLevels
4+
from twyn.base.constants import DEFAULT_TOP_PYPI_PACKAGES, AvailableLoggingLevels
55
from twyn.core.config_handler import ConfigHandler, TwynConfiguration, _get_logging_level
66
from twyn.dependency_parser import RequirementsTxtParser
77
from twyn.main import (
@@ -34,6 +34,7 @@ class TestCheckDependencies:
3434
selector_method="first-letter",
3535
logging_level=AvailableLoggingLevels.info,
3636
allowlist={"boto4", "boto2"},
37+
pypi_reference=DEFAULT_TOP_PYPI_PACKAGES,
3738
),
3839
),
3940
(
@@ -51,6 +52,7 @@ class TestCheckDependencies:
5152
selector_method="nearby-letter",
5253
logging_level=AvailableLoggingLevels.debug,
5354
allowlist={"boto4", "boto2"},
55+
pypi_reference=DEFAULT_TOP_PYPI_PACKAGES,
5456
),
5557
),
5658
(
@@ -63,6 +65,7 @@ class TestCheckDependencies:
6365
selector_method="all",
6466
logging_level=AvailableLoggingLevels.warning,
6567
allowlist=set(),
68+
pypi_reference=DEFAULT_TOP_PYPI_PACKAGES,
6669
),
6770
),
6871
(
@@ -81,6 +84,7 @@ class TestCheckDependencies:
8184
selector_method=None,
8285
logging_level=AvailableLoggingLevels.debug,
8386
allowlist=set(),
87+
pypi_reference=DEFAULT_TOP_PYPI_PACKAGES,
8488
),
8589
),
8690
],
@@ -232,6 +236,7 @@ def test_check_dependencies_ignores_package_in_allowlist(
232236
dependency_file=None,
233237
selector_method="first-letter",
234238
logging_level=AvailableLoggingLevels.info,
239+
pypi_reference=DEFAULT_TOP_PYPI_PACKAGES,
235240
)
236241

237242
with patch("twyn.main.ConfigHandler.resolve_config", return_value=m_config):

tests/trusted_packages/test_references.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def get_packages(self) -> set[str]:
3535
return {"foo", "bar"}
3636

3737
def test_get_packages(self):
38-
assert self.HardcodedPackageReference().get_packages() == {"foo", "bar"}
38+
assert self.HardcodedPackageReference(source="foo").get_packages() == {"foo", "bar"}
3939

4040

4141
class TestTopPyPiReference:

0 commit comments

Comments
 (0)