cli-command-parser 2024.4.20__tar.gz → 2024.5.18.post1__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 (67) hide show
  1. {cli_command_parser-2024.4.20/lib/cli_command_parser.egg-info → cli_command_parser-2024.5.18.post1}/PKG-INFO +4 -4
  2. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/__version__.py +1 -1
  3. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/annotations.py +4 -2
  4. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/command_parameters.py +24 -22
  5. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/commands.py +9 -11
  6. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/compat.py +3 -2
  7. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/config.py +63 -13
  8. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/context.py +14 -15
  9. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/conversion/argparse_ast.py +21 -21
  10. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/conversion/command_builder.py +15 -13
  11. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/conversion/visitor.py +8 -6
  12. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/core.py +15 -16
  13. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/documentation.py +8 -8
  14. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/error_handling.py +3 -3
  15. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/exceptions.py +0 -1
  16. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/formatting/commands.py +12 -9
  17. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/formatting/params.py +46 -50
  18. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/formatting/restructured_text.py +6 -6
  19. cli_command_parser-2024.5.18.post1/lib/cli_command_parser/formatting/utils.py +207 -0
  20. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/inputs/files.py +3 -4
  21. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/inputs/numeric.py +4 -4
  22. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/inputs/patterns.py +4 -2
  23. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/inputs/time.py +8 -7
  24. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/metadata.py +13 -13
  25. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/nargs.py +2 -2
  26. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/parameters/actions.py +6 -6
  27. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/parameters/base.py +22 -22
  28. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/parameters/choice_map.py +21 -9
  29. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/parameters/groups.py +6 -6
  30. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/parameters/option_strings.py +19 -19
  31. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/parameters/options.py +9 -9
  32. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/parse_tree.py +9 -9
  33. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/parser.py +16 -9
  34. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/testing.py +4 -4
  35. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/typing.py +27 -9
  36. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/utils.py +3 -3
  37. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1/lib/cli_command_parser.egg-info}/PKG-INFO +4 -4
  38. cli_command_parser-2024.5.18.post1/pyproject.toml +73 -0
  39. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/readme.rst +3 -3
  40. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/requirements-dev.txt +1 -0
  41. cli_command_parser-2024.4.20/lib/cli_command_parser/formatting/utils.py +0 -158
  42. cli_command_parser-2024.4.20/pyproject.toml +0 -21
  43. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/LICENSE +0 -0
  44. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/MANIFEST.in +0 -0
  45. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/entry_points.txt +0 -0
  46. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/__init__.py +0 -0
  47. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/__main__.py +0 -0
  48. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/conversion/__init__.py +0 -0
  49. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/conversion/__main__.py +0 -0
  50. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/conversion/argparse_utils.py +0 -0
  51. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/conversion/cli.py +0 -0
  52. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/conversion/utils.py +0 -0
  53. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/formatting/__init__.py +0 -0
  54. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/inputs/__init__.py +0 -0
  55. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/inputs/base.py +0 -0
  56. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/inputs/choices.py +0 -0
  57. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/inputs/exceptions.py +0 -0
  58. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/inputs/utils.py +0 -0
  59. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/parameters/__init__.py +0 -0
  60. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/parameters/pass_thru.py +0 -0
  61. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser/parameters/positionals.py +0 -0
  62. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser.egg-info/SOURCES.txt +0 -0
  63. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser.egg-info/dependency_links.txt +0 -0
  64. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser.egg-info/entry_points.txt +0 -0
  65. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser.egg-info/requires.txt +0 -0
  66. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/lib/cli_command_parser.egg-info/top_level.txt +0 -0
  67. {cli_command_parser-2024.4.20 → cli_command_parser-2024.5.18.post1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cli_command_parser
3
- Version: 2024.4.20
3
+ Version: 2024.5.18.post1
4
4
  Summary: CLI Command Parser
5
5
  Home-page: https://github.com/dskrypa/cli_command_parser
6
6
  Author: Doug Skrypa
@@ -34,7 +34,7 @@ Requires-Dist: astunparse; python_version < "3.9" and extra == "conversion"
34
34
  CLI Command Parser
35
35
  ##################
36
36
 
37
- |downloads| |py_version| |coverage_badge| |build_status| |Blue|
37
+ |downloads| |py_version| |coverage_badge| |build_status| |Ruff|
38
38
 
39
39
  .. |py_version| image:: https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20-blue
40
40
  :target: https://pypi.org/project/cli-command-parser/
@@ -45,8 +45,8 @@ CLI Command Parser
45
45
  .. |build_status| image:: https://github.com/dskrypa/cli_command_parser/actions/workflows/run-tests.yml/badge.svg
46
46
  :target: https://github.com/dskrypa/cli_command_parser/actions/workflows/run-tests.yml
47
47
 
48
- .. |Blue| image:: https://img.shields.io/badge/code%20style-blue-blue.svg
49
- :target: https://blue.readthedocs.io/
48
+ .. |Ruff| image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json
49
+ :target: https://docs.astral.sh/ruff/
50
50
 
51
51
  .. |downloads| image:: https://img.shields.io/pypi/dm/cli-command-parser
52
52
  :target: https://pypistats.org/packages/cli-command-parser
@@ -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__ = '2024.04.20'
4
+ __version__ = '2024.05.18-1'
5
5
  __author__ = 'Doug Skrypa'
6
6
  __author_email__ = 'dskrypa@gmail.com'
7
7
  __license__ = 'Apache 2.0'
@@ -5,8 +5,9 @@ Utilities for extracting types from annotations.
5
5
  """
6
6
 
7
7
  from collections.abc import Collection, Iterable
8
+ from functools import lru_cache
8
9
  from inspect import isclass
9
- from typing import Union, Optional, get_type_hints, get_origin, get_args as _get_args # pylint: disable=C0412
10
+ from typing import Union, Optional, get_type_hints as _get_type_hints, get_origin, get_args as _get_args
10
11
 
11
12
  try:
12
13
  from types import NoneType
@@ -15,9 +16,10 @@ except ImportError: # Added in 3.10
15
16
 
16
17
  __all__ = ['get_descriptor_value_type']
17
18
 
19
+ get_type_hints = lru_cache()(_get_type_hints) # Cache the attr:annotation mapping for each Command class
20
+
18
21
 
19
22
  def get_descriptor_value_type(command_cls: type, attr: str) -> Optional[type]:
20
- # TODO: Optimize this to cache get_type_hints for a given class?
21
23
  try:
22
24
  annotation = get_type_hints(command_cls)[attr]
23
25
  except (KeyError, NameError): # KeyError due to attr missing; NameError for forward references
@@ -12,25 +12,26 @@ 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, Optional, Collection, Iterator, List, Dict, Set, Tuple
15
+ from typing import TYPE_CHECKING, Collection, Iterator, Optional
16
16
 
17
- from .config import CommandConfig, AmbiguousComboMode
18
- from .exceptions import CommandDefinitionError, ParameterDefinitionError, AmbiguousShortForm, AmbiguousCombo
19
- from .parameters.base import ParamBase, Parameter, BaseOption, BasePositional
20
- from .parameters import SubCommand, PassThru, ActionFlag, ParamGroup, Action, help_action
17
+ from .config import AmbiguousComboMode, CommandConfig
18
+ from .exceptions import AmbiguousCombo, AmbiguousShortForm, CommandDefinitionError, ParameterDefinitionError
19
+ from .parameters import Action, ActionFlag, ParamGroup, PassThru, SubCommand, help_action
20
+ from .parameters.base import BaseOption, BasePositional, ParamBase, Parameter
21
21
 
22
22
  if TYPE_CHECKING:
23
23
  from .context import Context
24
24
  from .formatting.commands import CommandHelpFormatter
25
25
  from .typing import CommandCls, Strings
26
26
 
27
- __all__ = ['CommandParameters']
27
+ OptionMap = dict[str, BaseOption]
28
+ ActionFlags = list[ActionFlag]
28
29
 
29
- OptionMap = Dict[str, BaseOption]
30
- ActionFlags = List[ActionFlag]
30
+ __all__ = ['CommandParameters']
31
31
 
32
32
 
33
33
  class CommandParameters:
34
+ # fmt: off
34
35
  command: CommandCls #: The Command associated with this CommandParameters object
35
36
  formatter: CommandHelpFormatter #: The formatter used for this Command's help text
36
37
  command_parent: Optional[CommandCls] #: The parent Command, if any
@@ -39,13 +40,14 @@ class CommandParameters:
39
40
  _pass_thru: Optional[PassThru] = None #: A PassThru Parameter, if specified
40
41
  sub_command: Optional[SubCommand] = None #: A SubCommand Parameter, if specified
41
42
  action_flags: ActionFlags #: List of action flags
42
- split_action_flags: Tuple[ActionFlags, ActionFlags] #: Action flags split by before/after main
43
- options: List[BaseOption] #: List of optional Parameters
43
+ split_action_flags: tuple[ActionFlags, ActionFlags] #: Action flags split by before/after main
44
+ options: list[BaseOption] #: List of optional Parameters
44
45
  combo_option_map: OptionMap #: Mapping of {short opt: Parameter} (no dash characters)
45
- groups: List[ParamGroup] #: List of ParamGroup objects
46
- positionals: List[BasePositional] #: List of positional Parameters
47
- _deferred_positionals: List[BasePositional] = () #: Positional Parameters that are deferred to sub commands
46
+ groups: list[ParamGroup] #: List of ParamGroup objects
47
+ positionals: list[BasePositional] #: List of positional Parameters
48
+ _deferred_positionals: list[BasePositional] = () #: Positional Parameters that are deferred to sub commands
48
49
  option_map: OptionMap #: Mapping of {--opt / -opt: Parameter}
50
+ # fmt: on
49
51
 
50
52
  def __init__(
51
53
  self,
@@ -83,7 +85,7 @@ class CommandParameters:
83
85
  # endregion
84
86
 
85
87
  @cached_property
86
- def all_positionals(self) -> List[BasePositional]:
88
+ def all_positionals(self) -> list[BasePositional]:
87
89
  try:
88
90
  if not self.parent.sub_command:
89
91
  return self.parent.all_positionals + self.positionals
@@ -91,7 +93,7 @@ class CommandParameters:
91
93
  pass
92
94
  return self.positionals
93
95
 
94
- def get_positionals_to_parse(self, ctx: Context) -> List[BasePositional]:
96
+ def get_positionals_to_parse(self, ctx: Context) -> list[BasePositional]:
95
97
  if self.all_positionals:
96
98
  for i, param in enumerate(self.all_positionals):
97
99
  if not ctx.num_provided(param):
@@ -170,13 +172,13 @@ class CommandParameters:
170
172
  self._process_options(options)
171
173
  self._process_groups(groups)
172
174
 
173
- def _process_groups(self, groups: Set[ParamGroup]):
175
+ def _process_groups(self, groups: set[ParamGroup]):
174
176
  if self.parent:
175
177
  self.groups = sorted((*self.parent.groups, *groups)) if groups else self.parent.groups.copy()
176
178
  else:
177
179
  self.groups = sorted(groups) if groups else []
178
180
 
179
- def _process_positionals(self, params: List[BasePositional]):
181
+ def _process_positionals(self, params: list[BasePositional]):
180
182
  unfollowable = action_or_sub_cmd = split_index = None
181
183
  if self.parent and (deferred := self.parent._deferred_positionals):
182
184
  params = deferred + params
@@ -216,7 +218,7 @@ class CommandParameters:
216
218
 
217
219
  self.positionals = params
218
220
 
219
- def _process_options(self, params: List[BaseOption]):
221
+ def _process_options(self, params: list[BaseOption]):
220
222
  if parent := self.parent:
221
223
  option_map = parent.option_map.copy()
222
224
  combo_option_map = parent.combo_option_map.copy()
@@ -298,7 +300,7 @@ class CommandParameters:
298
300
  # region Ambiguous Short Combo Handling
299
301
 
300
302
  @cached_property
301
- def _classified_combo_options(self) -> Tuple[OptionMap, OptionMap]:
303
+ def _classified_combo_options(self) -> tuple[OptionMap, OptionMap]:
302
304
  multi_char_combos = {}
303
305
  items = self.combo_option_map.items()
304
306
  for combo, param in items:
@@ -308,7 +310,7 @@ class CommandParameters:
308
310
  return {}, multi_char_combos
309
311
 
310
312
  @cached_property
311
- def _potentially_ambiguous_combo_opts(self) -> Dict[str, Tuple[BaseOption, OptionMap]]:
313
+ def _potentially_ambiguous_combo_opts(self) -> dict[str, tuple[BaseOption, OptionMap]]:
312
314
  return _find_ambiguous_combos(*self._classified_combo_options)
313
315
 
314
316
  @cached_property
@@ -357,7 +359,7 @@ class CommandParameters:
357
359
 
358
360
  def short_option_to_param_value_pairs(
359
361
  self, option: str
360
- ) -> Tuple[List[Tuple[str, BaseOption, Optional[str]]], bool]:
362
+ ) -> tuple[list[tuple[str, BaseOption, Optional[str]]], bool]:
361
363
  option, eq, value = option.partition('=')
362
364
  if eq: # An `=` was present in the string
363
365
  # Note: if the option is not in this Command's option_map, the KeyError is handled by CommandParser
@@ -404,7 +406,7 @@ class CommandParameters:
404
406
 
405
407
  def _find_ambiguous_combos(
406
408
  single_char_combos: OptionMap, multi_char_combos: OptionMap
407
- ) -> Dict[str, Tuple[BaseOption, OptionMap]]:
409
+ ) -> dict[str, tuple[BaseOption, OptionMap]]:
408
410
  ambiguous_combo_options = {}
409
411
  for combo, param in multi_char_combos.items():
410
412
  if singles := {c: single_char_combos[c] for c in combo if c in single_char_combos}:
@@ -9,10 +9,10 @@ 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, Type, Sequence, Optional, overload
12
+ from typing import TYPE_CHECKING, Optional, Sequence, Type, overload
13
13
 
14
- from .core import CommandMeta, get_top_level_commands, get_params
15
- from .context import Context, ActionPhase, get_or_create_context
14
+ from .context import ActionPhase, Context, get_or_create_context
15
+ from .core import CommandMeta, get_params, get_top_level_commands
16
16
  from .exceptions import ParamConflict
17
17
  from .parser import parse_args_and_get_next_cmd
18
18
  from .utils import maybe_await
@@ -20,7 +20,7 @@ from .utils import maybe_await
20
20
  if TYPE_CHECKING:
21
21
  from .typing import Bool, CommandObj
22
22
 
23
- __all__ = ['Command', 'main']
23
+ __all__ = ['Command', 'AsyncCommand', 'main']
24
24
  log = logging.getLogger(__name__)
25
25
 
26
26
  Argv = Sequence[str]
@@ -54,12 +54,12 @@ class Command(ABC, metaclass=CommandMeta):
54
54
  @classmethod
55
55
  @overload
56
56
  def parse_and_run(cls: Type[CommandObj], argv: Argv = None, **kwargs) -> Optional[CommandObj]:
57
- ... # These overloads indicate that an instance of the same type or another may be returned
57
+ # These overloads indicate that an instance of the same type or another may be returned
58
+ ...
58
59
 
59
60
  @classmethod
60
61
  @overload
61
- def parse_and_run(cls, argv: Argv = None, **kwargs) -> Optional[CommandObj]:
62
- ...
62
+ def parse_and_run(cls, argv: Argv = None, **kwargs) -> Optional[CommandObj]: ...
63
63
 
64
64
  @classmethod
65
65
  def parse_and_run(cls, argv=None, **kwargs):
@@ -94,13 +94,11 @@ class Command(ABC, metaclass=CommandMeta):
94
94
 
95
95
  @classmethod
96
96
  @overload
97
- def parse(cls: Type[CommandObj], argv: Argv = None) -> CommandObj:
98
- ...
97
+ def parse(cls: Type[CommandObj], argv: Argv = None) -> CommandObj: ...
99
98
 
100
99
  @classmethod
101
100
  @overload
102
- def parse(cls, argv: Argv = None) -> CommandObj:
103
- ...
101
+ def parse(cls, argv: Argv = None) -> CommandObj: ...
104
102
 
105
103
  @classmethod
106
104
  def parse(cls, argv=None):
@@ -8,8 +8,9 @@ The :class:`WCTextWrapper` in this module extends the stdlib :class:`python:text
8
8
  characters.
9
9
  """
10
10
 
11
+ from __future__ import annotations
12
+
11
13
  from textwrap import TextWrapper
12
- from typing import List
13
14
 
14
15
  from .utils import wcswidth
15
16
 
@@ -24,7 +25,7 @@ class WCTextWrapper(TextWrapper):
24
25
  optional ``wcwidth`` dependency is available). Minimal formatting changes are applied. No logic has been changed.
