cli-command-parser 2024.9.7__tar.gz → 2024.12.15__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 (66) hide show
  1. {cli_command_parser-2024.9.7/lib/cli_command_parser.egg-info → cli_command_parser-2024.12.15}/PKG-INFO +6 -18
  2. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/__version__.py +1 -1
  3. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/annotations.py +1 -10
  4. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/compat.py +0 -10
  5. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/conversion/utils.py +2 -19
  6. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/documentation.py +1 -1
  7. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/exceptions.py +8 -8
  8. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/formatting/commands.py +11 -6
  9. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/formatting/params.py +17 -10
  10. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/formatting/restructured_text.py +94 -55
  11. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/inputs/base.py +3 -3
  12. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/inputs/numeric.py +1 -1
  13. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/metadata.py +69 -4
  14. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/parameters/actions.py +21 -21
  15. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/parameters/base.py +16 -8
  16. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/parameters/choice_map.py +2 -0
  17. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/parameters/groups.py +7 -0
  18. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/parameters/options.py +9 -6
  19. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/parser.py +4 -3
  20. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15/lib/cli_command_parser.egg-info}/PKG-INFO +6 -18
  21. cli_command_parser-2024.12.15/lib/cli_command_parser.egg-info/requires.txt +3 -0
  22. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/readme.rst +3 -13
  23. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/requirements-dev.txt +1 -1
  24. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/setup.cfg +2 -4
  25. cli_command_parser-2024.9.7/lib/cli_command_parser.egg-info/requires.txt +0 -8
  26. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/LICENSE +0 -0
  27. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/MANIFEST.in +0 -0
  28. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/entry_points.txt +0 -0
  29. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/__init__.py +0 -0
  30. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/__main__.py +0 -0
  31. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/command_parameters.py +0 -0
  32. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/commands.py +0 -0
  33. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/config.py +0 -0
  34. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/context.py +0 -0
  35. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/conversion/__init__.py +0 -0
  36. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/conversion/__main__.py +0 -0
  37. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/conversion/argparse_ast.py +0 -0
  38. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/conversion/argparse_utils.py +0 -0
  39. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/conversion/cli.py +0 -0
  40. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/conversion/command_builder.py +0 -0
  41. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/conversion/visitor.py +0 -0
  42. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/core.py +0 -0
  43. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/error_handling.py +0 -0
  44. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/formatting/__init__.py +0 -0
  45. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/formatting/utils.py +0 -0
  46. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/inputs/__init__.py +0 -0
  47. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/inputs/choices.py +0 -0
  48. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/inputs/exceptions.py +0 -0
  49. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/inputs/files.py +0 -0
  50. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/inputs/patterns.py +0 -0
  51. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/inputs/time.py +0 -0
  52. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/inputs/utils.py +0 -0
  53. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/nargs.py +0 -0
  54. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/parameters/__init__.py +0 -0
  55. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/parameters/option_strings.py +0 -0
  56. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/parameters/pass_thru.py +0 -0
  57. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/parameters/positionals.py +0 -0
  58. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/parse_tree.py +0 -0
  59. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/testing.py +0 -0
  60. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/typing.py +0 -0
  61. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser/utils.py +0 -0
  62. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser.egg-info/SOURCES.txt +0 -0
  63. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser.egg-info/dependency_links.txt +0 -0
  64. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser.egg-info/entry_points.txt +0 -0
  65. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/lib/cli_command_parser.egg-info/top_level.txt +0 -0
  66. {cli_command_parser-2024.9.7 → cli_command_parser-2024.12.15}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cli_command_parser
3
- Version: 2024.9.7
3
+ Version: 2024.12.15
4
4
  Summary: CLI Command Parser
5
5
  Home-page: https://github.com/dskrypa/cli_command_parser
6
6
  Author: Doug Skrypa
@@ -16,27 +16,25 @@ Classifier: License :: OSI Approved :: Apache Software License
16
16
  Classifier: Operating System :: OS Independent
17
17
  Classifier: Programming Language :: Python
18
18
  Classifier: Programming Language :: Python :: 3
