Skip to content

Commit

Permalink
✨ version 1.8.6
Browse files Browse the repository at this point in the history
shortcut in help text
  • Loading branch information
RF-Tar-Railt committed Mar 15, 2024
1 parent 0cf01d1 commit 4d23181
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 38 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# 更新日志

## Alconna 1.8.6

### 改进

- 帮助信息现在会显示命令的快捷指令

### 新增

- `Alconna.shortcut` 新增参数 `humanized`,用于指定快捷指令的可读性文本以供显示

```python
from arclet.alconna import Alconna, Args

alc = Alconna("test", Args["foo", int])
alc.shortcut("t(\d+)", command="test {0}", humanized="t[数字]")
```

- `CommandMeta` 新增属性 `hide_shortcut`, 用于在帮助信息里隐藏命令的快捷指令

## Alconna 1.8.5

### 修复
Expand Down
2 changes: 1 addition & 1 deletion src/arclet/alconna/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
from .typing import UnpackVar as UnpackVar
from .typing import Up as Up

__version__ = "1.8.5"
__version__ = "1.8.6"

# backward compatibility
AnyOne = ANY
9 changes: 8 additions & 1 deletion src/arclet/alconna/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,16 @@ def get_shortcuts(self) -> list[str]:
shortcuts = command_manager.get_shortcut(self)
for key, short in shortcuts.items():
if isinstance(short, InnerShortcutArgs):
result.append(key + (" ...args" if short.fuzzy else ""))
prefixes = f"[{'│'.join(short.prefixes)}]" if short.prefixes else ""
result.append(prefixes + key + (" ...args" if short.fuzzy else ""))
else:
result.append(key)
return result

def _get_shortcuts(self):
"""返回该命令注册的快捷命令"""
return command_manager.get_shortcut(self)

@overload
def shortcut(self, key: str, args: ShortcutArgs | None = None) -> str:
"""操作快捷命令
Expand All @@ -241,6 +246,7 @@ def shortcut(
fuzzy: bool = True,
prefix: bool = False,
wrapper: ShortcutRegWrapper | None = None,
humanized: str | None = None,
) -> str:
"""操作快捷命令
Expand All @@ -251,6 +257,7 @@ def shortcut(
fuzzy (bool, optional): 是否允许命令后随参数, 默认为 `True`
prefix (bool, optional): 是否调用时保留指令前缀, 默认为 `False`
wrapper (ShortcutRegWrapper, optional): 快捷指令的正则匹配结果的额外处理函数, 默认为 `None`
humanized (str, optional): 快捷指令的人类可读描述, 默认为 `None`
Returns:
str: 操作结果
Expand Down
63 changes: 45 additions & 18 deletions src/arclet/alconna/formatter.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, TypedDict
from weakref import WeakKeyDictionary

from nepattern import ANY, AnyString
from tarina import Empty, lang

from .args import Arg, Args
from .base import Option, Subcommand
from .typing import AllParam
from .typing import AllParam, InnerShortcutArgs

if TYPE_CHECKING:
from .core import Alconna
Expand Down Expand Up @@ -61,17 +61,25 @@ def ensure_node(targets: list[str], options: list[Option | Subcommand]):
return ensure_node(targets, options)


class TraceHead(TypedDict):
name: str
description: str
usage: str | None
example: str | None


@dataclass(eq=True)
class Trace:
"""存放命令节点数据的结构
该结构用于存放命令节点的数据,包括命令节点的头部、参数、分隔符和主体。
"""

head: dict[str, Any]
head: TraceHead
args: Args
separators: tuple[str, ...]
body: list[Option | Subcommand]
shortcuts: dict[str, Any]


class TextFormatter:
Expand All @@ -81,7 +89,7 @@ class TextFormatter:
"""

def __init__(self):
self.data = WeakKeyDictionary()
self.data: "WeakKeyDictionary[Alconna, Trace]" = WeakKeyDictionary()
self.ignore_names = set()

def add(self, base: Alconna):
Expand All @@ -92,18 +100,24 @@ def add(self, base: Alconna):
res = Trace(
{
"name": base.header_display,
"prefix": [],
"description": base.meta.description,
"usage": base.meta.usage,
"example": base.meta.example,
},
base.args,
base.separators,
base.options.copy(),
{} if base.meta.hide_shortcut else base._get_shortcuts(),
)
self.data[base] = res
return self

def update_shortcut(self, base: Alconna):
"""更新目标命令的快捷指令"""
if not base.meta.hide_shortcut:
self.data[base].shortcuts = base._get_shortcuts()
return self

def remove(self, base: Alconna):
"""移除目标命令"""
self.data.pop(base)
Expand Down Expand Up @@ -138,15 +152,15 @@ def _handle(trace: Trace):
_opts.append(i)
_visited.add(i)
return self.format(
Trace({"name": _parts[-1], 'prefix': [], 'description': _parts[-1]}, Args(), trace.separators, _opts) # noqa: E501
Trace({"name": _parts[-1], 'description': _parts[-1], 'example': None, 'usage': None}, Args(), trace.separators, _opts, {}) # noqa: E501
)
if isinstance(_cache, Option):
return self.format(
Trace({"name": "│".join(_cache.aliases), "prefix": [], "description": _cache.help_text}, _cache.args, _cache.separators, []) # noqa: E501
Trace({"name": "│".join(_cache.aliases), "description": _cache.help_text, 'example': None, 'usage': None}, _cache.args, _cache.separators, [], {}) # noqa: E501
)
if isinstance(_cache, Subcommand):
return self.format(
Trace({"name": "│".join(_cache.aliases), "prefix": [], "description": _cache.help_text}, _cache.args, _cache.separators, _cache.options) # noqa: E501
Trace({"name": "│".join(_cache.aliases), "description": _cache.help_text, 'example': None, 'usage': None}, _cache.args, _cache.separators, _cache.options, {}) # noqa: E501
)
return self.format(trace)

Expand All @@ -158,16 +172,19 @@ def format(self, trace: Trace) -> str:
Args:
trace (Trace): 命令节点数据
"""
title, desc, usage, example = self.header(trace.head, trace.separators)
title, desc, usage, example = self.header(trace.head)
param = self.parameters(trace.args)
body = self.body(trace.body)
res = f"{title} {param}\n{desc}"
res = f"{title}{trace.separators[0]}{param}\n{desc}"
shortcuts = self.shortcut(trace.shortcuts)
if usage:
res += f"\n{usage}"
if body:
res += f"\n\n{body}"
if example:
res += f"\n{example}"
if shortcuts:
res += f"\n{shortcuts}"
return res

def param(self, parameter: Arg) -> str:
Expand Down Expand Up @@ -212,20 +229,16 @@ def parameters(self, args: Args) -> str:
else res
)

