Browse Source

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
Khaleel Al-Adhami 2 months ago
parent
commit
1e07ec70fe

+ 32 - 19
reflex/components/component.py

@@ -51,7 +51,7 @@ from reflex.event import (
     no_args_event_spec,
 )
 from reflex.style import Style, format_as_emotion
-from reflex.utils import format, imports, types
+from reflex.utils import console, format, imports, types
 from reflex.utils.imports import ImportDict, ImportVar, ParsedImportDict, parse_imports
 from reflex.vars import VarData
 from reflex.vars.base import (
@@ -177,6 +177,18 @@ ComponentChild = types.PrimitiveType | Var | BaseComponent
 ComponentChildTypes = (*types.PrimitiveTypes, Var, BaseComponent)
 
 
+def _satisfies_type_hint(obj: Any, type_hint: Any) -> bool:
+    return types._isinstance(
+        obj,
+        type_hint,
+        nested=1,
+        treat_var_as_type=True,
+        treat_mutable_obj_as_immutable=(
+            isinstance(obj, Var) and not isinstance(obj, LiteralVar)
+        ),
+    )
+
+
 def satisfies_type_hint(obj: Any, type_hint: Any) -> bool:
     """Check if an object satisfies a type hint.
 
@@ -187,7 +199,23 @@ def satisfies_type_hint(obj: Any, type_hint: Any) -> bool:
     Returns:
         Whether the object satisfies the type hint.
     """
-    return types._isinstance(obj, type_hint, nested=1, treat_var_as_type=True)
+    if _satisfies_type_hint(obj, type_hint):
+        return True
+    if _satisfies_type_hint(obj, type_hint | None):
+        obj = (
+            obj
+            if not isinstance(obj, Var)
+            else (obj._var_value if isinstance(obj, LiteralVar) else obj)
+        )
+        console.deprecate(
+            "implicit-none-for-component-fields",
+            reason="Passing Vars with possible None values to component fields not explicitly marked as Optional is deprecated. "
+            + f"Passed {obj!s} of type {type(obj) if not isinstance(obj, Var) else obj._var_type} to {type_hint}.",
+            deprecation_version="0.7.2",
+            removal_version="0.8.0",
+        )
+        return True
+    return False
 
 
 def _components_from(
@@ -466,8 +494,6 @@ class Component(BaseComponent, ABC):
 
             # Check whether the key is a component prop.
             if types._issubclass(field_type, Var):
-                # Used to store the passed types if var type is a union.
-                passed_types = None
                 try:
                     kwargs[key] = determine_key(value)
 
@@ -490,21 +516,8 @@ class Component(BaseComponent, ABC):
                     # If it is not a valid var, check the base types.
                     passed_type = type(value)
                     expected_type = types.get_field_type(type(self), key)
-                if types.is_union(passed_type):
-                    # We need to check all possible types in the union.
-                    passed_types = (
-                        arg for arg in passed_type.__args__ if arg is not type(None)
-                    )
-                if (
-                    # If the passed var is a union, check if all possible types are valid.
-                    passed_types
-                    and not all(
-                        types._issubclass(pt, expected_type) for pt in passed_types
-                    )
-                ) or (
-                    # Else just check if the passed var type is valid.
-                    not passed_types and not satisfies_type_hint(value, expected_type)
-                ):
+
+                if not satisfies_type_hint(value, expected_type):
                     value_name = value._js_expr if isinstance(value, Var) else value
 
                     additional_info = (

+ 1 - 1
reflex/utils/console.py

@@ -250,7 +250,7 @@ def deprecate(
 
     if dedupe_key not in _EMITTED_DEPRECATION_WARNINGS:
         msg = (
-            f"{feature_name} has been deprecated in version {deprecation_version} {reason.rstrip('.')}. It will be completely "
+            f"{feature_name} has been deprecated in version {deprecation_version}. {reason.rstrip('.').lstrip('. ')}. It will be completely "
             f"removed in {removal_version}. ({loc})"
         )
         if _LOG_LEVEL <= LogLevel.WARNING:

+ 26 - 1
reflex/utils/decorator.py

@@ -1,6 +1,7 @@
 """Decorator utilities."""
 
-from typing import Callable, TypeVar
+import functools
+from typing import Callable, ParamSpec, TypeVar
 
 T = TypeVar("T")
 
@@ -23,3 +24,27 @@ def once(f: Callable[[], T]) -> Callable[[], T]:
         return value  # pyright: ignore[reportReturnType]
 
     return wrapper
+
+
+P = ParamSpec("P")
+
+
+def debug(f: Callable[P, T]) -> Callable[P, T]:
+    """A decorator that prints the function name, arguments, and result.
+
+    Args:
+        f: The function to call.
+
+    Returns:
+        A function that prints the function name, arguments, and result.
+    """
+
+    @functools.wraps(f)
+    def wrapper(*args: P.args, **kwargs: P.kwargs) -> T:
+        result = f(*args, **kwargs)
+        print(  # noqa: T201
+            f"Calling {f.__name__} with args: {args} and kwargs: {kwargs}, result: {result}"
+        )
+        return result
+
+    return wrapper

+ 114 - 22
reflex/utils/types.py

@@ -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
         )

+ 34 - 20
reflex/vars/base.py

@@ -359,7 +359,7 @@ def can_use_in_object_var(cls: GenericType) -> bool:
         return all(can_use_in_object_var(t) for t in types.get_args(cls))
     return (
         inspect.isclass(cls)
-        and not issubclass(cls, Var)
+        and not safe_issubclass(cls, Var)
         and serializers.can_serialize(cls, dict)
     )
 
@@ -796,7 +796,7 @@ class Var(Generic[VAR_TYPE]):
 
         if inspect.isclass(output):
             for var_subclass in _var_subclasses[::-1]:
-                if issubclass(output, var_subclass.var_subclass):
+                if safe_issubclass(output, var_subclass.var_subclass):
                     current_var_type = self._var_type
                     if current_var_type is Any:
                         new_var_type = var_type
@@ -808,7 +808,7 @@ class Var(Generic[VAR_TYPE]):
                     return to_operation_return  # pyright: ignore [reportReturnType]
 
             # If we can't determine the first argument, we just replace the _var_type.
-            if not issubclass(output, Var) or var_type is None:
+            if not safe_issubclass(output, Var) or var_type is None:
                 return dataclasses.replace(
                     self,
                     _var_type=output,
@@ -850,14 +850,15 @@ class Var(Generic[VAR_TYPE]):
         Raises:
             TypeError: If the type is not supported for guessing.
         """
-        from .number import NumberVar
         from .object import ObjectVar
 
         var_type = self._var_type
         if var_type is None:
             return self.to(None)
-        if types.is_optional(var_type):
-            var_type = types.get_args(var_type)[0]
+        if var_type is NoReturn:
+            return self.to(Any)
+
+        var_type = types.value_inside_optional(var_type)
 
         if var_type is Any:
             return self
@@ -866,11 +867,20 @@ class Var(Generic[VAR_TYPE]):
 
         if fixed_type in types.UnionTypes:
             inner_types = get_args(var_type)
+            non_optional_inner_types = [
+                types.value_inside_optional(inner_type) for inner_type in inner_types
+            ]
+            fixed_inner_types = [
+                get_origin(inner_type) or inner_type
+                for inner_type in non_optional_inner_types
+            ]
 
-            if all(
-                inspect.isclass(t) and issubclass(t, (int, float)) for t in inner_types
-            ):
-                return self.to(NumberVar, self._var_type)
+            for var_subclass in _var_subclasses[::-1]:
+                if all(
+                    safe_issubclass(t, var_subclass.python_types)
+                    for t in fixed_inner_types
+                ):
+                    return self.to(var_subclass.var_subclass, self._var_type)
 
             if can_use_in_object_var(var_type):
                 return self.to(ObjectVar, self._var_type)
@@ -888,7 +898,7 @@ class Var(Generic[VAR_TYPE]):
             return self.to(None)
 
         for var_subclass in _var_subclasses[::-1]:
-            if issubclass(fixed_type, var_subclass.python_types):
+            if safe_issubclass(fixed_type, var_subclass.python_types):
                 return self.to(var_subclass.var_subclass, self._var_type)
 
         if can_use_in_object_var(fixed_type):
@@ -916,17 +926,17 @@ class Var(Generic[VAR_TYPE]):
         if type_ is Literal:
             args = get_args(self._var_type)
             return args[0] if args else None
-        if issubclass(type_, str):
+        if safe_issubclass(type_, str):
             return ""
-        if issubclass(type_, types.get_args(int | float)):
+        if safe_issubclass(type_, types.get_args(int | float)):
             return 0
-        if issubclass(type_, bool):
+        if safe_issubclass(type_, bool):
             return False
-        if issubclass(type_, list):
+        if safe_issubclass(type_, list):
             return []
-        if issubclass(type_, Mapping):
+        if safe_issubclass(type_, Mapping):
             return {}
-        if issubclass(type_, tuple):
+        if safe_issubclass(type_, tuple):
             return ()
         if types.is_dataframe(type_):
             try:
@@ -937,7 +947,7 @@ class Var(Generic[VAR_TYPE]):
                 raise ImportError(
                     "Please install pandas to use dataframes in your app."
                 ) from e
-        return set() if issubclass(type_, set) else None
+        return set() if safe_issubclass(type_, set) else None
 
     def _get_setter_name(self, include_state: bool = True) -> str:
         """Get the name of the var's generated setter function.
@@ -1410,7 +1420,7 @@ class LiteralVar(Var):
         possible_bases = [
             base
             for base in bases_normalized
-            if issubclass(base, Var) and base != LiteralVar
+            if safe_issubclass(base, Var) and base != LiteralVar
         ]
 
         if not possible_bases:
@@ -1686,12 +1696,16 @@ def figure_out_type(value: Any) -> types.GenericType:
     if has_args(type_):
         return type_
     if isinstance(value, list):
+        if not value:
+            return Sequence[NoReturn]
         return Sequence[unionize(*(figure_out_type(v) for v in value))]
     if isinstance(value, set):
         return set[unionize(*(figure_out_type(v) for v in value))]
     if isinstance(value, tuple):
         return tuple[unionize(*(figure_out_type(v) for v in value)), ...]
     if isinstance(value, Mapping):
+        if not value:
+            return Mapping[NoReturn, NoReturn]
         return Mapping[
             unionize(*(figure_out_type(k) for k in value)),
             unionize(*(figure_out_type(v) for v in value.values())),
@@ -2700,7 +2714,7 @@ class CustomVarOperationReturn(Var[RETURN]):
 
 def var_operation_return(
     js_expression: str,
-    var_type: Type[RETURN] | None = None,
+    var_type: Type[RETURN] | GenericType | None = None,
     var_data: VarData | None = None,
 ) -> CustomVarOperationReturn[RETURN]:
     """Shortcut for creating a CustomVarOperationReturn.

+ 28 - 12
reflex/vars/object.py

@@ -2,6 +2,7 @@
 
 from __future__ import annotations
 
+import collections.abc
 import dataclasses
 import typing
 from inspect import isclass
@@ -51,6 +52,29 @@ ARRAY_INNER_TYPE = TypeVar("ARRAY_INNER_TYPE")
 OTHER_KEY_TYPE = TypeVar("OTHER_KEY_TYPE")
 
 
+def _determine_value_type(var_type: GenericType):
+    origin_var_type = get_origin(var_type) or var_type
+
+    if origin_var_type in types.UnionTypes:
+        return unionize(
+            *[
+                _determine_value_type(arg)
+                for arg in get_args(var_type)
+                if arg is not type(None)
+            ]
+        )
+
+    if is_typeddict(origin_var_type) or dataclasses.is_dataclass(origin_var_type):
+        annotations = get_type_hints(origin_var_type)
+        return unionize(*annotations.values())
+
+    if origin_var_type in [dict, Mapping, collections.abc.Mapping]:
+        args = get_args(var_type)
+        return args[1] if args else Any
+
+    return Any
+
+
 class ObjectVar(Var[OBJECT_TYPE], python_types=Mapping):
     """Base class for immutable object vars."""
 
@@ -68,22 +92,15 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=Mapping):
     ) -> Type[VALUE_TYPE]: ...
 
     @overload
-    def _value_type(self) -> Type: ...
+    def _value_type(self) -> GenericType: ...
 
-    def _value_type(self) -> Type:
+    def _value_type(self) -> GenericType:
         """Get the type of the values of the object.
 
         Returns:
             The type of the values of the object.
         """
-        fixed_type = get_origin(self._var_type) or self._var_type
-        if not isclass(fixed_type):
-            return Any  # pyright: ignore [reportReturnType]
-        if is_typeddict(fixed_type) or dataclasses.is_dataclass(fixed_type):
-            annotations = get_type_hints(fixed_type)
-            return unionize(*annotations.values())
-        args = get_args(self._var_type) if issubclass(fixed_type, Mapping) else ()
-        return args[1] if args else Any  # pyright: ignore [reportReturnType]
+        return _determine_value_type(self._var_type)
 
     def keys(self) -> ArrayVar[list[str]]:
         """Get the keys of the object.
@@ -268,8 +285,7 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=Mapping):
 
         var_type = self._var_type
 
-        if types.is_optional(var_type):
-            var_type = get_args(var_type)[0]
+        var_type = types.value_inside_optional(var_type)
 
         fixed_type = get_origin(var_type) or var_type
 

+ 53 - 7
reflex/vars/sequence.py

@@ -2,14 +2,15 @@
 
 from __future__ import annotations
 
+import collections.abc
 import dataclasses
 import inspect
 import json
 import re
-import typing
 from typing import (
     TYPE_CHECKING,
     Any,
+    Iterable,
     List,
     Literal,
     Mapping,
@@ -18,6 +19,7 @@ from typing import (
     Type,
     TypeVar,
     Union,
+    get_args,
     overload,
 )
 
@@ -26,6 +28,7 @@ from typing_extensions import TypeVar as TypingExtensionsTypeVar
 from reflex import constants
 from reflex.constants.base import REFLEX_VAR_OPENING_TAG
 from reflex.constants.colors import Color
+from reflex.utils import types
 from reflex.utils.exceptions import VarTypeError
 from reflex.utils.types import GenericType, get_origin
 
@@ -1622,6 +1625,47 @@ def is_tuple_type(t: GenericType) -> bool:
     return get_origin(t) is tuple
 
 
+def _determine_value_of_array_index(
+    var_type: GenericType, index: int | float | None = None
+):
+    """Determine the value of an array index.
+
+    Args:
+        var_type: The type of the array.
+        index: The index of the array.
+
+    Returns:
+        The value of the array index.
+    """
+    origin_var_type = get_origin(var_type) or var_type
+    if origin_var_type in types.UnionTypes:
+        return unionize(
+            *[
+                _determine_value_of_array_index(t, index)
+                for t in get_args(var_type)
+                if t is not type(None)
+            ]
+        )
+    if origin_var_type in [
+        Sequence,
+        Iterable,
+        list,
+        set,
+        collections.abc.Sequence,
+        collections.abc.Iterable,
+    ]:
+        args = get_args(var_type)
+        return args[0] if args else Any
+    if origin_var_type is tuple:
+        args = get_args(var_type)
+        return (
+            args[int(index) % len(args)]
+            if args and index is not None
+            else (unionize(*args) if args else Any)
+        )
+    return Any
+
+
 @var_operation
 def array_item_operation(array: ArrayVar, index: NumberVar | int):
     """Get an item from an array.
@@ -1633,12 +1677,14 @@ def array_item_operation(array: ArrayVar, index: NumberVar | int):
     Returns:
         The item from the array.
     """
-    args = typing.get_args(array._var_type)
-    if args and isinstance(index, LiteralNumberVar) and is_tuple_type(array._var_type):
-        index_value = int(index._var_value)
-        element_type = args[index_value % len(args)]
-    else:
-        element_type = unionize(*args)
+    element_type = _determine_value_of_array_index(
+        array._var_type,
+        (
+            index
+            if isinstance(index, int)
+            else (index._var_value if isinstance(index, LiteralNumberVar) else None)
+        ),
+    )
 
     return var_operation_return(
         js_expression=f"{array!s}.at({index!s})",

+ 87 - 1
tests/units/utils/test_utils.py

@@ -2,7 +2,17 @@ import os
 import typing
 from functools import cached_property
 from pathlib import Path
-from typing import Any, ClassVar, List, Literal, Type, Union
+from typing import (
+    Any,
+    ClassVar,
+    List,
+    Literal,
+    Mapping,
+    NoReturn,
+    Sequence,
+    Type,
+    Union,
+)
 
 import pytest
 import typer
@@ -109,12 +119,88 @@ def test_is_generic_alias(cls: type, expected: bool):
         (dict[str, str], dict[str, str], True),
         (dict[str, str], dict[str, Any], True),
         (dict[str, Any], dict[str, Any], True),
+        (Mapping[str, int], dict[str, int], False),
+        (Sequence[int], list[int], False),
+        (Sequence[int] | list[int], list[int], False),
+        (str, Literal["test", "value"], True),
+        (str, Literal["test", "value", 2, 3], True),
+        (int, Literal["test", "value"], False),
+        (int, Literal["test", "value", 2, 3], True),
+        *[
+            (NoReturn, super_class, True)
+            for super_class in [int, float, str, bool, list, dict, object, Any]
+        ],
+        *[
+            (list[NoReturn], list[super_class], True)
+            for super_class in [int, float, str, bool, list, dict, object, Any]
+        ],
     ],
 )
 def test_typehint_issubclass(subclass, superclass, expected):
     assert types.typehint_issubclass(subclass, superclass) == expected
 
 
+@pytest.mark.parametrize(
+    ("subclass", "superclass", "expected"),
+    [
+        *[
+            (base_type, base_type, True)
+            for base_type in [int, float, str, bool, list, dict]
+        ],
+        *[
+            (one_type, another_type, False)
+            for one_type in [int, float, str, list, dict]
+            for another_type in [int, float, str, list, dict]
+            if one_type != another_type
+        ],
+        (bool, int, True),
+        (int, bool, False),
+        (list, List, True),
+        (list, list[str], True),  # this is wrong, but it's a limitation of the function
+        (List, list, True),
+        (list[int], list, True),
+        (list[int], List, True),
+        (list[int], list[str], False),
+        (list[int], list[int], True),
+        (list[int], list[float], False),
+        (list[int], list[int | float], True),
+        (list[int], list[float | str], False),
+        (int | float, list[int | float], False),
+        (int | float, int | float | str, True),
+        (int | float, str | float, False),
+        (dict[str, int], dict[str, int], True),
+        (dict[str, bool], dict[str, int], True),
+        (dict[str, int], dict[str, bool], False),
+        (dict[str, Any], dict[str, str], False),
+        (dict[str, str], dict[str, str], True),
+        (dict[str, str], dict[str, Any], True),
+        (dict[str, Any], dict[str, Any], True),
+        (Mapping[str, int], dict[str, int], True),
+        (Sequence[int], list[int], True),
+        (Sequence[int] | list[int], list[int], True),
+        (str, Literal["test", "value"], True),
+        (str, Literal["test", "value", 2, 3], True),
+        (int, Literal["test", "value"], False),
+        (int, Literal["test", "value", 2, 3], True),
+        *[
+            (NoReturn, super_class, True)
+            for super_class in [int, float, str, bool, list, dict, object, Any]
+        ],
+        *[
+            (list[NoReturn], list[super_class], True)
+            for super_class in [int, float, str, bool, list, dict, object, Any]
+        ],
+    ],
+)
+def test_typehint_issubclass_mutable_as_immutable(subclass, superclass, expected):
+    assert (
+        types.typehint_issubclass(
+            subclass, superclass, treat_mutable_superclasss_as_immutable=True
+        )
+        == expected
+    )
+
+
 def test_validate_none_bun_path(mocker):
     """Test that an error is thrown when a bun path is not specified.