cli-command-parser 2024.5.18.post1__tar.gz → 2024.5.19__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 (65) hide show
  1. {cli_command_parser-2024.5.18.post1/lib/cli_command_parser.egg-info → cli_command_parser-2024.5.19}/PKG-INFO +1 -1
  2. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/__version__.py +1 -1
  3. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/commands.py +0 -3
  4. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/core.py +5 -2
  5. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/inputs/files.py +20 -6
  6. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/inputs/numeric.py +9 -24
  7. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/inputs/time.py +41 -13
  8. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/inputs/utils.py +37 -4
  9. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/parameters/choice_map.py +11 -1
  10. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19/lib/cli_command_parser.egg-info}/PKG-INFO +1 -1
  11. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/LICENSE +0 -0
  12. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/MANIFEST.in +0 -0
  13. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/entry_points.txt +0 -0
  14. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/__init__.py +0 -0
  15. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/__main__.py +0 -0
  16. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/annotations.py +0 -0
  17. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/command_parameters.py +0 -0
  18. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/compat.py +0 -0
  19. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/config.py +0 -0
  20. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/context.py +0 -0
  21. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/conversion/__init__.py +0 -0
  22. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/conversion/__main__.py +0 -0
  23. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/conversion/argparse_ast.py +0 -0
  24. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/conversion/argparse_utils.py +0 -0
  25. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/conversion/cli.py +0 -0
  26. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/conversion/command_builder.py +0 -0
  27. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/conversion/utils.py +0 -0
  28. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/conversion/visitor.py +0 -0
  29. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/documentation.py +0 -0
  30. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/error_handling.py +0 -0
  31. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/exceptions.py +0 -0
  32. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/formatting/__init__.py +0 -0
  33. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/formatting/commands.py +0 -0
  34. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/formatting/params.py +0 -0
  35. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/formatting/restructured_text.py +0 -0
  36. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/formatting/utils.py +0 -0
  37. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/inputs/__init__.py +0 -0
  38. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/inputs/base.py +0 -0
  39. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/inputs/choices.py +0 -0
  40. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/inputs/exceptions.py +0 -0
  41. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/inputs/patterns.py +0 -0
  42. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/metadata.py +0 -0
  43. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/nargs.py +0 -0
  44. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/parameters/__init__.py +0 -0
  45. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/parameters/actions.py +0 -0
  46. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/parameters/base.py +0 -0
  47. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/parameters/groups.py +0 -0
  48. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/parameters/option_strings.py +0 -0
  49. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/parameters/options.py +0 -0
  50. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/parameters/pass_thru.py +0 -0
  51. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/parameters/positionals.py +0 -0
  52. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/parse_tree.py +0 -0
  53. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/parser.py +0 -0
  54. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/testing.py +0 -0
  55. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/typing.py +0 -0
  56. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser/utils.py +0 -0
  57. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser.egg-info/SOURCES.txt +0 -0
  58. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser.egg-info/dependency_links.txt +0 -0
  59. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser.egg-info/entry_points.txt +0 -0
  60. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser.egg-info/requires.txt +0 -0
  61. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/lib/cli_command_parser.egg-info/top_level.txt +0 -0
  62. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/pyproject.toml +0 -0
  63. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/readme.rst +0 -0
  64. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/requirements-dev.txt +0 -0
  65. {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.19}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cli_command_parser
3
- Version: 2024.5.18.post1
3
+ Version: 2024.5.19
4
4
  Summary: CLI Command Parser
5
5
  Home-page: https://github.com/dskrypa/cli_command_parser
6
6
  Author: Doug Skrypa
@@ -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.05.18-1'
4
+ __version__ = '2024.05.19'
5
5
  __author__ = 'Doug Skrypa'
6
6
  __author_email__ = 'dskrypa@gmail.com'
7
7
  __license__ = 'Apache 2.0'
@@ -29,9 +29,6 @@ Argv = Sequence[str]
29
29
  class Command(ABC, metaclass=CommandMeta):
30
30
  """The main class that other Commands should extend."""
31
31
 