19
- Classifier: Programming Language :: Python :: 3.8
20
19
  Classifier: Programming Language :: Python :: 3.9
21
20
  Classifier: Programming Language :: Python :: 3.10
22
21
  Classifier: Programming Language :: Python :: 3.11
23
22
  Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
24
  Classifier: Topic :: Software Development :: User Interfaces
25
25
  Classifier: Topic :: Text Processing
26
- Requires-Python: >=3.8
26
+ Requires-Python: >=3.9
27
27
  Description-Content-Type: text/x-rst
28
28
  License-File: LICENSE
29
29
  Provides-Extra: wcwidth
30
30
  Requires-Dist: wcwidth; extra == "wcwidth"
31
- Provides-Extra: conversion
32
- Requires-Dist: astunparse; python_version < "3.9" and extra == "conversion"
33
31
 
34
32
  CLI Command Parser
35
33
  ##################
36
34
 
37
35
  |downloads| |py_version| |coverage_badge| |build_status| |Ruff|
38
36
 
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
37
+ .. |py_version| image:: https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20-blue
40
38
  :target: https://pypi.org/project/cli-command-parser/
41
39
 
42
40
  .. |coverage_badge| image:: https://codecov.io/gh/dskrypa/cli_command_parser/branch/main/graph/badge.svg
@@ -118,18 +116,8 @@ with optional dependencies::
118
116
  Python Version Compatibility
119
117
  ============================
120
118
 
121
- Python versions 3.8 and above are currently supported. The last release of CLI Command Parser that supported 3.7 was
122
- 2023-04-30. Support for Python 3.7 `officially ended on 2023-06-27 <https://devguide.python.org/versions/>`__.
123
-
124
- When using Python 3.8, some additional packages that backport functionality that was added in later Python versions
125
- are required for compatibility.
126
-
127
- To use the argparse to cli-command-parser conversion script with Python 3.8, there is a dependency on
128
- `astunparse <https://astunparse.readthedocs.io>`__. If you are using Python 3.9 or above, then ``astunparse`` is not
129
- necessary because the relevant code was added to the stdlib ``ast`` module. If you're unsure, you can install
130
- cli-command-parser with the following command to automatically handle whether that extra dependency is needed or not::
131
-
132
- $ pip install -U cli-command-parser[conversion]
119
+ Python versions 3.9 and above are currently supported. The last release of CLI Command Parser that supported 3.8 was
120
+ 2024-09-07. Support for Python 3.8 `officially ended on 2024-10-07 <https://devguide.python.org/versions/>`__.
133
121
 
134
122
 
135
123
  Links
@@ -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.09.07'
4
+ __version__ = '2024.12.15'
5
5
  __author__ = 'Doug Skrypa'
6
6
  __author_email__ = 'dskrypa@gmail.com'
7
7
  __license__ = 'Apache 2.0'
@@ -7,7 +7,7 @@ Utilities for extracting types from annotations.
7
7
  from collections.abc import Collection, Iterable
8
8
  from functools import lru_cache
9
9
  from inspect import isclass
10
- from typing import Union, Optional, get_type_hints as _get_type_hints, get_origin, get_args as _get_args
10
+ from typing import Optional, Union, get_args, get_origin, get_type_hints as _get_type_hints
11
11
 
12
12
  try:
13
13
  from types import NoneType
