cli-command-parser 2025.9.27__tar.gz → 2026.2.1__tar.gz
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.
- {cli_command_parser-2025.9.27/lib/cli_command_parser.egg-info → cli_command_parser-2026.2.1}/PKG-INFO +7 -10
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/__version__.py +1 -1
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/annotations.py +7 -11
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/command_parameters.py +52 -48
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/config.py +8 -8
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/context.py +12 -12
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/conversion/argparse_ast.py +5 -5
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/conversion/command_builder.py +4 -4
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/conversion/utils.py +3 -3
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/conversion/visitor.py +2 -2
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/core.py +8 -8
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/error_handling/base.py +4 -4
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/exceptions.py +3 -3
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/formatting/commands.py +3 -3
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/formatting/restructured_text.py +2 -2
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/formatting/utils.py +2 -2
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/inputs/__init__.py +19 -25
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/inputs/base.py +11 -2
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/inputs/choices.py +3 -3
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/inputs/files.py +4 -5
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/inputs/numeric.py +127 -13
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/inputs/patterns.py +7 -7
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/inputs/time.py +33 -37
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/inputs/utils.py +3 -3
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/metadata.py +7 -6
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/nargs.py +63 -58
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/parameters/base.py +17 -11
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/parameters/choice_map.py +11 -8
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/parameters/groups.py +8 -2
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/parameters/option_strings.py +7 -7
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/parameters/options.py +4 -4
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/parse_tree.py +9 -10
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/parser.py +1 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/testing.py +8 -8
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1/lib/cli_command_parser.egg-info}/PKG-INFO +7 -10
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser.egg-info/SOURCES.txt +0 -2
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/pyproject.toml +54 -1
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/readme.rst +3 -3
- cli_command_parser-2026.2.1/requirements-dev.txt +16 -0
- cli_command_parser-2026.2.1/setup.cfg +4 -0
- cli_command_parser-2025.9.27/lib/cli_command_parser.egg-info/entry_points.txt +0 -2
- cli_command_parser-2025.9.27/requirements-dev.txt +0 -12
- cli_command_parser-2025.9.27/setup.cfg +0 -50
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/LICENSE +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/MANIFEST.in +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/__init__.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/__main__.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/commands.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/compat.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/conversion/__init__.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/conversion/__main__.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/conversion/argparse_utils.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/conversion/cli.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/documentation.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/error_handling/__init__.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/error_handling/other.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/error_handling/windows.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/formatting/__init__.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/formatting/params.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/inputs/exceptions.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/parameters/__init__.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/parameters/actions.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/parameters/pass_thru.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/parameters/positionals.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/typing.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/utils.py +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser.egg-info/dependency_links.txt +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1/lib/cli_command_parser.egg-info}/entry_points.txt +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser.egg-info/requires.txt +0 -0
- {cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser.egg-info/top_level.txt +0 -0
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli_command_parser
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2026.2.1
|
|
4
4
|
Summary: CLI Command Parser
|
|
5
|
-
|
|
6
|
-
Author: Doug Skrypa
|
|
7
|
-
Author-email: dskrypa@gmail.com
|
|
8
|
-
License: Apache 2.0
|
|
5
|
+
Author-email: Doug Skrypa <dskrypa@gmail.com>
|
|
9
6
|
Project-URL: Source, https://github.com/dskrypa/cli_command_parser
|
|
10
7
|
Project-URL: Documentation, https://dskrypa.github.io/cli_command_parser
|
|
11
8
|
Project-URL: Issues, https://github.com/dskrypa/cli_command_parser/issues
|
|
@@ -16,14 +13,14 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
|
16
13
|
Classifier: Operating System :: OS Independent
|
|
17
14
|
Classifier: Programming Language :: Python
|
|
18
15
|
Classifier: Programming Language :: Python :: 3
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
20
16
|
Classifier: Programming Language :: Python :: 3.10
|
|
21
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
18
|
Classifier: Programming Language :: Python :: 3.12
|
|
23
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
21
|
Classifier: Topic :: Software Development :: User Interfaces
|
|
25
22
|
Classifier: Topic :: Text Processing
|
|
26
|
-
Requires-Python: >=3.
|
|
23
|
+
Requires-Python: >=3.10
|
|
27
24
|
Description-Content-Type: text/x-rst
|
|
28
25
|
License-File: LICENSE
|
|
29
26
|
Provides-Extra: wcwidth
|
|
@@ -35,7 +32,7 @@ CLI Command Parser
|
|
|
35
32
|
|
|
36
33
|
|downloads| |py_version| |coverage_badge| |build_status| |Ruff| |OpenSSF Best Practices|
|
|
37
34
|
|
|
38
|
-
.. |py_version| image:: https://img.shields.io/badge/python-3.
|
|
35
|
+
.. |py_version| image:: https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14%20-blue
|
|
39
36
|
:target: https://pypi.org/project/cli-command-parser/
|
|
40
37
|
|
|
41
38
|
.. |coverage_badge| image:: https://codecov.io/gh/dskrypa/cli_command_parser/branch/main/graph/badge.svg
|
|
@@ -120,8 +117,8 @@ with optional dependencies::
|
|
|
120
117
|
Python Version Compatibility
|
|
121
118
|
============================
|
|
122
119
|
|
|
123
|
-
Python versions 3.
|
|
124
|
-
|
|
120
|
+
Python versions 3.10 and above are currently supported. The last release of CLI Command Parser that supported 3.9 was
|
|
121
|
+
2025-09-27. Support for Python 3.9 `officially ended on 2025-10-31 <https://devguide.python.org/versions/>`__.
|
|
125
122
|
|
|
126
123
|
|
|
127
124
|
Links
|
{cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/__version__.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
__title__ = 'cli_command_parser'
|
|
2
2
|
__description__ = 'CLI Command Parser'
|
|
3
3
|
__url__ = 'https://github.com/dskrypa/cli_command_parser'
|
|
4
|
-
__version__ = '
|
|
4
|
+
__version__ = '2026.02.01'
|
|
5
5
|
__author__ = 'Doug Skrypa'
|
|
6
6
|
__author_email__ = 'dskrypa@gmail.com'
|
|
7
7
|
__license__ = 'Apache 2.0'
|
{cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/annotations.py
RENAMED
|
@@ -7,19 +7,15 @@ Utilities for extracting types from annotations.
|
|
|
7
7
|
from collections.abc import Collection, Iterable
|
|
8
8
|
from functools import lru_cache
|
|
9
9
|
from inspect import isclass
|
|
10
|
-
from
|
|
11
|
-
|
|
12
|
-
try:
|
|
13
|
-
from types import NoneType
|
|
14
|
-
except ImportError: # Added in 3.10
|
|
15
|
-
NoneType = type(None)
|
|
10
|
+
from types import NoneType, UnionType
|
|
11
|
+
from typing import Union, get_args, get_origin, get_type_hints as _get_type_hints
|
|
16
12
|
|
|
17
13
|
__all__ = ['get_descriptor_value_type']
|
|
18
14
|
|
|
19
15
|
get_type_hints = lru_cache()(_get_type_hints) # Cache the attr:annotation mapping for each Command class
|
|
20
16
|
|
|
21
17
|
|
|
22
|
-
def get_descriptor_value_type(command_cls: type, attr: str) ->
|
|
18
|
+
def get_descriptor_value_type(command_cls: type, attr: str) -> type | None:
|
|
23
19
|
try:
|
|
24
20
|
annotation = get_type_hints(command_cls)[attr]
|
|
25
21
|
except (KeyError, NameError): # KeyError due to attr missing; NameError for forward references
|
|
@@ -30,19 +26,19 @@ def get_descriptor_value_type(command_cls: type, attr: str) -> Optional[type]:
|
|
|
30
26
|
return get_annotation_value_type(annotation)
|
|
31
27
|
|
|
32
28
|
|
|
33
|
-
def get_annotation_value_type(annotation, from_union: bool = True, from_collection: bool = True) ->
|
|
29
|
+
def get_annotation_value_type(annotation, from_union: bool = True, from_collection: bool = True) -> type | None:
|
|
34
30
|
origin = get_origin(annotation)
|
|
35
31
|
# Note: get_origin returns `list` for `List[str]`, `List`, and `list[str]`; it returns `None` for `list`
|
|
36
32
|
if origin is None and isinstance(annotation, type):
|
|
37
33
|
return annotation
|
|
38
34
|
elif from_collection and isclass(origin) and issubclass(origin, (Collection, Iterable)):
|
|
39
35
|
return _type_from_collection(origin, annotation)
|
|
40
|
-
elif from_union and origin is Union:
|
|
36
|
+
elif from_union and (origin is Union or origin is UnionType):
|
|
41
37
|
return _type_from_union(annotation)
|
|
42
38
|
return None
|
|
43
39
|
|
|
44
40
|
|
|
45
|
-
def _type_from_union(annotation) ->
|
|
41
|
+
def _type_from_union(annotation) -> type | None:
|
|
46
42
|
args = get_args(annotation)
|
|
47
43
|
# Note: Unions of a single argument return the argument; i.e., Union[T] returns T, so the len can never be 1
|
|
48
44
|
if len(args) == 2 and NoneType in args:
|
|
@@ -50,7 +46,7 @@ def _type_from_union(annotation) -> Optional[type]:
|
|
|
50
46
|
return None
|
|
51
47
|
|
|
52
48
|
|
|
53
|
-
def _type_from_collection(origin, annotation) ->
|
|
49
|
+
def _type_from_collection(origin, annotation) -> type | None:
|
|
54
50
|
if not (args := get_args(annotation)):
|
|
55
51
|
return origin # The annotation was a collection with no content types specified
|
|
56
52
|
n_args = len(args)
|
|
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
from collections import defaultdict
|
|
14
14
|
from functools import cached_property
|
|
15
|
-
from typing import TYPE_CHECKING, Collection, Iterator
|
|
15
|
+
from typing import TYPE_CHECKING, Collection, Iterator
|
|
16
16
|
|
|
17
17
|
from .config import AmbiguousComboMode, CommandConfig
|
|
18
18
|
from .exceptions import AmbiguousCombo, AmbiguousShortForm, CommandDefinitionError, ParameterDefinitionError
|
|
@@ -34,11 +34,10 @@ class CommandParameters:
|
|
|
34
34
|
# fmt: off
|
|
35
35
|
command: CommandCls #: The Command associated with this CommandParameters object
|
|
36
36
|
formatter: CommandHelpFormatter #: The formatter used for this Command's help text
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
sub_command: Optional[SubCommand] = None #: A SubCommand Parameter, if specified
|
|
37
|
+
parent: CommandParameters | None #: The parent Command's CommandParameters
|
|
38
|
+
action: Action | None = None #: An Action Parameter, if specified
|
|
39
|
+
_pass_thru: PassThru | None = None #: A PassThru Parameter, if specified
|
|
40
|
+
sub_command: SubCommand | None = None #: A SubCommand Parameter, if specified
|
|
42
41
|
action_flags: ActionFlags #: List of action flags
|
|
43
42
|
split_action_flags: tuple[ActionFlags, ActionFlags] #: Action flags split by before/after main
|
|
44
43
|
options: list[BaseOption] #: List of optional Parameters
|
|
@@ -49,15 +48,8 @@ class CommandParameters:
|
|
|
49
48
|
option_map: OptionMap #: Mapping of {--opt / -opt: Parameter}
|
|
50
49
|
# fmt: on
|
|
51
50
|
|
|
52
|
-
def __init__(
|
|
53
|
-
self,
|
|
54
|
-
command: CommandCls,
|
|
55
|
-
command_parent: Optional[CommandCls],
|
|
56
|
-
parent_params: Optional[CommandParameters],
|
|
57
|
-
config: CommandConfig,
|
|
58
|
-
):
|
|
51
|
+
def __init__(self, command: CommandCls, parent_params: CommandParameters | None, config: CommandConfig):
|
|
59
52
|
self.command = command
|
|
60
|
-
self.command_parent = command_parent
|
|
61
53
|
self.parent = parent_params
|
|
62
54
|
self.config = config
|
|
63
55
|
self._process_parameters()
|
|
@@ -65,13 +57,12 @@ class CommandParameters:
|
|
|
65
57
|
def __repr__(self) -> str:
|
|
66
58
|
positionals = len(self.positionals)
|
|
67
59
|
options = len(self.options)
|
|
68
|
-
|
|
69
|
-
return f'<{cls_name}[command={self.command.__name__}, {positionals=}, {options=}]>'
|
|
60
|
+
return f'<{self.__class__.__name__}[command={self.command.__name__}, {positionals=}, {options=}]>'
|
|
70
61
|
|
|
71
62
|
# region PassThru Properties
|
|
72
63
|
|
|
73
64
|
@property
|
|
74
|
-
def pass_thru(self) ->
|
|
65
|
+
def pass_thru(self) -> PassThru | None:
|
|
75
66
|
if self._pass_thru:
|
|
76
67
|
return self._pass_thru
|
|
77
68
|
elif self.parent:
|
|
@@ -143,29 +134,32 @@ class CommandParameters:
|
|
|
143
134
|
groups = set()
|
|
144
135
|
|
|
145
136
|
for param in self._iter_parameters():
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
137
|
+
match param:
|
|
138
|
+
case BasePositional():
|
|
139
|
+
positionals.append(param)
|
|
140
|
+
case BaseOption():
|
|
141
|
+
options.append(param)
|
|
142
|
+
case ParamGroup():
|
|
143
|
+
# Groups will only be discovered here when defined with `as` - ex: `with ParamGroup(...) as foo:`
|
|
144
|
+
# Members will always be discovered at the top level since context managers share the outer scope
|
|
145
|
+
groups.add(param)
|
|
146
|
+
case PassThru():
|
|
147
|
+
if self.pass_thru:
|
|
148
|
+
raise CommandDefinitionError(
|
|
149
|
+
f'Invalid PassThru {param=} - it cannot follow another PassThru param'
|
|
150
|
+
)
|
|
151
|
+
self._pass_thru = param
|
|
152
|
+
case _:
|
|
153
|
+
raise CommandDefinitionError(
|
|
154
|
+
f'Unexpected type={param.__class__} for {param=} - custom parameters must extend'
|
|
155
|
+
' BasePositional, BaseOption, or ParamGroup'
|
|
156
|
+
)
|
|
163
157
|
|
|
164
158
|
param_group = param
|
|
165
159
|
while param_group := param_group.group:
|
|
166
160
|
groups.add(param_group)
|
|
167
161
|
|
|
168
|
-
if self.config.add_help and self.
|
|
162
|
+
if self.config.add_help and self.parent is not None and not self.parent._has_help:
|
|
169
163
|
options.append(help_action)
|
|
170
164
|
|
|
171
165
|
self._process_positionals(positionals)
|
|
@@ -204,7 +198,7 @@ class CommandParameters:
|
|
|
204
198
|
if param.has_choices and 0 in param.nargs: # It has local choices or is not required
|
|
205
199
|
unfollowable = param
|
|
206
200
|
else: # It's an Action
|
|
207
|
-
self.action = action_or_sub_cmd = param
|
|
201
|
+
self.action = action_or_sub_cmd = param # type: ignore
|
|
208
202
|
if not param.has_choices:
|
|
209
203
|
raise CommandDefinitionError(f'No choices were registered for {self.action}')
|
|
210
204
|
elif 0 in param.nargs or (param.nargs.variable and not param.has_choices):
|
|
@@ -264,7 +258,7 @@ class CommandParameters:
|
|
|
264
258
|
for param in action_flags:
|
|
265
259
|
if param.func is None:
|
|
266
260
|
raise ParameterDefinitionError(f'No function was registered for {param=}')
|
|
267
|
-
grouped_ordered_flags[param.before_main][param.order].append(param)
|
|
261
|
+
grouped_ordered_flags[param.before_main][param.order].append(param)
|
|
268
262
|
|
|
269
263
|
found_non_always = False
|
|
270
264
|
invalid = {}
|
|
@@ -301,6 +295,7 @@ class CommandParameters:
|
|
|
301
295
|
|
|
302
296
|
@cached_property
|
|
303
297
|
def _classified_combo_options(self) -> tuple[OptionMap, OptionMap]:
|
|
298
|
+
"""Tuple of (single char short:Option map, multi-char short:Option map) for options available in this command"""
|
|
304
299
|
multi_char_combos = {}
|
|
305
300
|
items = self.combo_option_map.items()
|
|
306
301
|
for combo, param in items:
|
|
@@ -315,14 +310,9 @@ class CommandParameters:
|
|
|
315
310
|
|
|
316
311
|
@cached_property
|
|
317
312
|
def _nested_potentially_ambiguous_combo_options(self):
|
|
318
|
-
|
|
319
|
-
for params in self._iter_nested_params():
|
|
320
|
-
nested_single_char_combos, nested_multi_char_combos = params._classified_combo_options
|
|
321
|
-
single_char_combos.update(nested_single_char_combos)
|
|
322
|
-
multi_char_combos.update(nested_multi_char_combos)
|
|
323
|
-
return _find_ambiguous_combos(single_char_combos, multi_char_combos)
|
|
313
|
+
return _find_ambiguous_combos(*self.nested_single_and_multi_char_short_options)
|
|
324
314
|
|
|
325
|
-
def _is_combo_potentially_ambiguous(self, option: str) ->
|
|
315
|
+
def _is_combo_potentially_ambiguous(self, option: str) -> bool | None:
|
|
326
316
|
# Called by short_option_to_param_value_pairs after ensuring the length is > 1
|
|
327
317
|
to_check = option[1:] # Strip leading '-'
|
|
328
318
|
# Note: len(to_check) will never be 2 here - this is only called if len(option) > 2
|
|
@@ -345,21 +335,34 @@ class CommandParameters:
|
|
|
345
335
|
|
|
346
336
|
# endregion
|
|
347
337
|
|
|
338
|
+
@cached_property
|
|
339
|
+
def nested_single_and_multi_char_short_options(self) -> tuple[OptionMap, OptionMap]:
|
|
340
|
+
single_char_shorts, multi_char_shorts = (xcs.copy() for xcs in self._classified_combo_options)
|
|
341
|
+
for params in self._iter_nested_params():
|
|
342
|
+
nested_single_char_shorts, nested_multi_char_shorts = params._classified_combo_options
|
|
343
|
+
single_char_shorts |= nested_single_char_shorts
|
|
344
|
+
multi_char_shorts |= nested_multi_char_shorts
|
|
345
|
+
|
|
346
|
+
return single_char_shorts, multi_char_shorts
|
|
347
|
+
|
|
348
348
|
def _iter_nested_params(self) -> Iterator[CommandParameters]:
|
|
349
349
|
if not self.sub_command:
|
|
350
350
|
return
|
|
351
|
+
|
|
351
352
|
get_params = self.command.__class__.params
|
|
353
|
+
seen = set()
|
|
352
354
|
for choice in self.sub_command.choices.values():
|
|
353
|
-
|
|
355
|
+
# choice.target is the (sub-)Command class that will be used if that choice was selected. Being None
|
|
356
|
+
# indicates it's a subcommand's local choice (i.e., get_params would return this CommandParameters object)
|
|
357
|
+
if choice.target is not None and choice.target not in seen:
|
|
358
|
+
seen.add(choice.target) # Some choices may be aliases for the same target Command
|
|
354
359
|
params: CommandParameters = get_params(choice.target)
|
|
355
360
|
yield params
|
|
356
361
|
yield from params._iter_nested_params()
|
|
357
362
|
|
|
358
363
|
# region Option Processing
|
|
359
364
|
|
|
360
|
-
def short_option_to_param_value_pairs(
|
|
361
|
-
self, option: str
|
|
362
|
-
) -> tuple[list[tuple[str, BaseOption, Optional[str]]], bool]:
|
|
365
|
+
def short_option_to_param_value_pairs(self, option: str) -> tuple[list[tuple[str, BaseOption, str | None]], bool]:
|
|
363
366
|
option, eq, value = option.partition('=')
|
|
364
367
|
if eq: # An `=` was present in the string
|
|
365
368
|
# Note: if the option is not in this Command's option_map, the KeyError is handled by CommandParser
|
|
@@ -393,6 +396,7 @@ class CommandParameters:
|
|
|
393
396
|
else:
|
|
394
397
|
yield from self.all_positionals
|
|
395
398
|
yield from self.options
|
|
399
|
+
|
|
396
400
|
if self.pass_thru and self.pass_thru not in exclude:
|
|
397
401
|
yield self.pass_thru
|
|
398
402
|
|
{cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/config.py
RENAMED
|
@@ -9,7 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
from collections import ChainMap
|
|
10
10
|
from enum import Enum
|
|
11
11
|
from string import whitespace
|
|
12
|
-
from typing import TYPE_CHECKING, Any, Callable, Generic,
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Callable, Generic, Sequence, Type, TypeVar, overload
|
|
13
13
|
|
|
14
14
|
from .exceptions import CommandDefinitionError
|
|
15
15
|
from .utils import FixedFlag, MissingMixin, _NotSet, positive_int
|
|
@@ -33,7 +33,7 @@ __all__ = [
|
|
|
33
33
|
|
|
34
34
|
CV = TypeVar('CV')
|
|
35
35
|
DV = TypeVar('DV')
|
|
36
|
-
ConfigValue =
|
|
36
|
+
ConfigValue = CV | DV
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
# region Config Option Enums
|
|
@@ -60,7 +60,7 @@ class ShowDefaults(FixedFlag):
|
|
|
60
60
|
# fmt: on
|
|
61
61
|
|
|
62
62
|
@classmethod
|
|
63
|
-
def _missing_(cls, value:
|
|
63
|
+
def _missing_(cls, value: str | int) -> ShowDefaults:
|
|
64
64
|
if isinstance(value, str):
|
|
65
65
|
try:
|
|
66
66
|
return cls._member_map_[value.upper().replace('-', '_')] # noqa
|
|
@@ -119,7 +119,7 @@ class OptionNameMode(FixedFlag):
|
|
|
119
119
|
# fmt: on
|
|
120
120
|
|
|
121
121
|
@classmethod
|
|
122
|
-
def _missing_(cls, value:
|
|
122
|
+
def _missing_(cls, value: str | int | None) -> OptionNameMode:
|
|
123
123
|
try:
|
|
124
124
|
return OPT_NAME_MODE_ALIASES[value]
|
|
125
125
|
except KeyError:
|
|
@@ -166,7 +166,7 @@ class SubcommandAliasHelpMode(MissingMixin, Enum):
|
|
|
166
166
|
# fmt: on
|
|
167
167
|
|
|
168
168
|
|
|
169
|
-
CmdAliasMode =
|
|
169
|
+
CmdAliasMode = SubcommandAliasHelpMode | str
|
|
170
170
|
|
|
171
171
|
|
|
172
172
|
class AmbiguousComboMode(MissingMixin, Enum):
|
|
@@ -313,7 +313,7 @@ class CommandConfig:
|
|
|
313
313
|
# region Error Handling Options
|
|
314
314
|
|
|
315
315
|
#: The :class:`.ErrorHandler` to be used by :meth:`.Command.__call__`
|
|
316
|
-
error_handler:
|
|
316
|
+
error_handler: ErrorHandler | None = ConfigItem(_NotSet)
|
|
317
317
|
|
|
318
318
|
#: Whether :meth:`.Command._after_main_` should always be called, even if an exception was raised in
|
|
319
319
|
#: :meth:`.Command.main` (similar to a ``finally`` block)
|
|
@@ -443,7 +443,7 @@ class CommandConfig:
|
|
|
443
443
|
strict_usage_column_width: bool = ConfigItem(False, bool)
|
|
444
444
|
|
|
445
445
|
@config_item(False)
|
|
446
|
-
def wrap_usage_str(self, value: Any) ->
|
|
446
|
+
def wrap_usage_str(self, value: Any) -> int | bool:
|
|
447
447
|
"""
|
|
448
448
|
Wrap the basic usage line after the specified number of characters, or automatically based on terminal size
|
|
449
449
|
if ``True`` is specified instead.
|
|
@@ -467,7 +467,7 @@ class CommandConfig:
|
|
|
467
467
|
|
|
468
468
|
# endregion
|
|
469
469
|
|
|
470
|
-
def __init__(self, parent:
|
|
470
|
+
def __init__(self, parent: CommandConfig | None = None, read_only: bool = False, **kwargs):
|
|
471
471
|
self._data = parent._data.new_child() if parent else ChainMap()
|
|
472
472
|
self._read_only = read_only
|
|
473
473
|
if kwargs:
|
{cli_command_parser-2025.9.27 → cli_command_parser-2026.2.1}/lib/cli_command_parser/context.py
RENAMED
|
@@ -14,7 +14,7 @@ from contextvars import ContextVar
|
|
|
14
14
|
from enum import Enum
|
|
15
15
|
from functools import cached_property
|
|
16
16
|
from inspect import Parameter as _Parameter, Signature
|
|
17
|
-
from typing import TYPE_CHECKING, Any, Callable, Collection, Iterator,
|
|
17
|
+
from typing import TYPE_CHECKING, Any, Callable, Collection, Iterator, Sequence, cast
|
|
18
18
|
|
|
19
19
|
from .config import DEFAULT_CONFIG, CommandConfig
|
|
20
20
|
from .error_handling import ErrorHandler, NullErrorHandler, extended_error_handler
|
|
@@ -24,16 +24,16 @@ from .utils import Terminal, _NotSet
|
|
|
24
24
|
if TYPE_CHECKING:
|
|
25
25
|
from .command_parameters import CommandParameters
|
|
26
26
|
from .commands import Command
|
|
27
|
-
from .parameters import ActionFlag, BaseOption,
|
|
27
|
+
from .parameters import ActionFlag, BaseOption, Parameter
|
|
28
28
|
from .typing import AnyConfig, Bool, CommandObj, CommandType, OptStr, ParamOrGroup, PathLike, StrSeq # noqa
|
|
29
29
|
|
|
30
|
+
Argv = StrSeq | None
|
|
31
|
+
|
|
30
32
|
__all__ = ['Context', 'ctx', 'get_current_context', 'get_or_create_context', 'get_context', 'get_parsed', 'get_raw_arg']
|
|
31
33
|
|
|
32
34
|
_context_stack = ContextVar('cli_command_parser.context.stack')
|
|
33
35
|
_TERMINAL = Terminal()
|
|
34
36
|
|
|
35
|
-
Argv = Optional['StrSeq']
|
|
36
|
-
|
|
37
37
|
|
|
38
38
|
class Context(AbstractContextManager): # Extending AbstractContextManager to make PyCharm's type checker happy
|
|
39
39
|
"""
|
|
@@ -47,19 +47,19 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
|
|
|
47
47
|
prog: OptStr = None
|
|
48
48
|
allow_argv_prog: Bool = True
|
|
49
49
|
_command_obj: CommandObj = None
|
|
50
|
-
_terminal_width:
|
|
50
|
+
_terminal_width: int | None
|
|
51
51
|
_provided: dict[ParamOrGroup, int]
|
|
52
52
|
|
|
53
53
|
def __init__(
|
|
54
54
|
self,
|
|
55
55
|
argv: Argv = None,
|
|
56
|
-
command_cls:
|
|
56
|
+
command_cls: CommandType | None = None,
|
|
57
57
|
*,
|
|
58
|
-
parent:
|
|
58
|
+
parent: Context | None = None,
|
|
59
59
|
config: AnyConfig = None,
|
|
60
60
|
terminal_width: int = None,
|
|
61
61
|
allow_argv_prog: Bool = None,
|
|
62
|
-
command:
|
|
62
|
+
command: CommandObj | None = None,
|
|
63
63
|
**kwargs,
|
|
64
64
|
):
|
|
65
65
|
self.command_cls = command_cls
|
|
@@ -125,7 +125,7 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
|
|
|
125
125
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
126
126
|
_context_stack.get().pop()
|
|
127
127
|
|
|
128
|
-
def __contains__(self, param:
|
|
128
|
+
def __contains__(self, param: ParamOrGroup | str | Any) -> bool:
|
|
129
129
|
try:
|
|
130
130
|
self._parsed[param]
|
|
131
131
|
except KeyError:
|
|
@@ -193,7 +193,7 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
|
|
|
193
193
|
return parsed
|
|
194
194
|
|
|
195
195
|
@cached_property
|
|
196
|
-
def params(self) ->
|
|
196
|
+
def params(self) -> CommandParameters | None:
|
|
197
197
|
"""
|
|
198
198
|
The :class:`.CommandParameters` object that contains the categorized Parameters from the Command associated
|
|
199
199
|
with this Context.
|
|
@@ -202,7 +202,7 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
|
|
|
202
202
|
return self.command_cls.__class__.params(self.command_cls)
|
|
203
203
|
return None
|
|
204
204
|
|
|
205
|
-
def get_error_handler(self) ->
|
|
205
|
+
def get_error_handler(self) -> ErrorHandler | NullErrorHandler:
|
|
206
206
|
"""Returns the :class:`.ErrorHandler` configured to be used."""
|
|
207
207
|
if (error_handler := self.config.error_handler) is _NotSet:
|
|
208
208
|
return extended_error_handler
|
|
@@ -426,7 +426,7 @@ ctx: Context = cast(Context, ContextProxy())
|
|
|
426
426
|
# region Public / Semi-Public Functions
|
|
427
427
|
|
|
428
428
|
|
|
429
|
-
def get_current_context(silent: bool = False) ->
|
|
429
|
+
def get_current_context(silent: bool = False) -> Context | None:
|
|
430
430
|
"""
|
|
431
431
|
Get the currently active parsing context.
|
|
432
432
|
|
|
@@ -8,7 +8,7 @@ from ast import AST, Assign, Call, withitem
|
|
|
8
8
|
from functools import cached_property, partial
|
|
9
9
|
from inspect import BoundArguments, Signature
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import TYPE_CHECKING, Callable, Collection, Generic, Iterator, Type, TypeVar
|
|
11
|
+
from typing import TYPE_CHECKING, Callable, Collection, Generic, Iterator, Type, TypeVar
|
|
12
12
|
|
|
13
13
|
from .argparse_utils import ArgumentParser as _ArgumentParser, SubParsersAction as _SubParsersAction
|
|
14
14
|
from .utils import get_name_repr, iter_module_parents, unparse
|
|
@@ -21,8 +21,8 @@ if TYPE_CHECKING:
|
|
|
21
21
|
__all__ = ['ParserArg', 'ArgGroup', 'MutuallyExclusiveGroup', 'AstArgumentParser', 'SubParser', 'Script']
|
|
22
22
|
log = logging.getLogger(__name__)
|
|
23
23
|
|
|
24
|
-
InitNode =
|
|
25
|
-
OptCall =
|
|
24
|
+
InitNode = Call | Assign | withitem
|
|
25
|
+
OptCall = Call | None
|
|
26
26
|
ParserCls = Type['AstArgumentParser']
|
|
27
27
|
ParserObj = TypeVar('ParserObj', bound='AstArgumentParser')
|
|
28
28
|
RepresentedCallable = TypeVar('RepresentedCallable', bound=Callable)
|
|
@@ -33,7 +33,7 @@ _NotSet = object()
|
|
|
33
33
|
|
|
34
34
|
class Script:
|
|
35
35
|
_parser_classes = {}
|
|
36
|
-
path:
|
|
36
|
+
path: Path | None
|
|
37
37
|
|
|
38
38
|
def __init__(self, src_text: str, smart_loop_handling: bool = True, path: PathLike = None):
|
|
39
39
|
self.smart_loop_handling = smart_loop_handling
|
|
@@ -162,7 +162,7 @@ class AstCallable:
|
|
|
162
162
|
def __repr__(self) -> str:
|
|
163
163
|
return f'<{self.__class__.__name__}[{self.init_call_repr()}]>'
|
|
164
164
|
|
|
165
|
-
def get_tracked_refs(self, module: str, name: str, default: D = _NotSet) ->
|
|
165
|
+
def get_tracked_refs(self, module: str, name: str, default: D = _NotSet) -> set[str] | D:
|
|
166
166
|
for tracked_ref, refs in self._tracked_refs.items():
|
|
167
167
|
if tracked_ref.module == module and tracked_ref.name == name:
|
|
168
168
|
return refs
|
|
@@ -3,11 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
import keyword
|
|
4
4
|
import logging
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
|
-
from ast import Attribute, Constant, DictComp, GeneratorExp, ListComp, Name, SetComp,
|
|
6
|
+
from ast import Attribute, Constant, DictComp, GeneratorExp, ListComp, Name, SetComp, Subscript, literal_eval
|
|
7
7
|
from dataclasses import dataclass, fields
|
|
8
8
|
from functools import cached_property
|
|
9
9
|
from itertools import count
|
|
10
|
-
from typing import TYPE_CHECKING, Generic, Iterable, Iterator,
|
|
10
|
+
from typing import TYPE_CHECKING, Generic, Iterable, Iterator, Type, TypeVar
|
|
11
11
|
|
|
12
12
|
from cli_command_parser.nargs import Nargs
|
|
13
13
|
|
|
@@ -45,12 +45,12 @@ class Converter(Generic[AC], ABC):
|
|
|
45
45
|
if newline_between_members is not None:
|
|
46
46
|
cls.newline_between_members = newline_between_members
|
|
47
47
|
|
|
48
|
-
def __init__(self, ast_obj:
|
|
48
|
+
def __init__(self, ast_obj: AC | Script, parent: Converter | None = None):
|
|
49
49
|
self.ast_obj = ast_obj
|
|
50
50
|
self.parent = parent
|
|
51
51
|
|
|
52
52
|
@classmethod
|
|
53
|
-
def for_ast_callable(cls, ast_obj:
|
|
53
|
+
def for_ast_callable(cls, ast_obj: AC | Type[AC]) -> Type[Converter[AC]]:
|
|
54
54
|
if not isinstance(ast_obj, type):
|
|
55
55
|
ast_obj = ast_obj.__class__
|
|
56
56
|
try:
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from ast import AST, Attribute, Call, Dict, List, Name, Set, Tuple, expr, unparse
|
|
4
|
-
from typing import Iterator
|
|
4
|
+
from typing import Iterator
|
|
5
5
|
|
|
6
6
|
__all__ = ['get_name_repr', 'iter_module_parents', 'collection_contents']
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def get_name_repr(node:
|
|
9
|
+
def get_name_repr(node: AST | expr) -> str:
|
|
10
10
|
if isinstance(node, Call):
|
|
11
11
|
node = node.func
|
|
12
12
|
|
|
@@ -29,7 +29,7 @@ def iter_module_parents(module: str) -> Iterator[str]:
|
|
|
29
29
|
module = parent
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def collection_contents(node: AST) ->
|
|
32
|
+
def collection_contents(node: AST) -> list[str]:
|
|
33
33
|
if isinstance(node, Dict):
|
|
34
34
|
return [unparse(key) for key in node.keys] # noqa
|
|
35
35
|
elif isinstance(node, (List, Set, Tuple)):
|
|
@@ -5,7 +5,7 @@ import re
|
|
|
5
5
|
from ast import AST, Assign, Attribute, Call, For, Import, ImportFrom, Name, NodeVisitor, expr
|
|
6
6
|
from collections import ChainMap, defaultdict
|
|
7
7
|
from functools import partial, wraps
|
|
8
|
-
from typing import TYPE_CHECKING, Callable, Collection, Iterator
|
|
8
|
+
from typing import TYPE_CHECKING, Callable, Collection, Iterator
|
|
9
9
|
|
|
10
10
|
from .argparse_ast import AstArgumentParser
|
|
11
11
|
from .utils import get_name_repr
|
|
@@ -156,7 +156,7 @@ class ScriptVisitor(NodeVisitor):
|
|
|
156
156
|
|
|
157
157
|
# endregion
|
|
158
158
|
|
|
159
|
-
def resolve_ref(self, name:
|
|
159
|
+
def resolve_ref(self, name: str | AST | Attribute | Name | expr):
|
|
160
160
|
if isinstance(name, Attribute) and isinstance(name.value, Call):
|
|
161
161
|
obj = self.visit_Call(name.value)
|
|
162
162
|
attr = name.attr
|
|
@@ -8,7 +8,7 @@ top-level Command.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
from abc import ABC, ABCMeta
|
|
11
|
-
from typing import TYPE_CHECKING, Any, Callable, Collection, Iterable, Iterator, Mapping,
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Callable, Collection, Iterable, Iterator, Mapping, TypeVar
|
|
12
12
|
from warnings import warn
|
|
13
13
|
from weakref import WeakSet
|
|
14
14
|
|
|
@@ -20,9 +20,9 @@ from .metadata import ProgramMetadata
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
21
21
|
from .typing import AnyConfig, CommandAny, CommandCls, Config, OptStr
|
|
22
22
|
|
|
23
|
-
Bases =
|
|
24
|
-
Choices =
|
|
25
|
-
OptChoices =
|
|
23
|
+
Bases = tuple[type, ...] | Iterable[type]
|
|
24
|
+
Choices = Mapping[str, str | None] | Collection[str]
|
|
25
|
+
OptChoices = Choices | None
|
|
26
26
|
T = TypeVar('T')
|
|
27
27
|
|
|
28
28
|
__all__ = ['CommandMeta', 'get_parent', 'get_config', 'get_params', 'get_metadata', 'get_top_level_commands']
|
|
@@ -134,7 +134,7 @@ class CommandMeta(ABCMeta, type):
|
|
|
134
134
|
_no_choices_registered_warning(choice, choices, cls, 'it has no parent Command')
|
|
135
135
|
|
|
136
136
|
@classmethod
|
|
137
|
-
def _from_parent(mcs, meth: Callable[[CommandCls], T], bases: Bases) ->
|
|
137
|
+
def _from_parent(mcs, meth: Callable[[CommandCls], T], bases: Bases) -> T | None:
|
|
138
138
|
for base in bases:
|
|
139
139
|
if isinstance(base, mcs):
|
|
140
140
|
return meth(base)
|
|
@@ -159,7 +159,7 @@ class CommandMeta(ABCMeta, type):
|
|
|
159
159
|
return None
|
|
160
160
|
|
|
161
161
|
@classmethod
|
|
162
|
-
def config(mcs, cls: CommandAny, default: T = None) ->
|
|
162
|
+
def config(mcs, cls: CommandAny, default: T = None) -> CommandConfig | T:
|
|
163
163
|
try:
|
|
164
164
|
return cls.__config # This attr is not overwritten for every subclass
|
|
165
165
|
except AttributeError: # This means that the Command and all of its parents have no custom config
|
|
@@ -170,7 +170,7 @@ class CommandMeta(ABCMeta, type):
|
|
|
170
170
|
# region Metaclass-Managed Command Attributes
|
|
171
171
|
|
|
172
172
|
@classmethod
|
|
173
|
-
def parent(mcs, cls: CommandAny, include_abc: bool = True) ->
|
|
173
|
+
def parent(mcs, cls: CommandAny, include_abc: bool = True) -> CommandCls | None:
|
|
174
174
|
"""
|
|
175
175
|
:param cls: A Command class or object
|
|
176
176
|
:param include_abc: If True, the first Command parent class in the given Command's mro will be returned,
|
|
@@ -211,7 +211,7 @@ class CommandMeta(ABCMeta, type):
|
|
|
211
211
|
cls = cls.__class__
|
|
212
212
|
parent = mcs.parent(cls, True)
|
|
213
213
|
parent_params = mcs.params(parent) if parent is not None else None
|
|
214
|
-
cls.__params = params = CommandParameters(cls,
|
|
214
|
+
cls.__params = params = CommandParameters(cls, parent_params, mcs.config(cls, DEFAULT_CONFIG))
|
|
215
215
|
return params
|
|
216
216
|
|
|
217
217
|
@classmethod
|