Skip to content

Commit 9661561

Browse files
authored
fix problems with does_obj_satisfy_typed_dict (#5936)
1 parent 0976ece commit 9661561

File tree

2 files changed

+70
-14
lines changed

2 files changed

+70
-14
lines changed

reflex/utils/types.py

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -633,12 +633,22 @@ def _issubclass(cls: GenericType, cls_check: GenericType, instance: Any = None)
633633
raise TypeError(msg) from te
634634

635635

636-
def does_obj_satisfy_typed_dict(obj: Any, cls: GenericType) -> bool:
636+
def does_obj_satisfy_typed_dict(
637+
obj: Any,
638+
cls: GenericType,
639+
*,
640+
nested: int = 0,
641+
treat_var_as_type: bool = True,
642+
treat_mutable_obj_as_immutable: bool = False,
643+
) -> bool:
637644
"""Check if an object satisfies a typed dict.
638645
639646
Args:
640647
obj: The object to check.
641648
cls: The typed dict to check against.
649+
nested: How many levels deep to check.
650+
treat_var_as_type: Whether to treat Var as the type it represents, i.e. _var_type.
651+
treat_mutable_obj_as_immutable: Whether to treat mutable objects as immutable. Useful if a component declares a mutable object as a prop, but the value is not expected to change.
642652
643653
Returns:
644654
Whether the object satisfies the typed dict.
@@ -648,19 +658,35 @@ def does_obj_satisfy_typed_dict(obj: Any, cls: GenericType) -> bool:
648658

649659
key_names_to_values = get_type_hints(cls)
650660
required_keys: frozenset[str] = getattr(cls, "__required_keys__", frozenset())
661+
is_closed = getattr(cls, "__closed__", False)
662+
extra_items_type = getattr(cls, "__extra_items__", Any)
651663

652-
if not all(
653-
isinstance(key, str)
654-
and key in key_names_to_values
655-
and _isinstance(value, key_names_to_values[key])
656-
for key, value in obj.items()
657-
):
658-
return False
659-
660-
# TODO in 3.14: Implement https://peps.python.org/pep-0728/ if it's approved
664+
for key, value in obj.items():
665+
if is_closed and key not in key_names_to_values:
666+
return False
667+
if nested:
668+
if key in key_names_to_values:
669+
expected_type = key_names_to_values[key]
670+
if not _isinstance(
671+
value,
672+
expected_type,
673+
nested=nested - 1,
674+
treat_var_as_type=treat_var_as_type,
675+
treat_mutable_obj_as_immutable=treat_mutable_obj_as_immutable,
676+
):
677+
return False
678+
else:
679+
if not _isinstance(
680+
value,
681+
extra_items_type,
682+
nested=nested - 1,
683+
treat_var_as_type=treat_var_as_type,
684+
treat_mutable_obj_as_immutable=treat_mutable_obj_as_immutable,
685+
):
686+
return False
661687

662688
# required keys are all present
663-
return required_keys.issubset(required_keys)
689+
return required_keys.issubset(frozenset(obj))
664690

665691

666692
def _isinstance(
@@ -721,7 +747,13 @@ def _isinstance(
721747
# cls is a typed dict
722748
if is_typeddict(cls):
723749
if nested:
724-
return does_obj_satisfy_typed_dict(obj, cls)
750+
return does_obj_satisfy_typed_dict(
751+
obj,
752+
cls,
753+
nested=nested - 1,
754+
treat_var_as_type=treat_var_as_type,
755+
treat_mutable_obj_as_immutable=treat_mutable_obj_as_immutable,
756+
)
725757
return isinstance(obj, dict)
726758

727759
# cls is a float

tests/units/utils/test_types.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Literal
1+
from typing import Any, Literal, TypedDict
22

33
import pytest
44

@@ -85,6 +85,22 @@ def test_has_args(cls, expected: bool) -> None:
8585
assert types.has_args(cls) == expected
8686

8787

88+
class UserInfo(TypedDict, total=False):
89+
"""A sample typed dict."""
90+
91+
sub: str
92+
name: str
93+
email: str
94+
95+
96+
class UserInfoTotal(TypedDict, total=True):
97+
"""A sample typed dict."""
98+
99+
sub: str
100+
name: str
101+
email: str
102+
103+
88104
@pytest.mark.parametrize(
89105
("value", "cls", "expected"),
90106
[
@@ -109,7 +125,15 @@ def test_has_args(cls, expected: bool) -> None:
109125
(Var.create(False), Var[bool], True),
110126
(Var.create(False), Var[bool] | None, True),
111127
(Var.create(False), Var[bool] | str, True),
128+
({"sub": "123", "name": "John"}, UserInfo, True),
129+
({"sub": "123"}, UserInfo, True),
130+
({"sub": 123}, UserInfo, False),
131+
({"sub": "123", "age": 30}, UserInfo, True),
132+
({"sub": "123", "name": "John"}, UserInfoTotal, False),
133+
({"sub": "123"}, UserInfoTotal, False),
134+
({"sub": 123}, UserInfoTotal, False),
135+
({"sub": "123", "age": 30}, UserInfoTotal, False),
112136
],
113137
)
114138
def test_isinstance(value, cls, expected: bool) -> None:
115-
assert types._isinstance(value, cls, nested=1, treat_var_as_type=True) == expected
139+
assert types._isinstance(value, cls, nested=2, treat_var_as_type=True) == expected

0 commit comments

Comments
 (0)