From 0192d15ad2747d8a3c692212c8d24ae06e3a6e1e Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Wed, 18 Oct 2023 11:04:05 -0300 Subject: [PATCH 1/4] fix: Hook methods should have default non-abstract implementations Signed-off-by: Federico Bond --- openfeature/hook/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openfeature/hook/__init__.py b/openfeature/hook/__init__.py index 5ca695a6..5bcdb4b8 100644 --- a/openfeature/hook/__init__.py +++ b/openfeature/hook/__init__.py @@ -1,7 +1,6 @@ from __future__ import annotations import typing -from abc import abstractmethod from dataclasses import dataclass from enum import Enum @@ -27,7 +26,6 @@ class HookContext: class Hook: - @abstractmethod def before(self, hook_context: HookContext, hints: dict) -> EvaluationContext: """ Runs before flag is resolved. @@ -40,7 +38,6 @@ def before(self, hook_context: HookContext, hints: dict) -> EvaluationContext: """ pass - @abstractmethod def after( self, hook_context: HookContext, details: FlagEvaluationDetails, hints: dict ): @@ -54,7 +51,6 @@ def after( """ pass - @abstractmethod def error(self, hook_context: HookContext, exception: Exception, hints: dict): """ Run when evaluation encounters an error. Errors thrown will be swallowed. @@ -65,7 +61,6 @@ def error(self, hook_context: HookContext, exception: Exception, hints: dict): """ pass - @abstractmethod def finally_after(self, hook_context: HookContext, hints: dict): """ Run after flag evaluation, including any error processing. @@ -76,7 +71,6 @@ def finally_after(self, hook_context: HookContext, hints: dict): """ pass - @abstractmethod def supports_flag_value_type(self, flag_type: FlagType) -> bool: """ Check to see if the hook supports the particular flag type. From 322a9dcdee5e63d0b99b7c32b76f47a099063f61 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Wed, 18 Oct 2023 11:13:21 -0300 Subject: [PATCH 2/4] fix: use correct return type for Hook.before method Signed-off-by: Federico Bond --- openfeature/hook/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openfeature/hook/__init__.py b/openfeature/hook/__init__.py index 5bcdb4b8..428313dd 100644 --- a/openfeature/hook/__init__.py +++ b/openfeature/hook/__init__.py @@ -26,7 +26,9 @@ class HookContext: class Hook: - def before(self, hook_context: HookContext, hints: dict) -> EvaluationContext: + def before( + self, hook_context: HookContext, hints: dict + ) -> typing.Optional[EvaluationContext]: """ Runs before flag is resolved. @@ -36,7 +38,7 @@ def before(self, hook_context: HookContext, hints: dict) -> EvaluationContext: :return: An EvaluationContext. It will be merged with the EvaluationContext instances from other hooks, the client and API. """ - pass + return None def after( self, hook_context: HookContext, details: FlagEvaluationDetails, hints: dict From 23995686b931548ea682661918c8a84c4b036472 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Wed, 18 Oct 2023 11:56:50 -0300 Subject: [PATCH 3/4] feat: make EvaluationContext a dataclass Signed-off-by: Federico Bond --- openfeature/evaluation_context.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/openfeature/evaluation_context.py b/openfeature/evaluation_context.py index 64edf6d1..829ada63 100644 --- a/openfeature/evaluation_context.py +++ b/openfeature/evaluation_context.py @@ -1,14 +1,11 @@ import typing +from dataclasses import dataclass, field +@dataclass class EvaluationContext: - def __init__( - self, - targeting_key: typing.Optional[str] = None, - attributes: typing.Optional[dict] = None, - ): - self.targeting_key = targeting_key - self.attributes = attributes or {} + targeting_key: typing.Optional[str] = None + attributes: dict = field(default_factory=dict) def merge(self, ctx2: "EvaluationContext") -> "EvaluationContext": if not (self and ctx2): From 91f985828d94c9d223c29225cb1501fe46070937 Mon Sep 17 00:00:00 2001 From: Federico Bond Date: Wed, 18 Oct 2023 11:57:48 -0300 Subject: [PATCH 4/4] test: add unit test for evaluation context merging in before_hooks Signed-off-by: Federico Bond --- tests/hook/test_hook_support.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/hook/test_hook_support.py b/tests/hook/test_hook_support.py index 02cfed1c..c88d237d 100644 --- a/tests/hook/test_hook_support.py +++ b/tests/hook/test_hook_support.py @@ -1,7 +1,8 @@ -from unittest.mock import ANY +from unittest.mock import ANY, MagicMock +from openfeature.evaluation_context import EvaluationContext from openfeature.flag_evaluation import FlagEvaluationDetails, FlagType -from openfeature.hook import HookContext +from openfeature.hook import Hook, HookContext from openfeature.hook.hook_support import ( after_all_hooks, after_hooks, @@ -37,6 +38,23 @@ def test_before_hooks_run_before_method(mock_hook): mock_hook.before.assert_called_with(hook_context=hook_context, hints=hook_hints) +def test_before_hooks_merges_evaluation_contexts(): + # Given + hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, "") + hook_1 = MagicMock(spec=Hook) + hook_1.before.return_value = EvaluationContext("foo", {"key_1": "val_1"}) + hook_2 = MagicMock(spec=Hook) + hook_2.before.return_value = EvaluationContext("bar", {"key_2": "val_2"}) + hook_3 = MagicMock(spec=Hook) + hook_3.before.return_value = None + + # When + context = before_hooks(FlagType.BOOLEAN, hook_context, [hook_1, hook_2, hook_3]) + + # Then + assert context == EvaluationContext("bar", {"key_1": "val_1", "key_2": "val_2"}) + + def test_after_hooks_run_after_method(mock_hook): # Given hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, "")