cli-command-parser 2023.10.14__tar.gz → 2024.4.20__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.10.14 → cli_command_parser-2024.4.20}/PKG-INFO +3 -1
- cli_command_parser-2024.4.20/entry_points.txt +2 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/__init__.py +1 -1
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/__version__.py +1 -1
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/annotations.py +4 -1
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/command_parameters.py +7 -7
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/commands.py +127 -16
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/config.py +2 -2
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/context.py +7 -7
- cli_command_parser-2024.4.20/lib/cli_command_parser/conversion/cli.py +56 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/conversion/visitor.py +28 -6
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/exceptions.py +1 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/formatting/commands.py +6 -7
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/formatting/params.py +1 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/inputs/choices.py +1 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/inputs/files.py +3 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/inputs/patterns.py +2 -2
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/inputs/time.py +6 -6
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/metadata.py +108 -11
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/parameters/base.py +23 -19
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/parameters/choice_map.py +7 -9
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/parameters/groups.py +8 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/parameters/option_strings.py +3 -3
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/parameters/options.py +1 -7
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/parser.py +12 -9
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/testing.py +6 -8
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/utils.py +10 -2
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser.egg-info/PKG-INFO +4 -2
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser.egg-info/SOURCES.txt +3 -0
- cli_command_parser-2024.4.20/lib/cli_command_parser.egg-info/entry_points.txt +2 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/setup.cfg +3 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/LICENSE +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/MANIFEST.in +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/__main__.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/compat.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/conversion/__init__.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/conversion/__main__.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/conversion/argparse_ast.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/conversion/argparse_utils.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/conversion/command_builder.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/conversion/utils.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/core.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/documentation.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/error_handling.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/formatting/__init__.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/formatting/restructured_text.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/formatting/utils.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/inputs/__init__.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/inputs/base.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/inputs/exceptions.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/inputs/numeric.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/inputs/utils.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/nargs.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/parameters/__init__.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/parameters/actions.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/parameters/pass_thru.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/parameters/positionals.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/parse_tree.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/typing.py +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser.egg-info/dependency_links.txt +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser.egg-info/requires.txt +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser.egg-info/top_level.txt +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/pyproject.toml +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/readme.rst +0 -0
- {cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/requirements-dev.txt +0 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: cli_command_parser
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2024.4.20
|
|
4
4
|
Summary: CLI Command Parser
|
|
5
5
|
Home-page: https://github.com/dskrypa/cli_command_parser
|
|
6
6
|
Author: Doug Skrypa
|
|
7
7
|
Author-email: dskrypa@gmail.com
|
|
8
8
|
License: Apache 2.0
|
|
9
9
|
Project-URL: Source, https://github.com/dskrypa/cli_command_parser
|
|
10
|
+
Project-URL: Documentation, https://dskrypa.github.io/cli_command_parser
|
|
11
|
+
Project-URL: Issues, https://github.com/dskrypa/cli_command_parser/issues
|
|
10
12
|
Classifier: Development Status :: 5 - Production/Stable
|
|
11
13
|
Classifier: Environment :: Console
|
|
12
14
|
Classifier: Intended Audience :: Developers
|
{cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/__init__.py
RENAMED
|
@@ -12,7 +12,7 @@ from .config import (
|
|
|
12
12
|
AmbiguousComboMode,
|
|
13
13
|
AllowLeadingDash,
|
|
14
14
|
)
|
|
15
|
-
from .commands import Command, main
|
|
15
|
+
from .commands import Command, AsyncCommand, main
|
|
16
16
|
from .context import Context, get_current_context, ctx, get_parsed, get_context, get_raw_arg
|
|
17
17
|
from .exceptions import (
|
|
18
18
|
CommandParserException,
|
{cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/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__ = '2024.04.20'
|
|
5
5
|
__author__ = 'Doug Skrypa'
|
|
6
6
|
__author_email__ = 'dskrypa@gmail.com'
|
|
7
7
|
__license__ = 'Apache 2.0'
|
{cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/annotations.py
RENAMED
|
@@ -17,11 +17,14 @@ __all__ = ['get_descriptor_value_type']
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def get_descriptor_value_type(command_cls: type, attr: str) -> Optional[type]:
|
|
20
|
+
# TODO: Optimize this to cache get_type_hints for a given class?
|
|
20
21
|
try:
|
|
21
22
|
annotation = get_type_hints(command_cls)[attr]
|
|
22
23
|
except (KeyError, NameError): # KeyError due to attr missing; NameError for forward references
|
|
23
24
|
return None
|
|
24
|
-
|
|
25
|
+
# Note: `inspect.get_annotations(obj)` returns a dict of where values are the string representations of the
|
|
26
|
+
# discovered annotations; values in the dict returned by `typing.get_type_hints` are the actual classes / typing
|
|
27
|
+
# aliases that were used, which are significantly more useful for this analysis.
|
|
25
28
|
return get_annotation_value_type(annotation)
|
|
26
29
|
|
|
27
30
|
|
|
@@ -92,10 +92,10 @@ class CommandParameters:
|
|
|
92
92
|
return self.positionals
|
|
93
93
|
|
|
94
94
|
def get_positionals_to_parse(self, ctx: Context) -> List[BasePositional]:
|
|
95
|
-
if
|
|
96
|
-
for i, param in enumerate(
|
|
95
|
+
if self.all_positionals:
|
|
96
|
+
for i, param in enumerate(self.all_positionals):
|
|
97
97
|
if not ctx.num_provided(param):
|
|
98
|
-
return [p for p in
|
|
98
|
+
return [p for p in self.all_positionals[i:]]
|
|
99
99
|
|
|
100
100
|
return []
|
|
101
101
|
|
|
@@ -391,15 +391,15 @@ class CommandParameters:
|
|
|
391
391
|
else:
|
|
392
392
|
yield from self.all_positionals
|
|
393
393
|
yield from self.options
|
|
394
|
-
if
|
|
395
|
-
yield pass_thru
|
|
394
|
+
if self.pass_thru and self.pass_thru not in exclude:
|
|
395
|
+
yield self.pass_thru
|
|
396
396
|
|
|
397
397
|
def required_check_params(self) -> Iterator[Parameter]:
|
|
398
398
|
ignore = SubCommand
|
|
399
399
|
yield from (p for p in self.all_positionals if p.required and not p.group and not isinstance(p, ignore))
|
|
400
400
|
yield from (p for p in self.options if p.required and not p.group)
|
|
401
|
-
if
|
|
402
|
-
yield
|
|
401
|
+
if self._pass_thru and self._pass_thru.required and not self._pass_thru.group:
|
|
402
|
+
yield self._pass_thru
|
|
403
403
|
|
|
404
404
|
|
|
405
405
|
def _find_ambiguous_combos(
|
{cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/commands.py
RENAMED
|
@@ -15,6 +15,7 @@ from .core import CommandMeta, get_top_level_commands, get_params
|
|
|
15
15
|
from .context import Context, ActionPhase, get_or_create_context
|
|
16
16
|
from .exceptions import ParamConflict
|
|
17
17
|
from .parser import parse_args_and_get_next_cmd
|
|
18
|
+
from .utils import maybe_await
|
|
18
19
|
|
|
19
20
|
if TYPE_CHECKING:
|
|
20
21
|
from .typing import Bool, CommandObj
|
|
@@ -28,6 +29,9 @@ Argv = Sequence[str]
|
|
|
28
29
|
class Command(ABC, metaclass=CommandMeta):
|
|
29
30
|
"""The main class that other Commands should extend."""
|
|
30
31
|
|
|
32
|
+
# TODO: Make the distinction between help/description clearer, or merge them?
|
|
33
|
+
# TODO: Pull help text from docstring for subcommands if not specified as help=?
|
|
34
|
+
|
|
31
35
|
#: The parsing Context used for this Command. Provided here for convenience - this reference to it is not used by
|
|
32
36
|
#: any CLI Command Parser internals, so it is safe for subclasses to redefine / overwrite it.
|
|
33
37
|
ctx: Context
|
|
@@ -155,17 +159,7 @@ class Command(ABC, metaclass=CommandMeta):
|
|
|
155
159
|
|
|
156
160
|
return ctx.actions_taken
|
|
157
161
|
|
|
158
|
-
def
|
|
159
|
-
"""
|
|
160
|
-
The first method called by :meth:`.__call__` (before :meth:`.main` and others).
|
|
161
|
-
|
|
162
|
-
Validates the number of ActionFlags that were specified, and calls all of the specified
|
|
163
|
-
:func:`~.options.before_main` / :obj:`~.options.action_flag` actions such as ``--help`` that were
|
|
164
|
-
defined with ``before_main=True`` and ``always_available=True`` in their configured order.
|
|
165
|
-
|
|
166
|
-
:param args: Positional arguments to pass to the :obj:`~.options.action_flag` methods
|
|
167
|
-
:param kwargs: Keyword arguments to pass to the :obj:`~.options.action_flag` methods
|
|
168
|
-
"""
|
|
162
|
+
def _check_param_conflicts_(self):
|
|
169
163
|
# TODO: --help should take precedence over input validation - right now, if a Path input expecting a
|
|
170
164
|
# non-existent file receives a file that exists, that error is reported instead of showing help text
|
|
171
165
|
ctx = self.__ctx
|
|
@@ -178,9 +172,24 @@ class Command(ABC, metaclass=CommandMeta):
|
|
|
178
172
|
if action is not None and not ctx.config.action_after_action_flags:
|
|
179
173
|
raise ParamConflict([action, *before], 'combining an action with action flags is disabled')
|
|
180
174
|
|
|
181
|
-
|
|
175
|
+
def _run_actions_(self, phase: ActionPhase, args: tuple, kwargs: dict):
|
|
176
|
+
for param in self.__ctx.iter_action_flags(phase):
|
|
182
177
|
param.func(self, *args, **kwargs)
|
|
183
178
|
|
|
179
|
+
def _pre_init_actions_(self, *args, **kwargs):
|
|
180
|
+
"""
|
|
181
|
+
The first method called by :meth:`.__call__` (before :meth:`.main` and others).
|
|
182
|
+
|
|
183
|
+
Validates the number of ActionFlags that were specified, and calls all of the specified
|
|
184
|
+
:func:`~.options.before_main` / :obj:`~.options.action_flag` actions such as ``--help`` that were
|
|
185
|
+
defined with ``before_main=True`` and ``always_available=True`` in their configured order.
|
|
186
|
+
|
|
187
|
+
:param args: Positional arguments to pass to the :obj:`~.options.action_flag` methods
|
|
188
|
+
:param kwargs: Keyword arguments to pass to the :obj:`~.options.action_flag` methods
|
|
189
|
+
"""
|
|
190
|
+
self._check_param_conflicts_()
|
|
191
|
+
self._run_actions_(ActionPhase.PRE_INIT, args, kwargs)
|
|
192
|
+
|
|
184
193
|
def _init_command_(self, *args, **kwargs):
|
|
185
194
|
"""
|
|
186
195
|
Called by :meth:`.__call__` after :meth:`._pre_init_actions_` and before :meth:`._before_main_`.
|
|
@@ -208,8 +217,7 @@ class Command(ABC, metaclass=CommandMeta):
|
|
|
208
217
|
:param args: Positional arguments to pass to the :obj:`~.options.action_flag` methods
|
|
209
218
|
:param kwargs: Keyword arguments to pass to the :obj:`~.options.action_flag` methods
|
|
210
219
|
"""
|
|
211
|
-
|
|
212
|
-
param.func(self, *args, **kwargs)
|
|
220
|
+
self._run_actions_(ActionPhase.BEFORE_MAIN, args, kwargs)
|
|
213
221
|
|
|
214
222
|
def main(self, *args, **kwargs) -> Optional[int]:
|
|
215
223
|
"""
|
|
@@ -245,8 +253,111 @@ class Command(ABC, metaclass=CommandMeta):
|
|
|
245
253
|
:param args: Positional arguments to pass to the :obj:`~.options.action_flag` methods
|
|
246
254
|
:param kwargs: Keyword arguments to pass to the :obj:`~.options.action_flag` methods
|
|
247
255
|
"""
|
|
248
|
-
|
|
249
|
-
|
|
256
|
+
self._run_actions_(ActionPhase.AFTER_MAIN, args, kwargs)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class AsyncCommand(Command, ABC):
|
|
260
|
+
"""
|
|
261
|
+
Asynchronous version of the main class that other Commands should extend.
|
|
262
|
+
|
|
263
|
+
To run an AsyncCommand, both :func:`main` and :meth:`.parse_and_run` can be used as if running a synchronous
|
|
264
|
+
:class:`Command`. The asynchronous version of :meth:`.parse_and_run` handles calling :func:`python:asyncio.run`.
|
|
265
|
+
|
|
266
|
+
For applications that need more direct control over how the event loop is run, :meth:`.parse_and_await` can be
|
|
267
|
+
used instead.
|
|
268
|
+
|
|
269
|
+
All `_sunder_` methods supported by :class:`Command` may be overridden with either synchronous or async versions,
|
|
270
|
+
and :class:`~.choice_map.Action` target methods may similarly be defined either way as well.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
@classmethod
|
|
274
|
+
def parse_and_run(cls, argv=None, **kwargs):
|
|
275
|
+
"""
|
|
276
|
+
Asynchronous version of :meth:`Command.parse_and_run`. Argument parsing is handled synchronously, then
|
|
277
|
+
:func:`python:asyncio.run` is called with the parsed command's :meth:`.__call__` coroutine.
|
|
278
|
+
|
|
279
|
+
For applications that need more direct control over how the event loop is run, :meth:`.parse_and_await` can be
|
|
280
|
+
used instead.
|
|
281
|
+
"""
|
|
282
|
+
import asyncio
|
|
283
|
+
|
|
284
|
+
ctx = get_or_create_context(cls, argv)
|
|
285
|
+
with ctx.get_error_handler():
|
|
286
|
+
self = cls.parse(argv)
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
self
|
|
290
|
+
except UnboundLocalError: # There was an error handled during parsing, so self was not defined
|
|
291
|
+
return None
|
|
292
|
+
else:
|
|
293
|
+
asyncio.run(self(**kwargs))
|
|
294
|
+
return self
|
|
295
|
+
|
|
296
|
+
@classmethod
|
|
297
|
+
async def parse_and_await(cls, argv=None, **kwargs):
|
|
298
|
+
"""
|
|
299
|
+
Coroutine alternative to :meth:`.parse_and_run`. This method does NOT call :func:`python:asyncio.run` - it is
|
|
300
|
+
meant to be used as ``await MyCommand.parse_and_await()`` with an existing event loop.
|
|
301
|
+
|
|
302
|
+
Simpler applications can likely use the easier :func:`main` function or :meth:`.parse_and_run` instead.
|
|
303
|
+
"""
|
|
304
|
+
ctx = get_or_create_context(cls, argv)
|
|
305
|
+
with ctx.get_error_handler():
|
|
306
|
+
self = cls.parse(argv)
|
|
307
|
+
|
|
308
|
+
try:
|
|
309
|
+
self
|
|
310
|
+
except UnboundLocalError: # There was an error handled during parsing, so self was not defined
|
|
311
|
+
return None
|
|
312
|
+
else:
|
|
313
|
+
await maybe_await(self(**kwargs))
|
|
314
|
+
return self
|
|
315
|
+
|
|
316
|
+
async def __call__(self, *args, **kwargs) -> int:
|
|
317
|
+
"""Asynchronous version of :meth:`Command.__call__`."""
|
|
318
|
+
with self._Command__ctx as ctx, ctx.get_error_handler(): # noqa
|
|
319
|
+
await maybe_await(self._pre_init_actions_(*args, **kwargs))
|
|
320
|
+
await maybe_await(self._init_command_(*args, **kwargs))
|
|
321
|
+
await maybe_await(self._before_main_(*args, **kwargs))
|
|
322
|
+
try:
|
|
323
|
+
await maybe_await(self.main(*args, **kwargs))
|
|
324
|
+
except BaseException:
|
|
325
|
+
if ctx.config.always_run_after_main:
|
|
326
|
+
log.debug('Caught exception - running _after_main_ before propagating', exc_info=True)
|
|
327
|
+
await maybe_await(self._after_main_(*args, **kwargs))
|
|
328
|
+
raise
|
|
329
|
+
else:
|
|
330
|
+
await maybe_await(self._after_main_(*args, **kwargs))
|
|
331
|
+
|
|
332
|
+
return ctx.actions_taken
|
|
333
|
+
|
|
334
|
+
async def _run_actions_(self, phase: ActionPhase, args: tuple, kwargs: dict):
|
|
335
|
+
"""Asynchronous version of :meth:`Command._run_actions_`."""
|
|
336
|
+
for param in self._Command__ctx.iter_action_flags(phase): # noqa
|
|
337
|
+
await maybe_await(param.func(self, *args, **kwargs))
|
|
338
|
+
|
|
339
|
+
async def _pre_init_actions_(self, *args, **kwargs):
|
|
340
|
+
"""Asynchronous version of :meth:`Command._pre_init_actions_`."""
|
|
341
|
+
self._check_param_conflicts_()
|
|
342
|
+
await self._run_actions_(ActionPhase.PRE_INIT, args, kwargs)
|
|
343
|
+
|
|
344
|
+
async def _before_main_(self, *args, **kwargs):
|
|
345
|
+
"""Asynchronous version of :meth:`Command._before_main_`."""
|
|
346
|
+
await self._run_actions_(ActionPhase.BEFORE_MAIN, args, kwargs)
|
|
347
|
+
|
|
348
|
+
async def main(self, *args, **kwargs) -> Optional[int]:
|
|
349
|
+
"""Asynchronous version of :meth:`Command.main`."""
|
|
350
|
+
with self._Command__ctx as ctx: # noqa
|
|
351
|
+
action = get_params(self).action
|
|
352
|
+
if action is not None and (ctx.actions_taken == 0 or ctx.config.action_after_action_flags):
|
|
353
|
+
ctx.actions_taken += 1
|
|
354
|
+
await maybe_await(action.target()(self, *args, **kwargs))
|
|
355
|
+
|
|
356
|
+
return ctx.actions_taken
|
|
357
|
+
|
|
358
|
+
async def _after_main_(self, *args, **kwargs):
|
|
359
|
+
"""Asynchronous version of :meth:`Command._after_main_`."""
|
|
360
|
+
await self._run_actions_(ActionPhase.AFTER_MAIN, args, kwargs)
|
|
250
361
|
|
|
251
362
|
|
|
252
363
|
def main(argv: Argv = None, return_command: Bool = False, **kwargs) -> Optional[CommandObj]:
|
{cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/config.py
RENAMED
|
@@ -244,8 +244,8 @@ class ConfigItem(Generic[CV, DV]):
|
|
|
244
244
|
def __set__(self, instance: CommandConfig, value: ConfigValue):
|
|
245
245
|
if instance._read_only:
|
|
246
246
|
raise AttributeError(f'Unable to set attribute {self.name}={value!r} because {instance} is read-only')
|
|
247
|
-
elif
|
|
248
|
-
value =
|
|
247
|
+
elif self.type is not None:
|
|
248
|
+
value = self.type(value)
|
|
249
249
|
instance._data[self.name] = value
|
|
250
250
|
|
|
251
251
|
def __delete__(self, instance: CommandConfig):
|
{cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/context.py
RENAMED
|
@@ -146,8 +146,8 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
|
|
|
146
146
|
@property
|
|
147
147
|
def terminal_width(self) -> int:
|
|
148
148
|
"""Returns the current terminal width as the number of characters that fit on a single line."""
|
|
149
|
-
if
|
|
150
|
-
return
|
|
149
|
+
if self._terminal_width is not None:
|
|
150
|
+
return self._terminal_width
|
|
151
151
|
return _TERMINAL.width
|
|
152
152
|
|
|
153
153
|
def get_parsed(
|
|
@@ -178,18 +178,18 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
|
|
|
178
178
|
if command is None:
|
|
179
179
|
command = self.command
|
|
180
180
|
with self:
|
|
181
|
-
if recursive and
|
|
182
|
-
parsed = parent.get_parsed(
|
|
181
|
+
if recursive and self.parent:
|
|
182
|
+
parsed = self.parent.get_parsed(
|
|
183
183
|
command, exclude=exclude, recursive=recursive, default=default, include_defaults=include_defaults
|
|
184
184
|
)
|
|
185
185
|
else:
|
|
186
186
|
parsed = {}
|
|
187
187
|
|
|
188
188
|
# TODO: Add way to get a nested dict with ParamGroup names as the keys of the nested sections?
|
|
189
|
-
if
|
|
190
|
-
for param in params.iter_params(exclude):
|
|
189
|
+
if self.params:
|
|
190
|
+
for param in self.params.iter_params(exclude):
|
|
191
191
|
if include_defaults or param in self._parsed:
|
|
192
|
-
parsed[param.name] = param.
|
|
192
|
+
parsed[param.name] = param.result(command, default)
|
|
193
193
|
|
|
194
194
|
return parsed
|
|
195
195
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from cli_command_parser import Command, Counter, Positional, Flag, ParamGroup, SubCommand, main
|
|
8
|
+
from cli_command_parser.inputs import Path as IPath
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
arg_parser = 'argparse.ArgumentParser'
|
|
13
|
+
cli_cp_cmd = 'cli-command-parser Command'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ParserConverter(Command, description=f'Tool to convert an {arg_parser} into a {cli_cp_cmd}'):
|
|
17
|
+
action = SubCommand()
|
|
18
|
+
input: Path
|
|
19
|
+
no_smart_for = Flag('-S', help='Disable "smart" for loop handling that attempts to dedupe common subparser params')
|
|
20
|
+
with ParamGroup('Common'):
|
|
21
|
+
verbose = Counter('-v', help='Increase logging verbosity (can specify multiple times)')
|
|
22
|
+
dry_run = Flag('-D', help='Print the actions that would be taken instead of taking them')
|
|
23
|
+
|
|
24
|
+
def _init_command_(self):
|
|
25
|
+
log_fmt = '%(asctime)s %(levelname)s %(name)s %(lineno)d %(message)s' if self.verbose > 1 else '%(message)s'
|
|
26
|
+
logging.basicConfig(level=logging.DEBUG if self.verbose else logging.INFO, format=log_fmt)
|
|
27
|
+
|
|
28
|
+
@cached_property
|
|
29
|
+
def script(self):
|
|
30
|
+
from cli_command_parser.conversion import Script
|
|
31
|
+
|
|
32
|
+
script = Script(self.input.read_text(), not self.no_smart_for, path=self.input)
|
|
33
|
+
log.debug(f'Found {script=}')
|
|
34
|
+
return script
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Convert(ParserConverter):
|
|
38
|
+
input: Path = Positional(type=IPath(type='file', exists=True), help=f'A file containing an {arg_parser}')
|
|
39
|
+
add_methods = Flag('--no-methods', '-M', default=True, help='Do not include boilerplate methods in Commands')
|
|
40
|
+
|
|
41
|
+
def main(self):
|
|
42
|
+
from cli_command_parser.conversion import convert_script
|
|
43
|
+
|
|
44
|
+
print(convert_script(self.script, self.add_methods))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Pprint(ParserConverter):
|
|
48
|
+
input: Path = Positional(type=IPath(type='file', exists=True), help=f'A file containing an {arg_parser}')
|
|
49
|
+
|
|
50
|
+
def main(self):
|
|
51
|
+
for parser in self.script.parsers:
|
|
52
|
+
parser.pprint()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == '__main__':
|
|
56
|
+
main()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
|
+
import re
|
|
4
5
|
from ast import NodeVisitor, AST, Assign, Call, For, Attribute, Name, Import, ImportFrom, expr
|
|
5
6
|
from collections import ChainMap, defaultdict
|
|
6
7
|
from functools import partial, wraps
|
|
@@ -43,7 +44,7 @@ class ScriptVisitor(NodeVisitor):
|
|
|
43
44
|
|
|
44
45
|
def __init__(self, smart_loop_handling: bool = True, track_refs: Collection[TrackedRef] = ()):
|
|
45
46
|
self.smart_loop_handling = smart_loop_handling
|
|
46
|
-
self.scopes = ChainMap()
|
|
47
|
+
self.scopes = ChainMap() # ChainMap that tracks the var/class/func/etc names available in a given scope
|
|
47
48
|
self._tracked_refs = set()
|
|
48
49
|
self._mod_name_tracked_map = defaultdict(dict)
|
|
49
50
|
for ref in track_refs:
|
|
@@ -74,11 +75,32 @@ class ScriptVisitor(NodeVisitor):
|
|
|
74
75
|
self.scopes[f'{as_name}.{name}'] = tracked
|
|
75
76
|
|
|
76
77
|
def visit_ImportFrom(self, node: ImportFrom):
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
"""
|
|
79
|
+
Processes a ``from module import names`` statement. If the module name matches one from which members were
|
|
80
|
+
registered to be tracked, then the imported names (and any ``as`` aliases) are processed. Members with
|
|
81
|
+
canonical names that match an item that was registered to be tracked are added to the current scope / variable
|
|
82
|
+
namespace.
|
|
83
|
+
|
|
84
|
+
Relative module imports are handled fuzzily - no attempt is made to determine the fully qualified module name
|
|
85
|
+
for the source file or to resolve what the relative import's fully qualified module name would be. This may
|
|
86
|
+
result in incorrect items being tracked if the name matched a tracked name in the matched tracked module.
|
|
87
|
+
"""
|
|
88
|
+
if level := node.level:
|
|
89
|
+
# For absolute imports, the level is 0
|
|
90
|
+
# For relative imports, the level is a positive int, representing the number of relative package levels
|
|
91
|
+
matches = re.compile(r'[^.]+\.' * level + re.escape(node.module) + '$').search
|
|
92
|
+
for module, name_tracked_map in self._mod_name_tracked_map.items():
|
|
93
|
+
if matches(module):
|
|
94
|
+
log.debug(f'Found fuzzy relative name match for {"." * level + node.module!r} to {module=}')
|
|
95
|
+
self._maybe_track_import_from(node, name_tracked_map)
|
|
96
|
+
elif name_tracked_map := self._mod_name_tracked_map.get(node.module):
|
|
97
|
+
self._maybe_track_import_from(node, name_tracked_map)
|
|
98
|
+
|
|
99
|
+
def _maybe_track_import_from(self, node: ImportFrom, name_tracked_map):
|
|
100
|
+
for name, as_name in imp_names(node):
|
|
101
|
+
if tracked := name_tracked_map.get(name):
|
|
102
|
+
log.debug(f'Found tracked import: {node.module}.{name} as {as_name}')
|
|
103
|
+
self.scopes[as_name] = tracked
|
|
82
104
|
|
|
83
105
|
# endregion
|
|
84
106
|
|
{cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/exceptions.py
RENAMED
|
@@ -123,6 +123,7 @@ class UsageError(CommandParserException):
|
|
|
123
123
|
"""Base exception for user errors"""
|
|
124
124
|
|
|
125
125
|
message: str = None
|
|
126
|
+
# TODO: Document that this is a recommended exception type to use when performing manual input validation
|
|
126
127
|
|
|
127
128
|
|
|
128
129
|
class ParamUsageError(UsageError):
|
|
@@ -70,8 +70,8 @@ class CommandHelpFormatter:
|
|
|
70
70
|
params = self.params
|
|
71
71
|
yield from params.all_positionals
|
|
72
72
|
yield from params.options
|
|
73
|
-
if
|
|
74
|
-
yield pass_thru
|
|
73
|
+
if params.pass_thru is not None:
|
|
74
|
+
yield params.pass_thru
|
|
75
75
|
|
|
76
76
|
def _usage_parts(self, sub_cmd_choice: str = None, allow_sys_argv: Bool = True) -> Iterator[str]:
|
|
77
77
|
yield 'usage:'
|
|
@@ -191,8 +191,8 @@ class CommandHelpFormatter:
|
|
|
191
191
|
# each choice value (should probably be configurable to explode or condense)
|
|
192
192
|
choice_str = f'{choice_base} {cmd_name}' if choice_base else cmd_name
|
|
193
193
|
yield from spaced_rst_header(f'Subcommand: {choice_str}', level)
|
|
194
|
-
if
|
|
195
|
-
yield
|
|
194
|
+
if choice.help:
|
|
195
|
+
yield choice.help
|
|
196
196
|
yield ''
|
|
197
197
|
|
|
198
198
|
if (command := choice.target) is None:
|
|
@@ -200,10 +200,9 @@ class CommandHelpFormatter:
|
|
|
200
200
|
yield from self._cmd_rst_lines(config, choice_str, allow_sys_argv)
|
|
201
201
|
else:
|
|
202
202
|
params = get_params(command)
|
|
203
|
-
|
|
204
|
-
yield from formatter._cmd_rst_lines(config, choice_str, allow_sys_argv)
|
|
203
|
+
yield from params.formatter._cmd_rst_lines(config, choice_str, allow_sys_argv)
|
|
205
204
|
if nested_sub_cmd := params.sub_command:
|
|
206
|
-
yield from formatter._sub_cmds_rst_lines(
|
|
205
|
+
yield from params.formatter._sub_cmds_rst_lines(
|
|
207
206
|
config, nested_sub_cmd, level, choice_str, depth + 1, allow_sys_argv
|
|
208
207
|
)
|
|
209
208
|
|
|
@@ -447,6 +447,7 @@ class GroupHelpFormatter(ParamHelpFormatter, param_cls=ParamGroup): # noqa # p
|
|
|
447
447
|
return f'{adjective} arguments'
|
|
448
448
|
|
|
449
449
|
def _get_spacer(self) -> str:
|
|
450
|
+
# TODO: Config option to set these chars OR to have them just be spaces
|
|
450
451
|
if self.param.mutually_exclusive:
|
|
451
452
|
return '\u00A6 ' # BROKEN BAR
|
|
452
453
|
elif self.param.mutually_dependent:
|
|
@@ -135,6 +135,7 @@ class ChoiceMap(Choices[T]):
|
|
|
135
135
|
|
|
136
136
|
def __init__(self, choices: Mapping[Any, T], *args, **kwargs):
|
|
137
137
|
super().__init__(choices, *args, **kwargs)
|
|
138
|
+
# TODO: Alternate ChoiceMap where values are used as help text, similar to SubCommand with local_choices
|
|
138
139
|
|
|
139
140
|
def __call__(self, value: str) -> T:
|
|
140
141
|
value = self._normalize(value)
|
|
@@ -200,6 +200,9 @@ class Json(Serialized):
|
|
|
200
200
|
def __init__(self, *, mode: str = 'rb', **kwargs):
|
|
201
201
|
import json
|
|
202
202
|
|
|
203
|
+
# TODO: catch JSONDecodeError and provide a standardized cleaner error message (with a way to disable this error handling)
|
|
204
|
+
# TODO: Update docs to not suggest importing `inputs as i`
|
|
205
|
+
|
|
203
206
|
write = allows_write(mode, True)
|
|
204
207
|
kwargs['pass_file'] = write # json.load just calls loads with f.read()
|
|
205
208
|
super().__init__(json.dump if write else json.loads, mode=mode, **kwargs)
|
|
@@ -124,8 +124,8 @@ class Regex(PatternInput[RegexResult]):
|
|
|
124
124
|
elif mode == RegexMode.GROUP:
|
|
125
125
|
return m.group(self.group)
|
|
126
126
|
elif mode == RegexMode.GROUPS:
|
|
127
|
-
if
|
|
128
|
-
return tuple(m.group(g) for g in groups)
|
|
127
|
+
if self.groups:
|
|
128
|
+
return tuple(m.group(g) for g in self.groups)
|
|
129
129
|
return m.groups()
|
|
130
130
|
else: # mode == RegexMode.DICT
|
|
131
131
|
return m.groupdict()
|
{cli_command_parser-2023.10.14 → cli_command_parser-2024.4.20}/lib/cli_command_parser/inputs/time.py
RENAMED
|
@@ -56,7 +56,7 @@ class different_locale:
|
|
|
56
56
|
|
|
57
57
|
def __enter__(self):
|
|
58
58
|
self._lock.acquire()
|
|
59
|
-
if not
|
|
59
|
+
if not self.locale:
|
|
60
60
|
return
|
|
61
61
|
# locale.getlocale does not support LC_ALL, but `setlocale(LC_ALL)` with no locale to set will return a str
|
|
62
62
|
# containing all of the current locale settings as `key1=val1;key2=val2;...;keyN=valN`
|
|
@@ -65,7 +65,7 @@ class different_locale:
|
|
|
65
65
|
# to remain set to `English_United States.1252` on Windows 10, which resulted in incorrectly encoded results.
|
|
66
66
|
# Using f'LC_CTYPE={locale};LC_TIME={locale}' seemed cleaner than setting LC_ALL in its entirety, but it
|
|
67
67
|
# resulted in `locale.Error: unsupported locale setting` on Ubuntu/WSL.
|
|
68
|
-
setlocale(LC_ALL, locale)
|
|
68
|
+
setlocale(LC_ALL, self.locale)
|
|
69
69
|
|
|
70
70
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
71
71
|
if self.locale:
|
|
@@ -212,13 +212,13 @@ class CalendarUnitInput(DTInput[Union[str, int]], ABC):
|
|
|
212
212
|
normalized = self.parse(value)
|
|
213
213
|
if normalized < self._min_index:
|
|
214
214
|
raise InvalidChoiceError(value, self.choices(), self.dt_type)
|
|
215
|
-
elif
|
|
216
|
-
return self._formats[
|
|
217
|
-
elif (names_or_abbreviations := self._formats.get(
|
|
215
|
+
elif self.out_format in (DTFormatMode.NUMERIC, DTFormatMode.NUMERIC_ISO):
|
|
216
|
+
return self._formats[self.out_format][normalized]
|
|
217
|
+
elif (names_or_abbreviations := self._formats.get(self.out_format)) is not None:
|
|
218
218
|
with different_locale(self.out_locale):
|
|
219
219
|
return names_or_abbreviations[normalized]
|
|
220
220
|
|
|
221
|
-
raise ValueError(f'Unexpected output format={
|
|
221
|
+
raise ValueError(f'Unexpected output format={self.out_format!r} for {self.dt_type}={normalized}')
|
|
222
222
|
|
|
223
223
|
|
|
224
224
|
class Day(CalendarUnitInput, dt_type='day of the week'):
|