Skip to content

Commit e61b69b

Browse files
authored
refactor: replace exception raising with error flag resolution (#474)
* replace exception raising with error flag resolution Signed-off-by: gruebel <[email protected]> * revert spec to commit 0cd553d Signed-off-by: gruebel <[email protected]> --------- Signed-off-by: gruebel <[email protected]>
1 parent 5a2825b commit e61b69b

File tree

5 files changed

+155
-57
lines changed

5 files changed

+155
-57
lines changed

openfeature/client.py

Lines changed: 88 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -446,12 +446,12 @@ def _establish_hooks_and_provider(
446446

447447
def _assert_provider_status(
448448
self,
449-
) -> None:
449+
) -> typing.Optional[OpenFeatureError]:
450450
status = self.get_provider_status()
451451
if status == ProviderStatus.NOT_READY:
452-
raise ProviderNotReadyError()
452+
return ProviderNotReadyError()
453453
if status == ProviderStatus.FATAL:
454-
raise ProviderFatalError()
454+
return ProviderFatalError()
455455
return None
456456

457457
def _before_hooks_and_merge_context(
@@ -511,7 +511,22 @@ async def evaluate_flag_details_async(
511511
)
512512

513513
try:
514-
self._assert_provider_status()
514+
if provider_err := self._assert_provider_status():
515+
error_hooks(
516+
flag_type,
517+
hook_context,
518+
provider_err,
519+
reversed_merged_hooks,
520+
hook_hints,
521+
)
522+
flag_evaluation = FlagEvaluationDetails(
523+
flag_key=flag_key,
524+
value=default_value,
525+
reason=Reason.ERROR,
526+
error_code=provider_err.error_code,
527+
error_message=provider_err.error_message,
528+
)
529+
return flag_evaluation
515530

516531
merged_context = self._before_hooks_and_merge_context(
517532
flag_type,
@@ -528,6 +543,11 @@ async def evaluate_flag_details_async(
528543
default_value,
529544
merged_context,
530545
)
546+
if err := flag_evaluation.get_exception():
547+
error_hooks(
548+
flag_type, hook_context, err, reversed_merged_hooks, hook_hints
549+
)
550+
return flag_evaluation
531551

532552
after_hooks(
533553
flag_type,
@@ -607,7 +627,22 @@ def evaluate_flag_details(
607627
)
608628

609629
try:
610-
self._assert_provider_status()
630+
if provider_err := self._assert_provider_status():
631+
error_hooks(
632+
flag_type,
633+
hook_context,
634+
provider_err,
635+
reversed_merged_hooks,
636+
hook_hints,
637+
)
638+
flag_evaluation = FlagEvaluationDetails(
639+
flag_key=flag_key,
640+
value=default_value,
641+
reason=Reason.ERROR,
642+
error_code=provider_err.error_code,
643+
error_message=provider_err.error_message,
644+
)
645+
return flag_evaluation
611646

612647
merged_context = self._before_hooks_and_merge_context(
613648
flag_type,
@@ -624,6 +659,12 @@ def evaluate_flag_details(
624659
default_value,
625660
merged_context,
626661
)
662+
if err := flag_evaluation.get_exception():
663+
error_hooks(
664+
flag_type, hook_context, err, reversed_merged_hooks, hook_hints
665+
)
666+
flag_evaluation.value = default_value
667+
return flag_evaluation
627668

628669
after_hooks(
629670
flag_type,
@@ -693,27 +734,33 @@ async def _create_provider_evaluation_async(
693734
}
694735
get_details_callable = get_details_callables_async.get(flag_type)
695736
if not get_details_callable:
696-
raise GeneralError(error_message="Unknown flag type")
737+
return FlagEvaluationDetails(
738+
flag_key=flag_key,
739+
value=default_value,
740+
reason=Reason.ERROR,
741+
error_code=ErrorCode.GENERAL,
742+
error_message="Unknown flag type",
743+
)
697744

698745
resolution = await get_details_callable(
699746
flag_key=flag_key,
700747
default_value=default_value,
701748
evaluation_context=evaluation_context,
702749
)
703-
resolution.raise_for_error()
750+
if resolution.error_code:
751+
return resolution.to_flag_evaluation_details(flag_key)
704752

705753
# we need to check the get_args to be compatible with union types.
706-
_typecheck_flag_value(resolution.value, flag_type)
754+
if err := _typecheck_flag_value(value=resolution.value, flag_type=flag_type):
755+
return FlagEvaluationDetails(
756+
flag_key=flag_key,
757+
value=resolution.value,
758+
reason=Reason.ERROR,
759+
error_code=err.error_code,
760+
error_message=err.error_message,
761+
)
707762

708-
return FlagEvaluationDetails(
709-
flag_key=flag_key,
710-
value=resolution.value,
711-
variant=resolution.variant,
712-
flag_metadata=resolution.flag_metadata or {},
713-
reason=resolution.reason,
714-
error_code=resolution.error_code,
715-
error_message=resolution.error_message,
716-
)
763+
return resolution.to_flag_evaluation_details(flag_key)
717764

718765
def _create_provider_evaluation(
719766
self,
@@ -743,27 +790,33 @@ def _create_provider_evaluation(
743790

744791
get_details_callable = get_details_callables.get(flag_type)
745792
if not get_details_callable:
746-
raise GeneralError(error_message="Unknown flag type")
793+
return FlagEvaluationDetails(
794+
flag_key=flag_key,
795+
value=default_value,
796+
reason=Reason.ERROR,
797+
error_code=ErrorCode.GENERAL,
798+
error_message="Unknown flag type",
799+
)
747800

748801
resolution = get_details_callable(
749802
flag_key=flag_key,
750803
default_value=default_value,
751804
evaluation_context=evaluation_context,
752805
)
753-
resolution.raise_for_error()
806+
if resolution.error_code:
807+
return resolution.to_flag_evaluation_details(flag_key)
754808

755809
# we need to check the get_args to be compatible with union types.
756-
_typecheck_flag_value(resolution.value, flag_type)
810+
if err := _typecheck_flag_value(value=resolution.value, flag_type=flag_type):
811+
return FlagEvaluationDetails(
812+
flag_key=flag_key,
813+
value=resolution.value,
814+
reason=Reason.ERROR,
815+
error_code=err.error_code,
816+
error_message=err.error_message,
817+
)
757818

758-
return FlagEvaluationDetails(
759-
flag_key=flag_key,
760-
value=resolution.value,
761-
variant=resolution.variant,
762-
flag_metadata=resolution.flag_metadata or {},
763-
reason=resolution.reason,
764-
error_code=resolution.error_code,
765-
error_message=resolution.error_message,
766-
)
819+
return resolution.to_flag_evaluation_details(flag_key)
767820

768821
def add_handler(self, event: ProviderEvent, handler: EventHandler) -> None:
769822
_event_support.add_client_handler(self, event, handler)
@@ -772,7 +825,9 @@ def remove_handler(self, event: ProviderEvent, handler: EventHandler) -> None:
772825
_event_support.remove_client_handler(self, event, handler)
773826

774827

775-
def _typecheck_flag_value(value: typing.Any, flag_type: FlagType) -> None:
828+
def _typecheck_flag_value(
829+
value: typing.Any, flag_type: FlagType
830+
) -> typing.Optional[OpenFeatureError]:
776831
type_map: TypeMap = {
777832
FlagType.BOOLEAN: bool,
778833
FlagType.STRING: str,
@@ -782,6 +837,7 @@ def _typecheck_flag_value(value: typing.Any, flag_type: FlagType) -> None:
782837
}
783838
_type = type_map.get(flag_type)
784839
if not _type:
785-
raise GeneralError(error_message="Unknown flag type")
840+
return GeneralError(error_message="Unknown flag type")
786841
if not isinstance(value, _type):
787-
raise TypeMismatchError(f"Expected type {_type} but got {type(value)}")
842+
return TypeMismatchError(f"Expected type {_type} but got {type(value)}")
843+
return None

openfeature/flag_evaluation.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from dataclasses import dataclass, field
55

66
from openfeature._backports.strenum import StrEnum
7-
from openfeature.exception import ErrorCode
7+
from openfeature.exception import ErrorCode, OpenFeatureError
88

99
if typing.TYPE_CHECKING: # pragma: no cover
1010
# resolves a circular dependency in type annotations
@@ -56,6 +56,11 @@ class FlagEvaluationDetails(typing.Generic[T_co]):
5656
error_code: typing.Optional[ErrorCode] = None
5757
error_message: typing.Optional[str] = None
5858

59+
def get_exception(self) -> typing.Optional[OpenFeatureError]:
60+
if self.error_code:
61+
return ErrorCode.to_exception(self.error_code, self.error_message or "")
62+
return None
63+
5964

6065
@dataclass
6166
class FlagEvaluationOptions:
@@ -79,3 +84,14 @@ def raise_for_error(self) -> None:
7984
if self.error_code:
8085
raise ErrorCode.to_exception(self.error_code, self.error_message or "")
8186
return None
87+
88+
def to_flag_evaluation_details(self, flag_key: str) -> FlagEvaluationDetails[U_co]:
89+
return FlagEvaluationDetails(
90+
flag_key=flag_key,
91+
value=self.value,
92+
variant=self.variant,
93+
flag_metadata=self.flag_metadata,
94+
reason=self.reason,
95+
error_code=self.error_code,
96+
error_message=self.error_message,
97+
)

openfeature/provider/in_memory_provider.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from openfeature._backports.strenum import StrEnum
55
from openfeature.evaluation_context import EvaluationContext
6-
from openfeature.exception import FlagNotFoundError
6+
from openfeature.exception import ErrorCode
77
from openfeature.flag_evaluation import FlagMetadata, FlagResolutionDetails, Reason
88
from openfeature.hook import Hook
99
from openfeature.provider import AbstractProvider, Metadata
@@ -74,93 +74,100 @@ def resolve_boolean_details(
7474
default_value: bool,
7575
evaluation_context: typing.Optional[EvaluationContext] = None,
7676
) -> FlagResolutionDetails[bool]:
77-
return self._resolve(flag_key, evaluation_context)
77+
return self._resolve(flag_key, default_value, evaluation_context)
7878

7979
async def resolve_boolean_details_async(
8080
self,
8181
flag_key: str,
8282
default_value: bool,
8383
evaluation_context: typing.Optional[EvaluationContext] = None,
8484
) -> FlagResolutionDetails[bool]:
85-
return await self._resolve_async(flag_key, evaluation_context)
85+
return await self._resolve_async(flag_key, default_value, evaluation_context)
8686

8787
def resolve_string_details(
8888
self,
8989
flag_key: str,
9090
default_value: str,
9191
evaluation_context: typing.Optional[EvaluationContext] = None,
9292
) -> FlagResolutionDetails[str]:
93-
return self._resolve(flag_key, evaluation_context)
93+
return self._resolve(flag_key, default_value, evaluation_context)
9494

9595
async def resolve_string_details_async(
9696
self,
9797
flag_key: str,
9898
default_value: str,
9999
evaluation_context: typing.Optional[EvaluationContext] = None,
100100
) -> FlagResolutionDetails[str]:
101-
return await self._resolve_async(flag_key, evaluation_context)
101+
return await self._resolve_async(flag_key, default_value, evaluation_context)
102102

103103
def resolve_integer_details(
104104
self,
105105
flag_key: str,
106106
default_value: int,
107107
evaluation_context: typing.Optional[EvaluationContext] = None,
108108
) -> FlagResolutionDetails[int]:
109-
return self._resolve(flag_key, evaluation_context)
109+
return self._resolve(flag_key, default_value, evaluation_context)
110110

111111
async def resolve_integer_details_async(
112112
self,
113113
flag_key: str,
114114
default_value: int,
115115
evaluation_context: typing.Optional[EvaluationContext] = None,
116116
) -> FlagResolutionDetails[int]:
117-
return await self._resolve_async(flag_key, evaluation_context)
117+
return await self._resolve_async(flag_key, default_value, evaluation_context)
118118

119119
def resolve_float_details(
120120
self,
121121
flag_key: str,
122122
default_value: float,
123123
evaluation_context: typing.Optional[EvaluationContext] = None,
124124
) -> FlagResolutionDetails[float]:
125-
return self._resolve(flag_key, evaluation_context)
125+
return self._resolve(flag_key, default_value, evaluation_context)
126126

127127
async def resolve_float_details_async(
128128
self,
129129
flag_key: str,
130130
default_value: float,
131131
evaluation_context: typing.Optional[EvaluationContext] = None,
132132
) -> FlagResolutionDetails[float]:
133-
return await self._resolve_async(flag_key, evaluation_context)
133+
return await self._resolve_async(flag_key, default_value, evaluation_context)
134134

135135
def resolve_object_details(
136136
self,
137137
flag_key: str,
138138
default_value: typing.Union[dict, list],
139139
evaluation_context: typing.Optional[EvaluationContext] = None,
140140
) -> FlagResolutionDetails[typing.Union[dict, list]]:
141-
return self._resolve(flag_key, evaluation_context)
141+
return self._resolve(flag_key, default_value, evaluation_context)
142142

143143
async def resolve_object_details_async(
144144
self,
145145
flag_key: str,
146146
default_value: typing.Union[dict, list],
147147
evaluation_context: typing.Optional[EvaluationContext] = None,
148148
) -> FlagResolutionDetails[typing.Union[dict, list]]:
149-
return await self._resolve_async(flag_key, evaluation_context)
149+
return await self._resolve_async(flag_key, default_value, evaluation_context)
150150

151151
def _resolve(
152152
self,
153153
flag_key: str,
154+
default_value: V,
154155
evaluation_context: typing.Optional[EvaluationContext],
155156
) -> FlagResolutionDetails[V]:
156157
flag = self._flags.get(flag_key)
157158
if flag is None:
158-
raise FlagNotFoundError(f"Flag '{flag_key}' not found")
159+
return FlagResolutionDetails(
160+
value=default_value,
161+
reason=Reason.ERROR,
162+
error_code=ErrorCode.FLAG_NOT_FOUND,
163+
error_message=f"Flag '{flag_key}' not found",
164+
)
159165
return flag.resolve(evaluation_context)
160166

161167
async def _resolve_async(
162168
self,
163169
flag_key: str,
170+
default_value: V,
164171
evaluation_context: typing.Optional[EvaluationContext],
165172
) -> FlagResolutionDetails[V]:
166-
return self._resolve(flag_key, evaluation_context)
173+
return self._resolve(flag_key, default_value, evaluation_context)

tests/provider/test_in_memory_provider.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from openfeature.exception import FlagNotFoundError
5+
from openfeature.exception import ErrorCode
66
from openfeature.flag_evaluation import FlagResolutionDetails, Reason
77
from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider
88

@@ -22,11 +22,18 @@ async def test_should_handle_unknown_flags_correctly():
2222
# Given
2323
provider = InMemoryProvider({})
2424
# When
25-
with pytest.raises(FlagNotFoundError):
26-
provider.resolve_boolean_details(flag_key="Key", default_value=True)
27-
with pytest.raises(FlagNotFoundError):
28-
await provider.resolve_integer_details_async(flag_key="Key", default_value=1)
25+
flag_sync = provider.resolve_boolean_details(flag_key="Key", default_value=True)
26+
flag_async = await provider.resolve_boolean_details_async(
27+
flag_key="Key", default_value=True
28+
)
2929
# Then
30+
assert flag_sync == flag_async
31+
for flag in [flag_sync, flag_async]:
32+
assert flag is not None
33+
assert flag.value is True
34+
assert flag.reason == Reason.ERROR
35+
assert flag.error_code == ErrorCode.FLAG_NOT_FOUND
36+
assert flag.error_message == "Flag 'Key' not found"
3037

3138

3239
@pytest.mark.asyncio

0 commit comments

Comments
 (0)