diff options
| -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 | 
