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
« 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.
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"""
9from __future__ import annotations
11from typing import Any
13from lexigram.features.backends.local import LocalProvider
14from lexigram.features.types import Flag, FlagContext, FlagEvaluation, FlagType
17class MemoryProvider(LocalProvider):
18 """Lightweight in-memory provider designed for use in tests.
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.
24 .. note::
25 **Choosing between in-memory providers:**
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.
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.
36 Typical usage in tests::
38 provider = MemoryProvider()
39 provider.set_flag("new_billing", enabled=True)
40 manager = FlagManager(provider)
41 """
43 def __init__(self) -> None:
44 super().__init__()
45 self._overrides: dict[str, FlagEvaluation] = {}
47 # -- Evaluation (checks overrides first) --------------------------------
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)
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)
69 # -- Test helpers -------------------------------------------------------
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)
75 async def set_flag(self, name: str, value: bool) -> None:
76 """Define a simple boolean flag."""
77 self.set_flag_sync(name, value)
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 )
95 def clear_override(self, name: str) -> None:
96 """Remove the test override for *name*."""
97 self._overrides.pop(name, None)
99 def reset(self) -> None:
100 """Clear all flags and overrides."""
101 self._flags.clear()
102 self._overrides.clear()
105__all__ = ["MemoryProvider"]