cli-command-parser 2026.2.1__tar.gz → 2026.6.27__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 (76) hide show
  1. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/MANIFEST.in +1 -0
  2. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/PKG-INFO +12 -4
  3. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/__init__.py +2 -1
  4. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/__version__.py +1 -1
  5. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/command_parameters.py +39 -27
  6. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/commands.py +17 -34
  7. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/compat.py +1 -1
  8. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/config.py +64 -56
  9. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/context.py +51 -31
  10. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/conversion/argparse_ast.py +144 -68
  11. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/conversion/cli.py +11 -6
  12. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/conversion/command_builder.py +35 -31
  13. cli_command_parser-2026.6.27/lib/cli_command_parser/conversion/utils.py +58 -0
  14. cli_command_parser-2026.6.27/lib/cli_command_parser/conversion/visitor.py +346 -0
  15. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/core.py +51 -30
  16. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/documentation.py +37 -29
  17. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/error_handling/base.py +13 -9
  18. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/error_handling/windows.py +2 -2
  19. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/exceptions.py +11 -12
  20. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/formatting/commands.py +31 -15
  21. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/formatting/params.py +106 -79
  22. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/formatting/restructured_text.py +21 -13
  23. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/formatting/utils.py +3 -3
  24. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/inputs/__init__.py +40 -14
  25. cli_command_parser-2026.6.27/lib/cli_command_parser/inputs/_typing.py +78 -0
  26. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/inputs/base.py +2 -2
  27. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/inputs/choices.py +29 -19
  28. cli_command_parser-2026.6.27/lib/cli_command_parser/inputs/files.py +538 -0
  29. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/inputs/numeric.py +132 -46
  30. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/inputs/patterns.py +51 -19
  31. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/inputs/time.py +128 -55
  32. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/inputs/utils.py +156 -47
  33. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/metadata.py +94 -65
  34. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/nargs.py +38 -18
  35. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/parameters/__init__.py +1 -1
  36. cli_command_parser-2026.6.27/lib/cli_command_parser/parameters/_typing.py +15 -0
  37. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/parameters/actions.py +52 -40
  38. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/parameters/base.py +290 -108
  39. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/parameters/choice_map.py +54 -53
  40. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/parameters/groups.py +11 -8
  41. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/parameters/option_strings.py +4 -12
  42. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/parameters/options.py +314 -80
  43. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/parameters/pass_thru.py +4 -2
  44. cli_command_parser-2026.6.27/lib/cli_command_parser/parameters/positionals.py +167 -0
  45. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/parse_tree.py +66 -51
  46. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/parser.py +18 -18
  47. cli_command_parser-2026.6.27/lib/cli_command_parser/py.typed +0 -0
  48. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/testing.py +46 -38
  49. cli_command_parser-2026.6.27/lib/cli_command_parser/typing.py +60 -0
  50. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/utils.py +35 -20
  51. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser.egg-info/PKG-INFO +12 -4
  52. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser.egg-info/SOURCES.txt +3 -0
  53. cli_command_parser-2026.6.27/lib/cli_command_parser.egg-info/requires.txt +10 -0
  54. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/pyproject.toml +1 -0
  55. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/readme.rst +5 -3
  56. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/requirements-dev.txt +1 -5
  57. cli_command_parser-2026.2.1/lib/cli_command_parser/conversion/utils.py +0 -38
  58. cli_command_parser-2026.2.1/lib/cli_command_parser/conversion/visitor.py +0 -227
  59. cli_command_parser-2026.2.1/lib/cli_command_parser/inputs/files.py +0 -246
  60. cli_command_parser-2026.2.1/lib/cli_command_parser/parameters/positionals.py +0 -91
  61. cli_command_parser-2026.2.1/lib/cli_command_parser/typing.py +0 -82
  62. cli_command_parser-2026.2.1/lib/cli_command_parser.egg-info/requires.txt +0 -3
  63. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/LICENSE +0 -0
  64. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/__main__.py +0 -0
  65. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/annotations.py +0 -0
  66. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/conversion/__init__.py +0 -0
  67. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/conversion/__main__.py +0 -0
  68. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/conversion/argparse_utils.py +0 -0
  69. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/error_handling/__init__.py +0 -0
  70. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/error_handling/other.py +0 -0
  71. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/formatting/__init__.py +0 -0
  72. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/inputs/exceptions.py +0 -0
  73. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser.egg-info/dependency_links.txt +0 -0
  74. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser.egg-info/entry_points.txt +0 -0
  75. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser.egg-info/top_level.txt +0 -0
  76. {cli_command_parser-2026.2.1 → cli_command_parser-2026.6.27}/setup.cfg +0 -0
