Merge remote-tracking branch 'upstream/main' into state-compression

This commit is contained in:
Benedikt Bartscher 2025-01-16 23:45:47 +01:00
commit 873b3a1306
No known key found for this signature in database
22 changed files with 228 additions and 139 deletions

View File

@ -410,7 +410,14 @@ export const connect = async (
autoUnref: false, autoUnref: false,
}); });
// Ensure undefined fields in events are sent as null instead of removed // Ensure undefined fields in events are sent as null instead of removed
socket.current.io.encoder.replacer = (k, v) => (v === undefined ? null : v) socket.current.io.encoder.replacer = (k, v) => (v === undefined ? null : v);
socket.current.io.decoder.tryParse = (str) => {
try {
return JSON5.parse(str);
} catch (e) {
return false;
}
};
function checkVisibility() { function checkVisibility() {
if (document.visibilityState === "visible") { if (document.visibilityState === "visible") {

View File

@ -136,6 +136,23 @@ def load_dynamic_serializer():
module_code_lines.insert(0, "const React = window.__reflex.react;") module_code_lines.insert(0, "const React = window.__reflex.react;")
function_line = next(
index
for index, line in enumerate(module_code_lines)
if line.startswith("export default function")
)
module_code_lines = [
line
for _, line in sorted(
enumerate(module_code_lines),
key=lambda x: (
not (x[1].startswith("import ") and x[0] < function_line),
x[0],
),
)
]
return "\n".join( return "\n".join(
[ [
"//__reflex_evaluate", "//__reflex_evaluate",

View File

@ -151,8 +151,8 @@ class ColorModeIconButton(IconButton):
dropdown_menu.trigger( dropdown_menu.trigger(
super().create( super().create(
ColorModeIcon.create(), ColorModeIcon.create(),
**props, ),
) **props,
), ),
dropdown_menu.content( dropdown_menu.content(
color_mode_item("light"), color_mode_item("light"),

View File

@ -567,6 +567,9 @@ class EnvironmentVariables:
# The maximum size of the reflex state in kilobytes. # The maximum size of the reflex state in kilobytes.
REFLEX_STATE_SIZE_LIMIT: EnvVar[int] = env_var(1000) REFLEX_STATE_SIZE_LIMIT: EnvVar[int] = env_var(1000)
# Whether to use the turbopack bundler.
REFLEX_USE_TURBOPACK: EnvVar[bool] = env_var(True)
# Whether to compress the reflex state. # Whether to compress the reflex state.
REFLEX_COMPRESS_STATE: EnvVar[bool] = env_var(False) REFLEX_COMPRESS_STATE: EnvVar[bool] = env_var(False)

View File

@ -182,7 +182,7 @@ class PackageJson(SimpleNamespace):
"@emotion/react": "11.13.3", "@emotion/react": "11.13.3",
"axios": "1.7.7", "axios": "1.7.7",
"json5": "2.2.3", "json5": "2.2.3",
"next": "14.2.16", "next": "15.1.4",
"next-sitemap": "4.2.3", "next-sitemap": "4.2.3",
"next-themes": "0.4.3", "next-themes": "0.4.3",
"react": "18.3.1", "react": "18.3.1",

View File

@ -519,7 +519,9 @@ def deploy(
if prerequisites.needs_reinit(frontend=True): if prerequisites.needs_reinit(frontend=True):
_init(name=config.app_name, loglevel=loglevel) _init(name=config.app_name, loglevel=loglevel)
prerequisites.check_latest_package_version(constants.ReflexHostingCLI.MODULE_NAME) prerequisites.check_latest_package_version(constants.ReflexHostingCLI.MODULE_NAME)
extra: dict[str, str] = (
{"config_path": config_path} if config_path is not None else {}
)
hosting_cli.deploy( hosting_cli.deploy(
app_name=app_name, app_name=app_name,
export_fn=lambda zip_dest_dir, export_fn=lambda zip_dest_dir,
@ -545,7 +547,7 @@ def deploy(
loglevel=type(loglevel).INFO, # type: ignore loglevel=type(loglevel).INFO, # type: ignore
token=token, token=token,
project=project, project=project,
config_path=config_path, **extra,
) )

View File

@ -105,6 +105,7 @@ from reflex.utils.exceptions import (
LockExpiredError, LockExpiredError,
ReflexRuntimeError, ReflexRuntimeError,
SetUndefinedStateVarError, SetUndefinedStateVarError,
StateMismatchError,
StateSchemaMismatchError, StateSchemaMismatchError,
StateSerializationError, StateSerializationError,
StateTooLargeError, StateTooLargeError,
@ -1204,7 +1205,6 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
fget=func, fget=func,
auto_deps=False, auto_deps=False,
deps=["router"], deps=["router"],
cache=True,
_js_expr=param, _js_expr=param,
_var_data=VarData.from_state(cls), _var_data=VarData.from_state(cls),
) )
@ -1548,7 +1548,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
# Return the direct parent of target_state_cls for subsequent linking. # Return the direct parent of target_state_cls for subsequent linking.
return parent_state return parent_state
def _get_state_from_cache(self, state_cls: Type[BaseState]) -> BaseState: def _get_state_from_cache(self, state_cls: Type[T_STATE]) -> T_STATE:
"""Get a state instance from the cache. """Get a state instance from the cache.
Args: Args:
@ -1556,11 +1556,19 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
Returns: Returns:
The instance of state_cls associated with this state's client_token. The instance of state_cls associated with this state's client_token.
Raises:
StateMismatchError: If the state instance is not of the expected type.
""" """
root_state = self._get_root_state() root_state = self._get_root_state()
return root_state.get_substate(state_cls.get_full_name().split(".")) substate = root_state.get_substate(state_cls.get_full_name().split("."))
if not isinstance(substate, state_cls):
raise StateMismatchError(
f"Searched for state {state_cls.get_full_name()} but found {substate}."
)
return substate
async def _get_state_from_redis(self, state_cls: Type[BaseState]) -> BaseState: async def _get_state_from_redis(self, state_cls: Type[T_STATE]) -> T_STATE:
"""Get a state instance from redis. """Get a state instance from redis.
Args: Args:
@ -1571,6 +1579,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
Raises: Raises:
RuntimeError: If redis is not used in this backend process. RuntimeError: If redis is not used in this backend process.
StateMismatchError: If the state instance is not of the expected type.
""" """
# Fetch all missing parent states from redis. # Fetch all missing parent states from redis.
parent_state_of_state_cls = await self._populate_parent_states(state_cls) parent_state_of_state_cls = await self._populate_parent_states(state_cls)
@ -1582,14 +1591,22 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
f"Requested state {state_cls.get_full_name()} is not cached and cannot be accessed without redis. " f"Requested state {state_cls.get_full_name()} is not cached and cannot be accessed without redis. "
"(All states should already be available -- this is likely a bug).", "(All states should already be available -- this is likely a bug).",
) )
return await state_manager.get_state(
state_in_redis = await state_manager.get_state(
token=_substate_key(self.router.session.client_token, state_cls), token=_substate_key(self.router.session.client_token, state_cls),
top_level=False, top_level=False,
get_substates=True, get_substates=True,
parent_state=parent_state_of_state_cls, parent_state=parent_state_of_state_cls,
) )
async def get_state(self, state_cls: Type[BaseState]) -> BaseState: if not isinstance(state_in_redis, state_cls):
raise StateMismatchError(
f"Searched for state {state_cls.get_full_name()} but found {state_in_redis}."
)
return state_in_redis
async def get_state(self, state_cls: Type[T_STATE]) -> T_STATE:
"""Get an instance of the state associated with this token. """Get an instance of the state associated with this token.
Allows for arbitrary access to sibling states from within an event handler. Allows for arbitrary access to sibling states from within an event handler.
@ -2358,6 +2375,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
return state # type: ignore return state # type: ignore
T_STATE = TypeVar("T_STATE", bound=BaseState)
class State(BaseState): class State(BaseState):
"""The app Base State.""" """The app Base State."""

View File

@ -2,6 +2,11 @@
from __future__ import annotations from __future__ import annotations
import inspect
import shutil
from pathlib import Path
from types import FrameType
from rich.console import Console from rich.console import Console
from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
from rich.prompt import Prompt from rich.prompt import Prompt
@ -188,6 +193,33 @@ def warn(msg: str, dedupe: bool = False, **kwargs):
print(f"[orange1]Warning: {msg}[/orange1]", **kwargs) print(f"[orange1]Warning: {msg}[/orange1]", **kwargs)
def _get_first_non_framework_frame() -> FrameType | None:
import click
import typer
import typing_extensions
import reflex as rx
# Exclude utility modules that should never be the source of deprecated reflex usage.
exclude_modules = [click, rx, typer, typing_extensions]
exclude_roots = [
p.parent.resolve()
if (p := Path(m.__file__)).name == "__init__.py"
else p.resolve()
for m in exclude_modules
]
# Specifically exclude the reflex cli module.
if reflex_bin := shutil.which(b"reflex"):
exclude_roots.append(Path(reflex_bin.decode()))
frame = inspect.currentframe()
while frame := frame and frame.f_back:
frame_path = Path(inspect.getfile(frame)).resolve()
if not any(frame_path.is_relative_to(root) for root in exclude_roots):
break
return frame
def deprecate( def deprecate(
feature_name: str, feature_name: str,
reason: str, reason: str,
@ -206,15 +238,27 @@ def deprecate(
dedupe: If True, suppress multiple console logs of deprecation message. dedupe: If True, suppress multiple console logs of deprecation message.
kwargs: Keyword arguments to pass to the print function. kwargs: Keyword arguments to pass to the print function.
""" """
if feature_name not in _EMITTED_DEPRECATION_WARNINGS: dedupe_key = feature_name
loc = ""
# See if we can find where the deprecation exists in "user code"
origin_frame = _get_first_non_framework_frame()
if origin_frame is not None:
filename = Path(origin_frame.f_code.co_filename)
if filename.is_relative_to(Path.cwd()):
filename = filename.relative_to(Path.cwd())
loc = f"{filename}:{origin_frame.f_lineno}"
dedupe_key = f"{dedupe_key} {loc}"
if dedupe_key not in _EMITTED_DEPRECATION_WARNINGS:
msg = ( msg = (
f"{feature_name} has been deprecated in version {deprecation_version} {reason.rstrip('.')}. It will be completely " f"{feature_name} has been deprecated in version {deprecation_version} {reason.rstrip('.')}. It will be completely "
f"removed in {removal_version}" f"removed in {removal_version}. ({loc})"
) )
if _LOG_LEVEL <= LogLevel.WARNING: if _LOG_LEVEL <= LogLevel.WARNING:
print(f"[yellow]DeprecationWarning: {msg}[/yellow]", **kwargs) print(f"[yellow]DeprecationWarning: {msg}[/yellow]", **kwargs)
if dedupe: if dedupe:
_EMITTED_DEPRECATION_WARNINGS.add(feature_name) _EMITTED_DEPRECATION_WARNINGS.add(dedupe_key)
def error(msg: str, dedupe: bool = False, **kwargs): def error(msg: str, dedupe: bool = False, **kwargs):

View File

@ -163,6 +163,10 @@ class StateSerializationError(ReflexError):
"""Raised when the state cannot be serialized.""" """Raised when the state cannot be serialized."""
class StateMismatchError(ReflexError, ValueError):
"""Raised when the state retrieved does not match the expected state."""
class SystemPackageMissingError(ReflexError): class SystemPackageMissingError(ReflexError):
"""Raised when a system package is missing.""" """Raised when a system package is missing."""

View File

@ -610,10 +610,14 @@ def initialize_web_directory():
init_reflex_json(project_hash=project_hash) init_reflex_json(project_hash=project_hash)
def _turbopack_flag() -> str:
return " --turbopack" if environment.REFLEX_USE_TURBOPACK.get() else ""
def _compile_package_json(): def _compile_package_json():
return templates.PACKAGE_JSON.render( return templates.PACKAGE_JSON.render(
scripts={ scripts={
"dev": constants.PackageJson.Commands.DEV, "dev": constants.PackageJson.Commands.DEV + _turbopack_flag(),
"export": constants.PackageJson.Commands.EXPORT, "export": constants.PackageJson.Commands.EXPORT,
"export_sitemap": constants.PackageJson.Commands.EXPORT_SITEMAP, "export_sitemap": constants.PackageJson.Commands.EXPORT_SITEMAP,
"prod": constants.PackageJson.Commands.PROD, "prod": constants.PackageJson.Commands.PROD,

View File

@ -17,6 +17,7 @@ import typer
from redis.exceptions import RedisError from redis.exceptions import RedisError
from reflex import constants from reflex import constants
from reflex.config import environment
from reflex.utils import console, path_ops, prerequisites from reflex.utils import console, path_ops, prerequisites
@ -156,24 +157,30 @@ def new_process(args, run: bool = False, show_logs: bool = False, **kwargs):
Raises: Raises:
Exit: When attempting to run a command with a None value. Exit: When attempting to run a command with a None value.
""" """
node_bin_path = str(path_ops.get_node_bin_path()) # Check for invalid command first.
if not node_bin_path and not prerequisites.CURRENTLY_INSTALLING_NODE:
console.warn(
"The path to the Node binary could not be found. Please ensure that Node is properly "
"installed and added to your system's PATH environment variable or try running "
"`reflex init` again."
)
if None in args: if None in args:
console.error(f"Invalid command: {args}") console.error(f"Invalid command: {args}")
raise typer.Exit(1) raise typer.Exit(1)
# Add the node bin path to the PATH environment variable.
path_env: str = os.environ.get("PATH", "")
# Add node_bin_path to the PATH environment variable.
if not environment.REFLEX_BACKEND_ONLY.get():
node_bin_path = str(path_ops.get_node_bin_path())
if not node_bin_path and not prerequisites.CURRENTLY_INSTALLING_NODE:
console.warn(
"The path to the Node binary could not be found. Please ensure that Node is properly "
"installed and added to your system's PATH environment variable or try running "
"`reflex init` again."
)
path_env = os.pathsep.join([node_bin_path, path_env])
env: dict[str, str] = { env: dict[str, str] = {
**os.environ, **os.environ,
"PATH": os.pathsep.join( "PATH": path_env,
[node_bin_path if node_bin_path else "", os.environ["PATH"]]
), # type: ignore
**kwargs.pop("env", {}), **kwargs.pop("env", {}),
} }
kwargs = { kwargs = {
"env": env, "env": env,
"stderr": None if show_logs else subprocess.STDOUT, "stderr": None if show_logs else subprocess.STDOUT,

View File

@ -561,7 +561,7 @@ class Var(Generic[VAR_TYPE]):
if _var_is_local is not None: if _var_is_local is not None:
console.deprecate( console.deprecate(
feature_name="_var_is_local", feature_name="_var_is_local",
reason="The _var_is_local argument is not supported for Var." reason="The _var_is_local argument is not supported for Var. "
"If you want to create a Var from a raw Javascript expression, use the constructor directly", "If you want to create a Var from a raw Javascript expression, use the constructor directly",
deprecation_version="0.6.0", deprecation_version="0.6.0",
removal_version="0.7.0", removal_version="0.7.0",
@ -569,7 +569,7 @@ class Var(Generic[VAR_TYPE]):
if _var_is_string is not None: if _var_is_string is not None:
console.deprecate( console.deprecate(
feature_name="_var_is_string", feature_name="_var_is_string",
reason="The _var_is_string argument is not supported for Var." reason="The _var_is_string argument is not supported for Var. "
"If you want to create a Var from a raw Javascript expression, use the constructor directly", "If you want to create a Var from a raw Javascript expression, use the constructor directly",
deprecation_version="0.6.0", deprecation_version="0.6.0",
removal_version="0.7.0", removal_version="0.7.0",
@ -1838,7 +1838,7 @@ class ComputedVar(Var[RETURN_TYPE]):
self, self,
fget: Callable[[BASE_STATE], RETURN_TYPE], fget: Callable[[BASE_STATE], RETURN_TYPE],
initial_value: RETURN_TYPE | types.Unset = types.Unset(), initial_value: RETURN_TYPE | types.Unset = types.Unset(),
cache: bool = False, cache: bool = True,
deps: Optional[List[Union[str, Var]]] = None, deps: Optional[List[Union[str, Var]]] = None,
auto_deps: bool = True, auto_deps: bool = True,
interval: Optional[Union[int, datetime.timedelta]] = None, interval: Optional[Union[int, datetime.timedelta]] = None,
@ -2253,7 +2253,7 @@ if TYPE_CHECKING:
def computed_var( def computed_var(
fget: None = None, fget: None = None,
initial_value: Any | types.Unset = types.Unset(), initial_value: Any | types.Unset = types.Unset(),
cache: bool = False, cache: bool = True,
deps: Optional[List[Union[str, Var]]] = None, deps: Optional[List[Union[str, Var]]] = None,
auto_deps: bool = True, auto_deps: bool = True,
interval: Optional[Union[datetime.timedelta, int]] = None, interval: Optional[Union[datetime.timedelta, int]] = None,
@ -2266,7 +2266,7 @@ def computed_var(
def computed_var( def computed_var(
fget: Callable[[BASE_STATE], RETURN_TYPE], fget: Callable[[BASE_STATE], RETURN_TYPE],
initial_value: RETURN_TYPE | types.Unset = types.Unset(), initial_value: RETURN_TYPE | types.Unset = types.Unset(),
cache: bool = False, cache: bool = True,
deps: Optional[List[Union[str, Var]]] = None, deps: Optional[List[Union[str, Var]]] = None,
auto_deps: bool = True, auto_deps: bool = True,
interval: Optional[Union[datetime.timedelta, int]] = None, interval: Optional[Union[datetime.timedelta, int]] = None,
@ -2278,7 +2278,7 @@ def computed_var(
def computed_var( def computed_var(
fget: Callable[[BASE_STATE], Any] | None = None, fget: Callable[[BASE_STATE], Any] | None = None,
initial_value: Any | types.Unset = types.Unset(), initial_value: Any | types.Unset = types.Unset(),
cache: Optional[bool] = None, cache: bool = True,
deps: Optional[List[Union[str, Var]]] = None, deps: Optional[List[Union[str, Var]]] = None,
auto_deps: bool = True, auto_deps: bool = True,
interval: Optional[Union[datetime.timedelta, int]] = None, interval: Optional[Union[datetime.timedelta, int]] = None,
@ -2304,15 +2304,6 @@ def computed_var(
ValueError: If caching is disabled and an update interval is set. ValueError: If caching is disabled and an update interval is set.
VarDependencyError: If user supplies dependencies without caching. VarDependencyError: If user supplies dependencies without caching.
""" """
if cache is None:
cache = False
console.deprecate(
"Default non-cached rx.var",
"the default value will be `@rx.var(cache=True)` in a future release. "
"To retain uncached var, explicitly pass `@rx.var(cache=False)`",
deprecation_version="0.6.8",
removal_version="0.7.0",
)
if cache is False and interval is not None: if cache is False and interval is not None:
raise ValueError("Cannot set update interval without caching.") raise ValueError("Cannot set update interval without caching.")

View File

@ -390,6 +390,7 @@ class ArgsFunctionOperation(CachedVarOperation, FunctionVar):
Returns: Returns:
The function var. The function var.
""" """
return_expr = Var.create(return_expr)
return cls( return cls(
_js_expr="", _js_expr="",
_var_type=_var_type, _var_type=_var_type,
@ -445,6 +446,7 @@ class ArgsFunctionOperationBuilder(CachedVarOperation, BuilderFunctionVar):
Returns: Returns:
The function var. The function var.
""" """
return_expr = Var.create(return_expr)
return cls( return cls(
_js_expr="", _js_expr="",
_var_type=_var_type, _var_type=_var_type,

View File

@ -20,7 +20,6 @@ from typing import (
from reflex.constants.base import Dirs from reflex.constants.base import Dirs
from reflex.utils.exceptions import PrimitiveUnserializableToJSON, VarTypeError from reflex.utils.exceptions import PrimitiveUnserializableToJSON, VarTypeError
from reflex.utils.imports import ImportDict, ImportVar from reflex.utils.imports import ImportDict, ImportVar
from reflex.utils.types import is_optional
from .base import ( from .base import (
CustomVarOperationReturn, CustomVarOperationReturn,
@ -431,7 +430,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)):
""" """
if not isinstance(other, NUMBER_TYPES): if not isinstance(other, NUMBER_TYPES):
raise_unsupported_operand_types("<", (type(self), type(other))) raise_unsupported_operand_types("<", (type(self), type(other)))
return less_than_operation(self, +other) return less_than_operation(+self, +other)
@overload @overload
def __le__(self, other: number_types) -> BooleanVar: ... def __le__(self, other: number_types) -> BooleanVar: ...
@ -450,7 +449,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)):
""" """
if not isinstance(other, NUMBER_TYPES): if not isinstance(other, NUMBER_TYPES):
raise_unsupported_operand_types("<=", (type(self), type(other))) raise_unsupported_operand_types("<=", (type(self), type(other)))
return less_than_or_equal_operation(self, +other) return less_than_or_equal_operation(+self, +other)
def __eq__(self, other: Any): def __eq__(self, other: Any):
"""Equal comparison. """Equal comparison.
@ -462,7 +461,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)):
The result of the comparison. The result of the comparison.
""" """
if isinstance(other, NUMBER_TYPES): if isinstance(other, NUMBER_TYPES):
return equal_operation(self, +other) return equal_operation(+self, +other)
return equal_operation(self, other) return equal_operation(self, other)
def __ne__(self, other: Any): def __ne__(self, other: Any):
@ -475,7 +474,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)):
The result of the comparison. The result of the comparison.
""" """
if isinstance(other, NUMBER_TYPES): if isinstance(other, NUMBER_TYPES):
return not_equal_operation(self, +other) return not_equal_operation(+self, +other)
return not_equal_operation(self, other) return not_equal_operation(self, other)
@overload @overload
@ -495,7 +494,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)):
""" """
if not isinstance(other, NUMBER_TYPES): if not isinstance(other, NUMBER_TYPES):
raise_unsupported_operand_types(">", (type(self), type(other))) raise_unsupported_operand_types(">", (type(self), type(other)))
return greater_than_operation(self, +other) return greater_than_operation(+self, +other)
@overload @overload
def __ge__(self, other: number_types) -> BooleanVar: ... def __ge__(self, other: number_types) -> BooleanVar: ...
@ -514,17 +513,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)):
""" """
if not isinstance(other, NUMBER_TYPES): if not isinstance(other, NUMBER_TYPES):
raise_unsupported_operand_types(">=", (type(self), type(other))) raise_unsupported_operand_types(">=", (type(self), type(other)))
return greater_than_or_equal_operation(self, +other) return greater_than_or_equal_operation(+self, +other)
def bool(self):
"""Boolean conversion.
Returns:
The boolean value of the number.
"""
if is_optional(self._var_type):
return boolify((self != None) & (self != 0)) # noqa: E711
return self != 0
def _is_strict_float(self) -> bool: def _is_strict_float(self) -> bool:
"""Check if the number is a float. """Check if the number is a float.

View File

@ -22,22 +22,22 @@ def ComputedVars():
count: int = 0 count: int = 0
# cached var with dep on count # cached var with dep on count
@rx.var(cache=True, interval=15) @rx.var(interval=15)
def count1(self) -> int: def count1(self) -> int:
return self.count return self.count
# cached backend var with dep on count # cached backend var with dep on count
@rx.var(cache=True, interval=15, backend=True) @rx.var(interval=15, backend=True)
def count1_backend(self) -> int: def count1_backend(self) -> int:
return self.count return self.count
# same as above but implicit backend with `_` prefix # same as above but implicit backend with `_` prefix
@rx.var(cache=True, interval=15) @rx.var(interval=15)
def _count1_backend(self) -> int: def _count1_backend(self) -> int:
return self.count return self.count
# explicit disabled auto_deps # explicit disabled auto_deps
@rx.var(interval=15, cache=True, auto_deps=False) @rx.var(interval=15, auto_deps=False)
def count3(self) -> int: def count3(self) -> int:
# this will not add deps, because auto_deps is False # this will not add deps, because auto_deps is False
print(self.count1) print(self.count1)
@ -45,19 +45,27 @@ def ComputedVars():
return self.count return self.count
# explicit dependency on count var # explicit dependency on count var
@rx.var(cache=True, deps=["count"], auto_deps=False) @rx.var(deps=["count"], auto_deps=False)
def depends_on_count(self) -> int: def depends_on_count(self) -> int:
return self.count return self.count
# explicit dependency on count1 var # explicit dependency on count1 var
@rx.var(cache=True, deps=[count1], auto_deps=False) @rx.var(deps=[count1], auto_deps=False)
def depends_on_count1(self) -> int: def depends_on_count1(self) -> int:
return self.count return self.count
@rx.var(deps=[count3], auto_deps=False, cache=True) @rx.var(
deps=[count3],
auto_deps=False,
)
def depends_on_count3(self) -> int: def depends_on_count3(self) -> int:
return self.count return self.count
# special floats should be properly decoded on the frontend
@rx.var(cache=True, initial_value=[])
def special_floats(self) -> list[float]:
return [42.9, float("nan"), float("inf"), float("-inf")]
@rx.event @rx.event
def increment(self): def increment(self):
self.count += 1 self.count += 1
@ -103,6 +111,11 @@ def ComputedVars():
State.depends_on_count3, State.depends_on_count3,
id="depends_on_count3", id="depends_on_count3",
), ),
rx.text("special_floats:"),
rx.text(
State.special_floats.join(", "),
id="special_floats",
),
), ),
) )
@ -224,6 +237,10 @@ async def test_computed_vars(
assert depends_on_count3 assert depends_on_count3
assert depends_on_count3.text == "0" assert depends_on_count3.text == "0"
special_floats = driver.find_element(By.ID, "special_floats")
assert special_floats
assert special_floats.text == "42.9, NaN, Infinity, -Infinity"
increment = driver.find_element(By.ID, "increment") increment = driver.find_element(By.ID, "increment")
assert increment.is_enabled() assert increment.is_enabled()

View File

@ -74,16 +74,16 @@ def DynamicRoute():
class ArgState(rx.State): class ArgState(rx.State):
"""The app state.""" """The app state."""
@rx.var @rx.var(cache=False)
def arg(self) -> int: def arg(self) -> int:
return int(self.arg_str or 0) return int(self.arg_str or 0)
class ArgSubState(ArgState): class ArgSubState(ArgState):
@rx.var(cache=True) @rx.var
def cached_arg(self) -> int: def cached_arg(self) -> int:
return self.arg return self.arg
@rx.var(cache=True) @rx.var
def cached_arg_str(self) -> str: def cached_arg_str(self) -> str:
return self.arg_str return self.arg_str

View File

@ -36,7 +36,7 @@ def LifespanApp():
print("Lifespan global started.") print("Lifespan global started.")
try: try:
while True: while True:
lifespan_task_global += inc # pyright: ignore[reportUnboundVariable] lifespan_task_global += inc # pyright: ignore[reportUnboundVariable, reportPossiblyUnboundVariable]
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
except asyncio.CancelledError as ce: except asyncio.CancelledError as ce:
print(f"Lifespan global cancelled: {ce}.") print(f"Lifespan global cancelled: {ce}.")
@ -45,11 +45,11 @@ def LifespanApp():
class LifespanState(rx.State): class LifespanState(rx.State):
interval: int = 100 interval: int = 100
@rx.var @rx.var(cache=False)
def task_global(self) -> int: def task_global(self) -> int:
return lifespan_task_global return lifespan_task_global
@rx.var @rx.var(cache=False)
def context_global(self) -> int: def context_global(self) -> int:
return lifespan_context_global return lifespan_context_global

View File

@ -22,31 +22,31 @@ def MediaApp():
img.format = format # type: ignore img.format = format # type: ignore
return img return img
@rx.var(cache=True) @rx.var
def img_default(self) -> Image.Image: def img_default(self) -> Image.Image:
return self._blue() return self._blue()
@rx.var(cache=True) @rx.var
def img_bmp(self) -> Image.Image: def img_bmp(self) -> Image.Image:
return self._blue(format="BMP") return self._blue(format="BMP")
@rx.var(cache=True) @rx.var
def img_jpg(self) -> Image.Image: def img_jpg(self) -> Image.Image:
return self._blue(format="JPEG") return self._blue(format="JPEG")
@rx.var(cache=True) @rx.var
def img_png(self) -> Image.Image: def img_png(self) -> Image.Image:
return self._blue(format="PNG") return self._blue(format="PNG")
@rx.var(cache=True) @rx.var
def img_gif(self) -> Image.Image: def img_gif(self) -> Image.Image:
return self._blue(format="GIF") return self._blue(format="GIF")
@rx.var(cache=True) @rx.var
def img_webp(self) -> Image.Image: def img_webp(self) -> Image.Image:
return self._blue(format="WEBP") return self._blue(format="WEBP")
@rx.var(cache=True) @rx.var
def img_from_url(self) -> Image.Image: def img_from_url(self) -> Image.Image:
img_url = "https://picsum.photos/id/1/200/300" img_url = "https://picsum.photos/id/1/200/300"
img_resp = httpx.get(img_url, follow_redirects=True) img_resp = httpx.get(img_url, follow_redirects=True)

View File

@ -908,7 +908,7 @@ class DynamicState(BaseState):
"""Increment the counter var.""" """Increment the counter var."""
self.counter = self.counter + 1 self.counter = self.counter + 1
@computed_var(cache=True) @computed_var
def comp_dynamic(self) -> str: def comp_dynamic(self) -> str:
"""A computed var that depends on the dynamic var. """A computed var that depends on the dynamic var.
@ -1549,11 +1549,11 @@ def test_app_with_valid_var_dependencies(compilable_app: tuple[App, Path]):
base: int = 0 base: int = 0
_backend: int = 0 _backend: int = 0
@computed_var(cache=True) @computed_var()
def foo(self) -> str: def foo(self) -> str:
return "foo" return "foo"
@computed_var(deps=["_backend", "base", foo], cache=True) @computed_var(deps=["_backend", "base", foo])
def bar(self) -> str: def bar(self) -> str:
return "bar" return "bar"
@ -1565,7 +1565,7 @@ def test_app_with_invalid_var_dependencies(compilable_app: tuple[App, Path]):
app, _ = compilable_app app, _ = compilable_app
class InvalidDepState(BaseState): class InvalidDepState(BaseState):
@computed_var(deps=["foolksjdf"], cache=True) @computed_var(deps=["foolksjdf"])
def bar(self) -> str: def bar(self) -> str:
return "bar" return "bar"

View File

@ -202,7 +202,7 @@ class GrandchildState(ChildState):
class GrandchildState2(ChildState2): class GrandchildState2(ChildState2):
"""A grandchild state fixture.""" """A grandchild state fixture."""
@rx.var(cache=True) @rx.var
def cached(self) -> str: def cached(self) -> str:
"""A cached var. """A cached var.
@ -215,7 +215,7 @@ class GrandchildState2(ChildState2):
class GrandchildState3(ChildState3): class GrandchildState3(ChildState3):
"""A great grandchild state fixture.""" """A great grandchild state fixture."""
@rx.var @rx.var(cache=False)
def computed(self) -> str: def computed(self) -> str:
"""A computed var. """A computed var.
@ -796,7 +796,7 @@ async def test_process_event_simple(test_state):
# The delta should contain the changes, including computed vars. # The delta should contain the changes, including computed vars.
assert update.delta == { assert update.delta == {
TestState.get_full_name(): {"num1": 69, "sum": 72.14, "upper": ""}, TestState.get_full_name(): {"num1": 69, "sum": 72.14},
GrandchildState3.get_full_name(): {"computed": ""}, GrandchildState3.get_full_name(): {"computed": ""},
} }
assert update.events == [] assert update.events == []
@ -823,7 +823,7 @@ async def test_process_event_substate(test_state, child_state, grandchild_state)
assert child_state.value == "HI" assert child_state.value == "HI"
assert child_state.count == 24 assert child_state.count == 24
assert update.delta == { assert update.delta == {
TestState.get_full_name(): {"sum": 3.14, "upper": ""}, # TestState.get_full_name(): {"sum": 3.14, "upper": ""},
ChildState.get_full_name(): {"value": "HI", "count": 24}, ChildState.get_full_name(): {"value": "HI", "count": 24},
GrandchildState3.get_full_name(): {"computed": ""}, GrandchildState3.get_full_name(): {"computed": ""},
} }
@ -839,7 +839,7 @@ async def test_process_event_substate(test_state, child_state, grandchild_state)
update = await test_state._process(event).__anext__() update = await test_state._process(event).__anext__()
assert grandchild_state.value2 == "new" assert grandchild_state.value2 == "new"
assert update.delta == { assert update.delta == {
TestState.get_full_name(): {"sum": 3.14, "upper": ""}, # TestState.get_full_name(): {"sum": 3.14, "upper": ""},
GrandchildState.get_full_name(): {"value2": "new"}, GrandchildState.get_full_name(): {"value2": "new"},
GrandchildState3.get_full_name(): {"computed": ""}, GrandchildState3.get_full_name(): {"computed": ""},
} }
@ -989,7 +989,7 @@ class InterdependentState(BaseState):
v1: int = 0 v1: int = 0
_v2: int = 1 _v2: int = 1
@rx.var(cache=True) @rx.var
def v1x2(self) -> int: def v1x2(self) -> int:
"""Depends on var v1. """Depends on var v1.
@ -998,7 +998,7 @@ class InterdependentState(BaseState):
""" """
return self.v1 * 2 return self.v1 * 2
@rx.var(cache=True) @rx.var
def v2x2(self) -> int: def v2x2(self) -> int:
"""Depends on backend var _v2. """Depends on backend var _v2.
@ -1007,7 +1007,7 @@ class InterdependentState(BaseState):
""" """
return self._v2 * 2 return self._v2 * 2
@rx.var(cache=True, backend=True) @rx.var(backend=True)
def v2x2_backend(self) -> int: def v2x2_backend(self) -> int:
"""Depends on backend var _v2. """Depends on backend var _v2.
@ -1016,7 +1016,7 @@ class InterdependentState(BaseState):
""" """
return self._v2 * 2 return self._v2 * 2
@rx.var(cache=True) @rx.var
def v1x2x2(self) -> int: def v1x2x2(self) -> int:
"""Depends on ComputedVar v1x2. """Depends on ComputedVar v1x2.
@ -1025,7 +1025,7 @@ class InterdependentState(BaseState):
""" """
return self.v1x2 * 2 # type: ignore return self.v1x2 * 2 # type: ignore
@rx.var(cache=True) @rx.var
def _v3(self) -> int: def _v3(self) -> int:
"""Depends on backend var _v2. """Depends on backend var _v2.
@ -1034,7 +1034,7 @@ class InterdependentState(BaseState):
""" """
return self._v2 return self._v2
@rx.var(cache=True) @rx.var
def v3x2(self) -> int: def v3x2(self) -> int:
"""Depends on ComputedVar _v3. """Depends on ComputedVar _v3.
@ -1239,7 +1239,7 @@ def test_computed_var_cached():
class ComputedState(BaseState): class ComputedState(BaseState):
v: int = 0 v: int = 0
@rx.var(cache=True) @rx.var
def comp_v(self) -> int: def comp_v(self) -> int:
nonlocal comp_v_calls nonlocal comp_v_calls
comp_v_calls += 1 comp_v_calls += 1
@ -1264,15 +1264,15 @@ def test_computed_var_cached_depends_on_non_cached():
class ComputedState(BaseState): class ComputedState(BaseState):
v: int = 0 v: int = 0
@rx.var @rx.var(cache=False)
def no_cache_v(self) -> int: def no_cache_v(self) -> int:
return self.v return self.v
@rx.var(cache=True) @rx.var
def dep_v(self) -> int: def dep_v(self) -> int:
return self.no_cache_v # type: ignore return self.no_cache_v # type: ignore
@rx.var(cache=True) @rx.var
def comp_v(self) -> int: def comp_v(self) -> int:
return self.v return self.v
@ -1304,14 +1304,14 @@ def test_computed_var_depends_on_parent_non_cached():
counter = 0 counter = 0
class ParentState(BaseState): class ParentState(BaseState):
@rx.var @rx.var(cache=False)
def no_cache_v(self) -> int: def no_cache_v(self) -> int:
nonlocal counter nonlocal counter
counter += 1 counter += 1
return counter return counter
class ChildState(ParentState): class ChildState(ParentState):
@rx.var(cache=True) @rx.var
def dep_v(self) -> int: def dep_v(self) -> int:
return self.no_cache_v # type: ignore return self.no_cache_v # type: ignore
@ -1357,7 +1357,7 @@ def test_cached_var_depends_on_event_handler(use_partial: bool):
def handler(self): def handler(self):
self.x = self.x + 1 self.x = self.x + 1
@rx.var(cache=True) @rx.var
def cached_x_side_effect(self) -> int: def cached_x_side_effect(self) -> int:
self.handler() self.handler()
nonlocal counter nonlocal counter
@ -1393,7 +1393,7 @@ def test_computed_var_dependencies():
def testprop(self) -> int: def testprop(self) -> int:
return self.v return self.v
@rx.var(cache=True) @rx.var
def comp_v(self) -> int: def comp_v(self) -> int:
"""Direct access. """Direct access.
@ -1402,7 +1402,7 @@ def test_computed_var_dependencies():
""" """
return self.v return self.v
@rx.var(cache=True, backend=True) @rx.var(backend=True)
def comp_v_backend(self) -> int: def comp_v_backend(self) -> int:
"""Direct access backend var. """Direct access backend var.
@ -1411,7 +1411,7 @@ def test_computed_var_dependencies():
""" """
return self.v return self.v
@rx.var(cache=True) @rx.var
def comp_v_via_property(self) -> int: def comp_v_via_property(self) -> int:
"""Access v via property. """Access v via property.
@ -1420,7 +1420,7 @@ def test_computed_var_dependencies():
""" """
return self.testprop return self.testprop
@rx.var(cache=True) @rx.var
def comp_w(self): def comp_w(self):
"""Nested lambda. """Nested lambda.
@ -1429,7 +1429,7 @@ def test_computed_var_dependencies():
""" """
return lambda: self.w return lambda: self.w
@rx.var(cache=True) @rx.var
def comp_x(self): def comp_x(self):
"""Nested function. """Nested function.
@ -1442,7 +1442,7 @@ def test_computed_var_dependencies():
return _ return _
@rx.var(cache=True) @rx.var
def comp_y(self) -> List[int]: def comp_y(self) -> List[int]:
"""Comprehension iterating over attribute. """Comprehension iterating over attribute.
@ -1451,7 +1451,7 @@ def test_computed_var_dependencies():
""" """
return [round(y) for y in self.y] return [round(y) for y in self.y]
@rx.var(cache=True) @rx.var
def comp_z(self) -> List[bool]: def comp_z(self) -> List[bool]:
"""Comprehension accesses attribute. """Comprehension accesses attribute.
@ -2027,10 +2027,6 @@ async def test_state_proxy(grandchild_state: GrandchildState, mock_app: rx.App):
assert mcall.args[0] == str(SocketEvent.EVENT) assert mcall.args[0] == str(SocketEvent.EVENT)
assert mcall.args[1] == StateUpdate( assert mcall.args[1] == StateUpdate(
delta={ delta={
parent_state.get_full_name(): {
"upper": "",
"sum": 3.14,
},
grandchild_state.get_full_name(): { grandchild_state.get_full_name(): {
"value2": "42", "value2": "42",
}, },
@ -2053,7 +2049,7 @@ class BackgroundTaskState(BaseState):
super().__init__(**kwargs) super().__init__(**kwargs)
self.router_data = {"simulate": "hydrate"} self.router_data = {"simulate": "hydrate"}
@rx.var @rx.var(cache=False)
def computed_order(self) -> List[str]: def computed_order(self) -> List[str]:
"""Get the order as a computed var. """Get the order as a computed var.
@ -3040,10 +3036,6 @@ async def test_get_state(mock_app: rx.App, token: str):
grandchild_state.value2 = "set_value" grandchild_state.value2 = "set_value"
assert test_state.get_delta() == { assert test_state.get_delta() == {
TestState.get_full_name(): {
"sum": 3.14,
"upper": "",
},
GrandchildState.get_full_name(): { GrandchildState.get_full_name(): {
"value2": "set_value", "value2": "set_value",
}, },
@ -3081,10 +3073,6 @@ async def test_get_state(mock_app: rx.App, token: str):
child_state2.value = "set_c2_value" child_state2.value = "set_c2_value"
assert new_test_state.get_delta() == { assert new_test_state.get_delta() == {
TestState.get_full_name(): {
"sum": 3.14,
"upper": "",
},
ChildState2.get_full_name(): { ChildState2.get_full_name(): {
"value": "set_c2_value", "value": "set_c2_value",
}, },
@ -3139,7 +3127,7 @@ async def test_get_state_from_sibling_not_cached(mock_app: rx.App, token: str):
child3_var: int = 0 child3_var: int = 0
@rx.var @rx.var(cache=False)
def v(self): def v(self):
pass pass
@ -3210,8 +3198,8 @@ def test_potentially_dirty_substates():
def bar(self) -> str: def bar(self) -> str:
return "" return ""
assert RxState._potentially_dirty_substates() == {State} assert RxState._potentially_dirty_substates() == set()
assert State._potentially_dirty_substates() == {C1} assert State._potentially_dirty_substates() == set()
assert C1._potentially_dirty_substates() == set() assert C1._potentially_dirty_substates() == set()
@ -3226,7 +3214,7 @@ def test_router_var_dep() -> None:
class RouterVarDepState(RouterVarParentState): class RouterVarDepState(RouterVarParentState):
"""A state with a router var dependency.""" """A state with a router var dependency."""
@rx.var(cache=True) @rx.var
def foo(self) -> str: def foo(self) -> str:
return self.router.page.params.get("foo", "") return self.router.page.params.get("foo", "")
@ -3421,7 +3409,7 @@ class MixinState(State, mixin=True):
_backend: int = 0 _backend: int = 0
_backend_no_default: dict _backend_no_default: dict
@rx.var(cache=True) @rx.var
def computed(self) -> str: def computed(self) -> str:
"""A computed var on mixin state. """A computed var on mixin state.

View File

@ -42,7 +42,7 @@ class SubA_A_A_A(SubA_A_A):
class SubA_A_A_B(SubA_A_A): class SubA_A_A_B(SubA_A_A):
"""SubA_A_A_B is a child of SubA_A_A.""" """SubA_A_A_B is a child of SubA_A_A."""
@rx.var(cache=True) @rx.var
def sub_a_a_a_cached(self) -> int: def sub_a_a_a_cached(self) -> int:
"""A cached var. """A cached var.
@ -117,7 +117,7 @@ class TreeD(Root):
d: int d: int
@rx.var @rx.var(cache=False)
def d_var(self) -> int: def d_var(self) -> int:
"""A computed var. """A computed var.
@ -156,7 +156,7 @@ class SubE_A_A_A_A(SubE_A_A_A):
sub_e_a_a_a_a: int sub_e_a_a_a_a: int
@rx.var @rx.var(cache=False)
def sub_e_a_a_a_a_var(self) -> int: def sub_e_a_a_a_a_var(self) -> int:
"""A computed var. """A computed var.
@ -183,7 +183,7 @@ class SubE_A_A_A_D(SubE_A_A_A):
sub_e_a_a_a_d: int sub_e_a_a_a_d: int
@rx.var(cache=True) @rx.var
def sub_e_a_a_a_d_var(self) -> int: def sub_e_a_a_a_d_var(self) -> int:
"""A computed var. """A computed var.

View File

@ -1004,7 +1004,7 @@ def test_all_number_operations():
assert ( assert (
str(even_more_complicated_number) str(even_more_complicated_number)
== "!(((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))" == "!(isTrue((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))))))"
) )
assert str(LiteralNumberVar.create(5) > False) == "(5 > 0)" assert str(LiteralNumberVar.create(5) > False) == "(5 > 0)"
@ -1814,10 +1814,7 @@ def cv_fget(state: BaseState) -> int:
], ],
) )
def test_computed_var_deps(deps: List[Union[str, Var]], expected: Set[str]): def test_computed_var_deps(deps: List[Union[str, Var]], expected: Set[str]):
@computed_var( @computed_var(deps=deps)
deps=deps,
cache=True,
)
def test_var(state) -> int: def test_var(state) -> int:
return 1 return 1
@ -1835,10 +1832,7 @@ def test_computed_var_deps(deps: List[Union[str, Var]], expected: Set[str]):
def test_invalid_computed_var_deps(deps: List): def test_invalid_computed_var_deps(deps: List):
with pytest.raises(TypeError): with pytest.raises(TypeError):
@computed_var( @computed_var(deps=deps)
deps=deps,
cache=True,
)
def test_var(state) -> int: def test_var(state) -> int:
return 1 return 1