Skip to content

Commit 184c036

Browse files
committed
fix unit tests, increased coverage, simplified implementation
1 parent 9275136 commit 184c036

File tree

8 files changed

+306
-168
lines changed

8 files changed

+306
-168
lines changed

openfeature/client.py

Lines changed: 15 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -470,45 +470,21 @@ def _typecheck_flag_value(value: typing.Any, flag_type: FlagType) -> None:
470470
raise TypeMismatchError(f"Expected type {_type} but got {type(value)}")
471471

472472

473-
class AsyncOpenFeatureClient:
474-
def __init__(
475-
self,
476-
domain: typing.Optional[str],
477-
version: typing.Optional[str],
478-
context: typing.Optional[EvaluationContext] = None,
479-
hooks: typing.Optional[typing.List[Hook]] = None,
480-
) -> None:
481-
self.domain = domain
482-
self.version = version
483-
self.context = context or EvaluationContext()
484-
self.hooks = hooks or []
485-
486-
@property
487-
def provider(self) -> FeatureProvider:
488-
return provider_registry.get_provider(self.domain)
489-
490-
def get_provider_status(self) -> ProviderStatus:
491-
return provider_registry.get_provider_status(self.provider)
492-
493-
def get_metadata(self) -> ClientMetadata:
494-
return ClientMetadata(domain=self.domain)
495-
496-
def add_hooks(self, hooks: typing.List[Hook]) -> None:
497-
self.hooks = self.hooks + hooks
498-
473+
class AsyncOpenFeatureClient(OpenFeatureClient):
499474
async def get_boolean_value(
500475
self,
501476
flag_key: str,
502477
default_value: bool,
503478
evaluation_context: typing.Optional[EvaluationContext] = None,
504479
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
505480
) -> bool:
506-
return await self.get_boolean_value(
481+
details = await self.get_boolean_details(
507482
flag_key,
508483
default_value,
509484
evaluation_context,
510485
flag_evaluation_options,
511486
)
487+
return details.value
512488

513489
async def get_boolean_details(
514490
self,
@@ -532,12 +508,13 @@ async def get_string_value(
532508
evaluation_context: typing.Optional[EvaluationContext] = None,
533509
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
534510
) -> str:
535-
return await self.get_string_details(
511+
details = await self.get_string_details(
536512
flag_key,
537513
default_value,
538514
evaluation_context,
539515
flag_evaluation_options,
540-
).value
516+
)
517+
return details.value
541518

542519
async def get_string_details(
543520
self,
@@ -561,12 +538,13 @@ async def get_integer_value(
561538
evaluation_context: typing.Optional[EvaluationContext] = None,
562539
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
563540
) -> int:
564-
return await self.get_integer_details(
541+
details = await self.get_integer_details(
565542
flag_key,
566543
default_value,
567544
evaluation_context,
568545
flag_evaluation_options,
569-
).value
546+
)
547+
return details.value
570548

571549
async def get_integer_details(
572550
self,
@@ -590,12 +568,13 @@ async def get_float_value(
590568
evaluation_context: typing.Optional[EvaluationContext] = None,
591569
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
592570
) -> float:
593-
return await self.get_float_details(
571+
details = await self.get_float_details(
594572
flag_key,
595573
default_value,
596574
evaluation_context,
597575
flag_evaluation_options,
598-
).value
576+
)
577+
return details.value
599578

600579
async def get_float_details(
601580
self,
@@ -619,12 +598,13 @@ async def get_object_value(
619598
evaluation_context: typing.Optional[EvaluationContext] = None,
620599
flag_evaluation_options: typing.Optional[FlagEvaluationOptions] = None,
621600
) -> typing.Union[dict, list]:
622-
return await self.get_object_details(
601+
details = await self.get_object_details(
623602
flag_key,
624603
default_value,
625604
evaluation_context,
626605
flag_evaluation_options,
627-
).value
606+
)
607+
return details.value
628608

629609
async def get_object_details(
630610
self,
@@ -844,9 +824,3 @@ async def _create_provider_evaluation(
844824
error_code=resolution.error_code,
845825
error_message=resolution.error_message,
846826
)
847-
848-
def add_handler(self, event: ProviderEvent, handler: EventHandler) -> None:
849-
_event_support.add_client_handler(self, event, handler)
850-
851-
def remove_handler(self, event: ProviderEvent, handler: EventHandler) -> None:
852-
_event_support.remove_client_handler(self, event, handler)

openfeature/hook/_hook_support.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def error_hooks(
2121
flag_type=flag_type, hooks=hooks, hook_method=HookType.ERROR, **kwargs
2222
)
2323

24+
2425
async def error_hooks_async(
2526
flag_type: FlagType,
2627
hook_context: HookContext,
@@ -33,6 +34,7 @@ async def error_hooks_async(
3334
flag_type=flag_type, hooks=hooks, hook_method=HookType.ERROR, **kwargs
3435
)
3536

37+
3638
def after_all_hooks(
3739
flag_type: FlagType,
3840
hook_context: HookContext,
@@ -44,6 +46,7 @@ def after_all_hooks(
4446
flag_type=flag_type, hooks=hooks, hook_method=HookType.FINALLY_AFTER, **kwargs
4547
)
4648

49+
4750
async def after_all_hooks_async(
4851
flag_type: FlagType,
4952
hook_context: HookContext,
@@ -68,6 +71,7 @@ def after_hooks(
6871
flag_type=flag_type, hooks=hooks, hook_method=HookType.AFTER, **kwargs
6972
)
7073

74+
7175
async def after_hooks_async(
7276
flag_type: FlagType,
7377
hook_context: HookContext,
@@ -98,6 +102,7 @@ def before_hooks(
98102

99103
return EvaluationContext()
100104

105+
101106
async def before_hooks_async(
102107
flag_type: FlagType,
103108
hook_context: HookContext,
@@ -137,6 +142,7 @@ def _execute_hooks(
137142
if hook.supports_flag_value_type(flag_type)
138143
]
139144

145+
140146
async def _execute_hooks_async(
141147
flag_type: FlagType,
142148
hooks: typing.List[Hook],
@@ -183,6 +189,7 @@ def _execute_hooks_unchecked(
183189
if hook.supports_flag_value_type(flag_type)
184190
]
185191

192+
186193
async def _execute_hooks_async_unchecked(
187194
flag_type: FlagType,
188195
hooks: typing.List[Hook],
@@ -228,6 +235,7 @@ def _execute_hook_checked(
228235
logger.exception(f"Exception when running {hook_method.value} hooks")
229236
return None
230237

238+
231239
async def _execute_hook_checked_async(
232240
hook: Hook, hook_method: HookType, **kwargs: typing.Any
233241
) -> typing.Optional[EvaluationContext]:

openfeature/provider/__init__.py

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -166,32 +166,7 @@ def emit(self, event: ProviderEvent, details: ProviderEventDetails) -> None:
166166
self._on_emit(self, event, details)
167167

168168

169-
class AsyncFeatureProvider(FeatureProvider):
170-
async def attach(
171-
self,
172-
on_emit: typing.Callable[
173-
[FeatureProvider, ProviderEvent, ProviderEventDetails], None
174-
],
175-
) -> None:
176-
self._on_emit = on_emit
177-
178-
async def detach(self) -> None:
179-
if hasattr(self, "_on_emit"):
180-
del self._on_emit
181-
182-
async def initialize(self, evaluation_context: EvaluationContext) -> None:
183-
pass
184-
185-
async def shutdown(self) -> None:
186-
pass
187-
188-
@abstractmethod
189-
async def get_metadata(self) -> Metadata:
190-
pass
191-
192-
async def get_provider_hooks(self) -> typing.List[Hook]:
193-
return []
194-
169+
class AsyncAbstractProvider(AbstractProvider):
195170
@abstractmethod
196171
async def resolve_boolean_details(
197172
self,
@@ -236,21 +211,3 @@ def resolve_object_details(
236211
evaluation_context: typing.Optional[EvaluationContext] = None,
237212
) -> FlagResolutionDetails[typing.Union[dict, list]]:
238213
pass
239-
240-
async def emit_provider_ready(self, details: ProviderEventDetails) -> None:
241-
self.emit(ProviderEvent.PROVIDER_READY, details)
242-
243-
async def emit_provider_configuration_changed(
244-
self, details: ProviderEventDetails
245-
) -> None:
246-
self.emit(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details)
247-
248-
async def emit_provider_error(self, details: ProviderEventDetails) -> None:
249-
self.emit(ProviderEvent.PROVIDER_ERROR, details)
250-
251-
async def emit_provider_stale(self, details: ProviderEventDetails) -> None:
252-
self.emit(ProviderEvent.PROVIDER_STALE, details)
253-
254-
async def emit(self, event: ProviderEvent, details: ProviderEventDetails) -> None:
255-
if hasattr(self, "_on_emit"):
256-
self._on_emit(self, event, details)

openfeature/provider/no_op_provider.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,65 @@ def resolve_object_details(
7575
reason=Reason.DEFAULT,
7676
variant=PASSED_IN_DEFAULT,
7777
)
78+
79+
80+
class AsyncNoOpProvider(NoOpProvider):
81+
async def resolve_boolean_details(
82+
self,
83+
flag_key: str,
84+
default_value: bool,
85+
evaluation_context: typing.Optional[EvaluationContext] = None,
86+
) -> FlagResolutionDetails[bool]:
87+
return FlagResolutionDetails(
88+
value=default_value,
89+
reason=Reason.DEFAULT,
90+
variant=PASSED_IN_DEFAULT,
91+
)
92+
93+
async def resolve_string_details(
94+
self,
95+
flag_key: str,
96+
default_value: str,
97+
evaluation_context: typing.Optional[EvaluationContext] = None,
98+
) -> FlagResolutionDetails[str]:
99+
return FlagResolutionDetails(
100+
value=default_value,
101+
reason=Reason.DEFAULT,
102+
variant=PASSED_IN_DEFAULT,
103+
)
104+
105+
async def resolve_integer_details(
106+
self,
107+
flag_key: str,
108+
default_value: int,
109+
evaluation_context: typing.Optional[EvaluationContext] = None,
110+
) -> FlagResolutionDetails[int]:
111+
return FlagResolutionDetails(
112+
value=default_value,
113+
reason=Reason.DEFAULT,
114+
variant=PASSED_IN_DEFAULT,
115+
)
116+
117+
async def resolve_float_details(
118+
self,
119+
flag_key: str,
120+
default_value: float,
121+
evaluation_context: typing.Optional[EvaluationContext] = None,
122+
) -> FlagResolutionDetails[float]:
123+
return FlagResolutionDetails(
124+
value=default_value,
125+
reason=Reason.DEFAULT,
126+
variant=PASSED_IN_DEFAULT,
127+
)
128+
129+
async def resolve_object_details(
130+
self,
131+
flag_key: str,
132+
default_value: typing.Union[dict, list],
133+
evaluation_context: typing.Optional[EvaluationContext] = None,
134+
) -> FlagResolutionDetails[typing.Union[dict, list]]:
135+
return FlagResolutionDetails(
136+
value=default_value,
137+
reason=Reason.DEFAULT,
138+
variant=PASSED_IN_DEFAULT,
139+
)

tests/conftest.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22

33
from openfeature import api
4-
from openfeature.provider.no_op_provider import NoOpProvider
4+
from openfeature.provider.no_op_provider import AsyncNoOpProvider, NoOpProvider
55

66

77
@pytest.fixture(autouse=True)
@@ -13,7 +13,22 @@ def clear_providers():
1313
api.clear_providers()
1414

1515

16+
@pytest.fixture(autouse=True)
17+
def clear_hooks_fixture():
18+
"""
19+
For tests that use add_hooks(), we need to clear the hooks to avoid issues
20+
in other tests.
21+
"""
22+
api.clear_hooks()
23+
24+
1625
@pytest.fixture()
1726
def no_op_provider_client():
1827
api.set_provider(NoOpProvider())
1928
return api.get_client()
29+
30+
31+
@pytest.fixture()
32+
def no_op_provider_client_async():
33+
api.set_provider(AsyncNoOpProvider())
34+
return api.get_client_async("my-async-client")

tests/hook/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44

55
from openfeature.evaluation_context import EvaluationContext
6+
from openfeature.hook import AsyncHook
67

78

89
@pytest.fixture()
@@ -14,3 +15,14 @@ def mock_hook():
1415
mock_hook.error.return_value = None
1516
mock_hook.finally_after.return_value = None
1617
return mock_hook
18+
19+
20+
@pytest.fixture()
21+
def mock_hook_async():
22+
mock_hook = AsyncHook()
23+
mock_hook.supports_flag_value_type = mock.MagicMock(return_value=True)
24+
mock_hook.before = mock.AsyncMock(return_value=None)
25+
mock_hook.after = mock.AsyncMock(return_value=None)
26+
mock_hook.error = mock.AsyncMock(return_value=None)
27+
mock_hook.finally_after = mock.AsyncMock(return_value=None)
28+
return mock_hook

0 commit comments

Comments
 (0)