@@ -42,15 +42,6 @@ def get_annotation_value_type(annotation, from_union: bool = True, from_collecti
42
42
  return None
43
43
 
44
44
 
45
- def get_args(annotation) -> tuple:
46
- """
47
- Wrapper around :func:`python:typing.get_args` for 3.7~8 compatibility, to make it behave more like it does in 3.9+
48
- """
49
- if getattr(annotation, '_special', False): # 3.7-3.8 generic collection alias with no content types
50
- return ()
51
- return _get_args(annotation)
52
-
53
-
54
45
  def _type_from_union(annotation) -> Optional[type]:
55
46
  args = get_args(annotation)
56
47
  # Note: Unions of a single argument return the argument; i.e., Union[T] returns T, so the len can never be 1
@@ -1,9 +1,4 @@
1
1
  """
2
- Compatibility / Patch module - used to back-port features to Python 3.7 and to avoid breaking changes in Enum/Flag in
3
- 3.11.
4
-
5
- Contains stdlib CPython functions / classes from Python 3.8 and 3.10.
6
-
7
2
  The :class:`WCTextWrapper` in this module extends the stdlib :class:`python:textwrap.TextWrapper` to support wide
8
3
  characters.
9
4
  """
@@ -16,8 +11,6 @@ from .utils import wcswidth
16
11
 
17
12
  __all__ = ['WCTextWrapper']
18
13
 
19
- # region textwrap
20
-
21
14
 
22
15
  class WCTextWrapper(TextWrapper):
23
16
  """
@@ -119,6 +112,3 @@ class WCTextWrapper(TextWrapper):
119
112
  break
120
113
 
121
114
  return lines
122
-
123
-
124
- # endregion
@@ -1,24 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- from ast import AST, expr, Call, Attribute, Name, Dict, List, Set, Tuple
4
-
5
- try:
6
- from ast import unparse
7
- except ImportError: # added in 3.9
8
- try:
9
- from astunparse import unparse as _unparse
10
- except ImportError as e:
11
- raise RuntimeError(
12
- 'Missing required dependency: astunparse (only required in Python 3.8 and below'
13
- ' - upgrade to 3.9 or above to avoid this dependency)'
14
- )
15
- else:
16
-
17
- def unparse(node):
18
- return ''.join(_unparse(node).splitlines())
19
-
20
-
21
- from typing import Union, Iterator, List as _List
3
+ from ast import AST, Attribute, Call, Dict, List, Name, Set, Tuple, expr, unparse
4
+ from typing import Iterator, List as _List, Union
22
5
 
23
6
  __all__ = ['get_name_repr', 'iter_module_parents', 'collection_contents']
24
7
 
@@ -372,6 +372,6 @@ class RstWriter:
372
372
  path = target_dir.joinpath(name + self.ext)
373
373
  log.debug(f'{prefix} {path.as_posix()}')
374
374
  if not self.dry_run:
375
- # Path.write_text on 3.8 does not support `newline`
375
+ # Path.write_text on 3.9 does not support `newline`
376
376
  with path.open('w', encoding=self.encoding, newline=self.newline) as f:
377
377
  f.write(content)
@@ -8,14 +8,14 @@ Exceptions for Command Parser
8
8
  from __future__ import annotations
9
9
 
10
10
  import sys
11
- from typing import TYPE_CHECKING, Any, Optional, Collection, Mapping
11
+ from typing import TYPE_CHECKING, Any, Collection, Mapping, Optional
12
12
 
13
13
  from .utils import _parse_tree_target_repr
14
14
 
15
15
  if TYPE_CHECKING:
16
- from .parameters import Parameter, BaseOption
16
+ from .parameters import BaseOption, Parameter
17
+ from .parse_tree import PosNode, Target, Word
17
18
  from .typing import ParamOrGroup
18
- from .parse_tree import PosNode, Word, Target
19
19
 
20
20
  __all__ = [
21
21
  'CommandParserException',
@@ -211,13 +211,13 @@ class BadArgument(ParamUsageError):
211
211
  class InvalidChoice(BadArgument):
212
212
  """Error raised when a value that does not match one of the pre-defined choices was provided for a Parameter"""
213
213
 
214
- def __init__(self, param: Optional[Parameter], invalid: Any, choices: Collection[Any]):
214
+ def __init__(self, param: Optional[Parameter], invalid: Any, choices: Collection[Any], env_var: str = None):
215
+ src = f' from env var={env_var!r}' if env_var else ''
215
216
  if isinstance(invalid, Collection) and not isinstance(invalid, str):
216
- bad_str = f'choices: {", ".join(map(repr, invalid))}'
217
+ bad_str = f'choices{src}: {", ".join(map(repr, invalid))}'
217
218
  else:
218
- bad_str = f'choice: {invalid!r}'
219
- choices_str = ', '.join(map(repr, choices))
220
- super().__init__(param, f'invalid {bad_str} (choose from: {choices_str})')
219
+ bad_str = f'choice{src}: {invalid!r}'
220
+ super().__init__(param, f'invalid {bad_str} (choose from: {", ".join(map(repr, choices))})')
221
221
 
222
222
 
223
223
  class MissingArgument(BadArgument):
@@ -12,9 +12,10 @@ from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Type,
12
12
 
13
13
  from ..context import NoActiveContext, ctx
14
14
  from ..core import get_metadata, get_params
15
+ from ..parameters.choice_map import ChoiceMap
15
16
  from ..parameters.groups import ParamGroup
16
17
  from ..utils import _NotSet, camel_to_snake_case
17
- from .restructured_text import RstTable, spaced_rst_header
18
+ from .restructured_text import spaced_rst_header
18
19
  from .utils import PartWrapper
19
20
 
20
21
  if TYPE_CHECKING:
@@ -47,6 +48,7 @@ class CommandHelpFormatter:
47
48
  for group in groups:
48
49
  if group.group: # prevent duplicates
49
50
  continue
51
+
50
52
  if group.contains_positional:
51
53
  self.pos_group.add(group)
52
54
  else:
@@ -162,14 +164,17 @@ class CommandHelpFormatter:
162
164
  yield description
163
165
  yield ''
164
166
 
165
- # TODO: The subcommand names in the group containing subcommand targets should link to their respective
166
- # subcommand sections
167
- for group in self.groups:
167
+ if self.pos_group.show_in_help:
168
168
  # TODO: Nested subcommands' local choices should not repeat the `subcommands` positional arguments section
169
169
  # that includes the nested subcommand choice being documented
170
+ if len(members := self.pos_group.members) == 1 and isinstance(members[0], ChoiceMap):
171
+ yield from members[0].formatter.rst_table().iter_build() # noqa
172
+ else:
173
+ yield from self.pos_group.formatter.rst_table().iter_build()
174
+
175
+ for group in self.groups[1:]:
170
176
  if group.show_in_help:
171
- table: RstTable = group.formatter.rst_table() # noqa
172
- yield from table.iter_build()
177
+ yield from group.formatter.rst_table().iter_build()
173
178
 
174
179
  if include_epilog and (epilog := self._meta.format_epilog(config.extended_epilog, allow_sys_argv)):
175
180
  yield epilog
@@ -16,7 +16,7 @@ from ..core import get_config
16
16
  from ..parameters import ParamGroup, PassThru, TriFlag
17
17
  from ..parameters.base import BaseOption, BasePositional
18
18
  from ..parameters.choice_map import Choice, ChoiceMap
19
- from .restructured_text import RstTable
19
+ from .restructured_text import Cell, Row, RstTable
20
20
  from .utils import _should_add_default, format_help_entry
21
21
 
22
22
  if TYPE_CHECKING:
@@ -223,6 +223,8 @@ class TriFlagHelpFormatter(OptionHelpFormatter, param_cls=TriFlag):
223
223
 
224
224
 
225
225
  class ChoiceMapHelpFormatter(ParamHelpFormatter, param_cls=ChoiceMap):
226
+ """Formatter for :class:`SubCommand` and :class:`Action` parameters (and any other params that extend ChoiceMap)"""
227
+
226
228
  param: ChoiceMap
227
229
 
228
230
  @cached_property
@@ -269,6 +271,7 @@ class ChoiceMapHelpFormatter(ParamHelpFormatter, param_cls=ChoiceMap):
269
271
 
270
272
  def _format_rst_rows(self) -> Iterator[tuple[str, OptStr]]:
271
273
  mode = ctx.config.cmd_alias_mode or SubcommandAliasHelpMode.ALIAS
274
+ # TODO: The subcommand names should link to their respective subcommand sections
272
275
  for choice_group in self.choice_groups:
273
276
  for choice, usage, description in choice_group.prepare(mode):
274
277
  yield f'``{usage}``', description
@@ -492,15 +495,19 @@ class GroupHelpFormatter(ParamHelpFormatter, param_cls=ParamGroup): # noqa # p
492
495
  table = RstTable(self.format_description())
493
496
  # TODO: non-nested when config.show_group_tree is False; maybe separate options for rst vs help
494
497
  for member in self.param.members:
495
- if member.show_in_help:
496
- formatter = member.formatter
497
- try:
498
- sub_table: RstTable = formatter.rst_table() # noqa
499
- except AttributeError:
500
- table.add_rows(formatter.rst_rows())
501
- else:
502
- sub_table.show_title = False
503
- table.add_row(sub_table.title, str(sub_table))
498
+ if not member.show_in_help:
499
+ continue
500
+
501
+ formatter = member.formatter
502
+ try:
503
+ sub_table: RstTable = formatter.rst_table() # noqa
504
+ except AttributeError:
505
+ table.add_rows(formatter.rst_rows())
506
+ else:
507
+ table._add_row(Row([Cell(str(sub_table), ext_right=True), Cell()]))
508
+ # If a config option to switch to the old way is added later, the old approach:
509
+ # sub_table.show_title = False
510
+ # table.add_row(sub_table.title, str(sub_table))
504
511
 
505
512
  return table
506
513
 
@@ -6,11 +6,8 @@ Utilities for formatting data using RST markup
6
6
 
7
7
  from __future__ import annotations
8
8
 
9
- from itertools import starmap
10
9
  from typing import TYPE_CHECKING, Any, Iterable, Iterator, Mapping, Sequence, TypeVar, Union
11
10
 
12
- from .utils import line_iter
13
-
14
11
  if TYPE_CHECKING:
15
12
  from ..typing import Bool, OptStr, Strings
16
13
 
@@ -119,7 +116,7 @@ class RstTable:
119
116
  body of this table.
120
117
  """
121
118
 
122
- __slots__ = ('title', 'subtitle', 'show_title', 'use_table_directive', 'rows', 'widths')
119
+ __slots__ = ('title', 'subtitle', 'show_title', 'use_table_directive', '_rows', '_widths', '_updated')
123
120
 
124
121
  def __init__(
125
122
  self,
@@ -134,8 +131,9 @@ class RstTable:
134
131
  self.subtitle = subtitle
135
132
  self.show_title = show_title
136
133
  self.use_table_directive = use_table_directive
137
- self.rows = []
138
- self.widths = []
134
+ self._rows = []
135
+ self._widths = ()
136
+ self._updated = False
139
137
  if headers:
140
138
  self.add_row(*headers, header=True)
141
139
 
@@ -163,6 +161,13 @@ class RstTable:
163
161
  table.add_kv_rows(data)
164
162
  return table
165
163
 
164
+ @property
165
+ def widths(self) -> tuple[int, ...]:
166
+ if self._updated:
167
+ self._widths = tuple(max(col) for col in zip(*(row.widths() for row in self._rows)))
168
+ self._updated = False
169
+ return self._widths
170
+
166
171
  def add_dict_rows(self, rows: RowMaps, columns: Sequence[T] = None, add_header: Bool = False):
167
172
  """Add a row for each dict in the given sequence of rows, where the keys represent the columns."""
168
173
  if not columns:
@@ -180,8 +185,11 @@ class RstTable:
180
185
  self.add_rows(data.items())
181
186
 
182
187
  def add_rows(self, rows: Iterable[Iterable[OptStr]]):
183
- for row in rows:
184
- self.add_row(*row)
188
+ self._add_rows(Row([Cell(c or '') for c in columns]) for columns in rows)
189
+
190
+ def _add_rows(self, rows: Iterable[Row]):
191
+ self._rows.extend(rows)
192
+ self._updated = True
185
193
 
186
194
  def add_row(self, *columns: OptStr, index: int = None, header: bool = False):
187
195
  """
@@ -192,34 +200,18 @@ class RstTable:
192
200
  the list of rows.
193
201
  :param header: If True, this row will be treated as a header row. Does not affect insertion order.
194
202
  """
195
- any_new_line, widths = _widths(columns)
196
- if self.widths:
197
- self.widths = tuple(starmap(max, zip(self.widths, widths)))
198
- else:
199
- self.widths = tuple(widths)
203
+ self._add_row(Row([Cell(c or '') for c in columns], header), index)
200
204
 
201
- columns = tuple(c or '' for c in columns)
205
+ def _add_row(self, row: Row, index: int = None):
202
206
  if index is None:
203
- self.rows.append((header, any_new_line, columns))
207
+ self._rows.append(row)
204
208
  else:
205
- self.rows.insert(index, (header, any_new_line, columns))
206
-
207
- def bar(self, char: str = '-') -> str:
208
- """
209
- :param char: The character to use for the bar. Defaults to ``-`` (for normal rows). Use ``=`` below a header
210
- row. See :du_rst:`Grid Tables<grid-tables>` for more info.
211
- :return: The formatted bar string
212
- """
213
- pre = ' ' if self.use_table_directive else ''
214
- return '+'.join([pre, *(char * (w + 2) for w in self.widths), ''])
215
-
216
- def _get_row_format(self) -> str:
217
- pre = ' ' if self.use_table_directive else ''
218
- return '|'.join([pre, *(f' {{:<{w}s}} ' for w in self.widths), ''])
209
+ self._rows.insert(index, row)
210
+ self._updated = True
219
211
 
220
212
  def __repr__(self) -> str:
221
213
  return (
222
- f'<RstTable[use_table_directive={self.use_table_directive}, rows={len(self.rows)},'
214
+ f'<RstTable[use_table_directive={self.use_table_directive}, rows={len(self._rows)},'
223
215
  f' title={self.title!r}, widths={self.widths}]>'
224
216
  )
225
217
 
@@ -230,38 +222,85 @@ class RstTable:
230
222
  yield ''
231
223
 
232
224
  if self.use_table_directive:
233
- options = {'subtitle': self.subtitle, 'widths': 'auto'}
234
- yield from _rst_directive('table', options=options, check=True)
225
+ yield from _rst_directive('table', options={'subtitle': self.subtitle, 'widths': 'auto'}, check=True)
235
226
  yield ''
236
-
237
- bar, header_bar = self.bar(), self.bar('=')
238
- format_row = self._get_row_format().format
239
- yield bar
240
- for header, any_new_line, row in self.rows:
241
- if any_new_line:
242
- for line in line_iter(*row):
243
- yield format_row(*line)
244
- else:
245
- yield format_row(*row)
246
-
247
- yield header_bar if header else bar
227
+ for line in self._iter_render():
228
+ yield ' ' + line
229
+ else:
230
+ yield from self._iter_render()
248
231
 
249
232
  yield ''
250
233
 
234
+ def _iter_render(self) -> Iterator[str]:
235
+ col_widths = self.widths
236
+ yield self._rows[0].render_upper_bar(col_widths)
237
+ for row in self._rows:
238
+ yield from row.render_lines(col_widths)
239
+
251
240
  def __str__(self) -> str:
252
241
  return '\n'.join(self.iter_build())
253
242
 
254
243
 
255
- def _widths(columns: Iterable[OptStr]) -> tuple[bool, list[int]]:
256
- widths = []
257
- any_new_line = False
258
- for column in columns:
259
- if not column:
260
- widths.append(0)
261
- elif '\n' in column:
262
- any_new_line = True
263
- widths.append(max(map(len, column.splitlines())))
244
+ class Cell:
245
+ __slots__ = ('text', 'lines', 'width', 'height', 'brd_bottom', 'brd_right')
246
+
247
+ def __init__(self, text: str = '', *, ext_right: bool = False, ext_below: bool = False):
248
+ self.text = text
249
+ if text:
250
+ self.lines = text.splitlines()
251
+ self.width = max(map(len, self.lines))
252
+ self.height = len(self.lines)
264
253
  else:
265
- widths.append(len(column))
254
+ self.lines = ['']
255
+ self.width = 0
256
+ self.height = 1
257
+
258
+ self.brd_bottom = not ext_below
259
+ self.brd_right = not ext_right
260
+
261
+ def render_upper_bar(self, width: int) -> str:
262
+ return ('-' * (width + 2)) + ('+' if self.brd_bottom else '|')
263
+
264
+ def render_lower_bar(self, width: int, char: str = '-') -> str:
265
+ if not self.brd_bottom:
266
+ char = ' '
267
+ return (char * (width + 2)) + ('+' if self.brd_bottom or self.brd_right else ' ')
268
+
269
+ def render_lines(self, width: int, max_lines: int) -> Iterator[str]:
270
+ format_line = f' {{:<{width}s}} {"|" if self.brd_right else " "}'.format
271
+ for line in self.lines:
272
+ yield format_line(line)
273
+
274
+ for _ in range(self.height, max_lines):
275
+ yield format_line('')
276
+
277
+ def __repr__(self) -> str:
278
+ return f'<{self.__class__.__name__}[{self.text!r}, width={self.width}, height={self.height}]>'
279
+
280
+
281
+ class Row:
282
+ __slots__ = ('cells', 'header')
283
+
284
+ def __init__(self, cells: list[Cell], header: bool = False):
285
+ self.cells = cells
286
+ self.header = header
287
+
288
+ def widths(self) -> Iterator[int]:
289
+ for cell in self.cells:
290
+ yield cell.width
291
+
292
+ def render_upper_bar(self, widths: Sequence[int]) -> str:
293
+ return '+' + ''.join(cell.render_upper_bar(w) for cell, w in zip(self.cells, widths))
294
+
295
+ def render_lower_bar(self, widths: Sequence[int]) -> str:
296
+ char = '=' if self.header else '-'
297
+ first = '+' if self.cells[0].brd_bottom else '|'
298
+ return first + ''.join(cell.render_lower_bar(w, char) for cell, w in zip(self.cells, widths))
299
+
300
+ def render_lines(self, widths: Sequence[int]) -> Iterator[str]:
301
+ max_lines = max(cell.height for cell in self.cells)
302
+ renderers = [cell.render_lines(w, max_lines) for cell, w in zip(self.cells, widths)]
303
+ for cell_strs in zip(*renderers):
304
+ yield '|' + ''.join(cell_strs)
266
305
 
267
- return any_new_line, widths
306
+ yield self.render_lower_bar(widths)
@@ -7,7 +7,7 @@ Custom input handlers for Parameters
7
7
  from abc import ABC, abstractmethod
8
8
  from typing import Any, Generic, Optional
9
9
 
10
- from ..typing import T, Bool
10
+ from ..typing import Bool, T
11
11
 
12
12
  __all__ = ['InputType']
13
13
 
@@ -25,11 +25,11 @@ class InputType(Generic[T], ABC):
25
25
 
26
26
  def is_valid_type(self, value: str) -> bool: # pylint: disable=W0613
27
27
  """
28
- Called during parsing when :meth:`.Parameter.would_accept` is called to determine if the value would be
28
+ Called during parsing when :meth:`.ParamAction.would_accept` is called to determine if the value would be
29
29
  accepted later for processing / conversion via :meth:`.__call__`. May be overridden in subclasses to
30
30
  provide actual validation, if necessary.
31
31
 
32
- Not called by :meth:`.Parameter.take_action` - value validation should happen in :meth:`.__call__`
32
+ Note: value validation should happen in :meth:`.__call__`, not in this method.
33
33
 
34
34
  :param value: A parsed argument
35
35
  :return: True if this input would accept it for processing later (where it may still be rejected), False if
@@ -26,7 +26,7 @@ class NumericInput(InputType[NT], ABC):
26
26
 
27
27
  def is_valid_type(self, value: str) -> bool:
28
28
  """
29
- Called during parsing when :meth:`.Parameter.would_accept` is called to determine if the value would be
29
+ Called during parsing when :meth:`.ParamAction.would_accept` is called to determine if the value would be
30
30
  accepted later for processing / conversion when called.
31
31
 
32
32
  :param value: The parsed argument to validate