From e8a71122496856fbf20033aab3e3a858fe98d5c1 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Wed, 15 Jan 2025 13:04:15 -0800 Subject: [PATCH 1/7] [ENG-4383] Handle special float values on frontend (#4638) Add a test case to `test_computed_vars.py` which renders a list of special floats. Fix #4637 --- reflex/.templates/web/utils/state.js | 9 ++++++++- tests/integration/test_computed_vars.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 41dbee446..ec603fd13 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -410,7 +410,14 @@ export const connect = async ( autoUnref: false, }); // 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() { if (document.visibilityState === "visible") { diff --git a/tests/integration/test_computed_vars.py b/tests/integration/test_computed_vars.py index 03aaf18b4..efa129430 100644 --- a/tests/integration/test_computed_vars.py +++ b/tests/integration/test_computed_vars.py @@ -58,6 +58,11 @@ def ComputedVars(): def depends_on_count3(self) -> int: 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 def increment(self): self.count += 1 @@ -103,6 +108,11 @@ def ComputedVars(): State.depends_on_count3, id="depends_on_count3", ), + rx.text("special_floats:"), + rx.text( + State.special_floats.join(", "), + id="special_floats", + ), ), ) @@ -224,6 +234,10 @@ async def test_computed_vars( assert depends_on_count3 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") assert increment.is_enabled() From caf29c368068616f4e899c7d5dca8e80cb20d774 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 15 Jan 2025 14:17:58 -0800 Subject: [PATCH 2/7] put import at the top of dynamic component evaluation (#4632) --- reflex/components/dynamic.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/reflex/components/dynamic.py b/reflex/components/dynamic.py index fbfc55f97..806d610df 100644 --- a/reflex/components/dynamic.py +++ b/reflex/components/dynamic.py @@ -136,6 +136,23 @@ def load_dynamic_serializer(): 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( [ "//__reflex_evaluate", From fbf9524a6cdca99efaa04892f01b9269a6756034 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Wed, 15 Jan 2025 14:19:11 -0800 Subject: [PATCH 3/7] Show file and line number in deprecation warnings (#4631) * Show file and line number in deprecation warnings * Exclude modules/packages by import Less bespoke method of considering some packages to be part of the framework and passed over when finding user code. --- reflex/utils/console.py | 50 ++++++++++++++++++++++++++++++++++++++--- reflex/vars/base.py | 4 ++-- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/reflex/utils/console.py b/reflex/utils/console.py index be545140a..8929b63b6 100644 --- a/reflex/utils/console.py +++ b/reflex/utils/console.py @@ -2,6 +2,11 @@ from __future__ import annotations +import inspect +import shutil +from pathlib import Path +from types import FrameType + from rich.console import Console from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn from rich.prompt import Prompt @@ -188,6 +193,33 @@ def warn(msg: str, dedupe: bool = False, **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( feature_name: str, reason: str, @@ -206,15 +238,27 @@ def deprecate( dedupe: If True, suppress multiple console logs of deprecation message. 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 = ( 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: print(f"[yellow]DeprecationWarning: {msg}[/yellow]", **kwargs) if dedupe: - _EMITTED_DEPRECATION_WARNINGS.add(feature_name) + _EMITTED_DEPRECATION_WARNINGS.add(dedupe_key) def error(msg: str, dedupe: bool = False, **kwargs): diff --git a/reflex/vars/base.py b/reflex/vars/base.py index 0a93901cd..4a75164f4 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -561,7 +561,7 @@ class Var(Generic[VAR_TYPE]): if _var_is_local is not None: console.deprecate( 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", deprecation_version="0.6.0", removal_version="0.7.0", @@ -569,7 +569,7 @@ class Var(Generic[VAR_TYPE]): if _var_is_string is not None: console.deprecate( 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", deprecation_version="0.6.0", removal_version="0.7.0", From 9fe8e6f1ce519547a6d23d3f8b5d8ce16515b734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Wed, 15 Jan 2025 14:20:04 -0800 Subject: [PATCH 4/7] bump nextJS to v15 (#4630) * bump nextJS to v15 * make turbopack depends on env var --- reflex/config.py | 3 +++ reflex/constants/installer.py | 2 +- reflex/utils/prerequisites.py | 6 +++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/reflex/config.py b/reflex/config.py index 0579b019f..7614417d5 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -567,6 +567,9 @@ class EnvironmentVariables: # The maximum size of the reflex state in kilobytes. REFLEX_STATE_SIZE_LIMIT: EnvVar[int] = env_var(1000) + # Whether to use the turbopack bundler. + REFLEX_USE_TURBOPACK: EnvVar[bool] = env_var(True) + environment = EnvironmentVariables() diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py index 0b45586dd..f9dd26b5a 100644 --- a/reflex/constants/installer.py +++ b/reflex/constants/installer.py @@ -182,7 +182,7 @@ class PackageJson(SimpleNamespace): "@emotion/react": "11.13.3", "axios": "1.7.7", "json5": "2.2.3", - "next": "14.2.16", + "next": "15.1.4", "next-sitemap": "4.2.3", "next-themes": "0.4.3", "react": "18.3.1", diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index d838c0eea..e450393c3 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -610,10 +610,14 @@ def initialize_web_directory(): init_reflex_json(project_hash=project_hash) +def _turbopack_flag() -> str: + return " --turbopack" if environment.REFLEX_USE_TURBOPACK.get() else "" + + def _compile_package_json(): return templates.PACKAGE_JSON.render( scripts={ - "dev": constants.PackageJson.Commands.DEV, + "dev": constants.PackageJson.Commands.DEV + _turbopack_flag(), "export": constants.PackageJson.Commands.EXPORT, "export_sitemap": constants.PackageJson.Commands.EXPORT_SITEMAP, "prod": constants.PackageJson.Commands.PROD, From b50b7692b22261654cc5a023d326c169bb968571 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 15 Jan 2025 14:21:33 -0800 Subject: [PATCH 5/7] improve type support for .get_state (#4623) * improve type support for .get_state * dang it darglint --- reflex/state.py | 31 ++++++++++++++++++++++++++----- reflex/utils/exceptions.py | 4 ++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index a31aae032..16875bec3 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -104,6 +104,7 @@ from reflex.utils.exceptions import ( LockExpiredError, ReflexRuntimeError, SetUndefinedStateVarError, + StateMismatchError, StateSchemaMismatchError, StateSerializationError, StateTooLargeError, @@ -1543,7 +1544,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): # Return the direct parent of target_state_cls for subsequent linking. 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. Args: @@ -1551,11 +1552,19 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): Returns: 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() - 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. Args: @@ -1566,6 +1575,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): Raises: 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. parent_state_of_state_cls = await self._populate_parent_states(state_cls) @@ -1577,14 +1587,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. " "(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), top_level=False, get_substates=True, 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. Allows for arbitrary access to sibling states from within an event handler. @@ -2316,6 +2334,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): return state +T_STATE = TypeVar("T_STATE", bound=BaseState) + + class State(BaseState): """The app Base State.""" diff --git a/reflex/utils/exceptions.py b/reflex/utils/exceptions.py index bceadc977..339abcda1 100644 --- a/reflex/utils/exceptions.py +++ b/reflex/utils/exceptions.py @@ -163,6 +163,10 @@ class StateSerializationError(ReflexError): """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): """Raised when a system package is missing.""" From cb24492371d8e3bb90ab6c924f5455b925279fd7 Mon Sep 17 00:00:00 2001 From: Khaleel Al-Adhami Date: Wed, 15 Jan 2025 14:23:45 -0800 Subject: [PATCH 6/7] fix boolean to boolen comparisons (#4620) * fix boolean to boolen comparisons * fixes #4618 * fix tests --- reflex/vars/number.py | 23 ++++++----------------- tests/units/test_var.py | 2 +- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/reflex/vars/number.py b/reflex/vars/number.py index d04aded35..a2a0293d5 100644 --- a/reflex/vars/number.py +++ b/reflex/vars/number.py @@ -20,7 +20,6 @@ from typing import ( from reflex.constants.base import Dirs from reflex.utils.exceptions import PrimitiveUnserializableToJSON, VarTypeError from reflex.utils.imports import ImportDict, ImportVar -from reflex.utils.types import is_optional from .base import ( CustomVarOperationReturn, @@ -431,7 +430,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): """ if not isinstance(other, NUMBER_TYPES): raise_unsupported_operand_types("<", (type(self), type(other))) - return less_than_operation(self, +other) + return less_than_operation(+self, +other) @overload 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): 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): """Equal comparison. @@ -462,7 +461,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): The result of the comparison. """ if isinstance(other, NUMBER_TYPES): - return equal_operation(self, +other) + return equal_operation(+self, +other) return equal_operation(self, other) def __ne__(self, other: Any): @@ -475,7 +474,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): The result of the comparison. """ if isinstance(other, NUMBER_TYPES): - return not_equal_operation(self, +other) + return not_equal_operation(+self, +other) return not_equal_operation(self, other) @overload @@ -495,7 +494,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): """ if not isinstance(other, NUMBER_TYPES): raise_unsupported_operand_types(">", (type(self), type(other))) - return greater_than_operation(self, +other) + return greater_than_operation(+self, +other) @overload 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): raise_unsupported_operand_types(">=", (type(self), type(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 + return greater_than_or_equal_operation(+self, +other) def _is_strict_float(self) -> bool: """Check if the number is a float. diff --git a/tests/units/test_var.py b/tests/units/test_var.py index bfa8aa35a..e5f5bbb2a 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -1004,7 +1004,7 @@ def test_all_number_operations(): assert ( 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)" From e8dd0ae47db200f49cfec7b05099f04e8250923b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Brand=C3=A9ho?= Date: Wed, 15 Jan 2025 15:57:50 -0800 Subject: [PATCH 7/7] don't need node when --backend_only is used (#4641) --- reflex/reflex.py | 6 ++++-- reflex/utils/processes.py | 29 ++++++++++++++++++----------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/reflex/reflex.py b/reflex/reflex.py index 22fcb9fb8..b0f4ccd91 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -519,7 +519,9 @@ def deploy( if prerequisites.needs_reinit(frontend=True): _init(name=config.app_name, loglevel=loglevel) 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( app_name=app_name, export_fn=lambda zip_dest_dir, @@ -545,7 +547,7 @@ def deploy( loglevel=type(loglevel).INFO, # type: ignore token=token, project=project, - config_path=config_path, + **extra, ) diff --git a/reflex/utils/processes.py b/reflex/utils/processes.py index 871b5f323..3673b36b2 100644 --- a/reflex/utils/processes.py +++ b/reflex/utils/processes.py @@ -17,6 +17,7 @@ import typer from redis.exceptions import RedisError from reflex import constants +from reflex.config import environment 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: Exit: When attempting to run a command with a None value. """ - 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." - ) + # Check for invalid command first. if None in args: console.error(f"Invalid command: {args}") 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] = { **os.environ, - "PATH": os.pathsep.join( - [node_bin_path if node_bin_path else "", os.environ["PATH"]] - ), # type: ignore + "PATH": path_env, **kwargs.pop("env", {}), } + kwargs = { "env": env, "stderr": None if show_logs else subprocess.STDOUT,