def header(self, root: dict[str, Any], separators: tuple[str, ...]) -> tuple[str, str, str, str]:
def header(self, root: TraceHead) -> tuple[str, str, str, str]:
"""头部节点的描述
Args:
root (dict[str, Any]): 头部节点数据
separators (tuple[str, ...]): 分隔符
root (TraceHead): 头部节点数据
"""
help_string = f"{desc}" if (desc := root.get("description")) else ""
help_string = f"{desc}" if (desc := root["description"]) else ""
usage = f"{lang.require('format', 'usage')}:\n{usage}" if (usage := root.get("usage")) else ""
example = f"{lang.require('format', 'example')}:\n{example}" if (example := root.get("example")) else ""
prefixs = f"[{''.join(map(str, prefixs))}]" if (prefixs := root.get("prefix", [])) != [] else ""
cmd = f"{prefixs}{root.get('name', '')}"
command_string = cmd or (root["name"] + separators[0])
return command_string, help_string, usage, example
return root["name"], help_string, usage, example

def opt(self, node: Option) -> str:
"""对单个选项的描述"""
Expand Down Expand Up @@ -263,5 +276,19 @@ def body(self, parts: list[Option | Subcommand]) -> str:
subcommand_help = f"{lang.require('format', 'subcommands')}:\n" if subcommand_string else ""
return f"{subcommand_help}{subcommand_string}{option_help}{option_string}"

def shortcut(self, shortcuts: dict[str, Any]) -> str:
"""快捷指令的描述"""
if not shortcuts:
return ""
result = []
for key, short in shortcuts.items():
if isinstance(short, InnerShortcutArgs):
_key = key + (" ...args" if short.fuzzy else "")
prefixes = f"[{'│'.join(short.prefixes)}]" if short.prefixes else ""
result.append(f"'{prefixes}{_key}' => {prefixes}{short.command} {' '.join(short.args)}")
else:
result.append(f"'{key}' => {short.origin!r}")
return f"{lang.require('format', 'shortcuts')}:\n" + "\n".join(result)


__all__ = ["TextFormatter", "Trace"]
1 change: 1 addition & 0 deletions src/arclet/alconna/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"notice": "Notice",
"usage": "Usage",
"example": "Example",
"shortcuts": "Shortcut Commands",
"subcommands": "Commands",
"subcommands.subs": "Commands in above command",
"subcommands.opts": "Options in above command",
Expand Down
1 change: 1 addition & 0 deletions src/arclet/alconna/i18n/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
"notice": "注释",
"usage": "用法",
"example": "使用示例",
"shortcuts": "快捷命令",
"subcommands": "可用的子命令有",
"subcommands.subs": "该子命令内可用的子命令有",
"subcommands.opts": "该子命令内可用的选项有",
Expand Down
39 changes: 26 additions & 13 deletions src/arclet/alconna/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import weakref
from copy import copy
from datetime import datetime
from typing import TYPE_CHECKING, Any, Match, MutableSet, Union, Callable
from typing import TYPE_CHECKING, Any, Match, MutableSet, Union
from weakref import WeakKeyDictionary, WeakValueDictionary

