Coverage for src/lexigram/features/backends/testing.py: 100%

28 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-26 02:04 +0800

1"""Lightweight in-memory provider designed for use in tests. 

2 

3Unlike :class:`~lexigram.feature_flags.backends.local.LocalProvider` this 

4provider works with plain booleans and supports per-flag *overrides* that 

5short-circuit normal evaluation — making it easy to force specific outcomes 

6in unit tests without wiring a full flag configuration. 

7""" 

8 

9from __future__ import annotations 

10 

11from typing import Any 

12 

13from lexigram.features.backends.local import LocalProvider 

14from lexigram.features.types import Flag, FlagContext, FlagEvaluation, FlagType 

15 

16 

17class MemoryProvider(LocalProvider): 

18 """Lightweight in-memory provider designed for use in tests. 

19 

20 Unlike :class:`~lexigram.feature_flags.backends.local.LocalProvider` this 

21 provider works with plain booleans and supports per-flag *overrides* that 

22 short-circuit normal evaluation. 

23 

24 .. note:: 

25 **Choosing between in-memory providers:** 

26 

27 * Use :class:`MemoryProvider` (this class) in **unit tests** — the 

28 ``_overrides`` dict lets you force specific flag outcomes without 

29 touching the flag configuration. 

30 

31 * Use :class:`~lexigram.features.backends.local.LocalProvider` 

32 for production single-node deployments or integration tests where 

33 you only need ``set_flag`` / ``get_flag`` semantics and do not need 

34 per-test overrides. 

35 

36 Typical usage in tests:: 

37 

38 provider = MemoryProvider() 

39 provider.set_flag("new_billing", enabled=True) 

40 manager = FlagManager(provider) 

41 """ 

42 

43 def __init__(self) -> None: 

44 super().__init__() 

45 self._overrides: dict[str, FlagEvaluation] = {} 

46 

47 # -- Evaluation (checks overrides first) -------------------------------- 

48 

49 async def evaluate( 

50 self, 

51 name: str, 

52 context: FlagContext | None = None, 

53 ) -> FlagEvaluation: 

54 """Evaluate, applying any explicit test override first.""" 

55 if name in self._overrides: 

56 return self._overrides[name] 

57 return await super().evaluate(name, context) 

58 

59 def evaluate_sync( 

60 self, 

61 name: str, 

62 context: FlagContext | None = None, 

63 ) -> FlagEvaluation: 

64 """Synchronous evaluation, applying overrides first.""" 

65 if name in self._overrides: 

66 return self._overrides[name] 

67 return super().evaluate_sync(name, context) 

68 

69 # -- Test helpers ------------------------------------------------------- 

70 

71 def set_flag_sync(self, name: str, value: bool) -> None: 

72 """Define a simple boolean flag (sync version).""" 

73 self._flags[name] = Flag(name=name, type=FlagType.BOOLEAN, enabled=value) 

74 

75 async def set_flag(self, name: str, value: bool) -> None: 

76 """Define a simple boolean flag.""" 

77 self.set_flag_sync(name, value) 

78 

79 def override( 

80 self, 

81 name: str, 

82 *, 

83 enabled: bool, 

84 value: Any = None, 

85 reason: str = "test_override", 

86 ) -> None: 

87 """Register a hard override for *name* that bypasses normal evaluation.""" 

88 self._overrides[name] = FlagEvaluation( 

89 flag_name=name, 

90 enabled=enabled, 

91 reason=reason, 

92 value=value if value is not None else enabled, 

93 ) 

94 

95 def clear_override(self, name: str) -> None: 

96 """Remove the test override for *name*.""" 

97 self._overrides.pop(name, None) 

98 

99 def reset(self) -> None: 

100 """Clear all flags and overrides.""" 

101 self._flags.clear() 

102 self._overrides.clear() 

103 

104 

105__all__ = ["MemoryProvider"]