cli-command-parser 2025.5.10__tar.gz → 2025.7.10__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.
Files changed (70) hide show
  1. {cli_command_parser-2025.5.10/lib/cli_command_parser.egg-info → cli_command_parser-2025.7.10}/PKG-INFO +6 -6
  2. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/__init__.py +25 -25
  3. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/__version__.py +1 -1
  4. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/context.py +5 -3
  5. cli_command_parser-2025.7.10/lib/cli_command_parser/conversion/__init__.py +12 -0
  6. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/conversion/cli.py +1 -1
  7. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/documentation.py +17 -1
  8. cli_command_parser-2025.7.10/lib/cli_command_parser/error_handling/__init__.py +38 -0
  9. cli_command_parser-2025.5.10/lib/cli_command_parser/error_handling.py → cli_command_parser-2025.7.10/lib/cli_command_parser/error_handling/base.py +15 -68
  10. cli_command_parser-2025.7.10/lib/cli_command_parser/error_handling/other.py +20 -0
  11. cli_command_parser-2025.7.10/lib/cli_command_parser/error_handling/windows.py +55 -0
  12. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/formatting/params.py +3 -0
  13. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/inputs/__init__.py +8 -8
  14. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/inputs/choices.py +2 -2
  15. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/inputs/exceptions.py +1 -1
  16. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/inputs/time.py +77 -72
  17. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/inputs/utils.py +2 -2
  18. cli_command_parser-2025.7.10/lib/cli_command_parser/parameters/__init__.py +6 -0
  19. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/parameters/base.py +8 -0
  20. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/parameters/options.py +9 -0
  21. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/parameters/positionals.py +3 -3
  22. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/testing.py +10 -10
  23. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10/lib/cli_command_parser.egg-info}/PKG-INFO +6 -6
  24. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser.egg-info/SOURCES.txt +4 -1
  25. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/readme.rst +5 -5
  26. cli_command_parser-2025.5.10/lib/cli_command_parser/conversion/__init__.py +0 -3
  27. cli_command_parser-2025.5.10/lib/cli_command_parser/parameters/__init__.py +0 -6
  28. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/LICENSE +0 -0
  29. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/MANIFEST.in +0 -0
  30. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/entry_points.txt +0 -0
  31. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/__main__.py +0 -0
  32. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/annotations.py +0 -0
  33. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/command_parameters.py +0 -0
  34. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/commands.py +0 -0
  35. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/compat.py +0 -0
  36. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/config.py +0 -0
  37. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/conversion/__main__.py +0 -0
  38. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/conversion/argparse_ast.py +0 -0
  39. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/conversion/argparse_utils.py +0 -0
  40. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/conversion/command_builder.py +0 -0
  41. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/conversion/utils.py +0 -0
  42. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/conversion/visitor.py +0 -0
  43. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/core.py +0 -0
  44. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/exceptions.py +0 -0
  45. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/formatting/__init__.py +0 -0
  46. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/formatting/commands.py +0 -0
  47. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/formatting/restructured_text.py +0 -0
  48. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/formatting/utils.py +0 -0
  49. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/inputs/base.py +0 -0
  50. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/inputs/files.py +0 -0
  51. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/inputs/numeric.py +0 -0
  52. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/inputs/patterns.py +0 -0
  53. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/metadata.py +0 -0
  54. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/nargs.py +0 -0
  55. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/parameters/actions.py +0 -0
  56. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/parameters/choice_map.py +0 -0
  57. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/parameters/groups.py +0 -0
  58. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/parameters/option_strings.py +0 -0
  59. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/parameters/pass_thru.py +0 -0
  60. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/parse_tree.py +0 -0
  61. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/parser.py +0 -0
  62. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/typing.py +0 -0
  63. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser/utils.py +0 -0
  64. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser.egg-info/dependency_links.txt +0 -0
  65. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser.egg-info/entry_points.txt +0 -0
  66. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser.egg-info/requires.txt +0 -0
  67. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/lib/cli_command_parser.egg-info/top_level.txt +0 -0
  68. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/pyproject.toml +0 -0
  69. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/requirements-dev.txt +0 -0
  70. {cli_command_parser-2025.5.10 → cli_command_parser-2025.7.10}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli_command_parser
3
- Version: 2025.5.10
3
+ Version: 2025.7.10
4
4
  Summary: CLI Command Parser
5
5
  Home-page: https://github.com/dskrypa/cli_command_parser
6
6
  Author: Doug Skrypa
@@ -58,11 +58,11 @@ CLI Command Parser is a class-based CLI argument parser that defines parameters
58
58
  tools to quickly and easily get started with basic CLIs, and it scales well to support even very large and complex
59
59
  CLIs while remaining readable and easy to maintain.
60
60
 
61
- The primary goals of this project:
62
- - Make it easy to define subcommands and actions in an clean and organized manner
63
- - Allow for inheritance so that common parameters don't need to be repeated
64
- - Make it easy to handle common initialization tasks for all actions / subcommands once
65
- - Reduce the amount of boilerplate code that is necessary for setting up parsing and handling argument values
61
+ Some of the primary goals and key features of this project:
62
+ - Minimal boilerplate code is necessary to define CLI parameters and access their parsed values
63
+ - Easy to use type annotations for CLI parameters
64
+ - Subcommands can inherit common parameters so they don't need to be repeated
65
+ - Easy to handle common initialization tasks for all actions / subcommands once
66
66
 
67
67
 
68
68
  Example Program
@@ -4,52 +4,52 @@ Command Parser
4
4
  :author: Doug Skrypa
