refactor: improve Hook Hints typing (#285)

* improve Hook Hints typing

Signed-off-by: gruebel <anton.gruebel@gmail.com>

* ignore lint issue for this line

Signed-off-by: gruebel <anton.gruebel@gmail.com>

* exclude TYPE_CHECKING from coverage report

Signed-off-by: gruebel <anton.gruebel@gmail.com>

---------

Signed-off-by: gruebel <anton.gruebel@gmail.com>
This commit is contained in:
Anton Grübel 2024-03-03 04:38:14 +01:00 committed by GitHub
parent 141858d235
commit 5acd6a6598
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 61 additions and 25 deletions

View File

@ -6,8 +6,9 @@ from dataclasses import dataclass, field
from openfeature._backports.strenum import StrEnum
from openfeature.exception import ErrorCode
if typing.TYPE_CHECKING: # resolves a circular dependency in type annotations
from openfeature.hook import Hook
if typing.TYPE_CHECKING: # pragma: no cover
# resolves a circular dependency in type annotations
from openfeature.hook import Hook, HookHints
class FlagType(StrEnum):
@ -48,7 +49,7 @@ class FlagEvaluationDetails(typing.Generic[T_co]):
@dataclass
class FlagEvaluationOptions:
hooks: typing.List[Hook] = field(default_factory=list)
hook_hints: dict = field(default_factory=dict)
hook_hints: HookHints = field(default_factory=dict)
U_co = typing.TypeVar("U_co", covariant=True)

View File

@ -1,7 +1,7 @@
from __future__ import annotations
import typing
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import TYPE_CHECKING
@ -20,24 +20,53 @@ class HookType(Enum):
ERROR = "error"
@dataclass
class HookContext:
flag_key: str
flag_type: FlagType
default_value: typing.Any
evaluation_context: EvaluationContext
client_metadata: typing.Optional[ClientMetadata] = None
provider_metadata: typing.Optional[Metadata] = None
def __init__( # noqa: PLR0913
self,
flag_key: str,
flag_type: FlagType,
default_value: typing.Any,
evaluation_context: EvaluationContext,
client_metadata: typing.Optional[ClientMetadata] = None,
provider_metadata: typing.Optional[Metadata] = None,
):
self.flag_key = flag_key
self.flag_type = flag_type
self.default_value = default_value
self.evaluation_context = evaluation_context
self.client_metadata = client_metadata
self.provider_metadata = provider_metadata
def __setattr__(self, key: str, value: typing.Any) -> None:
if hasattr(self, key) and key in ("flag_key", "flag_type", "default_value"):
if hasattr(self, key) and key in (
"flag_key",
"flag_type",
"default_value",
"client_metadata",
"provider_metadata",
):
raise AttributeError(f"Attribute {key!r} is immutable")
super().__setattr__(key, value)
# https://openfeature.dev/specification/sections/hooks/#requirement-421
HookHints = typing.Mapping[
str,
typing.Union[
bool,
int,
float,
str,
datetime,
typing.List[typing.Any],
typing.Dict[str, typing.Any],
],
]
class Hook:
def before(
self, hook_context: HookContext, hints: dict
self, hook_context: HookContext, hints: HookHints
) -> typing.Optional[EvaluationContext]:
"""
Runs before flag is resolved.
@ -54,7 +83,7 @@ class Hook:
self,
hook_context: HookContext,
details: FlagEvaluationDetails[typing.Any],
hints: dict,
hints: HookHints,
) -> None:
"""
Runs after a flag is resolved.
@ -67,7 +96,7 @@ class Hook:
pass
def error(
self, hook_context: HookContext, exception: Exception, hints: dict
self, hook_context: HookContext, exception: Exception, hints: HookHints
) -> None:
"""
Run when evaluation encounters an error. Errors thrown will be swallowed.
@ -78,7 +107,7 @@ class Hook:
"""
pass
def finally_after(self, hook_context: HookContext, hints: dict) -> None:
def finally_after(self, hook_context: HookContext, hints: HookHints) -> None:
"""
Run after flag evaluation, including any error processing.
This will always run. Errors will be swallowed.

View File

@ -4,7 +4,7 @@ from functools import reduce
from openfeature.evaluation_context import EvaluationContext
from openfeature.flag_evaluation import FlagEvaluationDetails, FlagType
from openfeature.hook import Hook, HookContext, HookType
from openfeature.hook import Hook, HookContext, HookHints, HookType
logger = logging.getLogger("openfeature")
@ -14,7 +14,7 @@ def error_hooks(
hook_context: HookContext,
exception: Exception,
hooks: typing.List[Hook],
hints: typing.Optional[typing.Mapping] = None,
hints: typing.Optional[HookHints] = None,
) -> None:
kwargs = {"hook_context": hook_context, "exception": exception, "hints": hints}
_execute_hooks(
@ -26,7 +26,7 @@ def after_all_hooks(
flag_type: FlagType,
hook_context: HookContext,
hooks: typing.List[Hook],
hints: typing.Optional[typing.Mapping] = None,
hints: typing.Optional[HookHints] = None,
) -> None:
kwargs = {"hook_context": hook_context, "hints": hints}
_execute_hooks(
@ -39,7 +39,7 @@ def after_hooks(
hook_context: HookContext,
details: FlagEvaluationDetails[typing.Any],
hooks: typing.List[Hook],
hints: typing.Optional[typing.Mapping] = None,
hints: typing.Optional[HookHints] = None,
) -> None:
kwargs = {"hook_context": hook_context, "details": details, "hints": hints}
_execute_hooks_unchecked(
@ -51,7 +51,7 @@ def before_hooks(
flag_type: FlagType,
hook_context: HookContext,
hooks: typing.List[Hook],
hints: typing.Optional[typing.Mapping] = None,
hints: typing.Optional[HookHints] = None,
) -> EvaluationContext:
kwargs = {"hook_context": hook_context, "hints": hints}
executed_hooks = _execute_hooks_unchecked(

View File

@ -40,10 +40,14 @@ def test_hook_context_has_immutable_and_mutable_fields():
4.1.3 - The "flag key", "flag type", and "default value" properties MUST be immutable.
4.1.4.1 - The evaluation context MUST be mutable only within the before hook.
4.2.2.2 - The client "metadata" field in the "hook context" MUST be immutable.
4.2.2.3 - The provider "metadata" field in the "hook context" MUST be immutable.
"""
# Given
hook_context = HookContext("flag_key", FlagType.BOOLEAN, True, EvaluationContext())
hook_context = HookContext(
"flag_key", FlagType.BOOLEAN, True, EvaluationContext(), ClientMetadata("name")
)
# When
with pytest.raises(AttributeError):
@ -52,10 +56,12 @@ def test_hook_context_has_immutable_and_mutable_fields():
hook_context.flag_type = FlagType.STRING
with pytest.raises(AttributeError):
hook_context.default_value = "new_value"
with pytest.raises(AttributeError):
hook_context.client_metadata = ClientMetadata("new_name")
with pytest.raises(AttributeError):
hook_context.provider_metadata = Metadata("name")
hook_context.evaluation_context = EvaluationContext("targeting_key")
hook_context.client_metadata = ClientMetadata("name")
hook_context.provider_metadata = Metadata("name")
# Then
assert hook_context.flag_key == "flag_key"
@ -63,7 +69,7 @@ def test_hook_context_has_immutable_and_mutable_fields():
assert hook_context.default_value is True
assert hook_context.evaluation_context.targeting_key == "targeting_key"
assert hook_context.client_metadata.name == "name"
assert hook_context.provider_metadata.name == "name"
assert hook_context.provider_metadata is None
def test_error_hooks_run_error_method(mock_hook):