diff --git a/reflex/config.py b/reflex/config.py index b3dc1b5474f..086b819939e 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -237,7 +237,7 @@ class BaseConfig: env_file: str | None = None # Whether to automatically create setters for state base vars - state_auto_setters: bool = True + state_auto_setters: bool | None = None # Whether to display the sticky "Built with Reflex" badge on all pages. show_built_with_reflex: bool | None = None diff --git a/reflex/state.py b/reflex/state.py index 908ac30a241..663bfe2645c 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -225,8 +225,20 @@ def __call__(self, *args: Any) -> EventSpec: EventHandlerValueError: If the given Var name is not a str NotImplementedError: If the setter for the given Var is async """ + from reflex.config import get_config from reflex.utils.exceptions import EventHandlerValueError + config = get_config() + if config.state_auto_setters is None: + console.deprecate( + feature_name="state_auto_setters defaulting to True", + reason="The default value will be changed to False in a future release. Set state_auto_setters explicitly or define setters explicitly. " + f"Used {self.state_cls.__name__}.setvar without defining it.", + deprecation_version="0.8.9", + removal_version="0.9.0", + dedupe=True, + ) + if args: if not isinstance(args[0], str): msg = f"Var name must be passed as a string, got {args[0]!r}" @@ -1036,7 +1048,7 @@ def _init_var(cls, name: str, prop: Var): ) raise VarTypeError(msg) cls._set_var(name, prop) - if cls.is_user_defined() and get_config().state_auto_setters: + if cls.is_user_defined() and get_config().state_auto_setters is not False: cls._create_setter(name, prop) cls._set_default_value(name, prop) @@ -1096,11 +1108,14 @@ def _set_var(cls, name: str, prop: Var): setattr(cls, name, prop) @classmethod - def _create_event_handler(cls, fn: Any): + def _create_event_handler( + cls, fn: Any, event_handler_cls: type[EventHandler] = EventHandler + ): """Create an event handler for the given function. Args: fn: The function to create an event handler for. + event_handler_cls: The event handler class to use. Returns: The event handler. @@ -1108,7 +1123,7 @@ def _create_event_handler(cls, fn: Any): # Check if function has stored event_actions from decorator event_actions = getattr(fn, "_rx_event_actions", {}) - return EventHandler( + return event_handler_cls( fn=fn, state_full_name=cls.get_full_name(), event_actions=event_actions ) @@ -1125,9 +1140,34 @@ def _create_setter(cls, name: str, prop: Var): name: The name of the var. prop: The var to create a setter for. """ + from reflex.config import get_config + + config = get_config() + _create_event_handler_kwargs = {} + + if config.state_auto_setters is None: + + class EventHandlerDeprecatedSetter(EventHandler): + def __call__(self, *args, **kwargs): + console.deprecate( + feature_name="state_auto_setters defaulting to True", + reason="The default value will be changed to False in a future release. Set state_auto_setters explicitly or define setters explicitly. " + f"Used {setter_name} in {cls.__name__} without defining it.", + deprecation_version="0.8.9", + removal_version="0.9.0", + dedupe=True, + ) + return super().__call__(*args, **kwargs) + + _create_event_handler_kwargs["event_handler_cls"] = ( + EventHandlerDeprecatedSetter + ) + setter_name = Var._get_setter_name_for_name(name) if setter_name not in cls.__dict__: - event_handler = cls._create_event_handler(prop._get_setter(name)) + event_handler = cls._create_event_handler( + prop._get_setter(name), **_create_event_handler_kwargs + ) cls.event_handlers[setter_name] = event_handler setattr(cls, setter_name, event_handler) diff --git a/reflex/utils/console.py b/reflex/utils/console.py index d33eef62ed3..9c25b3a1d00 100644 --- a/reflex/utils/console.py +++ b/reflex/utils/console.py @@ -7,6 +7,7 @@ import inspect import os import shutil +import sys import time from pathlib import Path from types import FrameType @@ -244,23 +245,38 @@ def warn(msg: str, *, dedupe: bool = False, **kwargs): print_to_log_file(f"[orange1]Warning: {msg}[/orange1]", **kwargs) -def _get_first_non_framework_frame() -> FrameType | None: +@once +def _exclude_paths_from_frame_info() -> list[Path]: + import importlib.util + import click + import granian + import socketio import typing_extensions import reflex as rx # Exclude utility modules that should never be the source of deprecated reflex usage. - exclude_modules = [click, rx, typing_extensions] + exclude_modules = [click, rx, typing_extensions, socketio, granian] + modules_paths = [file for m in exclude_modules if (file := m.__file__)] + [ + spec.origin + for m in [*sys.builtin_module_names, *sys.stdlib_module_names] + if (spec := importlib.util.find_spec(m)) and spec.origin + ] exclude_roots = [ p.parent.resolve() if (p := Path(file)).name == "__init__.py" else p.resolve() - for m in exclude_modules - if (file := m.__file__) + for file in modules_paths ] # Specifically exclude the reflex cli module. if reflex_bin := shutil.which(b"reflex"): exclude_roots.append(Path(reflex_bin.decode())) + return exclude_roots + + +def _get_first_non_framework_frame() -> FrameType | None: + exclude_roots = _exclude_paths_from_frame_info() + frame = inspect.currentframe() while frame := frame and frame.f_back: frame_path = Path(inspect.getfile(frame)).resolve() @@ -297,13 +313,13 @@ def deprecate( 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}" + 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('.').lstrip('. ')}. It will be completely " - f"removed in {removal_version}. ({loc})" + f"removed in {removal_version}.{loc}" ) if _LOG_LEVEL <= LogLevel.WARNING: print(f"[yellow]DeprecationWarning: {msg}[/yellow]", **kwargs) diff --git a/reflex/vars/base.py b/reflex/vars/base.py index 020d09aea05..e884fab23cb 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -930,6 +930,7 @@ def _get_setter(self, name: str) -> Callable[[BaseState, Any], None]: Returns: A function that that creates a setter for the var. """ + setter_name = Var._get_setter_name_for_name(name) def setter(state: Any, value: Any): """Get the setter for the var. @@ -951,7 +952,7 @@ def setter(state: Any, value: Any): setter.__annotations__["value"] = self._var_type - setter.__qualname__ = Var._get_setter_name_for_name(name) + setter.__qualname__ = setter_name return setter diff --git a/tests/integration/test_connection_banner.py b/tests/integration/test_connection_banner.py index c50a69e9474..6ff246cadfa 100644 --- a/tests/integration/test_connection_banner.py +++ b/tests/integration/test_connection_banner.py @@ -22,6 +22,10 @@ def ConnectionBanner(): class State(rx.State): foo: int = 0 + @rx.event + def set_foo(self, foo: int): + self.foo = foo + @rx.event async def delay(self): await asyncio.sleep(5) @@ -33,7 +37,7 @@ def index(): rx.button( "Increment", id="increment", - on_click=State.set_foo(State.foo + 1), # pyright: ignore [reportAttributeAccessIssue] + on_click=State.set_foo(State.foo + 1), ), rx.button("Delay", id="delay", on_click=State.delay), ) diff --git a/tests/integration/test_exception_handlers.py b/tests/integration/test_exception_handlers.py index 6430bc746a6..08c19415d95 100644 --- a/tests/integration/test_exception_handlers.py +++ b/tests/integration/test_exception_handlers.py @@ -28,6 +28,10 @@ class TestAppState(rx.State): react_error: bool = False + @rx.event + def set_react_error(self, value: bool): + self.react_error = value + def divide_by_number(self, number: int): """Divide by number and print the result. @@ -54,7 +58,7 @@ def index(): ), rx.button( "induce_react_error", - on_click=TestAppState.set_react_error(True), # pyright: ignore [reportAttributeAccessIssue] + on_click=TestAppState.set_react_error(True), id="induce-react-error-btn", ), rx.box( diff --git a/tests/integration/test_lifespan.py b/tests/integration/test_lifespan.py index aa164281b8e..aadbf3fdc75 100644 --- a/tests/integration/test_lifespan.py +++ b/tests/integration/test_lifespan.py @@ -53,6 +53,10 @@ async def lifespan_task(inc: int = 1): class LifespanState(rx.State): interval: int = 100 + @rx.event + def set_interval(self, interval: int): + self.interval = interval + @rx.var(cache=False) def task_global(self) -> int: return lifespan_task_global @@ -73,7 +77,7 @@ def index(): rx.moment( interval=LifespanState.interval, on_change=LifespanState.tick ), - on_click=LifespanState.set_interval( # pyright: ignore [reportAttributeAccessIssue] + on_click=LifespanState.set_interval( rx.cond(LifespanState.interval, 0, 100) ), id="toggle-tick",