Skip to content

Commit 157f9d1

Browse files
committed
feat: make config file optional (#295)
1 parent 6de0122 commit 157f9d1

File tree

7 files changed

+89
-25
lines changed

7 files changed

+89
-25
lines changed

src/twyn/cli.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def run(
124124
verbosity=verbosity,
125125
use_cache=not no_cache if no_cache is not None else no_cache,
126126
use_track=False if json else not no_track,
127+
load_config_from_file=True,
127128
)
128129
except TwynError as e:
129130
raise CliError(e.message) from e

src/twyn/config/config_handler.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logging
2-
from dataclasses import asdict, dataclass
2+
from dataclasses import asdict, dataclass, field
33
from enum import Enum
44
from pathlib import Path
55
from typing import Any, Optional, Union
@@ -18,6 +18,7 @@
1818
from twyn.config.exceptions import (
1919
AllowlistPackageAlreadyExistsError,
2020
AllowlistPackageDoesNotExistError,
21+
ConfigFileNotConfiguredError,
2122
InvalidSelectorMethodError,
2223
TOMLError,
2324
)
@@ -43,20 +44,19 @@ class TwynConfiguration:
4344
class ReadTwynConfiguration:
4445
"""Configuration for twyn as set by the user. It may have None values."""
4546

46-
dependency_file: Optional[str]
47-
selector_method: Optional[str]
48-
logging_level: Optional[AvailableLoggingLevels]
49-
allowlist: set[str]
50-
pypi_reference: Optional[str]
51-
use_cache: Optional[bool]
47+
dependency_file: Optional[str] = None
48+
selector_method: Optional[str] = None
49+
logging_level: Optional[AvailableLoggingLevels] = None
50+
allowlist: set[str] = field(default_factory=set)
51+
pypi_reference: Optional[str] = None
52+
use_cache: Optional[bool] = None
5253

5354

5455
class ConfigHandler:
5556
"""Manage reading and writing configurations for Twyn."""
5657

57-
def __init__(self, file_handler: BaseFileHandler, enforce_file: bool = True) -> None:
58+
def __init__(self, file_handler: Optional[BaseFileHandler] = None) -> None:
5859
self.file_handler = file_handler
59-
self._enforce_file = enforce_file
6060

6161
def resolve_config(
6262
self,
@@ -72,8 +72,12 @@ def resolve_config(
7272
7373
It will also handle default values, when appropriate.
7474
"""
75-
toml = self._read_toml()
76-
read_config = self._get_read_config(toml)
75+
if self.file_handler:
76+
toml = self._read_toml()
77+
read_config = self._get_read_config(toml)
78+
else:
79+
# When we're running twyn as a package we may not be interested on loading the config from the config file.
80+
read_config = ReadTwynConfiguration()
7781

7882
# Determine final selector method from CLI, config file, or default
7983
final_selector_method = selector_method or read_config.selector_method or DEFAULT_SELECTOR_METHOD
@@ -150,13 +154,17 @@ def _write_config(self, toml: TOMLDocument, config: ReadTwynConfiguration) -> No
150154
self._write_toml(toml)
151155

152156
def _write_toml(self, toml: TOMLDocument) -> None:
157+
if not self.file_handler:
158+
raise ConfigFileNotConfiguredError("Config file not configured. Cannot perform write operation.")
153159
self.file_handler.write(dumps(toml))
154160

155161
def _read_toml(self) -> TOMLDocument:
162+
if not self.file_handler:
163+
raise ConfigFileNotConfiguredError("Config file not configured. Cannot perform read operation.")
156164
try:
157165
return parse(self.file_handler.read())
158166
except PathNotFoundError:
159-
if not self._enforce_file and self.file_handler.is_handler_of_file(DEFAULT_PROJECT_TOML_FILE):
167+
if self.file_handler.is_handler_of_file(DEFAULT_PROJECT_TOML_FILE):
160168
return TOMLDocument()
161169
raise TOMLError(f"Error reading toml from {self.file_handler.file_path}") from None
162170

src/twyn/config/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,9 @@ class InvalidSelectorMethodError(TwynError):
3636
"""Exception for when an invalid selector method has been specified."""
3737

3838
message = "Invalid `Selector` was provided."
39+
40+
41+
class ConfigFileNotConfiguredError(TwynError):
42+
"""Exception for when a read/write operation has been attempted but no config file was configured."""
43+
44+
message = "Config file not configured."

src/twyn/main.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,18 @@ def check_dependencies(
3232
verbosity: AvailableLoggingLevels = AvailableLoggingLevels.none,
3333
use_cache: Optional[bool] = True,
3434
use_track: bool = False,
35+
load_config_from_file: bool = False,
3536
) -> TyposquatCheckResultList:
3637
"""Check if dependencies could be typosquats."""
37-
config_file_handler = FileHandler(config_file or ConfigHandler.get_default_config_file_path())
38-
config = ConfigHandler(config_file_handler, enforce_file=False).resolve_config(
39-
verbosity=verbosity, selector_method=selector_method, dependency_file=dependency_file, use_cache=use_cache
38+
config = get_config(
39+
load_config_from_file=load_config_from_file,
40+
config_file=config_file,
41+
verbosity=verbosity,
42+
selector_method=selector_method,
43+
dependency_file=dependency_file,
44+
use_cache=use_cache,
4045
)
46+
4147
_set_logging_level(config.logging_level)
4248

4349
cache_handler = CacheHandler() if config.use_cache else None
@@ -68,6 +74,26 @@ def check_dependencies(
6874
return typos_list
6975

7076

77+
def get_config(
78+
load_config_from_file: bool,
79+
config_file: Optional[str],
80+
verbosity: AvailableLoggingLevels,
81+
selector_method: Union[SelectorMethod, None],
82+
dependency_file: Optional[str],
83+
use_cache: Optional[bool],
84+
) -> ConfigHandler:
85+
if load_config_from_file:
86+
config_file_handler = FileHandler(config_file or ConfigHandler.get_default_config_file_path())
87+
else:
88+
config_file_handler = None
89+
return ConfigHandler(config_file_handler).resolve_config(
90+
verbosity=verbosity,
91+
selector_method=selector_method,
92+
dependency_file=dependency_file,
93+
use_cache=use_cache,
94+
)
95+
96+
7197
def _set_logging_level(logging_level: AvailableLoggingLevels) -> None:
7298
logger.setLevel(logging_level.value)
7399
logger.debug("Logging level: %s", logging_level.value)

tests/config/test_config_handler.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from tomlkit import TOMLDocument, dumps, parse
99
from twyn.base.constants import (
1010
DEFAULT_PROJECT_TOML_FILE,
11+
DEFAULT_SELECTOR_METHOD,
1112
DEFAULT_TOP_PYPI_PACKAGES,
1213
DEFAULT_TWYN_TOML_FILE,
1314
AvailableLoggingLevels,
@@ -16,6 +17,7 @@
1617
from twyn.config.exceptions import (
1718
AllowlistPackageAlreadyExistsError,
1819
AllowlistPackageDoesNotExistError,
20+
ConfigFileNotConfiguredError,
1921
InvalidSelectorMethodError,
2022
TOMLError,
2123
)
@@ -29,17 +31,11 @@ class TestConfigHandler:
2931
def throw_exception(self) -> NoReturn:
3032
raise PathNotFoundError
3133

32-
@patch("twyn.file_handler.file_handler.FileHandler.read")
33-
def test_enforce_file_error(self, mock_is_file: Mock) -> None:
34-
mock_is_file.side_effect = self.throw_exception
35-
with pytest.raises(TOMLError):
36-
ConfigHandler(FileHandler(DEFAULT_PROJECT_TOML_FILE), enforce_file=True).resolve_config()
37-
3834
@patch("twyn.file_handler.file_handler.FileHandler.read")
3935
def test_no_enforce_file_on_non_existent_file(self, mock_is_file: Mock) -> None:
4036
"""Resolving the config without enforcing the file to be present gives you defaults."""
4137
mock_is_file.side_effect = self.throw_exception
42-
config = ConfigHandler(FileHandler(DEFAULT_PROJECT_TOML_FILE), enforce_file=False).resolve_config()
38+
config = ConfigHandler(FileHandler(DEFAULT_PROJECT_TOML_FILE)).resolve_config()
4339

4440
assert config == TwynConfiguration(
4541
dependency_file=None,
@@ -141,6 +137,28 @@ def test_get_default_config_file_path_twyn_file_does_not_exist(
141137
assert not twyn_path.exists()
142138
assert ConfigHandler.get_default_config_file_path() == str(pyproject_toml_file)
143139

140+
def test_no_load_config_from_cache(self, pyproject_toml_file: Path) -> None:
141+
"""Check that in case of not loading the config from a file we use the default values."""
142+
# Config file exists
143+
assert pyproject_toml_file.exists()
144+
145+
config = ConfigHandler().resolve_config()
146+
147+
assert config.allowlist == set()
148+
assert config.dependency_file is None
149+
assert config.logging_level == AvailableLoggingLevels.warning
150+
assert config.use_cache is True
151+
assert config.selector_method == DEFAULT_SELECTOR_METHOD
152+
assert config.pypi_reference == DEFAULT_TOP_PYPI_PACKAGES
153+
154+
def test_cannot_write_if_file_not_configured(self) -> None:
155+
with pytest.raises(ConfigFileNotConfiguredError, match="write operation"):
156+
ConfigHandler()._write_toml(Mock())
157+
158+
def test_cannot_read_if_file_not_configured(self) -> None:
159+
with pytest.raises(ConfigFileNotConfiguredError, match="read operation"):
160+
ConfigHandler()._read_toml()
161+
144162

145163
class TestAllowlistConfigHandler:
146164
@patch("twyn.file_handler.file_handler.FileHandler.write")
@@ -210,7 +228,7 @@ def test_valid_selector_methods_accepted(self, valid_selector: str, tmp_path: Pa
210228
"""Test that all valid selector methods are accepted."""
211229
pyproject_toml = tmp_path / "pyproject.toml"
212230
pyproject_toml.write_text("")
213-
config = ConfigHandler(FileHandler(str(pyproject_toml)), enforce_file=False)
231+
config = ConfigHandler(FileHandler(str(pyproject_toml)))
214232

215233
# Should not raise any exception
216234
resolved_config = config.resolve_config(selector_method=valid_selector)
@@ -220,7 +238,7 @@ def test_invalid_selector_method_rejected(self, tmp_path: Path) -> None:
220238
"""Test that invalid selector methods are rejected with appropriate error."""
221239
pyproject_toml = tmp_path / "pyproject.toml"
222240
pyproject_toml.write_text("")
223-
config = ConfigHandler(FileHandler(str(pyproject_toml)), enforce_file=False)
241+
config = ConfigHandler(FileHandler(str(pyproject_toml)))
224242

225243
with pytest.raises(InvalidSelectorMethodError) as exc_info:
226244
config.resolve_config(selector_method="random-selector")

tests/main/test_cli.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ def test_click_arguments_dependency_file(self, mock_check_dependencies: Mock) ->
9191
verbosity=AvailableLoggingLevels.debug,
9292
use_cache=None,
9393
use_track=True,
94+
load_config_from_file=True,
9495
)
9596
]
9697

@@ -114,6 +115,7 @@ def test_click_arguments_dependency_file_in_different_path(self, mock_check_depe
114115
verbosity=AvailableLoggingLevels.none,
115116
use_cache=None,
116117
use_track=True,
118+
load_config_from_file=True,
117119
)
118120
]
119121

@@ -136,6 +138,7 @@ def test_click_arguments_single_dependency_cli(self, mock_check_dependencies: Mo
136138
verbosity=AvailableLoggingLevels.none,
137139
use_cache=None,
138140
use_track=True,
141+
load_config_from_file=True,
139142
)
140143
]
141144

@@ -170,6 +173,7 @@ def test_click_arguments_multiple_dependencies(self, mock_check_dependencies: Mo
170173
verbosity=AvailableLoggingLevels.none,
171174
use_cache=None,
172175
use_track=True,
176+
load_config_from_file=True,
173177
)
174178
]
175179

@@ -187,6 +191,7 @@ def test_click_arguments_default(self, mock_check_dependencies: Mock) -> None:
187191
verbosity=AvailableLoggingLevels.none,
188192
use_cache=None,
189193
use_track=True,
194+
load_config_from_file=True,
190195
)
191196
]
192197

tests/main/test_main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def test_options_priorities_assignation(
114114
3. default values
115115
116116
"""
117-
handler = ConfigHandler(FileHandler(""), enforce_file=False)
117+
handler = ConfigHandler(FileHandler(""))
118118

119119
with patch.object(handler, "_read_toml", return_value=parse(dumps({"tool": {"twyn": file_config}}))):
120120
resolved = handler.resolve_config(

0 commit comments

Comments
 (0)