32
- # TODO: Make the distinction between help/description clearer, or merge them?
33
- # TODO: Pull help text from docstring for subcommands if not specified as help=?
34
-
35
32
  #: The parsing Context used for this Command. Provided here for convenience - this reference to it is not used by
36
33
  #: any CLI Command Parser internals, so it is safe for subclasses to redefine / overwrite it.
37
34
  ctx: Context
@@ -94,11 +94,14 @@ class CommandMeta(ABCMeta, type):
94
94
  namespace['_CommandMeta__config'] = config
95
95
 
96
96
  cls = super().__new__(mcs, name, bases, namespace, **kwargs)
97
+ if metadata:
98
+ # If no overrides were provided, then this is skipped, and it will be initialized lazily later
99
+ # This must be set before calling _maybe_register_sub_cmd so overrides are available during registration
100
+ cls.__metadata = ProgramMetadata.for_command(cls, parent=mcs._from_parent(mcs.meta, bases), **metadata)
101
+
97
102
  if ABC not in bases:
98
103
  mcs._commands.add(cls)
99
104
  mcs._maybe_register_sub_cmd(cls, choice, choices, help)
100
- if metadata: # If no overrides were provided, then initialize lazily later
101
- cls.__metadata = ProgramMetadata.for_command(cls, parent=mcs._from_parent(mcs.meta, bases), **metadata)
102
105
 
103
106
  return cls
104
107
 
@@ -11,7 +11,7 @@ from abc import ABC
11
11
  from pathlib import Path as _Path
12
12
  from typing import Optional, Union
13
13
 
14
- from ..typing import Bool, Converter, PathLike, T
14
+ from ..typing import FP, Bool, Converter, PathLike, T
15
15
  from .base import InputType
16
16
  from .exceptions import InputValidationError
17
17
  from .utils import FileWrapper, InputParam, StatMode, allows_write, fix_windows_path
@@ -197,14 +197,28 @@ class Json(Serialized):
197
197
  :param kwargs: Additional keyword arguments to pass to :class:`.File`
198
198
  """
199
199
 
200
- def __init__(self, *, mode: str = 'rb', **kwargs):
200
+ def __init__(self, *, mode: str = 'rb', wrap_errors: bool = True, **kwargs):
201
201
  import json
202
202
 
203
- # TODO: catch JSONDecodeError and provide a standardized cleaner error message (with a way to disable this error handling)
204
-
205
203
  write = allows_write(mode, True)
206
- kwargs['pass_file'] = write # json.load just calls loads with f.read()
207
- super().__init__(json.dump if write else json.loads, mode=mode, **kwargs)
204
+ kwargs['pass_file'] = True
205
+ super().__init__(json.dump if write else self._load_json, mode=mode, **kwargs)
206
+ self.wrap_errors = wrap_errors
207
+
208
+ def _load_json(self, f: FP):
209
+ from json import JSONDecodeError, load
210
+
211
+ try:
212
+ return load(f)
213
+ except JSONDecodeError as e:
214
+ if self.wrap_errors:
215
+ if name := getattr(f, 'name', None):
216
+ msg = f'json from file={name!r} - are you sure it contains properly formatted json?'
217
+ else:
218
+ msg = "the provided json content - are you sure it's properly formatted json?"
219
+ raise InputValidationError(f'Unable to load {msg} - error: {e}') from e
220
+ else:
221
+ raise
208
222
 
209
223
 
210
224
  class Pickle(Serialized):
@@ -13,6 +13,7 @@ from typing import Optional, Union
13
13
  from ..typing import NT, Bool, Number, NumType, RngType
14
14
  from .base import InputType
15
15
  from .exceptions import InputValidationError
16
+ from .utils import RangeMixin, range_str
16
17
 
17
18
  __all__ = ['Range', 'NumRange']
18
19
 
@@ -102,7 +103,7 @@ class Range(NumericInput[NT]):
102
103
  raise InputValidationError(f'expected a value in the range {self._range_str()}')
103
104
 
104
105
 
105
- class NumRange(NumericInput[NT]):
106
+ class NumRange(RangeMixin, NumericInput[NT]):
106
107
  """
107
108
  A range of integers or floats, optionally only bounded on one side.
108
109
 
