diff --git a/packages/reflex-base/news/+frozenset-literal.bugfix.md b/packages/reflex-base/news/+frozenset-literal.bugfix.md new file mode 100644 index 00000000000..e76dd1a0bee --- /dev/null +++ b/packages/reflex-base/news/+frozenset-literal.bugfix.md @@ -0,0 +1 @@ +`LiteralVar.create` now accepts a `frozenset`, rendered as an array like a `set`, with its element type inferred so `rx.foreach` and array operations over it are typed. diff --git a/packages/reflex-base/src/reflex_base/vars/base.py b/packages/reflex-base/src/reflex_base/vars/base.py index 0671d80c917..df7f04df6a0 100644 --- a/packages/reflex-base/src/reflex_base/vars/base.py +++ b/packages/reflex-base/src/reflex_base/vars/base.py @@ -2185,7 +2185,7 @@ def figure_out_type(value: Any) -> types.GenericType: Returns: The type of the value. """ - if isinstance(value, (list, set, tuple, Mapping, Var)): + if isinstance(value, (list, set, frozenset, tuple, Mapping, Var)): if isinstance(value, Var): return value._var_type if has_args(value_type := type(value)): @@ -2194,8 +2194,11 @@ def figure_out_type(value: Any) -> types.GenericType: if not value: return Sequence[NoReturn] return Sequence[unionize(*{figure_out_type(v) for v in value[:100]})] - if isinstance(value, set): - return set[unionize(*{figure_out_type(v) for v in value})] + if isinstance(value, (set, frozenset)): + element_type = unionize(*{figure_out_type(v) for v in value}) + if isinstance(value, frozenset): + return frozenset[element_type] + return set[element_type] if isinstance(value, tuple): if not value: return tuple[NoReturn, ...] diff --git a/packages/reflex-base/src/reflex_base/vars/sequence.py b/packages/reflex-base/src/reflex_base/vars/sequence.py index 1ded6c57ab1..6798fa0cc9c 100644 --- a/packages/reflex-base/src/reflex_base/vars/sequence.py +++ b/packages/reflex-base/src/reflex_base/vars/sequence.py @@ -63,7 +63,7 @@ VALUE_TYPE = TypeVar("VALUE_TYPE") -class ArrayVar(Var[ARRAY_VAR_TYPE], python_types=(Sequence, set)): +class ArrayVar(Var[ARRAY_VAR_TYPE], python_types=(Sequence, set, frozenset)): """Base class for immutable array vars.""" def join(self, sep: StringVar | str = "") -> StringVar: @@ -1920,6 +1920,7 @@ def _determine_value_of_array_index( Iterable, list, set, + frozenset, collections.abc.Sequence, collections.abc.Iterable, ]: diff --git a/tests/units/test_var.py b/tests/units/test_var.py index fe0639d53ac..b7f81370f27 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -51,6 +51,7 @@ ConcatVarOperation, LiteralArrayVar, LiteralStringVar, + StringVar, ) import reflex as rx @@ -421,6 +422,19 @@ def test_date_comparison_uses_compare_datetime(): assert str(lhs == rhs) == '(compareDatetime("2024-01-01", "2024-01-02") === 0)' +def test_frozenset_literal_types_its_elements(): + """A frozen set literal infers its element type like a set does.""" + # The static array bound is Sequence, which no set satisfies; see the PR. + var = cast(ArrayVar, v(frozenset({"a"}))) + + assert var._var_type == frozenset[str] + assert isinstance(var[0], StringVar) + assert re.fullmatch( + r'\["a"\]\.map\(\(\((\w+)\) => \1\.toUpperCase\(\)\)\)', + str(var.map(lambda item: item.upper())), + ) + + def test_datetime_equality_with_other_type_uses_default_comparison(): value = v(datetime(2024, 1, 1)) expected_equality = ( @@ -436,6 +450,7 @@ def test_datetime_equality_with_other_type_uses_default_comparison(): [ (v([1, 2, 3]), "[1, 2, 3]"), (v({1, 2, 3}), "[1, 2, 3]"), + (v(frozenset({1, 2, 3})), "[1, 2, 3]"), (v(["1", "2", "3"]), '["1", "2", "3"]'), ( Var(_js_expr="foo")._var_set_state("state").to(list),