From 3cbebec9e713fb5b52137438f3a6be06a26fd622 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Fri, 8 Nov 2024 21:57:09 -0800 Subject: [PATCH 1/2] add typed dict type checking --- reflex/components/component.py | 20 ++++++++++++-- reflex/utils/types.py | 48 +++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/reflex/components/component.py b/reflex/components/component.py index face5d557f..dd623857a3 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -186,6 +186,23 @@ def evaluate_style_namespaces(style: ComponentStyle) -> dict: ComponentChild = Union[types.PrimitiveType, Var, BaseComponent] +def satisfies_type_hint(obj: Any, type_hint: Any) -> bool: + """Check if an object satisfies a type hint. + + Args: + obj: The object to check. + type_hint: The type hint to check against. + + Returns: + Whether the object satisfies the type hint. + """ + if isinstance(obj, LiteralVar): + return types._isinstance(obj._var_value, type_hint) + if isinstance(obj, Var): + return types._issubclass(obj._var_type, type_hint) + return types._isinstance(obj, type_hint) + + class Component(BaseComponent, ABC): """A component with style, event trigger and other props.""" @@ -460,8 +477,7 @@ def __init__(self, *args, **kwargs): ) ) or ( # Else just check if the passed var type is valid. - not passed_types - and not types._issubclass(passed_type, expected_type, value) + not passed_types and not satisfies_type_hint(value, expected_type) ): value_name = value._js_expr if isinstance(value, Var) else value diff --git a/reflex/utils/types.py b/reflex/utils/types.py index 27b6e7ce7f..0b51cb4327 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -14,6 +14,7 @@ Callable, ClassVar, Dict, + FrozenSet, Iterable, List, Literal, @@ -29,6 +30,7 @@ from typing import get_origin as get_origin_og import sqlalchemy +from typing_extensions import is_typeddict import reflex from reflex.components.core.breakpoints import Breakpoints @@ -494,6 +496,14 @@ def _issubclass(cls: GenericType, cls_check: GenericType, instance: Any = None) if isinstance(instance, Breakpoints): return _breakpoints_satisfies_typing(cls_check, instance) + if isinstance(cls_check_base, tuple): + cls_check_base = tuple( + cls_check_one if not is_typeddict(cls_check_one) else dict + for cls_check_one in cls_check_base + ) + if is_typeddict(cls_check_base): + cls_check_base = dict + # Check if the types match. try: return cls_check_base == Any or issubclass(cls_base, cls_check_base) @@ -503,6 +513,36 @@ def _issubclass(cls: GenericType, cls_check: GenericType, instance: Any = None) raise TypeError(f"Invalid type for issubclass: {cls_base}") from te +def does_obj_satisfy_typed_dict(obj: Any, cls: GenericType) -> bool: + """Check if an object satisfies a typed dict. + + Args: + obj: The object to check. + cls: The typed dict to check against. + + Returns: + Whether the object satisfies the typed dict. + """ + if not isinstance(obj, dict): + return False + + key_names_to_values = get_type_hints(cls) + required_keys: FrozenSet[str] = getattr(cls, "__required_keys__", frozenset()) + + if not all( + isinstance(key, str) + and key in key_names_to_values + and _isinstance(value, key_names_to_values[key]) + for key, value in obj.items() + ): + return False + + # TODO in 3.14: Implement https://peps.python.org/pep-0728/ if it's approved + + # required keys are all present + return required_keys.issubset(required_keys) + + def _isinstance(obj: Any, cls: GenericType, nested: bool = False) -> bool: """Check if an object is an instance of a class. @@ -529,6 +569,12 @@ def _isinstance(obj: Any, cls: GenericType, nested: bool = False) -> bool: origin = get_origin(cls) if origin is None: + # cls is a typed dict + if is_typeddict(cls): + if nested: + return does_obj_satisfy_typed_dict(obj, cls) + return isinstance(obj, dict) + # cls is a simple class return isinstance(obj, cls) @@ -553,7 +599,7 @@ def _isinstance(obj: Any, cls: GenericType, nested: bool = False) -> bool: and len(obj) == len(args) and all(_isinstance(item, arg) for item, arg in zip(obj, args)) ) - if origin is dict: + if origin in (dict, Breakpoints): return isinstance(obj, dict) and all( _isinstance(key, args[0]) and _isinstance(value, args[1]) for key, value in obj.items() From c6c56f316925d54a519487b087e7ac9e99b3ee8e Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 11 Nov 2024 13:06:25 -0800 Subject: [PATCH 2/2] technically it has to be a mapping, not specifically a dict --- reflex/utils/types.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/reflex/utils/types.py b/reflex/utils/types.py index 0b51cb4327..9361c1ce4a 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -18,6 +18,7 @@ Iterable, List, Literal, + Mapping, Optional, Sequence, Tuple, @@ -523,7 +524,7 @@ def does_obj_satisfy_typed_dict(obj: Any, cls: GenericType) -> bool: Returns: Whether the object satisfies the typed dict. """ - if not isinstance(obj, dict): + if not isinstance(obj, Mapping): return False key_names_to_values = get_type_hints(cls)