Skip to content

Commit

Permalink
🚑 version 1.7.7
Browse files Browse the repository at this point in the history
fix critical bug of `append_value`
  • Loading branch information
RF-Tar-Railt committed May 25, 2023
1 parent 739508a commit 62dcb4d
Show file tree
Hide file tree
Showing 14 changed files with 96 additions and 83 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# 更新日志

# Alconna 1.7.7

### 改进:

- 命令头部可以通过 `\\` 转义 `{}` 为原始字符,而不触发 `bracket header` 解析
- 快捷指令的参数部分可以通过 `\\` 转义 `{}` 为原始字符

### 修复:

- 修复 `append_value` 时列表对象引用错误的 bug

## Alconna 1.7.6

### 修复:
Expand Down
4 changes: 2 additions & 2 deletions devtool.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def analyse_option(option: Option, command: DataCollection[str | Any], raise_exc
_analyser.need_main_args = False
_analyser.raise_exception = True
_analyser.command.options.append(option)
default_compiler(_analyser, _analyser.command.namespace_config, argv.param_ids)
default_compiler(_analyser, argv.param_ids)
_analyser.command.options.clear()
try:
argv.build(command)
Expand All @@ -97,7 +97,7 @@ def analyse_subcommand(subcommand: Subcommand, command: DataCollection[str | Any
_analyser.need_main_args = False
_analyser.raise_exception = True
_analyser.command.options.append(subcommand)
default_compiler(_analyser, _analyser.command.namespace_config, argv.param_ids)
default_compiler(_analyser, argv.param_ids)
_analyser.command.options.clear()
try:
argv.build(command)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ authors = [
]
dependencies = [
"typing-extensions>=4.5.0",
"nepattern<0.6.0, >=0.5.6",
"nepattern<0.6.0, >=0.5.8",
"tarina>=0.3.3",
]
dynamic = ["version"]
Expand Down
19 changes: 8 additions & 11 deletions src/arclet/alconna/_internal/_analyser.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

import re
import traceback
from dataclasses import dataclass, field
from re import Match
from typing import TYPE_CHECKING, Any, Callable, Generic, Set
Expand All @@ -28,7 +27,7 @@
handle_shortcut, prompt
)
from ._header import Header
from ._util import levenshtein
from ._util import levenshtein, escape, unescape

if TYPE_CHECKING:
from ..core import Alconna
Expand Down Expand Up @@ -97,7 +96,6 @@ def default_compiler(analyser: SubAnalyser, pids: set[str]):
@dataclass
class SubAnalyser(Generic[TDC]):
"""子解析器, 用于子命令的解析"""

command: Subcommand
"""子命令"""
default_main_only: bool = field(default=False)
Expand Down Expand Up @@ -294,15 +292,16 @@ def shortcut(
break
if not isinstance(unit, str):
continue
unit = escape(unit)
if unit == f"{{%{data_index}}}":
argv.raw_data[i] = data.pop(0)
data_index += 1
elif f"{{%{data_index}}}" in unit:
argv.raw_data[i] = unit.replace(f"{{%{data_index}}}", str(data.pop(0)))
argv.raw_data[i] = unescape(unit.replace(f"{{%{data_index}}}", str(data.pop(0))))
data_index += 1
elif mat := re.search(r"\{\*(.*)\}", unit, re.DOTALL):
sep = mat[1]
argv.raw_data[i] = unit.replace(f"{{*{sep}}}", (sep or ' ').join(map(str, data)))
argv.raw_data[i] = unescape(unit.replace(f"{{*{sep}}}", (sep or ' ').join(map(str, data))))
data.clear()

argv.bak_data = argv.raw_data.copy()
Expand All @@ -313,11 +312,12 @@ def shortcut(
for j, unit in enumerate(argv.raw_data):
if not isinstance(unit, str):
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] = unit
argv.raw_data[j] = unescape(unit)
if argv.message_cache:
argv.token = argv.generate_token(argv.raw_data)
return self.process(argv)
Expand Down Expand Up @@ -420,10 +420,7 @@ def analyse(self, argv: Argv[TDC]) -> Arparma[TDC] | None:
self.args_result = analyse_args(argv, self.self_args)

def export(
self,
argv: Argv[TDC],
fail: bool = False,
exception: BaseException | None = None,
self, argv: Argv[TDC], fail: bool = False, exception: BaseException | None = None,
) -> Arparma[TDC]:
"""创建 `Arparma` 解析结果, 其一定是一次解析的最后部分
Expand All @@ -434,7 +431,7 @@ def export(
"""
result = Arparma(self.command.path, argv.origin, not fail, self.header_result)
if fail:
result.error_info = exception or repr(traceback.format_exc(limit=1))
result.error_info = exception
result.error_data = argv.release()
else:
if self.default_opt_result:
Expand Down
27 changes: 5 additions & 22 deletions src/arclet/alconna/_internal/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,7 @@ def _handle_keyword(
raise ParamsUnmatched(lang.require("args", "key_missing").format(target=may_arg, key=key))


def _loop_kw(
argv: Argv,
_loop: int,
seps: tuple[str, ...],
value: MultiVar,
default: Any
):
def _loop_kw(argv: Argv, _loop: int, seps: tuple[str, ...], value: MultiVar, default: Any):
"""循环关键字参数"""
result = {}
for _ in range(_loop):
Expand All @@ -103,12 +97,7 @@ def _loop_kw(


def _loop(
argv: Argv,
_loop: int,
seps: tuple[str, ...],
value: MultiVar,
default: Any,
kw: KeyWordVar | None
argv: Argv, _loop: int, seps: tuple[str, ...], value: MultiVar, default: Any, kw: KeyWordVar | None
):
"""循环参数"""
result = []
Expand All @@ -133,12 +122,7 @@ def _loop(
return tuple(result)


def multi_arg_handler(
argv: Argv,
args: Args,
arg: Arg,
result_dict: dict[str, Any],
):
def multi_arg_handler(argv: Argv, args: Args, arg: Arg, result_dict: dict[str, Any],):
"""处理可变参数
Args:
Expand Down Expand Up @@ -222,12 +206,10 @@ def analyse_args(argv: Argv, args: Args) -> dict[str, Any]:
result[key] = argv.converter(argv.release(arg.separators))
argv.current_index = argv.ndata
return result
elif value == AnyOne:
elif value == AnyOne or (value == STRING and _str):
result[key] = may_arg
elif value == AnyString:
result[key] = str(may_arg)
elif value == STRING and _str:
result[key] = may_arg
else:
res = (
value.invalidate(may_arg, default_val)
Expand Down Expand Up @@ -294,6 +276,7 @@ def handle_action(param: Option, source: OptionResult, target: OptionResult):
return source
return target
if not param.nargs:
source.value = source.value[:]
source.value.extend(target.value)
else:
for key, value in target.args.items():
Expand Down
6 changes: 4 additions & 2 deletions src/arclet/alconna/_internal/_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@
from nepattern.util import TPattern
from tarina import Empty, lang

from ._util import escape, unescape
from ..typing import TPrefixes


def handle_bracket(name: str, mapping: dict):
"""处理字符串中的括号对并转为正则表达式"""
pattern_map = all_patterns()
name = escape(name)
if len(parts := re.split(r"(\{.*?})", name)) <= 1:
return name, False
return unescape(name), False
for i, part in enumerate(parts):
if not part:
continue
Expand All @@ -35,7 +37,7 @@ def handle_bracket(name: str, mapping: dict):
parts[i] = f"(?P<{res[0]}>{pattern_map[res[1]].pattern})"
else:
parts[i] = f"(?P<{res[0]}>{res[1]})"
return "".join(parts), True
return unescape("".join(parts)), True


class Pair:
Expand Down
18 changes: 18 additions & 0 deletions src/arclet/alconna/_internal/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,21 @@ def levenshtein(source: str, target: str) -> float:
matrix[i][j] = min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, sub_distance)

return 1 - float(matrix[l_s][l_t]) / max(l_s, l_t)


ESCAPE = {"\\": "\x00", "[": "\x01", "]": "\x02", "{": "\x03", "}": "\x04", "|": "\x05"}
R_ESCAPE = {v: k for k, v in ESCAPE.items()}


def escape(string: str) -> str:
"""转义字符串"""
for k, v in ESCAPE.items():
string = string.replace("\\" + k, v)
return string


def unescape(string: str) -> str:
"""逆转义字符串, 自动去除空白符 """
for k, v in R_ESCAPE.items():
string = string.replace(k, v)
return string.strip()
10 changes: 3 additions & 7 deletions src/arclet/alconna/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,7 @@ def from_callable(cls, target: Callable) -> tuple[Args, bool]:
_args.add(name, value=anno, default=de)
return _args, method

def __init__(
self, *args: Arg, separators: str | Iterable[str] | None = None, **kwargs: TAValue
):
def __init__(self, *args: Arg, separators: str | Iterable[str] | None = None, **kwargs: TAValue):
"""
构造一个 `Args`
Expand Down Expand Up @@ -299,8 +297,7 @@ def __check_vars__(self):
if arg.name in self._visit:
continue
self._visit.add(arg.name)
_limit = False
if isinstance(arg.value, MultiVar) and not _limit:
if isinstance(arg.value, MultiVar):
if isinstance(arg.value.base, KeyWordVar):
if self.var_keyword:
raise InvalidParam(lang.require("args", "duplicate_kwargs"))
Expand All @@ -309,15 +306,14 @@ def __check_vars__(self):
raise InvalidParam(lang.require("args", "duplicate_varargs"))
else:
self.var_positional = arg.value
_limit = True
if isinstance(arg.value, KeyWordVar):
if self.var_keyword or self.var_positional:
raise InvalidParam(lang.require("args", "exclude_mutable_args"))
self.keyword_only.append(arg.name)
if arg.value.sep in arg.separators:
_tmp.insert(-1, Arg(f"_key_{arg.name}", value=f"-*{arg.name}"))
_tmp[-1].value = arg.value.base
if ArgFlag.OPTIONAL in arg.flag:
if arg.optional:
if self.var_keyword or self.var_positional:
raise InvalidParam(lang.require("args", "exclude_mutable_args"))
self.optional_count += 1
Expand Down
12 changes: 3 additions & 9 deletions src/arclet/alconna/arparma.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def __init__(
origin: TDC,
matched: bool = False,
header_match: HeadResult | None = None,
error_info: type[BaseException] | BaseException | str = '',
error_info: type[BaseException] | BaseException | None = None,
error_data: list[str | Any] | None = None,
main_args: dict[str, Any] | None = None,
options: dict[str, OptionResult] | None = None,
Expand All @@ -92,13 +92,13 @@ def __init__(
origin (TDC): 原始数据
matched (bool, optional): 是否匹配
header_match (HeadResult | None, optional): 命令头匹配结果
error_info (type[BaseException] | BaseException | str, optional): 错误信息
error_info (type[BaseException] | BaseException | None, optional): 错误信息
error_data (list[str | Any] | None, optional): 错误数据
main_args (dict[str, Any] | None, optional): 主参数匹配结果
options (dict[str, OptionResult] | None, optional): 选项匹配结果
subcommands (dict[str, SubcommandResult] | None, optional): 子命令匹配结果
"""
self._source = source
self.source = source
self.origin = origin
self.matched = matched
self.header_match = header_match or HeadResult()
Expand All @@ -114,12 +114,6 @@ def _clr(self):
for k in ks:
delattr(self, k)

@property
def source(self):
"""返回命令源"""
from .manager import command_manager
return command_manager.get_command(self._source)

@property
def header(self) -> dict[str, Any]:
"""返回可能解析到的命令头中的组信息"""
Expand Down
24 changes: 22 additions & 2 deletions src/arclet/alconna/builtin.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Callable, overload
from typing import Any, Callable, overload, cast

from .arparma import Arparma, ArparmaBehavior
from .core import Alconna
from .duplication import Duplication
from .stub import ArgsStub, OptionStub, SubcommandStub
from .exceptions import BehaveCancelled

__all__ = ["set_default"]
__all__ = ["set_default", "generate_duplication"]


def generate_duplication(alc: Alconna) -> type[Duplication]:
"""依据给定的命令生成一个解析结果的检查类。"""
from .base import Option, Subcommand
options = filter(lambda x: isinstance(x, Option), alc.options)
subcommands = filter(lambda x: isinstance(x, Subcommand), alc.options)
return cast(type[Duplication], type(
f"{alc.name.strip('/.-:')}Interface",
(Duplication,), {
"__annotations__": {
"args": ArgsStub,
**{opt.dest: OptionStub for opt in options},
**{sub.dest: SubcommandStub for sub in subcommands},
}
}
))


class _MISSING_TYPE: pass
Expand Down
25 changes: 5 additions & 20 deletions src/arclet/alconna/duplication.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@ class Duplication:

def __init__(self, target: Arparma):
from .base import Option, Subcommand
from .manager import command_manager
source = command_manager.get_command(target.source)
self.header = target.header.copy()
for key, value in self.__annotations__.items():
if isclass(value) and issubclass(value, BaseStub):
if value is ArgsStub:
setattr(self, key, ArgsStub(target.source.args).set_result(target.main_args))
setattr(self, key, ArgsStub(source.args).set_result(target.main_args))
elif value is SubcommandStub:
for subcommand in filter(lambda x: isinstance(x, Subcommand), target.source.options):
for subcommand in filter(lambda x: isinstance(x, Subcommand), source.options):
if subcommand.dest == key:
setattr(self, key, SubcommandStub(subcommand).set_result(target.subcommands.get(key, None)))
elif value is OptionStub:
for option in filter(lambda x: isinstance(x, Option), target.source.options):
for option in filter(lambda x: isinstance(x, Option), source.options):
if option.dest == key:
setattr(self, key, OptionStub(option).set_result(target.options.get(key, None)))
elif key != 'header':
Expand All @@ -41,20 +43,3 @@ def option(self, name: str) -> OptionStub | None:
def subcommand(self, name: str) -> SubcommandStub | None:
"""获取指定名称的子命令存根。"""
return cast(SubcommandStub, getattr(self, name, None))


def generate_duplication(arp: Arparma) -> Duplication:
"""依据给定的命令生成一个解析结果的检查类。"""
from .base import Option, Subcommand
options = filter(lambda x: isinstance(x, Option), arp.source.options)
subcommands = filter(lambda x: isinstance(x, Subcommand), arp.source.options)
return cast(Duplication, type(
f"{arp.source.name.strip('/.-:')}Interface",
(Duplication,), {
"__annotations__": {
"args": ArgsStub,
**{opt.dest: OptionStub for opt in options},
**{sub.dest: SubcommandStub for sub in subcommands},
}
}
)(arp))
Loading

0 comments on commit 62dcb4d

Please sign in to comment.