5
5
  """
6
6
 
7
+ from .commands import AsyncCommand, Command, main
7
8
  from .config import (
9
+ AllowLeadingDash,
10
+ AmbiguousComboMode,
8
11
  CommandConfig,
9
- ShowDefaults,
10
12
  OptionNameMode,
13
+ ShowDefaults,
11
14
  SubcommandAliasHelpMode,
12
- AmbiguousComboMode,
13
- AllowLeadingDash,
14
15
  )
15
- from .commands import Command, AsyncCommand, main
16
- from .context import Context, get_current_context, ctx, get_parsed, get_context, get_raw_arg
16
+ from .context import Context, ctx, get_context, get_current_context, get_parsed, get_raw_arg
17
+ from .error_handling import ErrorHandler, error_handler, extended_error_handler, no_exit_handler
17
18
  from .exceptions import (
18
- CommandParserException,
19
- CommandDefinitionError,
20
- ParameterDefinitionError,
21
- UsageError,
22
- ParamUsageError,
19
+ AmbiguousParseTree,
23
20
  BadArgument,
21
+ CommandDefinitionError,
22
+ CommandParserException,
24
23
  InvalidChoice,
25
24
  MissingArgument,
26
- TooManyArguments,
25
+ NoActiveContext,
27
26
  NoSuchOption,
28
- ParserExit,
29
27
  ParamConflict,
28
+ ParameterDefinitionError,
30
29
  ParamsMissing,
31
- NoActiveContext,
32
- AmbiguousParseTree,
30
+ ParamUsageError,
31
+ ParserExit,
32
+ TooManyArguments,
33
+ UsageError,
33
34
  )
34
- from .error_handling import ErrorHandler, error_handler, no_exit_handler, extended_error_handler
35
35
  from .formatting.commands import get_formatter
36
36
  from .nargs import REMAINDER
37
37
  from .parameters import (
38
+ Action,
39
+ ActionFlag,
40
+ BaseOption,
41
+ BasePositional,
42
+ Counter,
43
+ Flag,
44
+ Option,
38
45
  Parameter,
46
+ ParamGroup,
39
47
  PassThru,
40
- BasePositional,
41
48
  Positional,
42
49
  SubCommand,
43
- Action,
44
- BaseOption,
45
- Option,
46
- Flag,
47
- Counter,
48
- ActionFlag,
50
+ TriFlag,
49
51
  action_flag,
50
- before_main,
51
52
  after_main,
52
- ParamGroup,
53
- TriFlag,
53
+ before_main,
54
54
  )
55
55
  from .typing import Param, ParamOrGroup
@@ -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.05.10'
4
+ __version__ = '2025.07.10'
5
5
  __author__ = 'Doug Skrypa'
6
6
  __author_email__ = 'dskrypa@gmail.com'
7
7
  __license__ = 'Apache 2.0'
@@ -24,7 +24,7 @@ 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, Option, Parameter
27
+ from .parameters import ActionFlag, BaseOption, Option, Parameter
28
28
  from .typing import AnyConfig, Bool, CommandObj, CommandType, OptStr, ParamOrGroup, PathLike, StrSeq # noqa
29
29
 
30
30
  __all__ = ['Context', 'ctx', 'get_current_context', 'get_or_create_context', 'get_context', 'get_parsed', 'get_raw_arg']
@@ -250,9 +250,11 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
250
250
  """Not intended to be called by users. Used during parsing to determine if any Parameters are missing."""
251
251
  return [p for p in self.params.required_check_params() if not self._provided[p]]
252
252
 
253
- def missing_options_with_env_var(self) -> Iterator[Option]:
253
+ def missing_options_with_env_var(self) -> Iterator[BaseOption]:
254
254
  """Yields Option parameters that have an environment variable configured, and did not have any CLI values."""
255
- yield from (p for p in self.params.options if p.env_var and not self._provided[p])
255
+ for param in self.params.options:
256
+ if param.env_var and not self._provided[param]:
257
+ yield param
256
258
 
257
259
  # endregion
258
260
 
@@ -0,0 +1,12 @@
1
+ from .argparse_ast import (
2
+ AddVisitedChild,
3
+ ArgCollection,
4
+ ArgGroup,
5
+ AstArgumentParser,
6
+ AstCallable,
7
+ MutuallyExclusiveGroup,
8
+ Script,
9
+ SubParser,
10
+ visit_func,
11
+ )
12
+ from .command_builder import Converter, convert_script
@@ -4,7 +4,7 @@ import logging
4
4
  from functools import cached_property
5
5
  from pathlib import Path
6
6
 
7
- from cli_command_parser import Command, Counter, Positional, Flag, ParamGroup, SubCommand, main
7
+ from cli_command_parser import Command, Counter, Flag, ParamGroup, Positional, SubCommand, main
8
8
  from cli_command_parser.inputs import Path as IPath
9
9
 
10
10
  log = logging.getLogger(__name__)
@@ -132,15 +132,17 @@ def top_level_commands(commands: Commands) -> Commands:
132
132
  def import_module(path: PathLike):
133
133
  """Import the module / package from the given path"""
134
134
  path = Path(path)
135
- name = path.stem
135
+ name = _module_name(path)
136
136
  if path.is_dir():
137
137
  path /= '__init__.py'
138
+
138
139
  spec = spec_from_file_location(name, path)
139
140
  try:
140
141
  module = module_from_spec(spec)
141
142
  except AttributeError as e:
142
143
  path_str = path.as_posix()
143
144
  raise ImportError(f'Invalid path={path_str!r} - are you sure it is a Python module?', path=path_str) from e
145
+
144
146
  sys.modules[spec.name] = module # This is required for the program metadata introspection
145
147
  try:
146
148
  spec.loader.exec_module(module)
@@ -150,6 +152,20 @@ def import_module(path: PathLike):
150
152
  return module
151
153
 
152
154
 
155
+ def _module_name(path: Path) -> str:
156
+ if path.name == '__init__.py':
157
+ path = path.parent
158
+
159
+ parts = [path.stem]
160
+ while (path := path.parent).name: # / has no name
161
+ if path.joinpath('__init__.py').exists(): # it is a package
162
+ parts.append(path.name)
163
+ else:
164
+ break
165
+
166
+ return '.'.join(parts[::-1])
167
+
168
+
153
169
  def _is_command(obj, include_abc: Bool = False) -> bool:
154
170
  if not (isinstance(obj, CommandMeta) and obj is not Command):
155
171
  return False
@@ -0,0 +1,38 @@
1
+ """
2
+ Error handling for expected / unexpected exceptions.
3
+
4
+ The default handler will...
5
+
6
+ - Call ``print()`` after catching a :class:`python:KeyboardInterrupt`, before exiting
7
+ - Exit gracefully after catching a :class:`python:BrokenPipeError` (often caused by piping output to a tool like
8
+ ``tail``)
9
+
10
+ .. note::
11
+ Parameters defined in a base Command will be processed in the context of that Command. I.e., if a valid
12
+ subcommand argument was provided, but an Option defined in the parent Command has an invalid value, then the
13
+ exception that is raised about that invalid value will be raised before transferring control to the
14
+ subcommand's error handler.
15
+
16
+ :author: Doug Skrypa
17
+ """
18
+
19
+ from platform import system as _system
20
+
21
+ from .base import (
22
+ ErrorHandler,
23
+ Handler,
24
+ HandlerFunc,
25
+ NullErrorHandler,
26
+ error_handler,
27
+ extended_error_handler,
28
+ no_exit_handler,
29
+ )
30
+
31
+ if _system().lower() == 'windows':
32
+ from .windows import handle_kb_interrupt
33
+ else:
34
+ from .other import handle_kb_interrupt
35
+
36
+ __all__ = ['ErrorHandler', 'Handler', 'error_handler', 'extended_error_handler', 'no_exit_handler', 'NullErrorHandler']
37
+
38
+ error_handler.register(handle_kb_interrupt, KeyboardInterrupt)
@@ -1,34 +1,19 @@
1
1
  """
