cmdargparse 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ import cmd2 as _cmd2
2
+
3
+ from .argument import cmdargument
4
+ from .command import cmdcommand
5
+ from .field import cmdfield
6
+ from .namespace import Namespace
7
+ from .parser import ArgumentParser
8
+
9
+
10
+ _cmd2.set_default_argument_parser_type(ArgumentParser)
@@ -0,0 +1,99 @@
1
+ import dataclasses
2
+ import functools
3
+ from typing import (
4
+ TYPE_CHECKING,
5
+ Callable,
6
+ ClassVar,
7
+ Type,
8
+ TypeVar,
9
+ dataclass_transform,
10
+ overload,
11
+ )
12
+
13
+ from .field import _DeclFormSpecifier
14
+ from .namespace import Namespace
15
+
16
+
17
+ # ################################ TYPING ######################################
18
+
19
+
20
+ T = TypeVar("T")
21
+
22
+
23
+ # ################################ METACLASS ###################################
24
+
25
+
26
+ class _cmdargument(type):
27
+
28
+ # ################## DEFAULTS ##########################
29
+
30
+ _cmdargparse_default_form: ClassVar = "-"
31
+
32
+ @classmethod
33
+ def default_form(cls, form: _DeclFormSpecifier, /) -> None:
34
+ """Sets the global default declaration form specifier."""
35
+
36
+ cls._cmdargparse_default_form = form
37
+
38
+ # ################## DECORATOR #########################
39
+
40
+ @overload
41
+ @dataclass_transform(eq_default=False)
42
+ def __call__(
43
+ cls,
44
+ _cls: Type[T],
45
+ /,
46
+ ) -> Type[T]: ...
47
+
48
+ @overload
49
+ @dataclass_transform(eq_default=False)
50
+ def __call__(
51
+ cls,
52
+ /,
53
+ *,
54
+ form: _DeclFormSpecifier,
55
+ ) -> Callable[[Type[T]], Type[T]]: ...
56
+
57
+ @dataclass_transform(eq_default=False)
58
+ def __call__(
59
+ cls,
60
+ _cls: Type[T] | None = None,
61
+ /,
62
+ *,
63
+ form: _DeclFormSpecifier | None = None,
64
+ ) -> Callable[[Type[T]], Type[T]] | Type[T]:
65
+ """
66
+ Decorate as command argument object.
67
+
68
+ :param form:
69
+ The default declaration form specifier.
70
+ """
71
+
72
+ if _cls is None:
73
+ return functools.partial(
74
+ cls.__call__,
75
+ default=form,
76
+ ) # type: ignore
77
+
78
+ setattr(
79
+ _cls,
80
+ "_cmdargparse_default_form",
81
+ form or cls._cmdargparse_default_form,
82
+ )
83
+
84
+ return dataclasses.dataclass(
85
+ init=False,
86
+ repr=False,
87
+ eq=False,
88
+ )(_cls)
89
+
90
+
91
+ # ################################ CLASS #######################################
92
+
93
+
94
+ class cmdargument(
95
+ # Allow the type checker to find the namespace attributes.
96
+ (Namespace if TYPE_CHECKING else object),
97
+ metaclass=_cmdargument,
98
+ ):
99
+ pass
cmdargparse/command.py ADDED
@@ -0,0 +1,200 @@
1
+ import argparse
2
+ import dataclasses
3
+ import types
4
+ import typing
5
+ from typing import Any, Callable, Optional, Tuple, Type, TypeVar
6
+
7
+ import cmd2
8
+
9
+ from .field import _DeclFormSpecifier, _FieldDecls
10
+ from .namespace import Namespace
11
+ from .parser import ArgumentParser
12
+
13
+
14
+ # ################################ TYPING ######################################
15
+
16
+
17
+ TSELF = TypeVar("TSELF", cmd2.Cmd, cmd2.CommandSet)
18
+ TARGUMENT = TypeVar("TARGUMENT", bound=object)
19
+ TRETURN = TypeVar("TRETURN", Optional[bool], bool, None)
20
+
21
+
22
+ # ################################ METACLASS ###################################
23
+
24
+
25
+ class _cmdcommand(type):
26
+
27
+ # ################## DECORATOR #########################
28
+
29
+ def __call__(
30
+ cls,
31
+ argtype: Type[TARGUMENT],
32
+ /,
33
+ ) -> Callable[
34
+ [Callable[[TSELF, TARGUMENT], TRETURN]],
35
+ Callable[[TSELF, TARGUMENT], TRETURN],
36
+ ]:
37
+ """Decorate as command function."""
38
+
39
+ if hasattr(argtype, "_cmdargparse_command_decorator"):
40
+ return getattr(argtype, "_cmdargparse_command_decorator")
41
+
42
+ parser = ArgumentParser()
43
+
44
+ _dcfields = dataclasses.fields(
45
+ argtype # pyright: ignore[reportArgumentType]
46
+ )
47
+
48
+ default_form: _DeclFormSpecifier
49
+ default_form = getattr(argtype, "_cmdargparse_default_form")
50
+
51
+ for field in _dcfields:
52
+ decls: _FieldDecls | None
53
+ decls = field.metadata["decls"]
54
+
55
+ name_or_flags = (
56
+ () # Arguments are solely specified using the 'dest' keyword.
57
+ if decls is None
58
+ else argparse_name_or_flags(
59
+ field.name,
60
+ decls,
61
+ default_form=default_form,
62
+ )
63
+ )
64
+
65
+ args: dict[str, Any]
66
+ args = field.metadata["args"]
67
+
68
+ assert "dest" not in args, (
69
+ "Specifying 'dest' directly is not supported."
70
+ # <format-break>
71
+ )
72
+ args["dest"] = field.name
73
+
74
+ if (
75
+ args.get("action", None)
76
+ in ("store_true", "store_false", "store_const")
77
+ # <format-break>
78
+ ):
79
+ # Remove arguments for 'const' and 'type' if specified.
80
+ if args["action"] != "store_const":
81
+ args.pop("const", ...)
82
+ args.pop("type", ...)
83
+
84
+ parser.add_argument(*name_or_flags, **args)
85
+
86
+ continue
87
+
88
+ # Resolve the type from the fields type annotation.
89
+ if args.get("type", None) is None:
90
+ args["type"] = argparse_type(field.type)
91
+
92
+ # Remove custom unspecified arguments.
93
+ if args.get("choicesmap", None) is None:
94
+ args.pop("choicesmap", ...)
95
+
96
+ parser.add_argument(*name_or_flags, **args)
97
+
98
+ setattr(
99
+ argtype,
100
+ "_cmdargparse_command_decorator",
101
+ _cmdargparse_command_decorator := cmd2.with_argparser(
102
+ parser,
103
+ ns_provider=cls.namespace_provider(argtype),
104
+ ),
105
+ )
106
+
107
+ return _cmdargparse_command_decorator # type: ignore
108
+
109
+ # ################## HELPERS ###########################
110
+
111
+ @staticmethod
112
+ def namespace_provider(
113
+ argtype: Type[TARGUMENT],
114
+ /,
115
+ ) -> Callable[
116
+ [cmd2.Cmd | cmd2.CommandSet],
117
+ argparse.Namespace,
118
+ ]:
119
+ # Inherit all attributes that are assumed to be constants.
120
+ # (This includes all uppercase attributes not starting with an '_'.)
121
+ namespace = {
122
+ attr: getattr(argtype, attr)
123
+ for attr in dir(argtype)
124
+ if (
125
+ not attr.startswith("_")
126
+ and attr.isupper()
127
+ # <format-break>
128
+ )
129
+ }
130
+
131
+ def _provider(cmd: cmd2.Cmd | cmd2.CommandSet) -> Namespace:
132
+ return Namespace(**namespace)
133
+
134
+ return _provider
135
+
136
+
137
+ # ################################ CLASS #######################################
138
+
139
+
140
+ class cmdcommand(metaclass=_cmdcommand):
141
+
142
+ pass
143
+
144
+
145
+ # ################################ HELPERS #####################################
146
+
147
+
148
+ def argparse_name_or_flags(
149
+ field: str,
150
+ /,
151
+ decls: _FieldDecls,
152
+ *,
153
+ default_form: _DeclFormSpecifier,
154
+ ) -> Tuple[str, ...]:
155
+ (decl, altdecl) = (decls.decl, decls.altdecl)
156
+
157
+ form = decls.form or default_form
158
+ more_decls = decls.more_decls or ()
159
+
160
+ # In the augmented declaration specification format the automatically
161
+ # derived declaration is replaced if it has the same form.
162
+ if (decl is None and altdecl is not None) and (
163
+ (form == "-" and not altdecl.startswith("--"))
164
+ or (form == "--" and altdecl.startswith("--"))
165
+ ):
166
+ decl, altdecl = altdecl, None
167
+
168
+ return tuple(
169
+ _decl
170
+ for _decl in (
171
+ decl or (form + field),
172
+ altdecl,
173
+ *more_decls,
174
+ )
175
+ if _decl is not None
176
+ )
177
+
178
+
179
+ def argparse_type(type: Any, /) -> Type[Any]:
180
+ if isinstance(type, str):
181
+ raise TypeError("forward reference type annotations are not supported")
182
+
183
+ origin = typing.get_origin(type)
184
+ if origin is None:
185
+ return type
186
+
187
+ if origin is typing.Annotated:
188
+ (argtype, *_) = typing.get_args(type)
189
+ return argparse_type(argtype)
190
+
191
+ # We assume the first argument of the union type is the primary type that
192
+ # should be passed to the `argparse` module.
193
+ if isinstance(type, types.UnionType):
194
+ (argtype, *_) = typing.get_args(type)
195
+ return argtype
196
+ elif origin is typing.Union:
197
+ (argtype, *_) = typing.get_args(type)
198
+ return argtype
199
+
200
+ raise TypeError("unsupported type annotation")
cmdargparse/custom.py ADDED
@@ -0,0 +1,120 @@
1
+ import argparse
2
+ from typing import (
3
+ TYPE_CHECKING,
4
+ Annotated,
5
+ Any,
6
+ Generic,
7
+ Protocol,
8
+ Self,
9
+ Sequence,
10
+ TypeAlias,
11
+ TypeIs,
12
+ TypeVar,
13
+ )
14
+
15
+ from .namespace import Namespace
16
+ from .parser import ArgumentParser
17
+
18
+
19
+ # ################################ GLOBALS #####################################
20
+
21
+
22
+ __all__ = (
23
+ # fmt: off
24
+ "TANY",
25
+ "decorator",
26
+ "is_single_value", "is_multiple_values",
27
+ # fmt: on
28
+ )
29
+
30
+
31
+ # ################################ TYPING ######################################
32
+
33
+
34
+ T = TypeVar("T")
35
+ T_ct = TypeVar("T_ct", contravariant=True)
36
+
37
+ TANY: TypeAlias = Any
38
+
39
+
40
+ # ################################ DECORATORS ##################################
41
+
42
+
43
+ class _ArgparseActionDunderCallCallable(Protocol):
44
+ def __call__(
45
+ _self,
46
+ self: Annotated[Any, Self],
47
+ parser: argparse.ArgumentParser,
48
+ namespace: argparse.Namespace,
49
+ values: str | Sequence[Any] | None,
50
+ option_string: str | None = None,
51
+ ) -> None: ...
52
+
53
+
54
+ class _ActionDunderCallCallable(Protocol, Generic[T_ct]):
55
+ def __call__(
56
+ _self,
57
+ self: Annotated[Any, Self],
58
+ parser: ArgumentParser,
59
+ namespace: Namespace,
60
+ values: Sequence[T_ct] | T_ct,
61
+ option_string: str | None = None,
62
+ ) -> None: ...
63
+
64
+
65
+ class decorator:
66
+
67
+ @staticmethod
68
+ def call(
69
+ func: _ActionDunderCallCallable[T_ct],
70
+ /,
71
+ ) -> Annotated[Any, _ArgparseActionDunderCallCallable]:
72
+ """Supresses `reportIncompatibleMethodOverride` on `Action.__call__`."""
73
+ return func # pyright: ignore[reportReturnType]
74
+
75
+
76
+ # ################################ FUNCTIONS ###################################
77
+
78
+
79
+ def is_single_value(
80
+ values: Sequence[TANY] | TANY,
81
+ /,
82
+ ) -> TypeIs[TANY]:
83
+ """Typing helper for the `values` argument of `Action.__call__`."""
84
+ # The `argparse` module passes multiple values as a list.
85
+ return not isinstance(values, list)
86
+
87
+
88
+ def is_multiple_values(
89
+ values: Sequence[TANY] | TANY,
90
+ /,
91
+ ) -> TypeIs[Sequence[TANY]]:
92
+ """Typing helper for the `values` argument of `Action.__call__`."""
93
+ # The `argparse` module passes multiple values as a list.
94
+ return isinstance(values, list)
95
+
96
+
97
+ # ################################ TEMPALTES ###################################
98
+ if TYPE_CHECKING:
99
+
100
+ class ActionTemplate(argparse.Action):
101
+
102
+ def __init__(
103
+ self,
104
+ *args: Annotated[Any, "passthrough"],
105
+ **kwargs: Annotated[Any, "passthrough"],
106
+ ) -> None:
107
+ assert not args, (
108
+ "There should be no positional arguments if not explicitly "
109
+ "specified. (The `argparse` module passes by keyword.)"
110
+ )
111
+ super().__init__(*args, **kwargs)
112
+
113
+ @decorator.call
114
+ def __call__(
115
+ self,
116
+ parser: ArgumentParser,
117
+ namespace: Namespace,
118
+ values: Sequence[TANY] | TANY,
119
+ option_string: str | None = None,
120
+ ) -> None: ...