diff --git a/kiwi_scp/commands/cli.py b/kiwi_scp/commands/cli.py index 74c947d..ed1ceed 100644 --- a/kiwi_scp/commands/cli.py +++ b/kiwi_scp/commands/cli.py @@ -6,6 +6,27 @@ from typing import List, Optional import click +class MissingCMDObjectError(ValueError): + """raised if command object can't be found in its module""" + pass + + +class CMDObjectSubclassError(TypeError): + """raised if a command object is not inheriting click.Command""" + pass + + +class CMDUnregisteredError(ValueError): + """raised if commands have not been assigned to a command group""" + + unregistered: List[str] + + def __init__(self, unregistered): + self.unregistered = unregistered + + super().__init__(f"Some commands were not registered in a group above: {unregistered!r}") + + class KiwiCLI(click.MultiCommand): """Command Line Interface spread over multiple files in this directory""" @@ -27,19 +48,19 @@ class KiwiCLI(click.MultiCommand): except ImportError: return - member_name = f"{cmd_name.capitalize()}Command" + cmd_object_name = f"{cmd_name.capitalize()}Command" - if member_name in dir(cmd_module): - member = getattr(cmd_module, member_name) + if cmd_object_name in dir(cmd_module): + cmd_object = getattr(cmd_module, cmd_object_name) - if isinstance(member, click.Command): - return member + if isinstance(cmd_object, click.Command): + return cmd_object else: - raise Exception("Fail class") + raise CMDObjectSubclassError() else: - raise Exception("Fail member name") + raise MissingCMDObjectError() def format_commands(self, ctx: click.Context, formatter: click.HelpFormatter) -> None: commands = { @@ -71,4 +92,4 @@ class KiwiCLI(click.MultiCommand): cmd_names -= set(cmd_list) if len(cmd_names) > 0: - raise Exception(f"Some commands were not registered in a group above: {cmd_names}") + raise CMDUnregisteredError(cmd_names) diff --git a/kiwi_scp/commands/cmd.py b/kiwi_scp/commands/cmd.py index 1da8cc4..70d20e2 100644 --- a/kiwi_scp/commands/cmd.py +++ b/kiwi_scp/commands/cmd.py @@ -23,6 +23,10 @@ class KiwiCommandType(Enum): T = TypeVar("T") +class KiwiCommandNotImplementedError(Exception): + pass + + class KiwiCommand: type: KiwiCommandType = KiwiCommandType.SERVICES enabled_only: bool = False @@ -177,4 +181,4 @@ class KiwiCommand: @classmethod def run_for_filtered_services(cls, instance: Instance, project: Project, services: Services, new_service_names: List[str], **kwargs) -> None: - raise Exception + raise KiwiCommandNotImplementedError() diff --git a/kiwi_scp/config.py b/kiwi_scp/config.py index ad9e923..c60f81c 100644 --- a/kiwi_scp/config.py +++ b/kiwi_scp/config.py @@ -9,6 +9,25 @@ from ._constants import RE_SEMVER, RE_VARNAME, KIWI_CONF_NAME, RESERVED_PROJECT_ from .yaml import YAML +class InvalidFormatError(ValueError): + """raised if format recognition unsuccessful""" + + cls: type + member: Optional[str] + data: str + + def __init__(self, cls, data, member = None): + self.cls = cls + self.data = data + + if member is not None: + self.member = member + super().__init__(f"Invalid {self.cls.__name__!r}.{self.member!r} Format: {self.data!r}") + + else: + super().__init__(f"Invalid {self.cls.__name__!r} Format: {self.data!r}") + + class StorageConfig(BaseModel): """a storage subsection""" @@ -31,7 +50,17 @@ class StorageConfig(BaseModel): else: # undefined format - raise ValueError("Invalid Storage Format") + raise InvalidFormatError(cls, str(values)) + + +class ProjectNameReservedError(ValueError): + """raised if trying to create a project with a reserved name""" + + name: str + + def __init__(self, name): + self.name = name + super().__init__(f"Project name {self.name!r} is reserved!") class ProjectConfig(BaseModel): @@ -58,7 +87,7 @@ class ProjectConfig(BaseModel): """check if project name is allowed""" if value in RESERVED_PROJECT_NAMES: - raise ValueError(f"Project name '{value}' is reserved!") + raise ProjectNameReservedError(value) return value @@ -101,7 +130,7 @@ class ProjectConfig(BaseModel): else: # undefined format - raise ValueError("Invalid Project Format") + raise InvalidFormatError(ProjectConfig, values) class NetworkConfig(BaseModel): @@ -120,6 +149,18 @@ class NetworkConfig(BaseModel): } +class MissingMemberError(ValueError): + """raised if class member is missing a definition""" + + cls: type + member: str + + def __init__(self, cls, member): + self.cls = cls + self.member = member + super().__init__(f"Member {self.cls.__name__!r}.{self.member!r} is required!") + + class KiwiConfig(BaseModel): """represents a kiwi.yml""" @@ -225,7 +266,7 @@ class KiwiConfig(BaseModel): except Exception: # undefined format - raise ValueError("Invalid Shells Format") + raise InvalidFormatError(KiwiConfig, value, "shells") @validator("projects", pre=True) @classmethod @@ -254,7 +295,7 @@ class KiwiConfig(BaseModel): except Exception: # undefined format - raise ValueError("Invalid Projects Format") + raise InvalidFormatError(KiwiConfig, value, "projects") return result @@ -270,7 +311,7 @@ class KiwiConfig(BaseModel): except Exception: # undefined format - raise ValueError("Invalid Projects Format") + raise InvalidFormatError(KiwiConfig, value, "projects") @validator("environment", pre=True) @classmethod @@ -284,7 +325,7 @@ class KiwiConfig(BaseModel): idx = str(var_val).find("=") except Exception: # undefined format - raise ValueError("Invalid Environment Format") + raise InvalidFormatError(KiwiConfig, value, "environment") if idx == -1: # don't split, just define the variable @@ -325,7 +366,7 @@ class KiwiConfig(BaseModel): if value is None: # empty storage - raise ValueError("No Storage Given") + raise MissingMemberError(KiwiConfig, "storage") elif isinstance(value, dict): # native dict format @@ -350,7 +391,7 @@ class KiwiConfig(BaseModel): if value is None: # empty network - raise ValueError("No Network Given") + raise MissingMemberError(KiwiConfig, "network") elif isinstance(value, dict): # native dict format @@ -358,4 +399,4 @@ class KiwiConfig(BaseModel): else: # undefined format - raise ValueError("Invalid Network Format") + raise InvalidFormatError(KiwiConfig, value, "network") diff --git a/tests/test_config.py b/tests/test_config.py index 33a4421..cb3bd90 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -8,11 +8,18 @@ from kiwi_scp.config import KiwiConfig from kiwi_scp.yaml import YAML +class UnCoercibleError(ValueError): + pass + + class UnCoercible: """A class that doesn't have a string representation""" def __str__(self): - raise ValueError + raise UnCoercibleError() + + def __repr__(self) -> str: + return "UnCoercible()" class TestDefault: @@ -130,8 +137,8 @@ class TestShells: assert len(exc_info.value.errors()) == 1 error = exc_info.value.errors()[0] - assert error["msg"] == "Invalid Shells Format" - assert error["type"] == "value_error" + assert error["msg"] == "Invalid 'KiwiConfig'.'shells' Format: UnCoercible()" + assert error["type"] == "value_error.invalidformat" with pytest.raises(ValidationError) as exc_info: KiwiConfig(shells=["/bin/bash", UnCoercible()]) @@ -207,8 +214,8 @@ class TestProject: assert len(exc_info.value.errors()) == 1 error = exc_info.value.errors()[0] - assert error["msg"] == "Invalid Storage Format" - assert error["type"] == "value_error" + assert error["msg"] == "Invalid 'StorageConfig' Format: '{}'" + assert error["type"] == "value_error.invalidformat" def test_short(self): kiwi_dict = { @@ -239,6 +246,15 @@ class TestProject: assert p.enabled assert p.override_storage is None + def test_reserved_name(self): + with pytest.raises(ValidationError) as exc_info: + KiwiConfig(projects={"name": "config"}) + + assert len(exc_info.value.errors()) == 1 + error = exc_info.value.errors()[0] + assert error["msg"] == "Project name 'config' is reserved!" + assert error["type"] == "value_error.projectnamereserved" + def test_invalid_dict(self): with pytest.raises(ValidationError) as exc_info: KiwiConfig(projects={ @@ -248,8 +264,9 @@ class TestProject: assert len(exc_info.value.errors()) == 1 error = exc_info.value.errors()[0] - assert error["msg"] == "Invalid Project Format" - assert error["type"] == "value_error" + assert error["msg"] == "Invalid 'ProjectConfig' Format: " \ + "{'random key 1': 'random value 1', 'random key 2': 'random value 2'}" + assert error["type"] == "value_error.invalidformat" def test_coercible(self): c = KiwiConfig(projects="project") @@ -268,16 +285,16 @@ class TestProject: assert len(exc_info.value.errors()) == 1 error = exc_info.value.errors()[0] - assert error["msg"] == "Invalid Projects Format" - assert error["type"] == "value_error" + assert error["msg"] == "Invalid 'KiwiConfig'.'projects' Format: ['valid', UnCoercible()]" + assert error["type"] == "value_error.invalidformat" with pytest.raises(ValidationError) as exc_info: KiwiConfig(projects=UnCoercible()) assert len(exc_info.value.errors()) == 1 error = exc_info.value.errors()[0] - assert error["msg"] == "Invalid Projects Format" - assert error["type"] == "value_error" + assert error["msg"] == "Invalid 'KiwiConfig'.'projects' Format: UnCoercible()" + assert error["type"] == "value_error.invalidformat" class TestEnvironment: @@ -360,16 +377,16 @@ class TestEnvironment: assert len(exc_info.value.errors()) == 1 error = exc_info.value.errors()[0] - assert error["msg"] == "Invalid Environment Format" - assert error["type"] == "value_error" + assert error["msg"] == "Invalid 'KiwiConfig'.'environment' Format: UnCoercible()" + assert error["type"] == "value_error.invalidformat" with pytest.raises(ValidationError) as exc_info: KiwiConfig(environment=["valid", UnCoercible()]) assert len(exc_info.value.errors()) == 1 error = exc_info.value.errors()[0] - assert error["msg"] == "Invalid Environment Format" - assert error["type"] == "value_error" + assert error["msg"] == "Invalid 'KiwiConfig'.'environment' Format: None" + assert error["type"] == "value_error.invalidformat" class TestStorage: @@ -379,8 +396,8 @@ class TestStorage: assert len(exc_info.value.errors()) == 1 error = exc_info.value.errors()[0] - assert error["msg"] == "No Storage Given" - assert error["type"] == "value_error" + assert error["msg"] == "Member 'KiwiConfig'.'storage' is required!" + assert error["type"] == "value_error.missingmember" def test_dict(self): kiwi_dict = {"directory": "/test/directory"} @@ -395,8 +412,8 @@ class TestStorage: assert len(exc_info.value.errors()) == 1 error = exc_info.value.errors()[0] - assert error["msg"] == "Invalid Storage Format" - assert error["type"] == "value_error" + assert error["msg"] == "Invalid 'StorageConfig' Format: \"{'random key': 'random value'}\"" + assert error["type"] == "value_error.invalidformat" def test_str(self): c = KiwiConfig(storage="/test/directory") @@ -414,8 +431,8 @@ class TestStorage: assert len(exc_info.value.errors()) == 1 error = exc_info.value.errors()[0] - assert error["msg"] == "Invalid Storage Format" - assert error["type"] == "value_error" + assert error["msg"] == "Invalid 'StorageConfig' Format: '{}'" + assert error["type"] == "value_error.invalidformat" class TestNetwork: @@ -425,8 +442,8 @@ class TestNetwork: assert len(exc_info.value.errors()) == 1 error = exc_info.value.errors()[0] - assert error["msg"] == "No Network Given" - assert error["type"] == "value_error" + assert error["msg"] == "Member 'KiwiConfig'.'network' is required!" + assert error["type"] == "value_error.missingmember" def test_dict(self): kiwi_dict = { @@ -470,5 +487,5 @@ class TestNetwork: assert len(exc_info.value.errors()) == 1 error = exc_info.value.errors()[0] - assert error["msg"] == "Invalid Network Format" - assert error["type"] == "value_error" + assert error["msg"] == "Invalid 'KiwiConfig'.'network' Format: True" + assert error["type"] == "value_error.invalidformat"