From cc2f4d86808ebe9dd96222ff602e071d3784039d Mon Sep 17 00:00:00 2001 From: RF-Tar-Railt Date: Sun, 13 Oct 2024 16:32:35 +0800 Subject: [PATCH] :art: format --- benchmark.py | 1 - devtool.py | 4 +- src/arclet/alconna/__init__.py | 4 +- src/arclet/alconna/_dcls.py | 3 +- src/arclet/alconna/_internal/_analyser.py | 75 ++++------------ src/arclet/alconna/_internal/_argv.py | 2 +- src/arclet/alconna/_internal/_handlers.py | 34 +++++--- src/arclet/alconna/_internal/_header.py | 2 +- src/arclet/alconna/_internal/_shortcut.py | 5 +- src/arclet/alconna/_stargazing/compiler.py | 14 ++- src/arclet/alconna/_stargazing/flywheel.py | 4 +- src/arclet/alconna/args.py | 4 +- src/arclet/alconna/builtin.py | 5 +- src/arclet/alconna/completion.py | 16 ++-- src/arclet/alconna/config.py | 1 - src/arclet/alconna/core.py | 90 ++++++++++---------- src/arclet/alconna/exceptions.py | 4 + src/arclet/alconna/i18n/__init__.py | 1 - src/arclet/alconna/manager.py | 71 +++++++-------- src/arclet/alconna/model.py | 3 +- src/arclet/alconna/sistana/fragment.py | 7 +- src/arclet/alconna/sistana/model/capture.py | 7 +- src/arclet/alconna/sistana/model/snapshot.py | 4 +- src/arclet/alconna/sistana/some.py | 1 - src/arclet/alconna/typing.py | 7 +- tests/args_test.py | 2 +- tests/components_test.py | 2 +- tests/core_test.py | 5 +- tests/devtool.py | 4 +- tests/sistana/asserts.py | 3 +- tests/sistana/test_pattern.py | 2 +- 31 files changed, 176 insertions(+), 211 deletions(-) diff --git a/benchmark.py b/benchmark.py index 303318b1..cf148870 100644 --- a/benchmark.py +++ b/benchmark.py @@ -28,7 +28,6 @@ def __repr__(self): with namespace("test") as np: - np.enable_message_cache = False np.to_text = lambda x: x.text if x.__class__ is Plain else None alc = Alconna( ["."], diff --git a/devtool.py b/devtool.py index 230f9b2a..4b2a4087 100644 --- a/devtool.py +++ b/devtool.py @@ -5,15 +5,15 @@ from typing import Any, Literal from arclet.alconna._internal._analyser import Analyser, default_compiler -from arclet.alconna._internal._handlers import analyse_args as ala from arclet.alconna._internal._handlers import HEAD_HANDLES +from arclet.alconna._internal._handlers import analyse_args as ala from arclet.alconna._internal._handlers import analyse_option as alo from arclet.alconna._internal._header import Header from arclet.alconna.args import Args from arclet.alconna.argv import Argv from arclet.alconna.base import Option, Subcommand from arclet.alconna.config import Namespace -from arclet.alconna.typing import DataCollection, CommandMeta +from arclet.alconna.typing import CommandMeta, DataCollection class AnalyseError(Exception): diff --git a/src/arclet/alconna/__init__.py b/src/arclet/alconna/__init__.py index c4a5a3f3..11b0ed92 100644 --- a/src/arclet/alconna/__init__.py +++ b/src/arclet/alconna/__init__.py @@ -20,8 +20,8 @@ from .arparma import ArparmaBehavior as ArparmaBehavior from .base import Option as Option from .base import Subcommand as Subcommand -from .builtin import set_default as set_default from .builtin import conflict as conflict +from .builtin import set_default as set_default from .completion import CompSession as CompSession from .config import Namespace as Namespace from .config import config as config @@ -45,9 +45,9 @@ from .typing import Kw as Kw from .typing import MultiVar as MultiVar from .typing import Nargs as Nargs +from .typing import StrMulti as StrMulti from .typing import UnpackVar as UnpackVar from .typing import Up as Up -from .typing import StrMulti as StrMulti __version__ = "1.8.31" diff --git a/src/arclet/alconna/_dcls.py b/src/arclet/alconna/_dcls.py index 8871aa6b..cc5a01ae 100644 --- a/src/arclet/alconna/_dcls.py +++ b/src/arclet/alconna/_dcls.py @@ -1,8 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING from dataclasses import dataclass - +from typing import TYPE_CHECKING if TYPE_CHECKING: diff --git a/src/arclet/alconna/_internal/_analyser.py b/src/arclet/alconna/_internal/_analyser.py index 1caa82ac..7f449d01 100644 --- a/src/arclet/alconna/_internal/_analyser.py +++ b/src/arclet/alconna/_internal/_analyser.py @@ -11,10 +11,10 @@ from ..arparma import Arparma from ..base import Completion, Help, Option, Shortcut, Subcommand from ..completion import comp_ctx -from ..constraint import SHORTCUT_ARGS, SHORTCUT_REGEX_MATCH, SHORTCUT_REST, SHORTCUT_TRIGGER from ..exceptions import ( ArgumentMissing, FuzzyMatchSuccess, + InvalidHeader, InvalidParam, ParamsUnmatched, PauseTriggered, @@ -26,7 +26,6 @@ from ..typing import TDC from ._handlers import ( HEAD_HANDLES, - handle_head_fuzzy, analyse_args, analyse_param, handle_completion, @@ -35,7 +34,6 @@ handle_shortcut, prompt, ) -from ._shortcut import shortcut from ._header import Header from ._util import levenshtein @@ -209,8 +207,6 @@ class Analyser(SubAnalyser): command: Alconna """命令实例""" - used_tokens: set[int] - """已使用的token""" command_header: Header """命令头部""" header_handler: Callable[[Header, Argv], HeadResult] @@ -225,7 +221,6 @@ def __init__(self, alconna: Alconna, compiler: TCompile | None = None): """ super().__init__(alconna) self._compiler = compiler or default_compiler - self.used_tokens = set() def compile(self, param_ids: set[str]): self.extra_allow = not self.command.meta.strict or not self.command.namespace_config.strict @@ -234,78 +229,45 @@ def compile(self, param_ids: set[str]): self._compiler(self, param_ids) return self - def _clr(self): - self.used_tokens.clear() - super()._clr() - def __repr__(self): return f"<{self.__class__.__name__} of {self.command.path}>" - def process(self, argv: Argv[TDC]) -> Arparma[TDC] | None: + def process(self, argv: Argv[TDC]) -> Exception | None: """主体解析函数, 应针对各种情况进行解析 Args: argv (Argv[TDC]): 命令行参数 - Returns: - Arparma[TDC]: Arparma 解析结果 - - Raises: - ValueError: 快捷命令查找失败 - InvalidParam: 参数不匹配 - ArgumentMissing: 参数缺失 """ - if argv.message_cache and argv.token in self.used_tokens and (res := command_manager.get_record(argv.token)): - return res - try: - self.header_result = self.header_handler(self.command_header, argv) - except InvalidParam as e: - _next = e.args[1] - if _next.__class__ is not str or not _next: - raise e - argv.context[SHORTCUT_TRIGGER] = _next + if not self.header_result: try: - rest, short, mat = command_manager.find_shortcut(self.command, [_next] + argv.release()) - except ValueError as exc: - if argv.fuzzy_match and (res := handle_head_fuzzy(self.command_header, _next, argv.fuzzy_threshold)): - output_manager.send(self.command.name, lambda: res) - raise FuzzyMatchSuccess(res) from None - raise e from exc - - argv.context[SHORTCUT_ARGS] = short - argv.context[SHORTCUT_REST] = rest - argv.context[SHORTCUT_REGEX_MATCH] = mat - self.reset() - if isinstance(short, Arparma): - return short - shortcut(argv, rest, short, mat) - self.header_result = self.header_handler(self.command_header, argv) - self.header_result.origin = _next - - except RuntimeError as e: - exc = InvalidParam(lang.require("header", "error").format(target=argv.release(recover=True)[0])) - raise exc from e + self.header_result = self.header_handler(self.command_header, argv) + except InvalidHeader as e: + return e + except RuntimeError: + exc = InvalidParam(lang.require("header", "error").format(target=argv.release(recover=True)[0])) + return exc try: while analyse_param(self, argv) and argv.current_index != argv.ndata: argv.current_node = None except FuzzyMatchSuccess as e: output_manager.send(self.command.name, lambda: str(e)) - raise e + return e except SpecialOptionTriggered as sot: - raise _SPECIAL[sot.args[0]](self, argv) + return _SPECIAL[sot.args[0]](self, argv) except (InvalidParam, ArgumentMissing) as e1: if (rest := argv.release()) and isinstance(rest[-1], str): if rest[-1] in argv.completion_names and "completion" not in argv.namespace.disable_builtin_options: argv.bak_data[-1] = argv.bak_data[-1][: -len(rest[-1])].rstrip() - raise handle_completion(self, argv) + return handle_completion(self, argv) if (handler := argv.special.get(rest[-1])) and handler not in argv.namespace.disable_builtin_options: - raise _SPECIAL[handler](self, argv) + return _SPECIAL[handler](self, argv) if comp_ctx.get(None): if isinstance(e1, InvalidParam): argv.free(argv.current_node.separators if argv.current_node else None) - raise PauseTriggered(prompt(self, argv), e1, argv) from e1 - raise + return PauseTriggered(prompt(self, argv), e1, argv) + return e1 if self.default_main_only and not self.args_result: self.args_result = analyse_args(argv, self.self_args) @@ -317,15 +279,15 @@ def process(self, argv: Argv[TDC]) -> Arparma[TDC] | None: if len(rest) > 0: if isinstance(rest[-1], str) and rest[-1] in argv.completion_names: argv.bak_data[-1] = argv.bak_data[-1][: -len(rest[-1])].rstrip() - raise handle_completion(self, argv, rest[-2]) + return handle_completion(self, argv, rest[-2]) exc = ParamsUnmatched(lang.require("analyser", "param_unmatched").format(target=argv.next(move=False)[0])) else: exc = ArgumentMissing( self.self_args.argument[0].field.get_missing_tips(lang.require("analyser", "param_missing")) ) if comp_ctx.get(None) and isinstance(exc, ArgumentMissing): - raise PauseTriggered(prompt(self, argv), exc, argv) - raise exc + return PauseTriggered(prompt(self, argv), exc, argv) + return exc def export( self, @@ -357,7 +319,6 @@ def export( result.unpack() if argv.message_cache: command_manager.record(argv.token, result) - self.used_tokens.add(argv.token) self.reset() return result # type: ignore diff --git a/src/arclet/alconna/_internal/_argv.py b/src/arclet/alconna/_internal/_argv.py index 91f20cf4..c6fc8490 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, InitVar +from dataclasses import InitVar, dataclass, field, fields from typing import Any, Callable, ClassVar, Generic, Iterable, Literal from typing_extensions import Self diff --git a/src/arclet/alconna/_internal/_handlers.py b/src/arclet/alconna/_internal/_handlers.py index 1f1535cf..08556094 100644 --- a/src/arclet/alconna/_internal/_handlers.py +++ b/src/arclet/alconna/_internal/_handlers.py @@ -1,23 +1,31 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING, Any, Iterable, Callable +from typing import TYPE_CHECKING, Any, Callable, Iterable +from typing_extensions import NoReturn from nepattern import ANY, STRING, AnyString, BasePattern, TPattern from tarina import Empty, lang, safe_eval, split_once -from typing_extensions import NoReturn from ..action import Action from ..args import Arg, Args from ..base import Option, Subcommand from ..completion import Prompt, comp_ctx from ..config import config -from ..exceptions import AlconnaException, ArgumentMissing, FuzzyMatchSuccess, InvalidParam, PauseTriggered, SpecialOptionTriggered +from ..exceptions import ( + AlconnaException, + ArgumentMissing, + FuzzyMatchSuccess, + InvalidHeader, + InvalidParam, + PauseTriggered, + SpecialOptionTriggered, +) from ..model import HeadResult, OptionResult from ..output import output_manager -from ..typing import KWBool, MultiKeyWordVar, MultiVar, _ShortcutRegWrapper, _StrMulti, _AllParamPattern +from ..typing import KWBool, MultiKeyWordVar, MultiVar, _AllParamPattern, _StrMulti from ._header import Header -from ._util import escape, levenshtein, unescape +from ._util import levenshtein if TYPE_CHECKING: from ._analyser import Analyser, SubAnalyser @@ -479,7 +487,7 @@ def _header_handle0(header: "Header[set[str], TPattern]", argv: Argv): if header.compact and (mat := header.compact_pattern.match(cmd)): argv.rollback(cmd[len(mat[0]):], replace=True) return HeadResult(mat[0], mat[0], True, mat.groupdict(), header.mapping) - _after_analyse_header(header, argv, head_text, may_cmd, _str, _m_str) + _after_analyse_header(argv, head_text, may_cmd, _str, _m_str) def _header_handle1(header: "Header[TPattern, TPattern]", argv: Argv): @@ -499,7 +507,7 @@ def _header_handle1(header: "Header[TPattern, TPattern]", argv: Argv): if header.compact and (mat := header.compact_pattern.match(cmd)): argv.rollback(cmd[len(mat[0]):], replace=True) return HeadResult(mat[0], mat[0], True, mat.groupdict(), header.mapping) - _after_analyse_header(header, argv, head_text, may_cmd, _str, _m_str) + _after_analyse_header(argv, head_text, may_cmd, _str, _m_str) def _header_handle2(header: "Header[BasePattern, BasePattern]", argv: Argv): @@ -511,7 +519,7 @@ def _header_handle2(header: "Header[BasePattern, BasePattern]", argv: Argv): argv.rollback(head_text[len(str(val._value)):], replace=True) return HeadResult(val.value, val._value, True, fixes=header.mapping) may_cmd, _m_str = argv.next() - _after_analyse_header(header, argv, head_text, may_cmd, _str, _m_str) + _after_analyse_header(argv, head_text, may_cmd, _str, _m_str) HEAD_HANDLES: dict[int, Callable[[Header, Argv], HeadResult]] = { @@ -521,15 +529,15 @@ def _header_handle2(header: "Header[BasePattern, BasePattern]", argv: Argv): } -def _after_analyse_header(header: Header, argv: Argv, head_text: Any, may_cmd: Any, _str: bool, _m_str: bool) -> NoReturn: +def _after_analyse_header(argv: Argv, head_text: Any, may_cmd: Any, _str: bool, _m_str: bool) -> NoReturn: if _str: argv.rollback(may_cmd) - raise InvalidParam(lang.require("header", "error").format(target=head_text), head_text) + raise InvalidHeader(lang.require("header", "error").format(target=head_text), head_text) if _m_str and may_cmd: cmd = f"{head_text}{argv.separators[0]}{may_cmd}" - raise InvalidParam(lang.require("header", "error").format(target=cmd), cmd) + raise InvalidHeader(lang.require("header", "error").format(target=cmd), cmd) argv.rollback(may_cmd) - raise InvalidParam(lang.require("header", "error").format(target=head_text), None) + raise InvalidHeader(lang.require("header", "error").format(target=head_text), None) def handle_head_fuzzy(header: Header, source: str, threshold: float): @@ -579,8 +587,6 @@ def handle_shortcut(analyser: Analyser, argv: Argv): raise ArgumentMissing(lang.require("shortcut", "name_require")) if opt_v.get("action") == "delete": msg = analyser.command.shortcut(opt_v["name"], delete=True) - elif opt_v["command"] == "_": - msg = analyser.command.shortcut(opt_v["name"], None) elif opt_v["command"] == "$": msg = analyser.command.shortcut(opt_v["name"], fuzzy=True) else: diff --git a/src/arclet/alconna/_internal/_header.py b/src/arclet/alconna/_internal/_header.py index 1adddeba..65a5f43f 100644 --- a/src/arclet/alconna/_internal/_header.py +++ b/src/arclet/alconna/_internal/_header.py @@ -1,7 +1,7 @@ from __future__ import annotations import re -from typing import TypeVar, Generic +from typing import Generic, TypeVar from nepattern import BasePattern, MatchMode, all_patterns, parser from nepattern.util import TPattern diff --git a/src/arclet/alconna/_internal/_shortcut.py b/src/arclet/alconna/_internal/_shortcut.py index e6c7ca84..cfb8fd03 100644 --- a/src/arclet/alconna/_internal/_shortcut.py +++ b/src/arclet/alconna/_internal/_shortcut.py @@ -6,10 +6,9 @@ from tarina import lang from ..exceptions import ArgumentMissing, ParamsUnmatched -from ..typing import _ShortcutRegWrapper, TDC, InnerShortcutArgs -from ._util import escape, unescape +from ..typing import InnerShortcutArgs, _ShortcutRegWrapper from ._argv import Argv - +from ._util import escape, unescape INDEX_SLOT = re.compile(r"\{%(\d+)\}") WILDCARD_SLOT = re.compile(r"\{\*(.*)\}", re.DOTALL) diff --git a/src/arclet/alconna/_stargazing/compiler.py b/src/arclet/alconna/_stargazing/compiler.py index cfa2a220..405deb43 100644 --- a/src/arclet/alconna/_stargazing/compiler.py +++ b/src/arclet/alconna/_stargazing/compiler.py @@ -4,12 +4,21 @@ from typing import Any, Iterable, Sequence, overload import tarina +from elaina_segment import Buffer from elaina_triehard import TrieHard from arclet.alconna import Alconna, Arg, Args, Arparma, HeadResult, Option, OptionResult, Subcommand, SubcommandResult -from arclet.alconna.exceptions import ArgumentMissing, InvalidArgs, InvalidParam, NullMessage, ParamsUnmatched, UnexpectedElement +from arclet.alconna.exceptions import ( + ArgumentMissing, + InvalidArgs, + InvalidParam, + NullMessage, + ParamsUnmatched, + UnexpectedElement, +) from arclet.alconna.sistana import ( Analyzer, + AnalyzeSnapshot, Fragment, LoopflowExitReason, OptionPattern, @@ -17,14 +26,13 @@ SubcommandPattern, Track, Value, - AnalyzeSnapshot, ) from arclet.alconna.sistana.err import ParsePanic, Rejected from arclet.alconna.sistana.model.fragment import _Fragment from arclet.alconna.sistana.model.pointer import PointerRole from arclet.alconna.sistana.model.snapshot import SubcommandTraverse + from .flywheel import build_runes -from elaina_segment import Buffer def _alc_args_to_fragments(args: Args) -> deque[_Fragment]: diff --git a/src/arclet/alconna/_stargazing/flywheel.py b/src/arclet/alconna/_stargazing/flywheel.py index eb0f6455..2a5eb677 100644 --- a/src/arclet/alconna/_stargazing/flywheel.py +++ b/src/arclet/alconna/_stargazing/flywheel.py @@ -1,7 +1,9 @@ from __future__ import annotations + from typing import Sequence, TypeVar -from elaina_segment import Runes, build_runes as _build_runes +from elaina_segment import Runes +from elaina_segment import build_runes as _build_runes from flywheel import wrap_anycast T = TypeVar("T") diff --git a/src/arclet/alconna/args.py b/src/arclet/alconna/args.py index c5e23d0a..d1d6edd2 100644 --- a/src/arclet/alconna/args.py +++ b/src/arclet/alconna/args.py @@ -6,14 +6,14 @@ from enum import Enum from functools import partial from typing import Any, Callable, Generic, Iterable, List, Sequence, TypeVar, Union, cast +from typing_extensions import Self from nepattern import ANY, NONE, AntiPattern, BasePattern, MatchMode, RawStr, UnionPattern, parser from tarina import Empty, get_signature, lang -from typing_extensions import Self +from ._dcls import safe_dcls_kw from .exceptions import InvalidArgs from .typing import AllParam, KeyWordVar, KWBool, MultiKeyWordVar, MultiVar, TAValue, UnpackVar -from ._dcls import safe_dcls_kw _T = TypeVar("_T") diff --git a/src/arclet/alconna/builtin.py b/src/arclet/alconna/builtin.py index 861f090f..4dfe755a 100644 --- a/src/arclet/alconna/builtin.py +++ b/src/arclet/alconna/builtin.py @@ -1,13 +1,12 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any, Callable, cast, overload +from typing import Any, Callable, overload from tarina import Empty -from .config import lang from .arparma import Arparma, ArparmaBehavior -from .core import Alconna +from .config import lang from .exceptions import BehaveCancelled from .model import OptionResult, SubcommandResult diff --git a/src/arclet/alconna/completion.py b/src/arclet/alconna/completion.py index 49b81500..d00dc9c8 100644 --- a/src/arclet/alconna/completion.py +++ b/src/arclet/alconna/completion.py @@ -138,17 +138,13 @@ def enter(self, content: list | None = None) -> EnterResult: argv.bak_data = argv.raw_data.copy() argv.ndata = len(argv.bak_data) argv.current_index = 0 + argv.origin = argv.converter(argv.raw_data) if argv.message_cache: argv.token = argv.generate_token(argv.raw_data) - argv.origin = argv.converter(argv.raw_data) - exc = None - try: - res = self.source.process(argv) - if not res: - res = self.source.export(argv) - except Exception as e: - exc = e - if exc: + if res := command_manager.get_record(argv.token): + self.exit() + return EnterResult(res) + if exc := self.source.process(argv): if isinstance(exc, (ParamsUnmatched, SpecialOptionTriggered)): self.exit() return EnterResult(self.source.export(argv, True, exc)) @@ -157,7 +153,7 @@ def enter(self, content: list | None = None) -> EnterResult: return EnterResult(exception=self.trigger if isinstance(self.trigger, InvalidParam) else None) return EnterResult(exception=exc) self.exit() - return EnterResult(res) # noqa # type: ignore + return EnterResult(self.source.export(argv)) # noqa # type: ignore def push(self, *suggests: Prompt): """添加补全选项。 diff --git a/src/arclet/alconna/config.py b/src/arclet/alconna/config.py index 1ad985c2..6fd5cb90 100644 --- a/src/arclet/alconna/config.py +++ b/src/arclet/alconna/config.py @@ -3,7 +3,6 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any, Callable, ContextManager, Literal, TypedDict - from .i18n import lang as lang from .typing import DataCollection diff --git a/src/arclet/alconna/core.py b/src/arclet/alconna/core.py index e251ff68..16d35216 100644 --- a/src/arclet/alconna/core.py +++ b/src/arclet/alconna/core.py @@ -5,21 +5,31 @@ from dataclasses import dataclass, field from pathlib import Path from typing import Any, Callable, Generic, Literal, Sequence, TypeVar, cast, overload - -from nepattern import TPattern from typing_extensions import Self from weakref import WeakSet +from nepattern import TPattern from tarina import init_spec, lang from ._internal._analyser import Analyser, TCompile +from ._internal._handlers import handle_head_fuzzy +from ._internal._shortcut import shortcut as _shortcut from .args import Arg, Args from .arparma import Arparma, ArparmaBehavior, requirement_handler from .base import Completion, Help, Option, Shortcut, Subcommand from .config import Namespace, config -from .exceptions import ExecuteFailed, PauseTriggered, AlconnaException +from .constraint import SHORTCUT_ARGS, SHORTCUT_REGEX_MATCH, SHORTCUT_REST, SHORTCUT_TRIGGER +from .exceptions import ( + AlconnaException, + ExecuteFailed, + FuzzyMatchSuccess, + InvalidHeader, + PauseTriggered, + SpecialOptionTriggered, +) from .formatter import TextFormatter from .manager import ShortcutArgs, command_manager +from .output import output_manager from .typing import TDC, CommandMeta, InnerShortcutArgs, ShortcutRegWrapper T = TypeVar("T") @@ -223,7 +233,7 @@ def _get_shortcuts(self): return command_manager.get_shortcut(self) @overload - def shortcut(self, key: str | TPattern, args: ShortcutArgs | None = None) -> str: + def shortcut(self, key: str | TPattern, args: ShortcutArgs) -> str: """操作快捷命令 Args: @@ -295,17 +305,8 @@ def shortcut(self, key: str | TPattern, args: ShortcutArgs | None = None, delete args = cast(ShortcutArgs, kwargs) if args is not None: return command_manager.add_shortcut(self, key, args) - elif cmd := command_manager.recent_message: - alc = command_manager.last_using - if alc and alc == self: - return command_manager.add_shortcut(self, key, {"command": cmd}) # type: ignore - raise ValueError( - lang.require("shortcut", "recent_command_error").format( - target=self.path, source=getattr(alc, "path", "Unknown") - ) - ) else: - raise ValueError(lang.require("shortcut", "no_recent_command")) + raise ValueError(args) except Exception as e: if self.meta.raise_exception: raise e @@ -338,19 +339,42 @@ def subcommand(self, sub: Subcommand) -> Self: return self.add(sub) def _parse(self, message: TDC, ctx: dict[str, Any] | None = None) -> Arparma[TDC]: + if self.union: + for alc in self.union: + if (res := alc._parse(message, ctx)).matched: + return res analyser = command_manager.require(self) argv = command_manager.resolve(self) argv.enter(ctx).build(message) - try: - if cache := analyser.process(argv): - return cache + if argv.message_cache and (res := command_manager.get_record(argv.token)): + return res + if not (exc := analyser.process(argv)): return analyser.export(argv) - except PauseTriggered: - raise - except AlconnaException as e: - if self.meta.raise_exception: - raise e - return analyser.export(argv, True, e) + if isinstance(exc, InvalidHeader): + trigger = exc.args[1] + if trigger.__class__ is str and trigger: + argv.context[SHORTCUT_TRIGGER] = trigger + try: + rest, short, mat = command_manager.find_shortcut(self, [trigger] + argv.release()) + argv.context[SHORTCUT_ARGS] = short + argv.context[SHORTCUT_REST] = rest + argv.context[SHORTCUT_REGEX_MATCH] = mat + _shortcut(argv, rest, short, mat) + analyser.header_result = analyser.header_handler(analyser.command_header, argv) + analyser.header_result.origin = trigger + if not (exc := analyser.process(argv)): + return analyser.export(argv) + except ValueError: + if argv.fuzzy_match and (res := handle_head_fuzzy(analyser.command_header, trigger, argv.fuzzy_threshold)): + output_manager.send(self.name, lambda: res) + exc = FuzzyMatchSuccess(res) + except AlconnaException as e: + exc = e + if isinstance(exc, PauseTriggered): + raise exc + if self.meta.raise_exception and not isinstance(exc, (FuzzyMatchSuccess, SpecialOptionTriggered)): + raise exc + return analyser.export(argv, True, exc) def parse(self, message: TDC, ctx: dict[str, Any] | None = None) -> Arparma[TDC]: """命令分析功能, 传入字符串或消息链, 返回一个特定的数据集合类 @@ -412,26 +436,6 @@ def __add__(self, other) -> Self: def __or__(self, other: Alconna) -> Self: self.union.add(other) - - def _parse(message: TDC, ctx: dict[str, Any] | None = None) -> Arparma[TDC]: - for alc in self.union: - if (res := alc._parse(message, ctx)).matched: - return res - analyser = command_manager.require(self) - argv = command_manager.resolve(self) - argv.enter(ctx).build(message) - try: - if cache := analyser.process(argv): - return cache - return analyser.export(argv) - except PauseTriggered: - raise - except AlconnaException as e: - if self.meta.raise_exception: - raise e - return analyser.export(argv, True, e) - - self._parse = _parse return self def _calc_hash(self): diff --git a/src/arclet/alconna/exceptions.py b/src/arclet/alconna/exceptions.py index 7816f2a2..e4ebf5d7 100644 --- a/src/arclet/alconna/exceptions.py +++ b/src/arclet/alconna/exceptions.py @@ -13,6 +13,10 @@ class InvalidParam(AlconnaException): """传入参数验证失败""" +class InvalidHeader(AlconnaException): + """传入的消息头部无效""" + + class ArgumentMissing(AlconnaException): """组件内的 Args 参数未能解析到任何内容""" diff --git a/src/arclet/alconna/i18n/__init__.py b/src/arclet/alconna/i18n/__init__.py index 8f0ac98a..d30a12bb 100644 --- a/src/arclet/alconna/i18n/__init__.py +++ b/src/arclet/alconna/i18n/__init__.py @@ -6,5 +6,4 @@ from tarina.lang import lang - lang.load(Path(__file__).parent) diff --git a/src/arclet/alconna/manager.py b/src/arclet/alconna/manager.py index f297ac41..cbdb8f25 100644 --- a/src/arclet/alconna/manager.py +++ b/src/arclet/alconna/manager.py @@ -8,9 +8,9 @@ import weakref from copy import copy from datetime import datetime -from typing import TYPE_CHECKING, Any, Match, MutableSet, Union -from weakref import WeakValueDictionary from pathlib import Path +from typing import TYPE_CHECKING, Any, Match +from weakref import WeakValueDictionary from nepattern import TPattern from tarina import LRU, lang @@ -45,7 +45,7 @@ def max_count(self) -> int: __argv: dict[int, Argv] __abandons: list[int] __record: LRU[int, Arparma] - __shortcuts: dict[str, tuple[dict[str, Union[Arparma, InnerShortcutArgs]], dict[str, Union[Arparma, InnerShortcutArgs]]]] + __shortcuts: dict[str, tuple[dict[str, InnerShortcutArgs], dict[str, InnerShortcutArgs]]] def __init__(self): self.sign = "ALCONNA::" @@ -225,18 +225,18 @@ def set_enabled(self, command: Alconna | str, enabled: bool): """设置命令是否被禁用""" if isinstance(command, str): command = self.get_command(command) - if enabled and command in self.__abandons: + if enabled and command._hash in self.__abandons: self.__abandons.remove(command._hash) if not enabled and command not in self.__abandons: self.__abandons.append(command._hash) - def add_shortcut(self, target: Alconna, key: str | TPattern, source: Arparma[Any] | ShortcutArgs): + def add_shortcut(self, target: Alconna, key: str | TPattern, source: ShortcutArgs): """添加快捷命令 Args: target (Alconna): 目标命令 key (str): 快捷命令的名称 - source (Arparma | ShortcutArgs): 快捷命令的参数 + source (ShortcutArgs): 快捷命令的参数 """ namespace, name = self._command_part(target.path) argv = self.resolve(target) @@ -247,45 +247,38 @@ def add_shortcut(self, target: Alconna, key: str | TPattern, source: Arparma[Any else: _key = key.pattern _flags = key.flags - if isinstance(source, dict): - humanize = source.pop("humanized", None) - if source.get("prefix", False) and target.prefixes: - out = [] - for prefix in target.prefixes: - _shortcut[1][f"{re.escape(prefix)}{_key}"] = InnerShortcutArgs( - **{**source, "command": argv.converter(prefix + source.get("command", str(target.command)))}, - flags=_flags, - ) - out.append( - lang.require("shortcut", "add_success").format(shortcut=f"{prefix}{_key}", target=target.path) - ) - _shortcut[0][humanize or _key] = InnerShortcutArgs( - **{**source, "command": argv.converter(source.get("command", str(target.command))), "prefixes": target.prefixes}, + humanize = source.pop("humanized", None) + if source.get("prefix", False) and target.prefixes: + out = [] + for prefix in target.prefixes: + _shortcut[1][f"{re.escape(prefix)}{_key}"] = InnerShortcutArgs( + **{**source, "command": argv.converter(prefix + source.get("command", str(target.command)))}, flags=_flags, ) - target.formatter.update_shortcut(target) - return "\n".join(out) - _shortcut[0][humanize or _key] = _shortcut[1][_key] = InnerShortcutArgs( - **{**source, "command": argv.converter(source.get("command", str(target.command)))}, + out.append( + lang.require("shortcut", "add_success").format(shortcut=f"{prefix}{_key}", target=target.path) + ) + _shortcut[0][humanize or _key] = InnerShortcutArgs( + **{**source, "command": argv.converter(source.get("command", str(target.command))), "prefixes": target.prefixes}, flags=_flags, ) target.formatter.update_shortcut(target) - return lang.require("shortcut", "add_success").format(shortcut=_key, target=target.path) - elif source.matched: - _shortcut[0][_key] = _shortcut[1][_key] = source - target.formatter.update_shortcut(target) - return lang.require("shortcut", "add_success").format(shortcut=_key, target=target.path) - else: - raise ValueError(lang.require("manager", "incorrect_shortcut").format(target=f"{_key}")) + return "\n".join(out) + _shortcut[0][humanize or _key] = _shortcut[1][_key] = InnerShortcutArgs( + **{**source, "command": argv.converter(source.get("command", str(target.command)))}, + flags=_flags, + ) + target.formatter.update_shortcut(target) + return lang.require("shortcut", "add_success").format(shortcut=_key, target=target.path) - def get_shortcut(self, target: Alconna) -> dict[str, Union[Arparma[Any], InnerShortcutArgs]]: + def get_shortcut(self, target: Alconna) -> dict[str, InnerShortcutArgs]: """列出快捷命令 Args: target (Alconna): 目标命令 Returns: - dict[str, Arparma | InnerShortcutArgs]: 快捷命令的参数 + dict[str, InnerShortcutArgs]: 快捷命令的参数 """ namespace, name = self._command_part(target.path) cmd_hash = target._hash @@ -298,7 +291,7 @@ def get_shortcut(self, target: Alconna) -> dict[str, Union[Arparma[Any], InnerSh def find_shortcut( self, target: Alconna, data: list - ) -> tuple[list, Arparma[Any] | InnerShortcutArgs, Match[str] | None]: + ) -> tuple[list, InnerShortcutArgs, Match[str] | None]: """查找快捷命令 Args: @@ -306,7 +299,7 @@ def find_shortcut( data (list): 传入的命令数据 Returns: - tuple[list, Union[Arparma, InnerShortcutArgs], re.Match[str]]: 返回匹配的快捷命令 + tuple[list, InnerShortcutArgs, re.Match[str]]: 返回匹配的快捷命令 """ namespace, name = self._command_part(target.path) if not (_shortcut := self.__shortcuts.get(f"{namespace}.{name}")): @@ -316,12 +309,12 @@ def find_shortcut( if query in _shortcut[1]: return data, _shortcut[1][query], None for key, args in _shortcut[1].items(): - if isinstance(args, InnerShortcutArgs) and args.fuzzy and (mat := re.match(f"^{key}", query, args.flags)): + if args.fuzzy and (mat := re.match(f"^{key}", query, args.flags)): if len(query) > mat.span()[1]: data.insert(0, query[mat.span()[1]:]) return data, args, mat - elif mat := re.fullmatch(key, query, getattr(args, "flags", 0)): - if not (isinstance(args, InnerShortcutArgs) and not args.fuzzy and data): + elif mat := re.fullmatch(key, query, args.flags): + if not (not args.fuzzy and data): return data, _shortcut[1][key], mat if not data: break @@ -467,8 +460,6 @@ def clear_result(self, command: Alconna): for token in tokens: if self.__record[token]._id == command._hash: del self.__record[token] - ana = self.require(command) - ana.used_tokens.clear() @property def recent_message(self) -> DataCollection[str | Any] | None: diff --git a/src/arclet/alconna/model.py b/src/arclet/alconna/model.py index bb3be461..e128b472 100644 --- a/src/arclet/alconna/model.py +++ b/src/arclet/alconna/model.py @@ -1,6 +1,7 @@ from __future__ import annotations + from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Optional, Dict +from typing import TYPE_CHECKING, Any, Dict, Optional from nepattern import BasePattern diff --git a/src/arclet/alconna/sistana/fragment.py b/src/arclet/alconna/sistana/fragment.py index aed84333..ac5eca98 100644 --- a/src/arclet/alconna/sistana/fragment.py +++ b/src/arclet/alconna/sistana/fragment.py @@ -1,8 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Literal from dataclasses import dataclass -from elaina_segment import Segment, Quoted, UnmatchedQuoted +from typing import TYPE_CHECKING, Any, Literal + +from elaina_segment import Quoted, Segment, UnmatchedQuoted from arclet.alconna._dcls import safe_dcls_kw @@ -35,7 +36,7 @@ def apply_msgspec(self): t = self.type.value - from msgspec import convert, ValidationError + from msgspec import ValidationError, convert def _validate(v: Segment): if not isinstance(v, (str, Quoted, UnmatchedQuoted)): diff --git a/src/arclet/alconna/sistana/model/capture.py b/src/arclet/alconna/sistana/model/capture.py index 0fbce9a0..0877be0b 100644 --- a/src/arclet/alconna/sistana/model/capture.py +++ b/src/arclet/alconna/sistana/model/capture.py @@ -3,16 +3,15 @@ import re from dataclasses import dataclass from typing import Any, Generic, Tuple, TypeVar, Union +from typing_extensions import TypeAlias from elaina_segment import Quoted, UnmatchedQuoted -from typing_extensions import TypeAlias +from elaina_segment.buffer import AheadToken, Buffer, SegmentToken from arclet.alconna._dcls import safe_dcls_kw -from ..some import Some, Value - -from elaina_segment.buffer import AheadToken, Buffer, SegmentToken from ..err import RegexMismatch, UnexpectedType +from ..some import Some, Value T = TypeVar("T") diff --git a/src/arclet/alconna/sistana/model/snapshot.py b/src/arclet/alconna/sistana/model/snapshot.py index e376f928..08abdb42 100644 --- a/src/arclet/alconna/sistana/model/snapshot.py +++ b/src/arclet/alconna/sistana/model/snapshot.py @@ -3,10 +3,10 @@ from enum import Enum from typing import TYPE_CHECKING -from .mix import Mix - from tarina.trie import Trie +from .mix import Mix + if TYPE_CHECKING: from .pattern import OptionPattern, SubcommandPattern diff --git a/src/arclet/alconna/sistana/some.py b/src/arclet/alconna/sistana/some.py index e44e74ab..87e7553f 100644 --- a/src/arclet/alconna/sistana/some.py +++ b/src/arclet/alconna/sistana/some.py @@ -3,7 +3,6 @@ from dataclasses import dataclass from typing import Generic, TypeVar, Union - T = TypeVar("T") E = TypeVar("E", bound=BaseException) diff --git a/src/arclet/alconna/typing.py b/src/arclet/alconna/typing.py index 1eee9a6f..89b21d63 100644 --- a/src/arclet/alconna/typing.py +++ b/src/arclet/alconna/typing.py @@ -1,13 +1,12 @@ """Alconna 参数相关""" from __future__ import annotations -import re import inspect +import re from dataclasses import dataclass, field, fields, is_dataclass from typing import ( Any, Callable, - cast, Dict, Generic, Iterator, @@ -18,13 +17,15 @@ TypedDict, TypeVar, Union, + cast, final, overload, runtime_checkable, ) from typing_extensions import NotRequired, TypeAlias + +from nepattern import BasePattern, MatchFailed, MatchMode, parser from tarina import generic_isinstance, lang -from nepattern import BasePattern, MatchMode, parser, MatchFailed DataUnit = TypeVar("DataUnit", covariant=True) diff --git a/tests/args_test.py b/tests/args_test.py index 5d41a7e4..cfac4df5 100644 --- a/tests/args_test.py +++ b/tests/args_test.py @@ -1,6 +1,6 @@ from typing import Union -from nepattern import BasePattern, MatchMode, INTEGER, combine +from nepattern import INTEGER, BasePattern, MatchMode, combine from arclet.alconna import ArgFlag, Args, KeyWordVar, Kw, Nargs, StrMulti from devtool import analyse_args diff --git a/tests/components_test.py b/tests/components_test.py index 5d24d76e..cdf25760 100644 --- a/tests/components_test.py +++ b/tests/components_test.py @@ -1,5 +1,5 @@ from arclet.alconna import Alconna, Args, Arparma, ArparmaBehavior, Option, Subcommand -from arclet.alconna.builtin import set_default, conflict +from arclet.alconna.builtin import conflict, set_default from arclet.alconna.model import OptionResult from arclet.alconna.output import output_manager diff --git a/tests/core_test.py b/tests/core_test.py index c4e2cc37..b21db66c 100644 --- a/tests/core_test.py +++ b/tests/core_test.py @@ -422,7 +422,7 @@ def test_fuzzy(): def test_shortcut(): - from arclet.alconna import output_manager, namespace + from arclet.alconna import namespace, output_manager with namespace("test16") as ns: ns.disable_builtin_options = set() @@ -522,9 +522,6 @@ def wrapper(slot, content): alc16_8 = Alconna("core16_8", Args["bar", str]) res11 = alc16_8.parse("core16_8 1234") assert res11.bar == "1234" - alc16_8.parse("core16_8 --shortcut test _") - res12 = alc16_8.parse("test") - assert res12.bar == "1234" alc16_9 = Alconna("core16_9", Args["bar", str]) alc16_9.shortcut("test(.+)?", command="core16_9 {0}") diff --git a/tests/devtool.py b/tests/devtool.py index 230f9b2a..4b2a4087 100644 --- a/tests/devtool.py +++ b/tests/devtool.py @@ -5,15 +5,15 @@ from typing import Any, Literal from arclet.alconna._internal._analyser import Analyser, default_compiler -from arclet.alconna._internal._handlers import analyse_args as ala from arclet.alconna._internal._handlers import HEAD_HANDLES +from arclet.alconna._internal._handlers import analyse_args as ala from arclet.alconna._internal._handlers import analyse_option as alo from arclet.alconna._internal._header import Header from arclet.alconna.args import Args from arclet.alconna.argv import Argv from arclet.alconna.base import Option, Subcommand from arclet.alconna.config import Namespace -from arclet.alconna.typing import DataCollection, CommandMeta +from arclet.alconna.typing import CommandMeta, DataCollection class AnalyseError(Exception): diff --git a/tests/sistana/asserts.py b/tests/sistana/asserts.py index de814f64..e5d2bbeb 100644 --- a/tests/sistana/asserts.py +++ b/tests/sistana/asserts.py @@ -1,9 +1,10 @@ from __future__ import annotations + from contextlib import suppress from dataclasses import dataclass import pytest -from elaina_segment import Buffer, AheadToken, SegmentToken +from elaina_segment import AheadToken, Buffer, SegmentToken from elaina_segment.err import OutOfData from arclet.alconna.sistana.analyzer import Analyzer, LoopflowExitReason diff --git a/tests/sistana/test_pattern.py b/tests/sistana/test_pattern.py index 10f2b429..c220cf18 100644 --- a/tests/sistana/test_pattern.py +++ b/tests/sistana/test_pattern.py @@ -1,7 +1,7 @@ from __future__ import annotations -from elaina_segment import Buffer import pytest +from elaina_segment import Buffer from arclet.alconna.sistana import Fragment, SubcommandPattern from arclet.alconna.sistana.analyzer import LoopflowExitReason