@@ -1,4 +1,5 @@
1
1
  include *.rst
2
2
  include *.txt
3
+ include *.typed
3
4
  include LICENSE
4
5
  include MANIFEST.in
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cli_command_parser
3
- Version: 2026.2.1
3
+ Version: 2026.6.27
4
4
  Summary: CLI Command Parser
5
5
  Author-email: Doug Skrypa <dskrypa@gmail.com>
6
6
  Project-URL: Source, https://github.com/dskrypa/cli_command_parser
@@ -25,6 +25,12 @@ Description-Content-Type: text/x-rst
25
25
  License-File: LICENSE
26
26
  Provides-Extra: wcwidth
27
27
  Requires-Dist: wcwidth; extra == "wcwidth"
28
+ Provides-Extra: dev
29
+ Requires-Dist: ruff; extra == "dev"
30
+ Requires-Dist: pytest; extra == "dev"
31
+ Requires-Dist: pytest-cov; extra == "dev"
32
+ Requires-Dist: coverage; extra == "dev"
33
+ Requires-Dist: mypy>=2.1.0; extra == "dev"
28
34
  Dynamic: license-file
29
35
 
30
36
  CLI Command Parser
@@ -57,7 +63,7 @@ CLIs while remaining readable and easy to maintain.
57
63
 
58
64
  Some of the primary goals and key features of this project:
59
65
  - Minimal boilerplate code is necessary to define CLI parameters and access their parsed values
60
- - Easy to use type annotations for CLI parameters
66
+ - Typing support for CLI parameters that can be validated via type checkers
61
67
  - Subcommands can inherit common parameters so they don't need to be repeated
62
68
  - Easy to handle common initialization tasks for all actions / subcommands once
63
69
 
@@ -69,9 +75,11 @@ Example Program
69
75
 
70
76
  from cli_command_parser import Command, Option, main
71
77
 
72
- class Hello(Command, description='Simple greeting example'):
78
+ class Hello(Command):
79
+ """Simple greeting example"""
80
+
73
81
  name = Option('-n', default='World', help='The person to say hello to')
74
- count: int = Option('-c', default=1, help='Number of times to repeat the message')
82
+ count = Option('-c', type=int, default=1, help='Number of times to repeat the message')
75
83
 
76
84
  def main(self):
77
85
  for _ in range(self.count):