from tarina import LRU, lang
Expand Down Expand Up @@ -40,7 +40,7 @@ class CommandManager:
__argv: WeakKeyDictionary[Alconna, Argv]
__abandons: list[Alconna]
__record: LRU[int, Arparma]
__shortcuts: dict[str, dict[str, Union[Arparma, InnerShortcutArgs]]]
__shortcuts: dict[str, tuple[dict[str, Union[Arparma, InnerShortcutArgs]], dict[str, Union[Arparma, InnerShortcutArgs]]]]

def __init__(self):
self.cache_path = f"{__file__.replace('manager.py', '')}manager_cache.db"
Expand Down Expand Up @@ -206,26 +206,35 @@ def add_shortcut(self, target: Alconna, key: str, source: Arparma | ShortcutArgs
"""
namespace, name = self._command_part(target.path)
argv = self.resolve(target)
_shortcut = self.__shortcuts.setdefault(f"{namespace}.{name}", {})
_shortcut = self.__shortcuts.setdefault(f"{namespace}.{name}", ({}, {}))
if isinstance(source, dict):
humanize = source.pop("humanized", None)
if source.get("prefix", False) and target.prefixes:
prefixes = []
out = []
for prefix in target.prefixes:
if not isinstance(prefix, str):
continue
_shortcut[f"{re.escape(prefix)}{key}"] = InnerShortcutArgs(
prefixes.append(prefix)
_shortcut[1][f"{re.escape(prefix)}{key}"] = InnerShortcutArgs(
**{**source, "command": argv.converter(prefix + 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": prefixes}
)
target.formatter.update_shortcut(target)
return "\n".join(out)
_shortcut[key] = InnerShortcutArgs(
_shortcut[0][humanize or key] = _shortcut[1][key] = InnerShortcutArgs(
**{**source, "command": argv.converter(source.get("command", str(target.command)))}
)
target.formatter.update_shortcut(target)
return lang.require("shortcut", "add_success").format(shortcut=key, target=target.path)
elif source.matched:
_shortcut[key] = source
_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}"))
Expand All @@ -242,7 +251,10 @@ def get_shortcut(self, target: Alconna[TDC]) -> dict[str, Union[Arparma[TDC], In
namespace, name = self._command_part(target.path)
if target not in self.__analysers:
raise ValueError(lang.require("manager", "undefined_command").format(target=f"{namespace}.{name}"))
return self.__shortcuts.get(f"{namespace}.{name}", {})
shortcuts = self.__shortcuts.get(f"{namespace}.{name}", {})
if not shortcuts:
return {}
return shortcuts[0]

def find_shortcut(
self, target: Alconna[TDC], data: list
Expand All @@ -261,15 +273,15 @@ def find_shortcut(
raise ValueError(lang.require("manager", "undefined_command").format(target=f"{namespace}.{name}"))
query: str = data.pop(0)
while True:
if query in _shortcut:
return data, _shortcut[query], None
for key, args in _shortcut.items():
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)):
if len(query) > mat.span()[1]:
data.insert(0, query[mat.span()[1]:])
return data, args, mat
elif mat := re.fullmatch(key, query):
return data, _shortcut[key], mat
return data, _shortcut[1][key], mat
if not data:
break
next_data = data.pop(0)
Expand All @@ -287,7 +299,8 @@ def delete_shortcut(self, target: Alconna, key: str | None = None):
raise ValueError(lang.require("manager", "undefined_command").format(target=f"{namespace}.{name}"))
if key:
try:
del _shortcut[key]
_shortcut[0].pop(key, None)
del _shortcut[1][key]
return lang.require("shortcut", "delete_success").format(shortcut=key, target=target.path)
except KeyError as e:
raise ValueError(
Expand Down Expand Up @@ -438,7 +451,7 @@ def __repr__(self):
+ "Commands:\n"
+ f"[{', '.join([cmd.path for cmd in self.get_commands()])}]"
+ "\nShortcuts:\n"
+ "\n".join([f" {k} => {v}" for short in self.__shortcuts.values() for k, v in short.items()])
+ "\n".join([f" {k} => {v}" for short in self.__shortcuts.values() for k, v in short[0].items()])
+ "\nRecords:\n"
+ "\n".join([f" [{k}]: {v[1].origin}" for k, v in enumerate(self.__record.items()[:20])])
+ "\nDisabled Commands:\n"
Expand Down
4 changes: 2 additions & 2 deletions src/arclet/alconna/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
@dataclass(init=False, eq=True)
class Sentence:
__slots__ = ("name",)
__str__ = lambda self: self.name
__repr__ = lambda self: self.name
__str__ = lambda self: self.name # type: ignore
__repr__ = lambda self: self.name # type: ignore

def __init__(self, name):
self.name = name
Expand Down
Loading

0 comments on commit 4d23181

Please sign in to comment.