55from datetime import datetime
66from distutils .util import strtobool
77from enum import Enum
8- from typing import Any , Callable , Dict , List , Optional , Protocol
8+ from typing import Any , Callable , Dict , List , Optional , Protocol , Tuple
99
1010from aiohttp import web
1111
12- from .application_keys import APP_CONFIG_KEY
12+ from .application_keys import APP_CONFIG_KEY , APP_SETTINGS_KEY
1313
1414log = logging .getLogger (__name__ )
1515
16- APP_SETUP_KEY = f"{ __name__ } .setup"
16+ APP_SETUP_COMPLETED_KEY = f"{ __name__ } .setup"
1717
1818
1919class _SetupFunc (Protocol ):
@@ -23,44 +23,101 @@ def __call__(self, app: web.Application, *args: Any, **kwds: Any) -> bool:
2323 ...
2424
2525
26+ class _ApplicationSettings (Protocol ):
27+ def is_enabled (self , field_name : str ) -> bool :
28+ ...
29+
30+
2631class ModuleCategory (Enum ):
2732 SYSTEM = 0
2833 ADDON = 1
2934
3035
36+ # ERRORS ------------------------------------------------------------------
37+
38+
3139class SkipModuleSetup (Exception ):
3240 def __init__ (self , * , reason ) -> None :
3341 self .reason = reason
3442 super ().__init__ (reason )
3543
3644
3745class ApplicationSetupError (Exception ):
38- pass
46+ ...
3947
4048
4149class DependencyError (ApplicationSetupError ):
42- pass
50+ ...
51+
52+
53+ # HELPERS ------------------------------------------------------------------
54+
55+
56+ def _is_addon_enabled_from_config (
57+ cfg : Dict [str , Any ], dotted_section : str , section
58+ ) -> bool :
59+ try :
60+ parts : List [str ] = dotted_section .split ("." )
61+ # navigates app_config (cfg) searching for section
62+ searched_config = deepcopy (cfg )
63+ for part in parts :
64+ if section and part == "enabled" :
65+ # if section exists, no need to explicitly enable it
66+ return strtobool (f"{ searched_config .get (part , True )} " )
67+ searched_config = searched_config [part ]
4368
69+ except KeyError as ee :
70+ raise ApplicationSetupError (
71+ f"Cannot find required option '{ dotted_section } ' in app config's section '{ ee } '"
72+ ) from ee
73+ else :
74+ assert isinstance (searched_config , bool ) # nosec
75+ return searched_config
76+
77+
78+ def _get_app_settings_and_field_name (
79+ app : web .Application ,
80+ arg_module_name : str ,
81+ arg_settings_name : Optional [str ],
82+ setup_func_name : str ,
83+ logger : logging .Logger ,
84+ ) -> Tuple [Optional [_ApplicationSettings ], Optional [str ]]:
85+
86+ app_settings : Optional [_ApplicationSettings ] = app .get (APP_SETTINGS_KEY )
87+ settings_field_name = arg_settings_name
88+
89+ if app_settings :
90+
91+ if not settings_field_name :
92+ # FIXME: hard-coded WEBSERVER_ temporary
93+ settings_field_name = f"WEBSERVER_{ arg_module_name .split ('.' )[- 1 ].upper ()} "
94+
95+ logger .debug ("Checking addon's %s " , f"{ settings_field_name = } " )
96+
97+ if not hasattr (app_settings , settings_field_name ):
98+ raise ValueError (
99+ f"Invalid option { arg_settings_name = } in module's setup { setup_func_name } . "
100+ f"It must be a field in { app_settings .__class__ } "
101+ )
44102
45- def _is_app_module_enabled (cfg : Dict , parts : List [str ], section ) -> bool :
46- # navigates app_config (cfg) searching for section
47- searched_config = deepcopy (cfg )
48- for part in parts :
49- if section and part == "enabled" :
50- # if section exists, no need to explicitly enable it
51- return strtobool (f"{ searched_config .get (part , True )} " )
52- searched_config = searched_config [part ]
53- assert isinstance (searched_config , bool ) # nosec
54- return searched_config
103+ return app_settings , settings_field_name
104+
105+
106+ # PUBLIC API ------------------------------------------------------------------
107+
108+
109+ def is_setup_completed (module_name : str , app : web .Application ) -> bool :
110+ return module_name in app [APP_SETUP_COMPLETED_KEY ]
55111
56112
57113def app_module_setup (
58114 module_name : str ,
59115 category : ModuleCategory ,
60116 * ,
61117 depends : Optional [List [str ]] = None ,
62- config_section : str = None ,
63- config_enabled : str = None ,
118+ config_section : Optional [str ] = None ,
119+ config_enabled : Optional [str ] = None ,
120+ settings_name : Optional [str ] = None ,
64121 logger : logging .Logger = log ,
65122) -> Callable :
66123 """Decorator that marks a function as 'a setup function' for a given module in an application
@@ -77,6 +134,7 @@ def app_module_setup(
77134 :param depends: list of module_names that must be called first, defaults to None
78135 :param config_section: explicit configuration section, defaults to None (i.e. the name of the module, or last entry of the name if dotted)
79136 :param config_enabled: option in config to enable, defaults to None which is '$(module-section).enabled' (config_section and config_enabled are mutually exclusive)
137+ :param settings_name: field name in the app's settings that corresponds to this module. Defaults to the name of the module with app prefix.
80138 :raises DependencyError
81139 :raises ApplicationSetupError
82140 :return: True if setup was completed or False if setup was skipped
@@ -111,7 +169,7 @@ def _decorate(setup_func: _SetupFunc):
111169 logger .warning ("Rename '%s' to contain 'setup'" , setup_func .__name__ )
112170
113171 # metadata info
114- def setup_metadata () -> Dict :
172+ def setup_metadata () -> Dict [ str , Any ] :
115173 return {
116174 "module_name" : module_name ,
117175 "dependencies" : depends ,
@@ -132,56 +190,74 @@ def _wrapper(app: web.Application, *args, **kargs) -> bool:
132190 f"{ depends } " ,
133191 )
134192
135- if APP_SETUP_KEY not in app :
136- app [APP_SETUP_KEY ] = []
193+ if APP_SETUP_COMPLETED_KEY not in app :
194+ app [APP_SETUP_COMPLETED_KEY ] = []
137195
138196 if category == ModuleCategory .ADDON :
139197 # NOTE: ONLY addons can be enabled/disabled
140- # TODO: sometimes section is optional, check in config schema
141- cfg = app [APP_CONFIG_KEY ]
142-
143- try :
144- is_enabled = _is_app_module_enabled (
145- cfg , config_enabled .split ("." ), section
146- )
147- except KeyError as ee :
148- raise ApplicationSetupError (
149- f"Cannot find required option '{ config_enabled } ' in app config's section '{ ee } '"
150- ) from ee
151198
199+ # TODO: cfg will be fully replaced by app_settings section below
200+ cfg = app [APP_CONFIG_KEY ]
201+ is_enabled = _is_addon_enabled_from_config (cfg , config_enabled , section )
152202 if not is_enabled :
153203 logger .info (
154204 "Skipping '%s' setup. Explicitly disabled in config" ,
155205 module_name ,
156206 )
157207 return False
158208
209+ # NOTE: if not disabled by config, it can be disabled by settings (tmp while legacy maintained)
210+ app_settings , module_settings_name = _get_app_settings_and_field_name (
211+ app ,
212+ module_name ,
213+ settings_name ,
214+ setup_func .__name__ ,
215+ logger ,
216+ )
217+
218+ if (
219+ app_settings
220+ and module_settings_name
221+ and not app_settings .is_enabled (module_settings_name )
222+ ):
223+ logger .info (
224+ "Skipping setup %s. %s disabled in settings" ,
225+ f"{ module_name = } " ,
226+ f"{ module_settings_name = } " ,
227+ )
228+ return False
229+
159230 if depends :
231+ # TODO: no need to enforce. Use to deduce order instead.
160232 uninitialized = [
161- dep for dep in depends if dep not in app [ APP_SETUP_KEY ]
233+ dep for dep in depends if not is_setup_completed ( dep , app )
162234 ]
163235 if uninitialized :
164- msg = f"Cannot setup app module '{ module_name } ' because the following dependencies are still uninitialized: { uninitialized } "
165- log .error (msg )
166- raise DependencyError (msg )
167-
168- if module_name in app [APP_SETUP_KEY ]:
169- msg = f"'{ module_name } ' was already initialized in { app } . Setup can only be executed once per app."
170- logger .error (msg )
171- raise ApplicationSetupError (msg )
236+ raise DependencyError (
237+ f"Cannot setup app module '{ module_name } ' because the "
238+ f"following dependencies are still uninitialized: { uninitialized } "
239+ )
172240
173241 # execution of setup
174242 try :
243+ if is_setup_completed (module_name , app ):
244+ raise SkipModuleSetup (
245+ reason = f"'{ module_name } ' was already initialized in { app } ."
246+ " Setup can only be executed once per app."
247+ )
248+
175249 completed = setup_func (app , * args , ** kargs )
176250
177251 # post-setup
178252 if completed is None :
179253 completed = True
180254
181- if completed :
182- app [APP_SETUP_KEY ].append (module_name )
255+ if completed : # registers completed setup
256+ app [APP_SETUP_COMPLETED_KEY ].append (module_name )
183257 else :
184- raise SkipModuleSetup (reason = "Undefined" )
258+ raise SkipModuleSetup (
259+ reason = "Undefined (setup function returned false)"
260+ )
185261
186262 except SkipModuleSetup as exc :
187263 logger .warning ("Skipping '%s' setup: %s" , module_name , exc .reason )
@@ -197,17 +273,18 @@ def _wrapper(app: web.Application, *args, **kargs) -> bool:
197273 return completed
198274
199275 _wrapper .metadata = setup_metadata
200- _wrapper .MARK = "setup"
276+ _wrapper .mark_as_simcore_servicelib_setup_func = True
201277
202278 return _wrapper
203279
204280 return _decorate
205281
206282
207- def is_setup_function (fun ):
283+ def is_setup_function (fun : Callable ) -> bool :
284+ # TODO: use _SetupFunc protocol to check in runtime
208285 return (
209286 inspect .isfunction (fun )
210- and getattr (fun , "MARK" , None ) == "setup"
287+ and hasattr (fun , "mark_as_simcore_servicelib_setup_func" )
211288 and any (
212289 param .annotation == web .Application
213290 for _ , param in inspect .signature (fun ).parameters .items ()
0 commit comments