25
26
  """
26
27
 
27
- def _wrap_chunks(self, chunks: List[str]) -> List[str]:
28
+ def _wrap_chunks(self, chunks: list[str]) -> list[str]:
28
29
  """
29
30
  _wrap_chunks(chunks : [string]) -> [string]
30
31
 
@@ -8,8 +8,10 @@ from __future__ import annotations
8
8
 
9
9
  from collections import ChainMap
10
10
  from enum import Enum
11
- from typing import TYPE_CHECKING, Optional, Any, Union, Callable, Type, TypeVar, Generic, overload, Dict
11
+ from string import whitespace
12
+ from typing import TYPE_CHECKING, Any, Callable, Generic, Optional, Sequence, Type, TypeVar, Union, overload
12
13
 
14
+ from .exceptions import CommandDefinitionError
13
15
  from .utils import FixedFlag, MissingMixin, _NotSet, positive_int
14
16
 
15
17
  if TYPE_CHECKING:
@@ -17,7 +19,7 @@ if TYPE_CHECKING:
17
19
  from .error_handling import ErrorHandler
18
20
  from .formatting.commands import CommandHelpFormatter
19
21
  from .formatting.params import ParamHelpFormatter
20
- from .typing import Bool, ParamOrGroup, CommandType
22
+ from .typing import Bool, CommandType, ParamOrGroup
21
23
 
