From 62dcb4dc66111c98a5af747e80e910c2019b19d0 Mon Sep 17 00:00:00 2001 From: RF-Tar-Railt <3165388245@qq.com> Date: Fri, 26 May 2023 01:53:22 +0800 Subject: [PATCH] :ambulance: version 1.7.7 fix critical bug of `append_value` --- CHANGELOG.md | 11 +++++++++ devtool.py | 4 ++-- pyproject.toml | 2 +- src/arclet/alconna/_internal/_analyser.py | 19 +++++++--------- src/arclet/alconna/_internal/_handlers.py | 27 +++++------------------ src/arclet/alconna/_internal/_header.py | 6 +++-- src/arclet/alconna/_internal/_util.py | 18 +++++++++++++++ src/arclet/alconna/args.py | 10 +++------ src/arclet/alconna/arparma.py | 12 +++------- src/arclet/alconna/builtin.py | 24 ++++++++++++++++++-- src/arclet/alconna/duplication.py | 25 +++++---------------- src/arclet/alconna/manager.py | 2 +- tests/components_test.py | 9 ++++---- tests/core_test.py | 10 ++++++++- 14 files changed, 96 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4999e88b..72c5b111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # 更新日志 +# Alconna 1.7.7 + +### 改进: + +- 命令头部可以通过 `\\` 转义 `{}` 为原始字符,而不触发 `bracket header` 解析 +- 快捷指令的参数部分可以通过 `\\` 转义 `{}` 为原始字符 + +### 修复: + +- 修复 `append_value` 时列表对象引用错误的 bug + ## Alconna 1.7.6 ### 修复: diff --git a/devtool.py b/devtool.py index 365003ef..2b91cbd5 100644 --- a/devtool.py +++ b/devtool.py @@ -77,7 +77,7 @@ def analyse_option(option: Option, command: DataCollection[str | Any], raise_exc _analyser.need_main_args = False _analyser.raise_exception = True _analyser.command.options.append(option) - default_compiler(_analyser, _analyser.command.namespace_config, argv.param_ids) + default_compiler(_analyser, argv.param_ids) _analyser.command.options.clear() try: argv.build(command) @@ -97,7 +97,7 @@ def analyse_subcommand(subcommand: Subcommand, command: DataCollection[str | Any _analyser.need_main_args = False _analyser.raise_exception = True _analyser.command.options.append(subcommand) - default_compiler(_analyser, _analyser.command.namespace_config, argv.param_ids) + default_compiler(_analyser, argv.param_ids) _analyser.command.options.clear() try: argv.build(command) diff --git a/pyproject.toml b/pyproject.toml index 47984108..e4e2f4c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ ] dependencies = [ "typing-extensions>=4.5.0", - "nepattern<0.6.0, >=0.5.6", + "nepattern<0.6.0, >=0.5.8", "tarina>=0.3.3", ] dynamic = ["version"] diff --git a/src/arclet/alconna/_internal/_analyser.py b/src/arclet/alconna/_internal/_analyser.py index 6ea1d7c7..3d7ef311 100644 --- a/src/arclet/alconna/_internal/_analyser.py +++ b/src/arclet/alconna/_internal/_analyser.py @@ -1,7 +1,6 @@ from __future__ import annotations import re -import traceback from dataclasses import dataclass, field from re import Match from typing import TYPE_CHECKING, Any, Callable, Generic, Set @@ -28,7 +27,7 @@ handle_shortcut, prompt ) from ._header import Header -from ._util import levenshtein +from ._util import levenshtein, escape, unescape if TYPE_CHECKING: from ..core import Alconna @@ -97,7 +96,6 @@ def default_compiler(analyser: SubAnalyser, pids: set[str]): @dataclass class SubAnalyser(Generic[TDC]): """子解析器, 用于子命令的解析""" - command: Subcommand """子命令""" default_main_only: bool = field(default=False) @@ -294,15 +292,16 @@ def shortcut( break if not isinstance(unit, str): continue + unit = escape(unit) if unit == f"{{%{data_index}}}": argv.raw_data[i] = data.pop(0) data_index += 1 elif f"{{%{data_index}}}" in unit: - argv.raw_data[i] = unit.replace(f"{{%{data_index}}}", str(data.pop(0))) + argv.raw_data[i] = unescape(unit.replace(f"{{%{data_index}}}", str(data.pop(0)))) data_index += 1 elif mat := re.search(r"\{\*(.*)\}", unit, re.DOTALL): sep = mat[1] - argv.raw_data[i] = unit.replace(f"{{*{sep}}}", (sep or ' ').join(map(str, data))) + argv.raw_data[i] = unescape(unit.replace(f"{{*{sep}}}", (sep or ' ').join(map(str, data)))) data.clear() argv.bak_data = argv.raw_data.copy() @@ -313,11 +312,12 @@ def shortcut( for j, unit in enumerate(argv.raw_data): if not isinstance(unit, str): continue + unit = escape(unit) for i, c in enumerate(groups): unit = unit.replace(f"{{{i}}}", c) for k, v in gdict.items(): unit = unit.replace(f"{{{k}}}", v) - argv.raw_data[j] = unit + argv.raw_data[j] = unescape(unit) if argv.message_cache: argv.token = argv.generate_token(argv.raw_data) return self.process(argv) @@ -420,10 +420,7 @@ def analyse(self, argv: Argv[TDC]) -> Arparma[TDC] | None: self.args_result = analyse_args(argv, self.self_args) def export( - self, - argv: Argv[TDC], - fail: bool = False, - exception: BaseException | None = None, + self, argv: Argv[TDC], fail: bool = False, exception: BaseException | None = None, ) -> Arparma[TDC]: """创建 `Arparma` 解析结果, 其一定是一次解析的最后部分 @@ -434,7 +431,7 @@ def export( """ result = Arparma(self.command.path, argv.origin, not fail, self.header_result) if fail: - result.error_info = exception or repr(traceback.format_exc(limit=1)) + result.error_info = exception result.error_data = argv.release() else: if self.default_opt_result: diff --git a/src/arclet/alconna/_internal/_handlers.py b/src/arclet/alconna/_internal/_handlers.py index 3510d413..65e4e084 100644 --- a/src/arclet/alconna/_internal/_handlers.py +++ b/src/arclet/alconna/_internal/_handlers.py @@ -75,13 +75,7 @@ def _handle_keyword( raise ParamsUnmatched(lang.require("args", "key_missing").format(target=may_arg, key=key)) -def _loop_kw( - argv: Argv, - _loop: int, - seps: tuple[str, ...], - value: MultiVar, - default: Any -): +def _loop_kw(argv: Argv, _loop: int, seps: tuple[str, ...], value: MultiVar, default: Any): """循环关键字参数""" result = {} for _ in range(_loop): @@ -103,12 +97,7 @@ def _loop_kw( def _loop( - argv: Argv, - _loop: int, - seps: tuple[str, ...], - value: MultiVar, - default: Any, - kw: KeyWordVar | None + argv: Argv, _loop: int, seps: tuple[str, ...], value: MultiVar, default: Any, kw: KeyWordVar | None ): """循环参数""" result = [] @@ -133,12 +122,7 @@ def _loop( return tuple(result) -def multi_arg_handler( - argv: Argv, - args: Args, - arg: Arg, - result_dict: dict[str, Any], -): +def multi_arg_handler(argv: Argv, args: Args, arg: Arg, result_dict: dict[str, Any],): """处理可变参数 Args: @@ -222,12 +206,10 @@ def analyse_args(argv: Argv, args: Args) -> dict[str, Any]: result[key] = argv.converter(argv.release(arg.separators)) argv.current_index = argv.ndata return result - elif value == AnyOne: + elif value == AnyOne or (value == STRING and _str): result[key] = may_arg elif value == AnyString: result[key] = str(may_arg) - elif value == STRING and _str: - result[key] = may_arg else: res = ( value.invalidate(may_arg, default_val) @@ -294,6 +276,7 @@ def handle_action(param: Option, source: OptionResult, target: OptionResult): return source return target if not param.nargs: + source.value = source.value[:] source.value.extend(target.value) else: for key, value in target.args.items(): diff --git a/src/arclet/alconna/_internal/_header.py b/src/arclet/alconna/_internal/_header.py index d324ac93..bbd4b223 100644 --- a/src/arclet/alconna/_internal/_header.py +++ b/src/arclet/alconna/_internal/_header.py @@ -9,14 +9,16 @@ from nepattern.util import TPattern from tarina import Empty, lang +from ._util import escape, unescape from ..typing import TPrefixes def handle_bracket(name: str, mapping: dict): """处理字符串中的括号对并转为正则表达式""" pattern_map = all_patterns() + name = escape(name) if len(parts := re.split(r"(\{.*?})", name)) <= 1: - return name, False + return unescape(name), False for i, part in enumerate(parts): if not part: continue @@ -35,7 +37,7 @@ def handle_bracket(name: str, mapping: dict): parts[i] = f"(?P<{res[0]}>{pattern_map[res[1]].pattern})" else: parts[i] = f"(?P<{res[0]}>{res[1]})" - return "".join(parts), True + return unescape("".join(parts)), True class Pair: diff --git a/src/arclet/alconna/_internal/_util.py b/src/arclet/alconna/_internal/_util.py index 11a5b696..296133e5 100644 --- a/src/arclet/alconna/_internal/_util.py +++ b/src/arclet/alconna/_internal/_util.py @@ -19,3 +19,21 @@ def levenshtein(source: str, target: str) -> float: matrix[i][j] = min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, sub_distance) return 1 - float(matrix[l_s][l_t]) / max(l_s, l_t) + + +ESCAPE = {"\\": "\x00", "[": "\x01", "]": "\x02", "{": "\x03", "}": "\x04", "|": "\x05"} +R_ESCAPE = {v: k for k, v in ESCAPE.items()} + + +def escape(string: str) -> str: + """转义字符串""" + for k, v in ESCAPE.items(): + string = string.replace("\\" + k, v) + return string + + +def unescape(string: str) -> str: + """逆转义字符串, 自动去除空白符 """ + for k, v in R_ESCAPE.items(): + string = string.replace(k, v) + return string.strip() diff --git a/src/arclet/alconna/args.py b/src/arclet/alconna/args.py index 7ead9877..9f95f34c 100644 --- a/src/arclet/alconna/args.py +++ b/src/arclet/alconna/args.py @@ -211,9 +211,7 @@ def from_callable(cls, target: Callable) -> tuple[Args, bool]: _args.add(name, value=anno, default=de) return _args, method - def __init__( - self, *args: Arg, separators: str | Iterable[str] | None = None, **kwargs: TAValue - ): + def __init__(self, *args: Arg, separators: str | Iterable[str] | None = None, **kwargs: TAValue): """ 构造一个 `Args` @@ -299,8 +297,7 @@ def __check_vars__(self): if arg.name in self._visit: continue self._visit.add(arg.name) - _limit = False - if isinstance(arg.value, MultiVar) and not _limit: + if isinstance(arg.value, MultiVar): if isinstance(arg.value.base, KeyWordVar): if self.var_keyword: raise InvalidParam(lang.require("args", "duplicate_kwargs")) @@ -309,7 +306,6 @@ def __check_vars__(self): raise InvalidParam(lang.require("args", "duplicate_varargs")) else: self.var_positional = arg.value - _limit = True if isinstance(arg.value, KeyWordVar): if self.var_keyword or self.var_positional: raise InvalidParam(lang.require("args", "exclude_mutable_args")) @@ -317,7 +313,7 @@ def __check_vars__(self): if arg.value.sep in arg.separators: _tmp.insert(-1, Arg(f"_key_{arg.name}", value=f"-*{arg.name}")) _tmp[-1].value = arg.value.base - if ArgFlag.OPTIONAL in arg.flag: + if arg.optional: if self.var_keyword or self.var_positional: raise InvalidParam(lang.require("args", "exclude_mutable_args")) self.optional_count += 1 diff --git a/src/arclet/alconna/arparma.py b/src/arclet/alconna/arparma.py index 041935ee..ccb29d5f 100644 --- a/src/arclet/alconna/arparma.py +++ b/src/arclet/alconna/arparma.py @@ -80,7 +80,7 @@ def __init__( origin: TDC, matched: bool = False, header_match: HeadResult | None = None, - error_info: type[BaseException] | BaseException | str = '', + error_info: type[BaseException] | BaseException | None = None, error_data: list[str | Any] | None = None, main_args: dict[str, Any] | None = None, options: dict[str, OptionResult] | None = None, @@ -92,13 +92,13 @@ def __init__( origin (TDC): 原始数据 matched (bool, optional): 是否匹配 header_match (HeadResult | None, optional): 命令头匹配结果 - error_info (type[BaseException] | BaseException | str, optional): 错误信息 + error_info (type[BaseException] | BaseException | None, optional): 错误信息 error_data (list[str | Any] | None, optional): 错误数据 main_args (dict[str, Any] | None, optional): 主参数匹配结果 options (dict[str, OptionResult] | None, optional): 选项匹配结果 subcommands (dict[str, SubcommandResult] | None, optional): 子命令匹配结果 """ - self._source = source + self.source = source self.origin = origin self.matched = matched self.header_match = header_match or HeadResult() @@ -114,12 +114,6 @@ def _clr(self): for k in ks: delattr(self, k) - @property - def source(self): - """返回命令源""" - from .manager import command_manager - return command_manager.get_command(self._source) - @property def header(self) -> dict[str, Any]: """返回可能解析到的命令头中的组信息""" diff --git a/src/arclet/alconna/builtin.py b/src/arclet/alconna/builtin.py index 36b179fc..a4a37e32 100644 --- a/src/arclet/alconna/builtin.py +++ b/src/arclet/alconna/builtin.py @@ -1,12 +1,32 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any, Callable, overload +from typing import Any, Callable, overload, cast from .arparma import Arparma, ArparmaBehavior +from .core import Alconna +from .duplication import Duplication +from .stub import ArgsStub, OptionStub, SubcommandStub from .exceptions import BehaveCancelled -__all__ = ["set_default"] +__all__ = ["set_default", "generate_duplication"] + + +def generate_duplication(alc: Alconna) -> type[Duplication]: + """依据给定的命令生成一个解析结果的检查类。""" + from .base import Option, Subcommand + options = filter(lambda x: isinstance(x, Option), alc.options) + subcommands = filter(lambda x: isinstance(x, Subcommand), alc.options) + return cast(type[Duplication], type( + f"{alc.name.strip('/.-:')}Interface", + (Duplication,), { + "__annotations__": { + "args": ArgsStub, + **{opt.dest: OptionStub for opt in options}, + **{sub.dest: SubcommandStub for sub in subcommands}, + } + } + )) class _MISSING_TYPE: pass diff --git a/src/arclet/alconna/duplication.py b/src/arclet/alconna/duplication.py index 14ca4682..b23fc1b3 100644 --- a/src/arclet/alconna/duplication.py +++ b/src/arclet/alconna/duplication.py @@ -15,17 +15,19 @@ class Duplication: def __init__(self, target: Arparma): from .base import Option, Subcommand + from .manager import command_manager + source = command_manager.get_command(target.source) self.header = target.header.copy() for key, value in self.__annotations__.items(): if isclass(value) and issubclass(value, BaseStub): if value is ArgsStub: - setattr(self, key, ArgsStub(target.source.args).set_result(target.main_args)) + setattr(self, key, ArgsStub(source.args).set_result(target.main_args)) elif value is SubcommandStub: - for subcommand in filter(lambda x: isinstance(x, Subcommand), target.source.options): + for subcommand in filter(lambda x: isinstance(x, Subcommand), source.options): if subcommand.dest == key: setattr(self, key, SubcommandStub(subcommand).set_result(target.subcommands.get(key, None))) elif value is OptionStub: - for option in filter(lambda x: isinstance(x, Option), target.source.options): + for option in filter(lambda x: isinstance(x, Option), source.options): if option.dest == key: setattr(self, key, OptionStub(option).set_result(target.options.get(key, None))) elif key != 'header': @@ -41,20 +43,3 @@ def option(self, name: str) -> OptionStub | None: def subcommand(self, name: str) -> SubcommandStub | None: """获取指定名称的子命令存根。""" return cast(SubcommandStub, getattr(self, name, None)) - - -def generate_duplication(arp: Arparma) -> Duplication: - """依据给定的命令生成一个解析结果的检查类。""" - from .base import Option, Subcommand - options = filter(lambda x: isinstance(x, Option), arp.source.options) - subcommands = filter(lambda x: isinstance(x, Subcommand), arp.source.options) - return cast(Duplication, type( - f"{arp.source.name.strip('/.-:')}Interface", - (Duplication,), { - "__annotations__": { - "args": ArgsStub, - **{opt.dest: OptionStub for opt in options}, - **{sub.dest: SubcommandStub for sub in subcommands}, - } - } - )(arp)) diff --git a/src/arclet/alconna/manager.py b/src/arclet/alconna/manager.py index 8b3cbac2..0b95c2d9 100644 --- a/src/arclet/alconna/manager.py +++ b/src/arclet/alconna/manager.py @@ -371,7 +371,7 @@ def get_token(self, result: Arparma) -> int: def get_result(self, command: Alconna) -> list[Arparma]: """获取某个命令的所有 `Arparma` 对象""" - return [v for v in self.__record.values() if v.source == command] + return [v for v in self.__record.values() if v.source == command.path] @property def recent_message(self) -> DataCollection[str | Any] | None: diff --git a/tests/components_test.py b/tests/components_test.py index d50daf06..0f1a9801 100644 --- a/tests/components_test.py +++ b/tests/components_test.py @@ -1,6 +1,6 @@ from arclet.alconna import Alconna, Option, Args, Subcommand, Arparma, ArparmaBehavior -from arclet.alconna.builtin import set_default -from arclet.alconna.duplication import Duplication, generate_duplication +from arclet.alconna.builtin import set_default, generate_duplication +from arclet.alconna.duplication import Duplication from arclet.alconna.stub import ArgsStub, OptionStub, SubcommandStub from arclet.alconna.output import output_manager from arclet.alconna.model import OptionResult @@ -57,9 +57,10 @@ class Demo1(Duplication): com4_1 = Alconna(["!", "!"], "yiyu", Args["value;OH", str]) res = com4_1.parse("!yiyu") - dup = generate_duplication(res) + dup = generate_duplication(com4_1)(res) assert isinstance(dup, Duplication) + def test_output(): print("") output_manager.set_action(lambda x: {'bar': f'{x}!'}, "foo") @@ -67,8 +68,6 @@ def test_output(): assert output_manager.send("foo") == {"bar": "123!"} assert output_manager.send("foo", lambda: "321") == {"bar": "321!"} - - com5 = Alconna("comp5", Args["foo", int], Option("--bar", Args["bar", str])) output_manager.set_action(lambda x: x, "comp5") with output_manager.capture("comp5") as output: diff --git a/tests/core_test.py b/tests/core_test.py index 503cf5a2..39272674 100644 --- a/tests/core_test.py +++ b/tests/core_test.py @@ -105,6 +105,9 @@ def test_bracket_header(): assert res.matched is True assert res.header["r"] == 100 assert res.header["e"] == 36 + alc2_1 = Alconna(r"RD\{r:int\}") + assert not alc2_1.parse("RD100").matched + assert alc2_1.parse("RD{r:int}").matched def test_formatter(): @@ -602,8 +605,13 @@ def test_action(): assert res.query("xyz.value") == 4 assert res.query("foo_bar_q.value") == 3 + alc24_3 = Alconna( + "core24_3", Option("-t", default=False, action=append_value(True)) + ) + assert alc24_3.parse("core24_3 -t -t -t").query("t.value") == [True, True, True] + -def test_defualt(): +def test_default(): from arclet.alconna import store_value, OptionResult, append, store_true alc25 = Alconna(