diff options
| author | crupest <crupest@outlook.com> | 2024-11-11 01:12:29 +0800 | 
|---|---|---|
| committer | Yuqian Yang <crupest@crupest.life> | 2025-01-18 17:38:07 +0800 | 
| commit | f8a23e8e20c416dabd5578a1f121b6e602c959fa (patch) | |
| tree | ae0e509d11b974b6fda8fbef985f1e538913e6fd /tools/cru-py/cru | |
| parent | a7bbf582b695bb25d1e01152a9e037ad3b34e954 (diff) | |
| download | crupest-f8a23e8e20c416dabd5578a1f121b6e602c959fa.tar.gz crupest-f8a23e8e20c416dabd5578a1f121b6e602c959fa.tar.bz2 crupest-f8a23e8e20c416dabd5578a1f121b6e602c959fa.zip | |
HALF WORK: 2024.1.18
Diffstat (limited to 'tools/cru-py/cru')
| -rw-r--r-- | tools/cru-py/cru/config.py | 68 | ||||
| -rw-r--r-- | tools/cru-py/cru/list.py | 3 | ||||
| -rw-r--r-- | tools/cru-py/cru/service/_base.py | 9 | ||||
| -rw-r--r-- | tools/cru-py/cru/service/_config.py | 159 | ||||
| -rw-r--r-- | tools/cru-py/cru/service/_template.py | 17 | ||||
| -rw-r--r-- | tools/cru-py/cru/template.py | 16 | 
6 files changed, 191 insertions, 81 deletions
| diff --git a/tools/cru-py/cru/config.py b/tools/cru-py/cru/config.py index 8558106..9efcd55 100644 --- a/tools/cru-py/cru/config.py +++ b/tools/cru-py/cru/config.py @@ -40,7 +40,6 @@ class ConfigItem(Generic[_T]):          self._value_type = value_type          self._value = value          self._default = default -        self._default_value: _T | None = None      @property      def name(self) -> str: @@ -61,18 +60,25 @@ class ConfigItem(Generic[_T]):      @property      def value(self) -> _T:          if self._value is None: -            raise CruConfigError("Config value is not set.", self) +            raise CruConfigError( +                "Config value is not set.", +                self, +                user_message=f"Config item {self.name} is not set.", +            )          return self._value      @property -    def value_or_default(self) -> _T: -        if self._value is not None: -            return self._value -        elif self._default_value is not None: -            return self._default_value +    def value_str(self) -> str: +        return self.value_type.convert_value_to_str(self.value) + +    def set_value(self, v: _T | str, allow_convert_from_str=False): +        if allow_convert_from_str: +            self._value = self.value_type.check_value_or_try_convert_from_str(v)          else: -            self._default_value = self.generate_default_value() -            return self._default_value +            self._value = self.value_type.check_value(v) + +    def reset(self): +        self._value = None      @property      def default(self) -> ValueGeneratorBase[_T] | _T | None: @@ -82,21 +88,6 @@ class ConfigItem(Generic[_T]):      def can_generate_default(self) -> bool:          return self.default is not None -    def set_value( -        self, v: _T | str, *, empty_is_default=True, allow_convert_from_str=True -    ): -        if empty_is_default and v == "": -            self._value = None -        elif allow_convert_from_str: -            self._value = self.value_type.check_value_or_try_convert_from_str(v) -        else: -            self._value = self.value_type.check_value(v) - -    def reset(self, clear_default_cache=False): -        if clear_default_cache: -            self._default_value = None -        self._value = None -      def generate_default_value(self) -> _T:          if self.default is None:              raise CruConfigError( @@ -122,12 +113,20 @@ class ConfigItem(Generic[_T]):              self.value,              self.default,          ) +     +    @property +    def description_str(self) -> str: +        return f"{self.name} ({self.value_type.name}): {self.description}"  class Configuration(CruUniqueKeyList[ConfigItem[Any], str]):      def __init__(self):          super().__init__(lambda c: c.name) +    @property +    def all_not_set(self) -> bool: +        return self.cru_iter().all(lambda item: not item.is_set) +      def add_text_config(          self,          name: str, @@ -150,9 +149,21 @@ class Configuration(CruUniqueKeyList[ConfigItem[Any], str]):          self.add(item)          return item -    def reset_all(self, clear_default_cache=False) -> None: +    def set_config_item( +        self, +        name: str, +        value: Any | str, +        allow_convert_from_str=True, +    ) -> None: +        item = self.get(name) +        item.set_value( +            value, +            allow_convert_from_str=allow_convert_from_str, +        ) + +    def reset_all(self) -> None:          for item in self: -            item.reset(clear_default_cache) +            item.reset()      def to_dict(self) -> dict[str, Any]:          return {item.name: item.value for item in self} @@ -165,14 +176,11 @@ class Configuration(CruUniqueKeyList[ConfigItem[Any], str]):      def set_value_dict(          self,          value_dict: dict[str, Any], -        *, -        empty_is_default: bool = True, -        allow_convert_from_str: bool = True, +        allow_convert_from_str: bool = False,      ) -> None:          for name, value in value_dict.items():              item = self.get(name)              item.set_value(                  value, -                empty_is_default=empty_is_default,                  allow_convert_from_str=allow_convert_from_str,              ) diff --git a/tools/cru-py/cru/list.py b/tools/cru-py/cru/list.py index 42caab3..e329ae2 100644 --- a/tools/cru-py/cru/list.py +++ b/tools/cru-py/cru/list.py @@ -147,3 +147,6 @@ class CruUniqueKeyList(Generic[_T, _K]):      def __len__(self) -> int:          return len(self._list) + +    def cru_iter(self) -> CruIterator[_T]: +        return CruIterator(self._list) diff --git a/tools/cru-py/cru/service/_base.py b/tools/cru-py/cru/service/_base.py index 4454c2c..1ada93b 100644 --- a/tools/cru-py/cru/service/_base.py +++ b/tools/cru-py/cru/service/_base.py @@ -142,7 +142,7 @@ class AppFeaturePath(AppPath):      @property      def full_path(self) -> CruPath: -        return CruPath(self.parent.full_path, self.name) +        return CruPath(self.parent.full_path, self.name).resolve()  class AppRootPath(AppPath): @@ -217,7 +217,12 @@ class CommandDispatcher(AppFeatureProvider):              required=True,              type=str,          ) -        subparsers = arg_parser.add_subparsers(dest="command") +        subparsers = arg_parser.add_subparsers( +            dest="command", +            required=True, +            help="The management command to execute.", +            metavar="COMMAND", +        )          for feature in self.app.features:              if isinstance(feature, AppCommandFeatureProvider):                  info = feature.get_command_info() diff --git a/tools/cru-py/cru/service/_config.py b/tools/cru-py/cru/service/_config.py index 9c91c93..53cce28 100644 --- a/tools/cru-py/cru/service/_config.py +++ b/tools/cru-py/cru/service/_config.py @@ -1,5 +1,5 @@  from collections.abc import Iterable -from typing import Any, NoReturn +from typing import Any  from cru import CruException  from cru.config import Configuration, ConfigItem @@ -54,9 +54,6 @@ class AppConfigFileNotFoundError(AppConfigFileError):      def file_path(self) -> str:          return self._file_path -    def get_user_message(self) -> str: -        return f"Config file not found at {self.file_path}. You may need to create one." -  class AppConfigFileParseError(AppConfigFileError):      def __init__( @@ -99,9 +96,8 @@ class AppConfigFileEntryError(AppConfigFileError):      def entries_to_friendly_message(          entries: Iterable[SimpleLineConfigParser.Entry],      ) -> str: -        return "".join( -            f"line {entry.line_number}: {entry.key}={entry.value}\n" -            for entry in entries +        return "\n".join( +            f"line {entry.line_number}: {entry.key}={entry.value}" for entry in entries          )      @property @@ -121,11 +117,40 @@ class AppConfigDuplicateEntryError(AppConfigFileEntryError):          return "Duplicate entries found in config file" -class AppConfigEntryKeyNotDefinedError(AppConfigFileEntryError): +class AppConfigEntryKeyError(AppConfigFileEntryError): +    def __init__( +        self, +        message: str, +        configuration: Configuration, +        undefined_entries: Iterable[SimpleLineConfigParser.Entry], +        unset_items: Iterable[ConfigItem], +        *args, +        **kwargs, +    ) -> None: +        super().__init__(message, configuration, undefined_entries, *args, **kwargs) +        self._unset_items = list(unset_items) + +    @property +    def unset_items(self) -> list[ConfigItem]: +        return self._unset_items +      @property      def friendly_message_head(self) -> str:          return "Entry key not defined in app config" +    @property +    def unset_items_message(self) -> str: +        head = "App config items are not set in app config:\n" +        return head + "\n".join([item.name for item in self.unset_items]) + +    def get_user_message(self): +        m = [] +        if len(self.error_entries) > 0: +            m.append(super().get_user_message()) +        if len(self.unset_items) > 0: +            m.append(self.unset_items_message) +        return "\n".join(m) +  class AppConfigEntryValueFormatError(AppConfigFileEntryError):      @property @@ -153,15 +178,17 @@ class ConfigManager(AppCommandFeatureProvider):          self._init_app_defined_items()      def _init_app_defined_items(self) -> None: +        prefix = OWNER_NAME.upper() +          def _add_text(name: str, description: str) -> None:              self.configuration.add( -                ConfigItem(f"{OWNER_NAME}_{name}", description, TEXT_VALUE_TYPE) +                ConfigItem(f"{prefix}_{name}", description, TEXT_VALUE_TYPE)              )          def _add_uuid(name: str, description: str) -> None:              self.configuration.add(                  ConfigItem( -                    f"{OWNER_NAME}_{name}", +                    f"{prefix}_{name}",                      description,                      TEXT_VALUE_TYPE,                      default=UuidValueGenerator(), @@ -173,7 +200,7 @@ class ConfigManager(AppCommandFeatureProvider):          ) -> None:              self.configuration.add(                  ConfigItem( -                    f"{OWNER_NAME}_{name}", +                    f"{prefix}_{name}",                      description,                      TEXT_VALUE_TYPE,                      default=RandomStringValueGenerator(length, secure), @@ -182,7 +209,7 @@ class ConfigManager(AppCommandFeatureProvider):          def _add_int(name: str, description: str) -> None:              self.configuration.add( -                ConfigItem(f"{OWNER_NAME}_{name}", description, INTEGER_VALUE_TYPE) +                ConfigItem(f"{prefix}_{name}", description, INTEGER_VALUE_TYPE)              )          _add_text("DOMAIN", "domain name") @@ -227,12 +254,55 @@ class ConfigManager(AppCommandFeatureProvider):      def config_file_path(self) -> AppFeaturePath:          return self._config_file_path +    def get_config_str_dict(self) -> dict[str, str]: +        self.reload_config_file() +        return self.configuration.to_str_dict() + +    def _set_with_default(self) -> None: +        if not self.configuration.all_not_set: +            raise AppConfigError( +                "Config is not clean. " +                "Some config items are already set. " +                "Can't set again with default value.", +                self.configuration, +            ) +        for item in self.configuration: +            if item.can_generate_default: +                item.set_value(item.generate_default_value()) + +    def _to_config_file_content(self) -> str: +        content = "".join( +            [ +                f"{item.name}={item.value_str if item.is_set else ''}\n" +                for item in self.configuration +            ] +        ) +        return content + +    def _create_init_config_file(self) -> None: +        if self.config_file_path.check_self(): +            raise AppConfigError( +                "Config file already exists.", +                self.configuration, +                user_message=f"The config file at " +                f"{self.config_file_path.full_path_str} already exists.", +            ) +        self._set_with_default() +        self.config_file_path.ensure() +        with open( +            self.config_file_path.full_path, "w", encoding="utf-8", newline="\n" +        ) as file: +            file.write(self._to_config_file_content()) +      def _parse_config_file(self) -> SimpleLineConfigParser.Result:          if not self.config_file_path.check_self():              raise AppConfigFileNotFoundError(                  "Config file not found.",                  self.configuration,                  self.config_file_path.full_path_str, +                user_message=f"The config file at " +                f"{self.config_file_path.full_path_str} does not exist. " +                f"You can create an initial one with 'init' command.",              )          text = self.config_file_path.full_path.read_text() @@ -244,6 +314,11 @@ class ConfigManager(AppCommandFeatureProvider):                  "Failed to parse config file.", self.configuration, text              ) from e +    def _parse_and_print_config_file(self) -> None: +        parse_result = self._parse_config_file() +        for entry in parse_result: +            print(f"{entry.key}={entry.value}") +      def _check_duplicate(          self,          parse_result: dict[str, list[SimpleLineConfigParser.Entry]], @@ -252,9 +327,8 @@ class ConfigManager(AppCommandFeatureProvider):          duplicate_entries: list[SimpleLineConfigParser.Entry] = []          for key, entries in parse_result.items():              entry_dict[key] = entries[0] -            for entry in entries[1:]: -                duplicate_entries.append(entry) - +            if len(entries) > 1: +                duplicate_entries.extend(entries)          if len(duplicate_entries) > 0:              raise AppConfigDuplicateEntryError(                  "Duplicate entries found.", self.configuration, duplicate_entries @@ -262,18 +336,23 @@ class ConfigManager(AppCommandFeatureProvider):          return entry_dict -    def _check_defined( +    def _check_key(          self, entry_dict: dict[str, SimpleLineConfigParser.Entry]      ) -> dict[str, SimpleLineConfigParser.Entry]:          undefined: list[SimpleLineConfigParser.Entry] = []          for key, entry in entry_dict.items():              if not self.configuration.has_key(key):                  undefined.append(entry) -        if len(undefined) > 0: -            raise AppConfigEntryKeyNotDefinedError( +        unset_items: list[ConfigItem] = [] +        for item in self.configuration: +            if item.name not in entry_dict or entry_dict[item.name].value == "": +                unset_items.append(item) +        if len(undefined) > 0 or len(unset_items) > 0: +            raise AppConfigEntryKeyError(                  "Entry keys are not defined in app config.",                  self.configuration,                  undefined, +                unset_items,              )          return entry_dict @@ -286,9 +365,12 @@ class ConfigManager(AppCommandFeatureProvider):          for key, entry in entry_dict.items():              config_item = self.configuration.get(key)              try: -                value_dict[key] = config_item.value_type.convert_str_to_value( -                    entry.value -                ) +                if entry.value == "": +                    value_dict[key] = None +                else: +                    value_dict[key] = config_item.value_type.convert_str_to_value( +                        entry.value +                    )              except CruValueTypeError as e:                  error_entries.append(entry)                  errors.append(e) @@ -304,32 +386,38 @@ class ConfigManager(AppCommandFeatureProvider):          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) +        entry_dict = self._check_key(entry_dict)          value_dict = self._check_type(entry_dict)          return value_dict -    def reload_config_file(self) -> bool: +    def reload_config_file(self):          self.configuration.reset_all()          value_dict = self._read_config_file()          for key, value in value_dict.items(): -            # TODO: Continue here! -            self.configuration.set(key, value) -        return True +            self.configuration.set_config_item(key, value) -    def print_app_config_info(self): +    def _print_app_config_info(self):          for item in self.configuration: -            print(f"{item.name} ({item.value_type.name}): {item.description}") +            print(item.description_str)      def get_command_info(self):          return "config", "Manage configuration."      def setup_arg_parser(self, arg_parser) -> None: -        subparsers = arg_parser.add_subparsers(dest="config_command") +        subparsers = arg_parser.add_subparsers( +            dest="config_command", required=True, metavar="CONFIG_COMMAND" +        ) +        _init_parser = subparsers.add_parser( +            "init", help="Create an initial configuration file." +        )          _print_app_parser = subparsers.add_parser(              "print-app",              help="Print application configuration information "              "of the items defined in the application.",          ) +        _print_parser = subparsers.add_parser( +            "print", help="Print current configuration." +        )          _check_config_parser = subparsers.add_parser(              "check",              help="Check the validity of the configuration file.", @@ -343,5 +431,14 @@ class ConfigManager(AppCommandFeatureProvider):          )      def run_command(self, args) -> None: -        if args.config_command == "print-app": -            self.print_app_config_info() +        if args.config_command == "init": +            self._create_init_config_file() +        elif args.config_command == "print-app": +            self._print_app_config_info() +        elif args.config_command == "print": +            self._parse_and_print_config_file() +        elif args.config_command == "check": +            if args.format_only: +                self._parse_config_file() +            else: +                self._read_config_file() diff --git a/tools/cru-py/cru/service/_template.py b/tools/cru-py/cru/service/_template.py index cc7fddf..6ab1e69 100644 --- a/tools/cru-py/cru/service/_template.py +++ b/tools/cru-py/cru/service/_template.py @@ -41,28 +41,23 @@ class TemplateManager(AppCommandFeatureProvider):          )          return self._template_tree -    def list_files(self) -> list[str]: -        return ( -            CruIterator(self.template_tree.templates) -            .transform(lambda t: t[0]) -            .to_list() -        ) -      def print_file_lists(self) -> None: -        for file in self.list_files(): -            print(file) +        for file in CruIterator(self.template_tree.templates).transform(lambda t: t[0]): +            print(file.as_posix())      def generate_files(self) -> None:          config_manager = self.app.get_feature(ConfigManager)          self.template_tree.generate_to( -            self.generated_dir.full_path_str, config_manager.config_map +            self.generated_dir.full_path_str, config_manager.get_config_str_dict()          )      def get_command_info(self):          return ("template", "Manage templates.")      def setup_arg_parser(self, arg_parser): -        subparsers = arg_parser.add_subparsers(dest="template_command") +        subparsers = arg_parser.add_subparsers( +            dest="template_command", required=True, metavar="TEMPLATE_COMMAND" +        )          _list_parser = subparsers.add_parser("list", help="List templates.")          _variables_parser = subparsers.add_parser(              "variables", help="List variables for a specific template." diff --git a/tools/cru-py/cru/template.py b/tools/cru-py/cru/template.py index a07ca23..6e2cf24 100644 --- a/tools/cru-py/cru/template.py +++ b/tools/cru-py/cru/template.py @@ -4,6 +4,8 @@ import os.path  from pathlib import Path  from string import Template +from cru._path import CruPath +  from ._iter import CruIterator  from ._error import CruException @@ -77,7 +79,7 @@ class TemplateTree:          wrongly handled.          """          self._prefix = prefix -        self._files: list[tuple[str, CruTemplate]] = [] +        self._files: list[tuple[CruPath, CruTemplate]] = []          self._source = source          self._template_file_suffix = template_file_suffix          self._load() @@ -87,7 +89,7 @@ class TemplateTree:          return self._prefix      @property -    def templates(self) -> list[tuple[str, CruTemplate]]: +    def templates(self) -> list[tuple[CruPath, CruTemplate]]:          return self._files      @property @@ -99,24 +101,24 @@ class TemplateTree:          return self._template_file_suffix      @staticmethod -    def _scan_files(root_path: str) -> list[str]: -        result: list[str] = [] +    def _scan_files(root_path: str) -> list[CruPath]: +        result: list[CruPath] = []          for root, _dirs, files in os.walk(root_path):              for file in files:                  path = Path(root, file)                  path = path.relative_to(root_path) -                result.append(str(path.as_posix())) +                result.append(CruPath(path))          return result      def _load(self) -> None:          files = self._scan_files(self.source)          for file_path in files: -            template_file = os.path.join(self.source, file_path) +            template_file = Path(self.source) / file_path              with open(template_file, "r") as f:                  content = f.read()              template = CruTemplate(self.prefix, content)              if self.template_file_suffix is not None: -                should_be_template = file_path.endswith(self.template_file_suffix) +                should_be_template = file_path.name.endswith(self.template_file_suffix)                  if should_be_template and not template.has_variables:                      raise CruTemplateError(                          f"Template file {file_path} has no variables." | 
