Skip to content

Commit 1e07ec7

Browse files
authored
treat component prop class as immutable when provided a non literal var (#4898)
* treat component prop class as immutable when provided a non literal var * noreturn * don't treat component values as none by default * add a few tests * handle literals * treat_any_as_subtype_of_everything * NoReturn bcomes any * use value inside optional to avoid repeat calls * use safe_issubclass and handle union in sequences * handle tuples better * do smarter logic for value type in mapping
1 parent e8e02cb commit 1e07ec7

File tree

8 files changed

+375
-83
lines changed

8 files changed

+375
-83
lines changed

reflex/components/component.py

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
no_args_event_spec,
5252
)
5353
from reflex.style import Style, format_as_emotion
54-
from reflex.utils import format, imports, types
54+
from reflex.utils import console, format, imports, types
5555
from reflex.utils.imports import ImportDict, ImportVar, ParsedImportDict, parse_imports
5656
from reflex.vars import VarData
5757
from reflex.vars.base import (
@@ -177,6 +177,18 @@ def evaluate_style_namespaces(style: ComponentStyle) -> dict:
177177
ComponentChildTypes = (*types.PrimitiveTypes, Var, BaseComponent)
178178

179179

180+
def _satisfies_type_hint(obj: Any, type_hint: Any) -> bool:
181+
return types._isinstance(
182+
obj,
183+
type_hint,
184+
nested=1,
185+
treat_var_as_type=True,
186+
treat_mutable_obj_as_immutable=(
187+
isinstance(obj, Var) and not isinstance(obj, LiteralVar)
188+
),
189+
)
190+
191+
180192
def satisfies_type_hint(obj: Any, type_hint: Any) -> bool:
181193
"""Check if an object satisfies a type hint.
182194
@@ -187,7 +199,23 @@ def satisfies_type_hint(obj: Any, type_hint: Any) -> bool:
187199
Returns:
188200
Whether the object satisfies the type hint.
189201
"""
190-
return types._isinstance(obj, type_hint, nested=1, treat_var_as_type=True)
202+
if _satisfies_type_hint(obj, type_hint):
203+
return True
204+
if _satisfies_type_hint(obj, type_hint | None):
205+
obj = (
206+
obj
207+
if not isinstance(obj, Var)
208+
else (obj._var_value if isinstance(obj, LiteralVar) else obj)
209+
)
210+
console.deprecate(
211+
"implicit-none-for-component-fields",
212+
reason="Passing Vars with possible None values to component fields not explicitly marked as Optional is deprecated. "
213+
+ f"Passed {obj!s} of type {type(obj) if not isinstance(obj, Var) else obj._var_type} to {type_hint}.",
214+
deprecation_version="0.7.2",
215+
removal_version="0.8.0",
216+
)
217+
return True
218+
return False
191219

192220

193221
def _components_from(
@@ -466,8 +494,6 @@ def determine_key(value: Any):
466494

467495
# Check whether the key is a component prop.
468496
if types._issubclass(field_type, Var):
469-
# Used to store the passed types if var type is a union.
470-
passed_types = None
471497
try:
472498
kwargs[key] = determine_key(value)
473499

@@ -490,21 +516,8 @@ def determine_key(value: Any):
490516
# If it is not a valid var, check the base types.
491517
passed_type = type(value)
492518
expected_type = types.get_field_type(type(self), key)
493-
if types.is_union(passed_type):
494-
# We need to check all possible types in the union.
495-
passed_types = (
496-
arg for arg in passed_type.__args__ if arg is not type(None)
497-
)
498-
if (
499-
# If the passed var is a union, check if all possible types are valid.
500-
passed_types
501-
and not all(
502-
types._issubclass(pt, expected_type) for pt in passed_types
503-
)
504-
) or (
505-
# Else just check if the passed var type is valid.
506-
not passed_types and not satisfies_type_hint(value, expected_type)
507-
):
519+
520+
if not satisfies_type_hint(value, expected_type):
508521
value_name = value._js_expr if isinstance(value, Var) else value
509522

510523
additional_info = (

reflex/utils/console.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ def deprecate(
250250

251251
if dedupe_key not in _EMITTED_DEPRECATION_WARNINGS:
252252
msg = (
253-
f"{feature_name} has been deprecated in version {deprecation_version} {reason.rstrip('.')}. It will be completely "
253+
f"{feature_name} has been deprecated in version {deprecation_version}. {reason.rstrip('.').lstrip('. ')}. It will be completely "
254254
f"removed in {removal_version}. ({loc})"
255255
)
256256
if _LOG_LEVEL <= LogLevel.WARNING:

reflex/utils/decorator.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Decorator utilities."""
22

3-
from typing import Callable, TypeVar
3+
import functools
4+
from typing import Callable, ParamSpec, TypeVar
45

56
T = TypeVar("T")
67

@@ -23,3 +24,27 @@ def wrapper() -> T:
2324
return value # pyright: ignore[reportReturnType]
2425

2526
return wrapper
27+
28+
29+
P = ParamSpec("P")
30+
31+
32+
def debug(f: Callable[P, T]) -> Callable[P, T]:
33+
"""A decorator that prints the function name, arguments, and result.
34+
35+
Args:
36+
f: The function to call.
37+
38+
Returns:
39+
A function that prints the function name, arguments, and result.
40+
"""
41+
42+
@functools.wraps(f)
43+
def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
44+
result = f(*args, **kwargs)
45+
print( # noqa: T201
46+
f"Calling {f.__name__} with args: {args} and kwargs: {kwargs}, result: {result}"
47+
)
48+
return result
49+
50+
return wrapper

reflex/utils/types.py

Lines changed: 114 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
List,
2121
Literal,
2222
Mapping,
23+
NoReturn,
2324
Optional,
2425
Sequence,
2526
Tuple,
@@ -155,15 +156,7 @@ def get_type_hints(obj: Any) -> Dict[str, Any]:
155156
return get_type_hints_og(obj)
156157

157158

158-
def unionize(*args: GenericType) -> Type:
159-
"""Unionize the types.
160-
161-
Args:
162-
args: The types to unionize.
163-
164-
Returns:
165-
The unionized types.
166-
"""
159+
def _unionize(args: list[GenericType]) -> Type:
167160
if not args:
168161
return Any # pyright: ignore [reportReturnType]
169162
if len(args) == 1:
@@ -175,6 +168,18 @@ def unionize(*args: GenericType) -> Type:
175168
return Union[unionize(*first_half), unionize(*second_half)] # pyright: ignore [reportReturnType]
176169

177170

171+
def unionize(*args: GenericType) -> Type:
172+
"""Unionize the types.
173+
174+
Args:
175+
args: The types to unionize.
176+
177+
Returns:
178+
The unionized types.
179+
"""
180+
return _unionize([arg for arg in args if arg is not NoReturn])
181+
182+
178183
def is_none(cls: GenericType) -> bool:
179184
"""Check if a class is None.
180185
@@ -560,7 +565,12 @@ def does_obj_satisfy_typed_dict(obj: Any, cls: GenericType) -> bool:
560565

561566

562567
def _isinstance(
563-
obj: Any, cls: GenericType, *, nested: int = 0, treat_var_as_type: bool = True
568+
obj: Any,
569+
cls: GenericType,
570+
*,
571+
nested: int = 0,
572+
treat_var_as_type: bool = True,
573+
treat_mutable_obj_as_immutable: bool = False,
564574
) -> bool:
565575
"""Check if an object is an instance of a class.
566576
@@ -569,6 +579,7 @@ def _isinstance(
569579
cls: The class to check against.
570580
nested: How many levels deep to check.
571581
treat_var_as_type: Whether to treat Var as the type it represents, i.e. _var_type.
582+
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.
572583
573584
Returns:
574585
Whether the object is an instance of the class.
@@ -585,7 +596,13 @@ def _isinstance(
585596
obj._var_value, cls, nested=nested, treat_var_as_type=True
586597
)
587598
if isinstance(obj, Var):
588-
return treat_var_as_type and _issubclass(obj._var_type, cls)
599+
return treat_var_as_type and typehint_issubclass(
600+
obj._var_type,
601+
cls,
602+
treat_mutable_superclasss_as_immutable=treat_mutable_obj_as_immutable,
603+
treat_literals_as_union_of_types=True,
604+
treat_any_as_subtype_of_everything=True,
605+
)
589606

590607
if cls is None or cls is type(None):
591608
return obj is None
@@ -618,12 +635,18 @@ def _isinstance(
618635
args = get_args(cls)
619636

620637
if not args:
638+
if treat_mutable_obj_as_immutable:
639+
if origin is dict:
640+
origin = Mapping
641+
elif origin is list or origin is set:
642+
origin = Sequence
621643
# cls is a simple generic class
622644
return isinstance(obj, origin)
623645

624646
if nested > 0 and args:
625647
if origin is list:
626-
return isinstance(obj, list) and all(
648+
expected_class = Sequence if treat_mutable_obj_as_immutable else list
649+
return isinstance(obj, expected_class) and all(
627650
_isinstance(
628651
item,
629652
args[0],
@@ -657,7 +680,12 @@ def _isinstance(
657680
)
658681
)
659682
if origin in (dict, Mapping, Breakpoints):
660-
return isinstance(obj, Mapping) and all(
683+
expected_class = (
684+
dict
685+
if origin is dict and not treat_mutable_obj_as_immutable
686+
else Mapping
687+
)
688+
return isinstance(obj, expected_class) and all(
661689
_isinstance(
662690
key, args[0], nested=nested - 1, treat_var_as_type=treat_var_as_type
663691
)
@@ -670,7 +698,8 @@ def _isinstance(
670698
for key, value in obj.items()
671699
)
672700
if origin is set:
673-
return isinstance(obj, set) and all(
701+
expected_class = Sequence if treat_mutable_obj_as_immutable else set
702+
return isinstance(obj, expected_class) and all(
674703
_isinstance(
675704
item,
676705
args[0],
@@ -910,20 +939,32 @@ def safe_issubclass(cls: Type, cls_check: Type | tuple[Type, ...]):
910939
return False
911940

912941

913-
def typehint_issubclass(possible_subclass: Any, possible_superclass: Any) -> bool:
942+
def typehint_issubclass(
943+
possible_subclass: Any,
944+
possible_superclass: Any,
945+
*,
946+
treat_mutable_superclasss_as_immutable: bool = False,
947+
treat_literals_as_union_of_types: bool = True,
948+
treat_any_as_subtype_of_everything: bool = False,
949+
) -> bool:
914950
"""Check if a type hint is a subclass of another type hint.
915951
916952
Args:
917953
possible_subclass: The type hint to check.
918954
possible_superclass: The type hint to check against.
955+
treat_mutable_superclasss_as_immutable: Whether to treat target classes as immutable.
956+
treat_literals_as_union_of_types: Whether to treat literals as a union of their types.
957+
treat_any_as_subtype_of_everything: Whether to treat Any as a subtype of everything. This is the default behavior in Python.
919958
920959
Returns:
921960
Whether the type hint is a subclass of the other type hint.
922961
"""
923962
if possible_superclass is Any:
924963
return True
925964
if possible_subclass is Any:
926-
return False
965+
return treat_any_as_subtype_of_everything
966+
if possible_subclass is NoReturn:
967+
return True
927968

928969
provided_type_origin = get_origin(possible_subclass)
929970
accepted_type_origin = get_origin(possible_superclass)
@@ -932,6 +973,19 @@ def typehint_issubclass(possible_subclass: Any, possible_superclass: Any) -> boo
932973
# In this case, we are dealing with a non-generic type, so we can use issubclass
933974
return issubclass(possible_subclass, possible_superclass)
934975

976+
if treat_literals_as_union_of_types and is_literal(possible_superclass):
977+
args = get_args(possible_superclass)
978+
return any(
979+
typehint_issubclass(
980+
possible_subclass,
981+
type(arg),
982+
treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable,
983+
treat_literals_as_union_of_types=treat_literals_as_union_of_types,
984+
treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything,
985+
)
986+
for arg in args
987+
)
988+
935989
# Remove this check when Python 3.10 is the minimum supported version
936990
if hasattr(types, "UnionType"):
937991
provided_type_origin = (
@@ -948,29 +1002,67 @@ def typehint_issubclass(possible_subclass: Any, possible_superclass: Any) -> boo
9481002
if accepted_type_origin is Union:
9491003
if provided_type_origin is not Union:
9501004
return any(
951-
typehint_issubclass(possible_subclass, accepted_arg)
1005+
typehint_issubclass(
1006+
possible_subclass,
1007+
accepted_arg,
1008+
treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable,
1009+
treat_literals_as_union_of_types=treat_literals_as_union_of_types,
1010+
treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything,
1011+
)
9521012
for accepted_arg in accepted_args
9531013
)
9541014
return all(
9551015
any(
956-
typehint_issubclass(provided_arg, accepted_arg)
1016+
typehint_issubclass(
1017+
provided_arg,
1018+
accepted_arg,
1019+
treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable,
1020+
treat_literals_as_union_of_types=treat_literals_as_union_of_types,
1021+
treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything,
1022+
)
9571023
for accepted_arg in accepted_args
9581024
)
9591025
for provided_arg in provided_args
9601026
)
1027+
if provided_type_origin is Union:
1028+
return all(
1029+
typehint_issubclass(
1030+
provided_arg,
1031+
possible_superclass,
1032+
treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable,
1033+
treat_literals_as_union_of_types=treat_literals_as_union_of_types,
1034+
treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything,
1035+
)
1036+
for provided_arg in provided_args
1037+
)
1038+
1039+
provided_type_origin = provided_type_origin or possible_subclass
1040+
accepted_type_origin = accepted_type_origin or possible_superclass
1041+
1042+
if treat_mutable_superclasss_as_immutable:
1043+
if accepted_type_origin is dict:
1044+
accepted_type_origin = Mapping
1045+
elif accepted_type_origin is list or accepted_type_origin is set:
1046+
accepted_type_origin = Sequence
9611047

9621048
# Check if the origin of both types is the same (e.g., list for list[int])
963-
# This probably should be issubclass instead of ==
964-
if (provided_type_origin or possible_subclass) != (
965-
accepted_type_origin or possible_superclass
1049+
if not safe_issubclass(
1050+
provided_type_origin or possible_subclass, # pyright: ignore [reportArgumentType]
1051+
accepted_type_origin or possible_superclass, # pyright: ignore [reportArgumentType]
9661052
):
9671053
return False
9681054

9691055
# Ensure all specific types are compatible with accepted types
9701056
# Note this is not necessarily correct, as it doesn't check against contravariance and covariance
9711057
# It also ignores when the length of the arguments is different
9721058
return all(
973-
typehint_issubclass(provided_arg, accepted_arg)
1059+
typehint_issubclass(
1060+
provided_arg,
1061+
accepted_arg,
1062+
treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable,
1063+
treat_literals_as_union_of_types=treat_literals_as_union_of_types,
1064+
treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything,
1065+
)
9741066
for provided_arg, accepted_arg in zip(
9751067
provided_args, accepted_args, strict=False
9761068
)

0 commit comments

Comments
 (0)