cli-command-parser 2023.4.10__tar.gz → 2023.4.16__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-2023.4.10 → cli_command_parser-2023.4.16}/PKG-INFO +18 -1
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/__init__.py +8 -1
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/__version__.py +1 -1
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/command_parameters.py +46 -32
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/commands.py +2 -2
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/config.py +31 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/context.py +8 -25
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/conversion/command_builder.py +24 -6
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/conversion/visitor.py +16 -14
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/core.py +9 -6
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/formatting/commands.py +2 -2
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/formatting/params.py +7 -8
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/metadata.py +94 -19
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/nargs.py +37 -14
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parameters/base.py +39 -21
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parameters/choice_map.py +24 -17
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parameters/groups.py +5 -1
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parameters/options.py +17 -8
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parameters/pass_thru.py +1 -1
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parameters/positionals.py +8 -1
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parse_tree.py +16 -21
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parser.py +80 -75
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/typing.py +2 -1
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/utils.py +2 -3
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser.egg-info/PKG-INFO +18 -1
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser.egg-info/requires.txt +3 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/readme.rst +17 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/setup.cfg +2 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/LICENSE +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/MANIFEST.in +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/__main__.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/actions.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/annotations.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/compat.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/conversion/__init__.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/conversion/__main__.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/conversion/argparse_ast.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/conversion/argparse_utils.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/conversion/utils.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/documentation.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/error_handling.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/exceptions.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/formatting/__init__.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/formatting/restructured_text.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/formatting/utils.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/inputs/__init__.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/inputs/base.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/inputs/choices.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/inputs/exceptions.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/inputs/files.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/inputs/numeric.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/inputs/time.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/inputs/utils.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parameters/__init__.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/parameters/option_strings.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/testing.py +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser.egg-info/SOURCES.txt +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser.egg-info/dependency_links.txt +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser.egg-info/top_level.txt +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/pyproject.toml +0 -0
- {cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/requirements-dev.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cli_command_parser
|
|
3
|
-
Version: 2023.4.
|
|
3
|
+
Version: 2023.4.16
|
|
4
4
|
Summary: CLI Command Parser
|
|
5
5
|
Home-page: https://github.com/dskrypa/cli_command_parser
|
|
6
6
|
Author: Doug Skrypa
|
|
@@ -111,6 +111,23 @@ with optional dependencies::
|
|
|
111
111
|
$ pip install -U cli-command-parser[wcwidth]
|
|
112
112
|
|
|
113
113
|
|
|
114
|
+
Python Version Compatibility
|
|
115
|
+
============================
|
|
116
|
+
|
|
117
|
+
Python versions 3.7 and above are currently supported. CLI Command Parser will no longer support 3.7 after 2023-04-30,
|
|
118
|
+
ahead of the `official end of support for 3.7 on 2023-06-27 <https://devguide.python.org/versions/>`__.
|
|
119
|
+
|
|
120
|
+
When using 3.7 or 3.8, some additional packages that backport functionality that was added in later Python versions
|
|
121
|
+
are required for compatibility.
|
|
122
|
+
|
|
123
|
+
To use the argparse to cli-command-parser conversion script with Python 3.7 or 3.8, there is a dependency on
|
|
124
|
+
`astunparse <https://astunparse.readthedocs.io>`__. If you are using Python 3.9 or above, then ``astunparse`` is not
|
|
125
|
+
necessary because the relevant code was added to the stdlib ``ast`` module. If you're unsure, you can install
|
|
126
|
+
cli-command-parser with the following command to automatically handle whether that extra dependency is needed or not::
|
|
127
|
+
|
|
128
|
+
$ pip install -U cli-command-parser[conversion]
|
|
129
|
+
|
|
130
|
+
|
|
114
131
|
Links
|
|
115
132
|
*****
|
|
116
133
|
|
{cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/__init__.py
RENAMED
|
@@ -4,7 +4,14 @@ Command Parser
|
|
|
4
4
|
:author: Doug Skrypa
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from .config import
|
|
7
|
+
from .config import (
|
|
8
|
+
CommandConfig,
|
|
9
|
+
ShowDefaults,
|
|
10
|
+
OptionNameMode,
|
|
11
|
+
SubcommandAliasHelpMode,
|
|
12
|
+
AmbiguousComboMode,
|
|
13
|
+
AllowLeadingDash,
|
|
14
|
+
)
|
|
8
15
|
from .commands import Command, main
|
|
9
16
|
from .context import Context, get_current_context, ctx, get_parsed, get_context, get_raw_arg
|
|
10
17
|
from .exceptions import (
|
{cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/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__ = '2023.04.
|
|
4
|
+
__version__ = '2023.04.16'
|
|
5
5
|
__author__ = 'Doug Skrypa'
|
|
6
6
|
__author_email__ = 'dskrypa@gmail.com'
|
|
7
7
|
__license__ = 'Apache 2.0'
|
|
@@ -50,6 +50,7 @@ class CommandParameters:
|
|
|
50
50
|
combo_option_map: OptionMap #: Mapping of {short opt: Parameter} (no dash characters)
|
|
51
51
|
groups: List[ParamGroup] #: List of ParamGroup objects
|
|
52
52
|
positionals: List[BasePositional] #: List of positional Parameters
|
|
53
|
+
_deferred_positionals: List[BasePositional] = () #: Positional Parameters that are deferred to sub commands
|
|
53
54
|
option_map: OptionMap #: Mapping of {--opt / -opt: Parameter}
|
|
54
55
|
|
|
55
56
|
def __init__(self, command: CommandCls, command_parent: Optional[CommandCls], config: CommandConfig):
|
|
@@ -75,6 +76,15 @@ class CommandParameters:
|
|
|
75
76
|
return self.parent.pass_thru
|
|
76
77
|
return None
|
|
77
78
|
|
|
79
|
+
@cached_property
|
|
80
|
+
def all_positionals(self) -> List[BasePositional]:
|
|
81
|
+
try:
|
|
82
|
+
if not self.parent.sub_command:
|
|
83
|
+
return self.parent.all_positionals + self.positionals
|
|
84
|
+
except AttributeError:
|
|
85
|
+
pass
|
|
86
|
+
return self.positionals
|
|
87
|
+
|
|
78
88
|
@cached_property
|
|
79
89
|
def always_available_action_flags(self) -> Tuple[ActionFlag, ...]:
|
|
80
90
|
"""
|
|
@@ -92,8 +102,8 @@ class CommandParameters:
|
|
|
92
102
|
else:
|
|
93
103
|
formatter_factory = self.config.command_formatter or CommandHelpFormatter
|
|
94
104
|
formatter = formatter_factory(self.command, self)
|
|
105
|
+
formatter.maybe_add_positionals(self.all_positionals)
|
|
95
106
|
formatter.maybe_add_option(self._pass_thru)
|
|
96
|
-
formatter.maybe_add_positionals(self.positionals)
|
|
97
107
|
formatter.maybe_add_options(self.options)
|
|
98
108
|
formatter.maybe_add_groups(self.groups)
|
|
99
109
|
return formatter
|
|
@@ -129,36 +139,38 @@ class CommandParameters:
|
|
|
129
139
|
|
|
130
140
|
# region Initialization
|
|
131
141
|
|
|
132
|
-
def
|
|
133
|
-
"""
|
|
134
|
-
Process all of the :class:`.Parameter` / :class:`.ParamGroup` members in the associated :class:`.Command` class.
|
|
135
|
-
"""
|
|
142
|
+
def _iter_parameters(self) -> Iterator[ParamBase]:
|
|
136
143
|
name_param_map = {} # Allow subclasses to override names, but not within a given command
|
|
137
|
-
positionals = []
|
|
138
|
-
options = []
|
|
139
|
-
groups = set()
|
|
140
|
-
|
|
141
144
|
for attr, param in self.command.__dict__.items():
|
|
142
145
|
if attr.startswith('__') or not isinstance(param, ParamBase): # Name mangled Parameters are still processed
|
|
143
146
|
continue
|
|
144
|
-
|
|
145
|
-
name = param.name
|
|
146
147
|
try:
|
|
147
|
-
other_attr, other_param = name_param_map[name]
|
|
148
|
+
other_attr, other_param = name_param_map[param.name]
|
|
148
149
|
except KeyError:
|
|
149
|
-
name_param_map[name] = (attr, param)
|
|
150
|
+
name_param_map[param.name] = (attr, param)
|
|
151
|
+
yield param
|
|
150
152
|
else:
|
|
151
153
|
raise CommandDefinitionError(
|
|
152
154
|
'Name conflict - multiple parameters within a Command cannot have the same name - conflicting'
|
|
153
155
|
f' params: {other_attr}={other_param}, {attr}={param}'
|
|
154
156
|
)
|
|
155
157
|
|
|
158
|
+
def _process_parameters(self):
|
|
159
|
+
"""
|
|
160
|
+
Process all of the :class:`.Parameter` / :class:`.ParamGroup` members in the associated :class:`.Command` class.
|
|
161
|
+
"""
|
|
162
|
+
positionals = []
|
|
163
|
+
options = []
|
|
164
|
+
groups = set()
|
|
165
|
+
|
|
166
|
+
for param in self._iter_parameters():
|
|
156
167
|
if isinstance(param, BasePositional):
|
|
157
168
|
positionals.append(param)
|
|
158
169
|
elif isinstance(param, BaseOption):
|
|
159
170
|
options.append(param)
|
|
160
171
|
elif isinstance(param, ParamGroup):
|
|
161
172
|
# Groups will only be discovered here when defined with `as` - ex: `with ParamGroup(...) as foo:`
|
|
173
|
+
# Group members will always be discovered at the top level since context managers share the outer scope
|
|
162
174
|
groups.add(param)
|
|
163
175
|
elif isinstance(param, PassThru):
|
|
164
176
|
if self.pass_thru:
|
|
@@ -190,36 +202,37 @@ class CommandParameters:
|
|
|
190
202
|
self.groups = sorted(groups)
|
|
191
203
|
|
|
192
204
|
def _process_positionals(self, params: List[BasePositional]):
|
|
193
|
-
var_nargs_param = None
|
|
194
|
-
for param in params:
|
|
195
|
-
if
|
|
196
|
-
raise CommandDefinitionError(
|
|
197
|
-
f'Positional param={param!r} may not follow the sub command {self.sub_command} - re-order the'
|
|
198
|
-
' positionals, move it into the sub command(s), or convert it to an optional parameter'
|
|
199
|
-
)
|
|
200
|
-
elif var_nargs_param:
|
|
205
|
+
var_nargs_param = action_or_sub_cmd = split_index = None
|
|
206
|
+
for i, param in enumerate(params):
|
|
207
|
+
if var_nargs_param:
|
|
201
208
|
raise CommandDefinitionError(
|
|
202
209
|
f'Additional Positional parameters cannot follow {var_nargs_param} because it accepts'
|
|
203
210
|
f' a variable number of arguments with no specific choices defined - param={param!r} is invalid'
|
|
204
211
|
)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if self.action: # self.sub_command being already defined is handled above
|
|
212
|
+
elif isinstance(param, (SubCommand, Action)):
|
|
213
|
+
if action_or_sub_cmd:
|
|
208
214
|
raise CommandDefinitionError(
|
|
209
215
|
f'Only 1 Action xor SubCommand is allowed in a given Command - {self.command.__name__} cannot'
|
|
210
|
-
f' contain both {
|
|
216
|
+
f' contain both {action_or_sub_cmd} and {param}'
|
|
211
217
|
)
|
|
212
218
|
elif isinstance(param, SubCommand):
|
|
213
|
-
self.sub_command = param
|
|
219
|
+
self.sub_command = action_or_sub_cmd = param
|
|
220
|
+
split_index = i + 1
|
|
214
221
|
else:
|
|
215
|
-
self.action = param
|
|
222
|
+
self.action = action_or_sub_cmd = param
|
|
216
223
|
if not param.has_choices:
|
|
217
224
|
raise CommandDefinitionError(f'No choices were registered for {self.action}')
|
|
218
|
-
|
|
219
|
-
if param.nargs.variable and not param.has_choices:
|
|
225
|
+
elif param.nargs.variable and not param.has_choices:
|
|
220
226
|
var_nargs_param = param
|
|
221
227
|
|
|
222
|
-
|
|
228
|
+
if split_index:
|
|
229
|
+
params, self._deferred_positionals = params[:split_index], params[split_index:]
|
|
230
|
+
|
|
231
|
+
parent = self.parent
|
|
232
|
+
if parent and parent._deferred_positionals:
|
|
233
|
+
self.positionals = parent._deferred_positionals + params
|
|
234
|
+
else:
|
|
235
|
+
self.positionals = params
|
|
223
236
|
|
|
224
237
|
def _process_options(self, params: Collection[BaseOption]):
|
|
225
238
|
parent = self.parent
|
|
@@ -277,7 +290,7 @@ class CommandParameters:
|
|
|
277
290
|
for param in action_flags:
|
|
278
291
|
if param.func is None:
|
|
279
292
|
raise ParameterDefinitionError(f'No function was registered for param={param!r}')
|
|
280
|
-
grouped_ordered_flags[param.before_main][param.order].append(param)
|
|
293
|
+
grouped_ordered_flags[param.before_main][param.order].append(param) # noqa # PyCharm infers the wrong type
|
|
281
294
|
|
|
282
295
|
found_non_always = False
|
|
283
296
|
invalid = {}
|
|
@@ -449,6 +462,7 @@ class CommandParameters:
|
|
|
449
462
|
raise exc
|
|
450
463
|
|
|
451
464
|
def try_env_params(self, ctx: Context) -> Iterator[Option]:
|
|
465
|
+
"""Yields Option parameters that have an environment variable configured, and did not have any CLI values."""
|
|
452
466
|
for param in self.options:
|
|
453
467
|
try:
|
|
454
468
|
param.env_var # noqa
|
|
@@ -460,7 +474,7 @@ class CommandParameters:
|
|
|
460
474
|
|
|
461
475
|
def required_check_params(self) -> Iterator[Parameter]:
|
|
462
476
|
ignore = SubCommand
|
|
463
|
-
yield from (p for p in self.
|
|
477
|
+
yield from (p for p in self.all_positionals if p.required and not p.group and not isinstance(p, ignore))
|
|
464
478
|
yield from (p for p in self.options if p.required and not p.group)
|
|
465
479
|
pass_thru = self._pass_thru
|
|
466
480
|
if pass_thru and pass_thru.required and not pass_thru.group:
|
{cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/commands.py
RENAMED
|
@@ -108,11 +108,11 @@ class Command(ABC, metaclass=CommandMeta):
|
|
|
108
108
|
cmd_cls = cls
|
|
109
109
|
with ExitStack() as stack:
|
|
110
110
|
stack.enter_context(ctx)
|
|
111
|
-
sub_cmd = CommandParser.
|
|
111
|
+
sub_cmd = CommandParser.parse_args_and_get_next_cmd(ctx)
|
|
112
112
|
while sub_cmd:
|
|
113
113
|
cmd_cls = sub_cmd
|
|
114
114
|
ctx = stack.enter_context(ctx._sub_context(cmd_cls))
|
|
115
|
-
sub_cmd = CommandParser.
|
|
115
|
+
sub_cmd = CommandParser.parse_args_and_get_next_cmd(ctx)
|
|
116
116
|
|
|
117
117
|
return cmd_cls()
|
|
118
118
|
|
{cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/config.py
RENAMED
|
@@ -24,6 +24,7 @@ __all__ = [
|
|
|
24
24
|
'OptionNameMode',
|
|
25
25
|
'SubcommandAliasHelpMode',
|
|
26
26
|
'AmbiguousComboMode',
|
|
27
|
+
'AllowLeadingDash',
|
|
27
28
|
'DEFAULT_CONFIG',
|
|
28
29
|
]
|
|
29
30
|
|
|
@@ -178,6 +179,36 @@ class AmbiguousComboMode(MissingMixin, Enum):
|
|
|
178
179
|
STRICT = 'strict' # Reject multi-char short options that overlap with a single char one before parsing
|
|
179
180
|
|
|
180
181
|
|
|
182
|
+
class AllowLeadingDash(Enum):
|
|
183
|
+
"""
|
|
184
|
+
How a given Parameter should handle values with a leading dash (``-``). Only configurable at the Parameter level,
|
|
185
|
+
not the Command level.
|
|
186
|
+
|
|
187
|
+
The behavior based on each supported option:
|
|
188
|
+
|
|
189
|
+
:NUMERIC: Allow numeric values like ``-5`` and ``-1.3``, but reject values like ``-d``.
|
|
190
|
+
:ALWAYS: Always allow values with a leading dash.
|
|
191
|
+
:NEVER: Never allow values with a leading dash.
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
NUMERIC = 'numeric' # Allow a leading dash when the value is numeric
|
|
195
|
+
ALWAYS = 'always' # Always allow a leading dash
|
|
196
|
+
NEVER = 'never' # Never allow a leading dash
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def _missing_(cls, value):
|
|
200
|
+
if isinstance(value, str):
|
|
201
|
+
try:
|
|
202
|
+
return cls._member_map_[value.upper()] # noqa
|
|
203
|
+
except KeyError:
|
|
204
|
+
pass
|
|
205
|
+
elif value is True:
|
|
206
|
+
return cls.ALWAYS
|
|
207
|
+
elif value is False:
|
|
208
|
+
return cls.NEVER
|
|
209
|
+
return super()._missing_(value) # noqa
|
|
210
|
+
|
|
211
|
+
|
|
181
212
|
# endregion
|
|
182
213
|
|
|
183
214
|
|
{cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/context.py
RENAMED
|
@@ -32,31 +32,12 @@ if TYPE_CHECKING:
|
|
|
32
32
|
from .parameters import Parameter, ActionFlag
|
|
33
33
|
from .typing import Bool, ParamOrGroup, CommandType, AnyConfig, OptStr, PathLike
|
|
34
34
|
|
|
35
|
-
__all__ = [
|
|
36
|
-
'Context',
|
|
37
|
-
'ctx',
|
|
38
|
-
'get_current_context',
|
|
39
|
-
'get_or_create_context',
|
|
40
|
-
'get_context',
|
|
41
|
-
'get_parsed',
|
|
42
|
-
'get_raw_arg',
|
|
43
|
-
'ParseState',
|
|
44
|
-
]
|
|
35
|
+
__all__ = ['Context', 'ctx', 'get_current_context', 'get_or_create_context', 'get_context', 'get_parsed', 'get_raw_arg']
|
|
45
36
|
|
|
46
37
|
_context_stack = ContextVar('cli_command_parser.context.stack', default=[])
|
|
47
38
|
_TERMINAL = Terminal()
|
|
48
39
|
|
|
49
40
|
|
|
50
|
-
class ParseState(Enum):
|
|
51
|
-
INITIAL = 1
|
|
52
|
-
COMPLETE = 2
|
|
53
|
-
FAILED = 3
|
|
54
|
-
|
|
55
|
-
@property
|
|
56
|
-
def done(self) -> bool:
|
|
57
|
-
return self._value_ > 1
|
|
58
|
-
|
|
59
|
-
|
|
60
41
|
class Context(AbstractContextManager): # Extending AbstractContextManager to make PyCharm's type checker happy
|
|
61
42
|
"""
|
|
62
43
|
The parsing context.
|
|
@@ -69,6 +50,7 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
|
|
|
69
50
|
prog: OptStr = None
|
|
70
51
|
_terminal_width: Optional[int]
|
|
71
52
|
allow_argv_prog: Bool = True
|
|
53
|
+
_provided: Dict[ParamOrGroup, int]
|
|
72
54
|
|
|
73
55
|
def __init__(
|
|
74
56
|
self,
|
|
@@ -82,7 +64,6 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
|
|
|
82
64
|
):
|
|
83
65
|
self.command = command
|
|
84
66
|
self.parent = parent
|
|
85
|
-
self.state = ParseState.INITIAL
|
|
86
67
|
self.config = _normalize_config(config, kwargs, parent, command)
|
|
87
68
|
if parent is not None:
|
|
88
69
|
self._set_argv(parent.prog, argv)
|
|
@@ -128,8 +109,7 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
|
|
|
128
109
|
return self.__class__(argv, command, parent=self, **kwargs)
|
|
129
110
|
|
|
130
111
|
def __repr__(self) -> str:
|
|
131
|
-
|
|
132
|
-
return f'<{self.__class__.__name__}[state={self.state}, command={cmd_name}]>'
|
|
112
|
+
return f'<{self.__class__.__name__}[command={getattr(self.command, "__name__", None)}]>'
|
|
133
113
|
|
|
134
114
|
def __enter__(self) -> Context:
|
|
135
115
|
_context_stack.get().append(self)
|
|
@@ -182,7 +162,7 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
|
|
|
182
162
|
|
|
183
163
|
params = self.params
|
|
184
164
|
if params:
|
|
185
|
-
for group in (params.
|
|
165
|
+
for group in (params.all_positionals, params.options, (params.pass_thru,)):
|
|
186
166
|
for param in group:
|
|
187
167
|
if param and param not in exclude:
|
|
188
168
|
try:
|
|
@@ -220,7 +200,7 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
|
|
|
220
200
|
try:
|
|
221
201
|
return self._parsed[param]
|
|
222
202
|
except KeyError:
|
|
223
|
-
self._parsed[param] = value = param._init_value_factory(
|
|
203
|
+
self._parsed[param] = value = param._init_value_factory()
|
|
224
204
|
return value
|
|
225
205
|
|
|
226
206
|
def set_parsed_value(self, param: Parameter, value: Any):
|
|
@@ -237,6 +217,9 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
|
|
|
237
217
|
"""Not intended to be called by users. Used by Parameters during parsing to handle nargs."""
|
|
238
218
|
return self._provided[param]
|
|
239
219
|
|
|
220
|
+
def get_missing(self) -> List[Parameter]:
|
|
221
|
+
return [p for p in self.params.required_check_params() if self._provided[p] == 0]
|
|
222
|
+
|
|
240
223
|
# endregion
|
|
241
224
|
|
|
242
225
|
# region Actions
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import keyword
|
|
4
4
|
import logging
|
|
5
5
|
from abc import ABC, abstractmethod
|
|
6
|
-
from ast import literal_eval, Attribute, Name, GeneratorExp, Subscript, DictComp, ListComp, SetComp
|
|
6
|
+
from ast import literal_eval, Attribute, Name, GeneratorExp, Subscript, DictComp, ListComp, SetComp, Constant, Str
|
|
7
7
|
from dataclasses import dataclass, fields
|
|
8
8
|
from itertools import count
|
|
9
9
|
from typing import TYPE_CHECKING, Union, Optional, Iterator, Iterable, Type, TypeVar, Generic, List, Tuple
|
|
@@ -31,13 +31,16 @@ def convert_script(script: Script, add_methods: bool = False) -> str:
|
|
|
31
31
|
|
|
32
32
|
class Converter(Generic[AC], ABC):
|
|
33
33
|
converts: Type[AC] = None
|
|
34
|
+
newline_between_members: bool = False
|
|
34
35
|
_ac_converter_map = {}
|
|
35
36
|
|
|
36
|
-
def __init_subclass__(cls, converts: Type[AC] = None, **kwargs):
|
|
37
|
+
def __init_subclass__(cls, converts: Type[AC] = None, newline_between_members: bool = None, **kwargs):
|
|
37
38
|
super().__init_subclass__(**kwargs)
|
|
38
39
|
if converts:
|
|
39
40
|
cls.converts = converts
|
|
40
41
|
cls._ac_converter_map[converts] = cls
|
|
42
|
+
if newline_between_members is not None:
|
|
43
|
+
cls.newline_between_members = newline_between_members
|
|
41
44
|
|
|
42
45
|
def __init__(self, ast_obj: Union[AC, Script], parent: Optional[Converter] = None):
|
|
43
46
|
self.ast_obj = ast_obj
|
|
@@ -90,7 +93,10 @@ class ConverterGroup(Generic[C]):
|
|
|
90
93
|
yield from self.members
|
|
91
94
|
|
|
92
95
|
def format_all(self, indent: int = 0) -> Iterator[str]:
|
|
93
|
-
|
|
96
|
+
newline_between_members = self.member_type.newline_between_members
|
|
97
|
+
for i, member in enumerate(self.members):
|
|
98
|
+
if i and newline_between_members:
|
|
99
|
+
yield ''
|
|
94
100
|
yield from member.format_lines(indent)
|
|
95
101
|
|
|
96
102
|
|
|
@@ -284,7 +290,7 @@ class ParserConverter(CollectionConverter[AstArgumentParser], converts=AstArgume
|
|
|
284
290
|
# endregion
|
|
285
291
|
|
|
286
292
|
|
|
287
|
-
class GroupConverter(CollectionConverter[ArgGroup], converts=ArgGroup):
|
|
293
|
+
class GroupConverter(CollectionConverter[ArgGroup], converts=ArgGroup, newline_between_members=True):
|
|
288
294
|
ast_obj: ArgGroup
|
|
289
295
|
|
|
290
296
|
def format_lines(self, indent: int = 4) -> Iterator[str]:
|
|
@@ -364,6 +370,10 @@ class ParamConverter(Converter[ParserArg], converts=ParserArg):
|
|
|
364
370
|
return next(name for name in self._attr_name_candidates() if name not in RESERVED)
|
|
365
371
|
|
|
366
372
|
def _attr_name_candidates(self) -> Iterator[str]:
|
|
373
|
+
dest = self.ast_obj.init_func_raw_kwargs.get('dest')
|
|
374
|
+
if dest is not None and isinstance(dest, (Constant, Str)): # Str is for 3.7 compatibility
|
|
375
|
+
yield getattr(dest, dest._fields[0]) # .value for Constant, .s for Str
|
|
376
|
+
|
|
367
377
|
long, short, plain = self._grouped_opt_strs
|
|
368
378
|
if self.is_positional or self.is_pass_thru:
|
|
369
379
|
yield from plain
|
|
@@ -439,6 +449,7 @@ class ParamConverter(Converter[ParserArg], converts=ParserArg):
|
|
|
439
449
|
nargs = self.ast_obj.init_func_kwargs.get('nargs')
|
|
440
450
|
if not nargs:
|
|
441
451
|
return False
|
|
452
|
+
# TODO: Refactor to take advantage of new nargs=REMAINDER support
|
|
442
453
|
return nargs in self.ast_obj.get_tracked_refs('argparse', 'REMAINDER', ())
|
|
443
454
|
|
|
444
455
|
@cached_property
|
|
@@ -637,9 +648,16 @@ class FlagArgs(OptionArgs):
|
|
|
637
648
|
action = None
|
|
638
649
|
else:
|
|
639
650
|
if default == opposite:
|
|
640
|
-
|
|
651
|
+
const = None
|
|
652
|
+
if action == 'store_true':
|
|
653
|
+
default = None
|
|
654
|
+
elif not default and action == 'store_false':
|
|
655
|
+
default = 'True'
|
|
656
|
+
const = None
|
|
657
|
+
else:
|
|
658
|
+
const = value if default else None
|
|
659
|
+
|
|
641
660
|
action = None
|
|
642
|
-
const = value if default else None
|
|
643
661
|
|
|
644
662
|
kwargs['type'] = kwargs['nargs'] = None
|
|
645
663
|
if action:
|
|
@@ -137,20 +137,22 @@ class ScriptVisitor(NodeVisitor):
|
|
|
137
137
|
# endregion
|
|
138
138
|
|
|
139
139
|
def resolve_ref(self, name: Union[str, AST, Attribute, Name, expr]):
|
|
140
|
-
if
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
140
|
+
if isinstance(name, Attribute) and isinstance(name.value, Call):
|
|
141
|
+
obj = self.visit_Call(name.value)
|
|
142
|
+
attr = name.attr
|
|
143
|
+
else:
|
|
144
|
+
if not isinstance(name, str):
|
|
145
|
+
name = get_name_repr(name)
|
|
146
|
+
try:
|
|
147
|
+
return self.scopes[name]
|
|
148
|
+
except KeyError:
|
|
149
|
+
pass
|
|
150
|
+
try:
|
|
151
|
+
obj_name, attr = name.rsplit('.', 1)
|
|
152
|
+
obj = self.scopes[obj_name]
|
|
153
|
+
except (ValueError, KeyError):
|
|
154
|
+
return None
|
|
155
|
+
|
|
154
156
|
try:
|
|
155
157
|
can_call = attr in obj.visit_funcs
|
|
156
158
|
except (AttributeError, TypeError):
|
{cli_command_parser-2023.4.10 → cli_command_parser-2023.4.16}/lib/cli_command_parser/core.py
RENAMED
|
@@ -162,12 +162,7 @@ class CommandMeta(ABCMeta, type):
|
|
|
162
162
|
else:
|
|
163
163
|
return first if include_abc else parent
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
mro = type.mro(cls)[1:]
|
|
167
|
-
except TypeError: # a Command object was provided instead of a Command class
|
|
168
|
-
cls = cls.__class__
|
|
169
|
-
mro = type.mro(cls)[1:]
|
|
170
|
-
|
|
165
|
+
cls, mro = _mro(cls)
|
|
171
166
|
first = parent = None
|
|
172
167
|
for parent_cls in mro:
|
|
173
168
|
if isinstance(parent_cls, mcs):
|
|
@@ -200,6 +195,14 @@ class CommandMeta(ABCMeta, type):
|
|
|
200
195
|
return meta
|
|
201
196
|
|
|
202
197
|
|
|
198
|
+
def _mro(cmd_cls):
|
|
199
|
+
try:
|
|
200
|
+
return cmd_cls, type.mro(cmd_cls)[1:-1] # 0 is always the class itself, -1 is always object
|
|
201
|
+
except TypeError: # a Command object was provided instead of a Command class
|
|
202
|
+
cmd_cls = cmd_cls.__class__
|
|
203
|
+
return cmd_cls, type.mro(cmd_cls)[1:-1]
|
|
204
|
+
|
|
205
|
+
|
|
203
206
|
def _choice_items(choice: OptStr, choices: Optional[Choices]) -> Sequence[Tuple[OptStr, OptStr]]:
|
|
204
207
|
if not choices:
|
|
205
208
|
return ((choice, None),) # noqa
|
|
@@ -57,7 +57,7 @@ class CommandHelpFormatter:
|
|
|
57
57
|
if meta.usage:
|
|
58
58
|
return meta.usage
|
|
59
59
|
|
|
60
|
-
params = self.params.
|
|
60
|
+
params = self.params.all_positionals + self.params.options # noqa
|
|
61
61
|
pass_thru = self.params.pass_thru
|
|
62
62
|
if pass_thru is not None:
|
|
63
63
|
params.append(pass_thru)
|
|
@@ -155,7 +155,7 @@ def get_formatter(command: CommandAny) -> CommandHelpFormatter:
|
|
|
155
155
|
|
|
156
156
|
def get_usage_sub_cmds(command: CommandCls):
|
|
157
157
|
cmd_mcs: Type[CommandMeta] = command.__class__ # Using metaclass to avoid potentially overwritten attrs
|
|
158
|
-
parent: CommandType = cmd_mcs.parent(command)
|
|
158
|
+
parent: CommandType = cmd_mcs.parent(command, False)
|
|
159
159
|
if not parent:
|
|
160
160
|
return []
|
|
161
161
|
|
|
@@ -405,7 +405,6 @@ class PassThruHelpFormatter(ParamHelpFormatter, param_cls=PassThru):
|
|
|
405
405
|
|
|
406
406
|
class GroupHelpFormatter(ParamHelpFormatter, param_cls=ParamGroup): # noqa # pylint: disable=W0223
|
|
407
407
|
required_formatter_map: BoolFormatterMap = {True: '{{{}}}'.format, False: '[{}]'.format}
|
|
408
|
-
# TODO: #18 Group order changes between invocations - should be sorted as declared or alphanumerically (config?)
|
|
409
408
|
|
|
410
409
|
def _get_choice_delim(self) -> str:
|
|
411
410
|
param: ParamGroup = self.param
|
|
@@ -425,16 +424,16 @@ class GroupHelpFormatter(ParamHelpFormatter, param_cls=ParamGroup): # noqa # p
|
|
|
425
424
|
if description:
|
|
426
425
|
return description
|
|
427
426
|
group = self.param
|
|
428
|
-
if
|
|
429
|
-
if ctx.config.show_group_type and (group.mutually_exclusive or group.mutually_dependent):
|
|
430
|
-
return 'Mutually {} options'.format('exclusive' if group.mutually_exclusive else 'dependent')
|
|
431
|
-
else:
|
|
432
|
-
return 'Optional arguments'
|
|
433
|
-
else:
|
|
427
|
+
if group.description or group._name:
|
|
434
428
|
description = group.description or f'{group.name} options'
|
|
435
429
|
if ctx.config.show_group_type and (group.mutually_exclusive or group.mutually_dependent):
|
|
436
|
-
description += ' (mutually {
|
|
430
|
+
description += f' (mutually {"exclusive" if group.mutually_exclusive else "dependent"})'
|
|
437
431
|
return description
|
|
432
|
+
elif ctx.config.show_group_type and (group.mutually_exclusive or group.mutually_dependent):
|
|
433
|
+
return f'Mutually {"exclusive" if group.mutually_exclusive else "dependent"} options'
|
|
434
|
+
|
|
435
|
+
adjective = 'Required' if group.required else 'Other' if group.contains_required else 'Optional'
|
|
436
|
+
return f'{adjective} arguments'
|
|
438
437
|
|
|
439
438
|
def _get_spacer(self) -> str:
|
|
440
439
|
group = self.param
|