|
@@ -20,6 +20,7 @@ from typing import (
|
|
|
List,
|
|
|
Literal,
|
|
|
Mapping,
|
|
|
+ NoReturn,
|
|
|
Optional,
|
|
|
Sequence,
|
|
|
Tuple,
|
|
@@ -155,15 +156,7 @@ def get_type_hints(obj: Any) -> Dict[str, Any]:
|
|
|
return get_type_hints_og(obj)
|
|
|
|
|
|
|
|
|
-def unionize(*args: GenericType) -> Type:
|
|
|
- """Unionize the types.
|
|
|
-
|
|
|
- Args:
|
|
|
- args: The types to unionize.
|
|
|
-
|
|
|
- Returns:
|
|
|
- The unionized types.
|
|
|
- """
|
|
|
+def _unionize(args: list[GenericType]) -> Type:
|
|
|
if not args:
|
|
|
return Any # pyright: ignore [reportReturnType]
|
|
|
if len(args) == 1:
|
|
@@ -175,6 +168,18 @@ def unionize(*args: GenericType) -> Type:
|
|
|
return Union[unionize(*first_half), unionize(*second_half)] # pyright: ignore [reportReturnType]
|
|
|
|
|
|
|
|
|
+def unionize(*args: GenericType) -> Type:
|
|
|
+ """Unionize the types.
|
|
|
+
|
|
|
+ Args:
|
|
|
+ args: The types to unionize.
|
|
|
+
|
|
|
+ Returns:
|
|
|
+ The unionized types.
|
|
|
+ """
|
|
|
+ return _unionize([arg for arg in args if arg is not NoReturn])
|
|
|
+
|
|
|
+
|
|
|
def is_none(cls: GenericType) -> bool:
|
|
|
"""Check if a class is None.
|
|
|
|
|
@@ -560,7 +565,12 @@ def does_obj_satisfy_typed_dict(obj: Any, cls: GenericType) -> bool:
|
|
|
|
|
|
|
|
|
def _isinstance(
|
|
|
- obj: Any, cls: GenericType, *, nested: int = 0, treat_var_as_type: bool = True
|
|
|
+ obj: Any,
|
|
|
+ cls: GenericType,
|
|
|
+ *,
|
|
|
+ nested: int = 0,
|
|
|
+ treat_var_as_type: bool = True,
|
|
|
+ treat_mutable_obj_as_immutable: bool = False,
|
|
|
) -> bool:
|
|
|
"""Check if an object is an instance of a class.
|
|
|
|
|
@@ -569,6 +579,7 @@ def _isinstance(
|
|
|
cls: The class to check against.
|
|
|
nested: How many levels deep to check.
|
|
|
treat_var_as_type: Whether to treat Var as the type it represents, i.e. _var_type.
|
|
|
+ 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.
|
|
|
|
|
|
Returns:
|
|
|
Whether the object is an instance of the class.
|
|
@@ -585,7 +596,13 @@ def _isinstance(
|
|
|
obj._var_value, cls, nested=nested, treat_var_as_type=True
|
|
|
)
|
|
|
if isinstance(obj, Var):
|
|
|
- return treat_var_as_type and _issubclass(obj._var_type, cls)
|
|
|
+ return treat_var_as_type and typehint_issubclass(
|
|
|
+ obj._var_type,
|
|
|
+ cls,
|
|
|
+ treat_mutable_superclasss_as_immutable=treat_mutable_obj_as_immutable,
|
|
|
+ treat_literals_as_union_of_types=True,
|
|
|
+ treat_any_as_subtype_of_everything=True,
|
|
|
+ )
|
|
|
|
|
|
if cls is None or cls is type(None):
|
|
|
return obj is None
|
|
@@ -618,12 +635,18 @@ def _isinstance(
|
|
|
args = get_args(cls)
|
|
|
|
|
|
if not args:
|
|
|
+ if treat_mutable_obj_as_immutable:
|
|
|
+ if origin is dict:
|
|
|
+ origin = Mapping
|
|
|
+ elif origin is list or origin is set:
|
|
|
+ origin = Sequence
|
|
|
# cls is a simple generic class
|
|
|
return isinstance(obj, origin)
|
|
|
|
|
|
if nested > 0 and args:
|
|
|
if origin is list:
|
|
|
- return isinstance(obj, list) and all(
|
|
|
+ expected_class = Sequence if treat_mutable_obj_as_immutable else list
|
|
|
+ return isinstance(obj, expected_class) and all(
|
|
|
_isinstance(
|
|
|
item,
|
|
|
args[0],
|
|
@@ -657,7 +680,12 @@ def _isinstance(
|
|
|
)
|
|
|
)
|
|
|
if origin in (dict, Mapping, Breakpoints):
|
|
|
- return isinstance(obj, Mapping) and all(
|
|
|
+ expected_class = (
|
|
|
+ dict
|
|
|
+ if origin is dict and not treat_mutable_obj_as_immutable
|
|
|
+ else Mapping
|
|
|
+ )
|
|
|
+ return isinstance(obj, expected_class) and all(
|
|
|
_isinstance(
|
|
|
key, args[0], nested=nested - 1, treat_var_as_type=treat_var_as_type
|
|
|
)
|
|
@@ -670,7 +698,8 @@ def _isinstance(
|
|
|
for key, value in obj.items()
|
|
|
)
|
|
|
if origin is set:
|
|
|
- return isinstance(obj, set) and all(
|
|
|
+ expected_class = Sequence if treat_mutable_obj_as_immutable else set
|
|
|
+ return isinstance(obj, expected_class) and all(
|
|
|
_isinstance(
|
|
|
item,
|
|
|
args[0],
|
|
@@ -910,12 +939,22 @@ def safe_issubclass(cls: Type, cls_check: Type | tuple[Type, ...]):
|
|
|
return False
|
|
|
|
|
|
|
|
|
-def typehint_issubclass(possible_subclass: Any, possible_superclass: Any) -> bool:
|
|
|
+def typehint_issubclass(
|
|
|
+ possible_subclass: Any,
|
|
|
+ possible_superclass: Any,
|
|
|
+ *,
|
|
|
+ treat_mutable_superclasss_as_immutable: bool = False,
|
|
|
+ treat_literals_as_union_of_types: bool = True,
|
|
|
+ treat_any_as_subtype_of_everything: bool = False,
|
|
|
+) -> bool:
|
|
|
"""Check if a type hint is a subclass of another type hint.
|
|
|
|
|
|
Args:
|
|
|
possible_subclass: The type hint to check.
|
|
|
possible_superclass: The type hint to check against.
|
|
|
+ treat_mutable_superclasss_as_immutable: Whether to treat target classes as immutable.
|
|
|
+ treat_literals_as_union_of_types: Whether to treat literals as a union of their types.
|
|
|
+ treat_any_as_subtype_of_everything: Whether to treat Any as a subtype of everything. This is the default behavior in Python.
|
|
|
|
|
|
Returns:
|
|
|
Whether the type hint is a subclass of the other type hint.
|
|
@@ -923,7 +962,9 @@ def typehint_issubclass(possible_subclass: Any, possible_superclass: Any) -> boo
|
|
|
if possible_superclass is Any:
|
|
|
return True
|
|
|
if possible_subclass is Any:
|
|
|
- return False
|
|
|
+ return treat_any_as_subtype_of_everything
|
|
|
+ if possible_subclass is NoReturn:
|
|
|
+ return True
|
|
|
|
|
|
provided_type_origin = get_origin(possible_subclass)
|
|
|
accepted_type_origin = get_origin(possible_superclass)
|
|
@@ -932,6 +973,19 @@ def typehint_issubclass(possible_subclass: Any, possible_superclass: Any) -> boo
|
|
|
# In this case, we are dealing with a non-generic type, so we can use issubclass
|
|
|
return issubclass(possible_subclass, possible_superclass)
|
|
|
|
|
|
+ if treat_literals_as_union_of_types and is_literal(possible_superclass):
|
|
|
+ args = get_args(possible_superclass)
|
|
|
+ return any(
|
|
|
+ typehint_issubclass(
|
|
|
+ possible_subclass,
|
|
|
+ type(arg),
|
|
|
+ treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable,
|
|
|
+ treat_literals_as_union_of_types=treat_literals_as_union_of_types,
|
|
|
+ treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything,
|
|
|
+ )
|
|
|
+ for arg in args
|
|
|
+ )
|
|
|
+
|
|
|
# Remove this check when Python 3.10 is the minimum supported version
|
|
|
if hasattr(types, "UnionType"):
|
|
|
provided_type_origin = (
|
|
@@ -948,21 +1002,53 @@ def typehint_issubclass(possible_subclass: Any, possible_superclass: Any) -> boo
|
|
|
if accepted_type_origin is Union:
|
|
|
if provided_type_origin is not Union:
|
|
|
return any(
|
|
|
- typehint_issubclass(possible_subclass, accepted_arg)
|
|
|
+ typehint_issubclass(
|
|
|
+ possible_subclass,
|
|
|
+ accepted_arg,
|
|
|
+ treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable,
|
|
|
+ treat_literals_as_union_of_types=treat_literals_as_union_of_types,
|
|
|
+ treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything,
|
|
|
+ )
|
|
|
for accepted_arg in accepted_args
|
|
|
)
|
|
|
return all(
|
|
|
any(
|
|
|
- typehint_issubclass(provided_arg, accepted_arg)
|
|
|
+ typehint_issubclass(
|
|
|
+ provided_arg,
|
|
|
+ accepted_arg,
|
|
|
+ treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable,
|
|
|
+ treat_literals_as_union_of_types=treat_literals_as_union_of_types,
|
|
|
+ treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything,
|
|
|
+ )
|
|
|
for accepted_arg in accepted_args
|
|
|
)
|
|
|
for provided_arg in provided_args
|
|
|
)
|
|
|
+ if provided_type_origin is Union:
|
|
|
+ return all(
|
|
|
+ typehint_issubclass(
|
|
|
+ provided_arg,
|
|
|
+ possible_superclass,
|
|
|
+ treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable,
|
|
|
+ treat_literals_as_union_of_types=treat_literals_as_union_of_types,
|
|
|
+ treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything,
|
|
|
+ )
|
|
|
+ for provided_arg in provided_args
|
|
|
+ )
|
|
|
+
|
|
|
+ provided_type_origin = provided_type_origin or possible_subclass
|
|
|
+ accepted_type_origin = accepted_type_origin or possible_superclass
|
|
|
+
|
|
|
+ if treat_mutable_superclasss_as_immutable:
|
|
|
+ if accepted_type_origin is dict:
|
|
|
+ accepted_type_origin = Mapping
|
|
|
+ elif accepted_type_origin is list or accepted_type_origin is set:
|
|
|
+ accepted_type_origin = Sequence
|
|
|
|
|
|
# Check if the origin of both types is the same (e.g., list for list[int])
|
|
|
- # This probably should be issubclass instead of ==
|
|
|
- if (provided_type_origin or possible_subclass) != (
|
|
|
- accepted_type_origin or possible_superclass
|
|
|
+ if not safe_issubclass(
|
|
|
+ provided_type_origin or possible_subclass, # pyright: ignore [reportArgumentType]
|
|
|
+ accepted_type_origin or possible_superclass, # pyright: ignore [reportArgumentType]
|
|
|
):
|
|
|
return False
|
|
|
|
|
@@ -970,7 +1056,13 @@ def typehint_issubclass(possible_subclass: Any, possible_superclass: Any) -> boo
|
|
|
# Note this is not necessarily correct, as it doesn't check against contravariance and covariance
|
|
|
# It also ignores when the length of the arguments is different
|
|
|
return all(
|
|
|
- typehint_issubclass(provided_arg, accepted_arg)
|
|
|
+ typehint_issubclass(
|
|
|
+ provided_arg,
|
|
|
+ accepted_arg,
|
|
|
+ treat_mutable_superclasss_as_immutable=treat_mutable_superclasss_as_immutable,
|
|
|
+ treat_literals_as_union_of_types=treat_literals_as_union_of_types,
|
|
|
+ treat_any_as_subtype_of_everything=treat_any_as_subtype_of_everything,
|
|
|
+ )
|
|
|
for provided_arg, accepted_arg in zip(
|
|
|
provided_args, accepted_args, strict=False
|
|
|
)
|