Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/api-reference/var_system.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ If the type is known, it can be any of the following:
- `BooleanVar` represents a boolean expression. For example: `false`, `3 > 2`.
- `StringVar` represents an expression that evaluates to a string. For example: `'hello'`, `(2).toString()`.
- `ArrayVar` represents an expression that evaluates to an array object. For example: `[1, 2, 3]`, `'words'.split()`.
- `SetVar` represents a Python `set` or `frozenset`. The value is an array without duplicates on the frontend, and set operations such as `contains`, `union` and `issubset` compile over it; call `to_array()` to use it as an `ArrayVar`.
Comment thread
FarhanAliRaza marked this conversation as resolved.
- `ObjectVar` represents an expression that evaluates to an object. For example: `{a: 2, b: 3}`, `{deeply: {nested: {value: false}}}`.
- `NoneVar` represent null values. These can be either `undefined` or `null`.

Expand All @@ -41,6 +42,7 @@ State fields are converted to `Var` by default. Additionally, you can create a `
rx.Var.create(4) # NumberVar
rx.Var.create("hello") # StringVar
rx.Var.create([1, 2, 3]) # ArrayVar
rx.Var.create({1, 2, 3}) # SetVar
```

If you want to explicitly create a `Var` from a raw Javascript string, you can instantiate `rx.Var` directly:
Expand Down
31 changes: 31 additions & 0 deletions docs/vars/var-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,37 @@ def var_map_filter_reduce_example():
)
```

### Set Operations

A var annotated as `set` or `frozenset` supports set operations rather than list operations: `contains`, `length`, `union`, `intersection`, `difference`, `symmetric_difference`, `issubset`, `issuperset` and `isdisjoint`. The other operand may be another set var, a list var, or a plain Python set or list.
Comment thread
FarhanAliRaza marked this conversation as resolved.

Sets have no order or index. To use list operations such as indexing, `map` or `join`, call `to_array()` first; `rx.foreach` accepts a set var directly.

```python demo exec
class SetState(rx.State):
tags: set[str] = {"python", "reflex"}
featured: set[str] = {"reflex", "react"}

@rx.event
def toggle_react(self):
self.tags.symmetric_difference_update({"react"})


