Skip to content

Commit

Permalink
✨ version 1.7.32
Browse files Browse the repository at this point in the history
  • Loading branch information
RF-Tar-Railt committed Oct 22, 2023
1 parent 804a874 commit 7e425e2
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 32 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# 更新日志

## Alconna 1.7.32

### 改进
- 可以选择配置禁用哪些内置选项
- 自定义的与内置选项名称有冲突的选项 (例如 --help) 在禁用内置选项后能正常解析

## Alconna 1.7.31

### 改进
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 @@ -49,7 +49,7 @@
from .typing import UnpackVar as UnpackVar
from .typing import Up as Up

__version__ = "1.7.31"
__version__ = "1.7.32"

# backward compatibility
Arpamar = Arparma
Expand Down
8 changes: 4 additions & 4 deletions src/arclet/alconna/_internal/_analyser.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from ..action import Action
from ..args import Args
from ..arparma import Arparma
from ..base import Option, Subcommand
from ..base import Option, Subcommand, Help, Shortcut, Completion
from ..completion import comp_ctx
from ..config import config
from ..exceptions import ArgumentMissing, FuzzyMatchSuccess, ParamsUnmatched, PauseTriggered, SpecialOptionTriggered
Expand Down Expand Up @@ -71,7 +71,7 @@ def default_compiler(analyser: SubAnalyser, pids: set[str]):
pids (set[str]): 节点名集合
"""
for opts in analyser.command.options:
if isinstance(opts, Option):
if isinstance(opts, Option) and not isinstance(opts, (Help, Shortcut, Completion)):
if opts.compact or opts.action.type == 2 or not set(analyser.command.separators).issuperset(opts.separators): # noqa: E501
analyser.compact_params.append(opts)
_compile_opts(opts, analyser.compile_params) # type: ignore
Expand Down Expand Up @@ -376,10 +376,10 @@ def analyse(self, argv: Argv[TDC]) -> Arparma[TDC] | None:
return _SPECIAL[sot.args[0]](self, argv)
except (ParamsUnmatched, ArgumentMissing) as e1:
if (rest := argv.release()) and isinstance(rest[-1], str):
if rest[-1] in argv.completion_names:
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()
return handle_completion(self, argv)
if handler := argv.special.get(rest[-1]):
if (handler := argv.special.get(rest[-1])) and handler not in argv.namespace.disable_builtin_options:
return _SPECIAL[handler](self, argv)
if comp_ctx.get(None):
if isinstance(e1, ParamsUnmatched):
Expand Down
14 changes: 7 additions & 7 deletions src/arclet/alconna/_internal/_argv.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from dataclasses import InitVar, dataclass, field
from dataclasses import dataclass, field
from typing import Any, Callable, ClassVar, Generic, Iterable
from typing_extensions import Self

Expand All @@ -17,7 +17,7 @@
class Argv(Generic[TDC]):
"""命令行参数"""

namespace: InitVar[Namespace] = field(default=config.default_namespace)
namespace: Namespace = field(default=config.default_namespace)
fuzzy_match: bool = field(default=False)
"""当前命令是否模糊匹配"""
preprocessors: dict[type, Callable[..., Any]] = field(default_factory=dict)
Expand Down Expand Up @@ -57,15 +57,15 @@ class Argv(Generic[TDC]):

_cache: ClassVar[dict[type, dict[str, Any]]] = {}

def __post_init__(self, namespace: Namespace):
def __post_init__(self):
self.reset()
self.special: dict[str, str] = {}
self.special.update(
[(i, "help") for i in namespace.builtin_option_name["help"]]
+ [(i, "completion") for i in namespace.builtin_option_name["completion"]]
+ [(i, "shortcut") for i in namespace.builtin_option_name["shortcut"]]
[(i, "help") for i in self.namespace.builtin_option_name["help"]]
+ [(i, "completion") for i in self.namespace.builtin_option_name["completion"]]
+ [(i, "shortcut") for i in self.namespace.builtin_option_name["shortcut"]]
)
self.completion_names = namespace.builtin_option_name["completion"]
self.completion_names = self.namespace.builtin_option_name["completion"]
if __cache := self.__class__._cache.get(self.__class__, {}):
self.preprocessors.update(__cache.get("preprocessors") or {})
self.filter_out.extend(__cache.get("filter_out") or [])
Expand Down
19 changes: 12 additions & 7 deletions src/arclet/alconna/_internal/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ def step_varpos(argv: Argv, args: Args, result: dict[str, Any]):
for _ in range(loop):
may_arg, _str = argv.next(arg.separators)
if _str and may_arg in argv.special:
raise SpecialOptionTriggered(argv.special[may_arg])
if argv.special[may_arg] not in argv.namespace.disable_builtin_options:
raise SpecialOptionTriggered(argv.special[may_arg])
if not may_arg or (_str and may_arg in argv.param_ids):
argv.rollback(may_arg)
break
Expand Down Expand Up @@ -93,7 +94,8 @@ def step_varkey(argv: Argv, args: Args, result: dict[str, Any]):
for _ in range(loop):
may_arg, _str = argv.next(arg.separators)
if _str and may_arg in argv.special:
raise SpecialOptionTriggered(argv.special[may_arg])
if argv.special[may_arg] not in argv.namespace.disable_builtin_options:
raise SpecialOptionTriggered(argv.special[may_arg])
if not may_arg or (_str and may_arg in argv.param_ids) or not _str:
argv.rollback(may_arg)
break
Expand Down Expand Up @@ -127,7 +129,8 @@ def step_keyword(argv: Argv, args: Args, result: dict[str, Any]):
while count < target:
may_arg, _str = argv.next(tuple(kwonly_seps))
if _str and may_arg in argv.special:
raise SpecialOptionTriggered(argv.special[may_arg])
if argv.special[may_arg] not in argv.namespace.disable_builtin_options:
raise SpecialOptionTriggered(argv.special[may_arg])
if not may_arg or not _str:
argv.rollback(may_arg)
break
Expand Down Expand Up @@ -182,7 +185,8 @@ def analyse_args(argv: Argv, args: Args) -> dict[str, Any]:
argv.context = arg
may_arg, _str = argv.next(arg.separators)
if _str and may_arg in argv.special:
raise SpecialOptionTriggered(argv.special[may_arg])
if argv.special[may_arg] not in argv.namespace.disable_builtin_options:
raise SpecialOptionTriggered(argv.special[may_arg])
if _str and may_arg in argv.param_ids and arg.optional:
if (de := arg.field.default) is not None:
result[arg.name] = None if de is Empty else de
Expand Down Expand Up @@ -353,9 +357,10 @@ def analyse_param(analyser: SubAnalyser, argv: Argv, seps: tuple[str, ...] | Non
"""
_text, _str = argv.next(seps, move=False)
if _str and _text in argv.special:
if _text in argv.completion_names:
argv.bak_data[argv.current_index] = argv.bak_data[argv.current_index].replace(_text, "")
raise SpecialOptionTriggered(argv.special[_text])
if argv.special[_text] not in argv.namespace.disable_builtin_options:
if _text in argv.completion_names:
argv.bak_data[argv.current_index] = argv.bak_data[argv.current_index].replace(_text, "")
raise SpecialOptionTriggered(argv.special[_text])
if not _str or not _text:
_param = None
elif _text in analyser.compile_params:
Expand Down
14 changes: 13 additions & 1 deletion src/arclet/alconna/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,4 +345,16 @@ def add(self, opt: Option | Subcommand) -> Self:
return self