2
- Error handling for expected / unexpected exceptions.
3
-
4
- The default handler will...
5
-
6
- - Call ``print()`` after catching a :class:`python:KeyboardInterrupt`, before exiting
7
- - Exit gracefully after catching a :class:`python:BrokenPipeError` (often caused by piping output to a tool like
8
- ``tail``)
9
-
10
- .. note::
11
- Parameters defined in a base Command will be processed in the context of that Command. I.e., if a valid
12
- subcommand argument was provided, but an Option defined in the parent Command has an invalid value, then the
13
- exception that is raised about that invalid value will be raised before transferring control to the
14
- subcommand's error handler.
15
-
16
- :author: Doug Skrypa
2
+ Platform-agnostic error handling framework / handlers.
17
3
  """
18
4
 
19
5
  from __future__ import annotations
20
6
 
21
- import platform
22
7
  import sys
23
8
  from collections import ChainMap
24
- from typing import Callable, Iterator, Optional, Type, Union
9
+ from typing import Callable, Iterator, Type, TypeVar, Union
25
10
 
26
- from .exceptions import CommandParserException
11
+ from ..exceptions import CommandParserException
27
12
 
28
13
  __all__ = ['ErrorHandler', 'error_handler', 'extended_error_handler', 'no_exit_handler', 'NullErrorHandler']
29
14
 
30
- WINDOWS = platform.system().lower() == 'windows'
31
- HandlerFunc = Callable[[BaseException], Optional[bool]]
15
+ E = TypeVar('E', bound=BaseException)
16
+ HandlerFunc = Callable[[E], Union[bool, int, None]]
32
17
 
33
18
 
34
19
  class ErrorHandler:
@@ -42,7 +27,7 @@ class ErrorHandler:
42
27
  def __repr__(self) -> str:
43
28
  return f'<{self.__class__.__name__}[handlers={len(self.exc_handler_map)}]>'
44
29
 
45
- def register(self, handler: HandlerFunc, *exceptions: Type[BaseException]):
30
+ def register(self, handler: HandlerFunc, *exceptions: Type[E]):
46
31
  for exc in exceptions:
47
32
  self.exc_handler_map[exc] = Handler(exc, handler)
48
33
 
@@ -61,7 +46,7 @@ class ErrorHandler:
61
46
  return _handler
62
47
 
63
48
  @classmethod
64
- def cls_handler(cls, *exceptions: Type[BaseException]):
49
+ def cls_handler(cls, *exceptions: Type[E]):
65
50
  def _cls_handler(handler: Union[HandlerFunc, staticmethod]):
66
51
  for exc in exceptions:
67
52
  cls._exc_handler_map[exc] = Handler(exc, handler)
@@ -91,6 +76,7 @@ class ErrorHandler:
91
76
  for handler in self.iter_handlers(exc_type, exc_val):
92
77
  result = handler(exc_val)
93
78
  if result is True:
79
+ # This explicitly checks for True since 1 == True, but 1 is treated as an intended exit code
94
80
  return True
95
81
  if result or (isinstance(result, int) and result is not False):
96
82
  sys.exit(result)
@@ -130,62 +116,23 @@ class Handler:
130
116
  return issubclass(self.exc_cls, other.exc_cls)
131
117
 
132
118
 
133
- ErrorHandler.cls_handler(CommandParserException)(CommandParserException.exit)
119
+ # By default, all error handlers should call :meth:`CommandParserException.exit` for CommandParserExceptions
120
+ ErrorHandler.cls_handler(CommandParserException)(CommandParserException.exit) # noqa
134
121
 
135
122
  #: Default base :class:`ErrorHandler`
136
123
  error_handler: ErrorHandler = ErrorHandler()
137
- error_handler.register(lambda e: True, BrokenPipeError)
138
124
 
139
125
 
140
- @error_handler(KeyboardInterrupt)
141
- def handle_kb_interrupt(exc: KeyboardInterrupt) -> int:
142
- """
143
- Handles :class:`python:KeyboardInterrupt` by calling :func:`python:print` to avoid ending the program in a way that
144
- causes the next terminal prompt to be printed on the same line as the last (possibly incomplete) line of output.
145
- """
146
- try:
147
- print(flush=True) # Flush forces any potential closed/broken pipe-related error to be caught/handled here
148
- except BrokenPipeError:
149
- pass
150
- except OSError as e:
151
- # Handle the closed/broken pipe incorrect errno bug if triggered during the above print
152
- if not WINDOWS or not handle_win_os_pipe_error(e):
153
- raise
154
- return 130
126
+ @error_handler(BrokenPipeError)
127
+ def _handle_broken_pipe(exc: BrokenPipeError):
128
+ # This can't be registered as a lambda function because it would break the ability to pickle the handler
129
+ return True
155
130
 
156
131
 
157
132
  #: An :class:`ErrorHandler` that does not call :func:`python:sys.exit` for
158
133
  #: :class:`CommandParserExceptions<.CommandParserException>`
159
134
  no_exit_handler: ErrorHandler = error_handler.copy()
160
- no_exit_handler(CommandParserException)(CommandParserException.show)
135
+ no_exit_handler.register(CommandParserException.show, CommandParserException) # noqa
161
136
 
162
137
  #: The default :class:`ErrorHandler` (extends :obj:`error_handler`)
163
138
  extended_error_handler: ErrorHandler = error_handler.copy()
164
-
165
- if WINDOWS:
166
- import ctypes
167
-
168
- RtlGetLastNtStatus = ctypes.WinDLL('ntdll').RtlGetLastNtStatus
169
- RtlGetLastNtStatus.restype = ctypes.c_ulong
170
- NT_STATUSES = {0xC000_00B1: 'STATUS_PIPE_CLOSING', 0xC000_014B: 'STATUS_PIPE_BROKEN'}
171
-
172
- @extended_error_handler(OSError)
173
- def handle_win_os_pipe_error(exc: OSError):
174
- """
175
- This is a workaround for `[Windows] I/O on a broken pipe may raise an EINVAL OSError instead of BrokenPipeError
176
- <https://github.com/python/cpython/issues/79935>`_, which is a bug in the way that the
177
- windows error code for a broken pipe is translated into an errno value. It should be translated to
178
- :data:`~errno.EPIPE`, but it uses :data:`~errno.EINVAL` (22) instead.
179
-
180
- Prevents the following when piping output to utilities such as ``| head``::\n
181
- Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>
182
- OSError: [Errno 22] Invalid argument
183
- """
184
- if exc.errno == 22 and RtlGetLastNtStatus() in NT_STATUSES:
185
- try:
186
- sys.stdout.close()
187
- except OSError:
188
- pass
189
- return True
190
-
191
- return False
@@ -0,0 +1,20 @@
1
+ """
2
+ Error handling for expected / unexpected exceptions on non-Windows systems.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ __all__ = ['handle_kb_interrupt']
8
+
9
+
10
+ def handle_kb_interrupt(exc: KeyboardInterrupt) -> int:
11
+ """
12
+ Handles :class:`python:KeyboardInterrupt` by calling :func:`python:print` to avoid ending the program in a way that
13
+ causes the next terminal prompt to be printed on the same line as the last (possibly incomplete) line of output.
14
+ """
15
+ try:
16
+ print(flush=True) # Flush forces any potential closed/broken pipe-related error to be caught/handled here
17
+ except BrokenPipeError:
18
+ pass
19
+ # 130 (= 128 + SIGINT (2)) is used/expected by Bash; see: https://tldp.org/LDP/abs/html/exitcodes.html
20
+ return 130
@@ -0,0 +1,55 @@
1
+ """
2
+ Error handling for expected / unexpected exceptions on Windows systems.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import ctypes
8
+ import sys
9
+
10
+ from .base import extended_error_handler
11
+
12
+ __all__ = ['handle_kb_interrupt']
13
+
14
+ RtlGetLastNtStatus = ctypes.WinDLL('ntdll').RtlGetLastNtStatus
15
+ RtlGetLastNtStatus.restype = ctypes.c_ulong
16
+ NT_STATUSES = {0xC000_00B1: 'STATUS_PIPE_CLOSING', 0xC000_014B: 'STATUS_PIPE_BROKEN'}
17
+
18
+
19
+ def handle_kb_interrupt(exc: KeyboardInterrupt) -> int:
20
+ """
21
+ Handles :class:`python:KeyboardInterrupt` by calling :func:`python:print` to avoid ending the program in a way that
22
+ causes the next terminal prompt to be printed on the same line as the last (possibly incomplete) line of output.
23
+ """
24
+ try:
25
+ print(flush=True) # Flush forces any potential closed/broken pipe-related error to be caught/handled here
26
+ except BrokenPipeError:
27
+ pass
28
+ except OSError as e:
29
+ # Handle the closed/broken pipe incorrect errno bug if triggered during the above print
30
+ if not handle_win_os_pipe_error(e):
31
+ raise
32
+ return 130
33
+
34
+
35
+ @extended_error_handler(OSError)
36
+ def handle_win_os_pipe_error(exc: OSError):
37
+ """
38
+ This is a workaround for `[Windows] I/O on a broken pipe may raise an EINVAL OSError instead of BrokenPipeError
39
+ <https://github.com/python/cpython/issues/79935>`_, which is a bug in the way that the
40
+ Windows error code for a broken pipe is translated into an errno value. It should be translated to
41
+ :data:`~errno.EPIPE`, but it uses :data:`~errno.EINVAL` (22) instead.
42
+
43
+ Prevents the following when piping output to utilities such as ``| head``::
44
+
45
+ Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>
46
+ OSError: [Errno 22] Invalid argument
47
+ """
48
+ if exc.errno == 22 and RtlGetLastNtStatus() in NT_STATUSES:
49
+ try:
50
+ sys.stdout.close()
51
+ except OSError:
52
+ pass
53
+ return True
54
+
55
+ return False
@@ -56,6 +56,9 @@ class ParamHelpFormatter:
56
56
  def __init__(self, param: ParamOrGroup):
57
57
  self.param = param
58
58
 
59
+ def __getnewargs__(self):
60
+ return (self.param,)
61
+
59
62
  def maybe_wrap_usage(self, text: str) -> str:
60
63
  """
