From f3426456add1c0b29eda5c49ab9081b11d0aed87 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 3 Sep 2024 11:33:54 -0700 Subject: [PATCH 01/22] fix var in bare (#3873) --- reflex/components/base/bare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/components/base/bare.py b/reflex/components/base/bare.py index dca9a9287..2ed0cb590 100644 --- a/reflex/components/base/bare.py +++ b/reflex/components/base/bare.py @@ -28,7 +28,7 @@ class Bare(Component): """ if isinstance(contents, ImmutableVar): return cls(contents=contents) - if isinstance(contents, Var) and contents._get_all_var_data(): + if isinstance(contents, Var): contents = contents.to(str) else: contents = str(contents) if contents is not None else "" From c07a983f0520e953879f3397dae22a7bbe80c5dc Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Tue, 3 Sep 2024 11:39:05 -0700 Subject: [PATCH 02/22] add var_operation and move some operations to the new style (#3841) * add var_operations and move some operations to the new style * change bound style * can't assume int anymore * slice is not hashable (how did this work bef) * convert to int explicitly * move the rest of the operations to new style * fix bool guess type * forgot to precommit dangit * add type ignore to bool for now --- reflex/components/core/cond.py | 13 +- reflex/ivars/__init__.py | 1 - reflex/ivars/base.py | 415 +++++++----- reflex/ivars/number.py | 932 +++++++++++--------------- reflex/ivars/object.py | 318 +++------ reflex/ivars/sequence.py | 957 +++++++-------------------- tests/components/core/test_colors.py | 4 +- tests/components/core/test_cond.py | 7 +- tests/test_var.py | 13 +- 9 files changed, 960 insertions(+), 1700 deletions(-) diff --git a/reflex/components/core/cond.py b/reflex/components/core/cond.py index 80dd35f0f..6e6272665 100644 --- a/reflex/components/core/cond.py +++ b/reflex/components/core/cond.py @@ -9,7 +9,7 @@ from reflex.components.component import BaseComponent, Component, MemoizationLea from reflex.components.tags import CondTag, Tag from reflex.constants import Dirs from reflex.ivars.base import ImmutableVar, LiteralVar -from reflex.ivars.number import TernaryOperator +from reflex.ivars.number import ternary_operation from reflex.style import LIGHT_COLOR_MODE, resolved_color_mode from reflex.utils.imports import ImportDict, ImportVar from reflex.vars import Var, VarData @@ -163,11 +163,12 @@ def cond(condition: Any, c1: Any, c2: Any = None) -> Component | ImmutableVar: c2 = create_var(c2) # Create the conditional var. - return TernaryOperator.create( - condition=cond_var.to(bool), # type: ignore - if_true=c1, - if_false=c2, - _var_data=VarData(imports=_IS_TRUE_IMPORT), + return ternary_operation( + cond_var.bool()._replace( # type: ignore + merge_var_data=VarData(imports=_IS_TRUE_IMPORT), + ), # type: ignore + c1, + c2, ) diff --git a/reflex/ivars/__init__.py b/reflex/ivars/__init__.py index 8fa5196ff..2c1837510 100644 --- a/reflex/ivars/__init__.py +++ b/reflex/ivars/__init__.py @@ -12,7 +12,6 @@ from .number import LiteralNumberVar as LiteralNumberVar from .number import NumberVar as NumberVar from .object import LiteralObjectVar as LiteralObjectVar from .object import ObjectVar as ObjectVar -from .sequence import ArrayJoinOperation as ArrayJoinOperation from .sequence import ArrayVar as ArrayVar from .sequence import ConcatVarOperation as ConcatVarOperation from .sequence import LiteralArrayVar as LiteralArrayVar diff --git a/reflex/ivars/base.py b/reflex/ivars/base.py index f98d499a3..4cd3550dd 100644 --- a/reflex/ivars/base.py +++ b/reflex/ivars/base.py @@ -20,6 +20,7 @@ from typing import ( Generic, List, Literal, + NoReturn, Optional, Sequence, Set, @@ -384,10 +385,18 @@ class ImmutableVar(Var, Generic[VAR_TYPE]): return self.to(BooleanVar, output) if issubclass(output, NumberVar): - if fixed_type is not None and not issubclass(fixed_type, (int, float)): - raise TypeError( - f"Unsupported type {var_type} for NumberVar. Must be int or float." - ) + if fixed_type is not None: + if fixed_type is Union: + inner_types = get_args(base_type) + if not all(issubclass(t, (int, float)) for t in inner_types): + raise TypeError( + f"Unsupported type {var_type} for NumberVar. Must be int or float." + ) + + elif not issubclass(fixed_type, (int, float)): + raise TypeError( + f"Unsupported type {var_type} for NumberVar. Must be int or float." + ) return ToNumberVarOperation.create(self, var_type or float) if issubclass(output, BooleanVar): @@ -440,7 +449,7 @@ class ImmutableVar(Var, Generic[VAR_TYPE]): Raises: TypeError: If the type is not supported for guessing. """ - from .number import NumberVar + from .number import BooleanVar, NumberVar from .object import ObjectVar from .sequence import ArrayVar, StringVar @@ -454,11 +463,16 @@ class ImmutableVar(Var, Generic[VAR_TYPE]): fixed_type = get_origin(var_type) or var_type if fixed_type is Union: + inner_types = get_args(var_type) + if int in inner_types and float in inner_types: + return self.to(NumberVar, self._var_type) return self if not inspect.isclass(fixed_type): raise TypeError(f"Unsupported type {var_type} for guess_type.") + if issubclass(fixed_type, bool): + return self.to(BooleanVar, self._var_type) if issubclass(fixed_type, (int, float)): return self.to(NumberVar, self._var_type) if issubclass(fixed_type, dict): @@ -570,9 +584,9 @@ class ImmutableVar(Var, Generic[VAR_TYPE]): Returns: BooleanVar: A BooleanVar object representing the result of the equality check. """ - from .number import EqualOperation + from .number import equal_operation - return EqualOperation.create(self, other) + return equal_operation(self, other) def __ne__(self, other: Var | Any) -> BooleanVar: """Check if the current object is not equal to the given object. @@ -583,9 +597,9 @@ class ImmutableVar(Var, Generic[VAR_TYPE]): Returns: BooleanVar: A BooleanVar object representing the result of the comparison. """ - from .number import EqualOperation + from .number import equal_operation - return ~EqualOperation.create(self, other) + return ~equal_operation(self, other) def __gt__(self, other: Var | Any) -> BooleanVar: """Compare the current instance with another variable and return a BooleanVar representing the result of the greater than operation. @@ -596,9 +610,9 @@ class ImmutableVar(Var, Generic[VAR_TYPE]): Returns: BooleanVar: A BooleanVar representing the result of the greater than operation. """ - from .number import GreaterThanOperation + from .number import greater_than_operation - return GreaterThanOperation.create(self, other) + return greater_than_operation(self, other) def __ge__(self, other: Var | Any) -> BooleanVar: """Check if the value of this variable is greater than or equal to the value of another variable or object. @@ -609,9 +623,9 @@ class ImmutableVar(Var, Generic[VAR_TYPE]): Returns: BooleanVar: A BooleanVar object representing the result of the comparison. """ - from .number import GreaterThanOrEqualOperation + from .number import greater_than_or_equal_operation - return GreaterThanOrEqualOperation.create(self, other) + return greater_than_or_equal_operation(self, other) def __lt__(self, other: Var | Any) -> BooleanVar: """Compare the current instance with another variable using the less than (<) operator. @@ -622,9 +636,9 @@ class ImmutableVar(Var, Generic[VAR_TYPE]): Returns: A `BooleanVar` object representing the result of the comparison. """ - from .number import LessThanOperation + from .number import less_than_operation - return LessThanOperation.create(self, other) + return less_than_operation(self, other) def __le__(self, other: Var | Any) -> BooleanVar: """Compare if the current instance is less than or equal to the given value. @@ -635,9 +649,9 @@ class ImmutableVar(Var, Generic[VAR_TYPE]): Returns: A BooleanVar object representing the result of the comparison. """ - from .number import LessThanOrEqualOperation + from .number import less_than_or_equal_operation - return LessThanOrEqualOperation.create(self, other) + return less_than_or_equal_operation(self, other) def bool(self) -> BooleanVar: """Convert the var to a boolean. @@ -645,9 +659,9 @@ class ImmutableVar(Var, Generic[VAR_TYPE]): Returns: The boolean var. """ - from .number import ToBooleanVarOperation + from .number import boolify - return ToBooleanVarOperation.create(self) + return boolify(self) def __and__(self, other: Var | Any) -> ImmutableVar: """Perform a logical AND operation on the current instance and another variable. @@ -658,7 +672,7 @@ class ImmutableVar(Var, Generic[VAR_TYPE]): Returns: A `BooleanVar` object representing the result of the logical AND operation. """ - return AndOperation.create(self, other) + return and_operation(self, other) def __rand__(self, other: Var | Any) -> ImmutableVar: """Perform a logical AND operation on the current instance and another variable. @@ -669,7 +683,7 @@ class ImmutableVar(Var, Generic[VAR_TYPE]): Returns: A `BooleanVar` object representing the result of the logical AND operation. """ - return AndOperation.create(other, self) + return and_operation(other, self) def __or__(self, other: Var | Any) -> ImmutableVar: """Perform a logical OR operation on the current instance and another variable. @@ -680,7 +694,7 @@ class ImmutableVar(Var, Generic[VAR_TYPE]): Returns: A `BooleanVar` object representing the result of the logical OR operation. """ - return OrOperation.create(self, other) + return or_operation(self, other) def __ror__(self, other: Var | Any) -> ImmutableVar: """Perform a logical OR operation on the current instance and another variable. @@ -691,7 +705,7 @@ class ImmutableVar(Var, Generic[VAR_TYPE]): Returns: A `BooleanVar` object representing the result of the logical OR operation. """ - return OrOperation.create(other, self) + return or_operation(other, self) def __invert__(self) -> BooleanVar: """Perform a logical NOT operation on the current instance. @@ -699,9 +713,7 @@ class ImmutableVar(Var, Generic[VAR_TYPE]): Returns: A `BooleanVar` object representing the result of the logical NOT operation. """ - from .number import BooleanNotOperation - - return BooleanNotOperation.create(self.bool()) + return ~self.bool() def to_string(self) -> ImmutableVar: """Convert the var to a string. @@ -926,52 +938,92 @@ class LiteralVar(ImmutableVar): P = ParamSpec("P") -T = TypeVar("T", bound=ImmutableVar) +T = TypeVar("T") -def var_operation(*, output: Type[T]) -> Callable[[Callable[P, str]], Callable[P, T]]: +# NoReturn is used to match CustomVarOperationReturn with no type hint. +@overload +def var_operation( + func: Callable[P, CustomVarOperationReturn[NoReturn]], +) -> Callable[P, ImmutableVar]: ... + + +@overload +def var_operation( + func: Callable[P, CustomVarOperationReturn[bool]], +) -> Callable[P, BooleanVar]: ... + + +NUMBER_T = TypeVar("NUMBER_T", int, float, Union[int, float]) + + +@overload +def var_operation( + func: Callable[P, CustomVarOperationReturn[NUMBER_T]], +) -> Callable[P, NumberVar[NUMBER_T]]: ... + + +@overload +def var_operation( + func: Callable[P, CustomVarOperationReturn[str]], +) -> Callable[P, StringVar]: ... + + +LIST_T = TypeVar("LIST_T", bound=Union[List[Any], Tuple, Set]) + + +@overload +def var_operation( + func: Callable[P, CustomVarOperationReturn[LIST_T]], +) -> Callable[P, ArrayVar[LIST_T]]: ... + + +OBJECT_TYPE = TypeVar("OBJECT_TYPE", bound=Dict) + + +@overload +def var_operation( + func: Callable[P, CustomVarOperationReturn[OBJECT_TYPE]], +) -> Callable[P, ObjectVar[OBJECT_TYPE]]: ... + + +def var_operation( + func: Callable[P, CustomVarOperationReturn[T]], +) -> Callable[P, ImmutableVar[T]]: """Decorator for creating a var operation. Example: ```python - @var_operation(output=NumberVar) + @var_operation def add(a: NumberVar, b: NumberVar): - return f"({a} + {b})" + return custom_var_operation(f"{a} + {b}") ``` Args: - output: The output type of the operation. + func: The function to decorate. Returns: - The decorator. + The decorated function. """ - def decorator(func: Callable[P, str], output=output): - @functools.wraps(func) - def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: - args_vars = [ - LiteralVar.create(arg) if not isinstance(arg, Var) else arg - for arg in args - ] - kwargs_vars = { - key: LiteralVar.create(value) if not isinstance(value, Var) else value - for key, value in kwargs.items() - } - return output( - _var_name=func(*args_vars, **kwargs_vars), # type: ignore - _var_data=VarData.merge( - *[arg._get_all_var_data() for arg in args if isinstance(arg, Var)], - *[ - arg._get_all_var_data() - for arg in kwargs.values() - if isinstance(arg, Var) - ], - ), - ) + @functools.wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> ImmutableVar[T]: + func_args = list(inspect.signature(func).parameters) + args_vars = { + func_args[i]: (LiteralVar.create(arg) if not isinstance(arg, Var) else arg) + for i, arg in enumerate(args) + } + kwargs_vars = { + key: LiteralVar.create(value) if not isinstance(value, Var) else value + for key, value in kwargs.items() + } - return wrapper + return CustomVarOperation.create( + args=tuple(list(args_vars.items()) + list(kwargs_vars.items())), + return_var=func(*args_vars.values(), **kwargs_vars), # type: ignore + ).guess_type() - return decorator + return wrapper def unionize(*args: Type) -> Type: @@ -1100,114 +1152,64 @@ class CachedVarOperation: ) -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class AndOperation(CachedVarOperation, ImmutableVar): - """Class for the logical AND operation.""" +def and_operation(a: Var | Any, b: Var | Any) -> ImmutableVar: + """Perform a logical AND operation on two variables. - # The first var. - _var1: Var = dataclasses.field(default_factory=lambda: LiteralVar.create(None)) + Args: + a: The first variable. + b: The second variable. - # The second var. - _var2: Var = dataclasses.field(default_factory=lambda: LiteralVar.create(None)) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """Get the cached var name. - - Returns: - The cached var name. - """ - return f"({str(self._var1)} && {str(self._var2)})" - - def __hash__(self) -> int: - """Calculates the hash value of the object. - - Returns: - int: The hash value of the object. - """ - return hash((self.__class__.__name__, self._var1, self._var2)) - - @classmethod - def create( - cls, var1: Var | Any, var2: Var | Any, _var_data: VarData | None = None - ) -> AndOperation: - """Create an AndOperation. - - Args: - var1: The first var. - var2: The second var. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The AndOperation. - """ - var1, var2 = map(LiteralVar.create, (var1, var2)) - return AndOperation( - _var_name="", - _var_type=unionize(var1._var_type, var2._var_type), - _var_data=ImmutableVarData.merge(_var_data), - _var1=var1, - _var2=var2, - ) + Returns: + The result of the logical AND operation. + """ + return _and_operation(a, b) # type: ignore -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class OrOperation(CachedVarOperation, ImmutableVar): - """Class for the logical OR operation.""" +@var_operation +def _and_operation(a: ImmutableVar, b: ImmutableVar): + """Perform a logical AND operation on two variables. - # The first var. - _var1: Var = dataclasses.field(default_factory=lambda: LiteralVar.create(None)) + Args: + a: The first variable. + b: The second variable. - # The second var. - _var2: Var = dataclasses.field(default_factory=lambda: LiteralVar.create(None)) + Returns: + The result of the logical AND operation. + """ + return var_operation_return( + js_expression=f"({a} && {b})", + var_type=unionize(a._var_type, b._var_type), + ) - @cached_property_no_lock - def _cached_var_name(self) -> str: - """Get the cached var name. - Returns: - The cached var name. - """ - return f"({str(self._var1)} || {str(self._var2)})" +def or_operation(a: Var | Any, b: Var | Any) -> ImmutableVar: + """Perform a logical OR operation on two variables. - def __hash__(self) -> int: - """Calculates the hash value for the object. + Args: + a: The first variable. + b: The second variable. - Returns: - int: The hash value of the object. - """ - return hash((self.__class__.__name__, self._var1, self._var2)) + Returns: + The result of the logical OR operation. + """ + return _or_operation(a, b) # type: ignore - @classmethod - def create( - cls, var1: Var | Any, var2: Var | Any, _var_data: VarData | None = None - ) -> OrOperation: - """Create an OrOperation. - Args: - var1: The first var. - var2: The second var. - _var_data: Additional hooks and imports associated with the Var. +@var_operation +def _or_operation(a: ImmutableVar, b: ImmutableVar): + """Perform a logical OR operation on two variables. - Returns: - The OrOperation. - """ - var1, var2 = map(LiteralVar.create, (var1, var2)) - return OrOperation( - _var_name="", - _var_type=unionize(var1._var_type, var2._var_type), - _var_data=ImmutableVarData.merge(_var_data), - _var1=var1, - _var2=var2, - ) + Args: + a: The first variable. + b: The second variable. + + Returns: + The result of the logical OR operation. + """ + return var_operation_return( + js_expression=f"({a} || {b})", + var_type=unionize(a._var_type, b._var_type), + ) @dataclasses.dataclass( @@ -1797,3 +1799,114 @@ def immutable_computed_var( ) return wrapper + + +RETURN = TypeVar("RETURN") + + +class CustomVarOperationReturn(ImmutableVar[RETURN]): + """Base class for custom var operations.""" + + @classmethod + def create( + cls, + js_expression: str, + _var_type: Type[RETURN] | None = None, + _var_data: VarData | None = None, + ) -> CustomVarOperationReturn[RETURN]: + """Create a CustomVarOperation. + + Args: + js_expression: The JavaScript expression to evaluate. + _var_type: The type of the var. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The CustomVarOperation. + """ + return CustomVarOperationReturn( + _var_name=js_expression, + _var_type=_var_type or Any, + _var_data=ImmutableVarData.merge(_var_data), + ) + + +def var_operation_return( + js_expression: str, + var_type: Type[RETURN] | None = None, +) -> CustomVarOperationReturn[RETURN]: + """Shortcut for creating a CustomVarOperationReturn. + + Args: + js_expression: The JavaScript expression to evaluate. + var_type: The type of the var. + + Returns: + The CustomVarOperationReturn. + """ + return CustomVarOperationReturn.create(js_expression, var_type) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + **{"slots": True} if sys.version_info >= (3, 10) else {}, +) +class CustomVarOperation(CachedVarOperation, ImmutableVar[T]): + """Base class for custom var operations.""" + + _args: Tuple[Tuple[str, Var], ...] = dataclasses.field(default_factory=tuple) + + _return: CustomVarOperationReturn[T] = dataclasses.field( + default_factory=lambda: CustomVarOperationReturn.create("") + ) + + @cached_property_no_lock + def _cached_var_name(self) -> str: + """Get the cached var name. + + Returns: + The cached var name. + """ + return str(self._return) + + @cached_property_no_lock + def _cached_get_all_var_data(self) -> ImmutableVarData | None: + """Get the cached VarData. + + Returns: + The cached VarData. + """ + return ImmutableVarData.merge( + *map( + lambda arg: arg[1]._get_all_var_data(), + self._args, + ), + self._return._get_all_var_data(), + self._var_data, + ) + + @classmethod + def create( + cls, + args: Tuple[Tuple[str, Var], ...], + return_var: CustomVarOperationReturn[T], + _var_data: VarData | None = None, + ) -> CustomVarOperation[T]: + """Create a CustomVarOperation. + + Args: + args: The arguments to the operation. + return_var: The return var. + _var_data: Additional hooks and imports associated with the Var. + + Returns: + The CustomVarOperation. + """ + return CustomVarOperation( + _var_name="", + _var_type=return_var._var_type, + _var_data=ImmutableVarData.merge(_var_data), + _args=args, + _return=return_var, + ) diff --git a/reflex/ivars/number.py b/reflex/ivars/number.py index 241a1b595..68c856d18 100644 --- a/reflex/ivars/number.py +++ b/reflex/ivars/number.py @@ -5,23 +5,28 @@ from __future__ import annotations import dataclasses import json import sys -from typing import Any, Union +from typing import Any, Callable, TypeVar, Union from reflex.vars import ImmutableVarData, Var, VarData from .base import ( CachedVarOperation, + CustomVarOperationReturn, ImmutableVar, LiteralVar, cached_property_no_lock, unionize, + var_operation, + var_operation_return, ) +NUMBER_T = TypeVar("NUMBER_T", int, float, Union[int, float]) -class NumberVar(ImmutableVar[Union[int, float]]): + +class NumberVar(ImmutableVar[NUMBER_T]): """Base class for immutable number vars.""" - def __add__(self, other: number_types | boolean_types) -> NumberAddOperation: + def __add__(self, other: number_types | boolean_types): """Add two numbers. Args: @@ -30,9 +35,9 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The number addition operation. """ - return NumberAddOperation.create(self, +other) + return number_add_operation(self, +other) - def __radd__(self, other: number_types | boolean_types) -> NumberAddOperation: + def __radd__(self, other: number_types | boolean_types): """Add two numbers. Args: @@ -41,9 +46,9 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The number addition operation. """ - return NumberAddOperation.create(+other, self) + return number_add_operation(+other, self) - def __sub__(self, other: number_types | boolean_types) -> NumberSubtractOperation: + def __sub__(self, other: number_types | boolean_types): """Subtract two numbers. Args: @@ -52,9 +57,9 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The number subtraction operation. """ - return NumberSubtractOperation.create(self, +other) + return number_subtract_operation(self, +other) - def __rsub__(self, other: number_types | boolean_types) -> NumberSubtractOperation: + def __rsub__(self, other: number_types | boolean_types): """Subtract two numbers. Args: @@ -63,17 +68,17 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The number subtraction operation. """ - return NumberSubtractOperation.create(+other, self) + return number_subtract_operation(+other, self) - def __abs__(self) -> NumberAbsoluteOperation: + def __abs__(self): """Get the absolute value of the number. Returns: The number absolute operation. """ - return NumberAbsoluteOperation.create(self) + return number_abs_operation(self) - def __mul__(self, other: number_types | boolean_types) -> NumberMultiplyOperation: + def __mul__(self, other: number_types | boolean_types): """Multiply two numbers. Args: @@ -82,9 +87,9 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The number multiplication operation. """ - return NumberMultiplyOperation.create(self, +other) + return number_multiply_operation(self, +other) - def __rmul__(self, other: number_types | boolean_types) -> NumberMultiplyOperation: + def __rmul__(self, other: number_types | boolean_types): """Multiply two numbers. Args: @@ -93,9 +98,9 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The number multiplication operation. """ - return NumberMultiplyOperation.create(+other, self) + return number_multiply_operation(+other, self) - def __truediv__(self, other: number_types | boolean_types) -> NumberTrueDivision: + def __truediv__(self, other: number_types | boolean_types): """Divide two numbers. Args: @@ -104,9 +109,9 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The number true division operation. """ - return NumberTrueDivision.create(self, +other) + return number_true_division_operation(self, +other) - def __rtruediv__(self, other: number_types | boolean_types) -> NumberTrueDivision: + def __rtruediv__(self, other: number_types | boolean_types): """Divide two numbers. Args: @@ -115,9 +120,9 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The number true division operation. """ - return NumberTrueDivision.create(+other, self) + return number_true_division_operation(+other, self) - def __floordiv__(self, other: number_types | boolean_types) -> NumberFloorDivision: + def __floordiv__(self, other: number_types | boolean_types): """Floor divide two numbers. Args: @@ -126,9 +131,9 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The number floor division operation. """ - return NumberFloorDivision.create(self, +other) + return number_floor_division_operation(self, +other) - def __rfloordiv__(self, other: number_types | boolean_types) -> NumberFloorDivision: + def __rfloordiv__(self, other: number_types | boolean_types): """Floor divide two numbers. Args: @@ -137,9 +142,9 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The number floor division operation. """ - return NumberFloorDivision.create(+other, self) + return number_floor_division_operation(+other, self) - def __mod__(self, other: number_types | boolean_types) -> NumberModuloOperation: + def __mod__(self, other: number_types | boolean_types): """Modulo two numbers. Args: @@ -148,9 +153,9 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The number modulo operation. """ - return NumberModuloOperation.create(self, +other) + return number_modulo_operation(self, +other) - def __rmod__(self, other: number_types | boolean_types) -> NumberModuloOperation: + def __rmod__(self, other: number_types | boolean_types): """Modulo two numbers. Args: @@ -159,9 +164,9 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The number modulo operation. """ - return NumberModuloOperation.create(+other, self) + return number_modulo_operation(+other, self) - def __pow__(self, other: number_types | boolean_types) -> NumberExponentOperation: + def __pow__(self, other: number_types | boolean_types): """Exponentiate two numbers. Args: @@ -170,9 +175,9 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The number exponent operation. """ - return NumberExponentOperation.create(self, +other) + return number_exponent_operation(self, +other) - def __rpow__(self, other: number_types | boolean_types) -> NumberExponentOperation: + def __rpow__(self, other: number_types | boolean_types): """Exponentiate two numbers. Args: @@ -181,23 +186,23 @@ class NumberVar(ImmutableVar[Union[int, float]]): Returns: The number exponent operation. """ - return NumberExponentOperation.create(+other, self) + return number_exponent_operation(+other, self) - def __neg__(self) -> NumberNegateOperation: + def __neg__(self): """Negate the number. Returns: The number negation operation. """ - return NumberNegateOperation.create(self) + return number_negate_operation(self) - def __invert__(self) -> BooleanNotOperation: + def __invert__(self): """Boolean NOT the number. Returns: The boolean NOT operation. """ - return BooleanNotOperation.create(self.bool()) + return boolean_not_operation(self.bool()) def __pos__(self) -> NumberVar: """Positive the number. @@ -207,39 +212,39 @@ class NumberVar(ImmutableVar[Union[int, float]]): """ return self - def __round__(self) -> NumberRoundOperation: + def __round__(self): """Round the number. Returns: The number round operation. """ - return NumberRoundOperation.create(self) + return number_round_operation(self) - def __ceil__(self) -> NumberCeilOperation: + def __ceil__(self): """Ceil the number. Returns: The number ceil operation. """ - return NumberCeilOperation.create(self) + return number_ceil_operation(self) - def __floor__(self) -> NumberFloorOperation: + def __floor__(self): """Floor the number. Returns: The number floor operation. """ - return NumberFloorOperation.create(self) + return number_floor_operation(self) - def __trunc__(self) -> NumberTruncOperation: + def __trunc__(self): """Trunc the number. Returns: The number trunc operation. """ - return NumberTruncOperation.create(self) + return number_trunc_operation(self) - def __lt__(self, other: Any) -> LessThanOperation: + def __lt__(self, other: Any): """Less than comparison. Args: @@ -249,10 +254,10 @@ class NumberVar(ImmutableVar[Union[int, float]]): The result of the comparison. """ if isinstance(other, (NumberVar, BooleanVar, int, float, bool)): - return LessThanOperation.create(self, +other) - return LessThanOperation.create(self, other) + return less_than_operation(self, +other) + return less_than_operation(self, other) - def __le__(self, other: Any) -> LessThanOrEqualOperation: + def __le__(self, other: Any): """Less than or equal comparison. Args: @@ -262,10 +267,10 @@ class NumberVar(ImmutableVar[Union[int, float]]): The result of the comparison. """ if isinstance(other, (NumberVar, BooleanVar, int, float, bool)): - return LessThanOrEqualOperation.create(self, +other) - return LessThanOrEqualOperation.create(self, other) + return less_than_or_equal_operation(self, +other) + return less_than_or_equal_operation(self, other) - def __eq__(self, other: Any) -> EqualOperation: + def __eq__(self, other: Any): """Equal comparison. Args: @@ -275,10 +280,10 @@ class NumberVar(ImmutableVar[Union[int, float]]): The result of the comparison. """ if isinstance(other, (NumberVar, BooleanVar, int, float, bool)): - return EqualOperation.create(self, +other) - return EqualOperation.create(self, other) + return equal_operation(self, +other) + return equal_operation(self, other) - def __ne__(self, other: Any) -> NotEqualOperation: + def __ne__(self, other: Any): """Not equal comparison. Args: @@ -288,10 +293,10 @@ class NumberVar(ImmutableVar[Union[int, float]]): The result of the comparison. """ if isinstance(other, (NumberVar, BooleanVar, int, float, bool)): - return NotEqualOperation.create(self, +other) - return NotEqualOperation.create(self, other) + return not_equal_operation(self, +other) + return not_equal_operation(self, other) - def __gt__(self, other: Any) -> GreaterThanOperation: + def __gt__(self, other: Any): """Greater than comparison. Args: @@ -301,10 +306,10 @@ class NumberVar(ImmutableVar[Union[int, float]]): The result of the comparison. """ if isinstance(other, (NumberVar, BooleanVar, int, float, bool)): - return GreaterThanOperation.create(self, +other) - return GreaterThanOperation.create(self, other) + return greater_than_operation(self, +other) + return greater_than_operation(self, other) - def __ge__(self, other: Any) -> GreaterThanOrEqualOperation: + def __ge__(self, other: Any): """Greater than or equal comparison. Args: @@ -314,308 +319,258 @@ class NumberVar(ImmutableVar[Union[int, float]]): The result of the comparison. """ if isinstance(other, (NumberVar, BooleanVar, int, float, bool)): - return GreaterThanOrEqualOperation.create(self, +other) - return GreaterThanOrEqualOperation.create(self, other) + return greater_than_or_equal_operation(self, +other) + return greater_than_or_equal_operation(self, other) - def bool(self) -> NotEqualOperation: + def bool(self): """Boolean conversion. Returns: The boolean value of the number. """ - return NotEqualOperation.create(self, 0) + return self != 0 -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class BinaryNumberOperation(CachedVarOperation, NumberVar): - """Base class for immutable number vars that are the result of a binary operation.""" +def binary_number_operation( + func: Callable[[NumberVar, NumberVar], str], +) -> Callable[[number_types, number_types], NumberVar]: + """Decorator to create a binary number operation. - _lhs: NumberVar = dataclasses.field( - default_factory=lambda: LiteralNumberVar.create(0) - ) - _rhs: NumberVar = dataclasses.field( - default_factory=lambda: LiteralNumberVar.create(0) - ) + Args: + func: The binary number operation function. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. + Returns: + The binary number operation. + """ - Raises: - NotImplementedError: Must be implemented by subclasses - """ - raise NotImplementedError( - "BinaryNumberOperation must implement _cached_var_name" + @var_operation + def operation(lhs: NumberVar, rhs: NumberVar): + return var_operation_return( + js_expression=func(lhs, rhs), + var_type=unionize(lhs._var_type, rhs._var_type), ) - @classmethod - def create( - cls, lhs: number_types, rhs: number_types, _var_data: VarData | None = None - ): - """Create the binary number operation var. + def wrapper(lhs: number_types, rhs: number_types) -> NumberVar: + """Create the binary number operation. Args: lhs: The first number. rhs: The second number. - _var_data: Additional hooks and imports associated with the Var. Returns: - The binary number operation var. + The binary number operation. """ - _lhs, _rhs = map( - lambda v: LiteralNumberVar.create(v) if not isinstance(v, NumberVar) else v, - (lhs, rhs), - ) - return cls( - _var_name="", - _var_type=unionize(_lhs._var_type, _rhs._var_type), - _var_data=ImmutableVarData.merge(_var_data), - _lhs=_lhs, - _rhs=_rhs, - ) + return operation(lhs, rhs) # type: ignore + + return wrapper -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class UnaryNumberOperation(CachedVarOperation, NumberVar): - """Base class for immutable number vars that are the result of a unary operation.""" +@binary_number_operation +def number_add_operation(lhs: NumberVar, rhs: NumberVar): + """Add two numbers. - _value: NumberVar = dataclasses.field( - default_factory=lambda: LiteralNumberVar.create(0) + Args: + lhs: The first number. + rhs: The second number. + + Returns: + The number addition operation. + """ + return f"({lhs} + {rhs})" + + +@binary_number_operation +def number_subtract_operation(lhs: NumberVar, rhs: NumberVar): + """Subtract two numbers. + + Args: + lhs: The first number. + rhs: The second number. + + Returns: + The number subtraction operation. + """ + return f"({lhs} - {rhs})" + + +@var_operation +def number_abs_operation(value: NumberVar): + """Get the absolute value of the number. + + Args: + value: The number. + + Returns: + The number absolute operation. + """ + return var_operation_return( + js_expression=f"Math.abs({value})", var_type=value._var_type ) - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - Raises: - NotImplementedError: Must be implemented by subclasses. - """ - raise NotImplementedError( - "UnaryNumberOperation must implement _cached_var_name" - ) +@binary_number_operation +def number_multiply_operation(lhs: NumberVar, rhs: NumberVar): + """Multiply two numbers. - @classmethod - def create(cls, value: NumberVar, _var_data: VarData | None = None): - """Create the unary number operation var. + Args: + lhs: The first number. + rhs: The second number. - Args: - value: The number. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The unary number operation var. - """ - return cls( - _var_name="", - _var_type=value._var_type, - _var_data=ImmutableVarData.merge(_var_data), - _value=value, - ) + Returns: + The number multiplication operation. + """ + return f"({lhs} * {rhs})" -class NumberAddOperation(BinaryNumberOperation): - """Base class for immutable number vars that are the result of an addition operation.""" +@var_operation +def number_negate_operation( + value: NumberVar[NUMBER_T], +) -> CustomVarOperationReturn[NUMBER_T]: + """Negate the number. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. + Args: + value: The number. - Returns: - The name of the var. - """ - return f"({str(self._lhs)} + {str(self._rhs)})" + Returns: + The number negation operation. + """ + return var_operation_return(js_expression=f"-({value})", var_type=value._var_type) -class NumberSubtractOperation(BinaryNumberOperation): - """Base class for immutable number vars that are the result of a subtraction operation.""" +@binary_number_operation +def number_true_division_operation(lhs: NumberVar, rhs: NumberVar): + """Divide two numbers. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. + Args: + lhs: The first number. + rhs: The second number. - Returns: - The name of the var. - """ - return f"({str(self._lhs)} - {str(self._rhs)})" + Returns: + The number true division operation. + """ + return f"({lhs} / {rhs})" -class NumberAbsoluteOperation(UnaryNumberOperation): - """Base class for immutable number vars that are the result of an absolute operation.""" +@binary_number_operation +def number_floor_division_operation(lhs: NumberVar, rhs: NumberVar): + """Floor divide two numbers. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. + Args: + lhs: The first number. + rhs: The second number. - Returns: - The name of the var. - """ - return f"Math.abs({str(self._value)})" + Returns: + The number floor division operation. + """ + return f"Math.floor({lhs} / {rhs})" -class NumberMultiplyOperation(BinaryNumberOperation): - """Base class for immutable number vars that are the result of a multiplication operation.""" +@binary_number_operation +def number_modulo_operation(lhs: NumberVar, rhs: NumberVar): + """Modulo two numbers. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. + Args: + lhs: The first number. + rhs: The second number. - Returns: - The name of the var. - """ - return f"({str(self._lhs)} * {str(self._rhs)})" + Returns: + The number modulo operation. + """ + return f"({lhs} % {rhs})" -class NumberNegateOperation(UnaryNumberOperation): - """Base class for immutable number vars that are the result of a negation operation.""" +@binary_number_operation +def number_exponent_operation(lhs: NumberVar, rhs: NumberVar): + """Exponentiate two numbers. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. + Args: + lhs: The first number. + rhs: The second number. - Returns: - The name of the var. - """ - return f"-({str(self._value)})" + Returns: + The number exponent operation. + """ + return f"({lhs} ** {rhs})" -class NumberTrueDivision(BinaryNumberOperation): - """Base class for immutable number vars that are the result of a true division operation.""" +@var_operation +def number_round_operation(value: NumberVar): + """Round the number. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. + Args: + value: The number. - Returns: - The name of the var. - """ - return f"({str(self._lhs)} / {str(self._rhs)})" + Returns: + The number round operation. + """ + return var_operation_return(js_expression=f"Math.round({value})", var_type=int) -class NumberFloorDivision(BinaryNumberOperation): - """Base class for immutable number vars that are the result of a floor division operation.""" +@var_operation +def number_ceil_operation(value: NumberVar): + """Ceil the number. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. + Args: + value: The number. - Returns: - The name of the var. - """ - return f"Math.floor({str(self._lhs)} / {str(self._rhs)})" + Returns: + The number ceil operation. + """ + return var_operation_return(js_expression=f"Math.ceil({value})", var_type=int) -class NumberModuloOperation(BinaryNumberOperation): - """Base class for immutable number vars that are the result of a modulo operation.""" +@var_operation +def number_floor_operation(value: NumberVar): + """Floor the number. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. + Args: + value: The number. - Returns: - The name of the var. - """ - return f"({str(self._lhs)} % {str(self._rhs)})" + Returns: + The number floor operation. + """ + return var_operation_return(js_expression=f"Math.floor({value})", var_type=int) -class NumberExponentOperation(BinaryNumberOperation): - """Base class for immutable number vars that are the result of an exponent operation.""" +@var_operation +def number_trunc_operation(value: NumberVar): + """Trunc the number. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. + Args: + value: The number. - Returns: - The name of the var. - """ - return f"({str(self._lhs)} ** {str(self._rhs)})" - - -class NumberRoundOperation(UnaryNumberOperation): - """Base class for immutable number vars that are the result of a round operation.""" - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return f"Math.round({str(self._value)})" - - -class NumberCeilOperation(UnaryNumberOperation): - """Base class for immutable number vars that are the result of a ceil operation.""" - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return f"Math.ceil({str(self._value)})" - - -class NumberFloorOperation(UnaryNumberOperation): - """Base class for immutable number vars that are the result of a floor operation.""" - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return f"Math.floor({str(self._value)})" - - -class NumberTruncOperation(UnaryNumberOperation): - """Base class for immutable number vars that are the result of a trunc operation.""" - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return f"Math.trunc({str(self._value)})" + Returns: + The number trunc operation. + """ + return var_operation_return(js_expression=f"Math.trunc({value})", var_type=int) class BooleanVar(ImmutableVar[bool]): """Base class for immutable boolean vars.""" - def __invert__(self) -> BooleanNotOperation: + def __invert__(self): """NOT the boolean. Returns: The boolean NOT operation. """ - return BooleanNotOperation.create(self) + return boolean_not_operation(self) - def __int__(self) -> BooleanToIntOperation: + def __int__(self): """Convert the boolean to an int. Returns: The boolean to int operation. """ - return BooleanToIntOperation.create(self) + return boolean_to_number_operation(self) - def __pos__(self) -> BooleanToIntOperation: + def __pos__(self): """Convert the boolean to an int. Returns: The boolean to int operation. """ - return BooleanToIntOperation.create(self) + return boolean_to_number_operation(self) def bool(self) -> BooleanVar: """Boolean conversion. @@ -625,7 +580,7 @@ class BooleanVar(ImmutableVar[bool]): """ return self - def __lt__(self, other: boolean_types | number_types) -> LessThanOperation: + def __lt__(self, other: boolean_types | number_types): """Less than comparison. Args: @@ -634,9 +589,9 @@ class BooleanVar(ImmutableVar[bool]): Returns: The result of the comparison. """ - return LessThanOperation.create(+self, +other) + return less_than_operation(+self, +other) - def __le__(self, other: boolean_types | number_types) -> LessThanOrEqualOperation: + def __le__(self, other: boolean_types | number_types): """Less than or equal comparison. Args: @@ -645,9 +600,9 @@ class BooleanVar(ImmutableVar[bool]): Returns: The result of the comparison. """ - return LessThanOrEqualOperation.create(+self, +other) + return less_than_or_equal_operation(+self, +other) - def __eq__(self, other: boolean_types | number_types) -> EqualOperation: + def __eq__(self, other: boolean_types | number_types): """Equal comparison. Args: @@ -656,9 +611,9 @@ class BooleanVar(ImmutableVar[bool]): Returns: The result of the comparison. """ - return EqualOperation.create(+self, +other) + return equal_operation(+self, +other) - def __ne__(self, other: boolean_types | number_types) -> NotEqualOperation: + def __ne__(self, other: boolean_types | number_types): """Not equal comparison. Args: @@ -667,9 +622,9 @@ class BooleanVar(ImmutableVar[bool]): Returns: The result of the comparison. """ - return NotEqualOperation.create(+self, +other) + return not_equal_operation(+self, +other) - def __gt__(self, other: boolean_types | number_types) -> GreaterThanOperation: + def __gt__(self, other: boolean_types | number_types): """Greater than comparison. Args: @@ -678,11 +633,9 @@ class BooleanVar(ImmutableVar[bool]): Returns: The result of the comparison. """ - return GreaterThanOperation.create(+self, +other) + return greater_than_operation(+self, +other) - def __ge__( - self, other: boolean_types | number_types - ) -> GreaterThanOrEqualOperation: + def __ge__(self, other: boolean_types | number_types): """Greater than or equal comparison. Args: @@ -691,265 +644,151 @@ class BooleanVar(ImmutableVar[bool]): Returns: The result of the comparison. """ - return GreaterThanOrEqualOperation.create(+self, +other) + return greater_than_or_equal_operation(+self, +other) -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class BooleanToIntOperation(CachedVarOperation, NumberVar): - """Base class for immutable number vars that are the result of a boolean to int operation.""" +@var_operation +def boolean_to_number_operation(value: BooleanVar): + """Convert the boolean to a number. - _value: BooleanVar = dataclasses.field( - default_factory=lambda: LiteralBooleanVar.create(False) - ) + Args: + value: The boolean. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. + Returns: + The boolean to number operation. + """ + return var_operation_return(js_expression=f"Number({value})", var_type=int) - Returns: - The name of the var. - """ - return f"({str(self._value)} ? 1 : 0)" - @classmethod - def create(cls, value: BooleanVar, _var_data: VarData | None = None): - """Create the boolean to int operation var. +def comparison_operator( + func: Callable[[Var, Var], str], +) -> Callable[[Var | Any, Var | Any], BooleanVar]: + """Decorator to create a comparison operation. - Args: - value: The boolean. - _var_data: Additional hooks and imports associated with the Var. + Args: + func: The comparison operation function. - Returns: - The boolean to int operation var. - """ - return cls( - _var_name="", - _var_type=int, - _var_data=ImmutableVarData.merge(_var_data), - _value=value, + Returns: + The comparison operation. + """ + + @var_operation + def operation(lhs: Var, rhs: Var): + return var_operation_return( + js_expression=func(lhs, rhs), + var_type=bool, ) - -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class ComparisonOperation(CachedVarOperation, BooleanVar): - """Base class for immutable boolean vars that are the result of a comparison operation.""" - - _lhs: Var = dataclasses.field( - default_factory=lambda: LiteralBooleanVar.create(False) - ) - _rhs: Var = dataclasses.field( - default_factory=lambda: LiteralBooleanVar.create(False) - ) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Raises: - NotImplementedError: Must be implemented by subclasses. - """ - raise NotImplementedError("ComparisonOperation must implement _cached_var_name") - - @classmethod - def create(cls, lhs: Var | Any, rhs: Var | Any, _var_data: VarData | None = None): - """Create the comparison operation var. + def wrapper(lhs: Var | Any, rhs: Var | Any) -> BooleanVar: + """Create the comparison operation. Args: lhs: The first value. rhs: The second value. - _var_data: Additional hooks and imports associated with the Var. Returns: - The comparison operation var. + The comparison operation. """ - lhs, rhs = map(LiteralVar.create, (lhs, rhs)) - return cls( - _var_name="", - _var_type=bool, - _var_data=ImmutableVarData.merge(_var_data), - _lhs=lhs, - _rhs=rhs, - ) + return operation(lhs, rhs) + + return wrapper -class GreaterThanOperation(ComparisonOperation): - """Base class for immutable boolean vars that are the result of a greater than operation.""" +@comparison_operator +def greater_than_operation(lhs: Var, rhs: Var): + """Greater than comparison. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. + Args: + lhs: The first value. + rhs: The second value. - Returns: - The name of the var. - """ - return f"({str(self._lhs)} > {str(self._rhs)})" + Returns: + The result of the comparison. + """ + return f"({lhs} > {rhs})" -class GreaterThanOrEqualOperation(ComparisonOperation): - """Base class for immutable boolean vars that are the result of a greater than or equal operation.""" +@comparison_operator +def greater_than_or_equal_operation(lhs: Var, rhs: Var): + """Greater than or equal comparison. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. + Args: + lhs: The first value. + rhs: The second value. - Returns: - The name of the var. - """ - return f"({str(self._lhs)} >= {str(self._rhs)})" + Returns: + The result of the comparison. + """ + return f"({lhs} >= {rhs})" -class LessThanOperation(ComparisonOperation): - """Base class for immutable boolean vars that are the result of a less than operation.""" +@comparison_operator +def less_than_operation(lhs: Var, rhs: Var): + """Less than comparison. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. + Args: + lhs: The first value. + rhs: The second value. - Returns: - The name of the var. - """ - return f"({str(self._lhs)} < {str(self._rhs)})" + Returns: + The result of the comparison. + """ + return f"({lhs} < {rhs})" -class LessThanOrEqualOperation(ComparisonOperation): - """Base class for immutable boolean vars that are the result of a less than or equal operation.""" +@comparison_operator +def less_than_or_equal_operation(lhs: Var, rhs: Var): + """Less than or equal comparison. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. + Args: + lhs: The first value. + rhs: The second value. - Returns: - The name of the var. - """ - return f"({str(self._lhs)} <= {str(self._rhs)})" + Returns: + The result of the comparison. + """ + return f"({lhs} <= {rhs})" -class EqualOperation(ComparisonOperation): - """Base class for immutable boolean vars that are the result of an equal operation.""" +@comparison_operator +def equal_operation(lhs: Var, rhs: Var): + """Equal comparison. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. + Args: + lhs: The first value. + rhs: The second value. - Returns: - The name of the var. - """ - return f"({str(self._lhs)} === {str(self._rhs)})" + Returns: + The result of the comparison. + """ + return f"({lhs} === {rhs})" -class NotEqualOperation(ComparisonOperation): - """Base class for immutable boolean vars that are the result of a not equal operation.""" +@comparison_operator +def not_equal_operation(lhs: Var, rhs: Var): + """Not equal comparison. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. + Args: + lhs: The first value. + rhs: The second value. - Returns: - The name of the var. - """ - return f"({str(self._lhs)} !== {str(self._rhs)})" + Returns: + The result of the comparison. + """ + return f"({lhs} !== {rhs})" -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class LogicalOperation(CachedVarOperation, BooleanVar): - """Base class for immutable boolean vars that are the result of a logical operation.""" +@var_operation +def boolean_not_operation(value: BooleanVar): + """Boolean NOT the boolean. - _lhs: BooleanVar = dataclasses.field( - default_factory=lambda: LiteralBooleanVar.create(False) - ) - _rhs: BooleanVar = dataclasses.field( - default_factory=lambda: LiteralBooleanVar.create(False) - ) + Args: + value: The boolean. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Raises: - NotImplementedError: Must be implemented by subclasses. - """ - raise NotImplementedError("LogicalOperation must implement _cached_var_name") - - @classmethod - def create( - cls, lhs: boolean_types, rhs: boolean_types, _var_data: VarData | None = None - ): - """Create the logical operation var. - - Args: - lhs: The first boolean. - rhs: The second boolean. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The logical operation var. - """ - lhs, rhs = map( - lambda v: ( - LiteralBooleanVar.create(v) if not isinstance(v, BooleanVar) else v - ), - (lhs, rhs), - ) - return cls( - _var_name="", - _var_type=bool, - _var_data=ImmutableVarData.merge(_var_data), - _lhs=lhs, - _rhs=rhs, - ) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class BooleanNotOperation(CachedVarOperation, BooleanVar): - """Base class for immutable boolean vars that are the result of a logical NOT operation.""" - - _value: BooleanVar = dataclasses.field( - default_factory=lambda: LiteralBooleanVar.create(False) - ) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return f"!({str(self._value)})" - - @classmethod - def create(cls, value: boolean_types, _var_data: VarData | None = None): - """Create the logical NOT operation var. - - Args: - value: The value. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The logical NOT operation var. - """ - value = value if isinstance(value, Var) else LiteralBooleanVar.create(value) - return cls( - _var_name="", - _var_type=bool, - _var_data=ImmutableVarData.merge(_var_data), - _value=value, - ) + Returns: + The boolean NOT operation. + """ + return var_operation_return(js_expression=f"!({value})", var_type=bool) @dataclasses.dataclass( @@ -1111,7 +950,7 @@ class ToBooleanVarOperation(CachedVarOperation, BooleanVar): Returns: The name of the var. """ - return f"Boolean({str(self._original_value)})" + return str(self._original_value) @classmethod def create( @@ -1136,68 +975,35 @@ class ToBooleanVarOperation(CachedVarOperation, BooleanVar): ) -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class TernaryOperator(CachedVarOperation, ImmutableVar): - """Base class for immutable vars that are the result of a ternary operation.""" +@var_operation +def boolify(value: Var): + """Convert the value to a boolean. - _condition: BooleanVar = dataclasses.field( - default_factory=lambda: LiteralBooleanVar.create(False) - ) - _if_true: Var = dataclasses.field( - default_factory=lambda: LiteralNumberVar.create(0) - ) - _if_false: Var = dataclasses.field( - default_factory=lambda: LiteralNumberVar.create(0) + Args: + value: The value. + + Returns: + The boolean value. + """ + return var_operation_return( + js_expression=f"Boolean({value})", + var_type=bool, ) - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - Returns: - The name of the var. - """ - return ( - f"({str(self._condition)} ? {str(self._if_true)} : {str(self._if_false)})" - ) +@var_operation +def ternary_operation(condition: BooleanVar, if_true: Var, if_false: Var): + """Create a ternary operation. - @classmethod - def create( - cls, - condition: boolean_types, - if_true: Var | Any, - if_false: Var | Any, - _var_data: VarData | None = None, - ): - """Create the ternary operation var. + Args: + condition: The condition. + if_true: The value if the condition is true. + if_false: The value if the condition is false. - Args: - condition: The condition. - if_true: The value if the condition is true. - if_false: The value if the condition is false. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The ternary operation var. - """ - condition = ( - condition - if isinstance(condition, Var) - else LiteralBooleanVar.create(condition) - ) - _if_true, _if_false = map( - lambda v: (LiteralVar.create(v) if not isinstance(v, Var) else v), - (if_true, if_false), - ) - return TernaryOperator( - _var_name="", - _var_type=unionize(_if_true._var_type, _if_false._var_type), - _var_data=ImmutableVarData.merge(_var_data), - _condition=condition, - _if_true=_if_true, - _if_false=_if_false, - ) + Returns: + The ternary operation. + """ + return var_operation_return( + js_expression=f"({condition} ? {if_true} : {if_false})", + var_type=unionize(if_true._var_type, if_false._var_type), + ) diff --git a/reflex/ivars/object.py b/reflex/ivars/object.py index 5401b678a..49a21226f 100644 --- a/reflex/ivars/object.py +++ b/reflex/ivars/object.py @@ -30,6 +30,8 @@ from .base import ( LiteralVar, cached_property_no_lock, figure_out_type, + var_operation, + var_operation_return, ) from .number import BooleanVar, NumberVar from .sequence import ArrayVar, StringVar @@ -56,7 +58,9 @@ class ObjectVar(ImmutableVar[OBJECT_TYPE]): return str @overload - def _value_type(self: ObjectVar[Dict[KEY_TYPE, VALUE_TYPE]]) -> VALUE_TYPE: ... + def _value_type( + self: ObjectVar[Dict[KEY_TYPE, VALUE_TYPE]], + ) -> Type[VALUE_TYPE]: ... @overload def _value_type(self) -> Type: ... @@ -79,7 +83,7 @@ class ObjectVar(ImmutableVar[OBJECT_TYPE]): Returns: The keys of the object. """ - return ObjectKeysOperation.create(self) + return object_keys_operation(self) @overload def values( @@ -95,7 +99,7 @@ class ObjectVar(ImmutableVar[OBJECT_TYPE]): Returns: The values of the object. """ - return ObjectValuesOperation.create(self) + return object_values_operation(self) @overload def entries( @@ -111,9 +115,9 @@ class ObjectVar(ImmutableVar[OBJECT_TYPE]): Returns: The entries of the object. """ - return ObjectEntriesOperation.create(self) + return object_entries_operation(self) - def merge(self, other: ObjectVar) -> ObjectMergeOperation: + def merge(self, other: ObjectVar): """Merge two objects. Args: @@ -122,7 +126,7 @@ class ObjectVar(ImmutableVar[OBJECT_TYPE]): Returns: The merged object. """ - return ObjectMergeOperation.create(self, other) + return object_merge_operation(self, other) # NoReturn is used here to catch when key value is Any @overload @@ -270,7 +274,7 @@ class ObjectVar(ImmutableVar[OBJECT_TYPE]): Returns: The result of the check. """ - return ObjectHasOwnProperty.create(self, key) + return object_has_own_property_operation(self, key) @dataclasses.dataclass( @@ -387,207 +391,72 @@ class LiteralObjectVar(CachedVarOperation, ObjectVar[OBJECT_TYPE], LiteralVar): ) -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class ObjectToArrayOperation(CachedVarOperation, ArrayVar): - """Base class for object to array operations.""" +@var_operation +def object_keys_operation(value: ObjectVar): + """Get the keys of an object. - _value: ObjectVar = dataclasses.field( - default_factory=lambda: LiteralObjectVar.create({}) + Args: + value: The object to get the keys from. + + Returns: + The keys of the object. + """ + return var_operation_return( + js_expression=f"Object.keys({value})", + var_type=List[str], ) - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the operation. - Raises: - NotImplementedError: Must implement _cached_var_name. - """ - raise NotImplementedError( - "ObjectToArrayOperation must implement _cached_var_name" - ) +@var_operation +def object_values_operation(value: ObjectVar): + """Get the values of an object. - @classmethod - def create( - cls, - value: ObjectVar, - _var_type: GenericType | None = None, - _var_data: VarData | None = None, - ) -> ObjectToArrayOperation: - """Create the object to array operation. + Args: + value: The object to get the values from. - Args: - value: The value of the operation. - _var_data: Additional hooks and imports associated with the operation. - - Returns: - The object to array operation. - """ - return cls( - _var_name="", - _var_type=list if _var_type is None else _var_type, - _var_data=ImmutableVarData.merge(_var_data), - _value=value, - ) - - -class ObjectKeysOperation(ObjectToArrayOperation): - """Operation to get the keys of an object.""" - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the operation. - - Returns: - The name of the operation. - """ - return f"Object.keys({str(self._value)})" - - @classmethod - def create( - cls, - value: ObjectVar, - _var_data: VarData | None = None, - ) -> ObjectKeysOperation: - """Create the object keys operation. - - Args: - value: The value of the operation. - _var_data: Additional hooks and imports associated with the operation. - - Returns: - The object keys operation. - """ - return cls( - _var_name="", - _var_type=List[str], - _var_data=ImmutableVarData.merge(_var_data), - _value=value, - ) - - -class ObjectValuesOperation(ObjectToArrayOperation): - """Operation to get the values of an object.""" - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the operation. - - Returns: - The name of the operation. - """ - return f"Object.values({str(self._value)})" - - @classmethod - def create( - cls, - value: ObjectVar, - _var_data: VarData | None = None, - ) -> ObjectValuesOperation: - """Create the object values operation. - - Args: - value: The value of the operation. - _var_data: Additional hooks and imports associated with the operation. - - Returns: - The object values operation. - """ - return cls( - _var_name="", - _var_type=List[value._value_type()], - _var_data=ImmutableVarData.merge(_var_data), - _value=value, - ) - - -class ObjectEntriesOperation(ObjectToArrayOperation): - """Operation to get the entries of an object.""" - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the operation. - - Returns: - The name of the operation. - """ - return f"Object.entries({str(self._value)})" - - @classmethod - def create( - cls, - value: ObjectVar, - _var_data: VarData | None = None, - ) -> ObjectEntriesOperation: - """Create the object entries operation. - - Args: - value: The value of the operation. - _var_data: Additional hooks and imports associated with the operation. - - Returns: - The object entries operation. - """ - return cls( - _var_name="", - _var_type=List[Tuple[str, value._value_type()]], - _var_data=ImmutableVarData.merge(_var_data), - _value=value, - ) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class ObjectMergeOperation(CachedVarOperation, ObjectVar): - """Operation to merge two objects.""" - - _lhs: ObjectVar = dataclasses.field( - default_factory=lambda: LiteralObjectVar.create({}) - ) - _rhs: ObjectVar = dataclasses.field( - default_factory=lambda: LiteralObjectVar.create({}) + Returns: + The values of the object. + """ + return var_operation_return( + js_expression=f"Object.values({value})", + var_type=List[value._value_type()], ) - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the operation. - Returns: - The name of the operation. - """ - return f"({{...{str(self._lhs)}, ...{str(self._rhs)}}})" +@var_operation +def object_entries_operation(value: ObjectVar): + """Get the entries of an object. - @classmethod - def create( - cls, - lhs: ObjectVar, - rhs: ObjectVar, - _var_data: VarData | None = None, - ) -> ObjectMergeOperation: - """Create the object merge operation. + Args: + value: The object to get the entries from. - Args: - lhs: The left object to merge. - rhs: The right object to merge. - _var_data: Additional hooks and imports associated with the operation. + Returns: + The entries of the object. + """ + return var_operation_return( + js_expression=f"Object.entries({value})", + var_type=List[Tuple[str, value._value_type()]], + ) - Returns: - The object merge operation. - """ - # TODO: Figure out how to merge the types - return cls( - _var_name="", - _var_type=lhs._var_type, - _var_data=ImmutableVarData.merge(_var_data), - _lhs=lhs, - _rhs=rhs, - ) + +@var_operation +def object_merge_operation(lhs: ObjectVar, rhs: ObjectVar): + """Merge two objects. + + Args: + lhs: The first object to merge. + rhs: The second object to merge. + + Returns: + The merged object. + """ + return var_operation_return( + js_expression=f"({{...{lhs}, ...{rhs}}})", + var_type=Dict[ + Union[lhs._key_type(), rhs._key_type()], + Union[lhs._value_type(), rhs._value_type()], + ], + ) @dataclasses.dataclass( @@ -688,49 +557,18 @@ class ToObjectOperation(CachedVarOperation, ObjectVar): ) -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class ObjectHasOwnProperty(CachedVarOperation, BooleanVar): - """Operation to check if an object has a property.""" +@var_operation +def object_has_own_property_operation(object: ObjectVar, key: Var): + """Check if an object has a key. - _object: ObjectVar = dataclasses.field( - default_factory=lambda: LiteralObjectVar.create({}) + Args: + object: The object to check. + key: The key to check. + + Returns: + The result of the check. + """ + return var_operation_return( + js_expression=f"{object}.hasOwnProperty({key})", + var_type=bool, ) - _key: Var | Any = dataclasses.field(default_factory=lambda: LiteralVar.create(None)) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the operation. - - Returns: - The name of the operation. - """ - return f"{str(self._object)}.hasOwnProperty({str(self._key)})" - - @classmethod - def create( - cls, - object: ObjectVar, - key: Var | Any, - _var_data: VarData | None = None, - ) -> ObjectHasOwnProperty: - """Create the object has own property operation. - - Args: - object: The object to check. - key: The key to check. - _var_data: Additional hooks and imports associated with the operation. - - Returns: - The object has own property operation. - """ - return cls( - _var_name="", - _var_type=bool, - _var_data=ImmutableVarData.merge(_var_data), - _object=object, - _key=key if isinstance(key, Var) else LiteralVar.create(key), - ) diff --git a/reflex/ivars/sequence.py b/reflex/ivars/sequence.py index 47f5c6f01..25586e4df 100644 --- a/reflex/ivars/sequence.py +++ b/reflex/ivars/sequence.py @@ -34,16 +34,18 @@ from reflex.vars import ( from .base import ( CachedVarOperation, + CustomVarOperationReturn, ImmutableVar, LiteralVar, cached_property_no_lock, figure_out_type, unionize, + var_operation, + var_operation_return, ) from .number import ( BooleanVar, LiteralNumberVar, - NotEqualOperation, NumberVar, ) @@ -98,15 +100,7 @@ class StringVar(ImmutableVar[str]): """ return (self.split() * other).join() - @overload - def __getitem__(self, i: slice) -> ArrayJoinOperation: ... - - @overload - def __getitem__(self, i: int | NumberVar) -> StringItemOperation: ... - - def __getitem__( - self, i: slice | int | NumberVar - ) -> ArrayJoinOperation | StringItemOperation: + def __getitem__(self, i: slice | int | NumberVar) -> StringVar: """Get a slice of the string. Args: @@ -117,7 +111,7 @@ class StringVar(ImmutableVar[str]): """ if isinstance(i, slice): return self.split()[i].join() - return StringItemOperation.create(self, i) + return string_item_operation(self, i) def length(self) -> NumberVar: """Get the length of the string. @@ -133,7 +127,7 @@ class StringVar(ImmutableVar[str]): Returns: The string lower operation. """ - return StringLowerOperation.create(self) + return string_lower_operation(self) def upper(self) -> StringVar: """Convert the string to uppercase. @@ -141,7 +135,7 @@ class StringVar(ImmutableVar[str]): Returns: The string upper operation. """ - return StringUpperOperation.create(self) + return string_upper_operation(self) def strip(self) -> StringVar: """Strip the string. @@ -149,17 +143,17 @@ class StringVar(ImmutableVar[str]): Returns: The string strip operation. """ - return StringStripOperation.create(self) + return string_strip_operation(self) - def bool(self) -> NotEqualOperation: + def bool(self): """Boolean conversion. Returns: The boolean value of the string. """ - return NotEqualOperation.create(self.length(), 0) + return self.length() != 0 - def reversed(self) -> ArrayJoinOperation: + def reversed(self) -> StringVar: """Reverse the string. Returns: @@ -167,7 +161,7 @@ class StringVar(ImmutableVar[str]): """ return self.split().reverse().join() - def contains(self, other: StringVar | str) -> StringContainsOperation: + def contains(self, other: StringVar | str) -> BooleanVar: """Check if the string contains another string. Args: @@ -176,9 +170,9 @@ class StringVar(ImmutableVar[str]): Returns: The string contains operation. """ - return StringContainsOperation.create(self, other) + return string_contains_operation(self, other) - def split(self, separator: StringVar | str = "") -> StringSplitOperation: + def split(self, separator: StringVar | str = "") -> ArrayVar[List[str]]: """Split the string. Args: @@ -187,9 +181,9 @@ class StringVar(ImmutableVar[str]): Returns: The string split operation. """ - return StringSplitOperation.create(self, separator) + return string_split_operation(self, separator) - def startswith(self, prefix: StringVar | str) -> StringStartsWithOperation: + def startswith(self, prefix: StringVar | str) -> BooleanVar: """Check if the string starts with a prefix. Args: @@ -198,308 +192,106 @@ class StringVar(ImmutableVar[str]): Returns: The string starts with operation. """ - return StringStartsWithOperation.create(self, prefix) + return string_starts_with_operation(self, prefix) -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class StringToStringOperation(CachedVarOperation, StringVar): - """Base class for immutable string vars that are the result of a string to string operation.""" +@var_operation +def string_lower_operation(string: StringVar): + """Convert a string to lowercase. - _value: StringVar = dataclasses.field( - default_factory=lambda: LiteralStringVar.create("") + Args: + string: The string to convert. + + Returns: + The lowercase string. + """ + return var_operation_return(js_expression=f"{string}.toLowerCase()", var_type=str) + + +@var_operation +def string_upper_operation(string: StringVar): + """Convert a string to uppercase. + + Args: + string: The string to convert. + + Returns: + The uppercase string. + """ + return var_operation_return(js_expression=f"{string}.toUpperCase()", var_type=str) + + +@var_operation +def string_strip_operation(string: StringVar): + """Strip a string. + + Args: + string: The string to strip. + + Returns: + The stripped string. + """ + return var_operation_return(js_expression=f"{string}.trim()", var_type=str) + + +@var_operation +def string_contains_operation(haystack: StringVar, needle: StringVar | str): + """Check if a string contains another string. + + Args: + haystack: The haystack. + needle: The needle. + + Returns: + The string contains operation. + """ + return var_operation_return( + js_expression=f"{haystack}.includes({needle})", var_type=bool ) - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - Raises: - NotImplementedError: Must be implemented by subclasses. - """ - raise NotImplementedError( - "StringToStringOperation must implement _cached_var_name" - ) +@var_operation +def string_starts_with_operation(full_string: StringVar, prefix: StringVar | str): + """Check if a string starts with a prefix. - @classmethod - def create( - cls, - value: StringVar, - _var_data: VarData | None = None, - ) -> StringVar: - """Create a var from a string value. + Args: + full_string: The full string. + prefix: The prefix. - Args: - value: The value to create the var from. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - return cls( - _var_name="", - _var_type=str, - _var_data=ImmutableVarData.merge(_var_data), - _value=value, - ) - - -class StringLowerOperation(StringToStringOperation): - """Base class for immutable string vars that are the result of a string lower operation.""" - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return f"{str(self._value)}.toLowerCase()" - - -class StringUpperOperation(StringToStringOperation): - """Base class for immutable string vars that are the result of a string upper operation.""" - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return f"{str(self._value)}.toUpperCase()" - - -class StringStripOperation(StringToStringOperation): - """Base class for immutable string vars that are the result of a string strip operation.""" - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return f"{str(self._value)}.trim()" - - -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class StringContainsOperation(CachedVarOperation, BooleanVar): - """Base class for immutable boolean vars that are the result of a string contains operation.""" - - _haystack: StringVar = dataclasses.field( - default_factory=lambda: LiteralStringVar.create("") - ) - _needle: StringVar = dataclasses.field( - default_factory=lambda: LiteralStringVar.create("") + Returns: + Whether the string starts with the prefix. + """ + return var_operation_return( + js_expression=f"{full_string}.startsWith({prefix})", var_type=bool ) - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - Returns: - The name of the var. - """ - return f"{str(self._haystack)}.includes({str(self._needle)})" +@var_operation +def string_item_operation(string: StringVar, index: NumberVar | int): + """Get an item from a string. - @classmethod - def create( - cls, - haystack: StringVar | str, - needle: StringVar | str, - _var_data: VarData | None = None, - ) -> StringContainsOperation: - """Create a var from a string value. + Args: + string: The string. + index: The index of the item. - Args: - haystack: The haystack. - needle: The needle. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - return cls( - _var_name="", - _var_type=bool, - _var_data=ImmutableVarData.merge(_var_data), - _haystack=( - haystack - if isinstance(haystack, Var) - else LiteralStringVar.create(haystack) - ), - _needle=( - needle if isinstance(needle, Var) else LiteralStringVar.create(needle) - ), - ) + Returns: + The item from the string. + """ + return var_operation_return(js_expression=f"{string}.at({index})", var_type=str) -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class StringStartsWithOperation(CachedVarOperation, BooleanVar): - """Base class for immutable boolean vars that are the result of a string starts with operation.""" +@var_operation +def array_join_operation(array: ArrayVar, sep: StringVar | str = ""): + """Join the elements of an array. - _full_string: StringVar = dataclasses.field( - default_factory=lambda: LiteralStringVar.create("") - ) - _prefix: StringVar = dataclasses.field( - default_factory=lambda: LiteralStringVar.create("") - ) + Args: + array: The array. + sep: The separator. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return f"{str(self._full_string)}.startsWith({str(self._prefix)})" - - @classmethod - def create( - cls, - full_string: StringVar | str, - prefix: StringVar | str, - _var_data: VarData | None = None, - ) -> StringStartsWithOperation: - """Create a var from a string value. - - Args: - full_string: The full string. - prefix: The prefix. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - return cls( - _var_name="", - _var_type=bool, - _var_data=ImmutableVarData.merge(_var_data), - _full_string=( - full_string - if isinstance(full_string, Var) - else LiteralStringVar.create(full_string) - ), - _prefix=( - prefix if isinstance(prefix, Var) else LiteralStringVar.create(prefix) - ), - ) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class StringItemOperation(CachedVarOperation, StringVar): - """Base class for immutable string vars that are the result of a string item operation.""" - - _string: StringVar = dataclasses.field( - default_factory=lambda: LiteralStringVar.create("") - ) - _index: NumberVar = dataclasses.field( - default_factory=lambda: LiteralNumberVar.create(0) - ) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return f"{str(self._string)}.at({str(self._index)})" - - @classmethod - def create( - cls, - string: StringVar | str, - index: NumberVar | int, - _var_data: VarData | None = None, - ) -> StringItemOperation: - """Create a var from a string value. - - Args: - string: The string. - index: The index. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - return cls( - _var_name="", - _var_type=str, - _var_data=ImmutableVarData.merge(_var_data), - _string=( - string if isinstance(string, Var) else LiteralStringVar.create(string) - ), - _index=( - index if isinstance(index, Var) else LiteralNumberVar.create(index) - ), - ) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class ArrayJoinOperation(CachedVarOperation, StringVar): - """Base class for immutable string vars that are the result of an array join operation.""" - - _array: ArrayVar = dataclasses.field( - default_factory=lambda: LiteralArrayVar.create([]) - ) - _sep: StringVar = dataclasses.field( - default_factory=lambda: LiteralStringVar.create("") - ) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return f"{str(self._array)}.join({str(self._sep)})" - - @classmethod - def create( - cls, - array: ArrayVar, - sep: StringVar | str = "", - _var_data: VarData | None = None, - ) -> ArrayJoinOperation: - """Create a var from a string value. - - Args: - array: The array. - sep: The separator. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - return cls( - _var_name="", - _var_type=str, - _var_data=ImmutableVarData.merge(_var_data), - _array=array, - _sep=sep if isinstance(sep, Var) else LiteralStringVar.create(sep), - ) + Returns: + The joined elements. + """ + return var_operation_return(js_expression=f"{array}.join({sep})", var_type=str) # Compile regex for finding reflex var tags. @@ -721,7 +513,7 @@ VALUE_TYPE = TypeVar("VALUE_TYPE") class ArrayVar(ImmutableVar[ARRAY_VAR_TYPE]): """Base class for immutable array vars.""" - def join(self, sep: StringVar | str = "") -> ArrayJoinOperation: + def join(self, sep: StringVar | str = "") -> StringVar: """Join the elements of the array. Args: @@ -730,7 +522,7 @@ class ArrayVar(ImmutableVar[ARRAY_VAR_TYPE]): Returns: The joined elements. """ - return ArrayJoinOperation.create(self, sep) + return array_join_operation(self, sep) def reverse(self) -> ArrayVar[ARRAY_VAR_TYPE]: """Reverse the array. @@ -738,9 +530,9 @@ class ArrayVar(ImmutableVar[ARRAY_VAR_TYPE]): Returns: The reversed array. """ - return ArrayReverseOperation.create(self) + return array_reverse_operation(self) - def __add__(self, other: ArrayVar[ARRAY_VAR_TYPE]) -> ArrayConcatOperation: + def __add__(self, other: ArrayVar[ARRAY_VAR_TYPE]) -> ArrayVar[ARRAY_VAR_TYPE]: """Concatenate two arrays. Parameters: @@ -749,7 +541,7 @@ class ArrayVar(ImmutableVar[ARRAY_VAR_TYPE]): Returns: ArrayConcatOperation: The concatenation of the two arrays. """ - return ArrayConcatOperation.create(self, other) + return array_concat_operation(self, other) @overload def __getitem__(self, i: slice) -> ArrayVar[ARRAY_VAR_TYPE]: ... @@ -854,7 +646,7 @@ class ArrayVar(ImmutableVar[ARRAY_VAR_TYPE]): """ if isinstance(i, slice): return ArraySliceOperation.create(self, i) - return ArrayItemOperation.create(self, i).guess_type() + return array_item_operation(self, i) def length(self) -> NumberVar: """Get the length of the array. @@ -862,7 +654,7 @@ class ArrayVar(ImmutableVar[ARRAY_VAR_TYPE]): Returns: The length of the array. """ - return ArrayLengthOperation.create(self) + return array_length_operation(self) @overload @classmethod @@ -902,7 +694,7 @@ class ArrayVar(ImmutableVar[ARRAY_VAR_TYPE]): start = first_endpoint end = second_endpoint - return RangeOperation.create(start, end, step or 1) + return array_range_operation(start, end, step or 1) def contains(self, other: Any) -> BooleanVar: """Check if the array contains an element. @@ -913,7 +705,7 @@ class ArrayVar(ImmutableVar[ARRAY_VAR_TYPE]): Returns: The array contains operation. """ - return ArrayContainsOperation.create(self, other) + return array_contains_operation(self, other) def __mul__(self, other: NumberVar | int) -> ArrayVar[ARRAY_VAR_TYPE]: """Multiply the sequence by a number or integer. @@ -924,7 +716,7 @@ class ArrayVar(ImmutableVar[ARRAY_VAR_TYPE]): Returns: ArrayVar[ARRAY_VAR_TYPE]: The result of multiplying the sequence by the given number or integer. """ - return ArrayRepeatOperation.create(self, other) + return repeat_array_operation(self, other) __rmul__ = __mul__ # type: ignore @@ -1026,102 +818,20 @@ class LiteralArrayVar(CachedVarOperation, LiteralVar, ArrayVar[ARRAY_VAR_TYPE]): ) -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class StringSplitOperation(CachedVarOperation, ArrayVar): - """Base class for immutable array vars that are the result of a string split operation.""" +@var_operation +def string_split_operation(string: StringVar, sep: StringVar | str = ""): + """Split a string. - _string: StringVar = dataclasses.field( - default_factory=lambda: LiteralStringVar.create("") + Args: + string: The string to split. + sep: The separator. + + Returns: + The split string. + """ + return var_operation_return( + js_expression=f"{string}.split({sep})", var_type=List[str] ) - _sep: StringVar = dataclasses.field( - default_factory=lambda: LiteralStringVar.create("") - ) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return f"{str(self._string)}.split({str(self._sep)})" - - @classmethod - def create( - cls, - string: StringVar | str, - sep: StringVar | str, - _var_data: VarData | None = None, - ) -> StringSplitOperation: - """Create a var from a string value. - - Args: - string: The string. - sep: The separator. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - return cls( - _var_name="", - _var_type=List[str], - _var_data=ImmutableVarData.merge(_var_data), - _string=( - string if isinstance(string, Var) else LiteralStringVar.create(string) - ), - _sep=(sep if isinstance(sep, Var) else LiteralStringVar.create(sep)), - ) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class ArrayToArrayOperation(CachedVarOperation, ArrayVar): - """Base class for immutable array vars that are the result of an array to array operation.""" - - _value: ArrayVar = dataclasses.field( - default_factory=lambda: LiteralArrayVar.create([]) - ) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Raises: - NotImplementedError: Must be implemented by subclasses. - """ - raise NotImplementedError( - "ArrayToArrayOperation must implement _cached_var_name" - ) - - @classmethod - def create( - cls, - value: ArrayVar, - _var_data: VarData | None = None, - ) -> ArrayToArrayOperation: - """Create a var from a string value. - - Args: - value: The value to create the var from. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - return cls( - _var_name="", - _var_type=value._var_type, - _var_data=ImmutableVarData.merge(_var_data), - _value=value, - ) @dataclasses.dataclass( @@ -1135,7 +845,9 @@ class ArraySliceOperation(CachedVarOperation, ArrayVar): _array: ArrayVar = dataclasses.field( default_factory=lambda: LiteralArrayVar.create([]) ) - _slice: slice = dataclasses.field(default_factory=lambda: slice(None, None, None)) + _start: NumberVar | int = dataclasses.field(default_factory=lambda: 0) + _stop: NumberVar | int = dataclasses.field(default_factory=lambda: 0) + _step: NumberVar | int = dataclasses.field(default_factory=lambda: 1) @cached_property_no_lock def _cached_var_name(self) -> str: @@ -1147,7 +859,7 @@ class ArraySliceOperation(CachedVarOperation, ArrayVar): Raises: ValueError: If the slice step is zero. """ - start, end, step = self._slice.start, self._slice.stop, self._slice.step + start, end, step = self._start, self._stop, self._step normalized_start = ( LiteralVar.create(start) @@ -1165,16 +877,7 @@ class ArraySliceOperation(CachedVarOperation, ArrayVar): if step < 0: actual_start = end + 1 if end is not None else 0 actual_end = start + 1 if start is not None else self._array.length() - return str( - ArraySliceOperation.create( - ArrayReverseOperation.create( - ArraySliceOperation.create( - self._array, slice(actual_start, actual_end) - ) - ), - slice(None, None, -step), - ) - ) + return str(self._array[actual_start:actual_end].reverse()[::-step]) if step == 0: raise ValueError("slice step cannot be zero") return f"{str(self._array)}.slice({str(normalized_start)}, {str(normalized_end)}).filter((_, i) => i % {str(step)} === 0)" @@ -1206,80 +909,44 @@ class ArraySliceOperation(CachedVarOperation, ArrayVar): _var_type=array._var_type, _var_data=ImmutableVarData.merge(_var_data), _array=array, - _slice=slice, + _start=slice.start, + _stop=slice.stop, + _step=slice.step, ) -class ArrayReverseOperation(ArrayToArrayOperation): - """Base class for immutable string vars that are the result of a string reverse operation.""" +@var_operation +def array_reverse_operation( + array: ArrayVar[ARRAY_VAR_TYPE], +) -> CustomVarOperationReturn[ARRAY_VAR_TYPE]: + """Reverse an array. - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. + Args: + array: The array to reverse. - Returns: - The name of the var. - """ - return f"{str(self._value)}.slice().reverse()" - - -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class ArrayToNumberOperation(CachedVarOperation, NumberVar): - """Base class for immutable number vars that are the result of an array to number operation.""" - - _array: ArrayVar = dataclasses.field( - default_factory=lambda: LiteralArrayVar.create([]), + Returns: + The reversed array. + """ + return var_operation_return( + js_expression=f"{array}.slice().reverse()", + var_type=array._var_type, ) - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - Raises: - NotImplementedError: Must be implemented by subclasses. - """ - raise NotImplementedError( - "StringToNumberOperation must implement _cached_var_name" - ) +@var_operation +def array_length_operation(array: ArrayVar): + """Get the length of an array. - @classmethod - def create( - cls, - array: ArrayVar, - _var_data: VarData | None = None, - ) -> ArrayToNumberOperation: - """Create a var from a string value. + Args: + array: The array. - Args: - array: The array. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - return cls( - _var_name="", - _var_type=int, - _var_data=ImmutableVarData.merge(_var_data), - _array=array, - ) - - -class ArrayLengthOperation(ArrayToNumberOperation): - """Base class for immutable number vars that are the result of an array length operation.""" - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return f"{str(self._array)}.length" + Returns: + The length of the array. + """ + return var_operation_return( + js_expression=f"{array}.length", + var_type=int, + ) def is_tuple_type(t: GenericType) -> bool: @@ -1296,166 +963,65 @@ def is_tuple_type(t: GenericType) -> bool: return get_origin(t) is tuple -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class ArrayItemOperation(CachedVarOperation, ImmutableVar): - """Base class for immutable array vars that are the result of an array item operation.""" +@var_operation +def array_item_operation(array: ArrayVar, index: NumberVar | int): + """Get an item from an array. - _array: ArrayVar = dataclasses.field( - default_factory=lambda: LiteralArrayVar.create([]) - ) - _index: NumberVar = dataclasses.field( - default_factory=lambda: LiteralNumberVar.create(0) + Args: + array: The array. + index: The index of the item. + + 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) + + return var_operation_return( + js_expression=f"{str(array)}.at({str(index)})", + var_type=element_type, ) - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - Returns: - The name of the var. - """ - return f"{str(self._array)}.at({str(self._index)})" +@var_operation +def array_range_operation( + start: NumberVar | int, stop: NumberVar | int, step: NumberVar | int +): + """Create a range of numbers. - @classmethod - def create( - cls, - array: ArrayVar, - index: NumberVar | int, - _var_type: GenericType | None = None, - _var_data: VarData | None = None, - ) -> ArrayItemOperation: - """Create a var from a string value. + Args: + start: The start of the range. + stop: The end of the range. + step: The step of the range. - Args: - array: The array. - index: The index. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - args = typing.get_args(array._var_type) - if args and isinstance(index, int) and is_tuple_type(array._var_type): - element_type = args[index % len(args)] - else: - element_type = unionize(*args) - - return cls( - _var_name="", - _var_type=element_type if _var_type is None else _var_type, - _var_data=ImmutableVarData.merge(_var_data), - _array=array, - _index=index if isinstance(index, Var) else LiteralNumberVar.create(index), - ) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class RangeOperation(CachedVarOperation, ArrayVar): - """Base class for immutable array vars that are the result of a range operation.""" - - _start: NumberVar = dataclasses.field( - default_factory=lambda: LiteralNumberVar.create(0) - ) - _stop: NumberVar = dataclasses.field( - default_factory=lambda: LiteralNumberVar.create(0) - ) - _step: NumberVar = dataclasses.field( - default_factory=lambda: LiteralNumberVar.create(1) + Returns: + The range of numbers. + """ + return var_operation_return( + js_expression=f"Array.from({{ length: ({str(stop)} - {str(start)}) / {str(step)} }}, (_, i) => {str(start)} + i * {str(step)})", + var_type=List[int], ) - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - Returns: - The name of the var. - """ - start, end, step = self._start, self._stop, self._step - return f"Array.from({{ length: ({str(end)} - {str(start)}) / {str(step)} }}, (_, i) => {str(start)} + i * {str(step)})" +@var_operation +def array_contains_operation(haystack: ArrayVar, needle: Any | Var): + """Check if an array contains an element. - @classmethod - def create( - cls, - start: NumberVar | int, - stop: NumberVar | int, - step: NumberVar | int, - _var_data: VarData | None = None, - ) -> RangeOperation: - """Create a var from a string value. + Args: + haystack: The array to check. + needle: The element to check for. - Args: - start: The start of the range. - stop: The end of the range. - step: The step of the range. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - return cls( - _var_name="", - _var_type=List[int], - _var_data=ImmutableVarData.merge(_var_data), - _start=start if isinstance(start, Var) else LiteralNumberVar.create(start), - _stop=stop if isinstance(stop, Var) else LiteralNumberVar.create(stop), - _step=step if isinstance(step, Var) else LiteralNumberVar.create(step), - ) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class ArrayContainsOperation(CachedVarOperation, BooleanVar): - """Base class for immutable boolean vars that are the result of an array contains operation.""" - - _haystack: ArrayVar = dataclasses.field( - default_factory=lambda: LiteralArrayVar.create([]) + Returns: + The array contains operation. + """ + return var_operation_return( + js_expression=f"{haystack}.includes({needle})", + var_type=bool, ) - _needle: Var = dataclasses.field(default_factory=lambda: LiteralVar.create(None)) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return f"{str(self._haystack)}.includes({str(self._needle)})" - - @classmethod - def create( - cls, - haystack: ArrayVar, - needle: Any | Var, - _var_data: VarData | None = None, - ) -> ArrayContainsOperation: - """Create a var from a string value. - - Args: - haystack: The array. - needle: The element to check for. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - return cls( - _var_name="", - _var_type=bool, - _var_data=ImmutableVarData.merge(_var_data), - _haystack=haystack, - _needle=needle if isinstance(needle, Var) else LiteralVar.create(needle), - ) @dataclasses.dataclass( @@ -1547,102 +1113,39 @@ class ToArrayOperation(CachedVarOperation, ArrayVar): ) -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class ArrayRepeatOperation(CachedVarOperation, ArrayVar): - """Base class for immutable array vars that are the result of an array repeat operation.""" +@var_operation +def repeat_array_operation( + array: ArrayVar[ARRAY_VAR_TYPE], count: NumberVar | int +) -> CustomVarOperationReturn[ARRAY_VAR_TYPE]: + """Repeat an array a number of times. - _array: ArrayVar = dataclasses.field( - default_factory=lambda: LiteralArrayVar.create([]) - ) - _count: NumberVar = dataclasses.field( - default_factory=lambda: LiteralNumberVar.create(0) + Args: + array: The array to repeat. + count: The number of times to repeat the array. + + Returns: + The repeated array. + """ + return var_operation_return( + js_expression=f"Array.from({{ length: {count} }}).flatMap(() => {array})", + var_type=array._var_type, ) - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - Returns: - The name of the var. - """ - return f"Array.from({{ length: {str(self._count)} }}).flatMap(() => {str(self._array)})" +@var_operation +def array_concat_operation( + lhs: ArrayVar[ARRAY_VAR_TYPE], rhs: ArrayVar[ARRAY_VAR_TYPE] +) -> CustomVarOperationReturn[ARRAY_VAR_TYPE]: + """Concatenate two arrays. - @classmethod - def create( - cls, - array: ArrayVar, - count: NumberVar | int, - _var_data: VarData | None = None, - ) -> ArrayRepeatOperation: - """Create a var from a string value. + Args: + lhs: The left-hand side array. + rhs: The right-hand side array. - Args: - array: The array. - count: The number of times to repeat the array. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - return cls( - _var_name="", - _var_type=array._var_type, - _var_data=ImmutableVarData.merge(_var_data), - _array=array, - _count=count if isinstance(count, Var) else LiteralNumberVar.create(count), - ) - - -@dataclasses.dataclass( - eq=False, - frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, -) -class ArrayConcatOperation(CachedVarOperation, ArrayVar): - """Base class for immutable array vars that are the result of an array concat operation.""" - - _lhs: ArrayVar = dataclasses.field( - default_factory=lambda: LiteralArrayVar.create([]) + Returns: + The concatenated array. + """ + return var_operation_return( + js_expression=f"[...{lhs}, ...{rhs}]", + var_type=Union[lhs._var_type, rhs._var_type], ) - _rhs: ArrayVar = dataclasses.field( - default_factory=lambda: LiteralArrayVar.create([]) - ) - - @cached_property_no_lock - def _cached_var_name(self) -> str: - """The name of the var. - - Returns: - The name of the var. - """ - return f"[...{str(self._lhs)}, ...{str(self._rhs)}]" - - @classmethod - def create( - cls, - lhs: ArrayVar, - rhs: ArrayVar, - _var_data: VarData | None = None, - ) -> ArrayConcatOperation: - """Create a var from a string value. - - Args: - lhs: The left-hand side array. - rhs: The right-hand side array. - _var_data: Additional hooks and imports associated with the Var. - - Returns: - The var. - """ - # TODO: Figure out how to merge the types of a and b - return cls( - _var_name="", - _var_type=Union[lhs._var_type, rhs._var_type], - _var_data=ImmutableVarData.merge(_var_data), - _lhs=lhs, - _rhs=rhs, - ) diff --git a/tests/components/core/test_colors.py b/tests/components/core/test_colors.py index d5d5dc995..ace7adad6 100644 --- a/tests/components/core/test_colors.py +++ b/tests/components/core/test_colors.py @@ -67,11 +67,11 @@ def test_color(color, expected): [ ( rx.cond(True, rx.color("mint"), rx.color("tomato", 5)), - '(Boolean(true) ? "var(--mint-7)" : "var(--tomato-5)")', + '(true ? "var(--mint-7)" : "var(--tomato-5)")', ), ( rx.cond(True, rx.color(ColorState.color), rx.color(ColorState.color, 5)), # type: ignore - f'(Boolean(true) ? ("var(--"+{str(color_state_name)}.color+"-7)") : ("var(--"+{str(color_state_name)}.color+"-5)"))', + f'(true ? ("var(--"+{str(color_state_name)}.color+"-7)") : ("var(--"+{str(color_state_name)}.color+"-5)"))', ), ( rx.match( diff --git a/tests/components/core/test_cond.py b/tests/components/core/test_cond.py index e3fc40ae3..53f8e0640 100644 --- a/tests/components/core/test_cond.py +++ b/tests/components/core/test_cond.py @@ -23,7 +23,7 @@ def cond_state(request): def test_f_string_cond_interpolation(): # make sure backticks inside interpolation don't get escaped var = LiteralVar.create(f"x {cond(True, 'a', 'b')}") - assert str(var) == '("x "+(Boolean(true) ? "a" : "b"))' + assert str(var) == '("x "+(true ? "a" : "b"))' @pytest.mark.parametrize( @@ -97,7 +97,7 @@ def test_prop_cond(c1: Any, c2: Any): c1 = json.dumps(c1) if not isinstance(c2, Var): c2 = json.dumps(c2) - assert str(prop_cond) == f"(Boolean(true) ? {c1} : {c2})" + assert str(prop_cond) == f"(true ? {c1} : {c2})" def test_cond_no_mix(): @@ -141,8 +141,7 @@ def test_cond_computed_var(): state_name = format_state_name(CondStateComputed.get_full_name()) assert ( - str(comp) - == f"(Boolean(true) ? {state_name}.computed_int : {state_name}.computed_str)" + str(comp) == f"(true ? {state_name}.computed_int : {state_name}.computed_str)" ) assert comp._var_type == Union[int, str] diff --git a/tests/test_var.py b/tests/test_var.py index cca6cbec4..ba9cf24c8 100644 --- a/tests/test_var.py +++ b/tests/test_var.py @@ -12,6 +12,7 @@ from reflex.ivars.base import ( ImmutableVar, LiteralVar, var_operation, + var_operation_return, ) from reflex.ivars.function import ArgsFunctionOperation, FunctionStringVar from reflex.ivars.number import ( @@ -925,9 +926,9 @@ def test_function_var(): def test_var_operation(): - @var_operation(output=NumberVar) - def add(a: Union[NumberVar, int], b: Union[NumberVar, int]) -> str: - return f"({a} + {b})" + @var_operation + def add(a: Union[NumberVar, int], b: Union[NumberVar, int]): + return var_operation_return(js_expression=f"({a} + {b})", var_type=int) assert str(add(1, 2)) == "(1 + 2)" assert str(add(a=4, b=-9)) == "(4 + -9)" @@ -967,14 +968,14 @@ def test_all_number_operations(): assert ( str(even_more_complicated_number) - == "!(Boolean((Math.abs(Math.floor(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))) || (2 && Math.round(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))))))" + == "!(((Math.abs(Math.floor(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))) || (2 && Math.round(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2)))) !== 0))" ) assert str(LiteralNumberVar.create(5) > False) == "(5 > 0)" - assert str(LiteralBooleanVar.create(False) < 5) == "((false ? 1 : 0) < 5)" + assert str(LiteralBooleanVar.create(False) < 5) == "(Number(false) < 5)" assert ( str(LiteralBooleanVar.create(False) < LiteralBooleanVar.create(True)) - == "((false ? 1 : 0) < (true ? 1 : 0))" + == "(Number(false) < Number(true))" ) From 15a9f0a104fc30ab569b703925aef5739450ad63 Mon Sep 17 00:00:00 2001 From: Nikhil Rao Date: Tue, 3 Sep 2024 13:42:35 -0700 Subject: [PATCH 03/22] Disk state manager don't use byref (#3874) --- reflex/state.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index c0435d665..534672bd9 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -2457,6 +2457,20 @@ def _default_token_expiration() -> int: return get_config().redis_token_expiration +def _serialize_type(type_: Any) -> str: + """Serialize a type. + + Args: + type_: The type to serialize. + + Returns: + The serialized type. + """ + if not inspect.isclass(type_): + return f"{type_}" + return f"{type_.__module__}.{type_.__qualname__}" + + def state_to_schema( state: BaseState, ) -> List[ @@ -2480,7 +2494,7 @@ def state_to_schema( ( field_name, model_field.name, - model_field.type_, + _serialize_type(model_field.type_), ( model_field.required if isinstance(model_field.required, bool) @@ -2643,7 +2657,7 @@ class StateManagerDisk(StateManager): self.states[substate_token] = substate - state_dilled = dill.dumps((state_to_schema(substate), substate), byref=True) + state_dilled = dill.dumps((state_to_schema(substate), substate)) if not self.states_directory.exists(): self.states_directory.mkdir(parents=True, exist_ok=True) self.token_path(substate_token).write_bytes(state_dilled) From 59047303c9ea7d392f748152371c997dd066ccf1 Mon Sep 17 00:00:00 2001 From: Samarth Bhadane Date: Tue, 3 Sep 2024 18:34:03 -0700 Subject: [PATCH 04/22] /health endpoint for K8 Liveness and Readiness probes (#3855) * Added API Endpoint * Added API Endpoint * Added Unit Tests * Added Unit Tests * main * Apply suggestions from Code Review * Fix Ruff Formatting * Update Socket Events * Async Functions --- reflex/app.py | 37 +++++++++++- reflex/constants/event.py | 1 + reflex/model.py | 22 +++++++ reflex/utils/prerequisites.py | 25 ++++++++ tests/test_health_endpoint.py | 106 ++++++++++++++++++++++++++++++++++ 5 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 tests/test_health_endpoint.py diff --git a/reflex/app.py b/reflex/app.py index 5be0ef040..a3094885d 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -33,7 +33,7 @@ from typing import ( from fastapi import FastAPI, HTTPException, Request, UploadFile from fastapi.middleware import cors -from fastapi.responses import StreamingResponse +from fastapi.responses import JSONResponse, StreamingResponse from fastapi.staticfiles import StaticFiles from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn from socketio import ASGIApp, AsyncNamespace, AsyncServer @@ -65,7 +65,7 @@ from reflex.components.core.upload import Upload, get_upload_dir from reflex.components.radix import themes from reflex.config import get_config from reflex.event import Event, EventHandler, EventSpec, window_alert -from reflex.model import Model +from reflex.model import Model, get_db_status from reflex.page import ( DECORATED_PAGES, ) @@ -377,6 +377,7 @@ class App(MiddlewareMixin, LifespanMixin, Base): """Add default api endpoints (ping).""" # To test the server. self.api.get(str(constants.Endpoint.PING))(ping) + self.api.get(str(constants.Endpoint.HEALTH))(health) def _add_optional_endpoints(self): """Add optional api endpoints (_upload).""" @@ -1319,6 +1320,38 @@ async def ping() -> str: return "pong" +async def health() -> JSONResponse: + """Health check endpoint to assess the status of the database and Redis services. + + Returns: + JSONResponse: A JSON object with the health status: + - "status" (bool): Overall health, True if all checks pass. + - "db" (bool or str): Database status - True, False, or "NA". + - "redis" (bool or str): Redis status - True, False, or "NA". + """ + health_status = {"status": True} + status_code = 200 + + db_status, redis_status = await asyncio.gather( + get_db_status(), prerequisites.get_redis_status() + ) + + health_status["db"] = db_status + + if redis_status is None: + health_status["redis"] = False + else: + health_status["redis"] = redis_status + + if not health_status["db"] or ( + not health_status["redis"] and redis_status is not None + ): + health_status["status"] = False + status_code = 503 + + return JSONResponse(content=health_status, status_code=status_code) + + def upload(app: App): """Upload a file. diff --git a/reflex/constants/event.py b/reflex/constants/event.py index 351a1ac52..d454e6ea8 100644 --- a/reflex/constants/event.py +++ b/reflex/constants/event.py @@ -11,6 +11,7 @@ class Endpoint(Enum): EVENT = "_event" UPLOAD = "_upload" AUTH_CODESPACE = "auth-codespace" + HEALTH = "_health" def __str__(self) -> str: """Get the string representation of the endpoint. diff --git a/reflex/model.py b/reflex/model.py index 71e26f76a..fefb1f9e9 100644 --- a/reflex/model.py +++ b/reflex/model.py @@ -15,6 +15,7 @@ import alembic.runtime.environment import alembic.script import alembic.util import sqlalchemy +import sqlalchemy.exc import sqlalchemy.orm from reflex import constants @@ -51,6 +52,27 @@ def get_engine(url: str | None = None) -> sqlalchemy.engine.Engine: return sqlmodel.create_engine(url, echo=echo_db_query, connect_args=connect_args) +async def get_db_status() -> bool: + """Checks the status of the database connection. + + Attempts to connect to the database and execute a simple query to verify connectivity. + + Returns: + bool: The status of the database connection: + - True: The database is accessible. + - False: The database is not accessible. + """ + status = True + try: + engine = get_engine() + with engine.connect() as connection: + connection.execute(sqlalchemy.text("SELECT 1")) + except sqlalchemy.exc.OperationalError: + status = False + + return status + + SQLModelOrSqlAlchemy = Union[ Type[sqlmodel.SQLModel], Type[sqlalchemy.orm.DeclarativeBase] ] diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 697d51cf2..902c5111c 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -28,6 +28,7 @@ import typer from alembic.util.exc import CommandError from packaging import version from redis import Redis as RedisSync +from redis import exceptions from redis.asyncio import Redis from reflex import constants, model @@ -344,6 +345,30 @@ def parse_redis_url() -> str | dict | None: return dict(host=redis_url, port=int(redis_port), db=0) +async def get_redis_status() -> bool | None: + """Checks the status of the Redis connection. + + Attempts to connect to Redis and send a ping command to verify connectivity. + + Returns: + bool or None: The status of the Redis connection: + - True: Redis is accessible and responding. + - False: Redis is not accessible due to a connection error. + - None: Redis not used i.e redis_url is not set in rxconfig. + """ + try: + status = True + redis_client = get_redis_sync() + if redis_client is not None: + redis_client.ping() + else: + status = None + except exceptions.RedisError: + status = False + + return status + + def validate_app_name(app_name: str | None = None) -> str: """Validate the app name. diff --git a/tests/test_health_endpoint.py b/tests/test_health_endpoint.py new file mode 100644 index 000000000..fe350266f --- /dev/null +++ b/tests/test_health_endpoint.py @@ -0,0 +1,106 @@ +import json +from unittest.mock import MagicMock, Mock + +import pytest +import sqlalchemy +from redis.exceptions import RedisError + +from reflex.app import health +from reflex.model import get_db_status +from reflex.utils.prerequisites import get_redis_status + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "mock_redis_client, expected_status", + [ + # Case 1: Redis client is available and responds to ping + (Mock(ping=lambda: None), True), + # Case 2: Redis client raises RedisError + (Mock(ping=lambda: (_ for _ in ()).throw(RedisError)), False), + # Case 3: Redis client is not used + (None, None), + ], +) +async def test_get_redis_status(mock_redis_client, expected_status, mocker): + # Mock the `get_redis_sync` function to return the mock Redis client + mock_get_redis_sync = mocker.patch( + "reflex.utils.prerequisites.get_redis_sync", return_value=mock_redis_client + ) + + # Call the function + status = await get_redis_status() + + # Verify the result + assert status == expected_status + mock_get_redis_sync.assert_called_once() + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "mock_engine, execute_side_effect, expected_status", + [ + # Case 1: Database is accessible + (MagicMock(), None, True), + # Case 2: Database connection error (OperationalError) + ( + MagicMock(), + sqlalchemy.exc.OperationalError("error", "error", "error"), + False, + ), + ], +) +async def test_get_db_status(mock_engine, execute_side_effect, expected_status, mocker): + # Mock get_engine to return the mock_engine + mock_get_engine = mocker.patch("reflex.model.get_engine", return_value=mock_engine) + + # Mock the connection and its execute method + if mock_engine: + mock_connection = mock_engine.connect.return_value.__enter__.return_value + if execute_side_effect: + # Simulate execute method raising an exception + mock_connection.execute.side_effect = execute_side_effect + else: + # Simulate successful execute call + mock_connection.execute.return_value = None + + # Call the function + status = await get_db_status() + + # Verify the result + assert status == expected_status + mock_get_engine.assert_called_once() + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "db_status, redis_status, expected_status, expected_code", + [ + # Case 1: Both services are connected + (True, True, {"status": True, "db": True, "redis": True}, 200), + # Case 2: Database not connected, Redis connected + (False, True, {"status": False, "db": False, "redis": True}, 503), + # Case 3: Database connected, Redis not connected + (True, False, {"status": False, "db": True, "redis": False}, 503), + # Case 4: Both services not connected + (False, False, {"status": False, "db": False, "redis": False}, 503), + # Case 5: Database Connected, Redis not used + (True, None, {"status": True, "db": True, "redis": False}, 200), + ], +) +async def test_health(db_status, redis_status, expected_status, expected_code, mocker): + # Mock get_db_status and get_redis_status + mocker.patch("reflex.app.get_db_status", return_value=db_status) + mocker.patch( + "reflex.utils.prerequisites.get_redis_status", return_value=redis_status + ) + + # Call the async health function + response = await health() + + print(json.loads(response.body)) + print(expected_status) + + # Verify the response content and status code + assert response.status_code == expected_code + assert json.loads(response.body) == expected_status From d0b9b955b8157794d80a13578a554a52a0aa39d6 Mon Sep 17 00:00:00 2001 From: Samarth Bhadane Date: Wed, 4 Sep 2024 16:11:33 -0700 Subject: [PATCH 05/22] Update find_replace (#3886) --- reflex/utils/path_ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/utils/path_ops.py b/reflex/utils/path_ops.py index d38239a83..21065db99 100644 --- a/reflex/utils/path_ops.py +++ b/reflex/utils/path_ops.py @@ -209,4 +209,4 @@ def find_replace(directory: str | Path, find: str, replace: str): filepath = Path(root, file) text = filepath.read_text(encoding="utf-8") text = re.sub(find, replace, text) - filepath.write_text(text) + filepath.write_text(text, encoding="utf-8") From dade940632a0c1533611b5c771b830cb01b14659 Mon Sep 17 00:00:00 2001 From: Elijah Ahianyo Date: Thu, 5 Sep 2024 17:21:32 +0000 Subject: [PATCH 06/22] [REF-3592]Promote `rx.progress` from radix themes (#3878) * Promote `rx.progress` from radix themes * fix pyi * add warning when accessing `rx._x.progress` --- reflex/__init__.py | 2 +- reflex/__init__.pyi | 2 +- reflex/components/radix/__init__.pyi | 2 +- .../components/radix/primitives/__init__.pyi | 1 - .../radix/themes/components/__init__.pyi | 1 + reflex/experimental/__init__.py | 29 ++++++++++++++++--- 6 files changed, 29 insertions(+), 8 deletions(-) diff --git a/reflex/__init__.py b/reflex/__init__.py index abfec245c..d69e33d47 100644 --- a/reflex/__init__.py +++ b/reflex/__init__.py @@ -140,6 +140,7 @@ RADIX_THEMES_COMPONENTS_MAPPING: dict = { "components.radix.themes.components.radio_group": ["radio", "radio_group"], "components.radix.themes.components.dropdown_menu": ["menu", "dropdown_menu"], "components.radix.themes.components.separator": ["divider", "separator"], + "components.radix.themes.components.progress": ["progress"], } RADIX_THEMES_LAYOUT_MAPPING: dict = { @@ -205,7 +206,6 @@ RADIX_PRIMITIVES_MAPPING: dict = { "components.radix.primitives.form": [ "form", ], - "components.radix.primitives.progress": ["progress"], } COMPONENTS_CORE_MAPPING: dict = { diff --git a/reflex/__init__.pyi b/reflex/__init__.pyi index c7aefd412..22c7298fe 100644 --- a/reflex/__init__.pyi +++ b/reflex/__init__.pyi @@ -71,7 +71,6 @@ from .components.plotly import plotly as plotly from .components.radix.primitives.accordion import accordion as accordion from .components.radix.primitives.drawer import drawer as drawer from .components.radix.primitives.form import form as form -from .components.radix.primitives.progress import progress as progress from .components.radix.themes.base import theme as theme from .components.radix.themes.base import theme_panel as theme_panel from .components.radix.themes.color_mode import color_mode as color_mode @@ -106,6 +105,7 @@ from .components.radix.themes.components.hover_card import hover_card as hover_c from .components.radix.themes.components.icon_button import icon_button as icon_button from .components.radix.themes.components.inset import inset as inset from .components.radix.themes.components.popover import popover as popover +from .components.radix.themes.components.progress import progress as progress from .components.radix.themes.components.radio_cards import radio_cards as radio_cards from .components.radix.themes.components.radio_group import radio as radio from .components.radix.themes.components.radio_group import radio_group as radio_group diff --git a/reflex/components/radix/__init__.pyi b/reflex/components/radix/__init__.pyi index b61659320..8ba6c242b 100644 --- a/reflex/components/radix/__init__.pyi +++ b/reflex/components/radix/__init__.pyi @@ -8,7 +8,6 @@ from . import themes as themes from .primitives.accordion import accordion as accordion from .primitives.drawer import drawer as drawer from .primitives.form import form as form -from .primitives.progress import progress as progress from .themes.base import theme as theme from .themes.base import theme_panel as theme_panel from .themes.color_mode import color_mode as color_mode @@ -31,6 +30,7 @@ from .themes.components.hover_card import hover_card as hover_card from .themes.components.icon_button import icon_button as icon_button from .themes.components.inset import inset as inset from .themes.components.popover import popover as popover +from .themes.components.progress import progress as progress from .themes.components.radio_cards import radio_cards as radio_cards from .themes.components.radio_group import radio as radio from .themes.components.radio_group import radio_group as radio_group diff --git a/reflex/components/radix/primitives/__init__.pyi b/reflex/components/radix/primitives/__init__.pyi index e7461e9a7..0fec0fc40 100644 --- a/reflex/components/radix/primitives/__init__.pyi +++ b/reflex/components/radix/primitives/__init__.pyi @@ -6,4 +6,3 @@ from .accordion import accordion as accordion from .drawer import drawer as drawer from .form import form as form -from .progress import progress as progress diff --git a/reflex/components/radix/themes/components/__init__.pyi b/reflex/components/radix/themes/components/__init__.pyi index 6f6df88fa..29f15e311 100644 --- a/reflex/components/radix/themes/components/__init__.pyi +++ b/reflex/components/radix/themes/components/__init__.pyi @@ -22,6 +22,7 @@ from .hover_card import hover_card as hover_card from .icon_button import icon_button as icon_button from .inset import inset as inset from .popover import popover as popover +from .progress import progress as progress from .radio_cards import radio_cards as radio_cards from .radio_group import radio as radio from .radio_group import radio_group as radio_group diff --git a/reflex/experimental/__init__.py b/reflex/experimental/__init__.py index f0eca0c84..0c11deb85 100644 --- a/reflex/experimental/__init__.py +++ b/reflex/experimental/__init__.py @@ -32,18 +32,39 @@ class ExperimentalNamespace(SimpleNamespace): Returns: The toast namespace. """ - if "toast" not in _EMITTED_PROMOTION_WARNINGS: - _EMITTED_PROMOTION_WARNINGS.add("toast") - warn(f"`rx._x.toast` was promoted to `rx.toast`.") + self.register_component_warning("toast") return toast + @property + def progress(self): + """Temporary property returning the toast namespace. + + Remove this property when toast is fully promoted. + + Returns: + The toast namespace. + """ + self.register_component_warning("progress") + return progress + + @staticmethod + def register_component_warning(component_name: str): + """Add component to emitted warnings and throw a warning if it + doesn't exist. + + Args: + component_name: name of the component. + """ + if component_name not in _EMITTED_PROMOTION_WARNINGS: + _EMITTED_PROMOTION_WARNINGS.add(component_name) + warn(f"`rx._x.{component_name}` was promoted to `rx.{component_name}`.") + _x = ExperimentalNamespace( asset=asset, client_state=ClientStateVar.create, hooks=hooks, layout=layout, - progress=progress, PropsBase=PropsBase, run_in_thread=run_in_thread, ) From 677ae314fbe316b2a40003d9b686ac139718a400 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 5 Sep 2024 10:21:46 -0700 Subject: [PATCH 07/22] Use correct flexgen backend URL (#3891) --- reflex/constants/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/constants/base.py b/reflex/constants/base.py index c9be9a462..d5407902e 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -105,7 +105,7 @@ class Templates(SimpleNamespace): # The reflex.build backend host REFLEX_BUILD_BACKEND = os.environ.get( - "REFLEX_BUILD_BACKEND", "https://rxh-prod-flexgen.fly.dev" + "REFLEX_BUILD_BACKEND", "https://flexgen-prod-flexgen.fly.dev" ) # The URL to redirect to reflex.build From 2d6e531e49931811c61f731c82922ae3a37c1a7f Mon Sep 17 00:00:00 2001 From: Elijah Ahianyo Date: Thu, 5 Sep 2024 17:22:07 +0000 Subject: [PATCH 08/22] Remove demo template (#3888) --- reflex/.templates/apps/demo/.gitignore | 4 - .../.templates/apps/demo/assets/favicon.ico | Bin 4286 -> 0 bytes reflex/.templates/apps/demo/assets/github.svg | 10 - reflex/.templates/apps/demo/assets/icon.svg | 37 -- reflex/.templates/apps/demo/assets/logo.svg | 68 ---- .../.templates/apps/demo/assets/paneleft.svg | 13 - reflex/.templates/apps/demo/code/__init__.py | 1 - reflex/.templates/apps/demo/code/demo.py | 127 ------ .../apps/demo/code/pages/__init__.py | 7 - .../apps/demo/code/pages/chatapp.py | 31 -- .../apps/demo/code/pages/datatable.py | 360 ------------------ .../.templates/apps/demo/code/pages/forms.py | 257 ------------- .../apps/demo/code/pages/graphing.py | 253 ------------ .../.templates/apps/demo/code/pages/home.py | 56 --- reflex/.templates/apps/demo/code/sidebar.py | 178 --------- reflex/.templates/apps/demo/code/state.py | 22 -- .../apps/demo/code/states/form_state.py | 40 -- .../apps/demo/code/states/pie_state.py | 47 --- reflex/.templates/apps/demo/code/styles.py | 68 ---- .../apps/demo/code/webui/__init__.py | 0 .../demo/code/webui/components/__init__.py | 4 - .../apps/demo/code/webui/components/chat.py | 118 ------ .../code/webui/components/loading_icon.py | 19 - .../apps/demo/code/webui/components/modal.py | 56 --- .../apps/demo/code/webui/components/navbar.py | 70 ---- .../demo/code/webui/components/sidebar.py | 66 ---- .../.templates/apps/demo/code/webui/state.py | 146 ------- .../.templates/apps/demo/code/webui/styles.py | 88 ----- 28 files changed, 2146 deletions(-) delete mode 100644 reflex/.templates/apps/demo/.gitignore delete mode 100644 reflex/.templates/apps/demo/assets/favicon.ico delete mode 100644 reflex/.templates/apps/demo/assets/github.svg delete mode 100644 reflex/.templates/apps/demo/assets/icon.svg delete mode 100644 reflex/.templates/apps/demo/assets/logo.svg delete mode 100644 reflex/.templates/apps/demo/assets/paneleft.svg delete mode 100644 reflex/.templates/apps/demo/code/__init__.py delete mode 100644 reflex/.templates/apps/demo/code/demo.py delete mode 100644 reflex/.templates/apps/demo/code/pages/__init__.py delete mode 100644 reflex/.templates/apps/demo/code/pages/chatapp.py delete mode 100644 reflex/.templates/apps/demo/code/pages/datatable.py delete mode 100644 reflex/.templates/apps/demo/code/pages/forms.py delete mode 100644 reflex/.templates/apps/demo/code/pages/graphing.py delete mode 100644 reflex/.templates/apps/demo/code/pages/home.py delete mode 100644 reflex/.templates/apps/demo/code/sidebar.py delete mode 100644 reflex/.templates/apps/demo/code/state.py delete mode 100644 reflex/.templates/apps/demo/code/states/form_state.py delete mode 100644 reflex/.templates/apps/demo/code/states/pie_state.py delete mode 100644 reflex/.templates/apps/demo/code/styles.py delete mode 100644 reflex/.templates/apps/demo/code/webui/__init__.py delete mode 100644 reflex/.templates/apps/demo/code/webui/components/__init__.py delete mode 100644 reflex/.templates/apps/demo/code/webui/components/chat.py delete mode 100644 reflex/.templates/apps/demo/code/webui/components/loading_icon.py delete mode 100644 reflex/.templates/apps/demo/code/webui/components/modal.py delete mode 100644 reflex/.templates/apps/demo/code/webui/components/navbar.py delete mode 100644 reflex/.templates/apps/demo/code/webui/components/sidebar.py delete mode 100644 reflex/.templates/apps/demo/code/webui/state.py delete mode 100644 reflex/.templates/apps/demo/code/webui/styles.py diff --git a/reflex/.templates/apps/demo/.gitignore b/reflex/.templates/apps/demo/.gitignore deleted file mode 100644 index eab0d4b05..000000000 --- a/reflex/.templates/apps/demo/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -*.db -*.py[cod] -.web -__pycache__/ \ No newline at end of file diff --git a/reflex/.templates/apps/demo/assets/favicon.ico b/reflex/.templates/apps/demo/assets/favicon.ico deleted file mode 100644 index 166ae995eaa63fc96771410a758282dc30e925cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmeHL>rYc>81ELdEe;}zmYd}cUgmJRfwjUwD1`#s5KZP>mMqza#Viv|_7|8f+0+bX zHuqusuw-7Ca`DTu#4U4^o2bjO#K>4%N?Wdi*wZ3Vx%~Ef4}D1`U_EMRg3u z#2#M|V>}}q-@IaO@{9R}d*u7f&~5HfxSkmHVcazU#i30H zAGxQ5Spe!j9`KuGqR@aExK`-}sH1jvqoIp3C7Vm)9Tu=UPE;j^esN~a6^a$ZILngo;^ zGLXl(ZFyY&U!li`6}y-hUQ99v?s`U4O!kgog74FPw-9g+V)qs!jFGEQyvBf><U|E2vRmx|+(VI~S=lT?@~C5pvZOd`x{Q_+3tG6H=gtdWcf z)+7-Zp=UqH^J4sk^>_G-Ufn-2Hz z2mN12|C{5}U`^eCQuFz=F%wp@}SzA1MHEaM^CtJs<{}Tzu$bx2orTKiedgmtVGM{ zdd#vX`&cuiec|My_KW;y{Ryz2kFu9}=~us6hvx1ZqQCk(d+>HP>ks>mmHCjjDh{pe zKQkKpk0SeDX#XMqf$}QV{z=xrN!mQczJAvud@;zFqaU1ocq==Py)qsa=8UKrt!J7r z{RsTo^rgtZo%$rak)DN*D)!(Y^$@yL6Nd=#eu&?unzhH8yq>v{gkt8xcG3S%H)-y_ zqQ1|v|JT$0R~Y}omg2Y+nDvR+K|kzR5i^fmKF>j~N;A35Vr`JWh4yRqKl#P|qlx?` z@|CmBiP}ysYO%m2{eBG6&ix5 zr#u((F2{vb=W4jNmTQh3M^F2o80T49?w>*rv0mt)-o1y!{hRk`E#UVPdna6jnz`rw dKpn)r^--YJZpr;bYU`N~>#v3X5BRU&{{=gv-{1fM diff --git a/reflex/.templates/apps/demo/assets/github.svg b/reflex/.templates/apps/demo/assets/github.svg deleted file mode 100644 index 61c9d791b..000000000 --- a/reflex/.templates/apps/demo/assets/github.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/reflex/.templates/apps/demo/assets/icon.svg b/reflex/.templates/apps/demo/assets/icon.svg deleted file mode 100644 index b9cc89da9..000000000 --- a/reflex/.templates/apps/demo/assets/icon.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reflex/.templates/apps/demo/assets/logo.svg b/reflex/.templates/apps/demo/assets/logo.svg deleted file mode 100644 index 94fe1f511..000000000 --- a/reflex/.templates/apps/demo/assets/logo.svg +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/reflex/.templates/apps/demo/assets/paneleft.svg b/reflex/.templates/apps/demo/assets/paneleft.svg deleted file mode 100644 index ac9c5040a..000000000 --- a/reflex/.templates/apps/demo/assets/paneleft.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/reflex/.templates/apps/demo/code/__init__.py b/reflex/.templates/apps/demo/code/__init__.py deleted file mode 100644 index e1d286346..000000000 --- a/reflex/.templates/apps/demo/code/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Base template for Reflex.""" diff --git a/reflex/.templates/apps/demo/code/demo.py b/reflex/.templates/apps/demo/code/demo.py deleted file mode 100644 index bdb1a447d..000000000 --- a/reflex/.templates/apps/demo/code/demo.py +++ /dev/null @@ -1,127 +0,0 @@ -"""Welcome to Reflex! This file outlines the steps to create a basic app.""" - -from typing import Callable - -import reflex as rx - -from .pages import chatapp_page, datatable_page, forms_page, graphing_page, home_page -from .sidebar import sidebar -from .state import State -from .styles import * - -meta = [ - { - "name": "viewport", - "content": "width=device-width, shrink-to-fit=no, initial-scale=1", - }, -] - - -def template(main_content: Callable[[], rx.Component]) -> rx.Component: - """The template for each page of the app. - - Args: - main_content (Callable[[], rx.Component]): The main content of the page. - - Returns: - rx.Component: The template for each page of the app. - """ - menu_button = rx.chakra.box( - rx.chakra.menu( - rx.chakra.menu_button( - rx.chakra.icon( - tag="hamburger", - size="4em", - color=text_color, - ), - ), - rx.chakra.menu_list( - rx.chakra.menu_item(rx.chakra.link("Home", href="/", width="100%")), - rx.chakra.menu_divider(), - rx.chakra.menu_item( - rx.chakra.link( - "About", href="https://github.com/reflex-dev", width="100%" - ) - ), - rx.chakra.menu_item( - rx.chakra.link( - "Contact", href="mailto:founders@reflex.dev", width="100%" - ) - ), - ), - ), - position="fixed", - right="1.5em", - top="1.5em", - z_index="500", - ) - - return rx.chakra.hstack( - sidebar(), - main_content(), - rx.chakra.spacer(), - menu_button, - align_items="flex-start", - transition="left 0.5s, width 0.5s", - position="relative", - left=rx.cond(State.sidebar_displayed, "0px", f"-{sidebar_width}"), - ) - - -@rx.page("/", meta=meta) -@template -def home() -> rx.Component: - """Home page. - - Returns: - rx.Component: The home page. - """ - return home_page() - - -@rx.page("/forms", meta=meta) -@template -def forms() -> rx.Component: - """Forms page. - - Returns: - rx.Component: The settings page. - """ - return forms_page() - - -@rx.page("/graphing", meta=meta) -@template -def graphing() -> rx.Component: - """Graphing page. - - Returns: - rx.Component: The graphing page. - """ - return graphing_page() - - -@rx.page("/datatable", meta=meta) -@template -def datatable() -> rx.Component: - """Data Table page. - - Returns: - rx.Component: The chatapp page. - """ - return datatable_page() - - -@rx.page("/chatapp", meta=meta) -@template -def chatapp() -> rx.Component: - """Chatapp page. - - Returns: - rx.Component: The chatapp page. - """ - return chatapp_page() - - -# Create the app. -app = rx.App(style=base_style) diff --git a/reflex/.templates/apps/demo/code/pages/__init__.py b/reflex/.templates/apps/demo/code/pages/__init__.py deleted file mode 100644 index ab6df6e3a..000000000 --- a/reflex/.templates/apps/demo/code/pages/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""The pages of the app.""" - -from .chatapp import chatapp_page -from .datatable import datatable_page -from .forms import forms_page -from .graphing import graphing_page -from .home import home_page diff --git a/reflex/.templates/apps/demo/code/pages/chatapp.py b/reflex/.templates/apps/demo/code/pages/chatapp.py deleted file mode 100644 index acd11a599..000000000 --- a/reflex/.templates/apps/demo/code/pages/chatapp.py +++ /dev/null @@ -1,31 +0,0 @@ -"""The main Chat app.""" - -import reflex as rx - -from ..styles import * -from ..webui import styles -from ..webui.components import chat, modal, navbar, sidebar - - -def chatapp_page() -> rx.Component: - """The main app. - - Returns: - The UI for the main app. - """ - return rx.chakra.box( - rx.chakra.vstack( - navbar(), - chat.chat(), - chat.action_bar(), - sidebar(), - modal(), - bg=styles.bg_dark_color, - color=styles.text_light_color, - min_h="100vh", - align_items="stretch", - spacing="0", - style=template_content_style, - ), - style=template_page_style, - ) diff --git a/reflex/.templates/apps/demo/code/pages/datatable.py b/reflex/.templates/apps/demo/code/pages/datatable.py deleted file mode 100644 index 53351ecf3..000000000 --- a/reflex/.templates/apps/demo/code/pages/datatable.py +++ /dev/null @@ -1,360 +0,0 @@ -"""The settings page for the template.""" - -from typing import Any - -import reflex as rx -from reflex.components.datadisplay.dataeditor import DataEditorTheme - -from ..styles import * -from ..webui.state import State - - -class DataTableState(State): - """Datatable state.""" - - cols: list[Any] = [ - {"title": "Title", "type": "str"}, - { - "title": "Name", - "type": "str", - "group": "Data", - "width": 300, - }, - { - "title": "Birth", - "type": "str", - "group": "Data", - "width": 150, - }, - { - "title": "Human", - "type": "bool", - "group": "Data", - "width": 80, - }, - { - "title": "House", - "type": "str", - "group": "Data", - }, - { - "title": "Wand", - "type": "str", - "group": "Data", - "width": 250, - }, - { - "title": "Patronus", - "type": "str", - "group": "Data", - }, - { - "title": "Blood status", - "type": "str", - "group": "Data", - "width": 200, - }, - ] - - data = [ - [ - "1", - "Harry James Potter", - "31 July 1980", - True, - "Gryffindor", - "11' Holly phoenix feather", - "Stag", - "Half-blood", - ], - [ - "2", - "Ronald Bilius Weasley", - "1 March 1980", - True, - "Gryffindor", - "12' Ash unicorn tail hair", - "Jack Russell terrier", - "Pure-blood", - ], - [ - "3", - "Hermione Jean Granger", - "19 September, 1979", - True, - "Gryffindor", - "10¾' vine wood dragon heartstring", - "Otter", - "Muggle-born", - ], - [ - "4", - "Albus Percival Wulfric Brian Dumbledore", - "Late August 1881", - True, - "Gryffindor", - "15' Elder Thestral tail hair core", - "Phoenix", - "Half-blood", - ], - [ - "5", - "Rubeus Hagrid", - "6 December 1928", - False, - "Gryffindor", - "16' Oak unknown core", - "None", - "Part-Human (Half-giant)", - ], - [ - "6", - "Fred Weasley", - "1 April, 1978", - True, - "Gryffindor", - "Unknown", - "Unknown", - "Pure-blood", - ], - [ - "7", - "George Weasley", - "1 April, 1978", - True, - "Gryffindor", - "Unknown", - "Unknown", - "Pure-blood", - ], - ] - - -code_show = """rx.chakra.hstack( - rx.chakra.divider(orientation="vertical", height="100vh", border="solid black 1px"), - rx.chakra.vstack( - rx.chakra.box( - rx.data_editor( - columns=DataTableState.cols, - data=DataTableState.data, - draw_focus_ring=True, - row_height=50, - smooth_scroll_x=True, - smooth_scroll_y=True, - column_select="single", - # style - theme=DataEditorTheme(**darkTheme), - width="80vw", - height="80vh", - ), - ), - rx.chakra.spacer(), - height="100vh", - spacing="25", - ), -)""" - -state_show = """class DataTableState(State): - cols: list[Any] = [ - {"title": "Title", "type": "str"}, - { - "title": "Name", - "type": "str", - "group": "Data", - "width": 300, - }, - { - "title": "Birth", - "type": "str", - "group": "Data", - "width": 150, - }, - { - "title": "Human", - "type": "bool", - "group": "Data", - "width": 80, - }, - { - "title": "House", - "type": "str", - "group": "Data", - }, - { - "title": "Wand", - "type": "str", - "group": "Data", - "width": 250, - }, - { - "title": "Patronus", - "type": "str", - "group": "Data", - }, - { - "title": "Blood status", - "type": "str", - "group": "Data", - "width": 200, - }, - ]""" - -data_show = """[ - ["1", "Harry James Potter", "31 July 1980", True, "Gryffindor", "11' Holly phoenix feather", "Stag", "Half-blood"], - ["2", "Ronald Bilius Weasley", "1 March 1980", True,"Gryffindor", "12' Ash unicorn tail hair", "Jack Russell terrier", "Pure-blood"], - ["3", "Hermione Jean Granger", "19 September, 1979", True, "Gryffindor", "10¾' vine wood dragon heartstring", "Otter", "Muggle-born"], - ["4", "Albus Percival Wulfric Brian Dumbledore", "Late August 1881", True, "Gryffindor", "15' Elder Thestral tail hair core", "Phoenix", "Half-blood"], - ["5", "Rubeus Hagrid", "6 December 1928", False, "Gryffindor", "16' Oak unknown core", "None", "Part-Human (Half-giant)"], - ["6", "Fred Weasley", "1 April, 1978", True, "Gryffindor", "Unknown", "Unknown", "Pure-blood"], - ["7", "George Weasley", "1 April, 1978", True, "Gryffindor", "Unknown", "Unknown", "Pure-blood"], -]""" - - -darkTheme = { - "accent_color": "#8c96ff", - "accent_light": "rgba(202, 206, 255, 0.253)", - "text_dark": "#ffffff", - "text_medium": "#b8b8b8", - "text_light": "#a0a0a0", - "text_bubble": "#ffffff", - "bg_icon_header": "#b8b8b8", - "fg_icon_header": "#000000", - "text_header": "#a1a1a1", - "text_header_selected": "#000000", - "bg_cell": "#16161b", - "bg_cell_medium": "#202027", - "bg_header": "#212121", - "bg_header_has_focus": "#474747", - "bg_header_hovered": "#404040", - "bg_bubble": "#212121", - "bg_bubble_selected": "#000000", - "bg_search_result": "#423c24", - "border_color": "rgba(225,225,225,0.2)", - "drilldown_border": "rgba(225,225,225,0.4)", - "link_color": "#4F5DFF", - "header_font_style": "bold 14px", - "base_font_style": "13px", - "font_family": "Inter, Roboto, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Ubuntu, noto, arial, sans-serif", -} - -darkTheme_show = """darkTheme={ - "accent_color": "#8c96ff", - "accent_light": "rgba(202, 206, 255, 0.253)", - "text_dark": "#ffffff", - "text_medium": "#b8b8b8", - "text_light": "#a0a0a0", - "text_bubble": "#ffffff", - "bg_icon_header": "#b8b8b8", - "fg_icon_header": "#000000", - "text_header": "#a1a1a1", - "text_header_selected": "#000000", - "bg_cell": "#16161b", - "bg_cell_medium": "#202027", - "bg_header": "#212121", - "bg_header_has_focus": "#474747", - "bg_header_hovered": "#404040", - "bg_bubble": "#212121", - "bg_bubble_selected": "#000000", - "bg_search_result": "#423c24", - "border_color": "rgba(225,225,225,0.2)", - "drilldown_border": "rgba(225,225,225,0.4)", - "link_color": "#4F5DFF", - "header_font_style": "bold 14px", - "base_font_style": "13px", - "font_family": "Inter, Roboto, -apple-system, BlinkMacSystemFont, avenir next, avenir, segoe ui, helvetica neue, helvetica, Ubuntu, noto, arial, sans-serif", -}""" - - -def datatable_page() -> rx.Component: - """The UI for the settings page. - - Returns: - rx.Component: The UI for the settings page. - """ - return rx.chakra.box( - rx.chakra.vstack( - rx.chakra.heading( - "Data Table Demo", - font_size="3em", - ), - rx.chakra.hstack( - rx.chakra.vstack( - rx.chakra.box( - rx.data_editor( - columns=DataTableState.cols, - data=DataTableState.data, - draw_focus_ring=True, - row_height=50, - smooth_scroll_x=True, - smooth_scroll_y=True, - column_select="single", - # style - theme=DataEditorTheme(**darkTheme), - width="80vw", - ), - ), - rx.chakra.spacer(), - spacing="25", - ), - ), - rx.chakra.tabs( - rx.chakra.tab_list( - rx.chakra.tab("Code", style=tab_style), - rx.chakra.tab("Data", style=tab_style), - rx.chakra.tab("State", style=tab_style), - rx.chakra.tab("Styling", style=tab_style), - padding_x=0, - ), - rx.chakra.tab_panels( - rx.chakra.tab_panel( - rx.code_block( - code_show, - language="python", - show_line_numbers=True, - ), - width="100%", - padding_x=0, - padding_y=".25em", - ), - rx.chakra.tab_panel( - rx.code_block( - data_show, - language="python", - show_line_numbers=True, - ), - width="100%", - padding_x=0, - padding_y=".25em", - ), - rx.chakra.tab_panel( - rx.code_block( - state_show, - language="python", - show_line_numbers=True, - ), - width="100%", - padding_x=0, - padding_y=".25em", - ), - rx.chakra.tab_panel( - rx.code_block( - darkTheme_show, - language="python", - show_line_numbers=True, - ), - width="100%", - padding_x=0, - padding_y=".25em", - ), - width="100%", - ), - variant="unstyled", - color_scheme="purple", - align="end", - width="100%", - padding_top=".5em", - ), - style=template_content_style, - ), - style=template_page_style, - ) diff --git a/reflex/.templates/apps/demo/code/pages/forms.py b/reflex/.templates/apps/demo/code/pages/forms.py deleted file mode 100644 index 6e3150067..000000000 --- a/reflex/.templates/apps/demo/code/pages/forms.py +++ /dev/null @@ -1,257 +0,0 @@ -"""The settings page for the template.""" - -import reflex as rx - -from ..states.form_state import FormState, UploadState -from ..styles import * - -forms_1_code = """rx.chakra.vstack( - rx.chakra.form( - rx.chakra.vstack( - rx.chakra.input( - placeholder="First Name", - id="first_name", - ), - rx.chakra.input( - placeholder="Last Name", id="last_name" - ), - rx.chakra.hstack( - rx.chakra.checkbox("Checked", id="check"), - rx.chakra.switch("Switched", id="switch"), - ), - rx.chakra.button("Submit", - type_="submit", - bg="#ecfdf5", - color="#047857", - border_radius="lg", - ), - ), - on_submit=FormState.handle_submit, - ), - rx.chakra.divider(), - rx.chakra.heading("Results"), - rx.chakra.text(FormState.form_data.to_string()), - width="100%", -)""" - -color = "rgb(107,99,246)" - -forms_1_state = """class FormState(rx.State): - - form_data: dict = {} - - def handle_submit(self, form_data: dict): - "Handle the form submit." - self.form_data = form_data""" - - -forms_2_code = """rx.chakra.vstack( - rx.upload( - rx.chakra.vstack( - rx.chakra.button( - "Select File", - color=color, - bg="white", - border=f"1px solid {color}", - ), - rx.chakra.text( - "Drag and drop files here or click to select files" - ), - ), - border=f"1px dotted {color}", - padding="5em", - ), - rx.chakra.hstack(rx.foreach(rx.selected_files, rx.chakra.text)), - rx.chakra.button( - "Upload", - on_click=lambda: UploadState.handle_upload( - rx.upload_files() - ), - ), - rx.chakra.button( - "Clear", - on_click=rx.clear_selected_files, - ), - rx.foreach( - UploadState.img, lambda img: rx.chakra.image(src=img, width="20%", height="auto",) - ), - padding="5em", - width="100%", -)""" - -forms_2_state = """class UploadState(State): - "The app state." - - # The images to show. - img: list[str] - - async def handle_upload( - self, files: list[rx.UploadFile] - ): - "Handle the upload of file(s). - - Args: - files: The uploaded files. - " - for file in files: - upload_data = await file.read() - outfile = rx.get_asset_path(file.filename) - # Save the file. - with open(outfile, "wb") as file_object: - file_object.write(upload_data) - - # Update the img var. - self.img.append(f"/{file.filename}")""" - - -def forms_page() -> rx.Component: - """The UI for the settings page. - - Returns: - rx.Component: The UI for the settings page. - """ - return rx.chakra.box( - rx.chakra.vstack( - rx.chakra.heading( - "Forms Demo", - font_size="3em", - ), - rx.chakra.vstack( - rx.chakra.form( - rx.chakra.vstack( - rx.chakra.input( - placeholder="First Name", - id="first_name", - ), - rx.chakra.input(placeholder="Last Name", id="last_name"), - rx.chakra.hstack( - rx.chakra.checkbox("Checked", id="check"), - rx.chakra.switch("Switched", id="switch"), - ), - rx.chakra.button( - "Submit", - type_="submit", - bg="#ecfdf5", - color="#047857", - border_radius="lg", - ), - ), - on_submit=FormState.handle_submit, - ), - rx.chakra.divider(), - rx.chakra.heading("Results"), - rx.chakra.text(FormState.form_data.to_string()), - width="100%", - ), - rx.chakra.tabs( - rx.chakra.tab_list( - rx.chakra.tab("Code", style=tab_style), - rx.chakra.tab("State", style=tab_style), - padding_x=0, - ), - rx.chakra.tab_panels( - rx.chakra.tab_panel( - rx.code_block( - forms_1_code, - language="python", - show_line_numbers=True, - ), - width="100%", - padding_x=0, - padding_y=".25em", - ), - rx.chakra.tab_panel( - rx.code_block( - forms_1_state, - language="python", - show_line_numbers=True, - ), - width="100%", - padding_x=0, - padding_y=".25em", - ), - width="100%", - ), - variant="unstyled", - color_scheme="purple", - align="end", - width="100%", - padding_top=".5em", - ), - rx.chakra.heading("Upload Example", font_size="3em"), - rx.chakra.text("Try uploading some images and see how they look."), - rx.chakra.vstack( - rx.upload( - rx.chakra.vstack( - rx.chakra.button( - "Select File", - color=color, - bg="white", - border=f"1px solid {color}", - ), - rx.chakra.text( - "Drag and drop files here or click to select files" - ), - ), - border=f"1px dotted {color}", - padding="5em", - ), - rx.chakra.hstack(rx.foreach(rx.selected_files, rx.chakra.text)), - rx.chakra.button( - "Upload", - on_click=lambda: UploadState.handle_upload(rx.upload_files()), - ), - rx.chakra.button( - "Clear", - on_click=rx.clear_selected_files, - ), - rx.foreach( - UploadState.img, - lambda img: rx.chakra.image( - src=img, - width="20%", - height="auto", - ), - ), - padding="5em", - width="100%", - ), - rx.chakra.tabs( - rx.chakra.tab_list( - rx.chakra.tab("Code", style=tab_style), - rx.chakra.tab("State", style=tab_style), - padding_x=0, - ), - rx.chakra.tab_panels( - rx.chakra.tab_panel( - rx.code_block( - forms_2_code, - language="python", - show_line_numbers=True, - ), - width="100%", - padding_x=0, - padding_y=".25em", - ), - rx.chakra.tab_panel( - rx.code_block( - forms_2_state, - language="python", - show_line_numbers=True, - ), - width="100%", - padding_x=0, - padding_y=".25em", - ), - width="100%", - ), - variant="unstyled", - color_scheme="purple", - align="end", - width="100%", - padding_top=".5em", - ), - style=template_content_style, - ), - style=template_page_style, - ) diff --git a/reflex/.templates/apps/demo/code/pages/graphing.py b/reflex/.templates/apps/demo/code/pages/graphing.py deleted file mode 100644 index e56af96c1..000000000 --- a/reflex/.templates/apps/demo/code/pages/graphing.py +++ /dev/null @@ -1,253 +0,0 @@ -"""The dashboard page for the template.""" - -import reflex as rx - -from ..states.pie_state import PieChartState -from ..styles import * - -data_1 = [ - {"name": "Page A", "uv": 4000, "pv": 2400, "amt": 2400}, - {"name": "Page B", "uv": 3000, "pv": 1398, "amt": 2210}, - {"name": "Page C", "uv": 2000, "pv": 9800, "amt": 2290}, - {"name": "Page D", "uv": 2780, "pv": 3908, "amt": 2000}, - {"name": "Page E", "uv": 1890, "pv": 4800, "amt": 2181}, - {"name": "Page F", "uv": 2390, "pv": 3800, "amt": 2500}, - {"name": "Page G", "uv": 3490, "pv": 4300, "amt": 2100}, -] -data_1_show = """[ - {"name": "Page A", "uv": 4000, "pv": 2400, "amt": 2400}, - {"name": "Page B", "uv": 3000, "pv": 1398, "amt": 2210}, - {"name": "Page C", "uv": 2000, "pv": 9800, "amt": 2290}, - {"name": "Page D", "uv": 2780, "pv": 3908, "amt": 2000}, - {"name": "Page E", "uv": 1890, "pv": 4800, "amt": 2181}, - {"name": "Page F", "uv": 2390, "pv": 3800, "amt": 2500}, - {"name": "Page G", "uv": 3490, "pv": 4300, "amt": 2100}, -]""" - - -graph_1_code = """rx.recharts.composed_chart( - rx.recharts.area( - data_key="uv", stroke="#8884d8", fill="#8884d8" - ), - rx.recharts.bar( - data_key="amt", bar_size=20, fill="#413ea0" - ), - rx.recharts.line( - data_key="pv", type_="monotone", stroke="#ff7300" - ), - rx.recharts.x_axis(data_key="name"), - rx.recharts.y_axis(), - rx.recharts.cartesian_grid(stroke_dasharray="3 3"), - rx.recharts.graphing_tooltip(), - data=data, -)""" - - -graph_2_code = """rx.recharts.pie_chart( - rx.recharts.pie( - data=PieChartState.resources, - data_key="count", - name_key="type_", - cx="50%", - cy="50%", - start_angle=180, - end_angle=0, - fill="#8884d8", - label=True, - ), - rx.recharts.graphing_tooltip(), -), -rx.chakra.vstack( - rx.foreach( - PieChartState.resource_types, - lambda type_, i: rx.chakra.hstack( - rx.chakra.button( - "-", - on_click=PieChartState.decrement(type_), - ), - rx.chakra.text( - type_, - PieChartState.resources[i]["count"], - ), - rx.chakra.button( - "+", - on_click=PieChartState.increment(type_), - ), - ), - ), -)""" - -graph_2_state = """class PieChartState(rx.State): - resources: list[dict[str, Any]] = [ - dict(type_="🏆", count=1), - dict(type_="🪵", count=1), - dict(type_="🥑", count=1), - dict(type_="🧱", count=1), - ] - - @rx.cached_var - def resource_types(self) -> list[str]: - return [r["type_"] for r in self.resources] - - def increment(self, type_: str): - for resource in self.resources: - if resource["type_"] == type_: - resource["count"] += 1 - break - - def decrement(self, type_: str): - for resource in self.resources: - if ( - resource["type_"] == type_ - and resource["count"] > 0 - ): - resource["count"] -= 1 - break -""" - - -def graphing_page() -> rx.Component: - """The UI for the dashboard page. - - Returns: - rx.Component: The UI for the dashboard page. - """ - return rx.chakra.box( - rx.chakra.vstack( - rx.chakra.heading( - "Graphing Demo", - font_size="3em", - ), - rx.chakra.heading( - "Composed Chart", - font_size="2em", - ), - rx.chakra.stack( - rx.recharts.composed_chart( - rx.recharts.area(data_key="uv", stroke="#8884d8", fill="#8884d8"), - rx.recharts.bar(data_key="amt", bar_size=20, fill="#413ea0"), - rx.recharts.line(data_key="pv", type_="monotone", stroke="#ff7300"), - rx.recharts.x_axis(data_key="name"), - rx.recharts.y_axis(), - rx.recharts.cartesian_grid(stroke_dasharray="3 3"), - rx.recharts.graphing_tooltip(), - data=data_1, - # height="15em", - ), - width="100%", - height="20em", - ), - rx.chakra.tabs( - rx.chakra.tab_list( - rx.chakra.tab("Code", style=tab_style), - rx.chakra.tab("Data", style=tab_style), - padding_x=0, - ), - rx.chakra.tab_panels( - rx.chakra.tab_panel( - rx.code_block( - graph_1_code, - language="python", - show_line_numbers=True, - ), - width="100%", - padding_x=0, - padding_y=".25em", - ), - rx.chakra.tab_panel( - rx.code_block( - data_1_show, - language="python", - show_line_numbers=True, - ), - width="100%", - padding_x=0, - padding_y=".25em", - ), - width="100%", - ), - variant="unstyled", - color_scheme="purple", - align="end", - width="100%", - padding_top=".5em", - ), - rx.chakra.heading("Interactive Example", font_size="2em"), - rx.chakra.hstack( - rx.recharts.pie_chart( - rx.recharts.pie( - data=PieChartState.resources, - data_key="count", - name_key="type_", - cx="50%", - cy="50%", - start_angle=180, - end_angle=0, - fill="#8884d8", - label=True, - ), - rx.recharts.graphing_tooltip(), - ), - rx.chakra.vstack( - rx.foreach( - PieChartState.resource_types, - lambda type_, i: rx.chakra.hstack( - rx.chakra.button( - "-", - on_click=PieChartState.decrement(type_), - ), - rx.chakra.text( - type_, - PieChartState.resources[i]["count"], - ), - rx.chakra.button( - "+", - on_click=PieChartState.increment(type_), - ), - ), - ), - ), - width="100%", - height="15em", - ), - rx.chakra.tabs( - rx.chakra.tab_list( - rx.chakra.tab("Code", style=tab_style), - rx.chakra.tab("State", style=tab_style), - padding_x=0, - ), - rx.chakra.tab_panels( - rx.chakra.tab_panel( - rx.code_block( - graph_2_code, - language="python", - show_line_numbers=True, - ), - width="100%", - padding_x=0, - padding_y=".25em", - ), - rx.chakra.tab_panel( - rx.code_block( - graph_2_state, - language="python", - show_line_numbers=True, - ), - width="100%", - padding_x=0, - padding_y=".25em", - ), - width="100%", - ), - variant="unstyled", - color_scheme="purple", - align="end", - width="100%", - padding_top=".5em", - ), - style=template_content_style, - min_h="100vh", - ), - style=template_page_style, - min_h="100vh", - ) diff --git a/reflex/.templates/apps/demo/code/pages/home.py b/reflex/.templates/apps/demo/code/pages/home.py deleted file mode 100644 index 8d85e015b..000000000 --- a/reflex/.templates/apps/demo/code/pages/home.py +++ /dev/null @@ -1,56 +0,0 @@ -"""The home page of the app.""" - -import reflex as rx - -from ..styles import * - - -def home_page() -> rx.Component: - """The UI for the home page. - - Returns: - rx.Component: The UI for the home page. - """ - return rx.chakra.box( - rx.chakra.vstack( - rx.chakra.heading( - "Welcome to Reflex! 👋", - font_size="3em", - ), - rx.chakra.text( - "Reflex is an open-source app framework built specifically to allow you to build web apps in pure python. 👈 Select a demo from the sidebar to see some examples of what Reflex can do!", - ), - rx.chakra.heading( - "Things to check out:", - font_size="2em", - ), - rx.chakra.unordered_list( - rx.chakra.list_item( - "Take a look at ", - rx.chakra.link( - "reflex.dev", - href="https://reflex.dev", - color="rgb(107,99,246)", - ), - ), - rx.chakra.list_item( - "Check out our ", - rx.chakra.link( - "docs", - href="https://reflex.dev/docs/getting-started/introduction/", - color="rgb(107,99,246)", - ), - ), - rx.chakra.list_item( - "Ask a question in our ", - rx.chakra.link( - "community", - href="https://discord.gg/T5WSbC2YtQ", - color="rgb(107,99,246)", - ), - ), - ), - style=template_content_style, - ), - style=template_page_style, - ) diff --git a/reflex/.templates/apps/demo/code/sidebar.py b/reflex/.templates/apps/demo/code/sidebar.py deleted file mode 100644 index 5f634402f..000000000 --- a/reflex/.templates/apps/demo/code/sidebar.py +++ /dev/null @@ -1,178 +0,0 @@ -"""Sidebar component for the app.""" - -import reflex as rx - -from .state import State -from .styles import * - - -def sidebar_header() -> rx.Component: - """Sidebar header. - - Returns: - rx.Component: The sidebar header component. - """ - return rx.chakra.hstack( - rx.chakra.image( - src="/icon.svg", - height="2em", - ), - rx.chakra.spacer(), - rx.chakra.link( - rx.chakra.center( - rx.chakra.image( - src="/github.svg", - height="3em", - padding="0.5em", - ), - box_shadow=box_shadow, - bg="transparent", - border_radius=border_radius, - _hover={ - "bg": accent_color, - }, - ), - href="https://github.com/reflex-dev/reflex", - ), - width="100%", - border_bottom=border, - padding="1em", - ) - - -def sidebar_footer() -> rx.Component: - """Sidebar footer. - - Returns: - rx.Component: The sidebar footer component. - """ - return rx.chakra.hstack( - rx.chakra.link( - rx.chakra.center( - rx.chakra.image( - src="/paneleft.svg", - height="2em", - padding="0.5em", - ), - bg="transparent", - border_radius=border_radius, - **hover_accent_bg, - ), - on_click=State.toggle_sidebar_displayed, - transform=rx.cond(~State.sidebar_displayed, "rotate(180deg)", ""), - transition="transform 0.5s, left 0.5s", - position="relative", - left=rx.cond(State.sidebar_displayed, "0px", "20.5em"), - **overlapping_button_style, - ), - rx.chakra.spacer(), - rx.chakra.link( - rx.chakra.text( - "Docs", - ), - href="https://reflex.dev/docs/getting-started/introduction/", - ), - rx.chakra.link( - rx.chakra.text( - "Blog", - ), - href="https://reflex.dev/blog/", - ), - width="100%", - border_top=border, - padding="1em", - ) - - -def sidebar_item(text: str, icon: str, url: str) -> rx.Component: - """Sidebar item. - - Args: - text (str): The text of the item. - icon (str): The icon of the item. - url (str): The URL of the item. - - Returns: - rx.Component: The sidebar item component. - """ - return rx.chakra.link( - rx.chakra.hstack( - rx.chakra.image( - src=icon, - height="2.5em", - padding="0.5em", - ), - rx.chakra.text( - text, - ), - bg=rx.cond( - State.origin_url == f"/{text.lower()}/", - accent_color, - "transparent", - ), - color=rx.cond( - State.origin_url == f"/{text.lower()}/", - accent_text_color, - text_color, - ), - border_radius=border_radius, - box_shadow=box_shadow, - width="100%", - padding_x="1em", - ), - href=url, - width="100%", - ) - - -def sidebar() -> rx.Component: - """Sidebar. - - Returns: - rx.Component: The sidebar component. - """ - return rx.chakra.box( - rx.chakra.vstack( - sidebar_header(), - rx.chakra.vstack( - sidebar_item( - "Welcome", - "/github.svg", - "/", - ), - sidebar_item( - "Graphing Demo", - "/github.svg", - "/graphing", - ), - sidebar_item( - "Data Table Demo", - "/github.svg", - "/datatable", - ), - sidebar_item( - "Forms Demo", - "/github.svg", - "/forms", - ), - sidebar_item( - "Chat App Demo", - "/github.svg", - "/chatapp", - ), - width="100%", - overflow_y="auto", - align_items="flex-start", - padding="1em", - ), - rx.chakra.spacer(), - sidebar_footer(), - height="100dvh", - ), - display=["none", "none", "block"], - min_width=sidebar_width, - height="100%", - position="sticky", - top="0px", - border_right=border, - ) diff --git a/reflex/.templates/apps/demo/code/state.py b/reflex/.templates/apps/demo/code/state.py deleted file mode 100644 index a5c6f57bd..000000000 --- a/reflex/.templates/apps/demo/code/state.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Base state for the app.""" - -import reflex as rx - - -class State(rx.State): - """State for the app.""" - - sidebar_displayed: bool = True - - @rx.var - def origin_url(self) -> str: - """Get the url of the current page. - - Returns: - str: The url of the current page. - """ - return self.router_data.get("asPath", "") - - def toggle_sidebar_displayed(self) -> None: - """Toggle the sidebar displayed.""" - self.sidebar_displayed = not self.sidebar_displayed diff --git a/reflex/.templates/apps/demo/code/states/form_state.py b/reflex/.templates/apps/demo/code/states/form_state.py deleted file mode 100644 index 2b30e859e..000000000 --- a/reflex/.templates/apps/demo/code/states/form_state.py +++ /dev/null @@ -1,40 +0,0 @@ -import reflex as rx - -from ..state import State - - -class FormState(State): - """Form state.""" - - form_data: dict = {} - - def handle_submit(self, form_data: dict): - """Handle the form submit. - - Args: - form_data: The form data. - """ - self.form_data = form_data - - -class UploadState(State): - """The app state.""" - - # The images to show. - img: list[str] - - async def handle_upload(self, files: list[rx.UploadFile]): - """Handle the upload of file(s). - - Args: - files: The uploaded files. - """ - for file in files: - upload_data = await file.read() - outfile = rx.get_asset_path(file.filename) - # Save the file. - with open(outfile, "wb") as file_object: - file_object.write(upload_data) - - # Update the img var. - self.img.append(f"/{file.filename}") diff --git a/reflex/.templates/apps/demo/code/states/pie_state.py b/reflex/.templates/apps/demo/code/states/pie_state.py deleted file mode 100644 index 1c380c3af..000000000 --- a/reflex/.templates/apps/demo/code/states/pie_state.py +++ /dev/null @@ -1,47 +0,0 @@ -from typing import Any - -import reflex as rx - -from ..state import State - - -class PieChartState(State): - """Pie Chart State.""" - - resources: list[dict[str, Any]] = [ - dict(type_="🏆", count=1), - dict(type_="🪵", count=1), - dict(type_="🥑", count=1), - dict(type_="🧱", count=1), - ] - - @rx.cached_var - def resource_types(self) -> list[str]: - """Get the resource types. - - Returns: - The resource types. - """ - return [r["type_"] for r in self.resources] - - def increment(self, type_: str): - """Increment the count of a resource type. - - Args: - type_: The type of resource to increment. - """ - for resource in self.resources: - if resource["type_"] == type_: - resource["count"] += 1 - break - - def decrement(self, type_: str): - """Decrement the count of a resource type. - - Args: - type_: The type of resource to decrement. - """ - for resource in self.resources: - if resource["type_"] == type_ and resource["count"] > 0: - resource["count"] -= 1 - break diff --git a/reflex/.templates/apps/demo/code/styles.py b/reflex/.templates/apps/demo/code/styles.py deleted file mode 100644 index 5ca236ece..000000000 --- a/reflex/.templates/apps/demo/code/styles.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Styles for the app.""" - -import reflex as rx - -from .state import State - -border_radius = "0.375rem" -box_shadow = "0px 0px 0px 1px rgba(84, 82, 95, 0.14)" -border = "1px solid #F4F3F6" -text_color = "black" -accent_text_color = "#1A1060" -accent_color = "#F5EFFE" -hover_accent_color = {"_hover": {"color": accent_color}} -hover_accent_bg = {"_hover": {"bg": accent_color}} -content_width_vw = "90vw" -sidebar_width = "20em" - -template_page_style = { - "padding_top": "5em", - "padding_x": "2em", -} - -template_content_style = { - "width": rx.cond( - State.sidebar_displayed, - f"calc({content_width_vw} - {sidebar_width})", - content_width_vw, - ), - "min-width": sidebar_width, - "align_items": "flex-start", - "box_shadow": box_shadow, - "border_radius": border_radius, - "padding": "1em", - "margin_bottom": "2em", -} - -link_style = { - "color": text_color, - "text_decoration": "none", - **hover_accent_color, -} - -overlapping_button_style = { - "background_color": "white", - "border": border, - "border_radius": border_radius, -} - -base_style = { - rx.chakra.MenuButton: { - "width": "3em", - "height": "3em", - **overlapping_button_style, - }, - rx.chakra.MenuItem: hover_accent_bg, -} - -tab_style = { - "color": "#494369", - "font_weight": 600, - "_selected": { - "color": "#5646ED", - "bg": "#F5EFFE", - "padding_x": "0.5em", - "padding_y": "0.25em", - "border_radius": "8px", - }, -} diff --git a/reflex/.templates/apps/demo/code/webui/__init__.py b/reflex/.templates/apps/demo/code/webui/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/reflex/.templates/apps/demo/code/webui/components/__init__.py b/reflex/.templates/apps/demo/code/webui/components/__init__.py deleted file mode 100644 index e29eb0ab2..000000000 --- a/reflex/.templates/apps/demo/code/webui/components/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .loading_icon import loading_icon -from .modal import modal -from .navbar import navbar -from .sidebar import sidebar diff --git a/reflex/.templates/apps/demo/code/webui/components/chat.py b/reflex/.templates/apps/demo/code/webui/components/chat.py deleted file mode 100644 index fd9b6ba98..000000000 --- a/reflex/.templates/apps/demo/code/webui/components/chat.py +++ /dev/null @@ -1,118 +0,0 @@ -import reflex as rx - -from ...webui import styles -from ...webui.components import loading_icon -from ...webui.state import QA, State - - -def message(qa: QA) -> rx.Component: - """A single question/answer message. - - Args: - qa: The question/answer pair. - - Returns: - A component displaying the question/answer pair. - """ - return rx.chakra.box( - rx.chakra.box( - rx.chakra.text( - qa.question, - bg=styles.border_color, - shadow=styles.shadow_light, - **styles.message_style, - ), - text_align="right", - margin_top="1em", - ), - rx.chakra.box( - rx.chakra.text( - qa.answer, - bg=styles.accent_color, - shadow=styles.shadow_light, - **styles.message_style, - ), - text_align="left", - padding_top="1em", - ), - width="100%", - ) - - -def chat() -> rx.Component: - """List all the messages in a single conversation. - - Returns: - A component displaying all the messages in a single conversation. - """ - return rx.chakra.vstack( - rx.chakra.box(rx.foreach(State.chats[State.current_chat], message)), - py="8", - flex="1", - width="100%", - max_w="3xl", - padding_x="4", - align_self="center", - overflow="hidden", - padding_bottom="5em", - **styles.base_style[rx.chakra.Vstack], - ) - - -def action_bar() -> rx.Component: - """The action bar to send a new message. - - Returns: - The action bar to send a new message. - """ - return rx.chakra.box( - rx.chakra.vstack( - rx.chakra.form( - rx.chakra.form_control( - rx.chakra.hstack( - rx.chakra.input( - placeholder="Type something...", - value=State.question, - on_change=State.set_question, - _placeholder={"color": "#fffa"}, - _hover={"border_color": styles.accent_color}, - style=styles.input_style, - ), - rx.chakra.button( - rx.cond( - State.processing, - loading_icon(height="1em"), - rx.chakra.text("Send"), - ), - type_="submit", - _hover={"bg": styles.accent_color}, - style=styles.input_style, - ), - **styles.base_style[rx.chakra.Hstack], - ), - is_disabled=State.processing, - ), - on_submit=State.process_question, - width="100%", - ), - rx.chakra.text( - "ReflexGPT may return factually incorrect or misleading responses. Use discretion.", - font_size="xs", - color="#fff6", - text_align="center", - ), - width="100%", - max_w="3xl", - mx="auto", - **styles.base_style[rx.chakra.Vstack], - ), - position="sticky", - bottom="0", - left="0", - py="4", - backdrop_filter="auto", - backdrop_blur="lg", - border_top=f"1px solid {styles.border_color}", - align_items="stretch", - width="100%", - ) diff --git a/reflex/.templates/apps/demo/code/webui/components/loading_icon.py b/reflex/.templates/apps/demo/code/webui/components/loading_icon.py deleted file mode 100644 index 46cedb8ef..000000000 --- a/reflex/.templates/apps/demo/code/webui/components/loading_icon.py +++ /dev/null @@ -1,19 +0,0 @@ -import reflex as rx - - -class LoadingIcon(rx.Component): - """A custom loading icon component.""" - - library = "react-loading-icons" - tag = "SpinningCircles" - stroke: rx.Var[str] - stroke_opacity: rx.Var[str] - fill: rx.Var[str] - fill_opacity: rx.Var[str] - stroke_width: rx.Var[str] - speed: rx.Var[str] - height: rx.Var[str] - on_change: rx.EventHandler[lambda status: [status]] - - -loading_icon = LoadingIcon.create diff --git a/reflex/.templates/apps/demo/code/webui/components/modal.py b/reflex/.templates/apps/demo/code/webui/components/modal.py deleted file mode 100644 index c76b8dffd..000000000 --- a/reflex/.templates/apps/demo/code/webui/components/modal.py +++ /dev/null @@ -1,56 +0,0 @@ -import reflex as rx - -from ...webui.state import State - - -def modal() -> rx.Component: - """A modal to create a new chat. - - Returns: - The modal component. - """ - return rx.chakra.modal( - rx.chakra.modal_overlay( - rx.chakra.modal_content( - rx.chakra.modal_header( - rx.chakra.hstack( - rx.chakra.text("Create new chat"), - rx.chakra.icon( - tag="close", - font_size="sm", - on_click=State.toggle_modal, - color="#fff8", - _hover={"color": "#fff"}, - cursor="pointer", - ), - align_items="center", - justify_content="space-between", - ) - ), - rx.chakra.modal_body( - rx.chakra.input( - placeholder="Type something...", - on_blur=State.set_new_chat_name, - bg="#222", - border_color="#fff3", - _placeholder={"color": "#fffa"}, - ), - ), - rx.chakra.modal_footer( - rx.chakra.button( - "Create", - bg="#5535d4", - box_shadow="md", - px="4", - py="2", - h="auto", - _hover={"bg": "#4c2db3"}, - on_click=State.create_chat, - ), - ), - bg="#222", - color="#fff", - ), - ), - is_open=State.modal_open, - ) diff --git a/reflex/.templates/apps/demo/code/webui/components/navbar.py b/reflex/.templates/apps/demo/code/webui/components/navbar.py deleted file mode 100644 index e836f29ec..000000000 --- a/reflex/.templates/apps/demo/code/webui/components/navbar.py +++ /dev/null @@ -1,70 +0,0 @@ -import reflex as rx - -from ...webui import styles -from ...webui.state import State - - -def navbar(): - return rx.chakra.box( - rx.chakra.hstack( - rx.chakra.hstack( - rx.chakra.icon( - tag="hamburger", - mr=4, - on_click=State.toggle_drawer, - cursor="pointer", - ), - rx.chakra.link( - rx.chakra.box( - rx.chakra.image(src="favicon.ico", width=30, height="auto"), - p="1", - border_radius="6", - bg="#F0F0F0", - mr="2", - ), - href="/", - ), - rx.chakra.breadcrumb( - rx.chakra.breadcrumb_item( - rx.chakra.heading("ReflexGPT", size="sm"), - ), - rx.chakra.breadcrumb_item( - rx.chakra.text( - State.current_chat, size="sm", font_weight="normal" - ), - ), - ), - ), - rx.chakra.hstack( - rx.chakra.button( - "+ New chat", - bg=styles.accent_color, - px="4", - py="2", - h="auto", - on_click=State.toggle_modal, - ), - rx.chakra.menu( - rx.chakra.menu_button( - rx.chakra.avatar(name="User", size="md"), - rx.chakra.box(), - ), - rx.chakra.menu_list( - rx.chakra.menu_item("Help"), - rx.chakra.menu_divider(), - rx.chakra.menu_item("Settings"), - ), - ), - spacing="8", - ), - justify="space-between", - ), - bg=styles.bg_dark_color, - backdrop_filter="auto", - backdrop_blur="lg", - p="4", - border_bottom=f"1px solid {styles.border_color}", - position="sticky", - top="0", - z_index="100", - ) diff --git a/reflex/.templates/apps/demo/code/webui/components/sidebar.py b/reflex/.templates/apps/demo/code/webui/components/sidebar.py deleted file mode 100644 index b4ffdd41f..000000000 --- a/reflex/.templates/apps/demo/code/webui/components/sidebar.py +++ /dev/null @@ -1,66 +0,0 @@ -import reflex as rx - -from ...webui import styles -from ...webui.state import State - - -def sidebar_chat(chat: str) -> rx.Component: - """A sidebar chat item. - - Args: - chat: The chat item. - - Returns: - The sidebar chat item. - """ - return rx.chakra.hstack( - rx.chakra.box( - chat, - on_click=lambda: State.set_chat(chat), - style=styles.sidebar_style, - color=styles.icon_color, - flex="1", - ), - rx.chakra.box( - rx.chakra.icon( - tag="delete", - style=styles.icon_style, - on_click=State.delete_chat, - ), - style=styles.sidebar_style, - ), - color=styles.text_light_color, - cursor="pointer", - ) - - -def sidebar() -> rx.Component: - """The sidebar component. - - Returns: - The sidebar component. - """ - return rx.chakra.drawer( - rx.chakra.drawer_overlay( - rx.chakra.drawer_content( - rx.chakra.drawer_header( - rx.chakra.hstack( - rx.chakra.text("Chats"), - rx.chakra.icon( - tag="close", - on_click=State.toggle_drawer, - style=styles.icon_style, - ), - ) - ), - rx.chakra.drawer_body( - rx.chakra.vstack( - rx.foreach(State.chat_titles, lambda chat: sidebar_chat(chat)), - align_items="stretch", - ) - ), - ), - ), - placement="left", - is_open=State.drawer_open, - ) diff --git a/reflex/.templates/apps/demo/code/webui/state.py b/reflex/.templates/apps/demo/code/webui/state.py deleted file mode 100644 index 51739222d..000000000 --- a/reflex/.templates/apps/demo/code/webui/state.py +++ /dev/null @@ -1,146 +0,0 @@ -import asyncio - -import reflex as rx - -from ..state import State - -# openai.api_key = os.environ["OPENAI_API_KEY"] -# openai.api_base = os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1") - - -class QA(rx.Base): - """A question and answer pair.""" - - question: str - answer: str - - -DEFAULT_CHATS = { - "Intros": [], -} - - -class State(State): - """The app state.""" - - # A dict from the chat name to the list of questions and answers. - chats: dict[str, list[QA]] = DEFAULT_CHATS - - # The current chat name. - current_chat = "Intros" - - # The current question. - question: str - - # Whether we are processing the question. - processing: bool = False - - # The name of the new chat. - new_chat_name: str = "" - - # Whether the drawer is open. - drawer_open: bool = False - - # Whether the modal is open. - modal_open: bool = False - - def create_chat(self): - """Create a new chat.""" - # Add the new chat to the list of chats. - self.current_chat = self.new_chat_name - self.chats[self.new_chat_name] = [] - - # Toggle the modal. - self.modal_open = False - - def toggle_modal(self): - """Toggle the new chat modal.""" - self.modal_open = not self.modal_open - - def toggle_drawer(self): - """Toggle the drawer.""" - self.drawer_open = not self.drawer_open - - def delete_chat(self): - """Delete the current chat.""" - del self.chats[self.current_chat] - if len(self.chats) == 0: - self.chats = DEFAULT_CHATS - # set self.current_chat to the first chat. - self.current_chat = next(iter(self.chats)) - self.toggle_drawer() - - def set_chat(self, chat_name: str): - """Set the name of the current chat. - - Args: - chat_name: The name of the chat. - """ - self.current_chat = chat_name - self.toggle_drawer() - - @rx.var - def chat_titles(self) -> list[str]: - """Get the list of chat titles. - - Returns: - The list of chat names. - """ - return [*self.chats] - - async def process_question(self, form_data: dict[str, str]): - """Get the response from the API. - - Args: - form_data: A dict with the current question. - - Yields: - The current question and the response. - """ - # Check if the question is empty - if self.question == "": - return - - # Add the question to the list of questions. - qa = QA(question=self.question, answer="") - self.chats[self.current_chat].append(qa) - - # Clear the input and start the processing. - self.processing = True - self.question = "" - yield - - # # Build the messages. - # messages = [ - # {"role": "system", "content": "You are a friendly chatbot named Reflex."} - # ] - # for qa in self.chats[self.current_chat]: - # messages.append({"role": "user", "content": qa.question}) - # messages.append({"role": "assistant", "content": qa.answer}) - - # # Remove the last mock answer. - # messages = messages[:-1] - - # Start a new session to answer the question. - # session = openai.ChatCompletion.create( - # model=os.getenv("OPENAI_MODEL", "gpt-3.5-turbo"), - # messages=messages, - # stream=True, - # ) - - # Stream the results, yielding after every word. - # for item in session: - answer = "I don't know! This Chatbot still needs to add in AI API keys!" - for i in range(len(answer)): - # Pause to show the streaming effect. - await asyncio.sleep(0.1) - # Add one letter at a time to the output. - - # if hasattr(item.choices[0].delta, "content"): - # answer_text = item.choices[0].delta.content - self.chats[self.current_chat][-1].answer += answer[i] - self.chats = self.chats - yield - - # Toggle the processing flag. - self.processing = False diff --git a/reflex/.templates/apps/demo/code/webui/styles.py b/reflex/.templates/apps/demo/code/webui/styles.py deleted file mode 100644 index 62212ac1a..000000000 --- a/reflex/.templates/apps/demo/code/webui/styles.py +++ /dev/null @@ -1,88 +0,0 @@ -import reflex as rx - -bg_dark_color = "#111" -bg_medium_color = "#222" - -border_color = "#fff3" - -accennt_light = "#6649D8" -accent_color = "#5535d4" -accent_dark = "#4c2db3" - -icon_color = "#fff8" - -text_light_color = "#fff" -shadow_light = "rgba(17, 12, 46, 0.15) 0px 48px 100px 0px;" -shadow = "rgba(50, 50, 93, 0.25) 0px 50px 100px -20px, rgba(0, 0, 0, 0.3) 0px 30px 60px -30px, rgba(10, 37, 64, 0.35) 0px -2px 6px 0px inset;" - -message_style = dict(display="inline-block", p="4", border_radius="xl", max_w="30em") - -input_style = dict( - bg=bg_medium_color, - border_color=border_color, - border_width="1px", - p="4", -) - -icon_style = dict( - font_size="md", - color=icon_color, - _hover=dict(color=text_light_color), - cursor="pointer", - w="8", -) - -sidebar_style = dict( - border="double 1px transparent;", - border_radius="10px;", - background_image=f"linear-gradient({bg_dark_color}, {bg_dark_color}), radial-gradient(circle at top left, {accent_color},{accent_dark});", - background_origin="border-box;", - background_clip="padding-box, border-box;", - p="2", - _hover=dict( - background_image=f"linear-gradient({bg_dark_color}, {bg_dark_color}), radial-gradient(circle at top left, {accent_color},{accennt_light});", - ), -) - -base_style = { - rx.chakra.Avatar: { - "shadow": shadow, - "color": text_light_color, - # "bg": border_color, - }, - rx.chakra.Button: { - "shadow": shadow, - "color": text_light_color, - "_hover": { - "bg": accent_dark, - }, - }, - rx.chakra.Menu: { - "bg": bg_dark_color, - "border": f"red", - }, - rx.chakra.MenuList: { - "bg": bg_dark_color, - "border": f"1.5px solid {bg_medium_color}", - }, - rx.chakra.MenuDivider: { - "border": f"1px solid {bg_medium_color}", - }, - rx.chakra.MenuItem: { - "bg": bg_dark_color, - "color": text_light_color, - }, - rx.chakra.DrawerContent: { - "bg": bg_dark_color, - "color": text_light_color, - "opacity": "0.9", - }, - rx.chakra.Hstack: { - "align_items": "center", - "justify_content": "space-between", - }, - rx.chakra.Vstack: { - "align_items": "stretch", - "justify_content": "space-between", - }, -} From 22ad950c5d97bdf0fbc0dcd58437cddcc0f39f89 Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Thu, 5 Sep 2024 19:22:22 +0200 Subject: [PATCH 09/22] gitignore .web (#3885) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bbaa8f0c9..0f7d9e5ff 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ assets/external/* dist/* examples/ +.web .idea .vscode .coverage From 593515784ceb05861a49e68c57d46be48512730d Mon Sep 17 00:00:00 2001 From: Vishnu Deva Date: Thu, 5 Sep 2024 19:23:00 +0200 Subject: [PATCH 10/22] update overflowY in AUTO_HEIGHT_JS from hidden to scroll (#3882) --- reflex/components/el/elements/forms.py | 2 +- reflex/components/el/elements/forms.pyi | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/reflex/components/el/elements/forms.py b/reflex/components/el/elements/forms.py index dfbdc4c1f..f6ad10162 100644 --- a/reflex/components/el/elements/forms.py +++ b/reflex/components/el/elements/forms.py @@ -501,7 +501,7 @@ AUTO_HEIGHT_JS = """ const autoHeightOnInput = (e, is_enabled) => { if (is_enabled) { const el = e.target; - el.style.overflowY = "hidden"; + el.style.overflowY = "scroll"; el.style.height = "auto"; el.style.height = (e.target.scrollHeight) + "px"; if (el.form && !el.form.data_resize_on_reset) { diff --git a/reflex/components/el/elements/forms.pyi b/reflex/components/el/elements/forms.pyi index 7b20571eb..7eca3f854 100644 --- a/reflex/components/el/elements/forms.pyi +++ b/reflex/components/el/elements/forms.pyi @@ -1559,7 +1559,7 @@ class Select(BaseHTML): """ ... -AUTO_HEIGHT_JS = '\nconst autoHeightOnInput = (e, is_enabled) => {\n if (is_enabled) {\n const el = e.target;\n el.style.overflowY = "hidden";\n el.style.height = "auto";\n el.style.height = (e.target.scrollHeight) + "px";\n if (el.form && !el.form.data_resize_on_reset) {\n el.form.addEventListener("reset", () => window.setTimeout(() => autoHeightOnInput(e, is_enabled), 0))\n el.form.data_resize_on_reset = true;\n }\n }\n}\n' +AUTO_HEIGHT_JS = '\nconst autoHeightOnInput = (e, is_enabled) => {\n if (is_enabled) {\n const el = e.target;\n el.style.overflowY = "scroll";\n el.style.height = "auto";\n el.style.height = (e.target.scrollHeight) + "px";\n if (el.form && !el.form.data_resize_on_reset) {\n el.form.addEventListener("reset", () => window.setTimeout(() => autoHeightOnInput(e, is_enabled), 0))\n el.form.data_resize_on_reset = true;\n }\n }\n}\n' ENTER_KEY_SUBMIT_JS = "\nconst enterKeySubmitOnKeyDown = (e, is_enabled) => {\n if (is_enabled && e.which === 13 && !e.shiftKey) {\n e.preventDefault();\n if (!e.repeat) {\n if (e.target.form) {\n e.target.form.requestSubmit();\n }\n }\n }\n}\n" class Textarea(BaseHTML): From 6308eb66655613ff1ded788acb525108429adb2e Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 5 Sep 2024 14:36:33 -0700 Subject: [PATCH 11/22] Retain mutability inside `async with self` block (#3884) When emitting a state update, restore `_self_mutable` to the value it had previously so that `yield` in the middle of `async with self` does not result in an immutable StateProxy. Fix #3869 --- integration/test_background_task.py | 39 +++++++++++++++++++++++++++++ reflex/state.py | 3 ++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/integration/test_background_task.py b/integration/test_background_task.py index 45866ba00..3764f67b4 100644 --- a/integration/test_background_task.py +++ b/integration/test_background_task.py @@ -87,6 +87,13 @@ def BackgroundTask(): third_state = await self.get_state(ThirdState) await third_state._triple_count() + @rx.background + async def yield_in_async_with_self(self): + async with self: + self.counter += 1 + yield + self.counter += 1 + class OtherState(rx.State): @rx.background async def get_other_state(self): @@ -155,6 +162,11 @@ def BackgroundTask(): on_click=OtherState.get_other_state, id="increment-from-other-state", ), + rx.button( + "Yield in Async with Self", + on_click=State.yield_in_async_with_self, + id="yield-in-async-with-self", + ), rx.button("Reset", on_click=State.reset_counter, id="reset"), ) @@ -334,3 +346,30 @@ def test_get_state( increment_button.click() assert background_task._poll_for(lambda: counter.text == "13", timeout=5) + + +def test_yield_in_async_with_self( + background_task: AppHarness, + driver: WebDriver, + token: str, +): + """Test that yielding inside async with self does not disable mutability. + + Args: + background_task: harness for BackgroundTask app. + driver: WebDriver instance. + token: The token for the connected client. + """ + assert background_task.app_instance is not None + + # get a reference to all buttons + yield_in_async_with_self_button = driver.find_element( + By.ID, "yield-in-async-with-self" + ) + + # get a reference to the counter + counter = driver.find_element(By.ID, "counter") + assert background_task._poll_for(lambda: counter.text == "0", timeout=5) + + yield_in_async_with_self_button.click() + assert background_task._poll_for(lambda: counter.text == "2", timeout=5) diff --git a/reflex/state.py b/reflex/state.py index 534672bd9..c94adb39d 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -2295,11 +2295,12 @@ class StateProxy(wrapt.ObjectProxy): Returns: The state update. """ + original_mutable = self._self_mutable self._self_mutable = True try: return self.__wrapped__._as_state_update(*args, **kwargs) finally: - self._self_mutable = False + self._self_mutable = original_mutable class StateUpdate(Base): From cbe532cfc587a0554d1842cb7a7166f5a6fcc55b Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 5 Sep 2024 14:36:53 -0700 Subject: [PATCH 12/22] Include child imports in markdown component_map (#3883) If a component in the markdown component_map contains children components, use `_get_all_imports` to recursively enumerate them. Fix #3880 --- reflex/components/markdown/markdown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/components/markdown/markdown.py b/reflex/components/markdown/markdown.py index 272b576a2..cd560d00b 100644 --- a/reflex/components/markdown/markdown.py +++ b/reflex/components/markdown/markdown.py @@ -170,7 +170,7 @@ class Markdown(Component): ), }, *[ - component(_MOCK_ARG)._get_imports() # type: ignore + component(_MOCK_ARG)._get_all_imports() # type: ignore for component in self.component_map.values() ], CodeBlock.create(theme="light")._get_imports(), # type: ignore, From 43d79d3a2428122bf0284ce7f23fb3773a048a9f Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 5 Sep 2024 14:37:07 -0700 Subject: [PATCH 13/22] [REF-3570] Remove deprecated REDIS_URL syntax (#3892) --- reflex/utils/prerequisites.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 902c5111c..e061207f4 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -325,24 +325,19 @@ def parse_redis_url() -> str | dict | None: """Parse the REDIS_URL in config if applicable. Returns: - If redis-py syntax, return the URL as it is. Otherwise, return the host/port/db as a dict. + If url is non-empty, return the URL as it is. + + Raises: + ValueError: If the REDIS_URL is not a supported scheme. """ config = get_config() if not config.redis_url: return None - if config.redis_url.startswith(("redis://", "rediss://", "unix://")): - return config.redis_url - console.deprecate( - feature_name="host[:port] style redis urls", - reason="redis-py url syntax is now being used", - deprecation_version="0.3.6", - removal_version="0.6.0", - ) - redis_url, has_port, redis_port = config.redis_url.partition(":") - if not has_port: - redis_port = 6379 - console.info(f"Using redis at {config.redis_url}") - return dict(host=redis_url, port=int(redis_port), db=0) + if not config.redis_url.startswith(("redis://", "rediss://", "unix://")): + raise ValueError( + "REDIS_URL must start with 'redis://', 'rediss://', or 'unix://'." + ) + return config.redis_url async def get_redis_status() -> bool | None: From 477e1dece9166fb13825c4ae416b030d8f70ccb9 Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Mon, 9 Sep 2024 03:26:52 +0200 Subject: [PATCH 14/22] mixin computed vars should only be applied to highest level state (#3833) --- reflex/state.py | 5 ++++- tests/test_state.py | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/reflex/state.py b/reflex/state.py index c94adb39d..01c1bb7c9 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -431,8 +431,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): return [ v for mixin in cls._mixins() + [cls] - for v in mixin.__dict__.values() + for name, v in mixin.__dict__.items() if isinstance(v, (ComputedVar, ImmutableComputedVar)) + and name not in cls.inherited_vars ] @classmethod @@ -560,6 +561,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): for mixin in cls._mixins(): for name, value in mixin.__dict__.items(): + if name in cls.inherited_vars: + continue if isinstance(value, (ComputedVar, ImmutableComputedVar)): fget = cls._copy_fn(value.fget) newcv = value._replace( diff --git a/tests/test_state.py b/tests/test_state.py index ba8fc592f..9efda3454 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -3161,6 +3161,15 @@ class MixinState(State, mixin=True): num: int = 0 _backend: int = 0 + @rx.var(cache=True) + def computed(self) -> str: + """A computed var on mixin state. + + Returns: + A computed value. + """ + return "" + class UsesMixinState(MixinState, State): """A state that uses the mixin state.""" @@ -3168,8 +3177,26 @@ class UsesMixinState(MixinState, State): pass +class ChildUsesMixinState(UsesMixinState): + """A child state that uses the mixin state.""" + + pass + + def test_mixin_state() -> None: """Test that a mixin state works correctly.""" assert "num" in UsesMixinState.base_vars assert "num" in UsesMixinState.vars assert UsesMixinState.backend_vars == {"_backend": 0} + + assert "computed" in UsesMixinState.computed_vars + assert "computed" in UsesMixinState.vars + + +def test_child_mixin_state() -> None: + """Test that mixin vars are only applied to the highest state in the hierarchy.""" + assert "num" in ChildUsesMixinState.inherited_vars + assert "num" not in ChildUsesMixinState.base_vars + + assert "computed" in ChildUsesMixinState.inherited_vars + assert "computed" not in ChildUsesMixinState.computed_vars From fd13e559c6eb18d08ece079fe544bff3f35b39d8 Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Mon, 9 Sep 2024 03:29:37 +0200 Subject: [PATCH 15/22] improve state hierarchy validation, drop old testing special case (#3894) --- reflex/app.py | 7 +++---- reflex/state.py | 20 +++++--------------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index a3094885d..9e5c2541a 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -269,13 +269,12 @@ class App(MiddlewareMixin, LifespanMixin, Base): "`connect_error_component` is deprecated, use `overlay_component` instead" ) super().__init__(**kwargs) - base_state_subclasses = BaseState.__subclasses__() # Special case to allow test cases have multiple subclasses of rx.BaseState. - if not is_testing_env() and len(base_state_subclasses) > 1: - # Only one Base State class is allowed. + if not is_testing_env() and BaseState.__subclasses__() != [State]: + # Only rx.State is allowed as Base State subclass. raise ValueError( - "rx.BaseState cannot be subclassed multiple times. use rx.State instead" + "rx.BaseState cannot be subclassed directly. Use rx.State instead" ) if "breakpoints" in self.style: diff --git a/reflex/state.py b/reflex/state.py index 01c1bb7c9..5dfdf3d8d 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -495,21 +495,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): if cls.get_name() in set( c.get_name() for c in parent_state.class_subclasses ): - if is_testing_env(): - # Clear existing subclass with same name when app is reloaded via - # utils.prerequisites.get_app(reload=True) - parent_state.class_subclasses = set( - c - for c in parent_state.class_subclasses - if c.get_name() != cls.get_name() - ) - else: - # During normal operation, subclasses cannot have the same name, even if they are - # defined in different modules. - raise StateValueError( - f"The substate class '{cls.get_name()}' has been defined multiple times. " - "Shadowing substate classes is not allowed." - ) + # This should not happen, since we have added module prefix to state names in #3214 + raise StateValueError( + f"The substate class '{cls.get_name()}' has been defined multiple times. " + "Shadowing substate classes is not allowed." + ) # Track this new subclass in the parent state's subclasses set. parent_state.class_subclasses.add(cls) From 99ffbc8c399dc1b03d927c048a7b12ae70bf3003 Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Mon, 9 Sep 2024 03:35:47 +0200 Subject: [PATCH 16/22] fix var dependency dicts (#3842) --- reflex/state.py | 23 ++++++++++---------- tests/test_app.py | 11 +--------- tests/test_state.py | 52 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 22 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index 5dfdf3d8d..abf585513 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -584,6 +584,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): cls.event_handlers[name] = handler setattr(cls, name, handler) + # Initialize per-class var dependency tracking. + cls._computed_var_dependencies = defaultdict(set) + cls._substate_var_dependencies = defaultdict(set) cls._init_var_dependency_dicts() @staticmethod @@ -653,10 +656,6 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): Additional updates tracking dicts for vars and substates that always need to be recomputed. """ - # Initialize per-class var dependency tracking. - cls._computed_var_dependencies = defaultdict(set) - cls._substate_var_dependencies = defaultdict(set) - inherited_vars = set(cls.inherited_vars).union( set(cls.inherited_backend_vars), ) @@ -1006,20 +1005,20 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): Args: args: a dict of args """ + if not args: + return def argsingle_factory(param): - @ComputedVar def inner_func(self) -> str: return self.router.page.params.get(param, "") - return inner_func + return ComputedVar(fget=inner_func, cache=True) def arglist_factory(param): - @ComputedVar def inner_func(self) -> List: return self.router.page.params.get(param, []) - return inner_func + return ComputedVar(fget=inner_func, cache=True) for param, value in args.items(): if value == constants.RouteArgType.SINGLE: @@ -1033,8 +1032,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): cls.vars[param] = cls.computed_vars[param] = func._var_set_state(cls) # type: ignore setattr(cls, param, func) - # Reinitialize dependency tracking dicts. - cls._init_var_dependency_dicts() + # Reinitialize dependency tracking dicts. + cls._init_var_dependency_dicts() def __getattribute__(self, name: str) -> Any: """Get the state var. @@ -3600,5 +3599,7 @@ def reload_state_module( if subclass.__module__ == module and module is not None: state.class_subclasses.remove(subclass) state._always_dirty_substates.discard(subclass.get_name()) - state._init_var_dependency_dicts() + state._computed_var_dependencies = defaultdict(set) + state._substate_var_dependencies = defaultdict(set) + state._init_var_dependency_dicts() state.get_class_substate.cache_clear() diff --git a/tests/test_app.py b/tests/test_app.py index f933149f1..ab7b50bdf 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -906,7 +906,7 @@ class DynamicState(BaseState): """Increment the counter var.""" self.counter = self.counter + 1 - @computed_var + @computed_var(cache=True) def comp_dynamic(self) -> str: """A computed var that depends on the dynamic var. @@ -1049,9 +1049,6 @@ async def test_dynamic_route_var_route_change_completed_on_load( assert on_load_update == StateUpdate( delta={ state.get_name(): { - # These computed vars _shouldn't_ be here, because they didn't change - arg_name: exp_val, - f"comp_{arg_name}": exp_val, "loaded": exp_index + 1, }, }, @@ -1073,9 +1070,6 @@ async def test_dynamic_route_var_route_change_completed_on_load( assert on_set_is_hydrated_update == StateUpdate( delta={ state.get_name(): { - # These computed vars _shouldn't_ be here, because they didn't change - arg_name: exp_val, - f"comp_{arg_name}": exp_val, "is_hydrated": True, }, }, @@ -1097,9 +1091,6 @@ async def test_dynamic_route_var_route_change_completed_on_load( assert update == StateUpdate( delta={ state.get_name(): { - # These computed vars _shouldn't_ be here, because they didn't change - f"comp_{arg_name}": exp_val, - arg_name: exp_val, "counter": exp_index + 1, } }, diff --git a/tests/test_state.py b/tests/test_state.py index 9efda3454..0344ea052 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -649,7 +649,12 @@ def test_set_dirty_var(test_state): assert test_state.dirty_vars == set() -def test_set_dirty_substate(test_state, child_state, child_state2, grandchild_state): +def test_set_dirty_substate( + test_state: TestState, + child_state: ChildState, + child_state2: ChildState2, + grandchild_state: GrandchildState, +): """Test changing substate vars marks the value as dirty. Args: @@ -3077,6 +3082,51 @@ def test_potentially_dirty_substates(): assert C1._potentially_dirty_substates() == set() +def test_router_var_dep() -> None: + """Test that router var dependencies are correctly tracked.""" + + class RouterVarParentState(State): + """A parent state for testing router var dependency.""" + + pass + + class RouterVarDepState(RouterVarParentState): + """A state with a router var dependency.""" + + @rx.var(cache=True) + def foo(self) -> str: + return self.router.page.params.get("foo", "") + + foo = RouterVarDepState.computed_vars["foo"] + State._init_var_dependency_dicts() + + assert foo._deps(objclass=RouterVarDepState) == {"router"} + assert RouterVarParentState._potentially_dirty_substates() == {RouterVarDepState} + assert RouterVarParentState._substate_var_dependencies == { + "router": {RouterVarDepState.get_name()} + } + assert RouterVarDepState._computed_var_dependencies == { + "router": {"foo"}, + } + + rx_state = State() + parent_state = RouterVarParentState() + state = RouterVarDepState() + + # link states + rx_state.substates = {RouterVarParentState.get_name(): parent_state} + parent_state.parent_state = rx_state + state.parent_state = parent_state + parent_state.substates = {RouterVarDepState.get_name(): state} + + assert state.dirty_vars == set() + + # Reassign router var + state.router = state.router + assert state.dirty_vars == {"foo", "router"} + assert parent_state.dirty_substates == {RouterVarDepState.get_name()} + + @pytest.mark.asyncio async def test_setvar(mock_app: rx.App, token: str): """Test that setvar works correctly. From 21585867b992012d6e9604a432319cc50c694836 Mon Sep 17 00:00:00 2001 From: abulvenz Date: Mon, 9 Sep 2024 03:36:47 +0200 Subject: [PATCH 17/22] Adding array to array pluck operation. (#3868) --- integration/test_var_operations.py | 6 ++++++ reflex/ivars/sequence.py | 31 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/integration/test_var_operations.py b/integration/test_var_operations.py index 7a8f5473a..bd9db2d8a 100644 --- a/integration/test_var_operations.py +++ b/integration/test_var_operations.py @@ -20,6 +20,9 @@ def VarOperations(): from reflex.ivars.base import LiteralVar from reflex.ivars.sequence import ArrayVar + class Object(rx.Base): + str: str = "hello" + class VarOperationState(rx.State): int_var1: int = 10 int_var2: int = 5 @@ -29,6 +32,7 @@ def VarOperations(): list1: List = [1, 2] list2: List = [3, 4] list3: List = ["first", "second", "third"] + list4: List = [Object(name="obj_1"), Object(name="obj_2")] str_var1: str = "first" str_var2: str = "second" str_var3: str = "ThIrD" @@ -474,6 +478,7 @@ def VarOperations(): rx.text( VarOperationState.list1.contains(1).to_string(), id="list_contains" ), + rx.text(VarOperationState.list4.pluck("name").to_string(), id="list_pluck"), rx.text(VarOperationState.list1.reverse().to_string(), id="list_reverse"), # LIST, INT rx.text( @@ -749,6 +754,7 @@ def test_var_operations(driver, var_operations: AppHarness): ("list_and_list", "[3,4]"), ("list_or_list", "[1,2]"), ("list_contains", "true"), + ("list_pluck", '["obj_1","obj_2"]'), ("list_reverse", "[2,1]"), ("list_join", "firstsecondthird"), ("list_join_comma", "first,second,third"), diff --git a/reflex/ivars/sequence.py b/reflex/ivars/sequence.py index 25586e4df..8081cf442 100644 --- a/reflex/ivars/sequence.py +++ b/reflex/ivars/sequence.py @@ -707,6 +707,17 @@ class ArrayVar(ImmutableVar[ARRAY_VAR_TYPE]): """ return array_contains_operation(self, other) + def pluck(self, field: StringVar | str) -> ArrayVar: + """Pluck a field from the array. + + Args: + field: The field to pluck from the array. + + Returns: + The array pluck operation. + """ + return array_pluck_operation(self, field) + def __mul__(self, other: NumberVar | int) -> ArrayVar[ARRAY_VAR_TYPE]: """Multiply the sequence by a number or integer. @@ -915,6 +926,26 @@ class ArraySliceOperation(CachedVarOperation, ArrayVar): ) +@var_operation +def array_pluck_operation( + array: ArrayVar[ARRAY_VAR_TYPE], + field: StringVar | str, +) -> CustomVarOperationReturn[ARRAY_VAR_TYPE]: + """Pluck a field from an array of objects. + + Args: + array: The array to pluck from. + field: The field to pluck from the objects in the array. + + Returns: + The reversed array. + """ + return var_operation_return( + js_expression=f"{array}.map(e=>e?.[{field}])", + var_type=array._var_type, + ) + + @var_operation def array_reverse_operation( array: ArrayVar[ARRAY_VAR_TYPE], From b7c7197f1dd4f6f258127ae9f357be8780d21cb6 Mon Sep 17 00:00:00 2001 From: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> Date: Mon, 9 Sep 2024 04:10:46 +0200 Subject: [PATCH 18/22] fix initial state without cv fallback (#3670) --- reflex/compiler/utils.py | 4 +++- reflex/state.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index 1b69539ac..cbb402047 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -152,7 +152,9 @@ def compile_state(state: Type[BaseState]) -> dict: console.warn( f"Failed to compile initial state with computed vars, excluding them: {e}" ) - initial_state = state(_reflex_internal_init=True).dict(include_computed=False) + initial_state = state(_reflex_internal_init=True).dict( + initial=True, include_computed=False + ) return format.format_state(initial_state) diff --git a/reflex/state.py b/reflex/state.py index abf585513..cb18825c8 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1784,7 +1784,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): prop_name: self.get_value(getattr(self, prop_name)) for prop_name in self.base_vars } - if initial: + if initial and include_computed: computed_vars = { # Include initial computed vars. prop_name: ( From c70cba1e7c56d817270f6987b50650427e2749c4 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Sun, 8 Sep 2024 19:14:56 -0700 Subject: [PATCH 19/22] add fragment to foreach (#3877) --- integration/test_var_operations.py | 12 ++++++++++++ reflex/.templates/jinja/web/pages/utils.js.jinja2 | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/integration/test_var_operations.py b/integration/test_var_operations.py index bd9db2d8a..2feb80ae0 100644 --- a/integration/test_var_operations.py +++ b/integration/test_var_operations.py @@ -593,6 +593,16 @@ def VarOperations(): int_var2=VarOperationState.int_var2, id="memo_comp_nested", ), + # foreach in a match + rx.box( + rx.match( + VarOperationState.list3.length(), + (0, rx.text("No choices")), + (1, rx.text("One choice")), + rx.foreach(VarOperationState.list3, lambda choice: rx.text(choice)), + ), + id="foreach_in_match", + ), ) @@ -790,6 +800,8 @@ def test_var_operations(driver, var_operations: AppHarness): # rx.memo component with state ("memo_comp", "1210"), ("memo_comp_nested", "345"), + # foreach in a match + ("foreach_in_match", "first\nsecond\nthird"), ] for tag, expected in tests: diff --git a/reflex/.templates/jinja/web/pages/utils.js.jinja2 b/reflex/.templates/jinja/web/pages/utils.js.jinja2 index 8e9808fbb..4e3546070 100644 --- a/reflex/.templates/jinja/web/pages/utils.js.jinja2 +++ b/reflex/.templates/jinja/web/pages/utils.js.jinja2 @@ -64,11 +64,11 @@ {# Args: #} {# component: component dictionary #} {% macro render_iterable_tag(component) %} -{ {%- if component.iterable_type == 'dict' -%}Object.entries({{- component.iterable_state }}){%- else -%}{{- component.iterable_state }}{%- endif -%}.map(({{ component.arg_name }}, {{ component.arg_index }}) => ( +<>{ {%- if component.iterable_type == 'dict' -%}Object.entries({{- component.iterable_state }}){%- else -%}{{- component.iterable_state }}{%- endif -%}.map(({{ component.arg_name }}, {{ component.arg_index }}) => ( {% for child in component.children %} {{ render(child) }} {% endfor %} -))} +))} {%- endmacro %} From fa894289d4f7c4d66f888b7bd5944555ca61ca87 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Sun, 8 Sep 2024 19:21:05 -0700 Subject: [PATCH 20/22] Update docker-example (#3324) --- docker-example/README.md | 143 +++--------------- .../production-app-platform/.dockerignore | 5 + .../production-app-platform/Dockerfile | 65 ++++++++ .../production-app-platform/README.md | 113 ++++++++++++++ .../{ => production-compose}/.dockerignore | 0 .../{ => production-compose}/Caddy.Dockerfile | 0 .../{ => production-compose}/Caddyfile | 0 .../Dockerfile} | 5 +- docker-example/production-compose/README.md | 75 +++++++++ .../compose.prod.yaml | 0 .../compose.tools.yaml | 0 .../{ => production-compose}/compose.yaml | 1 - docker-example/requirements.txt | 1 - docker-example/simple-one-port/.dockerignore | 5 + docker-example/simple-one-port/Caddyfile | 14 ++ .../Dockerfile} | 28 +--- docker-example/simple-one-port/README.md | 36 +++++ docker-example/simple-two-port/.dockerignore | 5 + .../{ => simple-two-port}/Dockerfile | 9 +- docker-example/simple-two-port/README.md | 44 ++++++ 20 files changed, 398 insertions(+), 151 deletions(-) create mode 100644 docker-example/production-app-platform/.dockerignore create mode 100644 docker-example/production-app-platform/Dockerfile create mode 100644 docker-example/production-app-platform/README.md rename docker-example/{ => production-compose}/.dockerignore (100%) rename docker-example/{ => production-compose}/Caddy.Dockerfile (100%) rename docker-example/{ => production-compose}/Caddyfile (100%) rename docker-example/{prod.Dockerfile => production-compose/Dockerfile} (91%) create mode 100644 docker-example/production-compose/README.md rename docker-example/{ => production-compose}/compose.prod.yaml (100%) rename docker-example/{ => production-compose}/compose.tools.yaml (100%) rename docker-example/{ => production-compose}/compose.yaml (96%) delete mode 100644 docker-example/requirements.txt create mode 100644 docker-example/simple-one-port/.dockerignore create mode 100644 docker-example/simple-one-port/Caddyfile rename docker-example/{app.Dockerfile => simple-one-port/Dockerfile} (67%) create mode 100644 docker-example/simple-one-port/README.md create mode 100644 docker-example/simple-two-port/.dockerignore rename docker-example/{ => simple-two-port}/Dockerfile (67%) create mode 100644 docker-example/simple-two-port/README.md diff --git a/docker-example/README.md b/docker-example/README.md index 8ac32421d..552dba950 100644 --- a/docker-example/README.md +++ b/docker-example/README.md @@ -1,133 +1,30 @@ -# Reflex Docker Container +# Reflex Docker Examples -This example describes how to create and use a container image for Reflex with your own code. +This directory contains several examples of how to deploy Reflex apps using docker. -## Update Requirements +In all cases, ensure that your `requirements.txt` file is up to date and +includes the `reflex` package. -The `requirements.txt` includes the reflex package which is needed to install -Reflex framework. If you use additional packages in your project you have to add -this in the `requirements.txt` first. Copy the `Dockerfile`, `.dockerignore` and -the `requirements.txt` file in your project folder. +## `simple-two-port` -## Build Simple Reflex Container Image +The most basic production deployment exposes two HTTP ports and relies on an +existing load balancer to forward the traffic appropriately. -The main `Dockerfile` is intended to build a very simple, single container deployment that runs -the Reflex frontend and backend together, exposing ports 3000 and 8000. +## `simple-one-port` -To build your container image run the following command: +This deployment exports the frontend statically and serves it via a single HTTP +port using Caddy. This is useful for platforms that only support a single port +or where running a node server in the container is undesirable. -```bash -docker build -t reflex-app:latest . -``` +## `production-compose` -## Start Container Service +This deployment is intended for use with a standalone VPS that is only hosting a +single Reflex app. It provides the entire stack in a single `compose.yaml` +including a webserver, one or more backend instances, redis, and a postgres +database. -Finally, you can start your Reflex container service as follows: +## `production-app-platform` -```bash -docker run -it --rm -p 3000:3000 -p 8000:8000 --name app reflex-app:latest -``` - -It may take a few seconds for the service to become available. - -Access your app at http://localhost:3000. - -Note that this container has _no persistence_ and will lose all data when -stopped. You can use bind mounts or named volumes to persist the database and -uploaded_files directories as needed. - -# Production Service with Docker Compose and Caddy - -An example production deployment uses automatic TLS with Caddy serving static files -for the frontend and proxying requests to both the frontend and backend. - -Copy the following files to your project directory: - * `compose.yaml` - * `compose.prod.yaml` - * `compose.tools.yaml` - * `prod.Dockerfile` - * `Caddy.Dockerfile` - * `Caddyfile` - -The production app container, based on `prod.Dockerfile`, builds and exports the -frontend statically (to be served by Caddy). The resulting image only runs the -backend service. - -The `webserver` service, based on `Caddy.Dockerfile`, copies the static frontend -and `Caddyfile` into the container to configure the reverse proxy routes that will -forward requests to the backend service. Caddy will automatically provision TLS -for localhost or the domain specified in the environment variable `DOMAIN`. - -This type of deployment should use less memory and be more performant since -nodejs is not required at runtime. - -## Customize `Caddyfile` (optional) - -If the app uses additional backend API routes, those should be added to the -`@backend_routes` path matcher to ensure they are forwarded to the backend. - -## Build Reflex Production Service - -During build, set `DOMAIN` environment variable to the domain where the app will -be hosted! (Do not include http or https, it will always use https). - -**If `DOMAIN` is not provided, the service will default to `localhost`.** - -```bash -DOMAIN=example.com docker compose build -``` - -This will build both the `app` service from the `prod.Dockerfile` and the `webserver` -service via `Caddy.Dockerfile`. - -## Run Reflex Production Service - -```bash -DOMAIN=example.com docker compose up -``` - -The app should be available at the specified domain via HTTPS. Certificate -provisioning will occur automatically and may take a few minutes. - -### Data Persistence - -Named docker volumes are used to persist the app database (`db-data`), -uploaded_files (`upload-data`), and caddy TLS keys and certificates -(`caddy-data`). - -## More Robust Deployment - -For a more robust deployment, consider bringing the service up with -`compose.prod.yaml` which includes postgres database and redis cache, allowing -the backend to run with multiple workers and service more requests. - -```bash -DOMAIN=example.com docker compose -f compose.yaml -f compose.prod.yaml up -d -``` - -Postgres uses its own named docker volume for data persistence. - -## Admin Tools - -When needed, the services in `compose.tools.yaml` can be brought up, providing -graphical database administration (Adminer on http://localhost:8080) and a -redis cache browser (redis-commander on http://localhost:8081). It is not recommended -to deploy these services if they are not in active use. - -```bash -DOMAIN=example.com docker compose -f compose.yaml -f compose.prod.yaml -f compose.tools.yaml up -d -``` - -# Container Hosting - -Most container hosting services automatically terminate TLS and expect the app -to be listening on a single port (typically `$PORT`). - -To host a Reflex app on one of these platforms, like Google Cloud Run, Render, -Railway, etc, use `app.Dockerfile` to build a single image containing a reverse -proxy that will serve that frontend as static files and proxy requests to the -backend for specific endpoints. - -If the chosen platform does not support buildx and thus heredoc, you can copy -the Caddyfile configuration into a separate Caddyfile in the root of the -project. +This example deployment is intended for use with App hosting platforms, like +Azure, AWS, or Google Cloud Run. It is the backend of the deployment, which +depends on a separately hosted redis instance and static frontend deployment. \ No newline at end of file diff --git a/docker-example/production-app-platform/.dockerignore b/docker-example/production-app-platform/.dockerignore new file mode 100644 index 000000000..ea66a51f5 --- /dev/null +++ b/docker-example/production-app-platform/.dockerignore @@ -0,0 +1,5 @@ +.web +.git +__pycache__/* +Dockerfile +uploaded_files diff --git a/docker-example/production-app-platform/Dockerfile b/docker-example/production-app-platform/Dockerfile new file mode 100644 index 000000000..b0f6c69fc --- /dev/null +++ b/docker-example/production-app-platform/Dockerfile @@ -0,0 +1,65 @@ +# This docker file is intended to be used with container hosting services +# +# After deploying this image, get the URL pointing to the backend service +# and run API_URL=https://path-to-my-container.example.com reflex export frontend +# then copy the contents of `frontend.zip` to your static file server (github pages, s3, etc). +# +# Azure Static Web App example: +# npx @azure/static-web-apps-cli deploy --env production --app-location .web/_static +# +# For dynamic routes to function properly, ensure that 404s are redirected to /404 on the +# static file host (for github pages, this works out of the box; remember to create .nojekyll). +# +# For azure static web apps, add `staticwebapp.config.json` to to `.web/_static` with the following: +# { +# "responseOverrides": { +# "404": { +# "rewrite": "/404.html" +# } +# } +# } +# +# Note: many container hosting platforms require amd64 images, so when building on an M1 Mac +# for example, pass `docker build --platform=linux/amd64 ...` + +# Stage 1: init +FROM python:3.11 as init + +ARG uv=/root/.cargo/bin/uv + +# Install `uv` for faster package boostrapping +ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh +RUN /install.sh && rm /install.sh + +# Copy local context to `/app` inside container (see .dockerignore) +WORKDIR /app +COPY . . +RUN mkdir -p /app/data /app/uploaded_files + +# Create virtualenv which will be copied into final container +ENV VIRTUAL_ENV=/app/.venv +ENV PATH="$VIRTUAL_ENV/bin:$PATH" +RUN $uv venv + +# Install app requirements and reflex inside virtualenv +RUN $uv pip install -r requirements.txt + +# Deploy templates and prepare app +RUN reflex init + +# Stage 2: copy artifacts into slim image +FROM python:3.11-slim +WORKDIR /app +RUN adduser --disabled-password --home /app reflex +COPY --chown=reflex --from=init /app /app +# Install libpq-dev for psycopg2 (skip if not using postgres). +RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/* +USER reflex +ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1 + +# Needed until Reflex properly passes SIGTERM on backend. +STOPSIGNAL SIGKILL + +# Always apply migrations before starting the backend. +CMD [ -d alembic ] && reflex db migrate; \ + exec reflex run --env prod --backend-only --backend-port ${PORT:-8000} diff --git a/docker-example/production-app-platform/README.md b/docker-example/production-app-platform/README.md new file mode 100644 index 000000000..ad54f814f --- /dev/null +++ b/docker-example/production-app-platform/README.md @@ -0,0 +1,113 @@ +# production-app-platform + +This example deployment is intended for use with App hosting platforms, like +Azure, AWS, or Google Cloud Run. + +## Architecture + +The production deployment consists of a few pieces: + * Backend container - built by `Dockerfile` Runs the Reflex backend + service on port 8000 and is scalable to multiple instances. + * Redis container - A single instance the standard `redis` docker image should + share private networking with the backend + * Static frontend - HTML/CSS/JS files that are hosted via a CDN or static file + server. This is not included in the docker image. + +## Deployment + +These general steps do not cover the specifics of each platform, but all platforms should +support the concepts described here. + +### Vnet + +All containers in the deployment should be hooked up to the same virtual private +network so they can access the redis service and optionally the database server. +The vnet should not be exposed to the internet, use an ingress rule to terminate +TLS at the load balancer and forward the traffic to a backend service replica. + +### Redis + +Deploy a `redis` instance on the vnet. + +### Backend + +The backend is built by the `Dockerfile` in this directory. When deploying the +backend, be sure to set REDIS_URL=redis://internal-redis-hostname to connect to +the redis service. + +### Ingress + +Configure the load balancer for the app to forward traffic to port 8000 on the +backend service replicas. Most platforms will generate an ingress hostname +automatically. Make sure when you access the ingress endpoint on `/ping` that it +returns "pong", indicating that the backend is up an available. + +### Frontend + +The frontend should be hosted on a static file server or CDN. + +**Important**: when exporting the frontend, set the API_URL environment variable +to the ingress hostname of the backend service. + +If you will host the frontend from a path other than the root, set the +`FRONTEND_PATH` environment variable appropriately when exporting the frontend. + +Most static hosts will automatically use the `/404.html` file to handle 404 +errors. _This is essential for dynamic routes to work correctly._ Ensure that +missing routes return the `/404.html` content to the user if this is not the +default behavior. + +_For Github Pages_: ensure the file `.nojekyll` is present in the root of the repo +to avoid special processing of underscore-prefix directories, like `_next`. + +## Platform Notes + +The following sections are currently a work in progress and may be incomplete. + +### Azure + +In the Azure load balancer, per-message deflate is not supported. Add the following +to your `rxconfig.py` to workaround this issue. + +```python +import uvicorn.workers + +import reflex as rx + + +class NoWSPerMessageDeflate(uvicorn.workers.UvicornH11Worker): + CONFIG_KWARGS = { + **uvicorn.workers.UvicornH11Worker.CONFIG_KWARGS, + "ws_per_message_deflate": False, + } + + +config = rx.Config( + app_name="my_app", + gunicorn_worker_class="rxconfig.NoWSPerMessageDeflate", +) +``` + +#### Persistent Storage + +If you need to use a database or upload files, you cannot save them to the +container volume. Use Azure Files and mount it into the container at /app/uploaded_files. + +#### Resource Types + +* Create a new vnet with 10.0.0.0/16 + * Create a new subnet for redis, database, and containers +* Deploy redis as a Container Instances +* Deploy database server as "Azure Database for PostgreSQL" + * Create a new database for the app + * Set db-url as a secret containing the db user/password connection string +* Deploy Storage account for uploaded files + * Enable access from the vnet and container subnet + * Create a new file share + * In the environment, create a new files share (get the storage key) +* Deploy the backend as a Container App + * Create a custom Container App Environment linked up to the same vnet as the redis container. + * Set REDIS_URL and DB_URL environment variables + * Add the volume from the environment + * Add the volume mount to the container +* Deploy the frontend as a Static Web App \ No newline at end of file diff --git a/docker-example/.dockerignore b/docker-example/production-compose/.dockerignore similarity index 100% rename from docker-example/.dockerignore rename to docker-example/production-compose/.dockerignore diff --git a/docker-example/Caddy.Dockerfile b/docker-example/production-compose/Caddy.Dockerfile similarity index 100% rename from docker-example/Caddy.Dockerfile rename to docker-example/production-compose/Caddy.Dockerfile diff --git a/docker-example/Caddyfile b/docker-example/production-compose/Caddyfile similarity index 100% rename from docker-example/Caddyfile rename to docker-example/production-compose/Caddyfile diff --git a/docker-example/prod.Dockerfile b/docker-example/production-compose/Dockerfile similarity index 91% rename from docker-example/prod.Dockerfile rename to docker-example/production-compose/Dockerfile index 2cde12ca0..f73473df7 100644 --- a/docker-example/prod.Dockerfile +++ b/docker-example/production-compose/Dockerfile @@ -42,10 +42,11 @@ COPY --chown=reflex --from=init /app /app # Install libpq-dev for psycopg2 (skip if not using postgres). RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/* USER reflex -ENV PATH="/app/.venv/bin:$PATH" +ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1 # Needed until Reflex properly passes SIGTERM on backend. STOPSIGNAL SIGKILL # Always apply migrations before starting the backend. -CMD reflex db migrate && reflex run --env prod --backend-only +CMD [ -d alembic ] && reflex db migrate; \ + exec reflex run --env prod --backend-only diff --git a/docker-example/production-compose/README.md b/docker-example/production-compose/README.md new file mode 100644 index 000000000..87222327e --- /dev/null +++ b/docker-example/production-compose/README.md @@ -0,0 +1,75 @@ +# production-compose + +This example production deployment uses automatic TLS with Caddy serving static +files for the frontend and proxying requests to both the frontend and backend. +It is intended for use with a standalone VPS that is only hosting a single +Reflex app. + +The production app container (`Dockerfile`), builds and exports the frontend +statically (to be served by Caddy). The resulting image only runs the backend +service. + +The `webserver` service, based on `Caddy.Dockerfile`, copies the static frontend +and `Caddyfile` into the container to configure the reverse proxy routes that will +forward requests to the backend service. Caddy will automatically provision TLS +for localhost or the domain specified in the environment variable `DOMAIN`. + +This type of deployment should use less memory and be more performant since +nodejs is not required at runtime. + +## Customize `Caddyfile` (optional) + +If the app uses additional backend API routes, those should be added to the +`@backend_routes` path matcher to ensure they are forwarded to the backend. + +## Build Reflex Production Service + +During build, set `DOMAIN` environment variable to the domain where the app will +be hosted! (Do not include http or https, it will always use https). + +**If `DOMAIN` is not provided, the service will default to `localhost`.** + +```bash +DOMAIN=example.com docker compose build +``` + +This will build both the `app` service from the `prod.Dockerfile` and the `webserver` +service via `Caddy.Dockerfile`. + +## Run Reflex Production Service + +```bash +DOMAIN=example.com docker compose up +``` + +The app should be available at the specified domain via HTTPS. Certificate +provisioning will occur automatically and may take a few minutes. + +### Data Persistence + +Named docker volumes are used to persist the app database (`db-data`), +uploaded_files (`upload-data`), and caddy TLS keys and certificates +(`caddy-data`). + +## More Robust Deployment + +For a more robust deployment, consider bringing the service up with +`compose.prod.yaml` which includes postgres database and redis cache, allowing +the backend to run with multiple workers and service more requests. + +```bash +DOMAIN=example.com docker compose -f compose.yaml -f compose.prod.yaml up -d +``` + +Postgres uses its own named docker volume for data persistence. + +## Admin Tools + +When needed, the services in `compose.tools.yaml` can be brought up, providing +graphical database administration (Adminer on http://localhost:8080) and a +redis cache browser (redis-commander on http://localhost:8081). It is not recommended +to deploy these services if they are not in active use. + +```bash +DOMAIN=example.com docker compose -f compose.yaml -f compose.prod.yaml -f compose.tools.yaml up -d +``` \ No newline at end of file diff --git a/docker-example/compose.prod.yaml b/docker-example/production-compose/compose.prod.yaml similarity index 100% rename from docker-example/compose.prod.yaml rename to docker-example/production-compose/compose.prod.yaml diff --git a/docker-example/compose.tools.yaml b/docker-example/production-compose/compose.tools.yaml similarity index 100% rename from docker-example/compose.tools.yaml rename to docker-example/production-compose/compose.tools.yaml diff --git a/docker-example/compose.yaml b/docker-example/production-compose/compose.yaml similarity index 96% rename from docker-example/compose.yaml rename to docker-example/production-compose/compose.yaml index 8c2f97ba6..de79c0778 100644 --- a/docker-example/compose.yaml +++ b/docker-example/production-compose/compose.yaml @@ -12,7 +12,6 @@ services: DB_URL: sqlite:///data/reflex.db build: context: . - dockerfile: prod.Dockerfile volumes: - db-data:/app/data - upload-data:/app/uploaded_files diff --git a/docker-example/requirements.txt b/docker-example/requirements.txt deleted file mode 100644 index fc29504eb..000000000 --- a/docker-example/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -reflex diff --git a/docker-example/simple-one-port/.dockerignore b/docker-example/simple-one-port/.dockerignore new file mode 100644 index 000000000..ea66a51f5 --- /dev/null +++ b/docker-example/simple-one-port/.dockerignore @@ -0,0 +1,5 @@ +.web +.git +__pycache__/* +Dockerfile +uploaded_files diff --git a/docker-example/simple-one-port/Caddyfile b/docker-example/simple-one-port/Caddyfile new file mode 100644 index 000000000..13d94ce8e --- /dev/null +++ b/docker-example/simple-one-port/Caddyfile @@ -0,0 +1,14 @@ +:{$PORT} + +encode gzip + +@backend_routes path /_event/* /ping /_upload /_upload/* +handle @backend_routes { + reverse_proxy localhost:8000 +} + +root * /srv +route { + try_files {path} {path}/ /404.html + file_server +} \ No newline at end of file diff --git a/docker-example/app.Dockerfile b/docker-example/simple-one-port/Dockerfile similarity index 67% rename from docker-example/app.Dockerfile rename to docker-example/simple-one-port/Dockerfile index 4aae2f31d..4cd7e9a18 100644 --- a/docker-example/app.Dockerfile +++ b/docker-example/simple-one-port/Dockerfile @@ -10,31 +10,13 @@ FROM python:3.11 ARG PORT=8080 # Only set for local/direct access. When TLS is used, the API_URL is assumed to be the same as the frontend. ARG API_URL -ENV PORT=$PORT API_URL=${API_URL:-http://localhost:$PORT} +ENV PORT=$PORT API_URL=${API_URL:-http://localhost:$PORT} REDIS_URL=redis://localhost PYTHONUNBUFFERED=1 -# Install Caddy server inside image -RUN apt-get update -y && apt-get install -y caddy && rm -rf /var/lib/apt/lists/* +# Install Caddy and redis server inside image +RUN apt-get update -y && apt-get install -y caddy redis-server && rm -rf /var/lib/apt/lists/* WORKDIR /app -# Create a simple Caddyfile to serve as reverse proxy -RUN cat > Caddyfile < Date: Tue, 10 Sep 2024 00:50:40 +0200 Subject: [PATCH 21/22] simplify ImmutableComputedVar.__get__ (#3902) * simplify ImmutableComputedVar.__get__ * ruff it --- reflex/ivars/base.py | 43 +++++-------------------------------------- 1 file changed, 5 insertions(+), 38 deletions(-) diff --git a/reflex/ivars/base.py b/reflex/ivars/base.py index 4cd3550dd..50453544d 100644 --- a/reflex/ivars/base.py +++ b/reflex/ivars/base.py @@ -22,7 +22,6 @@ from typing import ( Literal, NoReturn, Optional, - Sequence, Set, Tuple, Type, @@ -1507,47 +1506,15 @@ class ImmutableComputedVar(ImmutableVar[RETURN_TYPE]): The value of the var for the given instance. """ if instance is None: - from reflex.state import BaseState - - path_to_function = self.fget.__qualname__.split(".") - class_name_where_defined = ( - path_to_function[-2] if len(path_to_function) > 1 else owner.__name__ - ) - - def contains_class_name(states: Sequence[Type]) -> bool: - return any(c.__name__ == class_name_where_defined for c in states) - - def is_not_mixin(state: Type[BaseState]) -> bool: - return not state._mixin - - def length_of_state(state: Type[BaseState]) -> int: - return len(inspect.getmro(state)) - - class_where_defined = cast( - Type[BaseState], - min( - filter( - lambda state: state.__module__ == self.fget.__module__, - filter( - is_not_mixin, - filter( - lambda state: contains_class_name( - inspect.getmro(state) - ), - inspect.getmro(owner), - ), - ), - ), - default=owner, - key=length_of_state, - ), - ) + state_where_defined = owner + while self.fget.__name__ in state_where_defined.inherited_vars: + state_where_defined = state_where_defined.get_parent_state() return self._replace( - _var_name=format_state_name(class_where_defined.get_full_name()) + _var_name=format_state_name(state_where_defined.get_full_name()) + "." + self._var_name, - merge_var_data=ImmutableVarData.from_state(class_where_defined), + merge_var_data=ImmutableVarData.from_state(state_where_defined), ).guess_type() if not self._cache: From fb721e1d1263283667293bde29661974b56a8d67 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Mon, 9 Sep 2024 17:59:40 -0700 Subject: [PATCH 22/22] delete states if it exists on run (#3901) * delete states if it exists * simplify ImmutableComputedVar.__get__ (#3902) * simplify ImmutableComputedVar.__get__ * ruff it --------- Co-authored-by: benedikt-bartscher <31854409+benedikt-bartscher@users.noreply.github.com> --- reflex/reflex.py | 4 ++++ reflex/state.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/reflex/reflex.py b/reflex/reflex.py index 741873f4e..c90e328e4 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -16,6 +16,7 @@ from reflex_cli.utils import dependency from reflex import constants from reflex.config import get_config from reflex.custom_components.custom_components import custom_components_cli +from reflex.state import reset_disk_state_manager from reflex.utils import console, redir, telemetry # Disable typer+rich integration for help panels @@ -180,6 +181,9 @@ def _run( if prerequisites.needs_reinit(frontend=frontend): _init(name=config.app_name, loglevel=loglevel) + # Delete the states folder if it exists. + reset_disk_state_manager() + # Find the next available open port if applicable. if frontend: frontend_port = processes.handle_port( diff --git a/reflex/state.py b/reflex/state.py index cb18825c8..1bd4cc946 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -2499,6 +2499,14 @@ def state_to_schema( ) +def reset_disk_state_manager(): + """Reset the disk state manager.""" + states_directory = prerequisites.get_web_dir() / constants.Dirs.STATES + if states_directory.exists(): + for path in states_directory.iterdir(): + path.unlink() + + class StateManagerDisk(StateManager): """A state manager that stores states in memory."""