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

1# /////////////////////////////////////////////////////////////// 

2# SERVICES._REGISTRY - Lightweight service registry 

3# Project: ezqt_app 

4# /////////////////////////////////////////////////////////////// 

5 

6"""Lightweight service registry supporting test-time instance overrides. 

7 

8Usage — production (no change needed in calling code):: 

9 

10 def get_config_service() -> ConfigService: 

11 return ServiceRegistry.get(ConfigService, ConfigService) 

12 

13Usage — tests:: 

14 

15 ServiceRegistry.register(ConfigService, FakeConfigService()) 

16 # … run test … 

17 ServiceRegistry.reset(ConfigService) 

18 

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""" 

23 

24from __future__ import annotations 

25 

26# /////////////////////////////////////////////////////////////// 

27# IMPORTS 

28# /////////////////////////////////////////////////////////////// 

29# Standard library imports 

30from collections.abc import Callable 

31from typing import TypeVar, cast 

32 

33_T = TypeVar("_T") 

34 

35 

36# /////////////////////////////////////////////////////////////// 

37# CLASSES 

38# /////////////////////////////////////////////////////////////// 

39class ServiceRegistry: 

40 """Lightweight service registry supporting test-time instance overrides. 

41 

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 """ 

47 

48 _instances: dict[type, object] = {} 

49 _overrides: dict[type, object] = {} 

50 

51 @classmethod 

52 def get(cls, service_type: type[_T], factory: Callable[[], _T]) -> _T: 

53 """Return the registered override, or the lazily-cached instance. 

54 

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]) 

68 

69 @classmethod 

70 def register(cls, service_type: type[_T], instance: _T) -> None: 

71 """Register *instance* as the active implementation for *service_type*. 

72 

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 

77 

78 @classmethod 

79 def reset(cls, service_type: type | None = None) -> None: 

80 """Remove override and cached instance. 

81 

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)