cli-command-parser 2024.5.18.post1__tar.gz → 2024.5.31__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.
- {cli_command_parser-2024.5.18.post1/lib/cli_command_parser.egg-info → cli_command_parser-2024.5.31}/PKG-INFO +1 -1
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/__version__.py +1 -1
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/commands.py +0 -3
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/core.py +5 -2
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/inputs/files.py +25 -6
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/inputs/numeric.py +9 -24
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/inputs/time.py +41 -13
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/inputs/utils.py +37 -4
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/parameters/choice_map.py +11 -1
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31/lib/cli_command_parser.egg-info}/PKG-INFO +1 -1
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/LICENSE +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/MANIFEST.in +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/entry_points.txt +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/__init__.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/__main__.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/annotations.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/command_parameters.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/compat.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/config.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/context.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/conversion/__init__.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/conversion/__main__.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/conversion/argparse_ast.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/conversion/argparse_utils.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/conversion/cli.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/conversion/command_builder.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/conversion/utils.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/conversion/visitor.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/documentation.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/error_handling.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/exceptions.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/formatting/__init__.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/formatting/commands.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/formatting/params.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/formatting/restructured_text.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/formatting/utils.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/inputs/__init__.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/inputs/base.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/inputs/choices.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/inputs/exceptions.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/inputs/patterns.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/metadata.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/nargs.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/parameters/__init__.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/parameters/actions.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/parameters/base.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/parameters/groups.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/parameters/option_strings.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/parameters/options.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/parameters/pass_thru.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/parameters/positionals.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/parse_tree.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/parser.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/testing.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/typing.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/utils.py +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser.egg-info/SOURCES.txt +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser.egg-info/dependency_links.txt +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser.egg-info/entry_points.txt +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser.egg-info/requires.txt +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser.egg-info/top_level.txt +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/pyproject.toml +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/readme.rst +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/requirements-dev.txt +0 -0
- {cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/setup.cfg +0 -0
|
@@ -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.
|
|
4
|
+
__version__ = '2024.05.31'
|
|
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
|
{cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/core.py
RENAMED
|
@@ -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
|
|
@@ -188,6 +188,11 @@ class Serialized(File):
|
|
|
188
188
|
self.converter = converter
|
|
189
189
|
self.pass_file = pass_file
|
|
190
190
|
|
|
191
|
+
def __repr__(self) -> str:
|
|
192
|
+
non_defaults = ', '.join(f'{k}={v!r}' for k, v in self.__dict__.items() if k != 'converter')
|
|
193
|
+
# `converter` must be excluded to prevent infinite recursion when an instance method is stored in that attr
|
|
194
|
+
return f'<{self.__class__.__name__}({non_defaults})>'
|
|
195
|
+
|
|
191
196
|
def _prep_file_wrapper(self, path: _Path) -> FileWrapper:
|
|
192
197
|
return FileWrapper(path, self.mode, self.encoding, self.errors, self.converter, self.pass_file, self.parents)
|
|
193
198
|
|
|
@@ -197,14 +202,28 @@ class Json(Serialized):
|
|
|
197
202
|
:param kwargs: Additional keyword arguments to pass to :class:`.File`
|
|
198
203
|
"""
|
|
199
204
|
|
|
200
|
-
def __init__(self, *, mode: str = 'rb', **kwargs):
|
|
205
|
+
def __init__(self, *, mode: str = 'rb', wrap_errors: bool = True, **kwargs):
|
|
201
206
|
import json
|
|
202
207
|
|
|
203
|
-
# TODO: catch JSONDecodeError and provide a standardized cleaner error message (with a way to disable this error handling)
|
|
204
|
-
|
|
205
208
|
write = allows_write(mode, True)
|
|
206
|
-
kwargs['pass_file'] =
|
|
207
|
-
super().__init__(json.dump if write else
|
|
209
|
+
kwargs['pass_file'] = True
|
|
210
|
+
super().__init__(json.dump if write else self._load_json, mode=mode, **kwargs)
|
|
211
|
+
self.wrap_errors = wrap_errors
|
|
212
|
+
|
|
213
|
+
def _load_json(self, f: FP):
|
|
214
|
+
from json import JSONDecodeError, load
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
return load(f)
|
|
218
|
+
except JSONDecodeError as e:
|
|
219
|
+
if self.wrap_errors:
|
|
220
|
+
if name := getattr(f, 'name', None):
|
|
221
|
+
msg = f'json from file={name!r} - are you sure it contains properly formatted json?'
|
|
222
|
+
else:
|
|
223
|
+
msg = "the provided json content - are you sure it's properly formatted json?"
|
|
224
|
+
raise InputValidationError(f'Unable to load {msg} - error: {e}') from e
|
|
225
|
+
else:
|
|
226
|
+
raise
|
|
208
227
|
|
|
209
228
|
|
|
210
229
|
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
|
-
|
|
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.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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['
|
|
36
|
-
_TIMEDELTA_UNITS = {'
|
|
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__(
|
|
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
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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.
|
|
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
|
|
14
|
-
from typing import TYPE_CHECKING,
|
|
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,
|
|
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:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/compat.py
RENAMED
|
File without changes
|
{cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/nargs.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/parser.py
RENAMED
|
File without changes
|
|
File without changes
|
{cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/typing.py
RENAMED
|
File without changes
|
{cli_command_parser-2024.5.18.post1 → cli_command_parser-2024.5.31}/lib/cli_command_parser/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|