From 31705950436832bac90dd678712329d82a870b21 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Tue, 25 Aug 2026 12:02:17 +0200 Subject: [PATCH 1/5] resolve type aliases --- .../src/reflex_base/utils/types.py | 30 ++++++++++++ .../reflex-base/src/reflex_base/vars/base.py | 4 ++ tests/units/vars/test_base.py | 49 +++++++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/packages/reflex-base/src/reflex_base/utils/types.py b/packages/reflex-base/src/reflex_base/utils/types.py index 1298287ec6e..eae0d5ff43b 100644 --- a/packages/reflex-base/src/reflex_base/utils/types.py +++ b/packages/reflex-base/src/reflex_base/utils/types.py @@ -6,6 +6,7 @@ import logging import sys import types +import typing from collections.abc import Callable, Iterable, Mapping, Sequence from enum import Enum from functools import cached_property, lru_cache @@ -49,6 +50,15 @@ # Potential Union types for isinstance checks. UnionTypes = (Union, types.UnionType) +# Potential TypeAliasType classes for isinstance checks. On 3.12+ the native +# typing.TypeAliasType (produced by the `type` statement) and the +# typing_extensions backport are distinct classes. +TypeAliasTypes: tuple[type, ...] = ( + (TypeAliasType, typing.TypeAliasType) + if sys.version_info >= (3, 12) + else (TypeAliasType,) +) + # Union of generic types. GenericType = type | _GenericAlias @@ -351,6 +361,26 @@ def is_classvar(a_type: Any) -> bool: ) +def resolve_type_alias(cls: GenericType) -> GenericType: + """Resolve a TypeAliasType (PEP 695 ``type`` statement) to its underlying value. + + Aliases appearing as members of a union are resolved as well. + + Args: + cls: The type to resolve. + + Returns: + The resolved type, or the original type if it contains no alias. + """ + while isinstance(cls, TypeAliasTypes): + cls = cls.__value__ + if is_union(cls): + args = get_args(cls) + if any(isinstance(arg, TypeAliasTypes) for arg in args): + return unionize(*(resolve_type_alias(arg) for arg in args)) + return cls + + def value_inside_optional(cls: GenericType) -> GenericType: """Get the value inside an Optional type or the original type. diff --git a/packages/reflex-base/src/reflex_base/vars/base.py b/packages/reflex-base/src/reflex_base/vars/base.py index c2c3300cf86..39b5538371c 100644 --- a/packages/reflex-base/src/reflex_base/vars/base.py +++ b/packages/reflex-base/src/reflex_base/vars/base.py @@ -1077,6 +1077,10 @@ def guess_type(self) -> Var: if var_type is NoReturn: return self.to(Any) + resolved_type = types.resolve_type_alias(var_type) + if resolved_type is not var_type: + return dataclasses.replace(self, _var_type=resolved_type).guess_type() + var_type = types.value_inside_optional(var_type) if var_type is Any: diff --git a/tests/units/vars/test_base.py b/tests/units/vars/test_base.py index e4d7e363e3c..6e702169bcf 100644 --- a/tests/units/vars/test_base.py +++ b/tests/units/vars/test_base.py @@ -77,3 +77,52 @@ def cv(self) -> int: replaced = cv._replace(_var_type=float) assert replaced._var_type is float + + +def _type_alias_types() -> list[type]: + import typing + + from typing_extensions import TypeAliasType + + native = getattr(typing, "TypeAliasType", None) + return ( + [TypeAliasType] if native in (None, TypeAliasType) else [TypeAliasType, native] + ) + + +@pytest.mark.parametrize("alias_cls", _type_alias_types()) +def test_guess_type_resolves_type_alias(alias_cls: type) -> None: + """A TypeAliasType (PEP 695 ``type`` statement) resolves to its value. + + State var annotations like ``type Key = Literal[...]`` reach guess_type as + a TypeAliasType, which must be unwrapped instead of raising TypeError. + """ + from typing import Literal + + from reflex_base.vars.base import Var + from reflex_base.vars.sequence import StringVar + + alias = alias_cls("ChartKey", Literal["day", "week"]) + + var = Var(_js_expr="key", _var_type=alias).guess_type() + assert isinstance(var, StringVar) + assert var._var_type == Literal["day", "week"] + + optional_var = Var(_js_expr="key", _var_type=alias | None).guess_type() + assert isinstance(optional_var, StringVar) + + +@pytest.mark.parametrize("alias_cls", _type_alias_types()) +def test_state_var_type_alias(alias_cls: type) -> None: + """A state var annotated with a TypeAliasType compiles.""" + from typing import Literal + + from reflex_base.vars.sequence import StringVar + + chart_key = alias_cls("ChartKey", Literal["day", "week"]) + + class TypeAliasState(State): + key: chart_key = "day" # pyright: ignore[reportInvalidTypeForm] + + assert isinstance(TypeAliasState.key, StringVar) + assert TypeAliasState.key._var_type == Literal["day", "week"] From fb3c49ea1b6c060a43d98e77913387538cfb9c4a Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Tue, 25 Aug 2026 12:13:19 +0200 Subject: [PATCH 2/5] move type alias tests to the reflex_base suite --- tests/units/reflex_base/vars/test_base.py | 46 ++++++++++++++++++++- tests/units/vars/test_base.py | 49 ----------------------- 2 files changed, 44 insertions(+), 51 deletions(-) diff --git a/tests/units/reflex_base/vars/test_base.py b/tests/units/reflex_base/vars/test_base.py index b0ff880aa20..570d7a37674 100644 --- a/tests/units/reflex_base/vars/test_base.py +++ b/tests/units/reflex_base/vars/test_base.py @@ -1,10 +1,16 @@ """Tests for reflex_base.vars.base state metaclass field handling.""" import threading -from typing import Any +import typing +from typing import Any, Literal +import pytest from reflex_base.utils.types import get_field_type -from reflex_base.vars.base import EvenMoreBasicBaseState, field +from reflex_base.vars.base import EvenMoreBasicBaseState, Var, field +from reflex_base.vars.sequence import StringVar +from typing_extensions import TypeAliasType + +from reflex.state import State _MARKER_ATTR = "_marker" @@ -87,3 +93,39 @@ class MyState(EvenMoreBasicBaseState): rebuilt = MyState.get_fields()["name"] assert rebuilt._check is check # pyright: ignore[reportAttributeAccessIssue] + + +def _type_alias_types() -> list[type]: + native = getattr(typing, "TypeAliasType", None) + return ( + [TypeAliasType] if native in (None, TypeAliasType) else [TypeAliasType, native] + ) + + +@pytest.mark.parametrize("alias_cls", _type_alias_types()) +def test_guess_type_resolves_type_alias(alias_cls: type) -> None: + """A TypeAliasType (PEP 695 ``type`` statement) resolves to its value. + + State var annotations like ``type Key = Literal[...]`` reach guess_type as + a TypeAliasType, which must be unwrapped instead of raising TypeError. + """ + alias = alias_cls("ChartKey", Literal["day", "week"]) + + var = Var(_js_expr="key", _var_type=alias).guess_type() + assert isinstance(var, StringVar) + assert var._var_type == Literal["day", "week"] + + optional_var = Var(_js_expr="key", _var_type=alias | None).guess_type() + assert isinstance(optional_var, StringVar) + + +@pytest.mark.parametrize("alias_cls", _type_alias_types()) +def test_state_var_type_alias(alias_cls: type) -> None: + """A state var annotated with a TypeAliasType compiles.""" + chart_key = alias_cls("ChartKey", Literal["day", "week"]) + + class TypeAliasState(State): + key: chart_key = "day" # pyright: ignore[reportInvalidTypeForm] + + assert isinstance(TypeAliasState.key, StringVar) + assert TypeAliasState.key._var_type == Literal["day", "week"] diff --git a/tests/units/vars/test_base.py b/tests/units/vars/test_base.py index 6e702169bcf..e4d7e363e3c 100644 --- a/tests/units/vars/test_base.py +++ b/tests/units/vars/test_base.py @@ -77,52 +77,3 @@ def cv(self) -> int: replaced = cv._replace(_var_type=float) assert replaced._var_type is float - - -def _type_alias_types() -> list[type]: - import typing - - from typing_extensions import TypeAliasType - - native = getattr(typing, "TypeAliasType", None) - return ( - [TypeAliasType] if native in (None, TypeAliasType) else [TypeAliasType, native] - ) - - -@pytest.mark.parametrize("alias_cls", _type_alias_types()) -def test_guess_type_resolves_type_alias(alias_cls: type) -> None: - """A TypeAliasType (PEP 695 ``type`` statement) resolves to its value. - - State var annotations like ``type Key = Literal[...]`` reach guess_type as - a TypeAliasType, which must be unwrapped instead of raising TypeError. - """ - from typing import Literal - - from reflex_base.vars.base import Var - from reflex_base.vars.sequence import StringVar - - alias = alias_cls("ChartKey", Literal["day", "week"]) - - var = Var(_js_expr="key", _var_type=alias).guess_type() - assert isinstance(var, StringVar) - assert var._var_type == Literal["day", "week"] - - optional_var = Var(_js_expr="key", _var_type=alias | None).guess_type() - assert isinstance(optional_var, StringVar) - - -@pytest.mark.parametrize("alias_cls", _type_alias_types()) -def test_state_var_type_alias(alias_cls: type) -> None: - """A state var annotated with a TypeAliasType compiles.""" - from typing import Literal - - from reflex_base.vars.sequence import StringVar - - chart_key = alias_cls("ChartKey", Literal["day", "week"]) - - class TypeAliasState(State): - key: chart_key = "day" # pyright: ignore[reportInvalidTypeForm] - - assert isinstance(TypeAliasState.key, StringVar) - assert TypeAliasState.key._var_type == Literal["day", "week"] From cbea3453c2bb754c3f4a9dca731a3e35fc623a13 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Tue, 25 Aug 2026 12:22:11 +0200 Subject: [PATCH 3/5] add news fragment --- packages/reflex-base/news/6944.bugfix.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/reflex-base/news/6944.bugfix.md diff --git a/packages/reflex-base/news/6944.bugfix.md b/packages/reflex-base/news/6944.bugfix.md new file mode 100644 index 00000000000..ee164929c25 --- /dev/null +++ b/packages/reflex-base/news/6944.bugfix.md @@ -0,0 +1 @@ +Resolve `TypeAliasType` annotations (PEP 695 `type` statements and the `typing_extensions` backport) to their underlying value in `Var.guess_type`, so state vars annotated with an alias like `type Key = Literal["day", "week"]` compile instead of raising `TypeError: Unsupported type ... for guess_type`. Aliases nested in unions (`Key | None`) are resolved as well. From 9e92905d7327ebe8530fd4c87ef640b0e87a3213 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Tue, 25 Aug 2026 13:44:05 +0200 Subject: [PATCH 4/5] resolve parameterized generic type aliases --- packages/reflex-base/news/6944.bugfix.md | 2 +- .../src/reflex_base/utils/types.py | 25 +++++++++++++-- tests/units/reflex_base/vars/test_base.py | 31 +++++++++++++++++-- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/packages/reflex-base/news/6944.bugfix.md b/packages/reflex-base/news/6944.bugfix.md index ee164929c25..e1d389781e1 100644 --- a/packages/reflex-base/news/6944.bugfix.md +++ b/packages/reflex-base/news/6944.bugfix.md @@ -1 +1 @@ -Resolve `TypeAliasType` annotations (PEP 695 `type` statements and the `typing_extensions` backport) to their underlying value in `Var.guess_type`, so state vars annotated with an alias like `type Key = Literal["day", "week"]` compile instead of raising `TypeError: Unsupported type ... for guess_type`. Aliases nested in unions (`Key | None`) are resolved as well. +Resolve `TypeAliasType` annotations (PEP 695 `type` statements and the `typing_extensions` backport) to their underlying value in `Var.guess_type`, so state vars annotated with an alias like `type Key = Literal["day", "week"]` compile instead of raising `TypeError: Unsupported type ... for guess_type`. Parameterized generic aliases (`Keys[str]` for `type Keys[T] = list[T]`) and aliases nested in unions (`Key | None`) are resolved as well. diff --git a/packages/reflex-base/src/reflex_base/utils/types.py b/packages/reflex-base/src/reflex_base/utils/types.py index eae0d5ff43b..aabb7712cc6 100644 --- a/packages/reflex-base/src/reflex_base/utils/types.py +++ b/packages/reflex-base/src/reflex_base/utils/types.py @@ -364,7 +364,9 @@ def is_classvar(a_type: Any) -> bool: def resolve_type_alias(cls: GenericType) -> GenericType: """Resolve a TypeAliasType (PEP 695 ``type`` statement) to its underlying value. - Aliases appearing as members of a union are resolved as well. + Handles bare aliases, subscripted generic aliases (``Keys[str]`` for + ``type Keys[T] = list[T]``, substituting the type parameters into the + alias value), and aliases appearing as members of a union. Args: cls: The type to resolve. @@ -374,10 +376,27 @@ def resolve_type_alias(cls: GenericType) -> GenericType: """ while isinstance(cls, TypeAliasTypes): cls = cls.__value__ + origin = get_origin(cls) + if isinstance(origin, TypeAliasTypes): + value = resolve_type_alias(origin.__value__) + if params := getattr(value, "__parameters__", ()): + # Map via the alias's type parameters: the value's __parameters__ + # are in appearance order, which may differ. + substitution = dict( + zip(origin.__type_params__, get_args(cls), strict=False) + ) + value = value[ # pyright: ignore[reportIndexIssue] + tuple(substitution.get(param, param) for param in params) + ] + return resolve_type_alias(value) if is_union(cls): args = get_args(cls) - if any(isinstance(arg, TypeAliasTypes) for arg in args): - return unionize(*(resolve_type_alias(arg) for arg in args)) + resolved_args = tuple(resolve_type_alias(arg) for arg in args) + if any( + resolved is not arg + for resolved, arg in zip(resolved_args, args, strict=True) + ): + return unionize(*resolved_args) return cls diff --git a/tests/units/reflex_base/vars/test_base.py b/tests/units/reflex_base/vars/test_base.py index 570d7a37674..7fe6f73e601 100644 --- a/tests/units/reflex_base/vars/test_base.py +++ b/tests/units/reflex_base/vars/test_base.py @@ -2,12 +2,13 @@ import threading import typing -from typing import Any, Literal +from typing import Any, Literal, TypeVar import pytest from reflex_base.utils.types import get_field_type from reflex_base.vars.base import EvenMoreBasicBaseState, Var, field -from reflex_base.vars.sequence import StringVar +from reflex_base.vars.object import ObjectVar +from reflex_base.vars.sequence import ArrayVar, StringVar from typing_extensions import TypeAliasType from reflex.state import State @@ -119,6 +120,32 @@ def test_guess_type_resolves_type_alias(alias_cls: type) -> None: assert isinstance(optional_var, StringVar) +@pytest.mark.parametrize("alias_cls", _type_alias_types()) +def test_guess_type_resolves_parameterized_type_alias(alias_cls: type) -> None: + """A subscripted generic alias (``type Keys[T] = list[T]``) resolves. + + The subscription keeps the TypeAliasType as the origin, so resolution has + to substitute the alias's type parameters into its value. + """ + t = TypeVar("t") + keys = alias_cls("Keys", list[t], type_params=(t,)) # pyright: ignore[reportGeneralTypeIssues] + + var = Var(_js_expr="keys", _var_type=keys[str]).guess_type() + assert isinstance(var, ArrayVar) + assert var._var_type == list[str] + + optional_var = Var(_js_expr="keys", _var_type=keys[str] | None).guess_type() + assert isinstance(optional_var, ArrayVar) + + k = TypeVar("k") + v = TypeVar("v") + # value's __parameters__ order (v, k) differs from type_params (k, v) + pair = alias_cls("Pair", dict[v, k], type_params=(k, v)) # pyright: ignore[reportGeneralTypeIssues] + pair_var = Var(_js_expr="pair", _var_type=pair[str, int]).guess_type() + assert isinstance(pair_var, ObjectVar) + assert pair_var._var_type == dict[int, str] + + @pytest.mark.parametrize("alias_cls", _type_alias_types()) def test_state_var_type_alias(alias_cls: type) -> None: """A state var annotated with a TypeAliasType compiles.""" From 098f76acc27b300f70dabc6533b3f619a154e444 Mon Sep 17 00:00:00 2001 From: Benedikt Bartscher Date: Tue, 25 Aug 2026 14:00:12 +0200 Subject: [PATCH 5/5] expand TypeVarTuple arguments when resolving variadic aliases --- .../src/reflex_base/utils/types.py | 54 ++++++++++++++++--- tests/units/reflex_base/vars/test_base.py | 27 +++++++++- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/packages/reflex-base/src/reflex_base/utils/types.py b/packages/reflex-base/src/reflex_base/utils/types.py index aabb7712cc6..c9185ea5ff9 100644 --- a/packages/reflex-base/src/reflex_base/utils/types.py +++ b/packages/reflex-base/src/reflex_base/utils/types.py @@ -37,7 +37,7 @@ from typing import get_type_hints as get_type_hints_og from typing_extensions import Self as Self -from typing_extensions import TypeAliasType +from typing_extensions import TypeAliasType, TypeVarTuple from typing_extensions import override as override from reflex_base import constants @@ -59,6 +59,14 @@ else (TypeAliasType,) ) +# Potential TypeVarTuple classes for isinstance checks (native on 3.11+, +# typing_extensions backport otherwise). +TypeVarTuples: tuple[type, ...] = ( + (TypeVarTuple, typing.TypeVarTuple) + if sys.version_info >= (3, 11) + else (TypeVarTuple,) +) + # Union of generic types. GenericType = type | _GenericAlias @@ -361,6 +369,36 @@ def is_classvar(a_type: Any) -> bool: ) +def _match_type_args( + type_params: tuple[Any, ...], args: tuple[Any, ...] +) -> dict[Any, Any]: + """Match subscription arguments to type parameters. + + A TypeVarTuple absorbs the middle arguments (mapped to a tuple); plain + parameters before and after it match positionally from either end. + + Args: + type_params: The alias's type parameters. + args: The subscription arguments. + + Returns: + A mapping from each type parameter to its argument(s). + """ + tvt_index = next( + (i for i, p in enumerate(type_params) if isinstance(p, TypeVarTuples)), None + ) + if tvt_index is None: + return dict(zip(type_params, args, strict=False)) + n_after = len(type_params) - tvt_index - 1 + substitution: dict[Any, Any] = dict( + zip(type_params[:tvt_index], args[:tvt_index], strict=False) + ) + substitution[type_params[tvt_index]] = args[tvt_index : len(args) - n_after] + if n_after: + substitution.update(zip(type_params[-n_after:], args[-n_after:], strict=False)) + return substitution + + def resolve_type_alias(cls: GenericType) -> GenericType: """Resolve a TypeAliasType (PEP 695 ``type`` statement) to its underlying value. @@ -382,12 +420,14 @@ def resolve_type_alias(cls: GenericType) -> GenericType: if params := getattr(value, "__parameters__", ()): # Map via the alias's type parameters: the value's __parameters__ # are in appearance order, which may differ. - substitution = dict( - zip(origin.__type_params__, get_args(cls), strict=False) - ) - value = value[ # pyright: ignore[reportIndexIssue] - tuple(substitution.get(param, param) for param in params) - ] + substitution = _match_type_args(origin.__type_params__, get_args(cls)) + flattened: list[Any] = [] + for param in params: + if isinstance(param, TypeVarTuples): + flattened.extend(substitution.get(param, (param,))) + else: + flattened.append(substitution.get(param, param)) + value = value[tuple(flattened)] # pyright: ignore[reportIndexIssue] return resolve_type_alias(value) if is_union(cls): args = get_args(cls) diff --git a/tests/units/reflex_base/vars/test_base.py b/tests/units/reflex_base/vars/test_base.py index 7fe6f73e601..6d4cf4b11c4 100644 --- a/tests/units/reflex_base/vars/test_base.py +++ b/tests/units/reflex_base/vars/test_base.py @@ -9,7 +9,7 @@ from reflex_base.vars.base import EvenMoreBasicBaseState, Var, field from reflex_base.vars.object import ObjectVar from reflex_base.vars.sequence import ArrayVar, StringVar -from typing_extensions import TypeAliasType +from typing_extensions import TypeAliasType, TypeVarTuple, Unpack from reflex.state import State @@ -146,6 +146,31 @@ def test_guess_type_resolves_parameterized_type_alias(alias_cls: type) -> None: assert pair_var._var_type == dict[int, str] +@pytest.mark.parametrize("alias_cls", _type_alias_types()) +def test_guess_type_resolves_variadic_type_alias(alias_cls: type) -> None: + """A variadic alias (``type Tup[*Ts] = tuple[*Ts]``) keeps all arguments. + + The TypeVarTuple must absorb every remaining subscription argument, not + just the one a plain positional zip would pair it with. + """ + ts = TypeVarTuple("ts") + tup = alias_cls("Tup", tuple[Unpack[ts]], type_params=(ts,)) # pyright: ignore[reportGeneralTypeIssues] + var = Var(_js_expr="t", _var_type=tup[str, int]).guess_type() + assert isinstance(var, ArrayVar) + assert var._var_type == tuple[str, int] + + t = TypeVar("t") + prefixed = alias_cls("Prefixed", dict[t, tuple[Unpack[ts]]], type_params=(t, ts)) # pyright: ignore[reportGeneralTypeIssues] + prefixed_var = Var(_js_expr="p", _var_type=prefixed[str, int, float]).guess_type() + assert isinstance(prefixed_var, ObjectVar) + assert prefixed_var._var_type == dict[str, tuple[int, float]] + + suffixed = alias_cls("Suffixed", dict[t, tuple[Unpack[ts]]], type_params=(ts, t)) # pyright: ignore[reportGeneralTypeIssues] + suffixed_var = Var(_js_expr="s", _var_type=suffixed[int, float, str]).guess_type() + assert isinstance(suffixed_var, ObjectVar) + assert suffixed_var._var_type == dict[str, tuple[int, float]] + + @pytest.mark.parametrize("alias_cls", _type_alias_types()) def test_state_var_type_alias(alias_cls: type) -> None: """A state var annotated with a TypeAliasType compiles."""