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 | 41f735ed9ddc9d96d27f7e1f2a6ca34af9cce9d9 (patch) | |
tree | ae0e509d11b974b6fda8fbef985f1e538913e6fd | |
parent | c0ba4d9d8d19d3faa7b4d2b3509546e37dd32364 (diff) | |
download | crupest-41f735ed9ddc9d96d27f7e1f2a6ca34af9cce9d9.tar.gz crupest-41f735ed9ddc9d96d27f7e1f2a6ca34af9cce9d9.tar.bz2 crupest-41f735ed9ddc9d96d27f7e1f2a6ca34af9cce9d9.zip |
HALF WORK: 2024.1.18
-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." |