cli-command-parser 2025.11.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 (78) hide show
  1. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/MANIFEST.in +1 -0
  2. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/PKG-INFO +12 -4
  3. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/__init__.py +2 -1
  4. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/__version__.py +1 -1
  5. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/command_parameters.py +41 -37
  6. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/commands.py +17 -34
  7. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/compat.py +1 -1
  8. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/config.py +64 -56
  9. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/context.py +51 -31
  10. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/conversion/argparse_ast.py +144 -68
  11. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/conversion/cli.py +11 -6
  12. {cli_command_parser-2025.11.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-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/core.py +51 -30
  16. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/documentation.py +37 -29
  17. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/error_handling/base.py +13 -9
  18. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/error_handling/windows.py +2 -2
  19. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/exceptions.py +11 -12
  20. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/formatting/commands.py +31 -15
  21. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/formatting/params.py +106 -79
  22. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/formatting/restructured_text.py +21 -13
  23. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/formatting/utils.py +3 -3
  24. cli_command_parser-2026.6.27/lib/cli_command_parser/inputs/__init__.py +91 -0
  25. cli_command_parser-2026.6.27/lib/cli_command_parser/inputs/_typing.py +78 -0
  26. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/inputs/base.py +10 -1
  27. {cli_command_parser-2025.11.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.6.27/lib/cli_command_parser/inputs/numeric.py +394 -0
  30. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/inputs/patterns.py +51 -19
  31. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/inputs/time.py +130 -62
  32. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/inputs/utils.py +156 -47
  33. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/metadata.py +94 -65
  34. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/nargs.py +38 -18
  35. {cli_command_parser-2025.11.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-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/parameters/actions.py +52 -40
  38. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/parameters/base.py +297 -109
  39. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/parameters/choice_map.py +54 -53
  40. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/parameters/groups.py +11 -8
  41. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/parameters/option_strings.py +4 -12
  42. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/parameters/options.py +314 -80
  43. {cli_command_parser-2025.11.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-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/parse_tree.py +66 -51
  46. {cli_command_parser-2025.11.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-2025.11.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-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/utils.py +35 -20
  51. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser.egg-info/PKG-INFO +12 -4
  52. {cli_command_parser-2025.11.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-2025.11.1 → cli_command_parser-2026.6.27}/pyproject.toml +1 -0
  55. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/readme.rst +5 -3
  56. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/requirements-dev.txt +1 -5
  57. cli_command_parser-2025.11.1/lib/cli_command_parser/conversion/utils.py +0 -38
  58. cli_command_parser-2025.11.1/lib/cli_command_parser/conversion/visitor.py +0 -227
  59. cli_command_parser-2025.11.1/lib/cli_command_parser/inputs/__init__.py +0 -65
  60. cli_command_parser-2025.11.1/lib/cli_command_parser/inputs/files.py +0 -246
  61. cli_command_parser-2025.11.1/lib/cli_command_parser/inputs/numeric.py +0 -193
  62. cli_command_parser-2025.11.1/lib/cli_command_parser/parameters/positionals.py +0 -91
  63. cli_command_parser-2025.11.1/lib/cli_command_parser/typing.py +0 -82
  64. cli_command_parser-2025.11.1/lib/cli_command_parser.egg-info/requires.txt +0 -3
  65. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/LICENSE +0 -0
  66. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/__main__.py +0 -0
  67. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/annotations.py +0 -0
  68. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/conversion/__init__.py +0 -0
  69. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/conversion/__main__.py +0 -0
  70. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/conversion/argparse_utils.py +0 -0
  71. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/error_handling/__init__.py +0 -0
  72. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/error_handling/other.py +0 -0
  73. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/formatting/__init__.py +0 -0
  74. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser/inputs/exceptions.py +0 -0
  75. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser.egg-info/dependency_links.txt +0 -0
  76. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser.egg-info/entry_points.txt +0 -0
  77. {cli_command_parser-2025.11.1 → cli_command_parser-2026.6.27}/lib/cli_command_parser.egg-info/top_level.txt +0 -0
  78. {cli_command_parser-2025.11.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: 2025.11.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__ = '2025.11.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,33 +38,27 @@ __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
- command_parent: CommandCls | None #: The parent Command, if any
38
41
  parent: CommandParameters | None #: The parent Command's CommandParameters
39
- action: Action | None = None #: An Action Parameter, if specified
40
- _pass_thru: PassThru | None = None #: A PassThru Parameter, if specified
41
- sub_command: SubCommand | None = None #: A SubCommand Parameter, if specified
42
42
  action_flags: ActionFlags #: List of action flags
43
43
  split_action_flags: tuple[ActionFlags, ActionFlags] #: Action flags split by before/after main
44
44
  options: list[BaseOption] #: List of optional Parameters
45
45
  combo_option_map: OptionMap #: Mapping of {short opt: Parameter} (no dash characters)
46
46
  groups: list[ParamGroup] #: List of ParamGroup objects
47
47
  positionals: list[BasePositional] #: List of positional Parameters
48
- _deferred_positionals: list[BasePositional] = () #: Positional Parameters that are deferred to sub commands
48
+ _deferred_positionals: Positionals = () #: Positional Parameters that are deferred to sub commands
49
49
  option_map: OptionMap #: Mapping of {--opt / -opt: Parameter}
50
50
  # fmt: on
51
51
 
52
- def __init__(
53
- self,
54
- command: CommandCls,
55
- command_parent: CommandCls | None,
56
- parent_params: CommandParameters | None,
57
- config: CommandConfig,
58
- ):
52
+ def __init__(self, command: CommandCls, parent_params: CommandParameters | None, config: CommandConfig):
59
53
  self.command = command
60
- self.command_parent = command_parent
61
54
  self.parent = parent_params
62
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
63
62
  self._process_parameters()
64
63
 
65
64
  def __repr__(self) -> str:
@@ -85,11 +84,8 @@ class CommandParameters:
85
84
 
86
85
  @cached_property
87
86
  def all_positionals(self) -> list[BasePositional]:
88
- try:
89
- if not self.parent.sub_command:
90
- return self.parent.all_positionals + self.positionals
91
- except AttributeError:
92
- pass
87
+ if self.parent and not self.parent.sub_command:
88
+ return self.parent.all_positionals + self.positionals
93
89
  return self.positionals
94
90
 
95
91
  def get_positionals_to_parse(self, ctx: Context) -> list[BasePositional]:
@@ -102,6 +98,7 @@ class CommandParameters:
102
98
 
103
99
  @cached_property
104
100
  def formatter(self) -> CommandHelpFormatter:
101
+ """The formatter used for this Command's help text."""
105
102
  from .formatting.commands import CommandHelpFormatter
106
103
 
107
104
  formatter_factory = self.config.command_formatter or CommandHelpFormatter
@@ -113,13 +110,13 @@ class CommandParameters:
113
110
  return formatter
114
111
 
115
112
  @cached_property
116
- def _has_help(self) -> bool:
113
+ def _has_help(self) -> Bool:
117
114
  return help_action in self.action_flags or (self.parent and self.parent._has_help)
118
115
 
119
116
  # region Initialization
120
117
 
121
118
  def _iter_parameters(self) -> Iterator[ParamBase]:
122
- 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
123
120
  for item in self.command.__dict__.items():
124
121
  attr, param = item
125
122
  if attr.startswith('__') or not isinstance(param, ParamBase): # Name mangled Parameters are still processed
@@ -167,7 +164,7 @@ class CommandParameters:
167
164
  while param_group := param_group.group:
168
165
  groups.add(param_group)
169
166
 
170
- if self.config.add_help and self.command_parent is not None and (not self.parent or not self.parent._has_help):
167
+ if self.config.add_help and self.parent is not None and not self.parent._has_help:
171
168
  options.append(help_action)
172
169
 
173
170
  self._process_positionals(positionals)
@@ -181,7 +178,9 @@ class CommandParameters:
181
178
  self.groups = sorted(groups) if groups else []
182
179
 
183
180
  def _process_positionals(self, params: list[BasePositional]):
184
- 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
185
184
  if self.parent and (deferred := self.parent._deferred_positionals):
186
185
  params = deferred + params
187
186
 
@@ -194,26 +193,28 @@ class CommandParameters:
194
193
  raise CommandDefinitionError(
195
194
  f'Additional Positional parameters cannot follow {unfollowable} {why} - {param=} is invalid'
196
195
  )
197
- elif isinstance(param, (SubCommand, Action)):
196
+
197
+ if isinstance(param, (SubCommand, Action)):
198
198
  if action_or_sub_cmd:
199
199
  raise CommandDefinitionError(
200
200
  f'Only 1 Action xor SubCommand is allowed in a given Command - {self.command.__name__} cannot'
201
201
  f' contain both {action_or_sub_cmd} and {param}'
202
202
  )
203
- elif isinstance(param, SubCommand):
203
+
204
+ if isinstance(param, SubCommand):
204
205
  self.sub_command = action_or_sub_cmd = param
205
206
  split_index = i + 1
206
207
  if param.has_choices and 0 in param.nargs: # It has local choices or is not required
207
208
  unfollowable = param
208
209
  else: # It's an Action
209
- self.action = action_or_sub_cmd = param # type: ignore
210
+ self.action = action_or_sub_cmd = param
210
211
  if not param.has_choices:
211
212
  raise CommandDefinitionError(f'No choices were registered for {self.action}')
212
213
  elif 0 in param.nargs or (param.nargs.variable and not param.has_choices):
213
214
  unfollowable = param
214
215
 
215
216
  if split_index:
216
- if self.sub_command.has_local_choices:
217
+ if self.sub_command.has_local_choices: # type: ignore[union-attr]
217
218
  self._deferred_positionals = params[split_index:]
218
219
  else:
219
220
  params, self._deferred_positionals = params[:split_index], params[split_index:]
@@ -260,19 +261,22 @@ class CommandParameters:
260
261
  f'{opt_type} {option=} conflict for command={self.command!r} between {existing} and {param}'
261
262
  )
262
263
 
263
- def _process_action_flags(self):
264
- action_flags = sorted(p for p in self.options if isinstance(p, ActionFlag))
265
- 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
+ }
266
270
  for param in action_flags:
267
271
  if param.func is None:
268
272
  raise ParameterDefinitionError(f'No function was registered for {param=}')
269
273
  grouped_ordered_flags[param.before_main][param.order].append(param)
270
274
 
271
275
  found_non_always = False
272
- invalid = {}
276
+ invalid: dict[tuple[bool, int | float], ActionFlags | ActionFlag] = {}
273
277
  for before_main, prio_params in grouped_ordered_flags.items():
274
278
  for prio, params in prio_params.items():
275
- 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
276
280
  if found_non_always and param.always_available:
277
281
  invalid[(before_main, prio)] = param
278
282
  elif not param.always_available:
@@ -304,7 +308,7 @@ class CommandParameters:
304
308
  @cached_property
305
309
  def _classified_combo_options(self) -> tuple[OptionMap, OptionMap]:
306
310
  """Tuple of (single char short:Option map, multi-char short:Option map) for options available in this command"""
307
- multi_char_combos = {}
311
+ multi_char_combos: OptionMap = {}
308
312
  items = self.combo_option_map.items()
309
313
  for combo, param in items:
310
314
  if len(combo) == 1: # combo_option_map is sorted in reverse length order, so all following will be 1 char
@@ -376,7 +380,7 @@ class CommandParameters:
376
380
  # Note: if the option is not in this Command's option_map, the KeyError is handled by CommandParser
377
381
  return [(option, self.option_map[option], value)], True
378
382
  else:
379
- value = None
383
+ value = None # type: ignore[assignment]
380
384
 
381
385
  try:
382
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: