Skip to content

Commit

Permalink
✨ support shortcut key include separator
Browse files Browse the repository at this point in the history
  • Loading branch information
RF-Tar-Railt committed Feb 10, 2024
1 parent 0f8407c commit 46a257a
Show file tree
Hide file tree
Showing 9 changed files with 59 additions and 49 deletions.
18 changes: 7 additions & 11 deletions src/arclet/alconna/_internal/_analyser.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
from ..arparma import Arparma
from ..base import Completion, Help, Option, Shortcut, Subcommand
from ..completion import comp_ctx
from ..config import config
from ..exceptions import ArgumentMissing, FuzzyMatchSuccess, InvalidParam, ParamsUnmatched, PauseTriggered, SpecialOptionTriggered
from ..manager import command_manager
from ..model import HeadResult, OptionResult, Sentence, SubcommandResult
Expand Down Expand Up @@ -286,10 +285,13 @@ def shortcut(
if self.command.meta.raise_exception:
raise exc
return self.export(argv, True, exc)
# if short.fuzzy and reg and len(trigger) > reg.span()[1]:
# argv.addon((trigger[reg.span()[1] :],))
argv.addon(short.args)
data = _handle_shortcut_data(argv, data)
if not data and argv.raw_data and any(isinstance(i, str) and "{%0}" in i for i in argv.raw_data):
exc = ArgumentMissing(lang.require("analyser", "param_missing"))
if self.command.meta.raise_exception:
raise exc
return self.export(argv, True, exc)
argv.bak_data = argv.raw_data.copy()
argv.addon(data)
if reg:
Expand Down Expand Up @@ -323,22 +325,16 @@ def process(self, argv: Argv[TDC]) -> Arparma[TDC]:
if self.command.meta.raise_exception:
raise e
return self.export(argv, True, e)
text = argv.separators[0].join([_next] + argv.release())
try:
short, mat = command_manager.find_shortcut(self.command, text)
rest, short, mat = command_manager.find_shortcut(self.command, [_next] + argv.release())
except ValueError as exc:
if self.command.meta.raise_exception:
raise e from exc
return self.export(argv, True, e)
else:
if mat and len(text) > mat.span()[1]:
data = text[mat.span()[1]:].lstrip(argv.separators[0]).split(argv.separators[0])
else:
data = []
# data = argv.release()
self.reset()
argv.reset()
return self.shortcut(argv, data, short, mat)
return self.shortcut(argv, rest, short, mat)

except FuzzyMatchSuccess as Fuzzy:
output_manager.send(self.command.name, lambda: str(Fuzzy))
Expand Down
2 changes: 1 addition & 1 deletion src/arclet/alconna/_internal/_argv.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def release(self, separate: tuple[str, ...] | None = None, recover: bool = False
list[str | Any]: 剩余的数据.
"""
_result = []
data = self.bak_data if recover else self.raw_data[self.current_index :]
data = self.bak_data if recover else self.raw_data[self.current_index:]
for _data in data:
if _data.__class__ is str:
_result.extend(split(_data, separate or (" ",), self.filter_crlf))
Expand Down
8 changes: 4 additions & 4 deletions src/arclet/alconna/_internal/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,14 +455,14 @@ def analyse_header(header: Header, argv: Argv) -> HeadResult:
elif content.__class__ is TPattern and (mat := content.fullmatch(head_text)):
return HeadResult(head_text, head_text, True, mat.groupdict(), mapping)
if header.compact and content.__class__ in (set, TPattern) and (mat := header.compact_pattern.match(head_text)):
argv.rollback(head_text[len(mat[0]) :], replace=True)
argv.rollback(head_text[len(mat[0]):], replace=True)
return HeadResult(mat[0], mat[0], True, mat.groupdict(), mapping)
if isinstance(content, BasePattern):
if (val := content.exec(head_text, Empty)).success:
return HeadResult(head_text, val.value, True, fixes=mapping)
if header.compact and (val := header.compact_pattern.exec(head_text, Empty)).success:
if _str:
argv.rollback(head_text[len(str(val.value)) :], replace=True)
argv.rollback(head_text[len(str(val.value)):], replace=True)
return HeadResult(val.value, val.value, True, fixes=mapping)

may_cmd, _m_str = argv.next()
Expand Down Expand Up @@ -537,9 +537,9 @@ def handle_shortcut(analyser: Analyser, argv: Argv):
elif opt_v["command"] == "_":
msg = analyser.command.shortcut(opt_v["name"], None)
elif opt_v["command"] == "$":
msg = analyser.command.shortcut(opt_v["name"], fuzzy=False)
msg = analyser.command.shortcut(opt_v["name"], fuzzy=True)
else:
msg = analyser.command.shortcut(opt_v["name"], fuzzy=False, command=opt_v["command"])
msg = analyser.command.shortcut(opt_v["name"], fuzzy=True, command=opt_v["command"])
output_manager.send(analyser.command.name, lambda: msg)
except Exception as e:
output_manager.send(analyser.command.name, lambda: str(e))
Expand Down
16 changes: 8 additions & 8 deletions src/arclet/alconna/_internal/_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def _match(command: str, pbfn: Callable[..., ...], comp: bool):
if command == self.pattern:
return command, None
if comp and command.startswith(self.pattern):
pbfn(command[len(self.pattern) :], replace=True)
pbfn(command[len(self.pattern):], replace=True)
return self.pattern, None
return None, None

Expand All @@ -70,7 +70,7 @@ def _match(command: str, pbfn: Callable[..., ...], comp: bool):
if mat := self.pattern.fullmatch(command):
return command, mat
if comp and (mat := self.pattern.match(command)):
pbfn(command[len(mat[0]) :], replace=True)
pbfn(command[len(mat[0]):], replace=True)
return mat[0], mat
return None, None

Expand Down Expand Up @@ -150,15 +150,15 @@ def match0(self, pf: Any, cmd: Any, p_str: bool, c_str: bool, pbfn: Callable[...
return (pf, cmd), (pf, val.value), True, None
if comp and (val := self.comp_pattern.exec(cmd, Empty)).success:
if c_str:
pbfn(cmd[len(str(val.value)) :], replace=True)
pbfn(cmd[len(str(val.value)):], replace=True)
return (pf, cmd), (pf, cmd[: len(str(val.value))]), True, None
return
if (val := self.patterns.exec(pf, Empty)).success:
if (val2 := self.command.exec(cmd, Empty)).success:
return (pf, cmd), (val.value, val2.value), True, None
if comp and (val2 := self.comp_pattern.exec(cmd, Empty)).success:
if c_str:
pbfn(cmd[len(str(val2.value)) :], replace=True)
pbfn(cmd[len(str(val2.value)):], replace=True)
return (pf, cmd), (val.value, cmd[: len(str(val2.value))]), True, None
return

Expand All @@ -168,7 +168,7 @@ def match1(self, pf: Any, cmd: Any, p_str: bool, c_str: bool, pbfn: Callable[...
if (val := self.patterns.exec(pf, Empty)).success and (mat := self.command.fullmatch(cmd)):
return (pf, cmd), (val.value, cmd), True, mat.groupdict()
if comp and (mat := self.comp_pattern.match(cmd)):
pbfn(cmd[len(mat[0]) :], replace=True)
pbfn(cmd[len(mat[0]):], replace=True)
return (pf, cmd), (pf, mat[0]), True, mat.groupdict()

def match(self, pf: Any, cmd: Any, p_str: bool, c_str: bool, pbfn: Callable[..., ...], comp: bool):
Expand All @@ -184,21 +184,21 @@ def match(self, pf: Any, cmd: Any, p_str: bool, c_str: bool, pbfn: Callable[...,
return pf, pf, True, mat.groupdict()
if comp and (mat := self.comp_pattern.match(pf)):
pbfn(cmd)
pbfn(pf[len(mat[0]) :], replace=True)
pbfn(pf[len(mat[0]):], replace=True)
return mat[0], mat[0], True, mat.groupdict()
if not c_str:
return
if mat := self.prefix.fullmatch((name := pf + cmd)):
return name, name, True, mat.groupdict()
if comp and (mat := self.comp_pattern.match(name)):
pbfn(name[len(mat[0]) :], replace=True)
pbfn(name[len(mat[0]):], replace=True)
return mat[0], mat[0], True, mat.groupdict()
return
if (val := self.patterns.exec(pf, Empty)).success:
if mat := self.command.fullmatch(cmd):
return (pf, cmd), (val.value, cmd), True, mat.groupdict()
if comp and (mat := self.command.match(cmd)):
pbfn(cmd[len(mat[0]) :], replace=True)
pbfn(cmd[len(mat[0]):], replace=True)
return (pf, cmd), (val.value, mat[0]), True, mat.groupdict()


Expand Down
2 changes: 1 addition & 1 deletion src/arclet/alconna/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ def enter(self, content: list | None = None) -> EnterResult:
if isinstance(self.trigger, InvalidParam):
argv.raw_data = argv.bak_data[: max(self.current_index, 1)]
argv.addon(input_)
argv.raw_data.extend(self.raw_data[max(self.current_index, 1) :])
argv.raw_data.extend(self.raw_data[max(self.current_index, 1):])
else:
argv.raw_data = argv.bak_data.copy()
argv.addon(input_)
Expand Down
2 changes: 2 additions & 0 deletions src/arclet/alconna/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
class ParamsUnmatched(Exception):
"""一个传入参数没有被选项或Args匹配"""


class InvalidParam(Exception):
"""传入参数验证失败"""


class ArgumentMissing(Exception):
"""组件内的 Args 参数未能解析到任何内容"""

Expand Down
1 change: 0 additions & 1 deletion src/arclet/alconna/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ def ensure_node(targets: list[str], options: list[Option | Subcommand]):
return ensure_node(targets, options)



@dataclass(eq=True)
class Trace:
"""存放命令节点数据的结构
Expand Down
39 changes: 24 additions & 15 deletions src/arclet/alconna/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,31 +237,40 @@ def get_shortcut(self, target: Alconna[TDC]) -> dict[str, Union[Arparma[TDC], In
return _shortcut

def find_shortcut(
self, target: Alconna[TDC], query: str
) -> tuple[Arparma[TDC] | InnerShortcutArgs, Match[str] | None]:
self, target: Alconna[TDC], data: list
) -> tuple[list, Arparma[TDC] | InnerShortcutArgs, Match[str] | None]:
"""查找快捷命令
Args:
target (Alconna): 目标命令
query (str): 快捷命令的名称.
target (Alconna): 目标命令对象
data (list): 传入的命令数据
Returns:
tuple[Union[Arparma, InnerShortcutArgs], re.Match[str]]: 返回匹配的快捷命令
tuple[list, Union[Arparma, InnerShortcutArgs], re.Match[str]]: 返回匹配的快捷命令
"""
namespace, name = self._command_part(target.path)
if not (_shortcut := self.__shortcuts.get(f"{namespace}.{name}")):
raise ValueError(lang.require("manager", "undefined_command").format(target=f"{namespace}.{name}"))
try:
return _shortcut[query], None
except KeyError as e:
query: str = data.pop(0)
while True:
if query in _shortcut:
return data, _shortcut[query], None
for key, args in _shortcut.items():
if isinstance(args, InnerShortcutArgs) and args.fuzzy and (mat := re.match(f"^{key}", query)):
return args, mat
if len(query) > mat.span()[1]:
data.insert(0, query[mat.span()[1]:])
return data, args, mat
elif mat := re.fullmatch(key, query):
return _shortcut[key], mat
raise ValueError(
lang.require("manager", "shortcut_parse_error").format(target=f"{namespace}.{name}", query=query)
) from e
return data, _shortcut[key], mat
if not data:
break
next_data = data.pop(0)
if not isinstance(next_data, str):
break
query += f"{target.separators[0]}{next_data}"
raise ValueError(
lang.require("manager", "shortcut_parse_error").format(target=f"{namespace}.{name}", query=query)
)

def delete_shortcut(self, target: Alconna, key: str | None = None):
"""删除快捷命令"""
Expand Down Expand Up @@ -351,10 +360,10 @@ def all_command_help(
command_string = (
"\n".join(
f" {str(index).rjust(len(str(page * max_length)), '0')} {slot[0]} : {slot[1]}"
for index, slot in enumerate(slots[(page - 1) * max_length : page * max_length], start=(page - 1) * max_length) # noqa: E501
for index, slot in enumerate(slots[(page - 1) * max_length: page * max_length], start=(page - 1) * max_length) # noqa: E501
)
if show_index
else "\n".join(f" - {n} : {d}" for n, d in slots[(page - 1) * max_length : page * max_length])
else "\n".join(f" - {n} : {d}" for n, d in slots[(page - 1) * max_length: page * max_length])
)
help_names = set()
for i in cmds:
Expand Down
20 changes: 12 additions & 8 deletions tests/core_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,20 +429,20 @@ def test_shortcut():
# 原始命令
alc16 = Alconna("core16", Args["foo", int], Option("bar", Args["baz", str]))
assert alc16.parse("core16 123 bar abcd").matched is True
# 指令缩写传入, TEST1 -> core16 321
alc16.parse("core16 --shortcut TEST1 'core16 321'")
res1 = alc16.parse("TEST1")
assert res1.foo == 321
# 指令缩写传入的不允许后随参数
alc16.parse("core16 --shortcut TEST2 core16")
res2 = alc16.parse("TEST2 442")
assert not res2.matched
# 构造体缩写传入;{i} 将被可能的正则匹配替换
alc16.shortcut(r"TEST(\d+)(.+)", {"args": ["{0}", "bar {1}"]})
res = alc16.parse("TEST123aa")
assert res.matched is True
assert res.foo == 123
assert res.baz == "aa"
# 指令缩写传入, TEST1 -> core16 321
alc16.parse("core16 --shortcut TEST1 'core16 321'")
res1 = alc16.parse("TEST1")
assert res1.foo == 321
# 指令缩写传入的允许后随参数
alc16.parse("core16 --shortcut TEST2 core16")
res2 = alc16.parse("TEST2 442")
assert res2.foo == 442
# 指令缩写也支持正则
alc16.parse(r"core16 --shortcut TESTa4(\d+) 'core16 {0}'")
res3 = alc16.parse("TESTa4257")
Expand All @@ -462,6 +462,7 @@ def test_shortcut():
assert not res7.matched
res8 = alc16_1.parse("echo \\\\'123\\\\'")
assert res8.content == "print('123')"
assert not alc16_1.parse("echo").matched

alc16_2 = Alconna([1, 2, "3"], "core16_2", Args["foo", bool])
alc16_2.shortcut("test", {"command": [1, "core16_2 True"]}) # type: ignore
Expand Down Expand Up @@ -507,6 +508,9 @@ def wrapper(slot, content):
alc16_6.parse("testhelp")
assert cap["output"] == "core16_6 <bar: str> \nUnknown"

alc16_7 = Alconna("core16_7", Args["bar", str])
alc16_7.shortcut("test 123", {"args": ["abc"]})
assert alc16_7.parse("test 123").bar == "abc"

def test_help():
from arclet.alconna import output_manager
Expand Down

0 comments on commit 46a257a

Please sign in to comment.