Coverage for typer / _click / types.py: 100%
271 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-09 00:15 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-09 00:15 +0000
1import os 1aebfcdg
2import sys 1aebfcdg
3from collections.abc import Callable, Sequence 1aebfcdg
4from datetime import datetime 1aebfcdg
5from typing import ( 1aebfcdg
6 IO,
7 TYPE_CHECKING,
8 Any,
9 ClassVar,
10 Literal,
11 NoReturn,
12 TypedDict,
13 TypeGuard,
14 TypeVar,
15 Union,
16 cast,
17)
19from ._compat import _get_argv_encoding, open_stream 1aebfcdg
20from .exceptions import BadParameter 1aebfcdg
21from .utils import LazyFile, _escape_control_characters, format_filename, safecall 1aebfcdg
23if TYPE_CHECKING: 1aebfcdg
24 from .core import Context, Parameter
25 from .shell_completion import CompletionItem
27ParamTypeValue = TypeVar("ParamTypeValue") 1aebfcdg
30class ParamType: 1aebfcdg
31 """Represents the type of a parameter. Validates and converts values
32 from the command line or Python into the correct type.
34 To implement a custom type, subclass and implement at least the
35 following:
37 - The `name` class attribute must be set.
38 - Calling an instance of the type with ``None`` must return
39 ``None``. This is already implemented by default.
40 - `convert` must convert string values to the correct type.
41 - `convert` must accept values that are already the correct
42 type.
43 - It must be able to convert a value if the ``ctx`` and ``param``
44 arguments are ``None``. This can occur when converting prompt
45 input.
46 """
48 is_composite: ClassVar[bool] = False 1aebfcdg
49 arity: ClassVar[int] = 1 1aebfcdg
50 name: str 1abcd
52 # if a list of this type is expected and the value is pulled from a
53 # string environment variable, this is what splits it up. `None`
54 # means any whitespace. For all parameters the general rule is that
55 # whitespace splits them up. The exception are paths and files which
56 # are split by ``os.path.pathsep`` by default (":" on Unix and ";" on
57 # Windows).
58 envvar_list_splitter: ClassVar[str | None] = None 1aebfcdg
60 def __call__( 1aebfcdg
61 self,
62 value: Any,
63 param: Union["Parameter", None] = None,
64 ctx: Union["Context", None] = None,
65 ) -> Any:
66 if value is not None: 1aebfcdg
67 return self.convert(value, param, ctx) 1aebfcdg
69 def get_metavar(self, param: "Parameter", ctx: "Context") -> str | None: 1aebfcdg
70 """Returns the metavar default for this param if it provides one."""
71 pass # pragma: no cover 1aebfcdg
73 def get_missing_message( 1aebfcdg
74 self, param: "Parameter", ctx: Union["Context", None]
75 ) -> str | None:
76 """Optionally might return extra information about a missing
77 parameter.
78 """
79 pass # pragma: no cover 1aebfcdg
81 def convert( 1aebfcdg
82 self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None]
83 ) -> Any:
84 pass # pragma: no cover
86 def split_envvar_value(self, rv: str) -> Sequence[str]: 1aebfcdg
87 """Given a value from an environment variable this splits it up
88 into small chunks depending on the defined envvar list splitter.
90 If the splitter is set to `None`, which means that whitespace splits,
91 then leading and trailing whitespace is ignored. Otherwise, leading
92 and trailing splitters usually lead to empty items being included.
93 """
94 return (rv or "").split(self.envvar_list_splitter) 1aebfcdg
96 def fail( 1aebfcdg
97 self,
98 message: str,
99 param: Union["Parameter", None] = None,
100 ctx: Union["Context", None] = None,
101 ) -> NoReturn:
102 """Helper method to fail with an invalid value message."""
103 raise BadParameter(message, ctx=ctx, param=param) 1aebfcdg
105 def shell_complete( 1aebfcdg
106 self, ctx: "Context", param: "Parameter", incomplete: str
107 ) -> list["CompletionItem"]:
108 """Return a list of `CompletionItem` objects for the
109 incomplete value. Most types do not provide completions, but
110 some do, and this allows custom types to provide custom
111 completions as well.
112 """
113 return [] 1aebfcdg
116class CompositeParamType(ParamType): 1aebfcdg
117 is_composite = True 1aebfcdg
119 @property 1aebfcdg
120 def arity(self) -> int: # type: ignore 1aebfcdg
121 raise NotImplementedError() # pragma: no cover
124class FuncParamType(ParamType): 1aebfcdg
125 def __init__(self, func: Callable[[Any], Any]) -> None: 1aebfcdg
126 self.name: str = getattr(func, "__name__", "function") 1aebfcdg
127 self.func = func 1aebfcdg
129 def convert( 1aebfcdg
130 self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None]
131 ) -> Any:
132 try: 1aebfcdg
133 return self.func(value) 1aebfcdg
134 except ValueError: 1aebfcdg
135 try: 1aebfcdg
136 value = str(value) 1aebfcdg
137 except UnicodeError: # pragma: no cover
138 assert isinstance(value, bytes)
139 value = value.decode("utf-8", "replace")
141 self.fail(_escape_control_characters(value), param, ctx) 1aebfcdg
144class StringParamType(ParamType): 1aebfcdg
145 name = "str" 1aebfcdg
147 def convert( 1aebfcdg
148 self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None]
149 ) -> Any:
150 if isinstance(value, bytes): 1aebfcdg
151 enc = _get_argv_encoding() 1aebfcdg
152 try: 1aebfcdg
153 value = value.decode(enc) 1aebfcdg
154 except UnicodeError: 1aebfcdg
155 fs_enc = sys.getfilesystemencoding() 1aebfcdg
156 if fs_enc != enc: 1aebfcdg
157 try: 1aebfcdg
158 value = value.decode(fs_enc) 1aebfcdg
159 except UnicodeError: 1aebfcdg
160 value = value.decode("utf-8", "replace") 1aebfcdg
161 else:
162 value = value.decode("utf-8", "replace") 1aebfcdg
163 return value 1aebfcdg
164 return str(value) 1aebfcdg
166 def __repr__(self) -> str: 1aebfcdg
167 return "STRING" 1aebfcdg
170class DateTime(ParamType): 1aebfcdg
171 """The DateTime type converts date strings into `datetime` objects.
173 The format strings which are checked are configurable, but default to some
174 common (non-timezone aware) ISO 8601 formats.
176 When specifying *DateTime* formats, you should only pass a list or a tuple.
177 Other iterables, like generators, may lead to surprising results.
179 The format strings are processed using ``datetime.strptime``, and this
180 consequently defines the format strings which are allowed.
182 Parsing is tried using each format, in order, and the first format which
183 parses successfully is used.
184 """
186 name = "datetime" 1aebfcdg
188 def __init__(self, formats: Sequence[str] | None = None): 1aebfcdg
189 self.formats: Sequence[str] = formats or [ 1aebfcdg
190 "%Y-%m-%d",
191 "%Y-%m-%dT%H:%M:%S",
192 "%Y-%m-%d %H:%M:%S",
193 ]
195 def get_metavar(self, param: "Parameter", ctx: "Context") -> str | None: 1aebfcdg
196 return f"<{'|'.join(self.formats)}>" 1aebfcdg
198 def _try_to_convert_date(self, value: Any, format: str) -> datetime | None: 1aebfcdg
199 try: 1aebfcdg
200 return datetime.strptime(value, format) 1aebfcdg
201 except ValueError: 1aebfcdg
202 return None 1aebfcdg
204 def convert( 1aebfcdg
205 self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None]
206 ) -> Any:
207 if isinstance(value, datetime): 1aebfcdg
208 return value 1aebfcdg
210 for format in self.formats: 1aebfcdg
211 converted = self._try_to_convert_date(value, format) 1aebfcdg
213 if converted is not None: 1aebfcdg
214 return converted 1aebfcdg
216 formats_str = ", ".join(map(repr, self.formats)) 1aebfcdg
217 self.fail( 1aebfcdg
218 f"{value!r} does not match the formats {formats_str}.",
219 param,
220 ctx,
221 )
223 def __repr__(self) -> str: 1aebfcdg
224 return "DateTime" 1aebfcdg
227class _NumberParamTypeBase(ParamType): 1aebfcdg
228 _number_class: ClassVar[type[Any]] 1abcd
230 def convert( 1aebfcdg
231 self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None]
232 ) -> Any:
233 try: 1aebfcdg
234 return self._number_class(value) 1aebfcdg
235 except ValueError: 1aebfcdg
236 self.fail( 1aebfcdg
237 f"{value!r} is not a valid {self.name}.",
238 param,
239 ctx,
240 )
243class _NumberRangeBase(_NumberParamTypeBase): 1aebfcdg
244 def __init__( 1aebfcdg
245 self,
246 min: float | None = None,
247 max: float | None = None,
248 min_open: bool = False,
249 max_open: bool = False,
250 clamp: bool = False,
251 ) -> None:
252 self.min = min 1aebfcdg
253 self.max = max 1aebfcdg
254 self.min_open = min_open 1aebfcdg
255 self.max_open = max_open 1aebfcdg
256 self.clamp = clamp 1aebfcdg
258 def convert( 1aebfcdg
259 self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None]
260 ) -> Any:
261 import operator 1aebfcdg
263 rv = super().convert(value, param, ctx) 1aebfcdg
264 lt_min: bool = self.min is not None and ( 1aebfcdg
265 operator.le if self.min_open else operator.lt
266 )(rv, self.min)
267 gt_max: bool = self.max is not None and ( 1aebfcdg
268 operator.ge if self.max_open else operator.gt
269 )(rv, self.max)
271 if self.clamp: 1aebfcdg
272 if lt_min: 1aebfcdg
273 return self._clamp(self.min, 1, self.min_open) # type: ignore[arg-type] 1aebfcdg
275 if gt_max: 1aebfcdg
276 return self._clamp(self.max, -1, self.max_open) # type: ignore[arg-type] 1aebfcdg
278 if lt_min or gt_max: 1aebfcdg
279 self.fail( 1aebfcdg
280 f"{rv} is not in the range {self._describe_range()}.",
281 param,
282 ctx,
283 )
285 return rv 1aebfcdg
287 def _clamp(self, bound: float, dir: Literal[1, -1], open: bool) -> float: 1aebfcdg
288 """Find the valid value to clamp to bound in the given
289 direction.
290 """
291 raise NotImplementedError # pragma: no cover
293 def _describe_range(self) -> str: 1aebfcdg
294 """Describe the range for use in help text."""
295 if self.min is None: 1aebfcdg
296 op = "<" if self.max_open else "<=" 1aebfcdg
297 return f"x{op}{self.max}" 1aebfcdg
299 if self.max is None: 1aebfcdg
300 op = ">" if self.min_open else ">=" 1aebfcdg
301 return f"x{op}{self.min}" 1aebfcdg
303 lop = "<" if self.min_open else "<=" 1aebfcdg
304 rop = "<" if self.max_open else "<=" 1aebfcdg
305 return f"{self.min}{lop}x{rop}{self.max}" 1aebfcdg
307 def __repr__(self) -> str: 1aebfcdg
308 clamp = " clamped" if self.clamp else "" 1aebfcdg
309 return f"<{type(self).__name__} {self._describe_range()}{clamp}>" 1aebfcdg
312class IntParamType(_NumberParamTypeBase): 1aebfcdg
313 name = "int" 1aebfcdg
314 _number_class = int 1aebfcdg
316 def __repr__(self) -> str: 1aebfcdg
317 return "INT" 1aebfcdg
320class IntRange(_NumberRangeBase, IntParamType): 1aebfcdg
321 """Restrict an `INT` value to a range of accepted values. See
323 If ``min`` or ``max`` are not passed, any value is accepted in that
324 direction. If ``min_open`` or ``max_open`` are enabled, the
325 corresponding boundary is not included in the range.
327 If ``clamp`` is enabled, a value outside the range is clamped to the
328 boundary instead of failing.
329 """
331 name = "int range" 1aebfcdg
333 def _clamp( # type: ignore 1aebfcdg
334 self, bound: int, dir: Literal[1, -1], open: bool
335 ) -> int:
336 if not open: 1aebfcdg
337 return bound 1aebfcdg
339 return bound + dir 1aebfcdg
342class FloatParamType(_NumberParamTypeBase): 1aebfcdg
343 name = "float" 1aebfcdg
344 _number_class = float 1aebfcdg
346 def __repr__(self) -> str: 1aebfcdg
347 return "FLOAT" 1aebfcdg
350class FloatRange(_NumberRangeBase, FloatParamType): 1aebfcdg
351 """Restrict a `FLOAT` value to a range of accepted
352 values. See `ranges`.
354 If ``min`` or ``max`` are not passed, any value is accepted in that
355 direction. If ``min_open`` or ``max_open`` are enabled, the
356 corresponding boundary is not included in the range.
358 If ``clamp`` is enabled, a value outside the range is clamped to the
359 boundary instead of failing. This is not supported if either
360 boundary is marked ``open``.
361 """
363 name = "float range" 1aebfcdg
365 def __init__( 1aebfcdg
366 self,
367 min: float | None = None,
368 max: float | None = None,
369 min_open: bool = False,
370 max_open: bool = False,
371 clamp: bool = False,
372 ) -> None:
373 super().__init__( 1aebfcdg
374 min=min, max=max, min_open=min_open, max_open=max_open, clamp=clamp
375 )
377 if (min_open or max_open) and clamp: 1aebfcdg
378 raise TypeError("Clamping is not supported for open bounds.") 1aebfcdg
380 def _clamp(self, bound: float, dir: Literal[1, -1], open: bool) -> float: 1aebfcdg
381 if not open: 1aebfcdg
382 return bound 1aebfcdg
384 # Could use math.nextafter here, but clamping an
385 # open float range doesn't seem to be particularly useful. It's
386 # left up to the user to write a callback to do it if needed.
387 raise RuntimeError(
388 "Clamping is not supported for open bounds."
389 ) # pragma: no cover
392class BoolParamType(ParamType): 1aebfcdg
393 name = "boolean" 1aebfcdg
395 bool_states: dict[str, bool] = { 1aebfcdg
396 "1": True,
397 "0": False,
398 "yes": True,
399 "no": False,
400 "true": True,
401 "false": False,
402 "on": True,
403 "off": False,
404 "t": True,
405 "f": False,
406 "y": True,
407 "n": False,
408 # Absence of value is considered False.
409 "": False,
410 }
411 """A mapping of string values to boolean states. 1aebfcdg
413 Mapping is inspired by `configparser.ConfigParser.BOOLEAN_STATES`
414 and extends it.
415 """
417 @staticmethod 1aebfcdg
418 def str_to_bool(value: str | bool) -> bool | None: 1aebfcdg
419 """Convert a string to a boolean value.
421 If the value is already a boolean, it is returned as-is. If the value is a
422 string, it is stripped of whitespaces and lower-cased, then checked against
423 the known boolean states pre-defined in the `BoolParamType.bool_states` mapping
424 above.
426 Returns `None` if the value does not match any known boolean state.
427 """
428 if isinstance(value, bool): 1aebfcdg
429 return value 1aebfcdg
430 return BoolParamType.bool_states.get(value.strip().lower()) 1aebfcdg
432 def convert( 1aebfcdg
433 self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None]
434 ) -> bool:
435 normalized = self.str_to_bool(value) 1aebfcdg
436 if normalized is None: 1aebfcdg
437 states = ", ".join(sorted(self.bool_states)) 1aebfcdg
438 self.fail( 1aebfcdg
439 f"{value!r} is not a valid boolean. Recognized values: {states}",
440 param,
441 ctx,
442 )
443 return normalized 1aebfcdg
445 def __repr__(self) -> str: 1aebfcdg
446 return "BOOL" 1aebfcdg
449class UUIDParameterType(ParamType): 1aebfcdg
450 name = "uuid" 1aebfcdg
452 def convert( 1aebfcdg
453 self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None]
454 ) -> Any:
455 import uuid 1aebfcdg
457 if isinstance(value, uuid.UUID): 1aebfcdg
458 return value 1aebfcdg
460 value = value.strip() 1aebfcdg
462 try: 1aebfcdg
463 return uuid.UUID(value) 1aebfcdg
464 except ValueError: 1aebfcdg
465 self.fail(f"{value!r} is not a valid UUID.", param, ctx) 1aebfcdg
467 def __repr__(self) -> str: 1aebfcdg
468 return "UUID" 1aebfcdg
471class File(ParamType): 1aebfcdg
472 """Declares a parameter to be a file for reading or writing. The file
473 is automatically closed once the context tears down (after the command
474 finished working).
476 Files can be opened for reading or writing. The special value ``-``
477 indicates stdin or stdout depending on the mode.
479 By default, the file is opened for reading text data, but it can also be
480 opened in binary mode or for writing. The encoding parameter can be used
481 to force a specific encoding.
483 The `lazy` flag controls if the file should be opened immediately or upon
484 first IO. The default is to be non-lazy for standard input and output
485 streams as well as files opened for reading, `lazy` otherwise. When opening a
486 file lazily for reading, it is still opened temporarily for validation, but
487 will not be held open until first IO. lazy is mainly useful when opening
488 for writing to avoid creating the file until it is needed.
490 Files can also be opened atomically in which case all writes go into a
491 separate file in the same folder and upon completion the file will
492 be moved over to the original location. This is useful if a file
493 regularly read by other users is modified.
494 """
496 name = "filename" 1aebfcdg
497 envvar_list_splitter: ClassVar[str] = os.path.pathsep 1aebfcdg
499 def __init__( 1aebfcdg
500 self,
501 mode: str = "r",
502 encoding: str | None = None,
503 errors: str | None = "strict",
504 lazy: bool | None = None,
505 atomic: bool = False,
506 ) -> None:
507 self.mode = mode 1aebfcdg
508 self.encoding = encoding 1aebfcdg
509 self.errors = errors 1aebfcdg
510 self.lazy = lazy 1aebfcdg
511 self.atomic = atomic 1aebfcdg
513 def resolve_lazy_flag(self, value: str | os.PathLike[str]) -> bool: 1aebfcdg
514 if self.lazy is not None: 1aebfcdg
515 return self.lazy 1aebfcdg
516 if os.fspath(value) == "-": 1aebfcdg
517 return False 1aebfcdg
518 elif "w" in self.mode: 1aebfcdg
519 return True 1aebfcdg
520 return False 1aebfcdg
522 def convert( 1aebfcdg
523 self,
524 value: str | os.PathLike[str] | IO[Any],
525 param: Union["Parameter", None],
526 ctx: Union["Context", None],
527 ) -> IO[Any]:
528 if _is_file_like(value): 1aebfcdg
529 return value 1aebfcdg
531 value = cast("str | os.PathLike[str]", value) 1aebfcdg
533 try: 1aebfcdg
534 lazy = self.resolve_lazy_flag(value) 1aebfcdg
536 if lazy: 1aebfcdg
537 lf = LazyFile( 1aebfcdg
538 value, self.mode, self.encoding, self.errors, atomic=self.atomic
539 )
541 if ctx is not None: 1aebfcdg
542 ctx.call_on_close(lf.close_intelligently) 1aebfcdg
544 return cast("IO[Any]", lf) 1aebfcdg
546 f, should_close = open_stream( 1aebfcdg
547 value, self.mode, self.encoding, self.errors, atomic=self.atomic
548 )
550 # If a context is provided, we automatically close the file
551 # at the end of the context execution (or flush out). If a
552 # context does not exist, it's the caller's responsibility to
553 # properly close the file. This for instance happens when the
554 # type is used with prompts.
555 if ctx is not None: 1aebfcdg
556 if should_close: 1aebfcdg
557 ctx.call_on_close(safecall(f.close)) 1aebfcdg
558 else:
559 ctx.call_on_close(safecall(f.flush)) 1aebfcdg
561 return f 1aebfcdg
562 except OSError as e: 1aebfcdg
563 filename = _escape_control_characters(format_filename(value)) 1aebfcdg
564 self.fail(f"'{filename}': {e.strerror}", param, ctx) 1aebfcdg
566 def shell_complete( 1aebfcdg
567 self, ctx: "Context", param: "Parameter", incomplete: str
568 ) -> list["CompletionItem"]:
569 """Return a special completion marker that tells the completion
570 system to use the shell to provide file path completions.
571 """
572 from .shell_completion import CompletionItem 1aebfcdg
574 return [CompletionItem(incomplete, type="file")] 1aebfcdg
577def _is_file_like(value: Any) -> TypeGuard[IO[Any]]: 1aebfcdg
578 return hasattr(value, "read") or hasattr(value, "write") 1aebfcdg
581class Tuple(CompositeParamType): 1aebfcdg
582 """The default behavior of Click is to apply a type on a value directly.
583 This works well in most cases, except for when `nargs` is set to a fixed
584 count and different types should be used for different items. In this
585 case the `Tuple` type can be used. This type can only be used
586 if `nargs` is set to a fixed number.
588 For more information see `tuple-type`.
590 This can be selected by using a Python tuple literal as a type.
591 """
593 def __init__(self, types: Sequence[type[Any] | ParamType]) -> None: 1aebfcdg
594 self.types: Sequence[ParamType] = [convert_type(ty) for ty in types] 1aebfcdg
596 @property 1aebfcdg
597 def name(self) -> str: # type: ignore[override] 1aebfcdg
598 return f"<{' '.join(ty.name for ty in self.types)}>" 1aebfcdg
600 @property 1aebfcdg
601 def arity(self) -> int: # type: ignore 1aebfcdg
602 return len(self.types) 1aebfcdg
604 def convert( 1aebfcdg
605 self, value: Any, param: Union["Parameter", None], ctx: Union["Context", None]
606 ) -> Any:
607 len_type = len(self.types) 1aebfcdg
608 len_value = len(value) 1aebfcdg
610 if len_value != len_type: 1aebfcdg
611 self.fail( 1aebfcdg
612 f"{len_type} values are required, but {len_value} given.",
613 param=param,
614 ctx=ctx,
615 )
617 return tuple( 1aebfcdg
618 ty(x, param, ctx) for ty, x in zip(self.types, value, strict=False)
619 )
622def convert_type(ty: Any | None, default: Any | None = None) -> ParamType: 1aebfcdg
623 """Find the most appropriate `ParamType` for the given Python
624 type. If the type isn't provided, it can be inferred from a default
625 value.
626 """
627 guessed_type = False 1aebfcdg
629 if ty is None and default is not None: 1aebfcdg
630 if isinstance(default, (tuple, list)): 1aebfcdg
631 # If the default is empty, ty will remain None and will
632 # return STRING.
633 if default: 1aebfcdg
634 item = default[0] 1aebfcdg
636 # A tuple of tuples needs to detect the inner types.
637 # Can't call convert recursively because that would
638 # incorrectly unwind the tuple to a single type.
639 if isinstance(item, (tuple, list)): 1aebfcdg
640 ty = tuple(map(type, item)) 1aebfcdg
641 else:
642 ty = type(item) 1aebfcdg
643 else:
644 ty = type(default) 1aebfcdg
646 guessed_type = True 1aebfcdg
648 if isinstance(ty, tuple): 1aebfcdg
649 return Tuple(ty) 1aebfcdg
651 if isinstance(ty, ParamType): 1aebfcdg
652 return ty 1aebfcdg
654 if ty is str or ty is None: 1aebfcdg
655 return STRING 1aebfcdg
657 if ty is int: 1aebfcdg
658 return INT 1aebfcdg
660 if ty is float: 1aebfcdg
661 return FLOAT 1aebfcdg
663 if ty is bool: 1aebfcdg
664 return BOOL 1aebfcdg
666 if guessed_type: 1aebfcdg
667 return STRING 1aebfcdg
669 return FuncParamType(ty) 1aebfcdg
672# A unicode string parameter type which is the implicit default. This
673# can also be selected by using ``str`` as type.
674STRING = StringParamType() 1aebfcdg
676# An integer parameter. This can also be selected by using ``int`` as
677# type.
678INT = IntParamType() 1aebfcdg
680# A floating point value parameter. This can also be selected by using
681# ``float`` as type.
682FLOAT = FloatParamType() 1aebfcdg
684# A boolean parameter. This is the default for boolean flags. This can
685# also be selected by using ``bool`` as a type.
686BOOL = BoolParamType() 1aebfcdg
688# A UUID parameter.
689UUID = UUIDParameterType() 1aebfcdg
692class OptionHelpExtra(TypedDict, total=False): 1aebfcdg
693 envvars: tuple[str, ...] 1abcd
694 default: str 1abcd
695 range: str 1abcd
696 required: str 1abcd