@@ -42,6 +42,7 @@ from .parameters import (
42
42
  Counter,
43
43
  Flag,
44
44
  Option,
45
+ Param,
45
46
  Parameter,
46
47
  ParamGroup,
47
48
  PassThru,
@@ -52,4 +53,4 @@ from .parameters import (
52
53
  after_main,
53
54
  before_main,
54
55
  )
55
- from .typing import Param, ParamOrGroup
56
+ from .typing import 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__ = '2026.02.01'
4
+ __version__ = '2026.06.27'
5
5
  __author__ = 'Doug Skrypa'
6
6
  __author_email__ = 'dskrypa@gmail.com'
7
7
  __license__ = 'Apache 2.0'
@@ -12,20 +12,25 @@ from __future__ import annotations
12
12
 
13
13
  from collections import defaultdict
14
14
  from functools import cached_property
15
- from typing import TYPE_CHECKING, Collection, Iterator
15
+ from typing import TYPE_CHECKING, Any, Collection, Iterator, Type, TypeAlias
16
16
 
17
17
  from .config import AmbiguousComboMode, CommandConfig
18
18
  from .exceptions import AmbiguousCombo, AmbiguousShortForm, CommandDefinitionError, ParameterDefinitionError
19
- from .parameters import Action, ActionFlag, ParamGroup, PassThru, SubCommand, help_action
19
+ from .parameters import ActionFlag, ParamGroup, PassThru, help_action
20
20
  from .parameters.base import BaseOption, BasePositional, ParamBase, Parameter
21
+ from .parameters.choice_map import Action, SubCommand
21
22
 
22
23
  if TYPE_CHECKING:
24
+ from .commands import Command
23
25
  from .context import Context
26
+ from .core import CommandMeta
24
27
  from .formatting.commands import CommandHelpFormatter
25
- from .typing import CommandCls, Strings
28
+ from .typing import Bool, Strings
26
29
 
30
+ CommandCls: TypeAlias = Type[Command] | CommandMeta
27
31
  OptionMap = dict[str, BaseOption]
28
32
  ActionFlags = list[ActionFlag]
33
+ Positionals = list[BasePositional] | tuple[()]
29
34
 
30
35
  __all__ = ['CommandParameters']
31
36
 
@@ -33,18 +38,14 @@ __all__ = ['CommandParameters']
33
38
  class CommandParameters:
34
39
  # fmt: off
35
40
  command: CommandCls #: The Command associated with this CommandParameters object
36
- formatter: CommandHelpFormatter #: The formatter used for this Command's help text
37
41
  parent: CommandParameters | None #: The parent Command's CommandParameters
38
- action: Action | None = None #: An Action Parameter, if specified
39
- _pass_thru: PassThru | None = None #: A PassThru Parameter, if specified
40
- sub_command: SubCommand | None = None #: A SubCommand Parameter, if specified
41
42
  action_flags: ActionFlags #: List of action flags
42
43
  split_action_flags: tuple[ActionFlags, ActionFlags] #: Action flags split by before/after main
43
44
  options: list[BaseOption] #: List of optional Parameters
44
45
  combo_option_map: OptionMap #: Mapping of {short opt: Parameter} (no dash characters)
45
46
  groups: list[ParamGroup] #: List of ParamGroup objects
46
47
  positionals: list[BasePositional] #: List of positional Parameters
47
- _deferred_positionals: list[BasePositional] = () #: Positional Parameters that are deferred to sub commands
48
+ _deferred_positionals: Positionals = () #: Positional Parameters that are deferred to sub commands
48
49
  option_map: OptionMap #: Mapping of {--opt / -opt: Parameter}
49
50
  # fmt: on
50
51
 
@@ -52,6 +53,12 @@ class CommandParameters:
52
53
  self.command = command
53
54
  self.parent = parent_params
54
55
  self.config = config
56
+ # fmt: off
57
+ # These are annotated here because mypy thinks they're invoked as descriptors when annotated at the class level
58
+ self.action: Action | None = None #: An Action Parameter, if specified
59
+ self.sub_command: SubCommand | None = None #: A SubCommand Parameter, if specified
60
+ self._pass_thru: PassThru | None = None #: A PassThru Parameter, if specified
61
+ # fmt: on
55
62
  self._process_parameters()
56
63
 
57
64
  def __repr__(self) -> str:
@@ -77,11 +84,8 @@ class CommandParameters:
77
84
 
78
85
  @cached_property
79
86
  def all_positionals(self) -> list[BasePositional]:
80
- try:
81
- if not self.parent.sub_command:
82
- return self.parent.all_positionals + self.positionals
83
- except AttributeError:
84
- pass
87
+ if self.parent and not self.parent.sub_command:
88
+ return self.parent.all_positionals + self.positionals
85
89
  return self.positionals
86
90
 
87
91
  def get_positionals_to_parse(self, ctx: Context) -> list[BasePositional]:
@@ -94,6 +98,7 @@ class CommandParameters:
94
98
 
95
99
  @cached_property
96
100
  def formatter(self) -> CommandHelpFormatter:
101
+ """The formatter used for this Command's help text."""
97
102
  from .formatting.commands import CommandHelpFormatter
98
103
 
99
104
  formatter_factory = self.config.command_formatter or CommandHelpFormatter
@@ -105,13 +110,13 @@ class CommandParameters:
105
110
  return formatter
106
111
 
107
112
  @cached_property
108
- def _has_help(self) -> bool:
113
+ def _has_help(self) -> Bool:
109
114
  return help_action in self.action_flags or (self.parent and self.parent._has_help)
110
115
 
111
116
  # region Initialization
112
117
 
113
118
  def _iter_parameters(self) -> Iterator[ParamBase]:
114
- name_param_map = {} # Allow subclasses to override names, but not within a given command
119
+ name_param_map: dict[str, Any] = {} # Allow subclasses to override names, but not within a given command
115
120
  for item in self.command.__dict__.items():
116
121
  attr, param = item
117
122
  if attr.startswith('__') or not isinstance(param, ParamBase): # Name mangled Parameters are still processed
@@ -173,7 +178,9 @@ class CommandParameters:
173
178
  self.groups = sorted(groups) if groups else []
174
179
 
175
180
  def _process_positionals(self, params: list[BasePositional]):
176
- unfollowable = action_or_sub_cmd = split_index = None
181
+ unfollowable: BasePositional | None = None
182
+ action_or_sub_cmd: SubCommand | Action | None = None
183
+ split_index: int = 0
177
184
  if self.parent and (deferred := self.parent._deferred_positionals):
178
185
  params = deferred + params
179
186
 
@@ -186,26 +193,28 @@ class CommandParameters:
186
193
  raise CommandDefinitionError(
187
194
  f'Additional Positional parameters cannot follow {unfollowable} {why} - {param=} is invalid'
188
195
  )
189
- elif isinstance(param, (SubCommand, Action)):
196
+
197
+ if isinstance(param, (SubCommand, Action)):
190
198
  if action_or_sub_cmd:
191
199
  raise CommandDefinitionError(
192
200
  f'Only 1 Action xor SubCommand is allowed in a given Command - {self.command.__name__} cannot'
193
201
  f' contain both {action_or_sub_cmd} and {param}'
194
202
  )
195
- elif isinstance(param, SubCommand):
203
+
204
+ if isinstance(param, SubCommand):
196
205
  self.sub_command = action_or_sub_cmd = param
197
206
  split_index = i + 1
198
207
  if param.has_choices and 0 in param.nargs: # It has local choices or is not required
199
208
  unfollowable = param
200
209
  else: # It's an Action
201
- self.action = action_or_sub_cmd = param # type: ignore
210
+ self.action = action_or_sub_cmd = param
202
211
  if not param.has_choices:
203
212
  raise CommandDefinitionError(f'No choices were registered for {self.action}')
204
213
  elif 0 in param.nargs or (param.nargs.variable and not param.has_choices):
205
214
  unfollowable = param
206
215
 
207
216
  if split_index:
208
- if self.sub_command.has_local_choices:
217
+ if self.sub_command.has_local_choices: # type: ignore[union-attr]
209
218
  self._deferred_positionals = params[split_index:]
210
219
  else:
211
220
  params, self._deferred_positionals = params[:split_index], params[split_index:]
@@ -252,19 +261,22 @@ class CommandParameters:
252
261
  f'{opt_type} {option=} conflict for command={self.command!r} between {existing} and {param}'
253
262
  )
