Skip to content

Commit 6de0122

Browse files
authored
feat: allow to set use_cache from config file (#292)
1 parent 138eaea commit 6de0122

File tree

13 files changed

+84
-48
lines changed

13 files changed

+84
-48
lines changed

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: 13 additions & 0 deletions
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:

src/twyn/file_handler/file_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,4 @@ def delete(self, delete_parent_dir: bool = False) -> None:
6565
)
6666

6767
def _get_file_path(self, file_path: str) -> Path:
68-
return (Path(os.getcwd()) / Path(file_path)).resolve()
68+
return Path(os.path.abspath(os.path.join(os.getcwd(), file_path)))

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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ 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:
@@ -59,6 +60,7 @@ def test_read_config_values(self, pyproject_toml_file: Path) -> None:
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

6365
def test_get_twyn_data_from_file(self, pyproject_toml_file: Path) -> None:
6466
handler = ConfigHandler(FileHandler(str(pyproject_toml_file)))
@@ -71,6 +73,7 @@ def test_get_twyn_data_from_file(self, pyproject_toml_file: Path) -> None:
7173
logging_level="debug",
7274
allowlist={"boto4", "boto2"},
7375
pypi_reference=None,
76+
use_cache=False,
7477
)
7578

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

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

0 commit comments

Comments
 (0)