61
64
  Wraps the provided text in parentheses / brackets / etc based on whether the associated Parameter is required,
@@ -10,17 +10,17 @@ import typing as _t
10
10
  from enum import Enum as _Enum
11
11
 
12
12
  from ..exceptions import ParameterDefinitionError as _ParameterDefinitionError
13
- from .exceptions import InputValidationError, InvalidChoiceError
14
- from .utils import StatMode, FileWrapper
15
13
  from .base import InputType
16
- from .choices import Choices, ChoiceMap, EnumChoices
17
- from .files import Path, File, Serialized, Json, Pickle
18
- from .numeric import Range, NumRange
19
- from .patterns import Regex, RegexMode, Glob
20
- from .time import Day, Month, TimeDelta, DateTime, Date, Time, DTFormatMode
14
+ from .choices import ChoiceMap, Choices, EnumChoices
15
+ from .exceptions import InputValidationError, InvalidChoiceError
16
+ from .files import File, Json, Path, Pickle, Serialized
17
+ from .numeric import NumRange, Range
18
+ from .patterns import Glob, Regex, RegexMode
19
+ from .time import Date, DateTime, Day, DTFormatMode, Month, Time, TimeDelta
20
+ from .utils import FileWrapper, StatMode
21
21
 
22
22
  if _t.TYPE_CHECKING:
