22import inspect
33import logging
44from collections .abc import Callable
5+ from contextlib import ContextDecorator
56from copy import deepcopy
67from enum import Enum
7- from typing import Any , Protocol
8+ from types import TracebackType
9+ from typing import Any , Final , Protocol
810
911import arrow
1012from aiohttp import web
1517
1618from .application_keys import APP_CONFIG_KEY , APP_SETTINGS_KEY
1719
18- log = logging .getLogger (__name__ )
20+ _logger = logging .getLogger (__name__ )
1921
20- APP_SETUP_COMPLETED_KEY = f"{ __name__ } .setup"
22+ APP_SETUP_COMPLETED_KEY : Final [ str ] = f"{ __name__ } .setup"
2123
2224
2325class _SetupFunc (Protocol ):
2426 __name__ : str
2527
26- def __call__ (self , app : web .Application , * args : Any , ** kwds : Any ) -> bool :
27- ...
28+ def __call__ (self , app : web .Application , * args : Any , ** kwds : Any ) -> bool : ...
2829
2930
3031class _ApplicationSettings (Protocol ):
31- def is_enabled (self , field_name : str ) -> bool :
32- ...
32+ def is_enabled (self , field_name : str ) -> bool : ...
3333
3434
3535class ModuleCategory (Enum ):
@@ -46,12 +46,10 @@ def __init__(self, *, reason) -> None:
4646 super ().__init__ (reason )
4747
4848
49- class ApplicationSetupError (Exception ):
50- ...
49+ class ApplicationSetupError (Exception ): ...
5150
5251
53- class DependencyError (ApplicationSetupError ):
54- ...
52+ class DependencyError (ApplicationSetupError ): ...
5553
5654
5755class SetupMetadataDict (TypedDict ):
@@ -134,20 +132,112 @@ def _get_app_settings_and_field_name(
134132 return app_settings , settings_field_name
135133
136134
135+ class _SetupTimingContext (ContextDecorator ):
136+ """Context manager/decorator for timing and logging module setup operations."""
137+
138+ def __init__ (
139+ self ,
140+ module_name : str ,
141+ * ,
142+ category : ModuleCategory | None = None ,
143+ depends : list [str ] | None = None ,
144+ ) -> None :
145+ """Initialize timing context.
146+
147+ :param module_name: Name of the module being set up
148+ :param category: Optional module category for detailed logging
149+ :param depends: Optional dependencies for detailed logging
150+ """
151+ self .module_name = module_name
152+ self .category = category
153+ self .depends = depends
154+ self .started = None
155+ self .head_msg = f"Setup of { module_name } "
156+
157+ def __enter__ (self ) -> None :
158+ self .started = arrow .utcnow ()
159+ if self .category is not None :
160+ _logger .info (
161+ "%s (%s, %s) started ... " ,
162+ self .head_msg ,
163+ f"{ self .category .name = } " ,
164+ f"{ self .depends } " ,
165+ )
166+ else :
167+ _logger .info ("%s started ..." , self .head_msg )
168+
169+ def __exit__ (
170+ self ,
171+ exc_type : type [BaseException ] | None ,
172+ exc_val : BaseException | None ,
173+ exc_tb : TracebackType | None ,
174+ ) -> None :
175+ if self .started :
176+ elapsed = (arrow .utcnow () - self .started ).total_seconds ()
177+ _logger .info ("%s completed [Elapsed: %3.1f secs]" , self .head_msg , elapsed )
178+
179+
137180# PUBLIC API ------------------------------------------------------------------
138181
139182
140183def is_setup_completed (module_name : str , app : web .Application ) -> bool :
141184 return module_name in app [APP_SETUP_COMPLETED_KEY ]
142185
143186
187+ def ensure_single_setup (
188+ module_name : str ,
189+ * ,
190+ logger : logging .Logger = _logger ,
191+ ) -> Callable [[Callable [..., Any ]], Callable [..., Any ]]:
192+ """Ensures a setup function is executed only once per application and handles completion.
193+
194+ :param module_name: Name of the module being set up
195+ """
196+
197+ def _skip_setup (reason : str ) -> bool :
198+ logger .info ("Skipping '%s' setup: %s" , module_name , reason )
199+ return False
200+
201+ def decorator (setup_func : _SetupFunc ) -> _SetupFunc :
202+
203+ @functools .wraps (setup_func )
204+ def _wrapper (app : web .Application , * args : Any , ** kwargs : Any ) -> bool :
205+
206+ # pre-setup init
207+ if APP_SETUP_COMPLETED_KEY not in app :
208+ app [APP_SETUP_COMPLETED_KEY ] = {}
209+
210+ # check
211+ if is_setup_completed (module_name , app ):
212+ return _skip_setup (
213+ f"'{ module_name } ' was already initialized in { app } ."
214+ " Setup can only be executed once per app."
215+ )
216+
217+ completed = setup_func (app , * args , ** kwargs )
218+
219+ # post-setup handling
220+ if completed is None :
221+ completed = True
222+
223+ if completed : # registers completed setup
224+ app [APP_SETUP_COMPLETED_KEY ].add (module_name )
225+ return completed
226+
227+ return _skip_setup ("Undefined (setup function returned false)" )
228+
229+ return _wrapper
230+
231+ return decorator
232+
233+
144234def app_module_setup (
145235 module_name : str ,
146236 category : ModuleCategory ,
147237 * ,
148238 settings_name : str | None = None ,
149239 depends : list [str ] | None = None ,
150- logger : logging .Logger = log ,
240+ logger : logging .Logger = _logger ,
151241 # TODO: SEE https://github.com/ITISFoundation/osparc-simcore/issues/2008
152242 # TODO: - settings_name becomes module_name!!
153243 # TODO: - plugin base should be aware of setup and settings -> model instead of function?
@@ -190,35 +280,24 @@ def setup(app: web.Application):
190280 module_name , depends , config_section , config_enabled
191281 )
192282
193- def _decorate (setup_func : _SetupFunc ):
194- if "setup" not in setup_func .__name__ :
195- logger .warning ("Rename '%s' to contain 'setup'" , setup_func .__name__ )
196-
197- # metadata info
198- def setup_metadata () -> SetupMetadataDict :
199- return SetupMetadataDict (
200- module_name = module_name ,
201- dependencies = depends ,
202- config_section = section ,
203- config_enabled = config_enabled ,
204- )
283+ # metadata info
284+ def _setup_metadata () -> SetupMetadataDict :
285+ return SetupMetadataDict (
286+ module_name = module_name ,
287+ dependencies = depends ,
288+ config_section = section ,
289+ config_enabled = config_enabled ,
290+ )
205291
206- # wrapper
207- @functools .wraps (setup_func )
208- def _wrapper (app : web .Application , * args , ** kargs ) -> bool :
209- # pre-setup
210- head_msg = f"Setup of { module_name } "
211- started = arrow .utcnow ()
212- logger .info (
213- "%s (%s, %s) started ... " ,
214- head_msg ,
215- f"{ category .name = } " ,
216- f"{ depends } " ,
217- )
292+ def decorator (setup_func : _SetupFunc ) -> _SetupFunc :
218293
219- if APP_SETUP_COMPLETED_KEY not in app :
220- app [APP_SETUP_COMPLETED_KEY ] = []
294+ assert setup_func .__name__ .startswith ( # nosec
295+ "setup_"
296+ ), f"Rename '{ setup_func .__name__ } ' start with 'setup_$(plugin-name)'"
221297
298+ @functools .wraps (setup_func )
299+ @_SetupTimingContext (module_name , category = category , depends = depends )
300+ def _wrapper (app : web .Application , * args , ** kargs ) -> bool :
222301 if category == ModuleCategory .ADDON :
223302 # ONLY addons can be enabled/disabled
224303
@@ -258,56 +337,28 @@ def _wrapper(app: web.Application, *args, **kargs) -> bool:
258337 return False
259338
260339 if depends :
261- # TODO: no need to enforce. Use to deduce order instead.
262340 uninitialized = [
263341 dep for dep in depends if not is_setup_completed (dep , app )
264342 ]
265343 if uninitialized :
266344 msg = f"Cannot setup app module '{ module_name } ' because the following dependencies are still uninitialized: { uninitialized } "
267345 raise DependencyError (msg )
268346
269- # execution of setup
270- try :
271- if is_setup_completed (module_name , app ):
272- raise SkipModuleSetupError ( # noqa: TRY301
273- reason = f"'{ module_name } ' was already initialized in { app } ."
274- " Setup can only be executed once per app."
275- )
276-
277- completed = setup_func (app , * args , ** kargs )
347+ # execution of setup with module name
348+ completed : bool = ensure_single_setup (module_name , logger = logger )(
349+ setup_func
350+ )(app , * args , ** kargs )
278351
279- # post-setup
280- if completed is None :
281- completed = True
282-
283- if completed : # registers completed setup
284- app [APP_SETUP_COMPLETED_KEY ].append (module_name )
285- else :
286- raise SkipModuleSetupError ( # noqa: TRY301
287- reason = "Undefined (setup function returned false)"
288- )
289-
290- except SkipModuleSetupError as exc :
291- logger .info ("Skipping '%s' setup: %s" , module_name , exc .reason )
292- completed = False
293-
294- elapsed = arrow .utcnow () - started
295- logger .info (
296- "%s %s [Elapsed: %3.1f secs]" ,
297- head_msg ,
298- "completed" if completed else "skipped" ,
299- elapsed .total_seconds (),
300- )
301352 return completed
302353
303- _wrapper .metadata = setup_metadata # type: ignore[attr-defined]
354+ _wrapper .metadata = _setup_metadata # type: ignore[attr-defined]
304355 _wrapper .mark_as_simcore_servicelib_setup_func = True # type: ignore[attr-defined]
305356 # NOTE: this is added by functools.wraps decorated
306357 assert _wrapper .__wrapped__ == setup_func # nosec
307358
308359 return _wrapper
309360
310- return _decorate
361+ return decorator
311362
312363
313364def is_setup_function (fun : Callable ) -> bool :
0 commit comments