From 8b58b9bca62596dc16472a3842ab4469e28fc4fc Mon Sep 17 00:00:00 2001 From: RF-Tar-Railt <3165388245@qq.com> Date: Sun, 19 Nov 2023 13:25:50 +0800 Subject: [PATCH] :sparkles: version 1.7.37 shortcut reg wrapper --- CHANGELOG.md | 11 +++- src/arclet/alconna/__init__.py | 2 +- src/arclet/alconna/_internal/_analyser.py | 18 +++--- src/arclet/alconna/_internal/_handlers.py | 58 ++++++++++++++--- src/arclet/alconna/core.py | 76 ++++++++++++++++++++++- src/arclet/alconna/manager.py | 36 ++++------- src/arclet/alconna/typing.py | 28 ++++++++- tests/core_test.py | 23 ++++++- 8 files changed, 202 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02fc2cc8..c17c8d12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,23 @@ # 更新日志 +## Alconna 1.7.37 + +### 改进 + +- `Alconna.shortcut` 可以用关键字参数传入 `command` 与 `args` 了 +- 允许提供参数来额外处理 `shortcut` 的正则匹配结果 + ## Alconna 1.7.36 ### 改进 -修改换行处理 +- 修改换行处理 ## Alconna 1.7.35 ### 改进 -为 completion 下提示列出的选中符号和未选中符号添加 i18n 支持 +- 为 completion 下提示列出的选中符号和未选中符号添加 i18n 支持 ## Alconna 1.7.34 diff --git a/src/arclet/alconna/__init__.py b/src/arclet/alconna/__init__.py index 297a0d31..2aaddf86 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.7.36" +__version__ = "1.7.37" # backward compatibility Arpamar = Arparma diff --git a/src/arclet/alconna/_internal/_analyser.py b/src/arclet/alconna/_internal/_analyser.py index 8ef80671..812e38d7 100644 --- a/src/arclet/alconna/_internal/_analyser.py +++ b/src/arclet/alconna/_internal/_analyser.py @@ -14,10 +14,10 @@ from ..completion import comp_ctx from ..config import config from ..exceptions import ArgumentMissing, FuzzyMatchSuccess, InvalidParam, ParamsUnmatched, PauseTriggered, SpecialOptionTriggered -from ..manager import ShortcutArgs, command_manager +from ..manager import command_manager from ..model import HeadResult, OptionResult, Sentence, SubcommandResult from ..output import output_manager -from ..typing import TDC +from ..typing import TDC, InnerShortcutArgs from ._handlers import ( _handle_shortcut_data, _handle_shortcut_reg, @@ -258,7 +258,7 @@ def __repr__(self): return f"<{self.__class__.__name__} of {self.command.path}>" def shortcut( - self, argv: Argv[TDC], trigger: str, data: list[Any], short: Arparma | ShortcutArgs, reg: Match | None = None + self, argv: Argv[TDC], trigger: str, data: list[Any], short: Arparma | InnerShortcutArgs, reg: Match | None = None ) -> Arparma[TDC]: """处理被触发的快捷命令 @@ -266,7 +266,7 @@ def shortcut( argv (Argv[TDC]): 命令行参数 trigger (str): 触发词 data (list[Any]): 剩余参数 - short (Arparma | ShortcutArgs): 快捷命令 + short (Arparma | InnerShortcutArgs): 快捷命令 reg (Match | None): 可能的正则匹配结果 Returns: @@ -278,20 +278,20 @@ def shortcut( if isinstance(short, Arparma): return short - argv.build(short.get("command", argv.converter(self.command.command or self.command.name))) - if not short.get("fuzzy") and data: + argv.build(short["command"]) # type: ignore + if not short["fuzzy"] and data: exc = ParamsUnmatched(lang.require("analyser", "param_unmatched").format(target=data[0])) if self.command.meta.raise_exception: raise exc return self.export(argv, True, exc) - if short.get("fuzzy") and reg and len(trigger) > reg.span()[1]: + if short["fuzzy"] and reg and len(trigger) > reg.span()[1]: argv.addon((trigger[reg.span()[1] :],)) - argv.addon(short.get("args", [])) + argv.addon(short["args"]) data = _handle_shortcut_data(argv, data) argv.bak_data = argv.raw_data.copy() argv.addon(data) if reg: - _handle_shortcut_reg(argv, reg.groups(), reg.groupdict()) + argv.raw_data = _handle_shortcut_reg(argv, reg.groups(), reg.groupdict(), short["wrapper"]) argv.bak_data = argv.raw_data.copy() if argv.message_cache: argv.token = argv.generate_token(argv.raw_data) diff --git a/src/arclet/alconna/_internal/_handlers.py b/src/arclet/alconna/_internal/_handlers.py index 9fd300d8..d4f7c790 100644 --- a/src/arclet/alconna/_internal/_handlers.py +++ b/src/arclet/alconna/_internal/_handlers.py @@ -15,7 +15,7 @@ from ..exceptions import ArgumentMissing, FuzzyMatchSuccess, InvalidParam, PauseTriggered, SpecialOptionTriggered from ..model import HeadResult, OptionResult, Sentence from ..output import output_manager -from ..typing import KWBool +from ..typing import KWBool, ShortcutRegWrapper from ._header import Double, Header from ._util import escape, levenshtein, unescape @@ -597,16 +597,58 @@ def _handle_shortcut_data(argv: Argv, data: list): return [unit for i, unit in enumerate(data) if i not in record] -def _handle_shortcut_reg(argv: Argv, groups: tuple[str, ...], gdict: dict[str, str]): - for j, unit in enumerate(argv.raw_data): +INDEX_REG_SLOT = re.compile(r"\{(\d+)\}") +KEY_REG_SLOT = re.compile(r"\{(\w+)\}") + + +def _handle_shortcut_reg(argv: Argv, groups: tuple[str, ...], gdict: dict[str, str], wrapper: ShortcutRegWrapper): + data = [] + for unit in argv.raw_data: if not isinstance(unit, str): + data.append(unit) 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] = unescape(unit) + if mat := INDEX_REG_SLOT.fullmatch(unit): + index = int(mat[1]) + if index >= len(groups): + continue + slot = groups[index] + if slot is None: + continue + data.append(wrapper(index, slot) or slot) + continue + if mat := KEY_REG_SLOT.fullmatch(unit): + key = mat[1] + if key not in gdict: + continue + slot = gdict[key] + if slot is None: + continue + data.append(wrapper(key, slot) or slot) + continue + if mat := INDEX_REG_SLOT.findall(unit): + for index in map(int, mat): + if index >= len(groups): + unit = unit.replace(f"{{{index}}}", "") + continue + slot = groups[index] + if slot is None: + unit = unit.replace(f"{{{index}}}", "") + continue + unit = unit.replace(f"{{{index}}}", str(wrapper(index, slot) or slot)) + if mat := KEY_REG_SLOT.findall(unit): + for key in mat: + if key not in gdict: + unit = unit.replace(f"{{{key}}}", "") + continue + slot = gdict[key] + if slot is None: + unit = unit.replace(f"{{{key}}}", "") + continue + unit = unit.replace(f"{{{key}}}", str(wrapper(key, slot) or slot)) + if unit: + data.append(unescape(unit)) + return data def _prompt_unit(analyser: Analyser, argv: Argv, trig: Arg): diff --git a/src/arclet/alconna/core.py b/src/arclet/alconna/core.py index 7898c0db..c5f005e3 100644 --- a/src/arclet/alconna/core.py +++ b/src/arclet/alconna/core.py @@ -5,7 +5,7 @@ from dataclasses import dataclass, field from functools import partial from pathlib import Path -from typing import Any, Callable, Generic, Sequence, TypeVar, overload +from typing import Any, Callable, Generic, Literal, Sequence, TypeVar, cast, overload from typing_extensions import Self from tarina import init_spec, lang @@ -19,7 +19,7 @@ from .exceptions import ExecuteFailed, NullMessage from .formatter import TextFormatter from .manager import ShortcutArgs, command_manager -from .typing import TDC, CommandMeta, DataCollection, TPrefixes +from .typing import TDC, CommandMeta, DataCollection, ShortcutRegWrapper, TPrefixes T_Duplication = TypeVar("T_Duplication", bound=Duplication) T = TypeVar("T") @@ -205,13 +205,79 @@ def get_shortcuts(self) -> list[str]: """返回该命令注册的快捷命令""" return command_manager.list_shortcut(self) - def shortcut(self, key: str, args: ShortcutArgs | None = None, delete: bool = False): + @overload + def shortcut(self, key: str, args: ShortcutArgs | None = None) -> str: + """操作快捷命令 + + Args: + key (str): 快捷命令名 + args (ShortcutArgs[TDC]): 快捷命令参数, 不传入时则尝试使用最近一次使用的命令 + + Returns: + str: 操作结果 + + Raises: + ValueError: 快捷命令操作失败时抛出 + """ + ... + + @overload + def shortcut( + self, + key: str, + *, + command: TDC | None = None, + arguments: list[Any] | None = None, + fuzzy: bool = True, + prefix: bool = False, + wrapper: ShortcutRegWrapper | None = None, + ) -> str: + """操作快捷命令 + + Args: + key (str): 快捷命令名 + command (TDC): 快捷命令指向的命令 + arguments (list[Any] | None, optional): 快捷命令参数, 默认为 `None` + fuzzy (bool, optional): 是否允许命令后随参数, 默认为 `True` + prefix (bool, optional): 是否调用时保留指令前缀, 默认为 `False` + wrapper (ShortcutRegWrapper, optional): 快捷指令的正则匹配结果的额外处理函数, 默认为 `None` + + Returns: + str: 操作结果 + + Raises: + ValueError: 快捷命令操作失败时抛出 + """ + ... + + @overload + def shortcut(self, key: str, *, delete: Literal[True]) -> str: + """操作快捷命令 + + Args: + key (str): 快捷命令名 + delete (bool): 是否删除快捷命令 + + Returns: + str: 操作结果 + + Raises: + ValueError: 快捷命令操作失败时抛出 + """ + ... + + def shortcut(self, key: str, args: ShortcutArgs | None = None, delete: bool = False, **kwargs): """操作快捷命令 Args: key (str): 快捷命令名 args (ShortcutArgs[TDC] | None, optional): 快捷命令参数, 不传入时则尝试使用最近一次使用的命令 delete (bool, optional): 是否删除快捷命令, 默认为 `False` + command (TDC, optional): 快捷命令指向的命令 + arguments (list[Any] | None, optional): 快捷命令参数, 默认为 `None` + fuzzy (bool, optional): 是否允许命令后随参数, 默认为 `True` + prefix (bool, optional): 是否调用时保留指令前缀, 默认为 `False` + wrapper (ShortcutRegWrapper, optional): 快捷指令的正则匹配结果的额外处理函数, 默认为 `None` Returns: str: 操作结果 @@ -223,6 +289,10 @@ def shortcut(self, key: str, args: ShortcutArgs | None = None, delete: bool = Fa if delete: command_manager.delete_shortcut(self, key) return lang.require("shortcut", "delete_success").format(shortcut=key, target=self.path) + if kwargs and not args: + kwargs["args"] = kwargs.pop("arguments", None) + kwargs = {k: v for k, v in kwargs.items() if v is not None} + args = cast(ShortcutArgs, kwargs) if args is not None: return command_manager.add_shortcut(self, key, args) elif cmd := command_manager.recent_message: diff --git a/src/arclet/alconna/manager.py b/src/arclet/alconna/manager.py index 578f5491..8a1dd971 100644 --- a/src/arclet/alconna/manager.py +++ b/src/arclet/alconna/manager.py @@ -8,8 +8,7 @@ import weakref from copy import copy from datetime import datetime -from typing import TYPE_CHECKING, Any, Match, TypedDict, Union, overload -from typing_extensions import NotRequired +from typing import TYPE_CHECKING, Any, Match, Union, cast, overload from weakref import WeakKeyDictionary, WeakValueDictionary from tarina import LRU, lang @@ -18,26 +17,13 @@ from .arparma import Arparma from .config import Namespace, config from .exceptions import ExceedMaxCount -from .typing import TDC, CommandMeta, DataCollection +from .typing import TDC, CommandMeta, DataCollection, ShortcutArgs, InnerShortcutArgs if TYPE_CHECKING: from ._internal._analyser import Analyser from .core import Alconna -class ShortcutArgs(TypedDict): - """快捷指令参数""" - - command: NotRequired[DataCollection[Any]] - """快捷指令的命令""" - args: NotRequired[list[Any]] - """快捷指令的附带参数""" - fuzzy: NotRequired[bool] - """是否允许命令后随参数""" - prefix: NotRequired[bool] - """是否调用时保留指令前缀""" - - class CommandManager: """ `Alconna` 命令管理器 @@ -54,7 +40,7 @@ class CommandManager: __argv: WeakKeyDictionary[Alconna, Argv] __abandons: list[Alconna] __record: LRU[int, Arparma] - __shortcuts: dict[str, Union[Arparma, ShortcutArgs]] + __shortcuts: dict[str, Union[Arparma, InnerShortcutArgs]] def __init__(self): self.cache_path = f"{__file__.replace('manager.py', '')}manager_cache.db" @@ -205,21 +191,23 @@ def add_shortcut(self, target: Alconna, key: str, source: Arparma | ShortcutArgs if isinstance(source, dict): source.setdefault("fuzzy", True) source.setdefault("prefix", False) + source.setdefault("wrapper", lambda slot, content: content) + source.setdefault("args", []) if source.get("prefix") and target.prefixes: out = [] for prefix in target.prefixes: if not isinstance(prefix, str): continue _src = source.copy() - _src["command"] = argv.converter(prefix + source.get("command", str(target.command))) + _src["command"] = argv.converter(prefix + source.get("command", str(target.command))) # type: ignore prefix = re.escape(prefix) - self.__shortcuts[f"{namespace}.{name}::{prefix}{key}"] = _src + self.__shortcuts[f"{namespace}.{name}::{prefix}{key}"] = cast(InnerShortcutArgs, _src) out.append( lang.require("shortcut", "add_success").format(shortcut=f"{prefix}{key}", target=target.path) ) return "\n".join(out) source["command"] = argv.converter(source.get("command", target.command or target.name)) - self.__shortcuts[f"{namespace}.{name}::{key}"] = source + self.__shortcuts[f"{namespace}.{name}::{key}"] = cast(InnerShortcutArgs, source) return lang.require("shortcut", "add_success").format(shortcut=key, target=target.path) elif source.matched: self.__shortcuts[f"{namespace}.{name}::{key}"] = source @@ -243,17 +231,17 @@ def list_shortcut(self, target: Alconna) -> list[str]: continue short = self.__shortcuts[i] if isinstance(short, dict): - result.append(i.split("::")[1] + (" ..." if short.get("fuzzy") else "")) + result.append(i.split("::")[1] + (" ..." if short["fuzzy"] else "")) else: result.append(i.split("::")[1]) return result @overload - def find_shortcut(self, target: Alconna[TDC]) -> list[Union[Arparma[TDC], ShortcutArgs]]: + def find_shortcut(self, target: Alconna[TDC]) -> list[Union[Arparma[TDC], InnerShortcutArgs]]: ... @overload - def find_shortcut(self, target: Alconna[TDC], query: str) -> tuple[Arparma[TDC] | ShortcutArgs, Match[str] | None]: + def find_shortcut(self, target: Alconna[TDC], query: str) -> tuple[Arparma[TDC] | InnerShortcutArgs, Match[str] | None]: ... def find_shortcut(self, target: Alconna[TDC], query: str | None = None): @@ -264,7 +252,7 @@ def find_shortcut(self, target: Alconna[TDC], query: str | None = None): query (str, optional): 快捷命令的名称. Defaults to None. Returns: - list[Union[Arparma, ShortcutArgs]] | tuple[Union[Arparma, ShortcutArgs], Match[str]]: \ + list[Union[Arparma, InnerShortcutArgs]] | tuple[Union[Arparma, InnerShortcutArgs], Match[str]]: \ 快捷命令的参数, 若没有 `query` 则返回目标命令的所有快捷命令, 否则返回匹配的快捷命令 """ namespace, name = self._command_part(target.path) diff --git a/src/arclet/alconna/typing.py b/src/arclet/alconna/typing.py index 94d12019..0f8e5b08 100644 --- a/src/arclet/alconna/typing.py +++ b/src/arclet/alconna/typing.py @@ -2,13 +2,39 @@ from __future__ import annotations from dataclasses import dataclass, field, fields, is_dataclass -from typing import Any, Dict, Iterator, List, Literal, Protocol, Tuple, TypeVar, Union, runtime_checkable +from typing import Any, Dict, Iterator, List, Literal, Protocol, Tuple, TypeVar, TypedDict, Union, runtime_checkable +from typing_extensions import NotRequired from nepattern import BasePattern, MatchMode, type_parser TPrefixes = Union[List[Union[str, object]], List[Tuple[object, str]]] DataUnit = TypeVar("DataUnit", covariant=True) +class ShortcutRegWrapper(Protocol): + def __call__(self, slot: int | str, content: str) -> Any: ... + +class ShortcutArgs(TypedDict): + """快捷指令参数""" + + command: NotRequired[DataCollection[Any]] + """快捷指令的命令""" + args: NotRequired[list[Any]] + """快捷指令的附带参数""" + fuzzy: NotRequired[bool] + """是否允许命令后随参数""" + prefix: NotRequired[bool] + """是否调用时保留指令前缀""" + wrapper: NotRequired[ShortcutRegWrapper] + """快捷指令的正则匹配结果的额外处理函数""" + + +class InnerShortcutArgs(TypedDict): + command: DataCollection[Any] + args: list[Any] + fuzzy: bool + prefix: bool + wrapper: ShortcutRegWrapper + @runtime_checkable class DataCollection(Protocol[DataUnit]): diff --git a/tests/core_test.py b/tests/core_test.py index 6bc6169c..e466883d 100644 --- a/tests/core_test.py +++ b/tests/core_test.py @@ -421,6 +421,8 @@ def test_fuzzy(): def test_shortcut(): + from arclet.alconna import output_manager + # 原始命令 alc16 = Alconna("core16", Args["foo", int], Option("bar", Args["baz", str])) assert alc16.parse("core16 123 bar abcd").matched is True @@ -449,8 +451,8 @@ def test_shortcut(): assert alc16.parse("tTest123").matched alc16_1 = Alconna("exec", Args["content", str]) - alc16_1.shortcut("echo", {"command": "exec print({%0})"}) - alc16_1.shortcut("echo1", {"command": "exec print(\\'{*\n}\\')"}) + alc16_1.shortcut("echo", command="exec print({%0})") + alc16_1.shortcut("echo1", command="exec print(\\'{*\n}\\')") res5 = alc16_1.parse("echo 123") assert res5.content == "print(123)" assert not alc16_1.parse("echo 123 456").matched @@ -491,6 +493,23 @@ def test_shortcut(): assert alc16_5.parse("*core16_5 False").matched assert alc16_5.parse("+test").foo is True + def wrapper(slot, content): + if content == "help": + return "--help" + return content + + alc16_6 = Alconna("core16_6", Args["bar", str]) + alc16_6.shortcut("test(?P.+)?", wrapper=wrapper, arguments=["{bar}"]) + assert alc16_6.parse("testabc").bar == "abc" + + with output_manager.capture("core16_6") as cap: + output_manager.set_action(lambda x: x, "core16_6") + alc16_6.parse("testhelp") + assert ( + cap["output"] + == "core16_6 \nUnknown" + ) + def test_help(): from arclet.alconna import output_manager