23
- from ..typing import TypeFunc, InputTypeFunc, ChoicesType
23
+ from ..typing import ChoicesType, InputTypeFunc, TypeFunc
24
24
 
25
25
  # fmt: off
26
26
  __all__ = [
@@ -8,9 +8,9 @@ from __future__ import annotations
8
8
 
9
9
  from abc import ABC, abstractmethod
10
10
  from enum import Enum
11
- from typing import TYPE_CHECKING, Any, Type, TypeVar, Collection, Iterator, Optional, Set, Mapping
11
+ from typing import TYPE_CHECKING, Any, Collection, Iterator, Mapping, Optional, Set, Type, TypeVar
12
12
 
13
- from ..typing import TypeFunc, T
13
+ from ..typing import T, TypeFunc
14
14
  from .base import InputType
15
15
  from .exceptions import InvalidChoiceError
16
16
 
@@ -4,7 +4,7 @@ Exceptions for custom input types / validators.
4
4
  :author: Doug Skrypa
5
5
  """
6
6
 
7
- from typing import Collection, Any
7
+ from typing import Any, Collection
8
8
 
9
9
  from ..exceptions import CommandParserException
10
10
 
@@ -112,6 +112,18 @@ class DTFormatMode(MissingMixin, Enum):
112
112
 
113
113
 
114
114
  class CalendarUnitInput(DTInput[Union[str, int]], ABC):
115
+ """
116
+ Input type representing a date/time unit.
117
+
118
+ :param full: Allow the full unit name to be provided
119
+ :param abbreviation: Allow abbreviations of unit names to be provided
120
+ :param numeric: Allow unit values to be specified as a decimal number
121
+ :param locale: An alternate locale to use when parsing input
122
+ :param out_format: A :class:`DTFormatMode` or str that matches a format mode. Defaults to full weekday name.
123
+ :param out_locale: Alternate locale to use for output. Defaults to the same value as ``locale``.
124
+ :param fix_default: Whether default values should be normalized using :meth:`~DTInput.fix_default`.
125
+ """
126
+
115
127
  __slots__ = ('full', 'abbreviation', 'numeric', 'out_format', 'out_locale')
116
128
  _formats: dict[DTFormatMode, Sequence[Union[str, int]]]
117
129
  _min_index: int = 0
@@ -131,17 +143,6 @@ class CalendarUnitInput(DTInput[Union[str, int]], ABC):
131
143
  out_locale: Locale = None,
132
144
  fix_default: Bool = True,
133
145
  ):
134
- """
135
- Input type representing a date/time unit.
136
-
137
- :param full: Allow the full unit name to be provided
138
- :param abbreviation: Allow abbreviations of unit names to be provided
139
- :param numeric: Allow unit values to be specified as a decimal number
140
- :param locale: An alternate locale to use when parsing input
141
- :param out_format: A :class:`DTFormatMode` or str that matches a format mode. Defaults to full weekday name.
142
- :param out_locale: Alternate locale to use for output. Defaults to the same value as ``locale``.
143
- :param fix_default: Whether default values should be normalized using :meth:`~DTInput.fix_default`.
144
- """
145
146
  if not (full or abbreviation or numeric):
146
147
  raise ValueError('At least one of full, abbreviation, or numeric must be True')
147
148
  super().__init__(locale=locale, fix_default=fix_default)
@@ -224,6 +225,20 @@ class CalendarUnitInput(DTInput[Union[str, int]], ABC):
224
225
 
225
226
 
226
227
  class Day(CalendarUnitInput, dt_type='day of the week'):
228
+ """
229
+ Input type representing a day of the week.
230
+
231
+ :param full: Allow the full day name to be provided
232
+ :param abbreviation: Allow abbreviations of day names to be provided
233
+ :param numeric: Allow weekdays to be specified as a decimal number
234
+ :param iso: Ignored if ``numeric`` is False. If True, then numeric weekdays are treated as ISO 8601 weekdays,
235
+ where 1 is Monday and 7 is Sunday. If False, then 0 is Monday and 6 is Sunday.
236
+ :param locale: An alternate locale to use when parsing input
237
+ :param out_format: A :class:`DTFormatMode` or str that matches a format mode. Defaults to full weekday name.
238
+ :param out_locale: Alternate locale to use for output. Defaults to the same value as ``locale``.
239
+ :param fix_default: Whether default values should be normalized using :meth:`~DTInput.fix_default`.
240
+ """
241
+
227
242
  __slots__ = ('iso',)
228
243
  _formats = {
229
244
  DTFormatMode.FULL: day_name,
@@ -244,21 +259,7 @@ class Day(CalendarUnitInput, dt_type='day of the week'):
244
259
  out_format: Union[str, DTFormatMode] = DTFormatMode.FULL,
245
260
  out_locale: Locale = None,
246
261
  fix_default: Bool = True,
247
- ):
248
- """
249
- Input type representing a day of the week.
250
-
251
- :param full: Allow the full day name to be provided
252
- :param abbreviation: Allow abbreviations of day names to be provided
253
- :param numeric: Allow weekdays to be specified as a decimal number
254
- :param iso: Ignored if ``numeric`` is False. If True, then numeric weekdays are treated as ISO 8601 weekdays,
255
- where 1 is Monday and 7 is Sunday. If False, then 0 is Monday and 6 is Sunday.
256
- :param locale: An alternate locale to use when parsing input
257
- :param out_format: A :class:`DTFormatMode` or str that matches a format mode. Defaults to full weekday name.
258
- :param out_locale: Alternate locale to use for output. Defaults to the same value as ``locale``.
259
- :param fix_default: Whether default values should be normalized using :meth:`~DTInput.fix_default`.
260
- """
261
- ...
262
+ ): ...
262
263
 
263
264
  def __init__(self, *, iso: Bool = False, **kwargs):
264
265
  super().__init__(**kwargs)
@@ -283,6 +284,18 @@ class Day(CalendarUnitInput, dt_type='day of the week'):
283
284
 
284
285
 
285
286
  class Month(CalendarUnitInput, dt_type='month', min_index=1):
287
+ """
288
+ Input type representing a month.
289
+
290
+ :param full: Allow the full month name to be provided
291
+ :param abbreviation: Allow abbreviations of month names to be provided
292
+ :param numeric: Allow months to be specified as a decimal number
293
+ :param locale: An alternate locale to use when parsing input
294
+ :param out_format: A :class:`DTFormatMode` or str that matches a format mode. Defaults to full month name.
295
+ :param out_locale: Alternate locale to use for output. Defaults to the same value as ``locale``.
296
+ :param fix_default: Whether default values should be normalized using :meth:`~DTInput.fix_default`.
297
+ """
298
+
286
299
  __slots__ = ()
287
300
  _formats = {
288
301
  DTFormatMode.FULL: month_name,
@@ -301,19 +314,7 @@ class Month(CalendarUnitInput, dt_type='month', min_index=1):
301
314
  out_format: Union[str, DTFormatMode] = DTFormatMode.FULL,
302
315
  out_locale: Locale = None,
303
316
  fix_default: Bool = True,
304
- ):
305
- """
306
- Input type representing a month.
307
-
308
- :param full: Allow the full month name to be provided
309
- :param abbreviation: Allow abbreviations of month names to be provided
310
- :param numeric: Allow months to be specified as a decimal number
311
- :param locale: An alternate locale to use when parsing input
312
- :param out_format: A :class:`DTFormatMode` or str that matches a format mode. Defaults to full month name.
313
- :param out_locale: Alternate locale to use for output. Defaults to the same value as ``locale``.
314
- :param fix_default: Whether default values should be normalized using :meth:`~DTInput.fix_default`.
315
- """
316
- ...
317
+ ): ...
317
318
 
318
319
  def __init__(self, *, numeric: Bool = True, **kwargs):
319
320
  super().__init__(numeric=numeric, **kwargs)
@@ -403,6 +404,7 @@ class DateTimeInput(DTInput[DT], ABC):
403
404
  _type: Type[DT]
404
405
  _earliest: TimeBound = None
405
406
  _latest: TimeBound = None
407
+ # TODO: Add usage examples to the more user-friendly docs
406
408
 
407
409
  def __init_subclass__(cls, type: Type[DT], **kwargs): # noqa
408
410
  super().__init_subclass__(dt_type=type.__name__, **kwargs)
@@ -501,6 +503,18 @@ class DateTimeInput(DTInput[DT], ABC):
501
503
 
502
504
 
503
505
  class DateTime(DateTimeInput[datetime], type=datetime):
506
+ """
507
+ Input type that accepts any number of datetime format strings for parsing input. Parsing results in returning
508
+ a :class:`python:datetime.datetime` object.
509
+
510
+ :param formats: One or more :ref:`datetime format strings <python:strftime-strptime-behavior>`. Defaults to
511
+ :data:`DEFAULT_DT_FMT`.
512
+ :param locale: An alternate locale to use when parsing input
513
+ :param earliest: If specified, the parsed value must be later than or equal to this
514
+ :param latest: If specified, the parsed value must be earlier than or equal to this
515
+ :param fix_default: Whether default values should be normalized using :meth:`~DTInput.fix_default`.
516
+ """
517
+
504
518
  def __init__(
505
519
  self,
506
520
  *formats: str,
@@ -509,23 +523,24 @@ class DateTime(DateTimeInput[datetime], type=datetime):
509
523
  latest: TimeBound = None,
510
524
  fix_default: Bool = True,
511
525
  ):
512
- """
513
- Input type that accepts any number of datetime format strings for parsing input. Parsing results in returning
514
- a :class:`python:datetime.datetime` object.
515
-
516
- :param formats: One or more :ref:`datetime format strings <python:strftime-strptime-behavior>`. Defaults to
517
- :data:`DEFAULT_DT_FMT`.
518
- :param locale: An alternate locale to use when parsing input
519
- :param earliest: If specified, the parsed value must be later than or equal to this
520
- :param latest: If specified, the parsed value must be earlier than or equal to this
521
- :param fix_default: Whether default values should be normalized using :meth:`~DTInput.fix_default`.
522
- """
523
526
  super().__init__(
524
527
  formats or (DEFAULT_DT_FMT,), locale=locale, earliest=earliest, latest=latest, fix_default=fix_default
525
528
  )
526
529
 
527
530
 
528
531
  class Date(DateTimeInput[date], type=date):
532
+ """
533
+ Input type that accepts any number of datetime format strings for parsing input. Parsing results in returning
534
+ a :class:`python:datetime.date` object.
535
+
536
+ :param formats: One or more :ref:`datetime format strings <python:strftime-strptime-behavior>`. Defaults to
537
+ :data:`DEFAULT_DT_FMT`.
538
+ :param locale: An alternate locale to use when parsing input
539
+ :param earliest: If specified, the parsed value must be later than or equal to this
540
+ :param latest: If specified, the parsed value must be earlier than or equal to this
541
+ :param fix_default: Whether default values should be normalized using :meth:`~DTInput.fix_default`.
542
+ """
543
+
529
544
  def __init__(
530
545
  self,
531
546
  *formats: str,
@@ -534,23 +549,24 @@ class Date(DateTimeInput[date], type=date):
534
549
  latest: TimeBound = None,
535
550
  fix_default: Bool = True,
536
551
  ):
537
- """
538
- Input type that accepts any number of datetime format strings for parsing input. Parsing results in returning
539
- a :class:`python:datetime.date` object.
540
-
541
- :param formats: One or more :ref:`datetime format strings <python:strftime-strptime-behavior>`. Defaults to
542
- :data:`DEFAULT_DT_FMT`.
543
- :param locale: An alternate locale to use when parsing input
544
- :param earliest: If specified, the parsed value must be later than or equal to this
545
- :param latest: If specified, the parsed value must be earlier than or equal to this
546
- :param fix_default: Whether default values should be normalized using :meth:`~DTInput.fix_default`.
547
- """
548
552
  super().__init__(
549
553
  formats or (DEFAULT_DATE_FMT,), locale=locale, earliest=earliest, latest=latest, fix_default=fix_default
550
554
  )
551
555
 
552
556
 
553
557
  class Time(DateTimeInput[time], type=time):
558
+ """
559
+ Input type that accepts any number of datetime format strings for parsing input. Parsing results in returning
560
+ a :class:`python:datetime.time` object.
561
+
562
+ :param formats: One or more :ref:`datetime format strings <python:strftime-strptime-behavior>`. Defaults to
563
+ :data:`DEFAULT_DT_FMT`.
564
+ :param locale: An alternate locale to use when parsing input
565
+ :param earliest: If specified, the parsed value must be later than or equal to this
566
+ :param latest: If specified, the parsed value must be earlier than or equal to this
567
+ :param fix_default: Whether default values should be normalized using :meth:`~DTInput.fix_default`.
568
+ """
569
+
554
570
  def __init__(
555
571
  self,
556
572
  *formats: str,
@@ -559,17 +575,6 @@ class Time(DateTimeInput[time], type=time):
559
575
  latest: TimeBound = None,
560
576
  fix_default: Bool = True,
561
577
  ):
562
- """
563
- Input type that accepts any number of datetime format strings for parsing input. Parsing results in returning
564
- a :class:`python:datetime.time` object.
565
-
566
- :param formats: One or more :ref:`datetime format strings <python:strftime-strptime-behavior>`. Defaults to
567
- :data:`DEFAULT_DT_FMT`.
568
- :param locale: An alternate locale to use when parsing input
569
- :param earliest: If specified, the parsed value must be later than or equal to this
570
- :param latest: If specified, the parsed value must be earlier than or equal to this
571
- :param fix_default: Whether default values should be normalized using :meth:`~DTInput.fix_default`.
572
- """
573
578
  super().__init__(
574
579
  formats or (DEFAULT_TIME_FMT,), locale=locale, earliest=earliest, latest=latest, fix_default=fix_default
575
580
  )
@@ -11,7 +11,7 @@ import warnings
11
11
  from contextlib import contextmanager
12
12
  from pathlib import Path
13
13
  from stat import S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK
14
- from typing import TYPE_CHECKING, Any, BinaryIO, ContextManager, TextIO, Union
14
+ from typing import TYPE_CHECKING, Any, BinaryIO, Iterator, TextIO, Union
15
15
  from weakref import finalize
16
16
 
17
17
  from ..utils import FixedFlag
@@ -169,7 +169,7 @@ class FileWrapper:
169
169
  self._close()
170
170
 
171
171
  @contextmanager
172
- def _file(self) -> ContextManager[FP]:
172
+ def _file(self) -> Iterator[FP]:
173
173
  try:
174
174
  yield self._open()
175
175
  finally:
@@ -0,0 +1,6 @@
1
+ from .base import BaseOption, BasePositional, Parameter
2
+ from .choice_map import Action, SubCommand
3
+ from .groups import ParamGroup
4
+ from .options import ActionFlag, Counter, Flag, Option, TriFlag, action_flag, after_main, before_main, help_action
5
+ from .pass_thru import PassThru
6
+ from .positionals import Positional
@@ -96,6 +96,14 @@ class ParamBase(ABC):
96
96
 
97
97
  # endregion
98
98
 
99
+ def __eq__(self, other: ParamBase) -> bool:
100
+ return (
101
+ self.__class__ == other.__class__
102
+ and self._attr_name == other._attr_name
103
+ and self._name == other._name
104
+ and self.command == other.command
105
+ )
106
+
99
107
  def __hash__(self) -> int:
100
108
  return hash(self.__class__) ^ hash(self._attr_name) ^ hash(self._name) ^ hash(self.command)
101
109
 
@@ -410,6 +410,15 @@ class ActionFlag(Flag, repr_attrs=('order', 'before_main')):
410
410
  # Note: If func is None, then CommandParameters._process_action_flags raises ParameterDefinitionError
411
411
  return partial(self.func, command) # imitates a bound method
412
412
 
413
+ def __reduce__(self):
414
+ # When a string is returned, it is treated as the name of a global variable. Since this class acts as a
415
+ # decorator that replaces the decorated method, this approach is necessary to prevent the following kind of
416
+ # PicklingError: Can't pickle <function help_action ...>: it's not the same object as ...options.help_action
417
+ if self._func is not None:
418
+ return self.__qualname__
419
+ else: # This is not generally expected
420
+ return super().__reduce__()
421
+
413
422
 
414
423
  #: Alias for :class:`ActionFlag`
415
424
  action_flag = ActionFlag # pylint: disable=C0103
@@ -12,11 +12,11 @@ from ..exceptions import ParameterDefinitionError
12
12
  from ..inputs import normalize_input_type
13
13
  from ..nargs import Nargs, NargsValue
14
14
  from ..utils import _NotSet
15
- from .actions import Store, Append
16
- from .base import BasePositional, AllowLeadingDashProperty
15
+ from .actions import Append, Store
16
+ from .base import AllowLeadingDashProperty, BasePositional
17
17
 
18
18
  if TYPE_CHECKING:
19
- from ..typing import InputTypeFunc, ChoicesType, LeadingDash, DefaultFunc
19
+ from ..typing import ChoicesType, DefaultFunc, InputTypeFunc, LeadingDash
20
20
 
21
21
  __all__ = ['Positional']
22
22
 
@@ -13,7 +13,7 @@ from difflib import unified_diff
13
13
  from io import BytesIO, StringIO
14
14
  from pathlib import Path
15
15
  from tempfile import TemporaryDirectory
16
- from typing import IO, TYPE_CHECKING, Any, Callable, ContextManager, Dict, Iterable, List, Tuple, Type, Union
16
+ from typing import IO, TYPE_CHECKING, Any, Callable, Iterable, Iterator, Type, Union
17
17
  from unittest import TestCase
18
18
  from unittest.mock import Mock, patch, seal
19
19
 
@@ -39,16 +39,16 @@ __all__ = [
39
39
  'TemporaryDir',
40
40
  ]
41
41
 
42
- Argv = List[str]
43
- Expected = Dict[str, Any]
44
- Kwargs = Dict[str, Any]
45
- Env = Dict[str, str]
46
- Case = Tuple[Argv, Expected]
47
- EnvCase = Tuple[Argv, Env, Expected]
42
+ Argv = list[str]
43
+ Expected = dict[str, Any]
44
+ Kwargs = dict[str, Any]
45
+ Env = dict[str, str]
46
+ Case = tuple[Argv, Expected]
47
+ EnvCase = tuple[Argv, Env, Expected]
48
48
  ExcType = Type[Exception]
49
- ExceptionCase = Union[Argv, Tuple[Argv, ExcType], Tuple[Argv, ExcType, str]]
49
+ ExceptionCase = Union[Argv, tuple[Argv, ExcType], tuple[Argv, ExcType, str]]
50
50
  ExcCases = Iterable[ExceptionCase]
51
- CallExceptionCase = Union[Tuple[Kwargs, ExcType], Tuple[Kwargs, ExcType, str]]
51
+ CallExceptionCase = Union[tuple[Kwargs, ExcType], tuple[Kwargs, ExcType, str]]
52
52
  CallExceptionCases = Iterable[CallExceptionCase]
53
53
 
54
54
  OPT_ENV_MOD = 'cli_command_parser.parser.environ'
@@ -344,7 +344,7 @@ def sealed_mock(*args, **kwargs):
344
344
 
345
345
 
346
346
  @contextmanager
347
- def load_command(directory: Path, name: str, cmd_name: str, **kwargs) -> ContextManager[CommandCls]:
347
+ def load_command(directory: Path, name: str, cmd_name: str, **kwargs) -> Iterator[CommandCls]:
348
348
  path = directory.joinpath(name)
349
349
  with Context.for_prog(path, **kwargs):
350
350
  yield load_commands(path)[cmd_name]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli_command_parser
3
- Version: 2025.5.10
3
+ Version: 2025.7.10
4
4
  Summary: CLI Command Parser
5
5
  Home-page: https://github.com/dskrypa/cli_command_parser
6
6
  Author: Doug Skrypa
@@ -58,11 +58,11 @@ CLI Command Parser is a class-based CLI argument parser that defines parameters
58
58
  tools to quickly and easily get started with basic CLIs, and it scales well to support even very large and complex
59
59
  CLIs while remaining readable and easy to maintain.
60
60
 
61
- The primary goals of this project:
62
- - Make it easy to define subcommands and actions in an clean and organized manner
63
- - Allow for inheritance so that common parameters don't need to be repeated
64
- - Make it easy to handle common initialization tasks for all actions / subcommands once
65
- - Reduce the amount of boilerplate code that is necessary for setting up parsing and handling argument values
61
+ Some of the primary goals and key features of this project:
62
+ - Minimal boilerplate code is necessary to define CLI parameters and access their parsed values
63
+ - Easy to use type annotations for CLI parameters
64
+ - Subcommands can inherit common parameters so they don't need to be repeated
65
+ - Easy to handle common initialization tasks for all actions / subcommands once
66
66
 
67
67
 
68
68
  Example Program
@@ -16,7 +16,6 @@ lib/cli_command_parser/config.py
16
16
  lib/cli_command_parser/context.py
17
17
  lib/cli_command_parser/core.py
18
18
  lib/cli_command_parser/documentation.py
19
- lib/cli_command_parser/error_handling.py
20
19
  lib/cli_command_parser/exceptions.py
21
20
  lib/cli_command_parser/metadata.py
22
21
  lib/cli_command_parser/nargs.py
@@ -39,6 +38,10 @@ lib/cli_command_parser/conversion/cli.py
39
38
  lib/cli_command_parser/conversion/command_builder.py
40
39
  lib/cli_command_parser/conversion/utils.py
41
40
  lib/cli_command_parser/conversion/visitor.py
41
+ lib/cli_command_parser/error_handling/__init__.py
42
+ lib/cli_command_parser/error_handling/base.py
43
+ lib/cli_command_parser/error_handling/other.py
44
+ lib/cli_command_parser/error_handling/windows.py
42
45
  lib/cli_command_parser/formatting/__init__.py
43
46
  lib/cli_command_parser/formatting/commands.py
44
47
  lib/cli_command_parser/formatting/params.py
@@ -26,11 +26,11 @@ CLI Command Parser is a class-based CLI argument parser that defines parameters
26
26
  tools to quickly and easily get started with basic CLIs, and it scales well to support even very large and complex
27
27
  CLIs while remaining readable and easy to maintain.
28
28
 
29
- The primary goals of this project:
30
- - Make it easy to define subcommands and actions in an clean and organized manner
31
- - Allow for inheritance so that common parameters don't need to be repeated
32
- - Make it easy to handle common initialization tasks for all actions / subcommands once
33
- - Reduce the amount of boilerplate code that is necessary for setting up parsing and handling argument values
29
+ Some of the primary goals and key features of this project:
30
+ - Minimal boilerplate code is necessary to define CLI parameters and access their parsed values
31
+ - Easy to use type annotations for CLI parameters
32
+ - Subcommands can inherit common parameters so they don't need to be repeated
33
+ - Easy to handle common initialization tasks for all actions / subcommands once
34
34
 
35
35
 
36
36
  Example Program
@@ -1,3 +0,0 @@
1
- from .argparse_ast import Script, AstCallable, ArgCollection, AstArgumentParser, SubParser, AddVisitedChild, visit_func
2
- from .argparse_ast import ArgGroup, MutuallyExclusiveGroup
3
- from .command_builder import Converter, convert_script
@@ -1,6 +0,0 @@
1
- from .base import Parameter, BasePositional, BaseOption
2
- from .choice_map import SubCommand, Action
3
- from .groups import ParamGroup
4
- from .options import Option, Flag, Counter, ActionFlag, action_flag, before_main, after_main, TriFlag, help_action
5
- from .pass_thru import PassThru
6
- from .positionals import Positional