diff options
Diffstat (limited to 'tools')
-rw-r--r-- | tools/cru-py/cru/__init__.py | 10 | ||||
-rw-r--r-- | tools/cru-py/cru/_base.py | 6 | ||||
-rw-r--r-- | tools/cru-py/cru/_error.py | 90 | ||||
-rw-r--r-- | tools/cru-py/cru/_type.py | 4 | ||||
-rw-r--r-- | tools/cru-py/cru/config.py | 6 | ||||
-rw-r--r-- | tools/cru-py/cru/list.py | 2 | ||||
-rw-r--r-- | tools/cru-py/cru/service/__main__.py | 12 | ||||
-rw-r--r-- | tools/cru-py/cru/service/_base.py | 37 | ||||
-rw-r--r-- | tools/cru-py/cru/service/_config.py | 51 |
9 files changed, 136 insertions, 82 deletions
diff --git a/tools/cru-py/cru/__init__.py b/tools/cru-py/cru/__init__.py index 7c1a5f1..66732ea 100644 --- a/tools/cru-py/cru/__init__.py +++ b/tools/cru-py/cru/__init__.py @@ -2,11 +2,11 @@ import sys from ._base import CRU, CruNamespaceError, CRU_NAME_PREFIXES from ._error import ( - cru_unreachable, CruException, - CruUserFriendlyException, + CruLogicError, CruInternalError, CruUnreachableError, + cru_unreachable, ) from ._const import ( CruConstantBase, @@ -40,11 +40,11 @@ __all__ = [ "CRU_NAME_PREFIXES", "check_python_version", "CruException", - "cru_unreachable", - "CruInitError", - "CruUserFriendlyException", "CruInternalError", + "CruLogicError", "CruUnreachableError", + "cru_unreachable", + "CruInitError", "CruConstantBase", "CruDontChange", "CruNotFound", diff --git a/tools/cru-py/cru/_base.py b/tools/cru-py/cru/_base.py index 0a22df4..2599d8f 100644 --- a/tools/cru-py/cru/_base.py +++ b/tools/cru-py/cru/_base.py @@ -1,10 +1,10 @@ from typing import Any from ._helper import remove_none -from ._error import CruInternalError +from ._error import CruException -class CruNamespaceError(CruInternalError): +class CruNamespaceError(CruException): """Raised when a namespace is not found.""" @@ -41,7 +41,7 @@ class _Cru: def check_name_format(name: str) -> tuple[str, str]: no_prefix_name = _Cru._maybe_remove_prefix(name) if no_prefix_name is None: - raise ValueError( + raise CruNamespaceError( f"Name {name} is not prefixed with any of {_Cru.NAME_PREFIXES}." ) return name, no_prefix_name diff --git a/tools/cru-py/cru/_error.py b/tools/cru-py/cru/_error.py index e4bf3d6..e53c787 100644 --- a/tools/cru-py/cru/_error.py +++ b/tools/cru-py/cru/_error.py @@ -1,29 +1,89 @@ -from typing import NoReturn +from __future__ import annotations + +from typing import NoReturn, cast, overload class CruException(Exception): """Base exception class of all exceptions in cru.""" + @overload + def __init__( + self, + message: None = None, + *args, + user_message: str, + **kwargs, + ): ... -class CruUnreachableError(CruException): - """Raised when a code path is unreachable.""" + @overload + def __init__( + self, + message: str, + *args, + user_message: str | None = None, + **kwargs, + ): ... + def __init__( + self, + message: str | None = None, + *args, + user_message: str | None = None, + **kwargs, + ): + if message is None: + message = user_message -def cru_unreachable() -> NoReturn: - raise CruUnreachableError() + super().__init__( + message, + *args, + **kwargs, + ) + self._message: str + self._message = cast(str, message) + self._user_message = user_message + @property + def message(self) -> str: + return self._message -class CruInternalError(CruException): - """Raised when an internal logic error occurs.""" + def get_user_message(self) -> str | None: + return self._user_message + def get_message(self, use_user: bool = True) -> str: + if use_user and self._user_message is not None: + return self._user_message + else: + return self._message -class CruUserFriendlyException(CruException): - def __init__( - self, message: str, user_message: str | None = None, *args, **kwargs - ) -> None: - super().__init__(message, *args, **kwargs) - self._user_message = user_message or message + @property + def is_internal(self) -> bool: + return False @property - def user_message(self) -> str: - return self._user_message + def is_logic_error(self) -> bool: + return False + + +class CruLogicError(CruException): + """Raised when a logic error occurs.""" + + @property + def is_logic_error(self) -> bool: + return True + + +class CruInternalError(CruException): + """Raised when an internal error occurs.""" + + @property + def is_internal(self) -> bool: + return True + + +class CruUnreachableError(CruInternalError): + """Raised when a code path is unreachable.""" + + +def cru_unreachable() -> NoReturn: + raise CruUnreachableError("Code should not reach here!") diff --git a/tools/cru-py/cru/_type.py b/tools/cru-py/cru/_type.py index 96d5d4b..1f81da3 100644 --- a/tools/cru-py/cru/_type.py +++ b/tools/cru-py/cru/_type.py @@ -1,7 +1,7 @@ from collections.abc import Iterable from typing import Any -from ._error import CruException, CruInternalError +from ._error import CruException, CruLogicError from ._iter import CruIterator @@ -17,7 +17,7 @@ class CruTypeSet(set[type]): def __init__(self, *types: type): type_set = CruIterator(types).filter(lambda t: t is not None).to_set() if not CruIterator(type_set).all(lambda t: isinstance(t, type)): - raise CruInternalError("TypeSet can only contain type.") + raise CruLogicError("TypeSet can only contain type.") super().__init__(type_set) def check_value( diff --git a/tools/cru-py/cru/config.py b/tools/cru-py/cru/config.py index b71f042..8558106 100644 --- a/tools/cru-py/cru/config.py +++ b/tools/cru-py/cru/config.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Any, TypeVar, Generic -from ._error import CruInternalError, CruException +from ._error import CruException from .list import CruUniqueKeyList from .value import ( INTEGER_VALUE_TYPE, @@ -110,8 +110,8 @@ class ConfigItem(Generic[_T]): self.value_type.check_value(v) return v except CruValueTypeError as e: - raise CruInternalError( - "Config value generator returns an invalid value." + raise CruConfigError( + "Config value generator returns an invalid value.", self ) from e def copy(self) -> "ConfigItem": diff --git a/tools/cru-py/cru/list.py b/tools/cru-py/cru/list.py index 53c3c77..42caab3 100644 --- a/tools/cru-py/cru/list.py +++ b/tools/cru-py/cru/list.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Callable, Iterator from typing import Any, Generic, Iterable, TypeAlias, TypeVar -from ._base import CruInternalError +from ._error import CruInternalError from ._iter import CruIterator from ._const import CruNotFound diff --git a/tools/cru-py/cru/service/__main__.py b/tools/cru-py/cru/service/__main__.py index c218bc6..1c10e82 100644 --- a/tools/cru-py/cru/service/__main__.py +++ b/tools/cru-py/cru/service/__main__.py @@ -1,4 +1,4 @@ -from cru import CruUserFriendlyException +from cru import CruException from ._app import create_app @@ -11,6 +11,10 @@ def main(): if __name__ == "__main__": try: main() - except CruUserFriendlyException as e: - print(f"Error: {e.user_message}") - exit(1) + except CruException as e: + user_message = e.get_user_message() + if user_message is not None: + print(f"Error: {user_message}") + exit(1) + else: + raise diff --git a/tools/cru-py/cru/service/_base.py b/tools/cru-py/cru/service/_base.py index 124acd5..4454c2c 100644 --- a/tools/cru-py/cru/service/_base.py +++ b/tools/cru-py/cru/service/_base.py @@ -7,17 +7,27 @@ import os from pathlib import Path from typing import TypeVar, overload -from cru import CruException, CruInternalError, CruPath, CruUserFriendlyException +from cru import CruException, CruLogicError, CruPath _Feature = TypeVar("_Feature", bound="AppFeatureProvider") OWNER_NAME = "crupest" -class InternalAppException(CruInternalError): +class AppError(CruException): pass +class AppFeatureError(AppError): + def __init__(self, message, feature: type | str, *args, **kwargs): + super().__init__(message, *args, **kwargs) + self._feature = feature + + @property + def feature(self) -> type | str: + return self._feature + + class AppPathError(CruException): def __init__(self, message, _path: str | Path, *args, **kwargs): super().__init__(message, *args, **kwargs) @@ -152,12 +162,12 @@ class AppRootPath(AppPath): @property def full_path(self) -> CruPath: if self._full_path is None: - raise CruInternalError("App root path is not set yet.") + raise AppError("App root path is not set yet.") return self._full_path def setup(self, path: os.PathLike) -> None: if self._full_path is not None: - raise CruInternalError("App root path is already set.") + raise AppError("App root path is already set.") self._full_path = CruPath(path) @@ -270,7 +280,7 @@ class AppBase: @staticmethod def get_instance() -> AppBase: if AppBase._instance is None: - raise CruInternalError("App instance not initialized") + raise AppError("App instance not initialized") return AppBase._instance def __init__(self, name: str): @@ -312,8 +322,9 @@ class AppBase: def ensure_app_initialized(self) -> AppRootPath: if not self.app_initialized: - raise CruUserFriendlyException( - "Root directory does not exist. Please run 'init' to create one." + raise AppError( + user_message="Root directory does not exist. " + "Please run 'init' to create one." ) return self.root @@ -328,7 +339,9 @@ class AppBase: def add_feature(self, feature: _Feature) -> _Feature: for f in self.features: if f.name == feature.name: - raise CruInternalError(f"Duplicate feature name: {feature.name}.") + raise AppFeatureError( + f"Duplicate feature name: {feature.name}.", feature.name + ) self._features.append(feature) return feature @@ -365,14 +378,12 @@ class AppBase: if isinstance(f, feature): return f else: - raise InternalAppException( - "Argument must be the name of feature or its class." - ) + raise CruLogicError("Argument must be the name of feature or its class.") - raise InternalAppException(f"Feature {feature} not found.") + raise AppFeatureError(f"Feature {feature} not found.", feature) def get_path(self, name: str) -> AppFeaturePath: for p in self._paths: if p.id == name or p.name == name: return p - raise InternalAppException(f"Application path {name} not found.") + raise AppPathError(f"Application path {name} not found.", name) diff --git a/tools/cru-py/cru/service/_config.py b/tools/cru-py/cru/service/_config.py index f2beeb2..9c91c93 100644 --- a/tools/cru-py/cru/service/_config.py +++ b/tools/cru-py/cru/service/_config.py @@ -1,7 +1,7 @@ from collections.abc import Iterable from typing import Any, NoReturn -from cru import CruException, CruUserFriendlyException +from cru import CruException from cru.config import Configuration, ConfigItem from cru.value import ( INTEGER_VALUE_TYPE, @@ -26,16 +26,6 @@ class AppConfigError(CruException): def configuration(self) -> Configuration: return self._configuration - @property - def friendly_error_message(self) -> str: - raise NotImplementedError("Subclasses must implement this method.") - - def to_friendly_error(self) -> CruUserFriendlyException: - return CruUserFriendlyException(self.friendly_error_message) - - def raise_friendly_error(self) -> NoReturn: - raise self.to_friendly_error() from self - class AppConfigFileError(AppConfigError): def __init__( @@ -64,8 +54,7 @@ class AppConfigFileNotFoundError(AppConfigFileError): def file_path(self) -> str: return self._file_path - @property - def friendly_error_message(self) -> str: + def get_user_message(self) -> str: return f"Config file not found at {self.file_path}. You may need to create one." @@ -76,22 +65,18 @@ class AppConfigFileParseError(AppConfigFileError): configuration: Configuration, file_content: str, *args, - cause: ParseError | None = None, **kwargs, ) -> None: super().__init__(message, configuration, *args, **kwargs) self._file_content = file_content - if cause is not None: - self._cause = cause - self._cause = self.__cause__ # type: ignore + self.__cause__: ParseError @property def file_content(self) -> str: return self._file_content - @property - def friendly_error_message(self) -> str: - return f"Error while parsing config file at line {self._cause.line_number}." + def get_user_message(self) -> str: + return f"Error while parsing config file at line {self.__cause__.line_number}." class AppConfigFileEntryError(AppConfigFileError): @@ -123,8 +108,7 @@ class AppConfigFileEntryError(AppConfigFileError): def friendly_message_head(self) -> str: return "Error entries found in config file" - @property - def friendly_error_message(self) -> str: + def get_user_message(self) -> str: return ( f"{self.friendly_message_head}:\n" f"{self.entries_to_friendly_message(self.error_entries)}" @@ -316,24 +300,19 @@ class ConfigManager(AppCommandFeatureProvider): ) from ExceptionGroup("Multiple format errors occurred.", errors) return value_dict - def _read_config_file(self, friendly: bool = False) -> dict[str, Any]: - try: - parsed = self._parse_config_file() - entry_groups = parsed.cru_iter().group_by(lambda e: e.key) - entry_dict = self._check_duplicate(entry_groups) - entry_dict = self._check_defined(entry_dict) - value_dict = self._check_type(entry_dict) - return value_dict - except AppConfigError as e: - if friendly: - e.raise_friendly_error() - raise + def _read_config_file(self) -> dict[str, Any]: + parsed = self._parse_config_file() + entry_groups = parsed.cru_iter().group_by(lambda e: e.key) + entry_dict = self._check_duplicate(entry_groups) + entry_dict = self._check_defined(entry_dict) + value_dict = self._check_type(entry_dict) + return value_dict def reload_config_file(self) -> bool: self.configuration.reset_all() value_dict = self._read_config_file() - # TODO: Continue here! - for key, value in config_dict.items(): + for key, value in value_dict.items(): + # TODO: Continue here! self.configuration.set(key, value) return True |