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
19 changes: 18 additions & 1 deletion src/twyn/base/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,33 @@
logger = logging.getLogger("twyn.errors")


class TwynError(click.ClickException):
class TwynError(Exception):
"""
Base exception from where all application errors will inherit.

Provides a default message field, that subclasses will override to provide more information in case it is not provided during the exception handling.
"""

message = ""

def __init__(self, message: str = "") -> None:
super().__init__(message or self.message)


class CliError(click.ClickException):
"""Error that will populate application errors to stdout. It does not inherit from `TwynError`."""

message = "CLI error"

def __init__(self, message: str = "") -> None:
super().__init__(message)

def show(self, file: Optional[IO[Any]] = None) -> None:
logger.debug(self.format_message(), exc_info=True)
logger.error(self.format_message(), exc_info=False)


class PackageNormalizingError(TwynError):
"""Exception for when it is not possible to normalize a package name."""

message = "Failed to normalize pacakges."
22 changes: 14 additions & 8 deletions src/twyn/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
SELECTOR_METHOD_MAPPING,
AvailableLoggingLevels,
)
from twyn.base.exceptions import CliError, TwynError
from twyn.config.config_handler import ConfigHandler
from twyn.file_handler.file_handler import FileHandler
from twyn.main import check_dependencies
Expand Down Expand Up @@ -104,14 +105,19 @@ def run(
if dependency_file and not any(dependency_file.endswith(key) for key in DEPENDENCY_FILE_MAPPING):
raise click.UsageError("Dependency file name not supported.", ctx=click.get_current_context())

errors = check_dependencies(
dependencies=set(dependency) or None,
config_file=config,
dependency_file=dependency_file,
selector_method=selector_method,
verbosity=verbosity,
use_cache=not no_cache,
)
try:
errors = check_dependencies(
dependencies=set(dependency) or None,
config_file=config,
dependency_file=dependency_file,
selector_method=selector_method,
verbosity=verbosity,
use_cache=not no_cache,
)
except TwynError as e:
raise CliError(e.message) from e
except Exception as e:
raise CliError("Unhandled exception occured.") from e

if errors:
for possible_typosquats in errors:
Expand Down
22 changes: 18 additions & 4 deletions src/twyn/config/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,37 @@


class TOMLError(TwynError):
"""TOML exception class."""

message = "TOML parsing error"

def __init__(self, message: str):
super().__init__(message)


class AllowlistError(TwynError):
def __init__(self, package_name: str = ""):
class BaseAllowlistError(TwynError):
"""Base `allowlist` exception."""

message = "Allowlist error"

def __init__(self, package_name: str = "") -> None:
message = self.message.format(package_name) if package_name else self.message
super().__init__(message)


class AllowlistPackageAlreadyExistsError(AllowlistError):
class AllowlistPackageAlreadyExistsError(BaseAllowlistError):
"""Exception class for when a package already exists in the allowlist."""

message = "Package '{}' is already present in the allowlist. Skipping."


class AllowlistPackageDoesNotExistError(AllowlistError):
class AllowlistPackageDoesNotExistError(BaseAllowlistError):
"""Exception class for when it is not possible to locate the desired pacakge in the allowlist."""

message = "Package '{}' is not present in the allowlist. Skipping."


class InvalidSelectorMethodError(TwynError):
"""Exception for when an invalid selector method has been specified."""

message = "Invalid `Selector` was provided."
4 changes: 4 additions & 0 deletions src/twyn/dependency_parser/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@


class NoMatchingParserError(TwynError):
"""Exception raised when no suitable dependency file parser can be automatically determined."""

message = "Could not assign a dependency file parser. Please specify it with --dependency-file"


class MultipleParsersError(TwynError):
"""Exception raised when multiple dependency file parsers are detected."""

message = "Can't auto detect dependencies file to parse. More than one format was found."
6 changes: 5 additions & 1 deletion src/twyn/file_handler/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@


class PathIsNotFileError(TwynError):
"""Exception raised when a specified path exists but is not a regular file."""

message = "Specified dependencies path is not a file"


class PathNotFoundError(TwynError, FileNotFoundError):
class PathNotFoundError(TwynError):
"""Exception raised when a specified file path does not exist in the filesystem."""

message = "Specified dependencies file path does not exist"
4 changes: 4 additions & 0 deletions src/twyn/similarity/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@


class DistanceAlgorithmError(TwynError):
"""Exception raised while running distance algorithm."""

message = "Exception raised while running distance algorithm"


class ThresholdError(TwynError, ValueError):
"""Exception raised when minimum threshold is greater than maximum threshold."""

message = "Minimum threshold cannot be greater than maximum threshold."
15 changes: 13 additions & 2 deletions src/twyn/trusted_packages/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,30 @@


class InvalidJSONError(TwynError):
"""Exception raised when JSON decoding of downloaded packages list fails."""

message = "Could not json decode the downloaded packages list"


class InvalidPyPiFormatError(TwynError, KeyError):
class InvalidPyPiFormatError(TwynError):
"""Exception raised when PyPI JSON format is invalid."""

message = "Invalid JSON format."


class EmptyPackagesListError(TwynError):
"""Exception raised when downloaded packages list is empty."""

message = "Downloaded packages list is empty"


class CharacterNotInMatrixError(TwynError, KeyError): ... # TODO add comments
class CharacterNotInMatrixError(TwynError):
"""Exception raised when a character is not found in the similarity matrix."""

message = "Character not found in similarity matrix"


class InvalidCacheError(TwynError):
"""Error for when the cache content is not valid."""

message = "Invalid cache content"
33 changes: 33 additions & 0 deletions tests/main/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from click.testing import CliRunner
from twyn import cli
from twyn.base.constants import AvailableLoggingLevels
from twyn.base.exceptions import TwynError
from twyn.trusted_packages.cache_handler import CacheEntry, CacheHandler
from twyn.trusted_packages.trusted_packages import TyposquatCheckResult

Expand Down Expand Up @@ -216,3 +217,35 @@ def test_dependency_file_name_has_to_be_recognized(self):
assert isinstance(result.exception, SystemExit)
assert result.exit_code == 2
assert "Dependency file name not supported." in result.output

@patch("twyn.cli.check_dependencies")
def test_base_twyn_error_is_caught_and_wrapped_in_cli_error(self, mock_check_dependencies, caplog):
"""Test that BaseTwynError is caught and wrapped in CliError."""
runner = CliRunner()

# Mock check_dependencies to raise a BaseTwynError
test_error = TwynError("Test base error")
test_error.message = "Test base error message"
mock_check_dependencies.side_effect = test_error

result = runner.invoke(cli.run, ["--dependency", "requests"])

assert result.exit_code == 1
assert isinstance(result.exception, SystemExit)
# Check that the error message was logged
assert "Test base error message" in caplog.text

@patch("twyn.cli.check_dependencies")
def test_unhandled_exception_is_caught_and_wrapped_in_cli_error(self, mock_check_dependencies, caplog):
"""Test that unhandled exceptions are caught and wrapped in CliError."""
runner = CliRunner()

# Mock check_dependencies to raise a generic exception
mock_check_dependencies.side_effect = ValueError("Unexpected error")

result = runner.invoke(cli.run, ["--dependency", "requests"])

assert result.exit_code == 1
assert isinstance(result.exception, SystemExit)
# Check that the generic error message was logged
assert "Unhandled exception occured." in caplog.text