Skip to content

Commit

Permalink
✨ version 1.7.41
Browse files Browse the repository at this point in the history
remainders to separate multipart MultiVar
  • Loading branch information
RF-Tar-Railt committed Dec 25, 2023
1 parent 63c8107 commit 31e1fae
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 43 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# 更新日志

## Alconna 1.7.41

### 改进

- `Args` 现在可以设置多个 MultiVar 参数
- 可以通过 `config.remainder` 配置字符来设置多个多参数之间的分隔符

## Alconna 1.7.40

### 改进
Expand Down
2 changes: 2 additions & 0 deletions exam7.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
from typing_extensions import Self
from dataclasses import dataclass, field


@dataclass
class Scope:
refer: "Node"
substance: dict[str, "Node"] = field(default_factory=dict)


NodeMap: dict[str, Scope] = {}


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.7.40"
__version__ = "1.7.41"

# backward compatibility
Arpamar = Arparma
Expand Down
46 changes: 27 additions & 19 deletions src/arclet/alconna/_internal/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from ..exceptions import ArgumentMissing, FuzzyMatchSuccess, InvalidParam, PauseTriggered, SpecialOptionTriggered
from ..model import HeadResult, OptionResult, Sentence
from ..output import output_manager
from ..typing import KWBool, ShortcutRegWrapper
from ..typing import KWBool, ShortcutRegWrapper, MultiKeyWordVar, MultiVar
from ._header import Double, Header
from ._util import escape, levenshtein, unescape

