1111 Callable ,
1212 Generic ,
1313 Iterable ,
14- List ,
1514 Literal ,
1615 Sequence ,
1716 Set ,
3231 Doc ,
3332 DocType ,
3433 T ,
35- assert_type ,
3634 detect_choices ,
3735 find_annotations ,
3836)
@@ -388,7 +386,7 @@ def __hash__(self):
388386 short : bool | str | list [str ] | None = False
389387 long : bool | str | list [str ] | None = False
390388 count : bool = False
391- default : T | EmptyType | None = Empty
389+ default : Default | T | EmptyType | None = Empty
392390 help : str | None = None
393391 parse : Callable [..., T ] | Sequence [Callable [..., Any ]] | None = None
394392 parse_inference : bool = True
@@ -420,7 +418,7 @@ def collect(
420418 default_short : bool = False ,
421419 default_long : bool = False ,
422420 state : State [Any ] | None = None ,
423- ) -> list [Arg [Any ]]:
421+ ) -> list [FinalArg [Any ]]:
424422 args : list [Arg [Any ]] = find_annotations (type_view , cls ) or [Arg ()]
425423
426424 exclusive = field .name if len (args ) > 1 else False
@@ -438,7 +436,7 @@ def collect(
438436 if field_metadata :
439437 args = field_metadata
440438
441- result : list [Arg [Any ]] = []
439+ result : list [FinalArg [Any ]] = []
442440 for i , arg in enumerate (args , start = 1 ):
443441 # field_name and default are evaluated outside `normalize` because they can
444442 # potentially depend on the type's dataclass-like field information, whereas
@@ -480,7 +478,7 @@ def normalize(
480478 state : State [Any ] | None = None ,
481479 destructure : Destructure | bool | None = None ,
482480 show_default : bool | str | DefaultFormatter | None = None ,
483- ) -> Arg [Any ]:
481+ ) -> FinalArg [Any ]:
484482 if type_view is None :
485483 type_view = TypeView (Any )
486484
@@ -517,28 +515,36 @@ def normalize(
517515 )
518516
519517 destructure = destructure or self .destructure
518+ if not destructure :
519+ destructure = None
520520 if destructure is True :
521521 destructure = Destructure ()
522522
523- result = dataclasses .replace (
524- self ,
525- default = default ,
526- field_name = field_name ,
523+ result : FinalArg [Any ] = FinalArg (
524+ # preserved from self
525+ count = self .count ,
526+ hidden = self .hidden ,
527+ deprecated = self .deprecated ,
528+ propagate = self .propagate ,
529+ parse_inference = self .parse_inference ,
530+ # computed/narrowed
527531 value_name = value_name ,
528- required = required ,
529532 short = short ,
530533 long = long ,
531- choices = choices ,
534+ default = default ,
535+ help = help ,
536+ parse = parse ,
537+ group = group ,
532538 action = action ,
533539 num_args = num_args ,
534- parse = parse ,
535- help = help ,
540+ choices = choices ,
536541 completion = completion ,
537- group = group ,
538- has_value = has_value ,
539- type_view = type_view ,
542+ required = required ,
543+ field_name = field_name ,
540544 show_default = default_formatter ,
541545 destructure = destructure ,
546+ has_value = has_value ,
547+ type_view = type_view ,
542548 )
543549 verify_type_compatibility (
544550 result , field_name , type_view , has_custom_parse = bool (self .parse )
@@ -550,27 +556,46 @@ def destructured(cls):
550556 """Mark a an argument as destructured. See also the shorter `Destructured` alias."""
551557 return cls (destructure = Destructure ())
552558
559+
560+ @dataclasses .dataclass (frozen = True )
561+ class FinalArg (Arg [T ]):
562+ """Post-normalization form of :class:`Arg` with narrowed field types.
563+
564+ Produced exclusively by :meth:`Arg.normalize`. After normalization all
565+ previously-optional/Empty fields are guaranteed to be concrete values.
566+ """
567+
568+ value_name : str = ""
569+ short : list [str ] | Literal [False ] = False
570+ long : list [str ] | Literal [False ] = False
571+ default : Default = dataclasses .field (default_factory = Default )
572+ group : Group = dataclasses .field (default_factory = Group )
573+ action : ArgActionType = ArgAction .set
574+ num_args : NumArgs = dataclasses .field (default_factory = NumArgs )
575+ required : bool = False
576+ field_name : str = ""
577+ has_value : bool = True
578+ type_view : TypeView [Any ] = dataclasses .field (default_factory = lambda : TypeView (Any ))
579+ parse : Callable [..., Any ] = dataclasses .field (default = parse_value )
580+ show_default : DefaultFormatter = dataclasses .field (default_factory = DefaultFormatter )
581+ destructure : Destructure | None = None
582+
553583 def names (self , * , n : int = 0 ) -> list [str ]:
554- short_names = cast (List [str ], self .short or [])
555- long_names = cast (List [str ], self .long or [])
556- result = short_names + long_names
557- if n :
558- return result [:n ]
559- return result
584+ result = (self .short or []) + (self .long or [])
585+ return result [:n ] if n else result
560586
561587 def names_str (self , delimiter : str = ", " , * , n : int = 0 ) -> str :
562- if self .long or self .short :
588+ if self .short or self .long :
563589 return delimiter .join (self .names (n = n ))
564-
565- return cast (str , self .value_name )
590+ return self .value_name
566591
567592 @cached_property
568593 def is_option (self ) -> bool :
569594 return bool (self .short or self .long )
570595
571596
572597def verify_type_compatibility (
573- arg : Arg [Any ],
598+ arg : FinalArg [Any ],
574599 field_name : str ,
575600 type_view : TypeView [Any ],
576601 * ,
@@ -591,7 +616,7 @@ def verify_type_compatibility(
591616 if has_custom_parse or ArgAction .is_custom (action ):
592617 return
593618
594- num_args = assert_type ( arg .num_args , NumArgs )
619+ num_args = arg .num_args
595620 if not num_args .required and num_args .default is None and not type_view .is_optional :
596621 raise ValueError (
597622 f"On field '{ field_name } ', `NumArgs(required=False, default=None)` requires the "
@@ -852,7 +877,9 @@ def infer_value_name(arg: Arg[Any], field_name: str, num_args: NumArgs) -> str:
852877 return field_name
853878
854879
855- def explode_negated_bool_args (args : Sequence [Arg [Any ]]) -> Iterable [Arg [Any ]]:
880+ def explode_negated_bool_args (
881+ args : Sequence [FinalArg [Any ]],
882+ ) -> Iterable [FinalArg [Any ]]:
856883 """Expand `--foo/--no-foo` solo arguments into dual-arguments.
857884
858885 Acts as a transform from `Arg(long='--foo/--no-foo')` to
@@ -861,7 +888,7 @@ def explode_negated_bool_args(args: Sequence[Arg[Any]]) -> Iterable[Arg[Any]]:
861888 for arg in args :
862889 yielded = False
863890 if isinstance (arg .action , ArgAction ) and arg .action .is_bool_action and arg .long :
864- long = cast ( List [ str ], arg .long )
891+ long = arg .long
865892
866893 negatives = [item for item in long if "--no-" in item ]
867894 positives = [item for item in long if "--no-" not in item ]
@@ -874,9 +901,9 @@ def explode_negated_bool_args(args: Sequence[Arg[Any]]) -> Iterable[Arg[Any]]:
874901 default_is_false = arg .default .fallback_value is False and not_required
875902 disabled : DefaultFormatter = DefaultFormatter .disabled ()
876903 group = dataclasses .replace (
877- cast ( Group , arg .group ) ,
904+ arg .group ,
878905 exclusive = True ,
879- id = cast ( str , arg .field_name ) ,
906+ id = arg .field_name ,
880907 )
881908
882909 positive_arg = dataclasses .replace (
0 commit comments