254
263
 
255
- def _process_action_flags(self):
256
- action_flags = sorted(p for p in self.options if isinstance(p, ActionFlag))
257
- grouped_ordered_flags = {True: defaultdict(list), False: defaultdict(list)}
264
+ def _process_action_flags(self) -> None:
265
+ action_flags: ActionFlags = sorted(p for p in self.options if isinstance(p, ActionFlag)) # type: ignore[misc]
266
+ grouped_ordered_flags: dict[bool, dict[int | float, ActionFlags]] = {
267
+ True: defaultdict(list),
268
+ False: defaultdict(list),
269
+ }
258
270
  for param in action_flags:
259
271
  if param.func is None:
260
272
  raise ParameterDefinitionError(f'No function was registered for {param=}')
261
273
  grouped_ordered_flags[param.before_main][param.order].append(param)
262
274
 
263
275
  found_non_always = False
264
- invalid = {}
276
+ invalid: dict[tuple[bool, int | float], ActionFlags | ActionFlag] = {}
265
277
  for before_main, prio_params in grouped_ordered_flags.items():
266
278
  for prio, params in prio_params.items():
267
- param: ActionFlag = params[0] # Don't pop and check `if params` - all are needed for the group check
279
+ param = params[0] # Don't pop and check `if params` - all are needed for the group check
268
280
  if found_non_always and param.always_available:
269
281
  invalid[(before_main, prio)] = param
270
282
  elif not param.always_available:
@@ -296,7 +308,7 @@ class CommandParameters:
296
308
  @cached_property
297
309
  def _classified_combo_options(self) -> tuple[OptionMap, OptionMap]:
298
310
  """Tuple of (single char short:Option map, multi-char short:Option map) for options available in this command"""