__all__ = ["CommandNode", "Option", "Subcommand"]
class Help(Option):
def _calc_hash(self):
return hash("$ALCONNA_BUILTIN_OPTION_HELP")


class Shortcut(Option):
def _calc_hash(self):
return hash("$ALCONNA_BUILTIN_OPTION_SHORTCUT")


class Completion(Option):
def _calc_hash(self):
return hash("$ALCONNA_BUILTIN_OPTION_COMPLETION")
3 changes: 2 additions & 1 deletion src/arclet/alconna/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from dataclasses import dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, ContextManager, TypedDict
from typing import TYPE_CHECKING, Any, Callable, ContextManager, Literal, TypedDict

from tarina import lang

Expand Down Expand Up @@ -39,6 +39,7 @@ class Namespace:
"""默认是否抛出异常"""
enable_message_cache: bool = field(default=True)
"""默认是否启用消息缓存"""
disable_builtin_options: set[Literal["help", "shortcut", "completion"]] = field(default_factory=set)
builtin_option_name: OptionNames = field(
default_factory=lambda: {
"help": {"--help", "-h"},
Expand Down
21 changes: 12 additions & 9 deletions src/arclet/alconna/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from ._internal._analyser import Analyser, TCompile
from .args import Arg, Args
from .arparma import Arparma, ArparmaBehavior, requirement_handler
from .base import Option, Subcommand
from .base import Option, Subcommand, Help, Shortcut, Completion
from .config import Namespace, config
from .duplication import Duplication
from .exceptions import ExecuteFailed, NullMessage
Expand All @@ -37,15 +37,18 @@ def handle_argv():


def add_builtin_options(options: list[Option | Subcommand], ns: Namespace) -> None:
options.append(Option("|".join(ns.builtin_option_name["help"]), help_text=lang.require("builtin", "option_help"))) # noqa: E501
options.append(
Option(
"|".join(ns.builtin_option_name["shortcut"]),
Args["action?", "delete|list"]["name?", str]["command", str, "$"],
help_text=lang.require("builtin", "option_shortcut"),
if "help" not in ns.disable_builtin_options:
options.append(Help("|".join(ns.builtin_option_name["help"]), help_text=lang.require("builtin", "option_help"))) # noqa: E501
if "shortcut" not in ns.disable_builtin_options:
options.append(
Shortcut(
"|".join(ns.builtin_option_name["shortcut"]),
Args["action?", "delete|list"]["name?", str]["command", str, "$"],
help_text=lang.require("builtin", "option_shortcut"),
)
)
)
options.append(Option("|".join(ns.builtin_option_name["completion"]), help_text=lang.require("builtin", "option_completion"))) # noqa: E501
if "completion" not in ns.disable_builtin_options:
options.append(Completion("|".join(ns.builtin_option_name["completion"]), help_text=lang.require("builtin", "option_completion"))) # noqa: E501


@dataclass(init=True, unsafe_hash=True)
Expand Down
28 changes: 26 additions & 2 deletions tests/core_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
MultiVar,
Option,
Subcommand,
namespace,
)


Expand Down Expand Up @@ -417,7 +418,7 @@ def test_shortcut():
alc16 = Alconna("core16", Args["foo", int], Option("bar", Args["baz", str]))
assert alc16.parse("core16 123 bar abcd").matched is True
# 构造体缩写传入;{i} 将被可能的正则匹配替换
alc16.shortcut("TEST(\d+)(.+)", {"args": ["{0}", "bar {1}"]})
alc16.shortcut(r"TEST(\d+)(.+)", {"args": ["{0}", "bar {1}"]})
res = alc16.parse("TEST123aa")
assert res.matched is True
assert res.foo == 123
Expand All @@ -431,7 +432,7 @@ def test_shortcut():
res2 = alc16.parse("TEST3 442")
assert res2.foo == 442
# 指令缩写也支持正则
alc16.parse("core16 --shortcut TESTa4(\d+) 'core16 {0}'")
alc16.parse(r"core16 --shortcut TESTa4(\d+) 'core16 {0}'")
res3 = alc16.parse("TESTa4257")
assert res3.foo == 257
alc16.parse("core16 --shortcut TESTac 'core16 2{%0}'")
Expand Down Expand Up @@ -851,5 +852,28 @@ def test_tips():
assert str(core27.parse("core27").error_info) == "参数 arg1 丢失"


def test_disable_builtin_option():
with namespace("test"):
core28 = Alconna("core28")
core28_1 = Alconna("core28_1", Args["text", MultiVar(str)])
core28.namespace_config.disable_builtin_options.add("shortcut")

res = core28.parse("core28 --shortcut 123 test")
assert not res.matched
assert str(res.error_info) == "参数 --shortcut 匹配失败"

res1 = core28_1.parse("core28_1 --shortcut 123 test")
assert res1.matched
assert res1.query("text") == ("--shortcut", "123", "test")

with namespace("test1") as ns:
ns.disable_builtin_options.add("help")
core28_2 = Alconna("core28_2", Option("--help"))

res2 = core28_2.parse("core28_2 --help")
assert res2.matched
assert res2.find("help")


if __name__ == "__main__":
pytest.main([__file__, "-vs"])

0 comments on commit 7e425e2

Please sign in to comment.