Skip to content

Commit 1e2e89d

Browse files
committed
docs: explain flag type validation and fallback behavior
Document the boolean/integer distinction, async type-mismatch fallback, and the regression cases. Keep the evaluation logic and test assertions unchanged. Signed-off-by: Hexecu <vaingloryhex@gmail.com>
1 parent 71fe148 commit 1e2e89d

2 files changed

Lines changed: 32 additions & 1 deletion

File tree

openfeature/client.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,19 @@ async def _create_provider_evaluation_async(
861861
default_value: FlagValueType,
862862
evaluation_context: EvaluationContext | None = None,
863863
) -> FlagEvaluationDetails[FlagValueType]:
864+
"""
865+
Resolve a flag asynchronously and validate the returned value.
866+
867+
Provider-reported errors are returned unchanged. A type mismatch detected
868+
by the client returns the caller's default with TYPE_MISMATCH error details.
869+
870+
:param provider: the provider selected for this evaluation
871+
:param flag_type: the requested flag type
872+
:param flag_key: the key of the selected flag
873+
:param default_value: fallback used for an unknown type or a type mismatch
874+
:param evaluation_context: context passed to the provider
875+
:return: evaluation details containing the resolved value or fallback
876+
"""
864877
get_details_callables_async: Mapping[FlagType, ResolveDetailsCallableAsync] = {
865878
FlagType.BOOLEAN: provider.resolve_boolean_details_async,
866879
FlagType.INTEGER: provider.resolve_integer_details_async,
@@ -991,6 +1004,17 @@ def track(
9911004
def _typecheck_flag_value(
9921005
value: typing.Any, flag_type: FlagType
9931006
) -> OpenFeatureError | None:
1007+
"""
1008+
Check a resolved value against the requested flag type without coercing it.
1009+
1010+
Booleans are not integer flag values, even though bool subclasses int in
1011+
Python. Other subclasses of the expected type remain valid.
1012+
1013+
:param value: the value returned by the provider
1014+
:param flag_type: the requested flag type
1015+
:return: None for a matching value, TypeMismatchError for an incompatible
1016+
value, or GeneralError for an unknown flag type
1017+
"""
9941018
type_map: TypeMap = {
9951019
FlagType.BOOLEAN: bool,
9961020
FlagType.STRING: str,
@@ -1001,7 +1025,6 @@ def _typecheck_flag_value(
10011025
py_type = type_map.get(flag_type)
10021026
if not py_type:
10031027
return GeneralError(error_message="Unknown flag type")
1004-
# bool is an int subclass in Python, but not an integer flag value.
10051028
if not isinstance(value, py_type) or (
10061029
flag_type == FlagType.INTEGER and isinstance(value, bool)
10071030
):

tests/test_client.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,7 @@ async def test_client_type_mismatch_exceptions():
424424

425425
@pytest.mark.asyncio
426426
async def test_typecheck_flag_value_general_error():
427+
"""Unknown flag types produce GENERAL rather than TYPE_MISMATCH."""
427428
# Given
428429
flag_value = "A"
429430
flag_type = None
@@ -453,6 +454,7 @@ async def test_typecheck_flag_value_general_error():
453454
async def test_client_returns_default_on_type_mismatch(
454455
flag_type, flag_value, default_value, is_async
455456
):
457+
"""Both APIs return typed defaults and pass the fallback to finally hooks."""
456458
provider = InMemoryProvider(
457459
{"flag": InMemoryFlag("enabled", {"enabled": flag_value})}
458460
)
@@ -518,6 +520,7 @@ async def test_client_returns_default_on_type_mismatch(
518520
async def test_client_preserves_matching_flag_types(
519521
flag_type, flag_value, default_value, is_async
520522
):
523+
"""Matching values keep their type and successful evaluation details."""
521524
provider = InMemoryProvider(
522525
{"flag": InMemoryFlag("enabled", {"enabled": flag_value})}
523526
)
@@ -545,14 +548,19 @@ async def test_client_preserves_matching_flag_types(
545548

546549

547550
def test_typecheck_flag_value_accepts_integer_subclasses():
551+
"""Rejecting booleans must not reject other integer subclasses."""
552+
548553
class IntegerValue(int):
554+
"""An integer subtype with the same value semantics as int."""
555+
549556
pass
550557

551558
assert _typecheck_flag_value(IntegerValue(1), FlagType.INTEGER) is None
552559

553560

554561
@pytest.mark.asyncio
555562
async def test_typecheck_flag_value_type_mismatch_error():
563+
"""An incompatible value reports both the expected and actual types."""
556564
# Given
557565
flag_value = "A"
558566
flag_type = FlagType.BOOLEAN

0 commit comments

Comments
 (0)