299
- multi_char_combos = {}
311
+ multi_char_combos: OptionMap = {}
300
312
  items = self.combo_option_map.items()
301
313
  for combo, param in items:
302
314
  if len(combo) == 1: # combo_option_map is sorted in reverse length order, so all following will be 1 char
@@ -368,7 +380,7 @@ class CommandParameters:
368
380
  # Note: if the option is not in this Command's option_map, the KeyError is handled by CommandParser
369
381
  return [(option, self.option_map[option], value)], True
370
382
  else:
371
- value = None
383
+ value = None # type: ignore[assignment]
372
384
 
373
385
  try:
374
386
  param = self.option_map[option]
@@ -9,16 +9,16 @@ from __future__ import annotations
9
9
  import logging
10
10
  from abc import ABC
11
11
  from contextlib import ExitStack
12
- from typing import TYPE_CHECKING, Sequence, TextIO, Type, overload
12
+ from typing import TYPE_CHECKING, Sequence, TextIO, Type
13
13
 
14
14
  from .context import ActionPhase, Context, get_or_create_context
15
- from .core import CommandMeta, get_params, get_top_level_commands
15
+ from .core import CommandMeta, get_metadata, get_params, get_top_level_commands
16
16
  from .exceptions import ParamConflict, ParserExit
17
17
  from .parser import parse_args_and_get_next_cmd
18
18
  from .utils import maybe_await
19
19
 
20
20
  if TYPE_CHECKING:
21
- from .typing import Bool, CommandObj
21
+ from .typing import Bool, Self
22
22
 
23
23
  __all__ = ['Command', 'AsyncCommand', 'main', 'print_help']
24
24
  log = logging.getLogger(__name__)
@@ -32,34 +32,25 @@ class Command(ABC, metaclass=CommandMeta):
32
32
  #: The parsing Context used for this Command. Provided here for convenience - this reference to it is not used by
33
33
  #: any CLI Command Parser internals, so it is safe for subclasses to redefine / overwrite it.
34
34
  ctx: Context
35
+ __ctx: Context
35
36
 
36
- def __new__(cls):
37
+ def __new__(cls) -> Command:
37
38
  # By storing the Context here instead of __init__, every single subclass won't need to
38
39
  # call super().__init__(...) from their own __init__ for this step
39
40
  self = super().__new__(cls)
40
41
  self.__ctx = ctx = get_or_create_context(cls, command=self)
41
42
  if not hasattr(self, 'ctx'):
42
- self.ctx: Context = ctx # noqa # PyCharm complains this is invalid, but doesn't understand it without it
43
+ self.ctx = ctx # noqa # PyCharm complains this is invalid, but doesn't understand it without it
43
44
  return self
44
45
 
45
46
  def __repr__(self) -> str:
46
47
  cls = self.__class__
47
- return f'<{cls.__name__} in prog={cls.__class__.meta(cls).prog!r}>'
48
+ return f'<{cls.__name__} in prog={get_metadata(cls).prog!r}>'
48
49
 
49
50
  # region Parse & Run
50
51
 
51
52
  @classmethod
52
- @overload
53
- def parse_and_run(cls: Type[CommandObj], argv: Argv = None, **kwargs) -> CommandObj | None:
54
- # These overloads indicate that an instance of the same type or another may be returned
55
- ...
56
-
57
- @classmethod
58
- @overload
59
- def parse_and_run(cls, argv: Argv = None, **kwargs) -> CommandObj | None: ...
60
-
61
- @classmethod
62
- def parse_and_run(cls, argv=None, **kwargs):
53
+ def parse_and_run(cls, argv: Argv | None = None, **kwargs) -> Self | None:
63
54
  """
64
55
  Primary entry point for parsing arguments, resolving subcommands, and running a command.
65
56
 
@@ -90,15 +81,7 @@ class Command(ABC, metaclass=CommandMeta):
90
81
  # region Parse
91
82
 
92
83
  @classmethod
93
- @overload
94
- def parse(cls: Type[CommandObj], argv: Argv = None) -> CommandObj: ...
95
-
96
- @classmethod
97
- @overload
98
- def parse(cls, argv: Argv = None) -> CommandObj: ...
99
-
100
- @classmethod
101
- def parse(cls, argv=None):
84
+ def parse(cls, argv: Argv | None = None) -> Self:
102
85
  """
