Coverage for src / ezqt_app / services / _registry.py: 90.00%
24 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-06 13:12 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-06 13:12 +0000
1# ///////////////////////////////////////////////////////////////
2# SERVICES._REGISTRY - Lightweight service registry
3# Project: ezqt_app
4# ///////////////////////////////////////////////////////////////
6"""Lightweight service registry supporting test-time instance overrides.
8Usage — production (no change needed in calling code)::
10 def get_config_service() -> ConfigService:
11 return ServiceRegistry.get(ConfigService, ConfigService)
13Usage — tests::
15 ServiceRegistry.register(ConfigService, FakeConfigService())
16 # … run test …
17 ServiceRegistry.reset(ConfigService)
19The registry acts as a lazy singleton cache in production and an override
20mechanism in tests, removing the need to monkey-patch private module
21variables or access ``_private`` attributes.
22"""
24from __future__ import annotations
26# ///////////////////////////////////////////////////////////////
27# IMPORTS
28# ///////////////////////////////////////////////////////////////
29# Standard library imports
30from collections.abc import Callable
31from typing import TypeVar, cast
33_T = TypeVar("_T")
36# ///////////////////////////////////////////////////////////////
37# CLASSES
38# ///////////////////////////////////////////////////////////////
39class ServiceRegistry:
40 """Lightweight service registry supporting test-time instance overrides.
42 In production, :meth:`get` acts as a lazy singleton: the factory is called
43 once and the result is cached. In tests, :meth:`register` injects a
44 replacement instance for a specific type; :meth:`reset` removes it and
45 clears the cached instance so the next :meth:`get` call rebuilds cleanly.
46 """
48 _instances: dict[type, object] = {}
49 _overrides: dict[type, object] = {}
51 @classmethod
52 def get(cls, service_type: type[_T], factory: Callable[[], _T]) -> _T:
53 """Return the registered override, or the lazily-cached instance.
55 Parameters
56 ----------
57 service_type:
58 The class used as the registry key.
59 factory:
60 Callable (usually the class itself) invoked once to create the
61 instance when no cached value exists.
62 """
63 if service_type in cls._overrides:
64 return cast(_T, cls._overrides[service_type])
65 if service_type not in cls._instances:
66 cls._instances[service_type] = factory()
67 return cast(_T, cls._instances[service_type])
69 @classmethod
70 def register(cls, service_type: type[_T], instance: _T) -> None:
71 """Register *instance* as the active implementation for *service_type*.
73 Primarily intended for test fixtures. The override takes precedence
74 over any cached instance until :meth:`reset` is called.
75 """
76 cls._overrides[service_type] = instance
78 @classmethod
79 def reset(cls, service_type: type | None = None) -> None:
80 """Remove override and cached instance.
82 Parameters
83 ----------
84 service_type:
85 Type to reset. Pass ``None`` to clear the entire registry
86 (useful in a global ``autouse`` teardown fixture).
87 """
88 if service_type is None: 88 ↛ 89line 88 didn't jump to line 89 because the condition on line 88 was never true
89 cls._overrides.clear()
90 cls._instances.clear()
91 else:
92 cls._overrides.pop(service_type, None)
93 cls._instances.pop(service_type, None)