diff --git a/src/twyn/cli.py b/src/twyn/cli.py index d8f64707..69466110 100644 --- a/src/twyn/cli.py +++ b/src/twyn/cli.py @@ -124,6 +124,7 @@ def run( verbosity=verbosity, use_cache=not no_cache if no_cache is not None else no_cache, use_track=False if json else not no_track, + load_config_from_file=True, ) except TwynError as e: raise CliError(e.message) from e diff --git a/src/twyn/config/config_handler.py b/src/twyn/config/config_handler.py index f1688b09..eb49bafd 100644 --- a/src/twyn/config/config_handler.py +++ b/src/twyn/config/config_handler.py @@ -1,5 +1,5 @@ import logging -from dataclasses import asdict, dataclass +from dataclasses import asdict, dataclass, field from enum import Enum from pathlib import Path from typing import Any, Optional, Union @@ -18,6 +18,7 @@ from twyn.config.exceptions import ( AllowlistPackageAlreadyExistsError, AllowlistPackageDoesNotExistError, + ConfigFileNotConfiguredError, InvalidSelectorMethodError, TOMLError, ) @@ -43,20 +44,19 @@ class TwynConfiguration: class ReadTwynConfiguration: """Configuration for twyn as set by the user. It may have None values.""" - dependency_file: Optional[str] - selector_method: Optional[str] - logging_level: Optional[AvailableLoggingLevels] - allowlist: set[str] - pypi_reference: Optional[str] - use_cache: Optional[bool] + dependency_file: Optional[str] = None + selector_method: Optional[str] = None + logging_level: Optional[AvailableLoggingLevels] = None + allowlist: set[str] = field(default_factory=set) + pypi_reference: Optional[str] = None + use_cache: Optional[bool] = None class ConfigHandler: """Manage reading and writing configurations for Twyn.""" - def __init__(self, file_handler: BaseFileHandler, enforce_file: bool = True) -> None: + def __init__(self, file_handler: Optional[BaseFileHandler] = None) -> None: self.file_handler = file_handler - self._enforce_file = enforce_file def resolve_config( self, @@ -72,8 +72,12 @@ def resolve_config( It will also handle default values, when appropriate. """ - toml = self._read_toml() - read_config = self._get_read_config(toml) + if self.file_handler: + toml = self._read_toml() + read_config = self._get_read_config(toml) + else: + # When we're running twyn as a package we may not be interested on loading the config from the config file. + read_config = ReadTwynConfiguration() # Determine final selector method from CLI, config file, or default 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 self._write_toml(toml) def _write_toml(self, toml: TOMLDocument) -> None: + if not self.file_handler: + raise ConfigFileNotConfiguredError("Config file not configured. Cannot perform write operation.") self.file_handler.write(dumps(toml)) def _read_toml(self) -> TOMLDocument: + if not self.file_handler: + raise ConfigFileNotConfiguredError("Config file not configured. Cannot perform read operation.") try: return parse(self.file_handler.read()) except PathNotFoundError: - if not self._enforce_file and self.file_handler.is_handler_of_file(DEFAULT_PROJECT_TOML_FILE): + if self.file_handler.is_handler_of_file(DEFAULT_PROJECT_TOML_FILE): return TOMLDocument() raise TOMLError(f"Error reading toml from {self.file_handler.file_path}") from None diff --git a/src/twyn/config/exceptions.py b/src/twyn/config/exceptions.py index ce02347d..7baaf37b 100644 --- a/src/twyn/config/exceptions.py +++ b/src/twyn/config/exceptions.py @@ -36,3 +36,9 @@ class InvalidSelectorMethodError(TwynError): """Exception for when an invalid selector method has been specified.""" message = "Invalid `Selector` was provided." + + +class ConfigFileNotConfiguredError(TwynError): + """Exception for when a read/write operation has been attempted but no config file was configured.""" + + message = "Config file not configured." diff --git a/src/twyn/main.py b/src/twyn/main.py index bff69612..ed489fe7 100644 --- a/src/twyn/main.py +++ b/src/twyn/main.py @@ -32,12 +32,18 @@ def check_dependencies( verbosity: AvailableLoggingLevels = AvailableLoggingLevels.none, use_cache: Optional[bool] = True, use_track: bool = False, + load_config_from_file: bool = False, ) -> TyposquatCheckResultList: """Check if dependencies could be typosquats.""" - config_file_handler = FileHandler(config_file or ConfigHandler.get_default_config_file_path()) - config = ConfigHandler(config_file_handler, enforce_file=False).resolve_config( - verbosity=verbosity, selector_method=selector_method, dependency_file=dependency_file, use_cache=use_cache + config = get_config( + load_config_from_file=load_config_from_file, + config_file=config_file, + verbosity=verbosity, + selector_method=selector_method, + dependency_file=dependency_file, + use_cache=use_cache, ) + _set_logging_level(config.logging_level) cache_handler = CacheHandler() if config.use_cache else None @@ -68,6 +74,26 @@ def check_dependencies( return typos_list +def get_config( + load_config_from_file: bool, + config_file: Optional[str], + verbosity: AvailableLoggingLevels, + selector_method: Union[SelectorMethod, None], + dependency_file: Optional[str], + use_cache: Optional[bool], +) -> ConfigHandler: + if load_config_from_file: + config_file_handler = FileHandler(config_file or ConfigHandler.get_default_config_file_path()) + else: + config_file_handler = None + return ConfigHandler(config_file_handler).resolve_config( + verbosity=verbosity, + selector_method=selector_method, + dependency_file=dependency_file, + use_cache=use_cache, + ) + + def _set_logging_level(logging_level: AvailableLoggingLevels) -> None: logger.setLevel(logging_level.value) logger.debug("Logging level: %s", logging_level.value) diff --git a/tests/config/test_config_handler.py b/tests/config/test_config_handler.py index be9bdaba..6e3623bb 100644 --- a/tests/config/test_config_handler.py +++ b/tests/config/test_config_handler.py @@ -8,6 +8,7 @@ from tomlkit import TOMLDocument, dumps, parse from twyn.base.constants import ( DEFAULT_PROJECT_TOML_FILE, + DEFAULT_SELECTOR_METHOD, DEFAULT_TOP_PYPI_PACKAGES, DEFAULT_TWYN_TOML_FILE, AvailableLoggingLevels, @@ -16,6 +17,7 @@ from twyn.config.exceptions import ( AllowlistPackageAlreadyExistsError, AllowlistPackageDoesNotExistError, + ConfigFileNotConfiguredError, InvalidSelectorMethodError, TOMLError, ) @@ -29,17 +31,11 @@ class TestConfigHandler: def throw_exception(self) -> NoReturn: raise PathNotFoundError - @patch("twyn.file_handler.file_handler.FileHandler.read") - def test_enforce_file_error(self, mock_is_file: Mock) -> None: - mock_is_file.side_effect = self.throw_exception - with pytest.raises(TOMLError): - ConfigHandler(FileHandler(DEFAULT_PROJECT_TOML_FILE), enforce_file=True).resolve_config() - @patch("twyn.file_handler.file_handler.FileHandler.read") def test_no_enforce_file_on_non_existent_file(self, mock_is_file: Mock) -> None: """Resolving the config without enforcing the file to be present gives you defaults.""" mock_is_file.side_effect = self.throw_exception - config = ConfigHandler(FileHandler(DEFAULT_PROJECT_TOML_FILE), enforce_file=False).resolve_config() + config = ConfigHandler(FileHandler(DEFAULT_PROJECT_TOML_FILE)).resolve_config() assert config == TwynConfiguration( dependency_file=None, @@ -141,6 +137,28 @@ def test_get_default_config_file_path_twyn_file_does_not_exist( assert not twyn_path.exists() assert ConfigHandler.get_default_config_file_path() == str(pyproject_toml_file) + def test_no_load_config_from_cache(self, pyproject_toml_file: Path) -> None: + """Check that in case of not loading the config from a file we use the default values.""" + # Config file exists + assert pyproject_toml_file.exists() + + config = ConfigHandler().resolve_config() + + assert config.allowlist == set() + assert config.dependency_file is None + assert config.logging_level == AvailableLoggingLevels.warning + assert config.use_cache is True + assert config.selector_method == DEFAULT_SELECTOR_METHOD + assert config.pypi_reference == DEFAULT_TOP_PYPI_PACKAGES + + def test_cannot_write_if_file_not_configured(self) -> None: + with pytest.raises(ConfigFileNotConfiguredError, match="write operation"): + ConfigHandler()._write_toml(Mock()) + + def test_cannot_read_if_file_not_configured(self) -> None: + with pytest.raises(ConfigFileNotConfiguredError, match="read operation"): + ConfigHandler()._read_toml() + class TestAllowlistConfigHandler: @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 """Test that all valid selector methods are accepted.""" pyproject_toml = tmp_path / "pyproject.toml" pyproject_toml.write_text("") - config = ConfigHandler(FileHandler(str(pyproject_toml)), enforce_file=False) + config = ConfigHandler(FileHandler(str(pyproject_toml))) # Should not raise any exception resolved_config = config.resolve_config(selector_method=valid_selector) @@ -220,7 +238,7 @@ def test_invalid_selector_method_rejected(self, tmp_path: Path) -> None: """Test that invalid selector methods are rejected with appropriate error.""" pyproject_toml = tmp_path / "pyproject.toml" pyproject_toml.write_text("") - config = ConfigHandler(FileHandler(str(pyproject_toml)), enforce_file=False) + config = ConfigHandler(FileHandler(str(pyproject_toml))) with pytest.raises(InvalidSelectorMethodError) as exc_info: config.resolve_config(selector_method="random-selector") diff --git a/tests/main/test_cli.py b/tests/main/test_cli.py index 9c363c4c..e0216a4a 100644 --- a/tests/main/test_cli.py +++ b/tests/main/test_cli.py @@ -91,6 +91,7 @@ def test_click_arguments_dependency_file(self, mock_check_dependencies: Mock) -> verbosity=AvailableLoggingLevels.debug, use_cache=None, use_track=True, + load_config_from_file=True, ) ] @@ -114,6 +115,7 @@ def test_click_arguments_dependency_file_in_different_path(self, mock_check_depe verbosity=AvailableLoggingLevels.none, use_cache=None, use_track=True, + load_config_from_file=True, ) ] @@ -136,6 +138,7 @@ def test_click_arguments_single_dependency_cli(self, mock_check_dependencies: Mo verbosity=AvailableLoggingLevels.none, use_cache=None, use_track=True, + load_config_from_file=True, ) ] @@ -170,6 +173,7 @@ def test_click_arguments_multiple_dependencies(self, mock_check_dependencies: Mo verbosity=AvailableLoggingLevels.none, use_cache=None, use_track=True, + load_config_from_file=True, ) ] @@ -187,6 +191,7 @@ def test_click_arguments_default(self, mock_check_dependencies: Mock) -> None: verbosity=AvailableLoggingLevels.none, use_cache=None, use_track=True, + load_config_from_file=True, ) ] diff --git a/tests/main/test_main.py b/tests/main/test_main.py index 0fa9f554..acdfd560 100644 --- a/tests/main/test_main.py +++ b/tests/main/test_main.py @@ -114,7 +114,7 @@ def test_options_priorities_assignation( 3. default values """ - handler = ConfigHandler(FileHandler(""), enforce_file=False) + handler = ConfigHandler(FileHandler("")) with patch.object(handler, "_read_toml", return_value=parse(dumps({"tool": {"twyn": file_config}}))): resolved = handler.resolve_config(