Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ repos:
rev: v1.17.1
hooks:
- id: mypy
files: openfeature
files: openfeature|tests/typechecking
21 changes: 10 additions & 11 deletions openfeature/evaluation_context/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,23 @@
__all__ = ["EvaluationContext", "get_evaluation_context", "set_evaluation_context"]

# https://openfeature.dev/specification/sections/evaluation-context#requirement-312
EvaluationContextAttributes = typing.Mapping[
EvaluationContextAttribute = typing.Union[
bool,
int,
float,
str,
typing.Union[
bool,
int,
float,
str,
datetime,
Sequence["EvaluationContextAttributes"],
"EvaluationContextAttributes",
],
datetime,
Sequence["EvaluationContextAttribute"],
typing.Mapping[str, "EvaluationContextAttribute"],
]


@dataclass
class EvaluationContext:
targeting_key: typing.Optional[str] = None
attributes: EvaluationContextAttributes = field(default_factory=dict)
attributes: typing.Mapping[str, EvaluationContextAttribute] = field(
default_factory=dict
)

def merge(self, ctx2: EvaluationContext) -> EvaluationContext:
if not (self and ctx2):
Expand Down
19 changes: 9 additions & 10 deletions openfeature/hook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,18 @@ def __setattr__(self, key: str, value: typing.Any) -> None:


# https://openfeature.dev/specification/sections/hooks/#requirement-421
HookHints = typing.Mapping[
HookHintValue = typing.Union[
bool,
int,
float,
str,
typing.Union[
bool,
int,
float,
str,
datetime,
Sequence["HookHints"],
typing.Mapping[str, "HookHints"],
],
datetime,
Sequence["HookHintValue"],
typing.Mapping[str, "HookHintValue"],
]

HookHints = typing.Mapping[str, HookHintValue]


class Hook:
def before(
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ module-root = ""
namespace = true

[tool.mypy]
files = "openfeature"
files = "openfeature,tests/typechecking"

python_version = "3.9" # should be identical to the minimum supported version
namespace_packages = true
Expand Down
8 changes: 8 additions & 0 deletions tests/typechecking/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Type Checking Tests

This directory contains type checking validation files that are designed to be checked by **type checkers only**, not executed by pytest.

## Purpose

These files validate that the type annotations work correctly through static analysis.
They ensure type safety for complex type definitions like `EvaluationContextAttribute` and other typed interfaces.
90 changes: 90 additions & 0 deletions tests/typechecking/evaluation_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Requirement 3.1.2"""

from datetime import datetime

from openfeature.evaluation_context import EvaluationContext

# positive
EvaluationContext(
targeting_key="key",
attributes={"bool": True},
)

EvaluationContext(
targeting_key="key",
attributes={"int": 42},
)

EvaluationContext(
targeting_key="key",
attributes={"float": 3.14},
)

EvaluationContext(
targeting_key="key",
attributes={"str": "value"},
)

EvaluationContext(
targeting_key="key",
attributes={"date": datetime.now()},
)

EvaluationContext(
targeting_key="key",
attributes={
"bool_list": [True, False],
"int_list": [1, 2],
"float_list": [1.1, 2.2],
"date_list": [datetime.now(), datetime.now()],
"str_list": ["a", "b"],
"list_list": [
["a", "b"],
["c", "d"],
],
"dict_list": [
{"int": 42},
{"str": "value"},
],
},
)

EvaluationContext(
targeting_key="key",
attributes={
"user": {
"id": 12345,
"name": "John Doe",
"active": True,
"last_login": datetime.now(),
"permissions": ["read", "write", "admin"],
"metadata": {
"source": "api",
"version": 2.1,
"tags": ["premium", "beta"],
"config": {
"nested_deeply": [
{"item": 1, "enabled": True},
{"item": 2, "enabled": False},
]
},
},
},
},
)

# negative
EvaluationContext(
targeting_key="key",
attributes={"null": None}, # type: ignore[dict-item]
)

EvaluationContext(
targeting_key="key",
attributes={"complex": -4.5j}, # type: ignore[dict-item]
)

EvaluationContext(
targeting_key="key",
attributes={42: 42}, # type: ignore[dict-item]
)
34 changes: 34 additions & 0 deletions tests/typechecking/hook_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Requirement 4.6.1"""

from openfeature.hook import HookData

# positive
any_hook_data: HookData = {
"user": {
"id": 12345,
"name": "John Doe",
"active": True,
"permissions": ["read", "write", "admin"],
"metadata": {
"source": "api",
"version": 2.1,
"tags": ["premium", "beta"],
"config": {
"nested_deeply": [
{"item": 1, "enabled": True},
{"item": 2, "enabled": False},
]
},
},
},
}


class ExampleClass:
pass


class_hook_data: HookData = {"example": ExampleClass}

# negative
int_key_hook_data: HookData = {42: 42} # type: ignore[dict-item]
60 changes: 60 additions & 0 deletions tests/typechecking/hook_hints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Requirement 4.1.2"""

from datetime import datetime

from openfeature.hook import HookHints

# positive
bool_hook_hint: HookHints = {"bool": True}

int_hook_hint: HookHints = {"int": 42}

float_hook_hint: HookHints = {"float": 3.14}

str_hook_hint: HookHints = {"str": "value"}

date_hook_hint: HookHints = {"date": datetime.now()}

list_hook_hint: HookHints = {
"bool_list": [True, False],
"int_list": [1, 2],
"float_list": [1.1, 2.2],
"date_list": [datetime.now(), datetime.now()],
"str_list": ["a", "b"],
"list_list": [
["a", "b"],
["c", "d"],
],
"dict_list": [
{"int": 42},
{"str": "value"},
],
}

dict_hook_hint: HookHints = {
"user": {
"id": 12345,
"name": "John Doe",
"active": True,
"last_login": datetime.now(),
"permissions": ["read", "write", "admin"],
"metadata": {
"source": "api",
"version": 2.1,
"tags": ["premium", "beta"],
"config": {
"nested_deeply": [
{"item": 1, "enabled": True},
{"item": 2, "enabled": False},
]
},
},
},
}

# negative
null_hook_hint: HookHints = {"null": None} # type: ignore[dict-item]

complex_hook_hint: HookHints = {"complex": -4.5j} # type: ignore[dict-item]

int_key_hook_hint: HookHints = {42: 42} # type: ignore[dict-item]