cli-command-parser 2025.7.13__tar.gz → 2025.11.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.7.13 → cli_command_parser-2025.11.1}/LICENSE +1 -1
- {cli_command_parser-2025.7.13/lib/cli_command_parser.egg-info → cli_command_parser-2025.11.1}/PKG-INFO +7 -10
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/__init__.py +1 -1
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/__version__.py +1 -1
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/annotations.py +7 -11
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/command_parameters.py +53 -41
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/commands.py +22 -8
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/config.py +8 -8
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/context.py +12 -12
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/conversion/argparse_ast.py +5 -5
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/conversion/command_builder.py +4 -4
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/conversion/utils.py +3 -3
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/conversion/visitor.py +2 -2
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/core.py +7 -7
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/error_handling/base.py +4 -4
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/exceptions.py +3 -3
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/formatting/commands.py +3 -3
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/formatting/restructured_text.py +2 -2
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/formatting/utils.py +2 -2
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/inputs/__init__.py +17 -23
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/inputs/base.py +2 -2
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/inputs/choices.py +3 -3
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/inputs/files.py +4 -5
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/inputs/numeric.py +2 -3
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/inputs/patterns.py +7 -7
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/inputs/time.py +32 -31
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/inputs/utils.py +3 -3
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/metadata.py +7 -6
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/nargs.py +63 -58
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/parameters/base.py +10 -10
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/parameters/choice_map.py +14 -11
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/parameters/groups.py +8 -2
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/parameters/option_strings.py +7 -7
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/parameters/options.py +4 -4
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/parse_tree.py +9 -10
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/parser.py +1 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/testing.py +8 -8
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1/lib/cli_command_parser.egg-info}/PKG-INFO +7 -10
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser.egg-info/SOURCES.txt +0 -2
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/pyproject.toml +54 -1
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/readme.rst +3 -3
- cli_command_parser-2025.11.1/requirements-dev.txt +16 -0
- cli_command_parser-2025.11.1/setup.cfg +4 -0
- cli_command_parser-2025.7.13/lib/cli_command_parser.egg-info/entry_points.txt +0 -2
- cli_command_parser-2025.7.13/requirements-dev.txt +0 -12
- cli_command_parser-2025.7.13/setup.cfg +0 -50
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/MANIFEST.in +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/__main__.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/compat.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/conversion/__init__.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/conversion/__main__.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/conversion/argparse_utils.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/conversion/cli.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/documentation.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/error_handling/__init__.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/error_handling/other.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/error_handling/windows.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/formatting/__init__.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/formatting/params.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/inputs/exceptions.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/parameters/__init__.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/parameters/actions.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/parameters/pass_thru.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/parameters/positionals.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/typing.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/utils.py +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser.egg-info/dependency_links.txt +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1/lib/cli_command_parser.egg-info}/entry_points.txt +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser.egg-info/requires.txt +0 -0
- {cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser.egg-info/top_level.txt +0 -0
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
same "printed page" as the copyright notice for easier
|
|
187
187
|
identification within third-party archives.
|
|
188
188
|
|
|
189
|
-
Copyright
|
|
189
|
+
Copyright 2025 Doug Skrypa
|
|
190
190
|
|
|
191
191
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
192
|
you may not use this file except in compliance with the License.
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cli_command_parser
|
|
3
|
-
Version: 2025.
|
|
3
|
+
Version: 2025.11.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.7.13 → cli_command_parser-2025.11.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__ = '2025.
|
|
4
|
+
__version__ = '2025.11.01'
|
|
5
5
|
__author__ = 'Doug Skrypa'
|
|
6
6
|
__author_email__ = 'dskrypa@gmail.com'
|
|
7
7
|
__license__ = 'Apache 2.0'
|
{cli_command_parser-2025.7.13 → cli_command_parser-2025.11.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,11 @@ 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
|
-
command_parent:
|
|
38
|
-
parent:
|
|
39
|
-
action:
|
|
40
|
-
_pass_thru:
|
|
41
|
-
sub_command:
|
|
37
|
+
command_parent: CommandCls | None #: The parent Command, if any
|
|
38
|
+
parent: CommandParameters | None #: The parent Command's CommandParameters
|
|
39
|
+
action: Action | None = None #: An Action Parameter, if specified
|
|
40
|
+
_pass_thru: PassThru | None = None #: A PassThru Parameter, if specified
|
|
41
|
+
sub_command: SubCommand | None = None #: A SubCommand Parameter, if specified
|
|
42
42
|
action_flags: ActionFlags #: List of action flags
|
|
43
43
|
split_action_flags: tuple[ActionFlags, ActionFlags] #: Action flags split by before/after main
|
|
44
44
|
options: list[BaseOption] #: List of optional Parameters
|
|
@@ -52,8 +52,8 @@ class CommandParameters:
|
|
|
52
52
|
def __init__(
|
|
53
53
|
self,
|
|
54
54
|
command: CommandCls,
|
|
55
|
-
command_parent:
|
|
56
|
-
parent_params:
|
|
55
|
+
command_parent: CommandCls | None,
|
|
56
|
+
parent_params: CommandParameters | None,
|
|
57
57
|
config: CommandConfig,
|
|
58
58
|
):
|
|
59
59
|
self.command = command
|
|
@@ -65,13 +65,12 @@ class CommandParameters:
|
|
|
65
65
|
def __repr__(self) -> str:
|
|
66
66
|
positionals = len(self.positionals)
|
|
67
67
|
options = len(self.options)
|
|
68
|
-
|
|
69
|
-
return f'<{cls_name}[command={self.command.__name__}, {positionals=}, {options=}]>'
|
|
68
|
+
return f'<{self.__class__.__name__}[command={self.command.__name__}, {positionals=}, {options=}]>'
|
|
70
69
|
|
|
71
70
|
# region PassThru Properties
|
|
72
71
|
|
|
73
72
|
@property
|
|
74
|
-
def pass_thru(self) ->
|
|
73
|
+
def pass_thru(self) -> PassThru | None:
|
|
75
74
|
if self._pass_thru:
|
|
76
75
|
return self._pass_thru
|
|
77
76
|
elif self.parent:
|
|
@@ -143,23 +142,26 @@ class CommandParameters:
|
|
|
143
142
|
groups = set()
|
|
144
143
|
|
|
145
144
|
for param in self._iter_parameters():
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
145
|
+
match param:
|
|
146
|
+
case BasePositional():
|
|
147
|
+
positionals.append(param)
|
|
148
|
+
case BaseOption():
|
|
149
|
+
options.append(param)
|
|
150
|
+
case ParamGroup():
|
|
151
|
+
# Groups will only be discovered here when defined with `as` - ex: `with ParamGroup(...) as foo:`
|
|
152
|
+
# Members will always be discovered at the top level since context managers share the outer scope
|
|
153
|
+
groups.add(param)
|
|
154
|
+
case PassThru():
|
|
155
|
+
if self.pass_thru:
|
|
156
|
+
raise CommandDefinitionError(
|
|
157
|
+
f'Invalid PassThru {param=} - it cannot follow another PassThru param'
|
|
158
|
+
)
|
|
159
|
+
self._pass_thru = param
|
|
160
|
+
case _:
|
|
161
|
+
raise CommandDefinitionError(
|
|
162
|
+
f'Unexpected type={param.__class__} for {param=} - custom parameters must extend'
|
|
163
|
+
' BasePositional, BaseOption, or ParamGroup'
|
|
164
|
+
)
|
|
163
165
|
|
|
164
166
|
param_group = param
|
|
165
167
|
while param_group := param_group.group:
|
|
@@ -204,7 +206,7 @@ class CommandParameters:
|
|
|
204
206
|
if param.has_choices and 0 in param.nargs: # It has local choices or is not required
|
|
205
207
|
unfollowable = param
|
|
206
208
|
else: # It's an Action
|
|
207
|
-
self.action = action_or_sub_cmd = param
|
|
209
|
+
self.action = action_or_sub_cmd = param # type: ignore
|
|
208
210
|
if not param.has_choices:
|
|
209
211
|
raise CommandDefinitionError(f'No choices were registered for {self.action}')
|
|
210
212
|
elif 0 in param.nargs or (param.nargs.variable and not param.has_choices):
|
|
@@ -264,7 +266,7 @@ class CommandParameters:
|
|
|
264
266
|
for param in action_flags:
|
|
265
267
|
if param.func is None:
|
|
266
268
|
raise ParameterDefinitionError(f'No function was registered for {param=}')
|
|
267
|
-
grouped_ordered_flags[param.before_main][param.order].append(param)
|
|
269
|
+
grouped_ordered_flags[param.before_main][param.order].append(param)
|
|
268
270
|
|
|
269
271
|
found_non_always = False
|
|
270
272
|
invalid = {}
|
|
@@ -301,6 +303,7 @@ class CommandParameters:
|
|
|
301
303
|
|
|
302
304
|
@cached_property
|
|
303
305
|
def _classified_combo_options(self) -> tuple[OptionMap, OptionMap]:
|
|
306
|
+
"""Tuple of (single char short:Option map, multi-char short:Option map) for options available in this command"""
|
|
304
307
|
multi_char_combos = {}
|
|
305
308
|
items = self.combo_option_map.items()
|
|
306
309
|
for combo, param in items:
|
|
@@ -315,14 +318,9 @@ class CommandParameters:
|
|
|
315
318
|
|
|
316
319
|
@cached_property
|
|
317
320
|
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)
|
|
321
|
+
return _find_ambiguous_combos(*self.nested_single_and_multi_char_short_options)
|
|
324
322
|
|
|
325
|
-
def _is_combo_potentially_ambiguous(self, option: str) ->
|
|
323
|
+
def _is_combo_potentially_ambiguous(self, option: str) -> bool | None:
|
|
326
324
|
# Called by short_option_to_param_value_pairs after ensuring the length is > 1
|
|
327
325
|
to_check = option[1:] # Strip leading '-'
|
|
328
326
|
# Note: len(to_check) will never be 2 here - this is only called if len(option) > 2
|
|
@@ -345,21 +343,34 @@ class CommandParameters:
|
|
|
345
343
|
|
|
346
344
|
# endregion
|
|
347
345
|
|
|
346
|
+
@cached_property
|
|
347
|
+
def nested_single_and_multi_char_short_options(self) -> tuple[OptionMap, OptionMap]:
|
|
348
|
+
single_char_shorts, multi_char_shorts = (xcs.copy() for xcs in self._classified_combo_options)
|
|
349
|
+
for params in self._iter_nested_params():
|
|
350
|
+
nested_single_char_shorts, nested_multi_char_shorts = params._classified_combo_options
|
|
351
|
+
single_char_shorts |= nested_single_char_shorts
|
|
352
|
+
multi_char_shorts |= nested_multi_char_shorts
|
|
353
|
+
|
|
354
|
+
return single_char_shorts, multi_char_shorts
|
|
355
|
+
|
|
348
356
|
def _iter_nested_params(self) -> Iterator[CommandParameters]:
|
|
349
357
|
if not self.sub_command:
|
|
350
358
|
return
|
|
359
|
+
|
|
351
360
|
get_params = self.command.__class__.params
|
|
361
|
+
seen = set()
|
|
352
362
|
for choice in self.sub_command.choices.values():
|
|
353
|
-
|
|
363
|
+
# choice.target is the (sub-)Command class that will be used if that choice was selected. Being None
|
|
364
|
+
# indicates it's a subcommand's local choice (i.e., get_params would return this CommandParameters object)
|
|
365
|
+
if choice.target is not None and choice.target not in seen:
|
|
366
|
+
seen.add(choice.target) # Some choices may be aliases for the same target Command
|
|
354
367
|
params: CommandParameters = get_params(choice.target)
|
|
355
368
|
yield params
|
|
356
369
|
yield from params._iter_nested_params()
|
|
357
370
|
|
|
358
371
|
# region Option Processing
|
|
359
372
|
|
|
360
|
-
def short_option_to_param_value_pairs(
|
|
361
|
-
self, option: str
|
|
362
|
-
) -> tuple[list[tuple[str, BaseOption, Optional[str]]], bool]:
|
|
373
|
+
def short_option_to_param_value_pairs(self, option: str) -> tuple[list[tuple[str, BaseOption, str | None]], bool]:
|
|
363
374
|
option, eq, value = option.partition('=')
|
|
364
375
|
if eq: # An `=` was present in the string
|
|
365
376
|
# Note: if the option is not in this Command's option_map, the KeyError is handled by CommandParser
|
|
@@ -393,6 +404,7 @@ class CommandParameters:
|
|
|
393
404
|
else:
|
|
394
405
|
yield from self.all_positionals
|
|
395
406
|
yield from self.options
|
|
407
|
+
|
|
396
408
|
if self.pass_thru and self.pass_thru not in exclude:
|
|
397
409
|
yield self.pass_thru
|
|
398
410
|
|
{cli_command_parser-2025.7.13 → cli_command_parser-2025.11.1}/lib/cli_command_parser/commands.py
RENAMED
|
@@ -9,18 +9,18 @@ from __future__ import annotations
|
|
|
9
9
|
import logging
|
|
10
10
|
from abc import ABC
|
|
11
11
|
from contextlib import ExitStack
|
|
12
|
-
from typing import TYPE_CHECKING,
|
|
12
|
+
from typing import TYPE_CHECKING, Sequence, TextIO, Type, overload
|
|
13
13
|
|
|
14
14
|
from .context import ActionPhase, Context, get_or_create_context
|
|
15
15
|
from .core import CommandMeta, get_params, get_top_level_commands
|
|
16
|
-
from .exceptions import ParamConflict
|
|
16
|
+
from .exceptions import ParamConflict, ParserExit
|
|
17
17
|
from .parser import parse_args_and_get_next_cmd
|
|
18
18
|
from .utils import maybe_await
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
21
21
|
from .typing import Bool, CommandObj
|
|
22
22
|
|
|
23
|
-
__all__ = ['Command', 'AsyncCommand', 'main']
|
|
23
|
+
__all__ = ['Command', 'AsyncCommand', 'main', 'print_help']
|
|
24
24
|
log = logging.getLogger(__name__)
|
|
25
25
|
|
|
26
26
|
Argv = Sequence[str]
|
|
@@ -50,13 +50,13 @@ class Command(ABC, metaclass=CommandMeta):
|
|
|
50
50
|
|
|
51
51
|
@classmethod
|
|
52
52
|
@overload
|
|
53
|
-
def parse_and_run(cls: Type[CommandObj], argv: Argv = None, **kwargs) ->
|
|
53
|
+
def parse_and_run(cls: Type[CommandObj], argv: Argv = None, **kwargs) -> CommandObj | None:
|
|
54
54
|
# These overloads indicate that an instance of the same type or another may be returned
|
|
55
55
|
...
|
|
56
56
|
|
|
57
57
|
@classmethod
|
|
58
58
|
@overload
|
|
59
|
-
def parse_and_run(cls, argv: Argv = None, **kwargs) ->
|
|
59
|
+
def parse_and_run(cls, argv: Argv = None, **kwargs) -> CommandObj | None: ...
|
|
60
60
|
|
|
61
61
|
@classmethod
|
|
62
62
|
def parse_and_run(cls, argv=None, **kwargs):
|
|
@@ -214,7 +214,7 @@ class Command(ABC, metaclass=CommandMeta):
|
|
|
214
214
|
"""
|
|
215
215
|
self._run_actions_(ActionPhase.BEFORE_MAIN, args, kwargs)
|
|
216
216
|
|
|
217
|
-
def main(self, *args, **kwargs) ->
|
|
217
|
+
def main(self, *args, **kwargs) -> int | None:
|
|
218
218
|
"""
|
|
219
219
|
Primary method that is called when running a Command.
|
|
220
220
|
|
|
@@ -340,7 +340,7 @@ class AsyncCommand(Command, ABC):
|
|
|
340
340
|
"""Asynchronous version of :meth:`Command._before_main_`."""
|
|
341
341
|
await self._run_actions_(ActionPhase.BEFORE_MAIN, args, kwargs)
|
|
342
342
|
|
|
343
|
-
async def main(self, *args, **kwargs) ->
|
|
343
|
+
async def main(self, *args, **kwargs) -> int | None:
|
|
344
344
|
"""Asynchronous version of :meth:`Command.main`."""
|
|
345
345
|
with self._Command__ctx as ctx: # noqa
|
|
346
346
|
action = get_params(self).action
|
|
@@ -355,7 +355,7 @@ class AsyncCommand(Command, ABC):
|
|
|
355
355
|
await self._run_actions_(ActionPhase.AFTER_MAIN, args, kwargs)
|
|
356
356
|
|
|
357
357
|
|
|
358
|
-
def main(argv: Argv = None, return_command: Bool = False, **kwargs) ->
|
|
358
|
+
def main(argv: Argv = None, return_command: Bool = False, **kwargs) -> CommandObj | None:
|
|
359
359
|
"""
|
|
360
360
|
Convenience function that can be used as the main entry point for a program.
|
|
361
361
|
|
|
@@ -389,3 +389,17 @@ def main(argv: Argv = None, return_command: Bool = False, **kwargs) -> Optional[
|
|
|
389
389
|
|
|
390
390
|
command = commands[0].parse_and_run(argv, **kwargs)
|
|
391
391
|
return command if return_command else None
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def print_help(command: Command, *, exit: bool = True, file: TextIO | None = None): # noqa
|
|
395
|
+
"""
|
|
396
|
+
User-callable version of the ``--help`` / ``-h`` action. Prints help text, then optionally exits.
|
|
397
|
+
|
|
398
|
+
:param command: The command for which help text should be printed.
|
|
399
|
+
:param exit: Whether a :class:`~.ParserExit` exception should be raised after help text is printed. If True (the
|
|
400
|
+
default), the program will exit with code 0 (success). If False, no exception will be raised.
|
|
401
|
+
:param file: The file-like object (stream) to write to; defaults to the current sys.stdout.
|
|
402
|
+
"""
|
|
403
|
+
print(get_params(command).formatter.format_help(), file=file)
|
|
404
|
+
if exit:
|
|
405
|
+
raise ParserExit
|
{cli_command_parser-2025.7.13 → cli_command_parser-2025.11.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.7.13 → cli_command_parser-2025.11.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)):
|