Skip to content

Commit a733d1f

Browse files
committed
feat: allow to set use_cache from config file
1 parent 85f8cce commit a733d1f

13 files changed

Lines changed: 107 additions & 56 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ twyn run --selector-method <method>
149149

150150
You can save your configurations in a `.toml` file, so you don't need to specify them everytime you run `Twyn` in your terminal.
151151

152-
By default, it will try to find a `twyn.roml` file in your working directory when it's trying to load your configurations. If it does not find it, it will fallback to `pyproject.toml`.
152+
By default, it will try to find a `twyn.toml` file in your working directory when it's trying to load your configurations. If it does not find it, it will fallback to `pyproject.toml`.
153153
However, you can specify a config file as follows:
154154

155155
```sh

src/twyn/base/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
DEFAULT_PROJECT_TOML_FILE = "pyproject.toml"
3030
DEFAULT_TWYN_TOML_FILE = "twyn.toml"
3131
DEFAULT_TOP_PYPI_PACKAGES = "https://hugovk.github.io/top-pypi-packages/top-pypi-packages.min.json"
32+
DEFAULT_USE_CACHE = True
3233

3334

3435
class AvailableLoggingLevels(Enum):

src/twyn/cli.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ def entry_point() -> None:
7474
@click.option(
7575
"--no-cache",
7676
is_flag=True,
77-
default=False,
77+
default=None,
7878
help="Disable use of the trusted packages cache. Always fetch from the source.",
7979
)
8080
@click.option(
@@ -96,7 +96,7 @@ def run(
9696
selector_method: str,
9797
v: bool,
9898
vv: bool,
99-
no_cache: bool,
99+
no_cache: Optional[bool],
100100
no_track: bool,
101101
json: bool,
102102
) -> int:
@@ -122,7 +122,7 @@ def run(
122122
config_file=config,
123123
dependency_file=dependency_file,
124124
verbosity=verbosity,
125-
use_cache=not no_cache,
125+
use_cache=not no_cache if no_cache is not None else no_cache,
126126
use_track=False if json else not no_track,
127127
)
128128
except TwynError as e:

src/twyn/config/config_handler.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
DEFAULT_SELECTOR_METHOD,
1212
DEFAULT_TOP_PYPI_PACKAGES,
1313
DEFAULT_TWYN_TOML_FILE,
14+
DEFAULT_USE_CACHE,
1415
SELECTOR_METHOD_KEYS,
1516
AvailableLoggingLevels,
1617
)
@@ -35,6 +36,7 @@ class TwynConfiguration:
3536
logging_level: AvailableLoggingLevels
3637
allowlist: set[str]
3738
pypi_reference: str
39+
use_cache: bool
3840

3941

4042
@dataclass
@@ -46,6 +48,7 @@ class ReadTwynConfiguration:
4648
logging_level: Optional[AvailableLoggingLevels]
4749
allowlist: set[str]
4850
pypi_reference: Optional[str]
51+
use_cache: Optional[bool]
4952

5053

5154
class ConfigHandler:
@@ -60,6 +63,7 @@ def resolve_config(
6063
selector_method: Optional[str] = None,
6164
dependency_file: Optional[str] = None,
6265
verbosity: AvailableLoggingLevels = AvailableLoggingLevels.none,
66+
use_cache: Optional[bool] = None,
6367
) -> TwynConfiguration:
6468
"""Resolve the configuration for Twyn.
6569
@@ -81,12 +85,20 @@ def resolve_config(
8185
f"Invalid selector_method '{final_selector_method}'. Must be one of: {valid_methods}"
8286
)
8387

88+
if use_cache is not None:
89+
final_use_cache = use_cache
90+
elif read_config.use_cache is not None:
91+
final_use_cache = read_config.use_cache
92+
else:
93+
final_use_cache = DEFAULT_USE_CACHE
94+
8495
return TwynConfiguration(
8596
dependency_file=dependency_file or read_config.dependency_file,
8697
selector_method=final_selector_method,
8798
logging_level=_get_logging_level(verbosity, read_config.logging_level),
8899
allowlist=read_config.allowlist,
89100
pypi_reference=read_config.pypi_reference or DEFAULT_TOP_PYPI_PACKAGES,
101+
use_cache=final_use_cache,
90102
)
91103

92104
def add_package_to_allowlist(self, package_name: str) -> None:
@@ -120,6 +132,7 @@ def _get_read_config(self, toml: TOMLDocument) -> ReadTwynConfiguration:
120132
logging_level=twyn_config_data.get("logging_level"),
121133
allowlist=set(twyn_config_data.get("allowlist", set())),
122134
pypi_reference=twyn_config_data.get("pypi_reference"),
135+
use_cache=twyn_config_data.get("use_cache"),
123136
)
124137

125138
def _write_config(self, toml: TOMLDocument, config: ReadTwynConfiguration) -> None:
@@ -149,7 +162,7 @@ def _read_toml(self) -> TOMLDocument:
149162

150163
@staticmethod
151164
def get_default_config_file_path() -> str:
152-
"""Determine if `twyn.toml` file exists. If not, it returns the default `pyproject.toml` file.
165+
"""Return `twyn.toml` if the file exists. If not, it returns the default `pyproject.toml` file.
153166
154167
It does not fail if the latter does not exist, as it is not mandatory to have a config file to run twyn.
155168
"""

src/twyn/main.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,20 @@ def check_dependencies(
3030
dependency_file: Optional[str] = None,
3131
dependencies: Optional[set[str]] = None,
3232
verbosity: AvailableLoggingLevels = AvailableLoggingLevels.none,
33-
use_cache: bool = True,
33+
use_cache: Optional[bool] = True,
3434
use_track: bool = False,
3535
) -> TyposquatCheckResultList:
3636
"""Check if dependencies could be typosquats."""
3737
config_file_handler = FileHandler(config_file or ConfigHandler.get_default_config_file_path())
3838
config = ConfigHandler(config_file_handler, enforce_file=False).resolve_config(
39-
verbosity=verbosity, selector_method=selector_method, dependency_file=dependency_file
39+
verbosity=verbosity, selector_method=selector_method, dependency_file=dependency_file, use_cache=use_cache
4040
)
4141
_set_logging_level(config.logging_level)
4242

43-
cache_handler = CacheHandler()
43+
cache_handler = CacheHandler() if config.use_cache else None
44+
4445
trusted_packages = TrustedPackages(
45-
names=TopPyPiReference(source=config.pypi_reference, cache_handler=cache_handler).get_packages(use_cache),
46+
names=TopPyPiReference(source=config.pypi_reference, cache_handler=cache_handler).get_packages(),
4647
algorithm=EditDistance(),
4748
selector=get_candidate_selector(config.selector_method),
4849
threshold_class=SimilarityThreshold,

src/twyn/trusted_packages/references.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22
from abc import ABC, abstractmethod
33
from datetime import datetime
4-
from typing import Any
4+
from typing import Any, Union
55

66
import requests
77

@@ -19,43 +19,47 @@
1919
class AbstractPackageReference(ABC):
2020
"""Represents a reference from where to retrieve trusted packages."""
2121

22-
def __init__(self, source: str, cache_handler: CacheHandler) -> None:
22+
def __init__(self, source: str, cache_handler: Union[CacheHandler, None] = None) -> None:
2323
self.source = source
2424
self.cache_handler = cache_handler
2525

2626
@abstractmethod
27-
def get_packages(self, use_cache: bool = True) -> set[str]:
27+
def get_packages(self) -> set[str]:
2828
"""Return the names of the trusted packages available in the reference."""
2929

3030

3131
class TopPyPiReference(AbstractPackageReference):
3232
"""Top PyPi packages retrieved from an online source."""
3333

34-
def get_packages(self, use_cache: bool = True) -> set[str]:
34+
def get_packages(self) -> set[str]:
3535
"""Download and parse online source of top Python Package Index packages."""
3636
packages_to_use = set()
37-
if use_cache:
38-
packages_to_use = self._get_packages_from_cache()
39-
# we don't save the cache here, we keep it as it is so the date remains the original one.
37+
packages_to_use = self._get_packages_from_cache_if_enabled()
38+
# we don't save the cache here, we keep it as it is so the date remains the original one.
4039

4140
if not packages_to_use:
4241
# no cache usage, no cache hit (non-existent or outdated) or cache was empty.
4342
logger.info("Fetching trusted packages from PyPI reference...")
4443
packages_to_use = self._parse(self._download())
45-
if use_cache:
46-
self._save_trusted_packages_to_cache(packages_to_use)
44+
45+
# New packages were downloaded, we create a new entry updating all values.
46+
self._save_trusted_packages_to_cache_if_enabled(packages_to_use)
4747

4848
normalized_packages = normalize_packages(packages_to_use)
4949
return normalized_packages
5050

51-
def _save_trusted_packages_to_cache(self, packages: set[str]) -> None:
51+
def _save_trusted_packages_to_cache_if_enabled(self, packages: set[str]) -> None:
5252
"""Save trusted packages using CacheHandler."""
53+
if not self.cache_handler:
54+
return
5355
cache_entry = CacheEntry(saved_date=datetime.now().date().isoformat(), packages=packages)
5456
self.cache_handler.write_entry(self.source, cache_entry)
5557
logger.debug("Saved %d trusted packages for source %s", len(packages), self.source)
5658

57-
def _get_packages_from_cache(self) -> set[str]:
59+
def _get_packages_from_cache_if_enabled(self) -> set[str]:
5860
"""Get packages from cache if it's present and up to date."""
61+
if not self.cache_handler:
62+
return set()
5963
cache_entry = self.cache_handler.get_cache_entry(self.source)
6064
if not cache_entry:
6165
logger.debug("No cache entry found for source: %s", self.source)

tests/config/test_config_handler.py

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,20 +47,22 @@ def test_no_enforce_file_on_non_existent_file(self, mock_is_file: Mock) -> None:
4747
logging_level=AvailableLoggingLevels.warning,
4848
allowlist=set(),
4949
pypi_reference=DEFAULT_TOP_PYPI_PACKAGES,
50+
use_cache=True,
5051
)
5152

5253
def test_config_raises_for_unknown_file(self) -> None:
5354
with pytest.raises(TOMLError):
5455
ConfigHandler(FileHandler("non-existent-file.toml")).resolve_config()
5556

56-
def test_read_config_values(self, pyproject_toml_file: str) -> None:
57+
def test_read_config_values(self, pyproject_toml_file: Path) -> None:
5758
config = ConfigHandler(file_handler=FileHandler(pyproject_toml_file)).resolve_config()
5859
assert config.dependency_file == "my_file.txt"
5960
assert config.selector_method == "all"
6061
assert config.logging_level == AvailableLoggingLevels.debug
6162
assert config.allowlist == {"boto4", "boto2"}
63+
assert config.use_cache is False
6264

63-
def test_get_twyn_data_from_file(self, pyproject_toml_file: str) -> None:
65+
def test_get_twyn_data_from_file(self, pyproject_toml_file: Path) -> None:
6466
handler = ConfigHandler(FileHandler(str(pyproject_toml_file)))
6567

6668
toml = handler._read_toml()
@@ -71,9 +73,10 @@ def test_get_twyn_data_from_file(self, pyproject_toml_file: str) -> None:
7173
logging_level="debug",
7274
allowlist={"boto4", "boto2"},
7375
pypi_reference=None,
76+
use_cache=False,
7477
)
7578

76-
def test_write_toml(self, pyproject_toml_file: str) -> None:
79+
def test_write_toml(self, pyproject_toml_file: Path) -> None:
7780
handler = ConfigHandler(FileHandler(pyproject_toml_file))
7881
toml = handler._read_toml()
7982

@@ -109,18 +112,34 @@ def test_write_toml(self, pyproject_toml_file: str) -> None:
109112
"logging_level": "debug",
110113
"allowlist": {},
111114
"pypi_reference": DEFAULT_TOP_PYPI_PACKAGES,
115+
"use_cache": False,
112116
},
113117
}
114118
}
115119

116-
def test_get_default_config_file_path_twyn_file_exists(self, pyproject_toml_file: Path) -> None:
120+
def test_get_default_config_file_path_twyn_file_exists(self, tmp_path: Path, pyproject_toml_file: Path) -> None:
117121
assert pyproject_toml_file.exists()
118-
with create_tmp_file(Path("twyn.toml"), ""):
119-
assert ConfigHandler.get_default_config_file_path() == DEFAULT_TWYN_TOML_FILE
122+
twyn_path = tmp_path / DEFAULT_TWYN_TOML_FILE
123+
with (
124+
create_tmp_file(twyn_path, ""),
125+
patch("twyn.config.config_handler.DEFAULT_TWYN_TOML_FILE", new=str(twyn_path)),
126+
patch("twyn.config.config_handler.DEFAULT_PROJECT_TOML_FILE", new=str(pyproject_toml_file)),
127+
):
128+
assert twyn_path.exists()
129+
130+
assert ConfigHandler.get_default_config_file_path() == str(twyn_path)
120131

121-
def test_get_default_config_file_path_twyn_file_does_not_exist(self, pyproject_toml_file: Path) -> None:
132+
def test_get_default_config_file_path_twyn_file_does_not_exist(
133+
self, tmp_path: Path, pyproject_toml_file: Path
134+
) -> None:
122135
assert pyproject_toml_file.exists()
123-
assert ConfigHandler.get_default_config_file_path() == DEFAULT_TWYN_TOML_FILE
136+
twyn_path = tmp_path / DEFAULT_TWYN_TOML_FILE
137+
with (
138+
patch("twyn.config.config_handler.DEFAULT_TWYN_TOML_FILE", new=str(twyn_path)),
139+
patch("twyn.config.config_handler.DEFAULT_PROJECT_TOML_FILE", new=str(pyproject_toml_file)),
140+
):
141+
assert not twyn_path.exists()
142+
assert ConfigHandler.get_default_config_file_path() == str(pyproject_toml_file)
124143

125144

126145
class TestAllowlistConfigHandler:

tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def pyproject_toml_file(tmp_path: Path) -> Iterator[Path]:
197197
selector_method="all"
198198
logging_level="debug"
199199
allowlist=["boto4", "boto2"]
200-
200+
use_cache=false
201201
"""
202202
with create_tmp_file(pyproject_toml, data) as tmp_file:
203203
yield tmp_file

tests/file_handler/test_file_handler.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
from unittest.mock import patch
1+
from pathlib import Path
2+
from unittest.mock import Mock, patch
23

34
import pytest
4-
from twyn.file_handler.exceptions import PathIsNotFileError, PathNotFoundError
5+
from twyn.file_handler.exceptions import PathIsNotFileError
56
from twyn.file_handler.file_handler import FileHandler
67

78

89
class TestFileHandler:
9-
def test_file_exists(self, pyproject_toml_file: str):
10+
def test_file_exists(self, pyproject_toml_file: Path) -> None:
1011
parser = FileHandler(pyproject_toml_file)
1112
assert parser.exists() is True
1213

13-
def test_read_file_success(self, pyproject_toml_file: str):
14+
def test_read_file_success(self, pyproject_toml_file: Path) -> None:
1415
parser = FileHandler(pyproject_toml_file)
1516
read = parser.read()
1617
assert len(read) > 1
@@ -26,10 +27,12 @@ def test_read_file_does_not_exist(
2627
@patch("pathlib.Path.exists")
2728
@patch("pathlib.Path.is_file")
2829
@pytest.mark.parametrize(
29-
("file_exists", "is_file", "exception"),
30-
[(False, False, PathNotFoundError), (True, False, PathIsNotFileError)],
30+
("file_exists", "is_file"),
31+
[(False, False), (True, False)],
3132
)
32-
def test_raise_for_valid_file(self, mock_is_file, mock_exists, file_exists, is_file, exception):
33+
def test_raise_for_valid_file(
34+
self, mock_is_file: Mock, mock_exists: Mock, file_exists: bool, is_file: bool
35+
) -> None:
3336
mock_exists.return_value = file_exists
3437
mock_is_file.return_value = is_file
3538

tests/main/test_cli.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def test_click_arguments_dependency_file(self, mock_check_dependencies: Mock) ->
8989
dependencies=None,
9090
selector_method="first-letter",
9191
verbosity=AvailableLoggingLevels.debug,
92-
use_cache=True,
92+
use_cache=None,
9393
use_track=True,
9494
)
9595
]
@@ -112,7 +112,7 @@ def test_click_arguments_dependency_file_in_different_path(self, mock_check_depe
112112
dependencies=None,
113113
selector_method=None,
114114
verbosity=AvailableLoggingLevels.none,
115-
use_cache=True,
115+
use_cache=None,
116116
use_track=True,
117117
)
118118
]
@@ -134,7 +134,7 @@ def test_click_arguments_single_dependency_cli(self, mock_check_dependencies: Mo
134134
dependencies={"reqests"},
135135
selector_method=None,
136136
verbosity=AvailableLoggingLevels.none,
137-
use_cache=True,
137+
use_cache=None,
138138
use_track=True,
139139
)
140140
]
@@ -168,7 +168,7 @@ def test_click_arguments_multiple_dependencies(self, mock_check_dependencies: Mo
168168
dependencies={"reqests", "reqeusts"},
169169
selector_method=None,
170170
verbosity=AvailableLoggingLevels.none,
171-
use_cache=True,
171+
use_cache=None,
172172
use_track=True,
173173
)
174174
]
@@ -185,7 +185,7 @@ def test_click_arguments_default(self, mock_check_dependencies: Mock) -> None:
185185
selector_method=None,
186186
dependencies=None,
187187
verbosity=AvailableLoggingLevels.none,
188-
use_cache=True,
188+
use_cache=None,
189189
use_track=True,
190190
)
191191
]

0 commit comments

Comments
 (0)