diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ccfb77c..3c25eaa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # 更新日志 +## Alconna 1.8.4 + +### 新增 + +- `command_manager.update` 上下文方法,用于修改 Alconna 对象后更新与其绑定的其他组件 + + ```python + from arclet.alconna import Args, Alconna, Option, command_manager + + alc = Alconna("test") + + with command_manager.update(alc): + alc.prefixes = ["!"] + alc.add(Option("foo", Args["bar", int])) + ``` + + ## Alconna 1.8.3 ### 修复 diff --git a/devtool.py b/devtool.py index 8071d11b..f1941405 100644 --- a/devtool.py +++ b/devtool.py @@ -12,21 +12,24 @@ from arclet.alconna.args import Args from arclet.alconna.argv import Argv from arclet.alconna.base import Option, Subcommand -from arclet.alconna.config import config -from arclet.alconna.typing import DataCollection +from arclet.alconna.config import Namespace +from arclet.alconna.typing import DataCollection, CommandMeta class AnalyseError(Exception): """分析时发生错误""" +dev_space = Namespace("devtool", enable_message_cache=False) + + class _DummyAnalyser(Analyser): filter_out = [] class _DummyALC: options = [] meta = namedtuple("Meta", ["keep_crlf", "fuzzy_match", "raise_exception"])(False, False, True) - namespace_config = config.default_namespace + namespace_config = dev_space def __new__(cls, *args, **kwargs): cls.command = cls._DummyALC() # type: ignore @@ -42,7 +45,8 @@ def analyse_args( context_style: Literal["bracket", "parentheses"] | None = None, **kwargs ): - argv = Argv(config.default_namespace, message_cache=False, context_style=context_style, filter_crlf=True) + meta = CommandMeta(keep_crlf=False, fuzzy_match=False, raise_exception=raise_exception, context_style=context_style) + argv = Argv(meta, dev_space) try: argv.enter(kwargs) argv.build(["test"] + command) @@ -64,7 +68,8 @@ def analyse_header( context_style: Literal["bracket", "parentheses"] | None = None, **kwargs ): - argv = Argv(config.default_namespace, message_cache=False, filter_crlf=True, context_style=context_style, separators=(sep,)) + meta = CommandMeta(keep_crlf=False, fuzzy_match=False, raise_exception=raise_exception, context_style=context_style) + argv = Argv(meta, dev_space, separators=(sep,)) command_header = Header.generate(command_name, headers, compact=compact) try: argv.enter(kwargs) @@ -83,7 +88,8 @@ def analyse_option( context_style: Literal["bracket", "parentheses"] | None = None, **kwargs ): - argv = Argv(config.default_namespace, message_cache=False, filter_crlf=True, context_style=context_style) + meta = CommandMeta(keep_crlf=False, fuzzy_match=False, raise_exception=raise_exception, context_style=context_style) + argv = Argv(meta, dev_space) _analyser = _DummyAnalyser.__new__(_DummyAnalyser) _analyser.reset() _analyser.command.separators = (" ",) @@ -109,7 +115,8 @@ def analyse_subcommand( context_style: Literal["bracket", "parentheses"] | None = None, **kwargs ): - argv = Argv(config.default_namespace, message_cache=False, filter_crlf=True, context_style=context_style) + meta = CommandMeta(keep_crlf=False, fuzzy_match=False, raise_exception=raise_exception, context_style=context_style) + argv = Argv(meta, dev_space) _analyser = _DummyAnalyser.__new__(_DummyAnalyser) _analyser.reset() _analyser.command.separators = (" ",) diff --git a/src/arclet/alconna/__init__.py b/src/arclet/alconna/__init__.py index fe1961d1..b75bd278 100644 --- a/src/arclet/alconna/__init__.py +++ b/src/arclet/alconna/__init__.py @@ -50,7 +50,7 @@ from .typing import UnpackVar as UnpackVar from .typing import Up as Up -__version__ = "1.8.3" +__version__ = "1.8.4" # backward compatibility AnyOne = ANY diff --git a/src/arclet/alconna/_internal/_analyser.py b/src/arclet/alconna/_internal/_analyser.py index 5728ffc2..1e3f0859 100644 --- a/src/arclet/alconna/_internal/_analyser.py +++ b/src/arclet/alconna/_internal/_analyser.py @@ -146,6 +146,9 @@ def _clr(self): def __post_init__(self): self.reset() + self.__calc_args__() + + def __calc_args__(self): self.self_args = self.command.args if self.command.nargs > 0 and self.command.nargs > self.self_args.optional_count: self.need_main_args = True # 如果need_marg那么match的元素里一定得有main_argument @@ -258,12 +261,14 @@ def __init__(self, alconna: Alconna[TDC], compiler: TCompile | None = None): compiler (TCompile | None, optional): 编译器方法 """ super().__init__(alconna) - self.fuzzy_match = alconna.meta.fuzzy_match + self._compiler = compiler or default_compiler self.used_tokens = set() - self.command_header = Header.generate(alconna.command, alconna.prefixes, alconna.meta.compact) - self.extra_allow = not alconna.meta.strict or not alconna.namespace_config.strict - compiler = compiler or default_compiler - compiler(self, command_manager.resolve(self.command).param_ids) + + def compile(self, param_ids: set[str]): + self.extra_allow = not self.command.meta.strict or not self.command.namespace_config.strict + self.command_header = Header.generate(self.command.command, self.command.prefixes, self.command.meta.compact) + self._compiler(self, param_ids) + return self def _clr(self): self.used_tokens.clear() diff --git a/src/arclet/alconna/_internal/_argv.py b/src/arclet/alconna/_internal/_argv.py index 6c6369c7..85e878e4 100644 --- a/src/arclet/alconna/_internal/_argv.py +++ b/src/arclet/alconna/_internal/_argv.py @@ -1,6 +1,6 @@ from __future__ import annotations -from dataclasses import dataclass, field, fields +from dataclasses import dataclass, field, fields, InitVar from typing import Any, Callable, ClassVar, Generic, Iterable, Literal from typing_extensions import Self @@ -11,38 +11,42 @@ from ..config import Namespace, config from ..constraint import ARGV_OVERRIDES from ..exceptions import NullMessage -from ..typing import TDC +from ..typing import TDC, CommandMeta @dataclass(repr=True) class Argv(Generic[TDC]): """命令行参数""" + meta: InitVar[CommandMeta] namespace: Namespace = field(default=config.default_namespace) - fuzzy_match: bool = field(default=False) - """当前命令是否模糊匹配""" - fuzzy_threshold: float = field(default=0.6) - """模糊匹配阈值""" - preprocessors: dict[type, Callable[..., Any]] = field(default_factory=dict) - """命令元素的预处理器""" - to_text: Callable[[Any], str | None] = field(default=lambda x: x if isinstance(x, str) else None) - """将命令元素转换为文本, 或者返回None以跳过该元素""" + """命名空间""" separators: tuple[str, ...] = field(default=(" ",)) """命令分隔符""" - context_style: Literal["bracket", "parentheses"] | None = field(default=None) - "命令上下文插值的风格,None 为关闭,bracket 为 {...},parentheses 为 $(...)" + + preprocessors: dict[type, Callable[..., Any]] = field(default_factory=dict) + """命令元素的预处理器""" filter_out: list[type] = field(default_factory=list) """需要过滤掉的命令元素""" checker: Callable[[Any], bool] | None = field(default=None) """检查传入命令""" + param_ids: set[str] = field(default_factory=set) + """节点名集合""" + + fuzzy_match: bool = field(init=False) + """当前命令是否模糊匹配""" + fuzzy_threshold: float = field(init=False) + """模糊匹配阈值""" + to_text: Callable[[Any], str | None] = field(default=lambda x: x if isinstance(x, str) else None) + """将命令元素转换为文本, 或者返回None以跳过该元素""" converter: Callable[[str | list], TDC] = field(default=lambda x: x) """将字符串或列表转为目标命令类型""" - filter_crlf: bool = field(default=True) + filter_crlf: bool = field(init=False) """是否过滤掉换行符""" - message_cache: bool = field(default=True) + message_cache: bool = field(init=False) """是否缓存消息""" - param_ids: set[str] = field(default_factory=set) - """节点名集合""" + context_style: Literal["bracket", "parentheses"] | None = field(init=False) + "命令上下文插值的风格,None 为关闭,bracket 为 {...},parentheses 为 $(...)" current_node: Arg | Subcommand | Option | None = field(init=False) """当前节点""" @@ -59,19 +63,15 @@ class Argv(Generic[TDC]): origin: TDC = field(init=False) """原始命令""" context: dict[str, Any] = field(init=False, default_factory=dict) + special: dict[str, str] = field(init=False, default_factory=dict) + completion_names: set[str] = field(init=False, default_factory=set) _sep: tuple[str, ...] | None = field(init=False) _cache: ClassVar[dict[type, dict[str, Any]]] = {} - def __post_init__(self): + def __post_init__(self, meta: CommandMeta): self.reset() - self.special: dict[str, str] = {} - self.special.update( - [(i, "help") for i in self.namespace.builtin_option_name["help"]] - + [(i, "completion") for i in self.namespace.builtin_option_name["completion"]] - + [(i, "shortcut") for i in self.namespace.builtin_option_name["shortcut"]] - ) - self.completion_names = self.namespace.builtin_option_name["completion"] + self.compile(meta) if __cache := self.__class__._cache.get(self.__class__, {}): self.preprocessors.update(__cache.get("preprocessors") or {}) self.filter_out.extend(__cache.get("filter_out") or []) @@ -79,6 +79,22 @@ def __post_init__(self): self.checker = __cache.get("checker") or self.checker self.converter = __cache.get("converter") or self.converter + def compile(self, meta: CommandMeta): + self.fuzzy_match = meta.fuzzy_match + self.fuzzy_threshold = meta.fuzzy_threshold + self.to_text = self.namespace.to_text + self.converter = self.namespace.converter + self.message_cache = self.namespace.enable_message_cache + self.filter_crlf = not meta.keep_crlf + self.context_style = meta.context_style + self.special = {} + self.special.update( + [(i, "help") for i in self.namespace.builtin_option_name["help"]] + + [(i, "completion") for i in self.namespace.builtin_option_name["completion"]] + + [(i, "shortcut") for i in self.namespace.builtin_option_name["shortcut"]] + ) + self.completion_names = self.namespace.builtin_option_name["completion"] + def reset(self): """重置命令行参数""" self.current_index = 0 diff --git a/src/arclet/alconna/base.py b/src/arclet/alconna/base.py index 979aeece..b35af60c 100644 --- a/src/arclet/alconna/base.py +++ b/src/arclet/alconna/base.py @@ -162,8 +162,6 @@ class Option(CommandNode): """命令选项默认值""" aliases: frozenset[str] """命令选项别名""" - priority: int - """命令选项优先级""" compact: bool "是否允许名称与后随参数之间无分隔符" @@ -179,7 +177,7 @@ def __init__( help_text: str | None = None, requires: str | list[str] | tuple[str, ...] | set[str] | None = None, compact: bool = False, - priority: int = 0, + priority: int = 0, ): """初始化命令选项 diff --git a/src/arclet/alconna/core.py b/src/arclet/alconna/core.py index 07ec16e1..326f350d 100644 --- a/src/arclet/alconna/core.py +++ b/src/arclet/alconna/core.py @@ -112,10 +112,9 @@ class Alconna(Subcommand, Generic[TDC]): behaviors: list[ArparmaBehavior] """命令行为器""" - @property - def compile(self) -> Callable[[TCompile | None], Analyser[TDC]]: + def compile(self, compiler: TCompile | None = None, param_ids: set[str] | None = None) -> Analyser[TDC]: """编译 `Alconna` 为对应的解析器""" - return partial(Analyser, self) + return Analyser(self, compiler).compile(param_ids) def __init__( self, @@ -183,19 +182,17 @@ def reset_namespace(self, namespace: Namespace | str, header: bool = True) -> Se namespace (Namespace | str): 命名空间 header (bool, optional): 是否保留命令头, 默认为 `True` """ - command_manager.delete(self) - if isinstance(namespace, str): - namespace = config.namespaces.setdefault(namespace, Namespace(namespace)) - self.namespace = namespace.name - self.path = f"{self.namespace}::{self.name}" - if header: - self.prefixes = namespace.prefixes.copy() - self.options = self.options[:-3] - add_builtin_options(self.options, namespace) - self.meta.fuzzy_match = namespace.fuzzy_match or self.meta.fuzzy_match - self.meta.raise_exception = namespace.raise_exception or self.meta.raise_exception - self._hash = self._calc_hash() - command_manager.register(self) + with command_manager.update(self): + if isinstance(namespace, str): + namespace = config.namespaces.setdefault(namespace, Namespace(namespace)) + self.namespace = namespace.name + self.path = f"{self.namespace}::{self.name}" + if header: + self.prefixes = namespace.prefixes.copy() + self.options = self.options[:-3] + add_builtin_options(self.options, namespace) + self.meta.fuzzy_match = namespace.fuzzy_match or self.meta.fuzzy_match + self.meta.raise_exception = namespace.raise_exception or self.meta.raise_exception return self def get_help(self) -> str: @@ -312,10 +309,8 @@ def add(self, opt: Option | Subcommand) -> Self: Returns: Self: 命令本身 """ - command_manager.delete(self) - self.options.insert(-3, opt) - self._hash = self._calc_hash() - command_manager.register(self) + with command_manager.update(self): + self.options.insert(-3, opt) return self @init_spec(Option, is_method=True) @@ -383,20 +378,18 @@ def __truediv__(self, other) -> Self: __rtruediv__ = __truediv__ def __add__(self, other) -> Self: - command_manager.delete(self) - if isinstance(other, Alconna): - self.options.extend(other.options) - elif isinstance(other, CommandMeta): - self.meta = other - elif isinstance(other, Option): - self.options.append(other) - elif isinstance(other, Args): - self.args += other - self.nargs = len(self.args) - elif isinstance(other, str): - self.options.append(Option(other)) - self._hash = self._calc_hash() - command_manager.register(self) + with command_manager.update(self): + if isinstance(other, Alconna): + self.options.extend(other.options) + elif isinstance(other, CommandMeta): + self.meta = other + elif isinstance(other, Option): + self.options.append(other) + elif isinstance(other, Args): + self.args += other + self.nargs = len(self.args) + elif isinstance(other, str): + self.options.append(Option(other)) return self def __or__(self, other: Alconna) -> Self: diff --git a/src/arclet/alconna/manager.py b/src/arclet/alconna/manager.py index 0f8ab80e..8357371f 100644 --- a/src/arclet/alconna/manager.py +++ b/src/arclet/alconna/manager.py @@ -8,7 +8,7 @@ import weakref from copy import copy from datetime import datetime -from typing import TYPE_CHECKING, Any, Match, MutableSet, Union +from typing import TYPE_CHECKING, Any, Match, MutableSet, Union, Callable from weakref import WeakKeyDictionary, WeakValueDictionary from tarina import LRU, lang @@ -113,19 +113,9 @@ def register(self, command: Alconna) -> None: if self.current_count >= self.max_count: raise ExceedMaxCount self.__argv.pop(command, None) - self.__argv[command] = __argv_type__.get()( - command.namespace_config, # type: ignore - fuzzy_match=command.meta.fuzzy_match, # type: ignore - fuzzy_threshold=command.meta.fuzzy_threshold, # type: ignore - to_text=command.namespace_config.to_text, # type: ignore - converter=command.namespace_config.converter, # type: ignore - separators=command.separators, # type: ignore - message_cache=command.namespace_config.enable_message_cache, # type: ignore - filter_crlf=not command.meta.keep_crlf, # type: ignore - context_style=command.meta.context_style, # type: ignore - ) + argv = self.__argv[command] = __argv_type__.get()(command.meta, command.namespace_config, command.separators) # type: ignore self.__analysers.pop(command, None) - self.__analysers[command] = command.compile(None) + self.__analysers[command] = command.compile(param_ids=argv.param_ids) namespace = self.__commands.setdefault(command.namespace, WeakValueDictionary()) if _cmd := namespace.get(command.name): if _cmd == command: @@ -174,6 +164,25 @@ def delete(self, command: Alconna | str) -> None: if self.__commands.get(namespace) == {}: del self.__commands[namespace] + @contextlib.contextmanager + def update(self, command: Alconna): + """同步命令更改""" + if command not in self.__argv: + raise ValueError(lang.require("manager", "undefined_command").format(target=command.path)) + command.formatter.remove(command) + argv = self.__argv.pop(command) + analyser = self.__analysers.pop(command) + yield + command._hash = command._calc_hash() + argv.namespace = command.namespace_config + argv.separators = command.separators + argv.compile(command.meta) + argv.param_ids.clear() + analyser.compile(argv.param_ids) + self.__argv[command] = argv + self.__analysers[command] = analyser + command.formatter.add(command) + def is_disable(self, command: Alconna) -> bool: """判断命令是否被禁用""" return command in self.__abandons diff --git a/src/arclet/alconna/typing.py b/src/arclet/alconna/typing.py index 1815a326..fcd06bed 100644 --- a/src/arclet/alconna/typing.py +++ b/src/arclet/alconna/typing.py @@ -208,8 +208,6 @@ class MultiKeyWordVar(MultiVar): class KWBool(BasePattern): """对布尔参数的包装""" - ... - class UnpackVar(BasePattern): """特殊参数,利用dataclass 的 field 生成 arg 信息,并返回dcls""" diff --git a/tests/devtool.py b/tests/devtool.py index 8071d11b..f1941405 100644 --- a/tests/devtool.py +++ b/tests/devtool.py @@ -12,21 +12,24 @@ from arclet.alconna.args import Args from arclet.alconna.argv import Argv from arclet.alconna.base import Option, Subcommand -from arclet.alconna.config import config -from arclet.alconna.typing import DataCollection +from arclet.alconna.config import Namespace +from arclet.alconna.typing import DataCollection, CommandMeta class AnalyseError(Exception): """分析时发生错误""" +dev_space = Namespace("devtool", enable_message_cache=False) + + class _DummyAnalyser(Analyser): filter_out = [] class _DummyALC: options = [] meta = namedtuple("Meta", ["keep_crlf", "fuzzy_match", "raise_exception"])(False, False, True) - namespace_config = config.default_namespace + namespace_config = dev_space def __new__(cls, *args, **kwargs): cls.command = cls._DummyALC() # type: ignore @@ -42,7 +45,8 @@ def analyse_args( context_style: Literal["bracket", "parentheses"] | None = None, **kwargs ): - argv = Argv(config.default_namespace, message_cache=False, context_style=context_style, filter_crlf=True) + meta = CommandMeta(keep_crlf=False, fuzzy_match=False, raise_exception=raise_exception, context_style=context_style) + argv = Argv(meta, dev_space) try: argv.enter(kwargs) argv.build(["test"] + command) @@ -64,7 +68,8 @@ def analyse_header( context_style: Literal["bracket", "parentheses"] | None = None, **kwargs ): - argv = Argv(config.default_namespace, message_cache=False, filter_crlf=True, context_style=context_style, separators=(sep,)) + meta = CommandMeta(keep_crlf=False, fuzzy_match=False, raise_exception=raise_exception, context_style=context_style) + argv = Argv(meta, dev_space, separators=(sep,)) command_header = Header.generate(command_name, headers, compact=compact) try: argv.enter(kwargs) @@ -83,7 +88,8 @@ def analyse_option( context_style: Literal["bracket", "parentheses"] | None = None, **kwargs ): - argv = Argv(config.default_namespace, message_cache=False, filter_crlf=True, context_style=context_style) + meta = CommandMeta(keep_crlf=False, fuzzy_match=False, raise_exception=raise_exception, context_style=context_style) + argv = Argv(meta, dev_space) _analyser = _DummyAnalyser.__new__(_DummyAnalyser) _analyser.reset() _analyser.command.separators = (" ",) @@ -109,7 +115,8 @@ def analyse_subcommand( context_style: Literal["bracket", "parentheses"] | None = None, **kwargs ): - argv = Argv(config.default_namespace, message_cache=False, filter_crlf=True, context_style=context_style) + meta = CommandMeta(keep_crlf=False, fuzzy_match=False, raise_exception=raise_exception, context_style=context_style) + argv = Argv(meta, dev_space) _analyser = _DummyAnalyser.__new__(_DummyAnalyser) _analyser.reset() _analyser.command.separators = (" ",)