Source code for action0.service.default
"""A process-wide default registry, for applications that want one.
Libraries and composable code should pass
:py:class:`~action0.service.registry.Registry` instances around
explicitly; but in an application or script there is often exactly one
registry anyway, and threading it through every call site is ceremony.
:py:func:`default_registry` provides that single instance on demand:
>>> from action0.service import Registry
>>> from action0.service import default_registry
>>> from action0.service import set_default_registry
>>> class Config:
... def __init__(self, env: str = "prod"):
... self.env = env
>>> _ = default_registry().register(Config)
>>> default_registry().get(Config).env
'prod'
>>> _ = set_default_registry(None) # reset so unrelated code starts fresh
The default registry is created lazily on first access and is *never
closed automatically* — closing it (and when) is the application's
responsibility. After closing it, call ``set_default_registry(None)`` so
the next access creates a fresh instance.
"""
import threading
from collections.abc import Iterator
from contextlib import contextmanager
from action0.service.registry import Registry
# guards lazy creation and replacement of the process-wide instance
_LOCK = threading.Lock()
_default: Registry | None = None
[docs]
def default_registry() -> Registry:
"""
Return the process-wide default registry, creating it on first access.
:returns: the shared :py:class:`~action0.service.registry.Registry`
instance (the same one on every call, until it is replaced with
:py:func:`set_default_registry`)
"""
global _default
registry = _default
if registry is None:
with _LOCK:
# double-checked: another thread may have created it while we
# waited for the lock
if _default is None:
_default = Registry()
registry = _default
return registry
[docs]
def set_default_registry(registry: "Registry | None") -> "Registry | None":
"""
Install ``registry`` as the process-wide default.
Passing ``None`` resets the default, so the next
:py:func:`default_registry` call creates a fresh instance — do this
after closing the previous default.
The previous instance is returned but not closed; dispose it yourself
if it held resources.
:param registry: the new default registry, or ``None`` to reset
:returns: the previously installed registry, or ``None``
"""
global _default
with _LOCK:
previous = _default
_default = registry
return previous
[docs]
@contextmanager
def using_default_registry(registry: Registry) -> Iterator[Registry]:
"""
Temporarily install ``registry`` as the process-wide default.
The previous default (or the not-yet-created state) is restored when
the ``with`` block ends, even on error — the pattern for tests that
exercise code relying on :py:func:`default_registry`::
with Registry() as registry, using_default_registry(registry):
registry.register_instance(FakeMailer(), provides=Mailer)
code_under_test()
Note this swaps process-global state: tests doing this cannot run
concurrently with other tests that touch the default registry.
:param registry: the registry to install for the duration
:returns: a context manager yielding ``registry``
"""
previous = set_default_registry(registry)
try:
yield registry
finally:
set_default_registry(previous)