Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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.18.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]