103
86
  Parses the specified arguments (or :data:`sys.argv`), and resolves the final subcommand class based on the
104
87
  parsed arguments, if necessary. Initializes the Command, but does not call any of its other methods.
@@ -111,7 +94,7 @@ class Command(ABC, metaclass=CommandMeta):
111
94
  with ExitStack() as stack:
112
95
  stack.enter_context(ctx)
113
96
  while sub_cmd := parse_args_and_get_next_cmd(ctx):
114
- cmd_cls = sub_cmd
97
+ cmd_cls = sub_cmd # type: ignore[assignment]
115
98
  ctx = stack.enter_context(ctx._sub_context(cmd_cls))
116
99
 
117
100
  return cmd_cls()
@@ -308,14 +291,14 @@ class AsyncCommand(Command, ABC):
308
291
  await maybe_await(self(**kwargs))
309
292
  return self
310
293
 
311
- async def __call__(self, *args, **kwargs) -> int:
294
+ async def __call__(self, *args, **kwargs) -> int: # type: ignore[override]
312
295
  """Asynchronous version of :meth:`Command.__call__`."""
313
- with self._Command__ctx as ctx, ctx.get_error_handler(): # noqa
296
+ with self._Command__ctx as ctx, ctx.get_error_handler(): # type: ignore[attr-defined]
314
297
  await maybe_await(self._pre_init_actions_(*args, **kwargs))
315
298
  await maybe_await(self._init_command_(*args, **kwargs))
316
299
  await maybe_await(self._before_main_(*args, **kwargs))
317
300
  try:
318
- await maybe_await(self.main(*args, **kwargs))
301
+ await maybe_await(self.main(*args, **kwargs)) # type: ignore[arg-type]
319
302
  except BaseException:
320
303
  if ctx.config.always_run_after_main:
321
304
  log.debug('Caught exception - running _after_main_ before propagating', exc_info=True)
@@ -328,7 +311,7 @@ class AsyncCommand(Command, ABC):
328
311
 
329
312
  async def _run_actions_(self, phase: ActionPhase, args: tuple, kwargs: dict):
330
313
  """Asynchronous version of :meth:`Command._run_actions_`."""
331
- for param in self._Command__ctx.iter_action_flags(phase): # noqa
314
+ for param in self._Command__ctx.iter_action_flags(phase): # type: ignore[attr-defined]
332
315
  await maybe_await(param.func(self, *args, **kwargs))
333
316
 
334
317
  async def _pre_init_actions_(self, *args, **kwargs):
@@ -340,9 +323,9 @@ class AsyncCommand(Command, ABC):
340
323
  """Asynchronous version of :meth:`Command._before_main_`."""
341
324
  await self._run_actions_(ActionPhase.BEFORE_MAIN, args, kwargs)
342
325
 
343
- async def main(self, *args, **kwargs) -> int | None:
326
+ async def main(self, *args, **kwargs) -> int | None: # type: ignore[override]
344
327
  """Asynchronous version of :meth:`Command.main`."""
345
- with self._Command__ctx as ctx: # noqa
328
+ with self._Command__ctx as ctx: # type: ignore[attr-defined]
346
329
  action = get_params(self).action
347
330
  if action is not None and (ctx.actions_taken == 0 or ctx.config.action_after_action_flags):
348
331
  ctx.actions_taken += 1
@@ -355,7 +338,7 @@ class AsyncCommand(Command, ABC):
355
338
  await self._run_actions_(ActionPhase.AFTER_MAIN, args, kwargs)
356
339
 
357
340
 
358
- def main(argv: Argv = None, return_command: Bool = False, **kwargs) -> CommandObj | None:
341
+ def main(argv: Argv | None = None, return_command: Bool = False, **kwargs) -> Command | None:
359
342
  """
360
343
  Convenience function that can be used as the main entry point for a program.
361
344
 
@@ -39,7 +39,7 @@ class WCTextWrapper(TextWrapper):
39
39
  if len(indent) + len(self.placeholder.lstrip()) > self.width:
40
40
  raise ValueError('placeholder too large for max width')
41
41
 
42
- lines = []
42
+ lines: list[str] = []
43
43
  # Arrange in reverse order so items can be efficiently popped from a stack of chucks.
44
44
  chunks.reverse()
45
45
  while chunks: