@@ -29,6 +29,9 @@ def get_args(t):
2929from .log import LOGGER
3030
3131
32+ ARGP = ArgumentParser ()
33+
34+
3235class ParseExpandAction (argparse .Action ):
3336 def __call__ (self , parser , namespace , values , option_string = None ):
3437 if not isinstance (values , list ):
@@ -106,11 +109,92 @@ def __str__(self):
106109 return repr (self )
107110
108111
112+ def mkarg (field ):
113+ arg = Arg (type = field .type )
114+ # HACK For detecting dataclasses._MISSING_TYPE
115+ if "dataclasses._MISSING_TYPE" not in repr (field .default ):
116+ arg ["default" ] = field .default
117+ if field .type == bool :
118+ arg ["action" ] = "store_true"
119+ elif inspect .isclass (field .type ):
120+ if issubclass (field .type , list ):
121+ arg ["nargs" ] = "+"
122+ if not hasattr (field .type , "SINGLETON" ):
123+ raise AttributeError (
124+ f"{ field .type .__qualname__ } missing attribute SINGLETON"
125+ )
126+ arg ["action" ] = list_action (field .type )
127+ arg ["type" ] = field .type .SINGLETON
128+ if hasattr (arg ["type" ], "load" ):
129+ # TODO (python3.8) Use Protocol
130+ arg ["type" ] = arg ["type" ].load
131+ elif get_origin (field .type ) is list :
132+ arg ["type" ] = get_args (field .type )[0 ]
133+ arg ["nargs" ] = "+"
134+ if "help" in field .metadata :
135+ arg ["help" ] = field .metadata ["help" ]
136+ return arg
137+
138+
139+ def convert_value (arg , value ):
140+ if value is None :
141+ # Return default if not found and available
142+ if "default" in arg :
143+ return arg ["default" ]
144+ raise MissingConfig
145+
146+ # TODO This is a oversimplification of argparse's nargs
147+ if not "nargs" in arg :
148+ value = value [0 ]
149+ if "type" in arg :
150+ # TODO This is a oversimplification of argparse's nargs
151+ if "nargs" in arg :
152+ value = list (map (arg ["type" ], value ))
153+ else :
154+ value = arg ["type" ](value )
155+ if "action" in arg :
156+ if isinstance (arg ["action" ], str ):
157+ # HACK This accesses _pop_action_class from ArgumentParser
158+ # which is prefaced with an underscore indicating it not an API
159+ # we can rely on
160+ arg ["action" ] = ARGP ._pop_action_class (arg )
161+ namespace = ConfigurableParsingNamespace ()
162+ action = arg ["action" ](dest = "dest" , option_strings = "" )
163+ action (None , namespace , value )
164+ value = namespace .dest
165+ return value
166+
167+
168+ def is_config_dict (value ):
169+ return bool (
170+ "arg" in value
171+ and "config" in value
172+ and isinstance (value ["config" ], dict )
173+ )
174+
175+
176+ def _fromdict (cls , ** kwargs ):
177+ for field in dataclasses .fields (cls ):
178+ if field .name in kwargs :
179+ value = kwargs [field .name ]
180+ config = {}
181+ if is_config_dict (value ):
182+ value , config = value ["arg" ], value ["config" ]
183+ value = convert_value (mkarg (field ), value )
184+ if inspect .isclass (value ) and issubclass (value , BaseConfigurable ):
185+ value = value .withconfig (
186+ {field .name : {"arg" : None , "config" : config }}
187+ )
188+ kwargs [field .name ] = value
189+ return cls (** kwargs )
190+
191+
109192def config (cls ):
110193 """
111194 Decorator to create a dataclass
112195 """
113196 datacls = dataclasses .dataclass (eq = True , frozen = True )(cls )
197+ datacls ._fromdict = classmethod (_fromdict )
114198 datacls ._replace = lambda self , * args , ** kwargs : dataclasses .replace (
115199 self , * args , ** kwargs
116200 )
@@ -130,8 +214,6 @@ class BaseConfigurable(abc.ABC):
130214 only parameter to the __init__ of a BaseDataFlowFacilitatorObject.
131215 """
132216
133- __argp = ArgumentParser ()
134-
135217 def __init__ (self , config : BaseConfig ) -> None :
136218 """
137219 BaseConfigurable takes only one argument to __init__,
@@ -225,41 +307,20 @@ def config_get(cls, config, above, *path) -> BaseConfig:
225307 with contextlib .suppress (KeyError ):
226308 value = traverse_config_get (config , * no_label_above )
227309
228- if value is None :
229- # Return default if not found and available
230- if "default" in arg :
231- return arg ["default" ]
232- raise MissingConfig (
233- "%s missing %r from %s"
234- % (
235- cls .__qualname__ ,
236- label_above [- 1 ],
237- "." .join (label_above [:- 1 ]),
238- )
310+ try :
311+ return convert_value (arg , value )
312+ except MissingConfig as error :
313+ error .args = (
314+ (
315+ "%s missing %r from %s"
316+ % (
317+ cls .__qualname__ ,
318+ label_above [- 1 ],
319+ "." .join (label_above [:- 1 ]),
320+ )
321+ ),
239322 )
240-
241- if value is None and "default" in arg :
242- return arg ["default" ]
243- # TODO This is a oversimplification of argparse's nargs
244- if not "nargs" in arg :
245- value = value [0 ]
246- if "type" in arg :
247- # TODO This is a oversimplification of argparse's nargs
248- if "nargs" in arg :
249- value = list (map (arg ["type" ], value ))
250- else :
251- value = arg ["type" ](value )
252- if "action" in arg :
253- if isinstance (arg ["action" ], str ):
254- # HACK This accesses _pop_action_class from ArgumentParser
255- # which is prefaced with an underscore indicating it not an API
256- # we can rely on
257- arg ["action" ] = cls .__argp ._pop_action_class (arg )
258- namespace = ConfigurableParsingNamespace ()
259- action = arg ["action" ](dest = "dest" , option_strings = "" )
260- action (None , namespace , value )
261- value = namespace .dest
262- return value
323+ raise
263324
264325 @classmethod
265326 def args (cls , args , * above ) -> Dict [str , Arg ]:
@@ -271,30 +332,7 @@ def args(cls, args, *above) -> Dict[str, Arg]:
271332 f"{ cls .__qualname__ } requires CONFIG property or implementation of args() classmethod"
272333 )
273334 for field in dataclasses .fields (cls .CONFIG ):
274- arg = Arg (type = field .type )
275- # HACK For detecting dataclasses._MISSING_TYPE
276- if "dataclasses._MISSING_TYPE" not in repr (field .default ):
277- arg ["default" ] = field .default
278- if field .type == bool :
279- arg ["action" ] = "store_true"
280- elif inspect .isclass (field .type ):
281- if issubclass (field .type , list ):
282- arg ["nargs" ] = "+"
283- if not hasattr (field .type , "SINGLETON" ):
284- raise AttributeError (
285- f"{ field .type .__qualname__ } missing attribute SINGLETON"
286- )
287- arg ["action" ] = list_action (field .type )
288- arg ["type" ] = field .type .SINGLETON
289- if hasattr (arg ["type" ], "load" ):
290- # TODO (python3.8) Use Protocol
291- arg ["type" ] = arg ["type" ].load
292- elif get_origin (field .type ) is list :
293- arg ["type" ] = get_args (field .type )[0 ]
294- arg ["nargs" ] = "+"
295- if "help" in field .metadata :
296- arg ["help" ] = field .metadata ["help" ]
297- cls .config_set (args , above , field .name , arg )
335+ cls .config_set (args , above , field .name , mkarg (field ))
298336 return args
299337
300338 @classmethod
0 commit comments