def var_set_example():
return rx.vstack(
rx.text(f"Tags: {SetState.tags.to_array().join(', ')}"),
rx.text(f"Has react: {SetState.tags.contains('react')}"),
rx.text(
f"Union: {SetState.tags.union(SetState.featured).to_array().join(', ')}"
),
rx.text(
f"Intersection: {SetState.tags.intersection(SetState.featured).to_array().join(', ')}"
),
rx.text(f"Is subset of featured: {SetState.tags.issubset(SetState.featured)}"),
rx.button("Toggle react", on_click=SetState.toggle_react),
)
```

### Lower, Upper, Split

The `lower` operator converts a string var to lowercase. The `upper` operator converts a string var to uppercase. The `split` operator splits a string var into a list.
Expand Down
6 changes: 6 additions & 0 deletions news/+set-var-array-operations.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
A `set` or `frozenset` state var is now a `SetVar` rather than an `ArrayVar`, so it no longer type-checks as a sequence in frontend code: indexing, slicing, `map`, `filter`, `join`, `reverse`, `+` and `*` on it are deprecated. They still compile for now with a deprecation warning and will be removed in 1.0. Call `.to_array()` first:

```python
State.tags[0] # deprecated
State.tags.to_array()[0]
```
1 change: 1 addition & 0 deletions news/+set-var.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `rx.vars.SetVar` for `set[T]` and `frozenset[T]` state vars and literals, with set operations (`contains`, `union`, `intersection`, `issubset`, ...) and `to_array()` for list operations and `rx.foreach`. Array operations used directly on a set var are deprecated.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
A `set` or `frozenset` state var is now a `SetVar` rather than an `ArrayVar`, so it no longer type-checks as a sequence in frontend code: indexing, slicing, `map`, `filter`, `join`, `reverse`, `+` and `*` on it are deprecated. They still compile for now with a deprecation warning and will be removed in 1.0. Call `.to_array()` first:

```python
State.tags[0] # deprecated
State.tags.to_array()[0]
```
1 change: 1 addition & 0 deletions packages/reflex-base/news/+set-var.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add `SetVar` for `set[T]` and `frozenset[T]` state vars, computed vars and literals, with `contains`, `length`, `union`, `intersection`, `difference`, `symmetric_difference`, `issubset`, `issuperset`, `isdisjoint` and `to_array` operations. Elements compare by value, so sets of tuples work. The value still travels to the frontend as a JSON array; `rx.foreach` accepts a set var directly. `frozenset` is now a serializable state var type and is accepted by `LiteralVar.create`.
Original file line number Diff line number Diff line change
Expand Up @@ -1827,6 +1827,104 @@ export const pyFlatMap = (arr, fn) =>
throw new TypeError(`flat_map value is not iterable: ${value}`);
});

/**
* Key a set element by value, so structured elements (tuples, dataclasses)
* compare the way Python compares them rather than by object identity.
* Numbers, booleans and null are their own key; strings and objects get
* distinct prefixes so a string can never share a key with an object's JSON.
* @param {*} x The set element.
* @returns {*} The key of the element.
*/
export const pySetKey = (x) => {
if (typeof x === "string") return "s" + x;
if (x !== null && typeof x === "object") return "o" + JSON.stringify(x);
return x;
};

/**
* Check whether a set (as an array) contains an element.
* @param {Array} arr The set.
* @param {*} x The element to look for.
* @returns {boolean} Whether the set contains the element.
*/
export const pySetHas = (arr, x) => {
const key = pySetKey(x);
return arr.some((y) => pySetKey(y) === key);
};

/**
* The elements of either set, keeping the order of a then b.
* @param {Array} a The first set.
* @param {Array} b The second set.
* @returns {Array} The union.
*/
export const pySetUnion = (a, b) => {
const seen = new Set(a.map(pySetKey));
const result = [...a];
for (const x of b) {
const key = pySetKey(x);
if (!seen.has(key)) {
seen.add(key);
result.push(x);
}
}
return result;
};

/**
* The elements of a that are also in b.
* @param {Array} a The first set.
* @param {Array} b The second set.
* @returns {Array} The intersection.
*/
export const pySetIntersection = (a, b) => {
const keys = new Set(b.map(pySetKey));
return a.filter((x) => keys.has(pySetKey(x)));
};

/**
* The elements of a that are not in b.
* @param {Array} a The first set.
* @param {Array} b The second set.
* @returns {Array} The difference.
*/
export const pySetDifference = (a, b) => {
const keys = new Set(b.map(pySetKey));
return a.filter((x) => !keys.has(pySetKey(x)));
};

/**
* The elements in exactly one of a and b, without duplicates even when an
* operand is a plain array that holds some.
* @param {Array} a The first set.
* @param {Array} b The second set.
* @returns {Array} The symmetric difference.
*/
export const pySetSymmetricDifference = (a, b) =>
pySetUnion(pySetDifference(a, b), pySetDifference(b, a));

/**
* Whether every element of a is in b.
* @param {Array} a The first set.
* @param {Array} b The second set.
* @returns {boolean} Whether a is a subset of b.
*/
export const pySetIsSubset = (a, b) => {
const keys = new Set(b.map(pySetKey));
return a.every((x) => keys.has(pySetKey(x)));
};

/**
* Whether a and b have no element in common.
* @param {Array} a The first set.
* @param {Array} b The second set.
* @returns {boolean} Whether the sets are disjoint.
*/
export const pySetIsDisjoint = (a, b) => {
const keys = new Set(b.map(pySetKey));
return !a.some((x) => keys.has(pySetKey(x)));
};

/**
* Merge refs into a single callback ref, attaching the node to all of them.
*
Expand Down
13 changes: 13 additions & 0 deletions packages/reflex-base/src/reflex_base/utils/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,19 @@ def serialize_set(value: set) -> list:
return list(value)


@serializer
def serialize_frozenset(value: frozenset) -> list:
"""Serialize a frozenset to a JSON serializable list.

Args:
value: The frozenset to serialize.

Returns:
The serialized list.
"""
return list(value)


@serializer
def serialize_sequence(value: Sequence) -> list:
"""Serialize a sequence to a JSON serializable list.
Expand Down
6 changes: 5 additions & 1 deletion packages/reflex-base/src/reflex_base/vars/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Immutable-Based Var System."""

from . import base, color, datetime, function, number, object, sequence
from . import base, color, datetime, function, number, object, sequence, set
from .base import (
EMPTY_VAR_INT,
EMPTY_VAR_STR,
Expand Down Expand Up @@ -29,6 +29,7 @@
LiteralStringVar,
StringVar,
)
from .set import LiteralSetVar, SetVar
from .special import use_hook_var, use_id

__all__ = [
Expand All @@ -49,11 +50,13 @@
"LiteralColorVar",
"LiteralNumberVar",
"LiteralObjectVar",
"LiteralSetVar",
"LiteralStringVar",
"LiteralVar",
"NumberVar",
"ObjectVar",
"RestProp",
"SetVar",
"StringVar",
"Var",
"VarData",
Expand All @@ -69,6 +72,7 @@
"number",
"object",
"sequence",
"set",
"use_hook_var",
"use_id",
"var_operation",
Expand Down
76 changes: 69 additions & 7 deletions packages/reflex-base/src/reflex_base/vars/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,15 @@
from .number import BooleanVar, LiteralBooleanVar, LiteralNumberVar, NumberVar
from .object import LiteralObjectVar, ObjectVar
from .sequence import ArrayVar, LiteralArrayVar, LiteralStringVar, StringVar
from .set import LiteralSetVar, SetVar


VAR_TYPE = TypeVar("VAR_TYPE", covariant=True)
OTHER_VAR_TYPE = TypeVar("OTHER_VAR_TYPE")
STRING_T = TypeVar("STRING_T", bound=str)
LITERAL_STRING_T = TypeVar("LITERAL_STRING_T", bound=LiteralString)
SEQUENCE_TYPE = TypeVar("SEQUENCE_TYPE", bound=Sequence)
SET_TYPE = TypeVar("SET_TYPE", bound=set | frozenset)

warnings.filterwarnings("ignore", message="fields may not start with an underscore")

Expand Down Expand Up @@ -1046,6 +1048,14 @@ def create(
_var_data: VarData | None = None,
) -> LiteralArrayVar[SEQUENCE_TYPE]: ...

@overload
@classmethod
def create(
cls,
value: SET_TYPE,
_var_data: VarData | None = None,
) -> LiteralSetVar[SET_TYPE]: ...

@overload
@classmethod
def create(
Expand Down Expand Up @@ -1112,6 +1122,12 @@ def to(
output: type[SEQUENCE_TYPE],
) -> ArrayVar[SEQUENCE_TYPE]: ...

@overload
def to(
self,
output: type[SET_TYPE],
) -> SetVar[SET_TYPE]: ...

@overload
def to(
self,
Expand Down Expand Up @@ -1884,6 +1900,7 @@ def _create_literal_var(
# (EMPTY_VAR_STR is built while base.py is still loading).
from .object import LiteralObjectVar
from .sequence import ArrayVar, LiteralStringVar
from .set import LiteralSetVar # noqa: F401

if (literal_subclass := _literal_var_for(value)) is not None:
return literal_subclass.create(value, _var_data=_var_data)
Expand Down Expand Up @@ -2105,6 +2122,16 @@ def var_operation(
) -> Callable[P, ArrayVar[LIST_T]]: ...


SET_T = TypeVar("SET_T", bound=set | frozenset)


@overload
def var_operation(
func: Callable[P, CustomVarOperationReturn[SET_T]]
| Callable[P, CustomVarOperationReturn[SET_T | None]],
) -> Callable[P, SetVar[SET_T]]: ...


OBJECT_TYPE = TypeVar("OBJECT_TYPE", bound=Mapping)


Expand Down Expand Up @@ -2174,7 +2201,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)):
Expand All @@ -2183,8 +2210,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, ...]
Expand Down Expand Up @@ -2889,6 +2919,20 @@ def __get__(
owner: type,
) -> ArrayVar[tuple[LIST_INSIDE, ...]]: ...

@overload
def __get__(
self: ComputedVar[set[LIST_INSIDE]],
instance: None,
owner: type,
) -> SetVar[set[LIST_INSIDE]]: ...

@overload
def __get__(
self: ComputedVar[frozenset[LIST_INSIDE]],
instance: None,
owner: type,
) -> SetVar[frozenset[LIST_INSIDE]]: ...

@overload
def __get__(
self: ComputedVar[SQLA_TYPE],
Expand Down Expand Up @@ -3206,6 +3250,20 @@ def __get__(
owner: type,
) -> ArrayVar[tuple[LIST_INSIDE, ...]]: ...

@overload
def __get__(
self: AsyncComputedVar[set[LIST_INSIDE]],
instance: None,
owner: type,
) -> SetVar[set[LIST_INSIDE]]: ...

@overload
def __get__(
self: AsyncComputedVar[frozenset[LIST_INSIDE]],
instance: None,
owner: type,
) -> SetVar[frozenset[LIST_INSIDE]]: ...

@overload
def __get__(
self: AsyncComputedVar[SQLA_TYPE],
Expand Down Expand Up @@ -4457,10 +4515,7 @@ def __get__(

@overload
def __get__(
self: Field[list[V]]
| Field[set[V]]
| Field[list[V] | None]
| Field[set[V] | None],
self: Field[list[V]] | Field[list[V] | None],
instance: None,
owner: Any,
) -> ArrayVar[Sequence[V]]: ...
Expand All @@ -4472,6 +4527,13 @@ def __get__(
owner: Any,
) -> ArrayVar[SEQUENCE_TYPE]: ...

@overload
def __get__(
self: Field[SET_TYPE] | Field[SET_TYPE | None],
Comment thread
FarhanAliRaza marked this conversation as resolved.
instance: None,
owner: Any,
) -> SetVar[SET_TYPE]: ...

# The bounds of these type variables admit `None` (for computed vars of an
# optional object). Matched against `Field[X | None]`, a `self` union of
# `Field[T]` and `Field[T | None]` lets a checker solve `T` to `X | None`, so
Expand Down
Loading
Loading