Skip to content

Commit 7c264ab

Browse files
authored
Merge branch 'main' into hook-no-abstractmethod
2 parents 91f9858 + 84af1ae commit 7c264ab

File tree

8 files changed

+89
-5
lines changed

8 files changed

+89
-5
lines changed

openfeature/api.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ def set_provider(provider: AbstractProvider):
2525
global _provider
2626
if provider is None:
2727
raise GeneralError(error_message="No provider")
28+
if _provider:
29+
_provider.shutdown()
2830
_provider = provider
31+
provider.initialize(_evaluation_context)
2932

3033

3134
def get_provider() -> typing.Optional[AbstractProvider]:
@@ -63,3 +66,7 @@ def clear_hooks():
6366
def get_hooks() -> typing.List[Hook]:
6467
global _hooks
6568
return _hooks
69+
70+
71+
def shutdown():
72+
_provider.shutdown()

openfeature/client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ def _create_provider_evaluation(
377377
flag_key=flag_key,
378378
value=resolution.value,
379379
variant=resolution.variant,
380+
flag_metadata=resolution.flag_metadata or {},
380381
reason=resolution.reason,
381382
error_code=resolution.error_code,
382383
error_message=resolution.error_message,

openfeature/flag_evaluation.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class Reason(StrEnum):
2929
UNKNOWN = "UNKNOWN"
3030

3131

32+
FlagMetadata = typing.Mapping[str, typing.Any]
33+
3234
T = typing.TypeVar("T", covariant=True)
3335

3436

@@ -37,6 +39,7 @@ class FlagEvaluationDetails(typing.Generic[T]):
3739
flag_key: str
3840
value: T
3941
variant: typing.Optional[str] = None
42+
flag_metadata: FlagMetadata = field(default_factory=dict)
4043
reason: typing.Optional[Reason] = None
4144
error_code: typing.Optional[ErrorCode] = None
4245
error_message: typing.Optional[str] = None
@@ -58,4 +61,4 @@ class FlagResolutionDetails(typing.Generic[U]):
5861
error_message: typing.Optional[str] = None
5962
reason: typing.Optional[Reason] = None
6063
variant: typing.Optional[str] = None
61-
flag_metadata: typing.Optional[str] = None
64+
flag_metadata: FlagMetadata = field(default_factory=dict)

openfeature/provider/in_memory_provider.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import typing
2-
from dataclasses import dataclass
2+
from dataclasses import dataclass, field
33

44
from openfeature._backports.strenum import StrEnum
55
from openfeature.evaluation_context import EvaluationContext
66
from openfeature.exception import ErrorCode
7-
from openfeature.flag_evaluation import FlagResolutionDetails, Reason
7+
from openfeature.flag_evaluation import FlagMetadata, FlagResolutionDetails, Reason
88
from openfeature.hook import Hook
99
from openfeature.provider.metadata import Metadata
1010
from openfeature.provider.provider import AbstractProvider
@@ -29,6 +29,7 @@ class State(StrEnum):
2929
flag_key: str
3030
default_variant: str
3131
variants: typing.Dict[str, T]
32+
flag_metadata: FlagMetadata = field(default_factory=dict)
3233
state: State = State.ENABLED
3334
context_evaluator: typing.Optional[
3435
typing.Callable[["InMemoryFlag", EvaluationContext], FlagResolutionDetails[T]]
@@ -46,6 +47,7 @@ def resolve(
4647
value=self.variants[self.default_variant],
4748
reason=Reason.STATIC,
4849
variant=self.default_variant,
50+
flag_metadata=self.flag_metadata,
4951
)
5052

5153

openfeature/provider/provider.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88

99

1010
class AbstractProvider:
11+
def initialize(self, evaluation_context: EvaluationContext):
12+
pass
13+
14+
def shutdown(self):
15+
pass
16+
1117
@abstractmethod
1218
def get_metadata(self) -> Metadata:
1319
pass

tests/test_api.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
get_provider_metadata,
1313
set_evaluation_context,
1414
set_provider,
15+
shutdown,
1516
)
1617
from openfeature.evaluation_context import EvaluationContext
1718
from openfeature.exception import ErrorCode, GeneralError
1819
from openfeature.hook import Hook
1920
from openfeature.provider.metadata import Metadata
2021
from openfeature.provider.no_op_provider import NoOpProvider
22+
from openfeature.provider.provider import AbstractProvider
2123

2224

2325
def test_should_not_raise_exception_with_noop_client():
@@ -56,6 +58,32 @@ def test_should_try_set_provider_and_fail_if_none_provided():
5658
assert ge.value.error_code == ErrorCode.GENERAL
5759

5860

61+
def test_should_invoke_provider_initialize_function_on_newly_registered_provider():
62+
# Given
63+
evaluation_context = EvaluationContext("targeting_key", {"attr1": "val1"})
64+
provider = MagicMock(spec=AbstractProvider)
65+
66+
# When
67+
set_evaluation_context(evaluation_context)
68+
set_provider(provider)
69+
70+
# Then
71+
provider.initialize.assert_called_with(evaluation_context)
72+
73+
74+
def test_should_invoke_provider_shutdown_function_once_provider_is_no_longer_in_use():
75+
# Given
76+
provider_1 = MagicMock(spec=AbstractProvider)
77+
provider_2 = MagicMock(spec=AbstractProvider)
78+
79+
# When
80+
set_provider(provider_1)
81+
set_provider(provider_2)
82+
83+
# Then
84+
assert provider_1.shutdown.called
85+
86+
5987
def test_should_return_a_provider_if_setup_correctly():
6088
# Given
6189
set_provider(NoOpProvider())
@@ -116,3 +144,15 @@ def test_should_add_hooks_to_api_hooks():
116144

117145
# Then
118146
assert get_hooks() == [hook_1, hook_2]
147+
148+
149+
def test_should_call_provider_shutdown_on_api_shutdown():
150+
# Given
151+
provider = MagicMock(spec=AbstractProvider)
152+
set_provider(provider)
153+
154+
# When
155+
shutdown()
156+
157+
# Then
158+
assert provider.shutdown.called

tests/test_client.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from openfeature.exception import ErrorCode, OpenFeatureError
88
from openfeature.flag_evaluation import Reason
99
from openfeature.hook import Hook
10+
from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider
1011
from openfeature.provider.no_op_provider import NoOpProvider
1112

1213

@@ -97,6 +98,28 @@ def test_should_raise_exception_when_invalid_flag_type_provided(no_op_provider_c
9798
assert flag.reason == Reason.ERROR
9899

99100

101+
def test_should_pass_flag_metadata_from_resolution_to_evaluation_details():
102+
# Given
103+
provider = InMemoryProvider(
104+
{
105+
"Key": InMemoryFlag(
106+
"Key",
107+
"true",
108+
{"true": True, "false": False},
109+
flag_metadata={"foo": "bar"},
110+
)
111+
}
112+
)
113+
client = OpenFeatureClient("my-client", None, provider)
114+
115+
# When
116+
details = client.get_boolean_details(flag_key="Key", default_value=False)
117+
118+
# Then
119+
assert details is not None
120+
assert details.flag_metadata == {"foo": "bar"}
121+
122+
100123
def test_should_handle_a_generic_exception_thrown_by_a_provider(no_op_provider_client):
101124
# Given
102125
exception_hook = MagicMock(spec=Hook)

tests/test_flag_evaluation.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
from openfeature.flag_evaluation import FlagEvaluationDetails, Reason
33

44

5-
def test_evaulation_details_reason_should_be_a_string():
5+
def test_evaluation_details_reason_should_be_a_string():
66
# Given
77
flag_key = "my-flag"
88
flag_value = 100
99
variant = "1-hundred"
10+
flag_metadata = {}
1011
reason = Reason.DEFAULT
1112
error_code = ErrorCode.GENERAL
1213
error_message = "message"
@@ -16,6 +17,7 @@ def test_evaulation_details_reason_should_be_a_string():
1617
flag_key,
1718
flag_value,
1819
variant,
20+
flag_metadata,
1921
reason,
2022
error_code,
2123
error_message,
@@ -30,7 +32,7 @@ def test_evaulation_details_reason_should_be_a_string():
3032
assert reason == flag_details.reason
3133

3234

33-
def test_evaulation_details_reason_should_be_a_string_when_set():
35+
def test_evaluation_details_reason_should_be_a_string_when_set():
3436
# Given
3537
flag_key = "my-flag"
3638
flag_value = 100

0 commit comments

Comments
 (0)