aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2024-11-11 01:12:29 +0800
committerYuqian Yang <crupest@crupest.life>2025-01-18 17:38:07 +0800
commit41f735ed9ddc9d96d27f7e1f2a6ca34af9cce9d9 (patch)
treeae0e509d11b974b6fda8fbef985f1e538913e6fd
parentc0ba4d9d8d19d3faa7b4d2b3509546e37dd32364 (diff)
downloadcrupest-41f735ed9ddc9d96d27f7e1f2a6ca34af9cce9d9.tar.gz
crupest-41f735ed9ddc9d96d27f7e1f2a6ca34af9cce9d9.tar.bz2
crupest-41f735ed9ddc9d96d27f7e1f2a6ca34af9cce9d9.zip
HALF WORK: 2024.1.18
-rw-r--r--tools/cru-py/cru/config.py68
-rw-r--r--tools/cru-py/cru/list.py3
-rw-r--r--tools/cru-py/cru/service/_base.py9
-rw-r--r--tools/cru-py/cru/service/_config.py159
-rw-r--r--tools/cru-py/cru/service/_template.py17
-rw-r--r--tools/cru-py/cru/template.py16
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."