Skip to content

Commit d423d6c

Browse files
committed
impving setup
1 parent ec42045 commit d423d6c

File tree

1 file changed

+123
-72
lines changed

1 file changed

+123
-72
lines changed

packages/service-library/src/servicelib/aiohttp/application_setup.py

Lines changed: 123 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
import inspect
33
import logging
44
from collections.abc import Callable
5+
from contextlib import ContextDecorator
56
from copy import deepcopy
67
from enum import Enum
7-
from typing import Any, Protocol
8+
from types import TracebackType
9+
from typing import Any, Final, Protocol
810

911
import arrow
1012
from aiohttp import web
@@ -15,21 +17,19 @@
1517

1618
from .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

2325
class _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

3031
class _ApplicationSettings(Protocol):
31-
def is_enabled(self, field_name: str) -> bool:
32-
...
32+
def is_enabled(self, field_name: str) -> bool: ...
3333

3434

3535
class 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

5755
class 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

140183
def 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+
144234
def 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

313364
def is_setup_function(fun: Callable) -> bool:

0 commit comments

Comments
 (0)