88 _StoreFalseAction ,
99 _StoreTrueAction ,
1010 _SubParsersAction ,
11+ _VersionAction ,
1112 Action ,
1213 ArgumentParser ,
1314)
1415from collections import defaultdict
15- from dataclasses import Field , dataclass , field , make_dataclass
16+ from dataclasses import MISSING , Field , dataclass , field , make_dataclass
1617from functools import cached_property
1718import re
18- from typing import Callable
19+ import sys
20+ from typing import Annotated , Callable , Literal , Optional
1921from warnings import warn
2022
23+ from tyro .conf import OmitSubcommandPrefixes
24+
25+ from .. import Options
26+
2127from .form_dict import DataClass
22- from ..tag import Tag
2328
2429
2530try :
26- from tyro .conf import Positional
31+ from tyro .constructors import PrimitiveConstructorSpec
32+ from tyro .conf import Positional , arg
2733except ImportError :
2834 from ..exceptions import DependencyRequired
2935
@@ -75,14 +81,8 @@ def has_property(self):
7581
7682
7783def parser_to_dataclass (parser : ArgumentParser , name : str = "Args" ) -> DataClass | list [DataClass ]:
78- """Note that in contrast to the argparse, we create default values.
79- When an optional flag is not used, argparse put None, we have a default value.
80-
81- This does make sense for most values and should not pose problems for truthy-values.
82- Ex. checking `if namespace.my_int` still returns False for both argparse-None and our-0.
83-
84- Be aware that for Path this might pose a big difference:
85- parser.add_argument("--path", type=Path) -> becomes Path('.'), not None!
84+ """
85+ Note: Ex. parser.add_argument("--time", type=time) -> does work at all in argparse, here it works.
8686 """
8787 subparsers : list [_SubParsersAction ] = []
8888
@@ -144,6 +144,22 @@ def _make_dataclass_from_actions(
144144 match action :
145145 case _HelpAction ():
146146 continue
147+ case _VersionAction ():
148+ # NOTE Should be probably implemented in tyro. Here that way:
149+ # run(add_version="1.2.3")
150+ # run(add_version_package="intelmq") -> get pip version
151+ arg_type = Annotated [
152+ None ,
153+ PrimitiveConstructorSpec (
154+ nargs = "*" ,
155+ metavar = "" ,
156+ instance_from_str = lambda _ , v = action .version : print (v ) or sys .exit (0 ),
157+ is_instance = lambda _ : True ,
158+ # NOTE tyro might not diplay anything here,
159+ # but it displays `(default: )`
160+ str_from_instance = (lambda _ , v = action .version : [str (v )]),
161+ ),
162+ ]
147163 case _SubParsersAction ():
148164 # Note that there is only one _SubParsersAction in argparse
149165 # but to be sure, we allow multiple of them
@@ -156,6 +172,14 @@ def _make_dataclass_from_actions(
156172 subparser .description ,
157173 )
158174 subparser_fields .append ((subname , sub_dc )) # required, no default
175+
176+ from functools import reduce
177+
178+ union_type = reduce (lambda a , b : a | b , [aa [1 ] for aa in subparser_fields ])
179+
180+ result = OmitSubcommandPrefixes [Positional [union_type ]]
181+ pos_fields .append (("_subparsers" , result ))
182+ subparser_fields .clear ()
159183 continue
160184 case _AppendAction ():
161185 arg_type = list [action .type or str ]
@@ -185,36 +209,69 @@ def _make_dataclass_from_actions(
185209 case _CountAction ():
186210 arg_type = int
187211 case _:
188- arg_type = action .type or str
212+ if action .type :
213+ arg_type = action .type
214+ elif action .default :
215+ arg_type = type (action .default )
216+ else :
217+ arg_type = str
218+
219+ metavar = None
220+ if "default" not in opt and "default_factory" not in opt :
221+ if action .choices :
222+ # With the drop of Python 3.10, use:
223+ # arg_type = Literal[*action.choices]
224+ arg_type = Annotated [arg_type , Options (* action .choices )]
225+
226+ if not action .option_strings and action .default is None and action .nargs != "?" :
227+ opt ["default" ] = MISSING
228+ else :
189229 if action .default is None :
190- # parser.add_argument("--path", type=Path) -> becomes Path('.'), not None!
230+ # parser.add_argument("--path", type=Path) -> becomes None, not Path('.').
191231 # By default, argparse put None if not used in the CLI.
192232 # Which makes tyro output the warning: annotated with type `<class 'str'>`, but the default value `None`
193233 # We either make None an option by `arg_type |= None`
194234 # or else we default the value.
195- # This creates a slightly different behaviour, however, the behaviour is slightly different
196- # nevertheless.
197- # Ex. parser.add_argument("--time", type=time) -> does work poorly in argparse.
198- action .default = Tag (annotation = arg_type )._make_default_value ()
199- if "default" not in opt and "default_factory" not in opt :
200- opt ["default" ] = action .default if action .default != SUPPRESS else None
235+ if arg_type is not None :
236+ arg_type |= None
237+ opt ["default" ] = action .default if action .default != SUPPRESS else None
201238
202239 # build a dataclass field, either optional, or positional
203- met = { "metadata" : {"help" : action .help } }
240+ opt [ "metadata" ] = {"help" : action .help }
204241 if action .option_strings :
205242 # normal_fields.append((action.dest, arg_type, field(**opt, **met)))
206- normal_fields .append ((af .name , arg_type , field (** opt , ** met )))
243+ # Annotated[arg_type, arg(metavar=metavar)]
244+ normal_fields .append ((af .name , arg_type , field (** opt )))
207245
208246 # Generate back-compatible property if dest != field_name
209247 if af .name != action .dest and not af .has_property :
210248 af .add (lambda self , field_name = af .name : getattr (self , field_name ))
211-
212249 else :
213- pos_fields .append ((action .dest , Positional [arg_type ], field (** met )))
250+ pos_fields .append ((action .dest , Positional [arg_type ], field (** opt )))
251+
252+ # Subparser can have the same field name as the parser. We use the latter.
253+ # Ex:
254+ # parser.add_argument('--level', type=int, default=1)
255+ # subparsers = parser.add_subparsers(dest='command')
256+ # run_parser = subparsers.add_parser('run')
257+ # run_parser.add_argument('--level', type=int, default=5)
258+ uniq_fields = []
259+ seen = set ()
260+ # for f in reversed(subparser_fields + pos_fields + normal_fields):
261+ for f in reversed (pos_fields + normal_fields ):
262+ if f [0 ] not in seen :
263+ seen .add (f [0 ])
264+ uniq_fields .append (f )
265+
266+ # if subparser_fields:
267+ # from functools import reduce
268+ # union_type = reduce(lambda a, b: a | b, [aa[1] for aa in subparser_fields])
269+ # result = OmitSubcommandPrefixes[Positional[union_type]]
270+ # uniq_fields.append(("_subparsers", result ))
214271
215272 dc = make_dataclass (
216273 name ,
217- subparser_fields + pos_fields + normal_fields ,
274+ reversed ( uniq_fields ) ,
218275 namespace = {k : prop .generate_property () for k , prop in properties .items ()},
219276 )
220277 if helptext or description :
0 commit comments