33they follow a similar API for instantiation and usage.
44'''
55import abc
6- from typing import Dict , Any
6+ from argparse import ArgumentParser
7+ from typing import Dict , Any , Tuple , NamedTuple
8+
9+ from .util .data import traverse_config_set , traverse_config_get
710
811from .util .entrypoint import Entrypoint
912
1013from .log import LOGGER
1114
15+ class MissingArg (Exception ):
16+ '''
17+ Raised when a BaseConfigurable is missing an argument from the args dict it
18+ created with args(). If this exception is raised then the config() method is
19+ attempting to retrive an argument which was not set in the args() method.
20+ '''
21+
22+ class MissingConfig (Exception ):
23+ '''
24+ Raised when a BaseConfigurable is missing an argument from the config dict.
25+ Also raised if there was no default value set and the argument is missing.
26+ '''
27+
28+ class MissingRequiredProperty (Exception ):
29+ '''
30+ Raised when a BaseDataFlowFacilitatorObject is missing some property which
31+ should have been defined in the class.
32+ '''
33+
1234class LoggingLogger (object ):
1335 '''
1436 Provide the logger property using Python's builtin logging module.
@@ -28,7 +50,11 @@ class BaseConfig(object):
2850 All DFFML Base Objects should take an object (likely a typing.NamedTuple) as
2951 as their config.
3052 '''
31- pass
53+
54+ class ConfigurableParsingNamespace (object ):
55+
56+ def __init__ (self ):
57+ self .dest = None
3258
3359class BaseConfigurable (abc .ABC ):
3460 '''
@@ -38,6 +64,8 @@ class BaseConfigurable(abc.ABC):
3864 only parameter to the __init__ of a BaseDataFlowFacilitatorObject.
3965 '''
4066
67+ __argp = ArgumentParser ()
68+
4169 def __init__ (self , config : BaseConfig ) -> None :
4270 '''
4371 BaseConfigurable takes only one argument to __init__,
@@ -47,19 +75,93 @@ def __init__(self, config: BaseConfig) -> None:
4775 '''
4876 self .config = config
4977
78+ @classmethod
79+ def add_orig_label (cls , * above ):
80+ return (
81+ list (above ) + cls .ENTRY_POINT_NAME + [cls .ENTRY_POINT_ORIG_LABEL ]
82+ )
83+
84+ @classmethod
85+ def add_label (cls , * above ):
86+ return (
87+ list (above ) + cls .ENTRY_POINT_NAME + [cls .ENTRY_POINT_LABEL ]
88+ )
89+
90+ @classmethod
91+ def config_set (cls , args , above , * path ) -> BaseConfig :
92+ return traverse_config_set (args ,
93+ * (cls .add_orig_label (* above ) + list (path )))
94+
95+ @classmethod
96+ def config_get (cls , config , above , * path ) -> BaseConfig :
97+ # unittest.mock.patch doesn't work if we cache args() output.
98+ args = cls .args ({})
99+ args_above = cls .add_orig_label () + list (path )
100+ label_above = cls .add_label (* above ) + list (path )
101+ no_label_above = cls .add_label (* above )[:- 1 ] + list (path )
102+ try :
103+ arg = traverse_config_get (args , * args_above )
104+ except KeyError as error :
105+ raise MissingArg ('Arg %r missing from %s%s%s' % \
106+ (args_above [- 1 ],
107+ cls .__qualname__ ,
108+ '.' if args_above [:- 1 ] else '' ,
109+ '.' .join (args_above [:- 1 ]),)) from error
110+ try :
111+ value = traverse_config_get (config , * label_above )
112+ except KeyError as error :
113+ try :
114+ value = traverse_config_get (config , * no_label_above )
115+ except KeyError as error :
116+ if 'default' in arg :
117+ return arg ['default' ]
118+ raise MissingConfig ('%s missing %r from %s' % \
119+ (cls .__qualname__ ,
120+ label_above [- 1 ],
121+ '.' .join (label_above [:- 1 ]),)) from error
122+
123+ if value is None \
124+ and 'default' in arg :
125+ return arg ['default' ]
126+ # TODO This is a oversimplification of argparse's nargs
127+ if not 'nargs' in arg :
128+ value = value [0 ]
129+ if 'type' in arg :
130+ # TODO This is a oversimplification of argparse's nargs
131+ if 'nargs' in arg :
132+ value = list (map (arg ['type' ], value ))
133+ else :
134+ value = arg ['type' ](value )
135+ if 'action' in arg :
136+ if isinstance (arg ['action' ], str ):
137+ # HACK This accesses _pop_action_class from ArgumentParser
138+ # which is prefaced with an underscore indicating it not an API
139+ # we can rely on
140+ arg ['action' ] = cls .__argp ._pop_action_class (arg )
141+ namespace = ConfigurableParsingNamespace ()
142+ action = arg ['action' ](dest = 'dest' , option_strings = '' )
143+ action (None , namespace , value )
144+ value = namespace .dest
145+ return value
146+
50147 @classmethod
51148 @abc .abstractmethod
52- def args (cls ) -> Dict [str , Any ]:
53- pass
149+ def args (cls , * above ) -> Dict [str , Any ]:
150+ '''
151+ Return a dict containing arguments required for this class
152+ '''
54153
55154 @classmethod
56155 @abc .abstractmethod
57- def config (cls , cmd ):
58- pass
156+ def config (cls , config , * above ):
157+ '''
158+ Create the BaseConfig required to instantiate this class by parsing the
159+ config dict.
160+ '''
59161
60162 @classmethod
61- def withconfig (cls , cmd ):
62- return cls (cls .config (cmd ))
163+ def withconfig (cls , config , * above ):
164+ return cls (cls .config (config , * above ))
63165
64166class BaseDataFlowFacilitatorObjectContext (LoggingLogger ):
65167 '''
@@ -110,14 +212,17 @@ def __init__(self, config: BaseConfig) -> None:
110212 self .__ensure_property ('CONTEXT' )
111213 self .__ensure_property ('ENTRY_POINT' )
112214
215+ def __repr__ (self ):
216+ return '%s(%r)' % (self .__class__ .__qualname__ , self .config )
217+
113218 @abc .abstractmethod
114219 def __call__ (self ) -> 'BaseDataFlowFacilitatorObjectContext' :
115220 pass
116221
117222 @classmethod
118223 def __ensure_property (cls , property_name ):
119224 if getattr (cls , property_name , None ) is None :
120- raise ValueError ( 'BaseDataFlowFacilitatorObject \' s may not be ' + \
121- 'created without a `%s`. ' % ( property_name ,) + \
122- ' Missing %s.%s' \
123- % (cls .__qualname__ , property_name ,))
225+ raise MissingRequiredProperty (
226+ 'BaseDataFlowFacilitatorObjects may not be '
227+ 'created without a `%s`. Missing %s.%s' \
228+ % (property_name , cls .__qualname__ , property_name ,))
0 commit comments