Expand Down Expand Up @@ -44,34 +44,37 @@ def _validate(argv: Argv, target: Arg[Any], value: BasePattern[Any], result: dic
result[target.name] = res._value # noqa


def step_varpos(argv: Argv, args: Args, result: dict[str, Any]):
value, arg = args.argument.var_positional
def step_varpos(argv: Argv, args: Args, slot: tuple[MultiVar, Arg], result: dict[str, Any]):
value, arg = slot
argv.context = arg
key = arg.name
default_val = arg.field.default
_result = []
kwonly_seps = tuple(arg.value.sep for arg in args.argument.keyword_only.values()) # type: ignore
loop = len(argv.release(arg.separators))
if value.length > 0:
loop = min(loop, value.length)
for _ in range(loop):
count = 0
while argv.current_index != argv.ndata:
may_arg, _str = argv.next(arg.separators)
if _str and may_arg in argv.special:
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
if _str and may_arg in config.remainders:
break
if _str and kwonly_seps and split_once(pat.match(may_arg)["name"], kwonly_seps, argv.filter_crlf)[0] in args.argument.keyword_only: # noqa: E501
argv.rollback(may_arg)
break
if _str and args.argument.var_keyword and args.argument.var_keyword[0].base.sep in may_arg: # type: ignore
if _str and args.argument.vars_keyword and args.argument.vars_keyword[0][0].base.sep in may_arg:
argv.rollback(may_arg)
break
if (res := value.base.exec(may_arg)).flag != "valid":
argv.rollback(may_arg)
break
_result.append(res._value) # noqa
count += 1
if 0 < value.length <= count:
break
if not _result:
if default_val is not None:
_result = default_val if isinstance(default_val, Iterable) else ()
Expand All @@ -82,23 +85,23 @@ def step_varpos(argv: Argv, args: Args, result: dict[str, Any]):
result[key] = tuple(_result)


def step_varkey(argv: Argv, args: Args, result: dict[str, Any]):
value, arg = args.argument.var_keyword
def step_varkey(argv: Argv, slot: tuple[MultiKeyWordVar, Arg], result: dict[str, Any]):
value, arg = slot
argv.context = arg
name = arg.name
default_val = arg.field.default
_result = {}
loop = len(argv.release(arg.separators))
if value.length > 0:
loop = min(loop, value.length)
for _ in range(loop):
count = 0
while argv.current_index != argv.ndata:
may_arg, _str = argv.next(arg.separators)
if _str and may_arg in argv.special:
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
if _str and may_arg in config.remainders:
break
if not (_kwarg := re.match(rf"^(-*[^{value.base.sep}]+){value.base.sep}(.*?)$", may_arg)):
argv.rollback(may_arg)
break
Expand All @@ -109,6 +112,9 @@ def step_varkey(argv: Argv, args: Args, result: dict[str, Any]):
argv.rollback(may_arg)
break
_result[key] = res._value # noqa
count += 1
if 0 < value.length <= count:
break
if not _result:
if default_val is not None:
_result = default_val if isinstance(default_val, dict) else {}
Expand All @@ -134,13 +140,15 @@ def step_keyword(argv: Argv, args: Args, result: dict[str, Any]):
if not may_arg or not _str:
argv.rollback(may_arg)
break
if _str and may_arg in config.remainders:
break
key, _m_arg = split_once(may_arg, kwonly_seps1, argv.filter_crlf)
_key = pat.match(key)["name"]
if _key not in args.argument.keyword_only:
_key = key
if _key not in args.argument.keyword_only:
argv.rollback(may_arg)
if args.argument.var_keyword or (_str and may_arg in argv.param_ids):
if args.argument.vars_keyword or (_str and may_arg in argv.param_ids):
break
for arg in args.argument.keyword_only.values():
if arg.value.base.exec(may_arg).flag == "valid": # type: ignore
Expand Down Expand Up @@ -216,12 +224,12 @@ def analyse_args(argv: Argv, args: Args) -> dict[str, Any]:
result[arg.name] = None if de is Empty else de
elif not arg.optional:
raise e
if args.argument.var_positional:
step_varpos(argv, args, result)
for slot in args.argument.vars_positional:
step_varpos(argv, args, slot, result)
if args.argument.keyword_only:
step_keyword(argv, args, result)
if args.argument.var_keyword:
step_varkey(argv, args, result)
for slot in args.argument.vars_keyword:
step_varkey(argv, slot, result)
argv.context = None
return result

Expand Down
28 changes: 13 additions & 15 deletions src/arclet/alconna/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
from copy import deepcopy
from enum import Enum
from functools import partial
from typing import Any, Callable, Generic, Iterable, List, Sequence, Type, TypeVar, Union
from typing import Any, Callable, Generic, Iterable, List, Sequence, Type, TypeVar, Union, cast
from typing_extensions import Self, TypeAlias

from nepattern import AllParam, AnyOne, BasePattern, MatchMode, RawStr, UnionPattern, all_patterns, type_parser
from tarina import Empty, get_signature, lang

from .exceptions import InvalidArgs
from .typing import KeyWordVar, KWBool, MultiVar, UnpackVar
from .typing import KeyWordVar, KWBool, MultiVar, UnpackVar, MultiKeyWordVar


def safe_dcls_kw(**kwargs):
Expand Down Expand Up @@ -180,9 +180,9 @@ class _argument(List[Arg[Any]]):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.normal: list[Arg[Any]] = []
self.var_positional: tuple[MultiVar, Arg[Any]] | None = None
self.var_keyword: tuple[MultiVar, Arg[Any]] | None = None
self.keyword_only: dict[str, Arg[Any]] = {}
self.vars_positional: list[tuple[MultiVar, Arg[Any]]] = []
self.vars_keyword: list[tuple[MultiKeyWordVar, Arg[Any]]] = []
self.unpack: tuple[Arg, Args] | None = None


Expand Down Expand Up @@ -252,7 +252,7 @@ def from_callable(cls, target: Callable, kw_sep: str = "=") -> tuple[Args, bool]
if param.kind == param.VAR_POSITIONAL:
anno = MultiVar(anno, "*")
if param.kind == param.VAR_KEYWORD:
anno = MultiVar(KeyWordVar(anno), "*")
anno = MultiKeyWordVar(KeyWordVar(anno), "*")
_args.add(name, value=anno, default=de)
return _args, method

Expand Down Expand Up @@ -349,25 +349,23 @@ def __check_vars__(self):
break
if isinstance(arg.value, MultiVar):
if isinstance(arg.value.base, KeyWordVar):
if self.argument.var_keyword:
raise InvalidArgs(lang.require("args", "duplicate_kwargs"))
if self.argument.var_positional and arg.value.base.sep in self.argument.var_positional[1].separators: # noqa: E501
raise InvalidArgs("varkey cannot use the same sep as varpos's Arg")
self.argument.var_keyword = (arg.value, arg)
elif self.argument.var_positional:
raise InvalidArgs(lang.require("args", "duplicate_varargs"))
for slot in self.argument.vars_positional:
_, a = slot
if arg.value.base.sep in a.separators:
raise InvalidArgs("varkey cannot use the same sep as varpos's Arg")
self.argument.vars_keyword.append((cast(MultiKeyWordVar, arg.value), arg))
elif self.argument.keyword_only:
raise InvalidArgs(lang.require("args", "exclude_mutable_args"))
else:
self.argument.var_positional = (arg.value, arg)
self.argument.vars_positional.append((arg.value, arg))
elif isinstance(arg.value, KeyWordVar):
if self.argument.var_keyword:
if self.argument.vars_keyword:
raise InvalidArgs(lang.require("args", "exclude_mutable_args"))
self.argument.keyword_only[arg.name] = arg
else:
self.argument.normal.append(arg)
if arg.optional:
if self.argument.var_keyword or self.argument.var_positional:
if self.argument.vars_keyword or self.argument.vars_positional:
raise InvalidArgs(lang.require("args", "exclude_mutable_args"))
self.optional_count += 1
self.argument.clear()
Expand Down
2 changes: 2 additions & 0 deletions src/arclet/alconna/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ class _AlconnaConfig:
"""模糊匹配阈值"""
_default_namespace = "Alconna"
"""默认命名空间名称"""
remainders: set[str] = {"--"}
"""参数分隔标记"""
namespaces: dict[str, Namespace] = {_default_namespace: Namespace(_default_namespace)}

@property
Expand Down
21 changes: 13 additions & 8 deletions src/arclet/alconna/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,21 +94,22 @@ class CommandMeta:


TDC = TypeVar("TDC", bound=DataCollection[Any])
T = TypeVar("T")


class KeyWordVar(BasePattern):
class KeyWordVar(BasePattern[T]):
"""对具名参数的包装"""

base: BasePattern

def __init__(self, value: BasePattern | Any, sep: str = "="):
def __init__(self, value: BasePattern[T] | type[T], sep: str = "="):
"""构建一个具名参数
Args:
value (type | BasePattern): 参数的值
sep (str, optional): 参数的分隔符
"""
self.base = value if isinstance(value, BasePattern) else type_parser(value)
self.base = value if isinstance(value, BasePattern) else type_parser(value) # type: ignore
self.sep = sep
assert isinstance(self.base, BasePattern)
super().__init__(model=MatchMode.KEEP, origin=self.base.origin, alias=f"@{sep}{self.base}")
Expand All @@ -120,28 +121,28 @@ def __repr__(self):
class _Kw:
__slots__ = ()

def __getitem__(self, item):
def __getitem__(self, item: BasePattern[T] | type[T]):
return KeyWordVar(item)

__matmul__ = __getitem__
__rmatmul__ = __getitem__


class MultiVar(BasePattern):
class MultiVar(BasePattern[T]):
"""对可变参数的包装"""

base: BasePattern
base: BasePattern[T]
flag: Literal["+", "*"]
length: int

def __init__(self, value: BasePattern | Any, flag: int | Literal["+", "*"] = "+"):
def __init__(self, value: BasePattern[T] | type[T], flag: int | Literal["+", "*"] = "+"):
"""构建一个可变参数
Args:
value (type | BasePattern): 参数的值
flag (int | Literal["+", "*"]): 参数的标记
"""
self.base = value if isinstance(value, BasePattern) else type_parser(value)
self.base = value if isinstance(value, BasePattern) else type_parser(value) # type: ignore
assert isinstance(self.base, BasePattern)
if not isinstance(flag, int):
alias = f"({self.base}{flag})"
Expand All @@ -162,6 +163,10 @@ def __repr__(self):
return self.alias


class MultiKeyWordVar(MultiVar):
base: KeyWordVar


Nargs = MultiVar
Kw = _Kw()

Expand Down
11 changes: 11 additions & 0 deletions tests/args_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,17 @@ class People:
assert analyse_args(arg19_1, ["name=alice&age=16"]) == {"people": People("alice", 16)}


def test_multi_multi():
from arclet.alconna.typing import MultiVar

arg20 = Args["foo", MultiVar(str)]["bar", MultiVar(int)]
assert analyse_args(arg20, ["a b -- 1 2"]) == {"foo": ("a", "b"), "bar": (1, 2)}

arg20_1 = Args["foo", MultiVar(int)]["bar", MultiVar(str)]
assert analyse_args(arg20_1, ["1 2 -- a b"]) == {"foo": (1, 2), "bar": ("a", "b")}
assert analyse_args(arg20_1, ["1 2 a b"]) == {"foo": (1, 2), "bar": ("a", "b")}


if __name__ == "__main__":
import pytest

Expand Down

0 comments on commit 31e1fae

Please sign in to comment.