Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/twyn/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 20 additions & 12 deletions src/twyn/config/config_handler.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -18,6 +18,7 @@
from twyn.config.exceptions import (
AllowlistPackageAlreadyExistsError,
AllowlistPackageDoesNotExistError,
ConfigFileNotConfiguredError,
InvalidSelectorMethodError,
TOMLError,
)
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions src/twyn/config/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
32 changes: 29 additions & 3 deletions src/twyn/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
36 changes: 27 additions & 9 deletions tests/config/test_config_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -16,6 +17,7 @@
from twyn.config.exceptions import (
AllowlistPackageAlreadyExistsError,
AllowlistPackageDoesNotExistError,
ConfigFileNotConfiguredError,
InvalidSelectorMethodError,
TOMLError,
)
Expand All @@ -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,
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand Down
5 changes: 5 additions & 0 deletions tests/main/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
]

Expand All @@ -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,
)
]

Expand All @@ -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,
)
]

Expand Down Expand Up @@ -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,
)
]

Expand All @@ -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,
)
]

Expand Down
2 changes: 1 addition & 1 deletion tests/main/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down