@@ -122,10 +123,6 @@ class NumRange(NumericInput[NT]):
122
123
 
123
124
  __slots__ = ('type', 'snap', 'min', 'max', 'include_min', 'include_max')
124
125
  snap: bool
125
- min: Number
126
- max: Number
127
- include_min: bool
128
- include_max: bool
129
126
 
130
127
  def __init__(
131
128
  self,
@@ -170,17 +167,7 @@ class NumRange(NumericInput[NT]):
170
167
  return f'<{self.__class__.__name__}({self.type!r}, snap={self.snap!r})[{self._range_str()}]>'
171
168
 
172
169
  def _range_str(self, var: str = 'N') -> str:
173
- if self.min is not None:
174
- min_str = f'{self.min} {"<=" if self.include_min else "<"} '
175
- else:
176
- min_str = ''
177
-
178
- if self.max is not None:
179
- max_str = f' {"<=" if self.include_max else "<"} {self.max}'
180
- else:
181
- max_str = ''
182
-
183
- return f'{min_str}{var}{max_str}'
170
+ return range_str(self.min, self.max, self.include_min, self.include_max, var)
184
171
 
185
172
  def handle_invalid(self, bound: Number, inclusive: bool, snap_dir: int) -> Number:
186
173
  """
@@ -199,11 +186,9 @@ class NumRange(NumericInput[NT]):
199
186
 
200
187
  def __call__(self, value: str) -> NT:
201
188
  value = self.type(value)
202
- if self.min is not None:
203
- # Bad if < when inclusive, bad if <= when exclusive
204
- if (value < self.min) if self.include_min else (value <= self.min):
205
- return self.handle_invalid(self.min, self.include_min, 1)
206
- if self.max is not None:
207
- if (value > self.max) if self.include_max else (value >= self.max):
208
- return self.handle_invalid(self.max, self.include_max, -1)
209
- return value
189
+ if self.value_lt_min(value):
190
+ return self.handle_invalid(self.min, self.include_min, 1)
191
+ elif self.value_gt_max(value):
192
+ return self.handle_invalid(self.max, self.include_max, -1)
193
+ else:
194
+ return value
@@ -24,16 +24,17 @@ from locale import LC_ALL, setlocale
24
24
  from threading import RLock
25
25
  from typing import Collection, Iterator, Literal, Optional, Sequence, Type, TypeVar, Union, overload
26
26
 
27
- from ..typing import Bool, Locale, T, TimeBound
27
+ from ..typing import Bool, Locale, Number, T, TimeBound
28
28
  from ..utils import MissingMixin
29
29
  from .base import InputType
30
30
  from .exceptions import InputValidationError, InvalidChoiceError
31
+ from .utils import RangeMixin, range_str
31
32
 
32
33
  __all__ = ['DTFormatMode', 'Day', 'Month', 'TimeDelta', 'DateTime', 'Date', 'Time']
33
34
 
34
35
  DT = TypeVar('DT')
35
- TimeUnit = Literal['days', 'seconds', 'microseconds', 'milliseconds', 'minutes', 'hours', 'weeks']
36
- _TIMEDELTA_UNITS = {'days', 'seconds', 'microseconds', 'milliseconds', 'minutes', 'hours', 'weeks'}
36
+ TimeUnit = Literal['microseconds', 'milliseconds', 'seconds', 'minutes', 'hours', 'days', 'weeks']
37
+ _TIMEDELTA_UNITS = {'microseconds', 'milliseconds', 'seconds', 'minutes', 'hours', 'days', 'weeks'}
37
38
  DEFAULT_DATE_FMT = '%Y-%m-%d'
38
39
  DEFAULT_TIME_FMT = '%H:%M:%S'
39
40
  DEFAULT_DT_FMT = '%Y-%m-%d %H:%M:%S'
@@ -227,8 +228,8 @@ class Day(CalendarUnitInput, dt_type='day of the week'):
227
228
  _formats = {
228
229
  DTFormatMode.FULL: day_name,
229
230
  DTFormatMode.ABBREVIATION: day_abbr,
230
- DTFormatMode.NUMERIC: range(7),
231
- DTFormatMode.NUMERIC_ISO: range(1, 8),
231
+ DTFormatMode.NUMERIC: range(7), # 0 = Monday; 6 = Sunday
232
+ DTFormatMode.NUMERIC_ISO: range(1, 8), # 1 = Monday; 7 = Sunday
232
233
  }
233
234
 
234
235
  @overload
@@ -336,35 +337,62 @@ class Month(CalendarUnitInput, dt_type='month', min_index=1):
336
337
  # endregion
337
338
 
338
339
 
339
- class TimeDelta(InputType[timedelta]):
340
- __slots__ = ('unit',)
340
+ class TimeDelta(RangeMixin, InputType[timedelta]):
341
+ __slots__ = ('unit', 'min', 'max', 'include_min', 'include_max', 'int_only')
341
342
 
342
- def __init__(self, unit: TimeUnit, fix_default: Bool = True):
343
+ def __init__(
344
+ self,
345
+ unit: TimeUnit,
346
+ *,
347
+ min: Number = None, # noqa
348
+ max: Number = None, # noqa
349
+ include_min: Bool = True,
350
+ include_max: Bool = False,
351
+ int_only: Bool = False,
352
+ fix_default: Bool = True,
353
+ ):
343
354
  unit = unit.lower()
344
355
  if unit not in _TIMEDELTA_UNITS:
345
356
  raise TypeError(f'Invalid {unit=} - expected one of: {", ".join(sorted(_TIMEDELTA_UNITS))}')
357
+ elif min is not None and max is not None and min >= max:
358
+ raise ValueError(f'Invalid {min=} >= {max=} - min must be less than max')
359
+
346
360
  super().__init__(fix_default)
347
361
  self.unit = unit
348
- # TODO: min/max params like NumRange?
362
+ self.min = min
363
+ self.max = max
364
+ self.include_min = include_min
365
+ self.include_max = include_max
366
+ self.int_only = int_only
349
367
 
350
368
  def __call__(self, value: Union[str, int, float]) -> timedelta:
351
369
  if isinstance(value, str):
352
370
  try:
353
371
  value = float(value.replace(',', '').replace('_', '')) # allow comma or _ between thousands
354
372
  except ValueError as e:
355
- raise InputValidationError(
356
- f'Invalid numeric {self.unit}={value!r} - expected an integer or float'
357
- ) from e
373
+ exp_type = 'integer' if self.int_only else 'integer or float'
374
+ raise self._invalid(value, f'expected an {exp_type}') from e
375
+
376
+ if self.value_lt_min(value) or self.value_gt_max(value):
377
+ raise self._invalid(value, f'expected a value in the range {self._range_str()}')
378
+ elif self.int_only and int(value) != value:
379
+ raise self._invalid(value, f'expected an integer, not a {value.__class__.__name__}')
358
380
 
359
381
  return timedelta(**{self.unit: value})
360
382
 
383
+ def _invalid(self, value: Number, message: str) -> InputValidationError:
384
+ return InputValidationError(f'Invalid numeric {self.unit}={value!r} - {message}')
385
+
386
+ def _range_str(self) -> str:
387
+ return range_str(self.min, self.max, self.include_min, self.include_max, self.unit)
388
+
361
389
  def fix_default(self, value: Union[int, float, timedelta, None]) -> Optional[timedelta]:
362
390
  if value is None or isinstance(value, timedelta) or not self._fix_default:
363
391
  return value
364
392
  return self(value)
365
393
 
366
394
  def format_metavar(self, choice_delim: str = ',', sort_choices: bool = False) -> str:
367
- return f'{{{self.unit}}}'
395
+ return f'{{{self._range_str()}}}'
368
396
 
369
397
 
370
398
  # region Date/Time Parse Inputs
@@ -10,17 +10,17 @@ import sys
10
10
  import warnings
11
11
  from contextlib import contextmanager
12
12
  from pathlib import Path
13
- from stat import S_IFMT, S_IFDIR, S_IFCHR, S_IFBLK, S_IFREG, S_IFIFO, S_IFLNK, S_IFSOCK
14
- from typing import TYPE_CHECKING, Union, Any, TextIO, BinaryIO, ContextManager
13
+ from stat import S_IFBLK, S_IFCHR, S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK
14
+ from typing import TYPE_CHECKING, Any, BinaryIO, ContextManager, TextIO, Union
15
15
  from weakref import finalize
16
16
 
17
17
  from ..utils import FixedFlag
18
18
  from .exceptions import InputValidationError
19
19
 
20
20
  if TYPE_CHECKING:
21
- from ..typing import Bool, FP, Converter
21
+ from ..typing import FP, Bool, Converter, Number
22
22
 
23
- __all__ = ['InputParam', 'StatMode', 'FileWrapper', 'fix_windows_path']
23
+ __all__ = ['InputParam', 'StatMode', 'FileWrapper', 'fix_windows_path', 'range_str', 'RangeMixin']
24
24
 
25
25
 
26
26
  class InputParam:
@@ -214,3 +214,36 @@ def fix_windows_path(path: Path) -> Path:
214
214
  return alt_path
215
215
  else:
216
216
  return path
217
+
218
+
219
+ def range_str(min_val: Number, max_val: Number, include_min: Bool, include_max: Bool, var: str = 'N') -> str:
220
+ if min_val is not None:
221
+ min_str = f'{min_val} {"<=" if include_min else "<"} '
222
+ else:
223
+ min_str = ''
224
+
225
+ if max_val is not None:
226
+ max_str = f' {"<=" if include_max else "<"} {max_val}'
227
+ else:
228
+ max_str = ''
229
+
230
+ return f'{min_str}{var}{max_str}'
231
+
232
+
233
+ class RangeMixin:
234
+ __slots__ = () # It isn't possible to use 2+ bases when they both have content in __slots__
235
+ min: Number
236
+ max: Number
237
+ include_min: bool
238
+ include_max: bool
239
+
240
+ def value_lt_min(self, value: Number) -> bool:
241
+ if self.min is not None:
242
+ # Bad if < when inclusive, bad if <= when exclusive
243
+ return (value < self.min) if self.include_min else (value <= self.min)
244
+ return False
245
+
246
+ def value_gt_max(self, value: Number) -> bool:
247
+ if self.max is not None:
248
+ return (value > self.max) if self.include_max else (value >= self.max)
249
+ return False
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
  from functools import partial
10
10
  from string import printable, whitespace
11
11
  from types import MethodType
12
- from typing import Callable, Collection, Generic, Mapping, NoReturn, Optional, Type, TypeVar, Union
12
+ from typing import TYPE_CHECKING, Callable, Collection, Generic, Mapping, NoReturn, Optional, Type, TypeVar, Union
13
13
 
14
14
  from ..context import ctx
15
15
  from ..exceptions import BadArgument, CommandDefinitionError, InvalidChoice, ParameterDefinitionError
@@ -20,6 +20,9 @@ from ..utils import _NotSet, camel_to_snake_case, short_repr
20
20
  from .actions import Concatenate
21
21
  from .base import BasePositional
22
22
 
23
+ if TYPE_CHECKING:
24
+ from ..metadata import ProgramMetadata
25
+
23
26
  __all__ = ['SubCommand', 'Action', 'Choice', 'ChoiceMap']
24
27
 
25
28
  T = TypeVar('T')
@@ -247,6 +250,13 @@ class SubCommand(ChoiceMap[CommandCls], title='Subcommands', choice_validation_e
247
250
  else:
248
251
  self._validate_positional(choice)
249
252
 
253
+ if help is None:
254
+ # This approach was used because importing get_metadata from core would result in a circular dependency
255
+ meta: ProgramMetadata = command.__class__.meta(command)
256
+ # print(f'Registering {choice=} -> {command=} w/ {meta.description=}, {meta.parent=}')
257
+ if meta.description and (not meta.parent or meta.parent.description != meta.description):
258
+ help = meta.description # noqa
259
+
250
260
  try:
251
261
  self.register_choice(choice, command, help)
252
262
  except CommandDefinitionError:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cli_command_parser
3
- Version: 2024.5.18.post1
3
+ Version: 2024.5.19
4
4
  Summary: CLI Command Parser
5
5
  Home-page: https://github.com/dskrypa/cli_command_parser
6
6
  Author: Doug Skrypa