Skip to content

Commit f087c40

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix-imports
2 parents be669bd + e61b69b commit f087c40

9 files changed

Lines changed: 160 additions & 61 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,10 @@ jobs:
8080
python-version: "3.13"
8181

8282
- name: Initialize CodeQL
83-
uses: github/codeql-action/init@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3
83+
uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3
8484
with:
8585
languages: python
8686
config-file: ./.github/codeql-config.yml
8787

8888
- name: Perform CodeQL Analysis
89-
uses: github/codeql-action/analyze@1b549b9259bda1cb5ddde3b41741a82a2d15a841 # v3
89+
uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
default_stages: [pre-commit]
22
repos:
33
- repo: https://github.com/astral-sh/ruff-pre-commit
4-
rev: v0.11.3
4+
rev: v0.11.4
55
hooks:
66
- id: ruff
77
args: [--fix]

CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* @open-feature/sdk-python-maintainers @open-feature/maintainers

openfeature/client.py

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

445445
def _assert_provider_status(
446446
self,
447-
) -> None:
447+
) -> typing.Optional[OpenFeatureError]:
448448
status = self.get_provider_status()
449449
if status == ProviderStatus.NOT_READY:
450-
raise ProviderNotReadyError()
450+
return ProviderNotReadyError()
451451
if status == ProviderStatus.FATAL:
452-
raise ProviderFatalError()
452+
return ProviderFatalError()
453453
return None
454454

455455
def _before_hooks_and_merge_context(
@@ -509,7 +509,22 @@ async def evaluate_flag_details_async(
509509
)
510510

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

514529
merged_context = self._before_hooks_and_merge_context(
515530
flag_type,
@@ -526,6 +541,11 @@ async def evaluate_flag_details_async(
526541
default_value,
527542
merged_context,
528543
)
544+
if err := flag_evaluation.get_exception():
545+
error_hooks(
546+
flag_type, hook_context, err, reversed_merged_hooks, hook_hints
547+
)
548+
return flag_evaluation
529549

530550
after_hooks(
531551
flag_type,
@@ -605,7 +625,22 @@ def evaluate_flag_details(
605625
)
606626

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

610645
merged_context = self._before_hooks_and_merge_context(
611646
flag_type,
@@ -622,6 +657,12 @@ def evaluate_flag_details(
622657
default_value,
623658
merged_context,
624659
)
660+
if err := flag_evaluation.get_exception():
661+
error_hooks(
662+
flag_type, hook_context, err, reversed_merged_hooks, hook_hints
663+
)
664+
flag_evaluation.value = default_value
665+
return flag_evaluation
625666

626667
after_hooks(
627668
flag_type,
@@ -691,27 +732,33 @@ async def _create_provider_evaluation_async(
691732
}
692733
get_details_callable = get_details_callables_async.get(flag_type)
693734
if not get_details_callable:
694-
raise GeneralError(error_message="Unknown flag type")
735+
return FlagEvaluationDetails(
736+
flag_key=flag_key,
737+
value=default_value,
738+
reason=Reason.ERROR,
739+
error_code=ErrorCode.GENERAL,
740+
error_message="Unknown flag type",
741+
)
695742

696743
resolution = await get_details_callable(
697744
flag_key=flag_key,
698745
default_value=default_value,
699746
evaluation_context=evaluation_context,
700747
)
701-
resolution.raise_for_error()
748+
if resolution.error_code:
749+
return resolution.to_flag_evaluation_details(flag_key)
702750

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

706-
return FlagEvaluationDetails(
707-
flag_key=flag_key,
708-
value=resolution.value,
709-
variant=resolution.variant,
710-
flag_metadata=resolution.flag_metadata or {},
711-
reason=resolution.reason,
712-
error_code=resolution.error_code,
713-
error_message=resolution.error_message,
714-
)
761+
return resolution.to_flag_evaluation_details(flag_key)
715762

716763
def _create_provider_evaluation(
717764
self,
@@ -741,27 +788,33 @@ def _create_provider_evaluation(
741788

742789
get_details_callable = get_details_callables.get(flag_type)
743790
if not get_details_callable:
744-
raise GeneralError(error_message="Unknown flag type")
791+
return FlagEvaluationDetails(
792+
flag_key=flag_key,
793+
value=default_value,
794+
reason=Reason.ERROR,
795+
error_code=ErrorCode.GENERAL,
796+
error_message="Unknown flag type",
797+
)
745798

746799
resolution = get_details_callable(
747800
flag_key=flag_key,
748801
default_value=default_value,
749802
evaluation_context=evaluation_context,
750803
)
751-
resolution.raise_for_error()
804+
if resolution.error_code:
805+
return resolution.to_flag_evaluation_details(flag_key)
752806

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

756-
return FlagEvaluationDetails(
757-
flag_key=flag_key,
758-
value=resolution.value,
759-
variant=resolution.variant,
760-
flag_metadata=resolution.flag_metadata or {},
761-
reason=resolution.reason,
762-
error_code=resolution.error_code,
763-
error_message=resolution.error_message,
764-
)
817+
return resolution.to_flag_evaluation_details(flag_key)
765818

766819
def add_handler(self, event: ProviderEvent, handler: EventHandler) -> None:
767820
_event_support.add_client_handler(self, event, handler)
@@ -770,7 +823,9 @@ def remove_handler(self, event: ProviderEvent, handler: EventHandler) -> None:
770823
_event_support.remove_client_handler(self, event, handler)
771824

772825

773-
def _typecheck_flag_value(value: typing.Any, flag_type: FlagType) -> None:
826+
def _typecheck_flag_value(
827+
value: typing.Any, flag_type: FlagType
828+
) -> typing.Optional[OpenFeatureError]:
774829
type_map: TypeMap = {
775830
FlagType.BOOLEAN: bool,
776831
FlagType.STRING: str,
@@ -780,6 +835,7 @@ def _typecheck_flag_value(value: typing.Any, flag_type: FlagType) -> None:
780835
}
781836
_type = type_map.get(flag_type)
782837
if not _type:
783-
raise GeneralError(error_message="Unknown flag type")
838+
return GeneralError(error_message="Unknown flag type")
784839
if not isinstance(value, _type):
785-
raise TypeMismatchError(f"Expected type {_type} but got {type(value)}")
840+
return TypeMismatchError(f"Expected type {_type} but got {type(value)}")
841+
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)

0 commit comments

Comments
 (0)