22
24
  __all__ = [
23
25
  'CommandConfig',
@@ -49,11 +51,13 @@ class ShowDefaults(FixedFlag):
49
51
  is equivalent to ``ShowDefaults.MISSING | ShowDefaults.NEVER``, which will result in no default values being shown.
50
52
  """
51
53
 
54
+ # fmt: off
52
55
  NEVER = 1 #: Never include the default value in help text
53
56
  MISSING = 2 #: Only include the default value if ``default:`` is not already present
54
57
  TRUTHY = 4 #: Only include the default value if it is treated as True in a boolean context
55
58
  NON_EMPTY = 8 #: Only include the default value if it is not ``None`` or an empty container
56
59
  ANY = 16 #: Any default value, regardless of truthiness, will be included
60
+ # fmt: on
57
61
 
58
62
  @classmethod
59
63
  def _missing_(cls, value: Union[str, int]) -> ShowDefaults:
@@ -104,6 +108,7 @@ class OptionNameMode(FixedFlag):
104
108
  - ``'underscore'`` or ``'dash'`` or ``'both'`` or ``'both_underscore'`` or ``'both_dash'`` or ``'none'``
105
109
  """
106
110
 
111
+ # fmt: off
107
112
  UNDERSCORE = 1
108
113
  DASH = 2
109
114
  BOTH = 3 # = 1|2
@@ -111,6 +116,7 @@ class OptionNameMode(FixedFlag):
111
116
  BOTH_UNDERSCORE = 15 # & 8 -> show only underscore version
112
117
  BOTH_DASH = 23 # & 16 -> show only dash version
113
118
  NONE = 32
119
+ # fmt: on
114
120
 
115
121
  @classmethod
116
122
  def _missing_(cls, value: Union[str, int, None]) -> OptionNameMode:
@@ -153,9 +159,11 @@ class SubcommandAliasHelpMode(MissingMixin, Enum):
153
159
  ``Alias of: <first choice/alias value>``.
154
160
  """
155
161
 
162
+ # fmt: off
156
163
  REPEAT = 'repeat' # Repeat the description as if it was a separate subcommand
157
164
  COMBINE = 'combine' # Combine aliases onto a single line
158
165
  ALIAS = 'alias' # Indicate the subcommand that it is an alias for; do not repeat the description
166
+ # fmt: on
159
167
 
160
168
 
161
169
  CmdAliasMode = Union[SubcommandAliasHelpMode, str]
@@ -176,9 +184,11 @@ class AmbiguousComboMode(MissingMixin, Enum):
176
184
  input.
177
185
  """
178
186
 
187
+ # fmt: off
179
188
  IGNORE = 'ignore' # Ignore potentially ambiguous combinations of short options entirely
180
189
  PERMISSIVE = 'permissive' # Allow multi-char short options that overlap with a single char one for exact matches
181
190
  STRICT = 'strict' # Reject multi-char short options that overlap with a single char one before parsing
191
+ # fmt: on
182
192
 
183
193
 
184
194
  class AllowLeadingDash(Enum):
@@ -193,9 +203,11 @@ class AllowLeadingDash(Enum):
193
203
  :NEVER: Never allow values with a leading dash.
194
204
  """
195
205
 
206
+ # fmt: off
196
207
  NUMERIC = 'numeric' # Allow a leading dash when the value is numeric
197
208
  ALWAYS = 'always' # Always allow a leading dash
198
209
  NEVER = 'never' # Never allow a leading dash
210
+ # fmt: on
199
211
 
200
212
  @classmethod
201
213
  def _missing_(cls, value):
@@ -218,6 +230,13 @@ class AllowLeadingDash(Enum):
218
230
 
219
231
 
220
232
  class ConfigItem(Generic[CV, DV]):
233
+ """
234
+ A single configurable setting in the :class:`CommandConfig`.
235
+
236
+ :param default: Default config value to use if no explicit value is provided
237
+ :param type: A class or other callable that will be called to validate/normalize provided values
238
+ """
239
+
221
240
  __slots__ = ('default', 'type', 'name')
222
241
 
223
242
  def __init__(self, default: DV, type: Callable[..., CV] = None): # noqa
@@ -229,17 +248,16 @@ class ConfigItem(Generic[CV, DV]):
229
248
  owner.FIELDS.add(name)
230
249
 
231
250
  @overload
232
- def __get__(self, instance: None, owner: Type[CommandConfig]) -> ConfigItem[CV, DV]:
233
- ...
251
+ def __get__(self, instance: None, owner: Type[CommandConfig]) -> ConfigItem[CV, DV]: ...
234
252
 
235
253
  @overload
236
- def __get__(self, instance: CommandConfig, owner: Type[CommandConfig]) -> ConfigValue:
237
- ...
254
+ def __get__(self, instance: CommandConfig, owner: Type[CommandConfig]) -> ConfigValue: ...
238
255
 
239
256
  def __get__(self, instance, owner):
240
- if instance is None:
257
+ try:
258
+ return instance._data.get(self.name, self.default)
259
+ except AttributeError: # instance is None
241
260
  return self
242
- return instance._data.get(self.name, self.default)
243
261
 
244
262
  def __set__(self, instance: CommandConfig, value: ConfigValue):
245
263
  if instance._read_only:
@@ -261,6 +279,8 @@ class ConfigItem(Generic[CV, DV]):
261
279
 
262
280
 
263
281
  class DynamicConfigItem(ConfigItem):
282
+ # A ConfigItem with a setter :paramref:`.ConfigItem.type` defined as a method in :class:`CommandConfig`.
283
+
264
284
  __slots__ = ('__doc__',)
265
285
 
266
286
  def __init__(self, default: DV, type: Callable[..., CV]): # noqa
@@ -372,6 +392,35 @@ class CommandConfig:
372
392
  #: Whether there should be a visual indicator in help text for the parameters that are members of a given group
373
393
  show_group_tree: Bool = ConfigItem(False, bool)
374
394
 
395
+ @config_item(('\u00a6 ', '\u2551 ', '\u2502 '))
396
+ def group_tree_spacers(self, value: tuple[str, str, str] | Sequence[str]) -> tuple[str, str, str]:
397
+ """
398
+ The spacer characters to use at the beginning of each line when :attr:`.show_group_tree` is True.
399
+
400
+ The default spacers:
401
+
402
+ +--------------------+-----------+------------------------------+
403
+ | Parameter Type | Character | Character Name |
404
+ +====================+===========+==============================+
405
+ | Mutually Exclusive | \u00a6 | BROKEN BAR |
406
+ +--------------------+-----------+------------------------------+
407
+ | Mutually dependent | \u2551 | BOX DRAWINGS DOUBLE VERTICAL |
408
+ +--------------------+-----------+------------------------------+
409
+ | Other | \u2502 | BOX DRAWINGS LIGHT VERTICAL |
410
+ +--------------------+-----------+------------------------------+
411
+
412
+ :param value: A 3-tuple (or other sequence with 3 items) of spacer strings to be used for
413
+ (mutually exclusive, mutually dependent, other) group members, respectively.
414
+ :return: The validated and normalized value (or the default value if this property is accessed without
415
+ providing explicit values)
416
+ """
417
+ # Note: extra spaces in the docstring table are intentional - the escape sequences each collapse to one char
418
+ if isinstance(value, Sequence) and len(value) == 3 and all(isinstance(v, str) for v in value):
419
+ return tuple(f'{v} ' if v and v[-1] not in whitespace else v for v in value) # noqa
420
+ raise CommandDefinitionError(
421
+ f'Invalid group_tree_spacers={value!r} - expected a 3-tuple of 2-character strings'
422
+ )
423
+
375
424
  #: Whether mutually exclusive / dependent groups should include that fact in their descriptions
376
425
  show_group_type: Bool = ConfigItem(True, bool)
377
426
 
@@ -386,16 +435,17 @@ class CommandConfig:
386
435
  #: they were successfully detected
387
436
  extended_epilog: Bool = ConfigItem(True, bool)
388
437
 
389
- #: Width (in characters) for the usage column in help text
438
+ #: Width (in characters) for the usage column in help text, after which the parameter descriptions begin.
390
439
  usage_column_width: int = ConfigItem(30, int)
391
440
 
392
- #: Min width (in chars) for the usage column in help text after adjusting for group indentation / terminal width
393
- min_usage_column_width: int = ConfigItem(20, int)
441
+ #: Whether the :attr:`.usage_column_width` should be enforced for parameters with usage text parts that exceed it.
442
+ #: By default, that setting only defines where the parameter descriptions begin.
443
+ strict_usage_column_width: bool = ConfigItem(False, bool)
394
444
 
395
445
  @config_item(False)
396
446
  def wrap_usage_str(self, value: Any) -> Union[int, bool]:
397
447
  """
398
- Wrap the basic usage string after the specified number of characters, or automatically based on terminal size
448
+ Wrap the basic usage line after the specified number of characters, or automatically based on terminal size
399
449
  if ``True`` is specified instead.
400
450
  """
401
451
  if value is True or value is False:
@@ -432,7 +482,7 @@ class CommandConfig:
432
482
  settings = ', '.join(f'{k}={v!r}' for k, v in self.as_dict(False).items())
433
483
  return f'<{self.__class__.__name__}[depth={len(self._data.maps)}]({settings})>'
434
484
 
435
- def as_dict(self, full: Bool = True) -> Dict[str, Any]:
485
+ def as_dict(self, full: Bool = True) -> dict[str, Any]:
436
486
  """Return a dict representing the configured options."""
437
487
  if full:
438
488
  return {key: getattr(self, key) for key in self.FIELDS}
@@ -13,20 +13,19 @@ from contextlib import AbstractContextManager
13
13
  from contextvars import ContextVar
14
14
  from enum import Enum
15
15
  from functools import cached_property
16
- from inspect import Signature, Parameter as _Parameter
17
- from typing import TYPE_CHECKING, Any, Callable, Union, Sequence, Optional, Iterator, Collection, cast
18
- from typing import Dict, Tuple, List
16
+ from inspect import Parameter as _Parameter, Signature
17
+ from typing import TYPE_CHECKING, Any, Callable, Collection, Iterator, Optional, Sequence, Union, cast
19
18
 
20
- from .config import CommandConfig, DEFAULT_CONFIG
19
+ from .config import DEFAULT_CONFIG, CommandConfig
21
20
  from .error_handling import ErrorHandler, NullErrorHandler, extended_error_handler
22
21
  from .exceptions import NoActiveContext
23
- from .utils import _NotSet, Terminal
22
+ from .utils import Terminal, _NotSet
24
23
 
25
24
  if TYPE_CHECKING:
26
25
  from .command_parameters import CommandParameters
27
26
  from .commands import Command
28
- from .parameters import Parameter, Option, ActionFlag
29
- from .typing import Bool, ParamOrGroup, CommandType, CommandObj, AnyConfig, OptStr, StrSeq, PathLike # noqa
27
+ from .parameters import ActionFlag, Option, Parameter
28
+ from .typing import AnyConfig, Bool, CommandObj, CommandType, OptStr, ParamOrGroup, PathLike, StrSeq # noqa
30
29
 
31
30
  __all__ = ['Context', 'ctx', 'get_current_context', 'get_or_create_context', 'get_context', 'get_parsed', 'get_raw_arg']
32
31
 
@@ -49,7 +48,7 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
49
48
  allow_argv_prog: Bool = True
50
49
  _command_obj: CommandObj = None
51
50
  _terminal_width: Optional[int]
52
- _provided: Dict[ParamOrGroup, int]
51
+ _provided: dict[ParamOrGroup, int]
53
52
 
54
53
  def __init__(
55
54
  self,
@@ -158,7 +157,7 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
158
157
  recursive: Bool = True,
159
158
  default: Any = None,
160
159
  include_defaults: Bool = True,
161
- ) -> Dict[str, Any]:
160
+ ) -> dict[str, Any]:
162
161
  """
163
162
  Returns all of the parsed arguments as a dictionary.
164
163
 
@@ -247,7 +246,7 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
247
246
  """Not intended to be called by users. Used by Parameters during parsing to handle nargs."""
248
247
  return self._provided[param]
249
248
 
250
- def get_missing(self) -> List[Parameter]:
249
+ def get_missing(self) -> list[Parameter]:
251
250
  """Not intended to be called by users. Used during parsing to determine if any Parameters are missing."""
252
251
  return [p for p in self.params.required_check_params() if not self._provided[p]]
253
252
 
@@ -260,7 +259,7 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
260
259
  # region Actions
261
260
 
262
261
  @cached_property
263
- def _parsed_action_flags(self) -> Tuple[int, List[ActionFlag], List[ActionFlag]]:
262
+ def _parsed_action_flags(self) -> tuple[int, list[ActionFlag], list[ActionFlag]]:
264
263
  """
265
264
  Not intended to be accessed by users. Returns a tuple containing the total number of action flags provided, the
266
265
  action flags to run before main, and the action flags to run after main.
@@ -281,13 +280,13 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
281
280
  return self._parsed_action_flags[0]
282
281
 
283
282
  @cached_property
284
- def all_action_flags(self) -> List[ActionFlag]:
283
+ def all_action_flags(self) -> list[ActionFlag]:
285
284
  """Not intended to be accessed by users. Returns all parsed action flags."""
286
285
  _, before_main, after_main = self._parsed_action_flags
287
286
  return before_main + after_main
288
287
 
289
288
  @cached_property
290
- def categorized_action_flags(self) -> Dict[ActionPhase, Sequence[ActionFlag]]:
289
+ def categorized_action_flags(self) -> dict[ActionPhase, Sequence[ActionFlag]]:
291
290
  """
292
291
  Not intended to be accessed by users. Returns a dict of parsed action flags, categorized by the
293
292
  :class:`ActionPhase` during which they will run.
@@ -321,7 +320,7 @@ class Context(AbstractContextManager): # Extending AbstractContextManager to ma
321
320
 
322
321
 
323
322
  def _normalize_config(
324
- config: AnyConfig, kwargs: Dict[str, Any], parent: Context | None, command: CommandType | None
323
+ config: AnyConfig, kwargs: dict[str, Any], parent: Context | None, command: CommandType | None
325
324
  ) -> CommandConfig:
326
325
  if config is not None:
327
326
  if kwargs:
@@ -469,7 +468,7 @@ def get_context(command: Command) -> Context:
469
468
 
470
469
  def get_parsed(
471
470
  command: Command, to_call: Callable = None, default: Any = None, include_defaults: Bool = True
472
- ) -> Dict[str, Any]:
471
+ ) -> dict[str, Any]:
473
472
  """
474
473
  Provides a way to obtain all of the arguments that were parsed for the given Command as a dictionary.
475
474