Bläddra i källkod

Merge branch 'main' into add-validation-to-function-vars

Khaleel Al-Adhami 3 månader sedan
förälder
incheckning
24f341d125
79 ändrade filer med 765 tillägg och 397 borttagningar
  1. 13 5
      .github/workflows/benchmarks.yml
  2. 2 2
      .github/workflows/check_outdated_dependencies.yml
  3. 0 7
      .github/workflows/integration_app_harness.yml
  4. 1 1
      benchmarks/benchmark_package_size.py
  5. 3 3
      benchmarks/test_benchmark_compile_components.py
  6. 5 5
      benchmarks/test_benchmark_compile_pages.py
  7. 0 2
      poetry.lock
  8. 6 3
      pyproject.toml
  9. 1 4
      reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js
  10. 94 59
      reflex/app.py
  11. 3 3
      reflex/app_mixins/lifespan.py
  12. 0 3
      reflex/app_module_for_backend.py
  13. 1 1
      reflex/components/core/html.py
  14. 1 1
      reflex/components/core/upload.py
  15. 1 1
      reflex/components/datadisplay/code.py
  16. 5 3
      reflex/components/dynamic.py
  17. 13 3
      reflex/components/next/image.py
  18. 4 2
      reflex/components/next/image.pyi
  19. 1 1
      reflex/components/plotly/plotly.py
  20. 4 4
      reflex/components/radix/primitives/accordion.py
  21. 1 1
      reflex/components/radix/primitives/drawer.py
  22. 4 4
      reflex/components/radix/primitives/drawer.pyi
  23. 2 2
      reflex/components/radix/themes/components/icon_button.py
  24. 1 0
      reflex/components/radix/themes/components/icon_button.pyi
  25. 3 1
      reflex/components/radix/themes/components/tooltip.py
  26. 1 0
      reflex/components/radix/themes/components/tooltip.pyi
  27. 2 2
      reflex/components/recharts/general.py
  28. 2 2
      reflex/components/recharts/general.pyi
  29. 1 1
      reflex/components/recharts/polar.py
  30. 4 4
      reflex/components/recharts/recharts.py
  31. 1 1
      reflex/components/sonner/toast.py
  32. 4 7
      reflex/config.py
  33. 2 0
      reflex/constants/compiler.py
  34. 9 9
      reflex/constants/installer.py
  35. 1 1
      reflex/constants/style.py
  36. 4 4
      reflex/event.py
  37. 51 23
      reflex/experimental/client_state.py
  38. 4 4
      reflex/experimental/hooks.py
  39. 1 1
      reflex/experimental/layout.pyi
  40. 14 0
      reflex/reflex.py
  41. 23 24
      reflex/state.py
  42. 6 6
      reflex/testing.py
  43. 1 4
      reflex/utils/codespaces.py
  44. 10 10
      reflex/utils/exceptions.py
  45. 4 4
      reflex/utils/exec.py
  46. 166 3
      reflex/utils/prerequisites.py
  47. 1 1
      reflex/vars/base.py
  48. 2 2
      reflex/vars/function.py
  49. 3 3
      reflex/vars/number.py
  50. 20 9
      scripts/bun_install.sh
  51. 4 0
      scripts/install.ps1
  52. 0 30
      tests/integration/conftest.py
  53. 2 2
      tests/integration/test_background_task.py
  54. 1 1
      tests/integration/test_call_script.py
  55. 3 1
      tests/integration/test_client_storage.py
  56. 1 1
      tests/integration/test_component_state.py
  57. 1 1
      tests/integration/test_connection_banner.py
  58. 1 1
      tests/integration/test_deploy_url.py
  59. 1 1
      tests/integration/test_dynamic_routes.py
  60. 1 1
      tests/integration/test_event_actions.py
  61. 1 1
      tests/integration/test_event_chain.py
  62. 1 1
      tests/integration/test_exception_handlers.py
  63. 2 2
      tests/integration/test_form_submit.py
  64. 1 1
      tests/integration/test_input.py
  65. 1 1
      tests/integration/test_login_flow.py
  66. 1 1
      tests/integration/test_server_side_event.py
  67. 1 1
      tests/integration/test_upload.py
  68. 1 1
      tests/integration/test_var_operations.py
  69. 1 1
      tests/integration/tests_playwright/test_datetime_operations.py
  70. 1 1
      tests/integration/tests_playwright/test_table.py
  71. 1 30
      tests/units/conftest.py
  72. 1 1
      tests/units/middleware/test_hydrate_middleware.py
  73. 50 50
      tests/units/test_app.py
  74. 161 0
      tests/units/test_prerequisites.py
  75. 2 2
      tests/units/test_route.py
  76. 14 14
      tests/units/test_state.py
  77. 1 1
      tests/units/test_state_tree.py
  78. 1 1
      tests/units/test_testing.py
  79. 2 2
      tests/units/test_var.py

+ 13 - 5
.github/workflows/benchmarks.yml

@@ -81,18 +81,22 @@ jobs:
       matrix:
         # Show OS combos first in GUI
         os: [ubuntu-latest, windows-latest, macos-latest]
-        python-version: ['3.10.16', '3.11.11', '3.12.8']
+        python-version: ["3.10.16", "3.11.11", "3.12.8"]
         exclude:
           - os: windows-latest
-            python-version: '3.10.16'
+            python-version: "3.10.16"
+          - os: windows-latest
+            python-version: "3.11.11"
           # keep only one python version for MacOS
           - os: macos-latest
-            python-version: '3.10.16'
+            python-version: "3.10.16"
           - os: macos-latest
             python-version: "3.11.11"
         include:
           - os: windows-latest
-            python-version: '3.10.11'
+            python-version: "3.10.11"
+          - os: windows-latest
+            python-version: "3.11.9"
 
     runs-on: ${{ matrix.os }}
     steps:
@@ -155,7 +159,11 @@ jobs:
     runs-on: ${{ matrix.os }}
     steps:
       - uses: actions/checkout@v4
-
+      - name: Set up python
+        id: setup-python
+        uses: actions/setup-python@v5
+        with:
+          python-version: ${{ matrix.python-version }}
       - name: Install Poetry
         uses: snok/install-poetry@v1
         with:

+ 2 - 2
.github/workflows/check_outdated_dependencies.yml

@@ -55,7 +55,7 @@ jobs:
           path: reflex-web
       - name: Install Requirements for reflex-web
         working-directory: ./reflex-web
-        run: poetry run uv pip install -r requirements.txt
+        run: poetry run uv pip install $(grep -ivE "reflex " requirements.txt)
       - name: Install additional dependencies for DB access
         run: poetry run uv pip install psycopg
       - name: Init Website for reflex-web
@@ -73,7 +73,7 @@ jobs:
           echo "$outdated"
 
           # Ignore 3rd party dependencies that are not updated.
-          filtered_outdated=$(echo "$outdated" | grep -vE 'Package|@chakra-ui|lucide-react|@splinetool/runtime|ag-grid-react|framer-motion|react-markdown|remark-math|remark-gfm|rehype-katex|rehype-raw|remark-unwrap-images' || true)
+          filtered_outdated=$(echo "$outdated" | grep -vE 'Package|@chakra-ui|lucide-react|@splinetool/runtime|ag-grid-react|framer-motion|react-markdown|remark-math|remark-gfm|rehype-katex|rehype-raw|remark-unwrap-images|ag-grid' || true)
           no_extra=$(echo "$filtered_outdated" | grep -vE '\|\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-' || true)
 
 

+ 0 - 7
.github/workflows/integration_app_harness.yml

@@ -50,14 +50,7 @@ jobs:
       - run: poetry run uv pip install pyvirtualdisplay pillow pytest-split pytest-retry
       - name: Run app harness tests
         env:
-          SCREENSHOT_DIR: /tmp/screenshots/${{ matrix.state_manager }}/${{ matrix.python-version }}/${{ matrix.split_index }}
           REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }}
         run: |
           poetry run playwright install chromium
           poetry run pytest tests/integration --retries 3 --maxfail=5 --splits 2 --group ${{matrix.split_index}}
-      - uses: actions/upload-artifact@v4
-        name: Upload failed test screenshots
-        if: always()
-        with:
-          name: failed_test_screenshots
-          path: /tmp/screenshots

+ 1 - 1
benchmarks/benchmark_package_size.py

@@ -21,7 +21,7 @@ def get_package_size(venv_path: Path, os_name):
         ValueError: when venv does not exist or python version is None.
     """
     python_version = get_python_version(venv_path, os_name)
-    print("Python version:", python_version)  # noqa: T201
+    print("Python version:", python_version)
     if python_version is None:
         raise ValueError("Error: Failed to determine Python version.")
 

+ 3 - 3
benchmarks/test_benchmark_compile_components.py

@@ -122,7 +122,7 @@ def AppWithTenComponentsOnePage():
     def index() -> rx.Component:
         return rx.center(rx.vstack(*render_component(1)))
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
     app.add_page(index)
 
 
@@ -133,7 +133,7 @@ def AppWithHundredComponentOnePage():
     def index() -> rx.Component:
         return rx.center(rx.vstack(*render_component(100)))
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
     app.add_page(index)
 
 
@@ -144,7 +144,7 @@ def AppWithThousandComponentsOnePage():
     def index() -> rx.Component:
         return rx.center(rx.vstack(*render_component(1000)))
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
     app.add_page(index)
 
 

+ 5 - 5
benchmarks/test_benchmark_compile_pages.py

@@ -162,7 +162,7 @@ def AppWithOnePage():
             height="100vh",
         )
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
     app.add_page(index)
 
 
@@ -170,7 +170,7 @@ def AppWithTenPages():
     """A reflex app with 10 pages."""
     import reflex as rx
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
     render_multiple_pages(app, 10)
 
 
@@ -178,7 +178,7 @@ def AppWithHundredPages():
     """A reflex app with 100 pages."""
     import reflex as rx
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
     render_multiple_pages(app, 100)
 
 
@@ -186,7 +186,7 @@ def AppWithThousandPages():
     """A reflex app with Thousand pages."""
     import reflex as rx
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
     render_multiple_pages(app, 1000)
 
 
@@ -194,7 +194,7 @@ def AppWithTenThousandPages():
     """A reflex app with ten thousand pages."""
     import reflex as rx
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
     render_multiple_pages(app, 10000)
 
 

+ 0 - 2
poetry.lock

@@ -461,7 +461,6 @@ files = [
     {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"},
     {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"},
     {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"},
-    {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"},
     {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"},
     {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"},
     {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"},
@@ -472,7 +471,6 @@ files = [
     {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"},
     {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"},
     {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"},
-    {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"},
     {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"},
     {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"},
     {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"},

+ 6 - 3
pyproject.toml

@@ -87,15 +87,18 @@ reportIncompatibleVariableOverride = false
 target-version = "py39"
 output-format = "concise"
 lint.isort.split-on-trailing-comma = false
-lint.select = ["B", "C4", "D", "E", "ERA", "F", "FURB", "I", "PERF", "PTH", "RUF", "SIM", "T", "TRY", "W"]
+lint.select = ["B", "C4", "D", "E", "ERA", "F", "FURB", "I", "N", "PERF", "PTH", "RUF", "SIM", "T", "TRY", "W"]
 lint.ignore = ["B008", "D205", "E501", "F403", "SIM115", "RUF006", "RUF012", "TRY0"]
 lint.pydocstyle.convention = "google"
 
 [tool.ruff.lint.per-file-ignores]
 "__init__.py" = ["F401"]
-"tests/*.py" = ["D100", "D103", "D104", "B018", "PERF", "T"]
+"tests/*.py" = ["D100", "D103", "D104", "B018", "PERF", "T", "N"]
+"benchmarks/*.py" = ["D100", "D103", "D104", "B018", "PERF", "T", "N"]
 "reflex/.templates/*.py" = ["D100", "D103", "D104"]
-"*.pyi" = ["D301", "D415", "D417", "D418", "E742"]
+"*.pyi" = ["D301", "D415", "D417", "D418", "E742", "N"]
+"pyi_generator.py" = ["N802"]
+"reflex/constants/*.py" = ["N"]
 "*/blank.py" = ["I001"]
 
 [tool.pytest.ini_options]

+ 1 - 4
reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js

@@ -16,10 +16,7 @@ export default function RadixThemesColorModeProvider({ children }) {
     if (isDevMode) {
       const lastCompiledTimeInLocalStorage =
         localStorage.getItem("last_compiled_time");
-      if (
-        lastCompiledTimeInLocalStorage &&
-        lastCompiledTimeInLocalStorage !== lastCompiledTimeStamp
-      ) {
+      if (lastCompiledTimeInLocalStorage !== lastCompiledTimeStamp) {
         // on app startup, make sure the application color mode is persisted correctly.
         setTheme(defaultColorMode);
         localStorage.setItem("last_compiled_time", lastCompiledTimeStamp);

+ 94 - 59
reflex/app.py

@@ -251,36 +251,36 @@ class App(MiddlewareMixin, LifespanMixin):
     # Attributes to add to the html root tag of every page.
     html_custom_attrs: Optional[Dict[str, str]] = None
 
-    # A map from a route to an unevaluated page. PRIVATE.
-    unevaluated_pages: Dict[str, UnevaluatedPage] = dataclasses.field(
+    # A map from a route to an unevaluated page.
+    _unevaluated_pages: Dict[str, UnevaluatedPage] = dataclasses.field(
         default_factory=dict
     )
 
-    # A map from a page route to the component to render. Users should use `add_page`. PRIVATE.
-    pages: Dict[str, Component] = dataclasses.field(default_factory=dict)
+    # A map from a page route to the component to render. Users should use `add_page`.
+    _pages: Dict[str, Component] = dataclasses.field(default_factory=dict)
 
-    # The backend API object. PRIVATE.
-    api: FastAPI = None  # type: ignore
+    # The backend API object.
+    _api: FastAPI | None = None
 
-    # The state class to use for the app. PRIVATE.
-    state: Optional[Type[BaseState]] = None
+    # The state class to use for the app.
+    _state: Optional[Type[BaseState]] = None
 
     # Class to manage many client states.
     _state_manager: Optional[StateManager] = None
 
-    # Mapping from a route to event handlers to trigger when the page loads. PRIVATE.
-    load_events: Dict[str, List[IndividualEventType[[], Any]]] = dataclasses.field(
+    # Mapping from a route to event handlers to trigger when the page loads.
+    _load_events: Dict[str, List[IndividualEventType[[], Any]]] = dataclasses.field(
         default_factory=dict
     )
 
-    # Admin dashboard to view and manage the database. PRIVATE.
+    # Admin dashboard to view and manage the database.
     admin_dash: Optional[AdminDash] = None
 
-    # The async server name space. PRIVATE.
-    event_namespace: Optional[EventNamespace] = None
+    # The async server name space.
+    _event_namespace: Optional[EventNamespace] = None
 
-    # Background tasks that are currently running. PRIVATE.
-    background_tasks: Set[asyncio.Task] = dataclasses.field(default_factory=set)
+    # Background tasks that are currently running.
+    _background_tasks: Set[asyncio.Task] = dataclasses.field(default_factory=set)
 
     # Frontend Error Handler Function
     frontend_exception_handler: Callable[[Exception], None] = (
@@ -292,6 +292,24 @@ class App(MiddlewareMixin, LifespanMixin):
         [Exception], Union[EventSpec, List[EventSpec], None]
     ] = default_backend_exception_handler
 
+    @property
+    def api(self) -> FastAPI | None:
+        """Get the backend api.
+
+        Returns:
+            The backend api.
+        """
+        return self._api
+
+    @property
+    def event_namespace(self) -> EventNamespace | None:
+        """Get the event namespace.
+
+        Returns:
+            The event namespace.
+        """
+        return self._event_namespace
+
     def __post_init__(self):
         """Initialize the app.
 
@@ -311,7 +329,7 @@ class App(MiddlewareMixin, LifespanMixin):
             set_breakpoints(self.style.pop("breakpoints"))
 
         # Set up the API.
-        self.api = FastAPI(lifespan=self._run_lifespan_tasks)
+        self._api = FastAPI(lifespan=self._run_lifespan_tasks)
         self._add_cors()
         self._add_default_endpoints()
 
@@ -334,8 +352,8 @@ class App(MiddlewareMixin, LifespanMixin):
 
     def _enable_state(self) -> None:
         """Enable state for the app."""
-        if not self.state:
-            self.state = State
+        if not self._state:
+            self._state = State
             self._setup_state()
 
     def _setup_state(self) -> None:
@@ -344,13 +362,13 @@ class App(MiddlewareMixin, LifespanMixin):
         Raises:
             RuntimeError: If the socket server is invalid.
         """
-        if not self.state:
+        if not self._state:
             return
 
         config = get_config()
 
         # Set up the state manager.
-        self._state_manager = StateManager.create(state=self.state)
+        self._state_manager = StateManager.create(state=self._state)
 
         # Set up the Socket.IO AsyncServer.
         if not self.sio:
@@ -381,12 +399,13 @@ class App(MiddlewareMixin, LifespanMixin):
         namespace = config.get_event_namespace()
 
         # Create the event namespace and attach the main app. Not related to any paths.
-        self.event_namespace = EventNamespace(namespace, self)
+        self._event_namespace = EventNamespace(namespace, self)
 
         # Register the event namespace with the socket.
         self.sio.register_namespace(self.event_namespace)
         # Mount the socket app with the API.
-        self.api.mount(str(constants.Endpoint.EVENT), socket_app)
+        if self.api:
+            self.api.mount(str(constants.Endpoint.EVENT), socket_app)
 
         # Check the exception handlers
         self._validate_exception_handlers()
@@ -397,24 +416,35 @@ class App(MiddlewareMixin, LifespanMixin):
         Returns:
             The string representation of the app.
         """
-        return f"<App state={self.state.__name__ if self.state else None}>"
+        return f"<App state={self._state.__name__ if self._state else None}>"
 
     def __call__(self) -> FastAPI:
         """Run the backend api instance.
 
+        Raises:
+            ValueError: If the app has not been initialized.
+
         Returns:
             The backend api.
         """
+        if not self.api:
+            raise ValueError("The app has not been initialized.")
         return self.api
 
     def _add_default_endpoints(self):
         """Add default api endpoints (ping)."""
         # To test the server.
+        if not self.api:
+            return
+
         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)."""
+        if not self.api:
+            return
+
         if Upload.is_used:
             # To upload files.
             self.api.post(str(constants.Endpoint.UPLOAD))(upload(self))
@@ -432,6 +462,8 @@ class App(MiddlewareMixin, LifespanMixin):
 
     def _add_cors(self):
         """Add CORS middleware to the app."""
+        if not self.api:
+            return
         self.api.add_middleware(
             cors.CORSMiddleware,
             allow_credentials=True,
@@ -521,13 +553,13 @@ class App(MiddlewareMixin, LifespanMixin):
         # Check if the route given is valid
         verify_route_validity(route)
 
-        if route in self.unevaluated_pages and environment.RELOAD_CONFIG.is_set():
+        if route in self._unevaluated_pages and environment.RELOAD_CONFIG.is_set():
             # when the app is reloaded(typically for app harness tests), we should maintain
             # the latest render function of a route.This applies typically to decorated pages
             # since they are only added when app._compile is called.
-            self.unevaluated_pages.pop(route)
+            self._unevaluated_pages.pop(route)
 
-        if route in self.unevaluated_pages:
+        if route in self._unevaluated_pages:
             route_name = (
                 f"`{route}` or `/`"
                 if route == constants.PageNames.INDEX_ROUTE
@@ -540,15 +572,15 @@ class App(MiddlewareMixin, LifespanMixin):
 
         # Setup dynamic args for the route.
         # this state assignment is only required for tests using the deprecated state kwarg for App
-        state = self.state if self.state else State
+        state = self._state if self._state else State
         state.setup_dynamic_args(get_route_args(route))
 
         if on_load:
-            self.load_events[route] = (
+            self._load_events[route] = (
                 on_load if isinstance(on_load, list) else [on_load]
             )
 
-        self.unevaluated_pages[route] = UnevaluatedPage(
+        self._unevaluated_pages[route] = UnevaluatedPage(
             component=component,
             route=route,
             title=title,
@@ -563,10 +595,10 @@ class App(MiddlewareMixin, LifespanMixin):
 
         Args:
             route: The route of the page to compile.
-            save_page: If True, the compiled page is saved to self.pages.
+            save_page: If True, the compiled page is saved to self._pages.
         """
         component, enable_state = compiler.compile_unevaluated_page(
-            route, self.unevaluated_pages[route], self.state, self.style, self.theme
+            route, self._unevaluated_pages[route], self._state, self.style, self.theme
         )
 
         if enable_state:
@@ -575,7 +607,7 @@ class App(MiddlewareMixin, LifespanMixin):
         # Add the page.
         self._check_routes_conflict(route)
         if save_page:
-            self.pages[route] = component
+            self._pages[route] = component
 
     def get_load_events(self, route: str) -> list[IndividualEventType[[], Any]]:
         """Get the load events for a route.
@@ -589,7 +621,7 @@ class App(MiddlewareMixin, LifespanMixin):
         route = route.lstrip("/")
         if route == "":
             route = constants.PageNames.INDEX_ROUTE
-        return self.load_events.get(route, [])
+        return self._load_events.get(route, [])
 
     def _check_routes_conflict(self, new_route: str):
         """Verify if there is any conflict between the new route and any existing route.
@@ -613,7 +645,7 @@ class App(MiddlewareMixin, LifespanMixin):
             constants.RouteRegex.SINGLE_CATCHALL_SEGMENT,
             constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT,
         )
-        for route in self.pages:
+        for route in self._pages:
             replaced_route = replace_brackets_with_keywords(route)
             for rw, r, nr in zip(
                 replaced_route.split("/"), route.split("/"), new_route.split("/")
@@ -671,6 +703,9 @@ class App(MiddlewareMixin, LifespanMixin):
     def _setup_admin_dash(self):
         """Setup the admin dash."""
         # Get the admin dash.
+        if not self.api:
+            return
+
         admin_dash = self.admin_dash
 
         if admin_dash and admin_dash.models:
@@ -775,10 +810,10 @@ class App(MiddlewareMixin, LifespanMixin):
 
     def _setup_overlay_component(self):
         """If a State is not used and no overlay_component is specified, do not render the connection modal."""
-        if self.state is None and self.overlay_component is default_overlay_component:
+        if self._state is None and self.overlay_component is default_overlay_component:
             self.overlay_component = None
-        for k, component in self.pages.items():
-            self.pages[k] = self._add_overlay_to_component(component)
+        for k, component in self._pages.items():
+            self._pages[k] = self._add_overlay_to_component(component)
 
     def _add_error_boundary_to_component(self, component: Component) -> Component:
         if self.error_boundary is None:
@@ -790,14 +825,14 @@ class App(MiddlewareMixin, LifespanMixin):
 
     def _setup_error_boundary(self):
         """If a State is not used and no error_boundary is specified, do not render the error boundary."""
-        if self.state is None and self.error_boundary is default_error_boundary:
+        if self._state is None and self.error_boundary is default_error_boundary:
             self.error_boundary = None
 
-        for k, component in self.pages.items():
+        for k, component in self._pages.items():
             # Skip the 404 page
             if k == constants.Page404.SLUG:
                 continue
-            self.pages[k] = self._add_error_boundary_to_component(component)
+            self._pages[k] = self._add_error_boundary_to_component(component)
 
     def _apply_decorated_pages(self):
         """Add @rx.page decorated pages to the app.
@@ -823,11 +858,11 @@ class App(MiddlewareMixin, LifespanMixin):
         Raises:
             VarDependencyError: When a computed var has an invalid dependency.
         """
-        if not self.state:
+        if not self._state:
             return
 
         if not state:
-            state = self.state
+            state = self._state
 
         for var in state.computed_vars.values():
             if not var._cache:
@@ -853,13 +888,13 @@ class App(MiddlewareMixin, LifespanMixin):
         """
         from reflex.utils.exceptions import ReflexRuntimeError
 
-        self.pages = {}
+        self._pages = {}
 
         def get_compilation_time() -> str:
             return str(datetime.now().time()).split(".")[0]
 
         # Render a default 404 page if the user didn't supply one
-        if constants.Page404.SLUG not in self.unevaluated_pages:
+        if constants.Page404.SLUG not in self._unevaluated_pages:
             self.add_page(route=constants.Page404.SLUG)
 
         # Fix up the style.
@@ -877,7 +912,7 @@ class App(MiddlewareMixin, LifespanMixin):
 
         should_compile = self._should_compile()
 
-        for route in self.unevaluated_pages:
+        for route in self._unevaluated_pages:
             console.debug(f"Evaluating page: {route}")
             self._compile_page(route, save_page=should_compile)
 
@@ -904,7 +939,7 @@ class App(MiddlewareMixin, LifespanMixin):
         progress.start()
         task = progress.add_task(
             f"[{get_compilation_time()}] Compiling:",
-            total=len(self.pages)
+            total=len(self._pages)
             + fixed_pages_within_executor
             + adhoc_steps_without_executor,
         )
@@ -923,7 +958,7 @@ class App(MiddlewareMixin, LifespanMixin):
 
         # This has to happen before compiling stateful components as that
         # prevents recursive functions from reaching all components.
-        for component in self.pages.values():
+        for component in self._pages.values():
             # Add component._get_all_imports() to all_imports.
             all_imports.update(component._get_all_imports())
 
@@ -938,12 +973,12 @@ class App(MiddlewareMixin, LifespanMixin):
             stateful_components_path,
             stateful_components_code,
             page_components,
-        ) = compiler.compile_stateful_components(self.pages.values())
+        ) = compiler.compile_stateful_components(self._pages.values())
 
         progress.advance(task)
 
         # Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State.
-        if code_uses_state_contexts(stateful_components_code) and self.state is None:
+        if code_uses_state_contexts(stateful_components_code) and self._state is None:
             raise ReflexRuntimeError(
                 "To access rx.State in frontend components, at least one "
                 "subclass of rx.State must be defined in the app."
@@ -980,10 +1015,10 @@ class App(MiddlewareMixin, LifespanMixin):
                 max_workers=environment.REFLEX_COMPILE_THREADS.get() or None
             )
 
-        for route, component in zip(self.pages, page_components):
+        for route, component in zip(self._pages, page_components):
             ExecutorSafeFunctions.COMPONENTS[route] = component
 
-        ExecutorSafeFunctions.STATE = self.state
+        ExecutorSafeFunctions.STATE = self._state
 
         with executor:
             result_futures = []
@@ -993,7 +1028,7 @@ class App(MiddlewareMixin, LifespanMixin):
                 result_futures.append(f)
 
             # Compile the pre-compiled pages.
-            for route in self.pages:
+            for route in self._pages:
                 _submit_work(
                     ExecutorSafeFunctions.compile_page,
                     route,
@@ -1028,7 +1063,7 @@ class App(MiddlewareMixin, LifespanMixin):
 
         # Compile the contexts.
         compile_results.append(
-            compiler.compile_contexts(self.state, self.theme),
+            compiler.compile_contexts(self._state, self.theme),
         )
         if self.theme is not None:
             # Fix #2992 by removing the top-level appearance prop
@@ -1150,9 +1185,9 @@ class App(MiddlewareMixin, LifespanMixin):
                 )
 
         task = asyncio.create_task(_coro())
-        self.background_tasks.add(task)
+        self._background_tasks.add(task)
         # Clean up task from background_tasks set when complete.
-        task.add_done_callback(self.background_tasks.discard)
+        task.add_done_callback(self._background_tasks.discard)
         return task
 
     def _validate_exception_handlers(self):
@@ -1162,11 +1197,11 @@ class App(MiddlewareMixin, LifespanMixin):
             ValueError: If the custom exception handlers are invalid.
 
         """
-        FRONTEND_ARG_SPEC = {
+        frontend_arg_spec = {
             "exception": Exception,
         }
 
-        BACKEND_ARG_SPEC = {
+        backend_arg_spec = {
             "exception": Exception,
         }
 
@@ -1174,8 +1209,8 @@ class App(MiddlewareMixin, LifespanMixin):
             ["frontend", "backend"],
             [self.frontend_exception_handler, self.backend_exception_handler],
             [
-                FRONTEND_ARG_SPEC,
-                BACKEND_ARG_SPEC,
+                frontend_arg_spec,
+                backend_arg_spec,
             ],
         ):
             if hasattr(handler_fn, "__name__"):

+ 3 - 3
reflex/app_mixins/lifespan.py

@@ -12,7 +12,7 @@ from typing import Callable, Coroutine, Set, Union
 from fastapi import FastAPI
 
 from reflex.utils import console
-from reflex.utils.exceptions import InvalidLifespanTaskType
+from reflex.utils.exceptions import InvalidLifespanTaskTypeError
 
 from .mixin import AppMixin
 
@@ -64,10 +64,10 @@ class LifespanMixin(AppMixin):
             task_kwargs: The kwargs of the task.
 
         Raises:
-            InvalidLifespanTaskType: If the task is a generator function.
+            InvalidLifespanTaskTypeError: If the task is a generator function.
         """
         if inspect.isgeneratorfunction(task) or inspect.isasyncgenfunction(task):
-            raise InvalidLifespanTaskType(
+            raise InvalidLifespanTaskTypeError(
                 f"Task {task.__name__} of type generator must be decorated with contextlib.asynccontextmanager."
             )
 

+ 0 - 3
reflex/app_module_for_backend.py

@@ -5,14 +5,12 @@ Only the app attribute is explicitly exposed.
 from concurrent.futures import ThreadPoolExecutor
 
 from reflex import constants
-from reflex.utils import telemetry
 from reflex.utils.exec import is_prod_mode
 from reflex.utils.prerequisites import get_and_validate_app
 
 if constants.CompileVars.APP != "app":
     raise AssertionError("unexpected variable name for 'app'")
 
-telemetry.send("compile")
 app, app_module = get_and_validate_app(reload=False)
 # For py3.9 compatibility when redis is used, we MUST add any decorator pages
 # before compiling the app in a thread to avoid event loop error (REF-2172).
@@ -31,6 +29,5 @@ del app_module
 del compile_future
 del get_and_validate_app
 del is_prod_mode
-del telemetry
 del constants
 del ThreadPoolExecutor

+ 1 - 1
reflex/components/core/html.py

@@ -14,7 +14,7 @@ class Html(Div):
     """
 
     # The HTML to render.
-    dangerouslySetInnerHTML: Var[Dict[str, str]]
+    dangerouslySetInnerHTML: Var[Dict[str, str]]  # noqa: N815
 
     @classmethod
     def create(cls, *children, **props):

+ 1 - 1
reflex/components/core/upload.py

@@ -190,7 +190,7 @@ class GhostUpload(Fragment):
 class Upload(MemoizationLeaf):
     """A file upload component."""
 
-    library = "react-dropzone@14.2.10"
+    library = "react-dropzone@14.3.5"
 
     tag = ""
 

+ 1 - 1
reflex/components/datadisplay/code.py

@@ -382,7 +382,7 @@ for theme_name in dir(Theme):
 class CodeBlock(Component, MarkdownComponentMap):
     """A code block."""
 
-    library = "react-syntax-highlighter@15.6.0"
+    library = "react-syntax-highlighter@15.6.1"
 
     tag = "PrismAsyncLight"
 

+ 5 - 3
reflex/components/dynamic.py

@@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Union
 
 from reflex import constants
 from reflex.utils import imports
-from reflex.utils.exceptions import DynamicComponentMissingLibrary
+from reflex.utils.exceptions import DynamicComponentMissingLibraryError
 from reflex.utils.format import format_library_name
 from reflex.utils.serializers import serializer
 from reflex.vars import Var, get_unique_variable_name
@@ -36,13 +36,15 @@ def bundle_library(component: Union["Component", str]):
         component: The component to bundle the library with.
 
     Raises:
-        DynamicComponentMissingLibrary: Raised when a dynamic component is missing a library.
+        DynamicComponentMissingLibraryError: Raised when a dynamic component is missing a library.
     """
     if isinstance(component, str):
         bundled_libraries.add(component)
         return
     if component.library is None:
-        raise DynamicComponentMissingLibrary("Component must have a library to bundle.")
+        raise DynamicComponentMissingLibraryError(
+            "Component must have a library to bundle."
+        )
     bundled_libraries.add(format_library_name(component.library))
 
 

+ 13 - 3
reflex/components/next/image.py

@@ -3,11 +3,13 @@
 from typing import Any, Literal, Optional, Union
 
 from reflex.event import EventHandler, no_args_event_spec
-from reflex.utils import types
+from reflex.utils import console, types
 from reflex.vars.base import Var
 
 from .base import NextComponent
 
+DEFAULT_W_H = "100%"
+
 
 class Image(NextComponent):
     """Display an image."""
@@ -53,7 +55,7 @@ class Image(NextComponent):
     loading: Var[Literal["lazy", "eager"]]
 
     # A Data URL to be used as a placeholder image before the src image successfully loads. Only takes effect when combined with placeholder="blur".
-    blurDataURL: Var[str]
+    blur_data_url: Var[str]
 
     # Fires when the image has loaded.
     on_load: EventHandler[no_args_event_spec]
@@ -80,8 +82,16 @@ class Image(NextComponent):
         Returns:
             _type_: _description_
         """
+        if "blurDataURL" in props:
+            console.deprecate(
+                feature_name="blurDataURL",
+                reason="Use blur_data_url instead",
+                deprecation_version="0.7.0",
+                removal_version="0.8.0",
+            )
+            props["blur_data_url"] = props.pop("blurDataURL")
+
         style = props.get("style", {})
-        DEFAULT_W_H = "100%"
 
         def check_prop_type(prop_name, prop_value):
             if types.check_prop_in_allowed_types(prop_value, allowed_types=[int]):

+ 4 - 2
reflex/components/next/image.pyi

@@ -11,6 +11,8 @@ from reflex.vars.base import Var
 
 from .base import NextComponent
 
+DEFAULT_W_H = "100%"
+
 class Image(NextComponent):
     @overload
     @classmethod
@@ -30,7 +32,7 @@ class Image(NextComponent):
         loading: Optional[
             Union[Literal["eager", "lazy"], Var[Literal["eager", "lazy"]]]
         ] = None,
-        blurDataURL: Optional[Union[Var[str], str]] = None,
+        blur_data_url: Optional[Union[Var[str], str]] = None,
         style: Optional[Style] = None,
         key: Optional[Any] = None,
         id: Optional[Any] = None,
@@ -71,7 +73,7 @@ class Image(NextComponent):
             priority: When true, the image will be considered high priority and preload. Lazy loading is automatically disabled for images using priority.
             placeholder: A placeholder to use while the image is loading. Possible values are blur, empty, or data:image/.... Defaults to empty.
             loading: The loading behavior of the image. Defaults to lazy. Can hurt performance, recommended to use `priority` instead.
-            blurDataURL: A Data URL to be used as a placeholder image before the src image successfully loads. Only takes effect when combined with placeholder="blur".
+            blur_data_url: A Data URL to be used as a placeholder image before the src image successfully loads. Only takes effect when combined with placeholder="blur".
             on_load: Fires when the image has loaded.
             on_error: Fires when the image has an error.
             style: The style of the component.

+ 1 - 1
reflex/components/plotly/plotly.py

@@ -95,7 +95,7 @@ class Plotly(NoSSRComponent):
 
     library = "react-plotly.js@2.6.0"
 
-    lib_dependencies: List[str] = ["plotly.js@2.35.2"]
+    lib_dependencies: List[str] = ["plotly.js@2.35.3"]
 
     tag = "Plot"
 

+ 4 - 4
reflex/components/radix/primitives/accordion.py

@@ -485,11 +485,11 @@ to {
         Returns:
             The style of the component.
         """
-        slideDown = LiteralVar.create(
+        slide_down = LiteralVar.create(
             "${slideDown} var(--animation-duration) var(--animation-easing)",
         )
 
-        slideUp = LiteralVar.create(
+        slide_up = LiteralVar.create(
             "${slideUp} var(--animation-duration) var(--animation-easing)",
         )
 
@@ -503,8 +503,8 @@ to {
                 "display": "block",
                 "height": "var(--space-3)",
             },
-            "&[data-state='open']": {"animation": slideDown},
-            "&[data-state='closed']": {"animation": slideUp},
+            "&[data-state='open']": {"animation": slide_down},
+            "&[data-state='closed']": {"animation": slide_up},
             _inherited_variant_selector("classic"): {
                 "color": "var(--accent-contrast)",
             },

+ 1 - 1
reflex/components/radix/primitives/drawer.py

@@ -66,7 +66,7 @@ class DrawerRoot(DrawerComponent):
     scroll_lock_timeout: Var[int]
 
     # When `True`, it prevents scroll restoration. Defaults to `True`.
-    preventScrollRestoration: Var[bool]
+    prevent_scroll_restoration: Var[bool]
 
     # Enable background scaling, it requires container element with `vaul-drawer-wrapper` attribute to scale its background.
     should_scale_background: Var[bool]

+ 4 - 4
reflex/components/radix/primitives/drawer.pyi

@@ -81,7 +81,7 @@ class DrawerRoot(DrawerComponent):
         snap_points: Optional[List[Union[float, str]]] = None,
         fade_from_index: Optional[Union[Var[int], int]] = None,
         scroll_lock_timeout: Optional[Union[Var[int], int]] = None,
-        preventScrollRestoration: Optional[Union[Var[bool], bool]] = None,
+        prevent_scroll_restoration: Optional[Union[Var[bool], bool]] = None,
         should_scale_background: Optional[Union[Var[bool], bool]] = None,
         close_threshold: Optional[Union[Var[float], float]] = None,
         as_child: Optional[Union[Var[bool], bool]] = None,
@@ -129,7 +129,7 @@ class DrawerRoot(DrawerComponent):
             snap_points: Array of numbers from 0 to 100 that corresponds to % of the screen a given snap point should take up. Should go from least visible. Also Accept px values, which doesn't take screen height into account.
             fade_from_index: Index of a snapPoint from which the overlay fade should be applied. Defaults to the last snap point.
             scroll_lock_timeout: Duration for which the drawer is not draggable after scrolling content inside of the drawer. Defaults to 500ms
-            preventScrollRestoration: When `True`, it prevents scroll restoration. Defaults to `True`.
+            prevent_scroll_restoration: When `True`, it prevents scroll restoration. Defaults to `True`.
             should_scale_background: Enable background scaling, it requires container element with `vaul-drawer-wrapper` attribute to scale its background.
             close_threshold: Number between 0 and 1 that determines when the drawer should be closed.
             as_child: Change the default rendered element for the one passed as a child.
@@ -567,7 +567,7 @@ class Drawer(ComponentNamespace):
         snap_points: Optional[List[Union[float, str]]] = None,
         fade_from_index: Optional[Union[Var[int], int]] = None,
         scroll_lock_timeout: Optional[Union[Var[int], int]] = None,
-        preventScrollRestoration: Optional[Union[Var[bool], bool]] = None,
+        prevent_scroll_restoration: Optional[Union[Var[bool], bool]] = None,
         should_scale_background: Optional[Union[Var[bool], bool]] = None,
         close_threshold: Optional[Union[Var[float], float]] = None,
         as_child: Optional[Union[Var[bool], bool]] = None,
@@ -615,7 +615,7 @@ class Drawer(ComponentNamespace):
             snap_points: Array of numbers from 0 to 100 that corresponds to % of the screen a given snap point should take up. Should go from least visible. Also Accept px values, which doesn't take screen height into account.
             fade_from_index: Index of a snapPoint from which the overlay fade should be applied. Defaults to the last snap point.
             scroll_lock_timeout: Duration for which the drawer is not draggable after scrolling content inside of the drawer. Defaults to 500ms
-            preventScrollRestoration: When `True`, it prevents scroll restoration. Defaults to `True`.
+            prevent_scroll_restoration: When `True`, it prevents scroll restoration. Defaults to `True`.
             should_scale_background: Enable background scaling, it requires container element with `vaul-drawer-wrapper` attribute to scale its background.
             close_threshold: Number between 0 and 1 that determines when the drawer should be closed.
             as_child: Change the default rendered element for the one passed as a child.

+ 2 - 2
reflex/components/radix/themes/components/icon_button.py

@@ -22,6 +22,8 @@ from ..base import (
 
 LiteralButtonSize = Literal["1", "2", "3", "4"]
 
+RADIX_TO_LUCIDE_SIZE = {"1": 12, "2": 24, "3": 36, "4": 48}
+
 
 class IconButton(elements.Button, RadixLoadingProp, RadixThemesComponent):
     """A button designed specifically for usage with a single icon."""
@@ -72,8 +74,6 @@ class IconButton(elements.Button, RadixLoadingProp, RadixThemesComponent):
                 "IconButton requires a child icon. Pass a string as the first child or a rx.icon."
             )
         if "size" in props:
-            RADIX_TO_LUCIDE_SIZE = {"1": 12, "2": 24, "3": 36, "4": 48}
-
             if isinstance(props["size"], str):
                 children[0].size = RADIX_TO_LUCIDE_SIZE[props["size"]]
             else:

+ 1 - 0
reflex/components/radix/themes/components/icon_button.pyi

@@ -14,6 +14,7 @@ from reflex.vars.base import Var
 from ..base import RadixLoadingProp, RadixThemesComponent
 
 LiteralButtonSize = Literal["1", "2", "3", "4"]
+RADIX_TO_LUCIDE_SIZE = {"1": 12, "2": 24, "3": 36, "4": 48}
 
 class IconButton(elements.Button, RadixLoadingProp, RadixThemesComponent):
     @overload

+ 3 - 1
reflex/components/radix/themes/components/tooltip.py

@@ -28,6 +28,9 @@ LiteralStickyType = Literal[
 ]
 
 
+ARIA_LABEL_KEY = "aria_label"
+
+
 # The Tooltip inherits props from the Tooltip.Root, Tooltip.Portal, Tooltip.Content
 class Tooltip(RadixThemesComponent):
     """Floating element that provides a control with contextual information via pointer or focus."""
@@ -104,7 +107,6 @@ class Tooltip(RadixThemesComponent):
         Returns:
             The created component.
         """
-        ARIA_LABEL_KEY = "aria_label"
         if props.get(ARIA_LABEL_KEY) is not None:
             props[format.to_kebab_case(ARIA_LABEL_KEY)] = props.pop(ARIA_LABEL_KEY)
 

+ 1 - 0
reflex/components/radix/themes/components/tooltip.pyi

@@ -14,6 +14,7 @@ from ..base import RadixThemesComponent
 LiteralSideType = Literal["top", "right", "bottom", "left"]
 LiteralAlignType = Literal["start", "center", "end"]
 LiteralStickyType = Literal["partial", "always"]
+ARIA_LABEL_KEY = "aria_label"
 
 class Tooltip(RadixThemesComponent):
     @overload

+ 2 - 2
reflex/components/recharts/general.py

@@ -250,10 +250,10 @@ class Cell(Recharts):
     alias = "RechartsCell"
 
     # The presentation attribute of a rectangle in bar or a sector in pie.
-    fill: Var[str]
+    fill: Var[str | Color]
 
     # The presentation attribute of a rectangle in bar or a sector in pie.
-    stroke: Var[str]
+    stroke: Var[str | Color]
 
 
 responsive_container = ResponsiveContainer.create

+ 2 - 2
reflex/components/recharts/general.pyi

@@ -488,8 +488,8 @@ class Cell(Recharts):
     def create(  # type: ignore
         cls,
         *children,
-        fill: Optional[Union[Var[str], str]] = None,
-        stroke: Optional[Union[Var[str], str]] = None,
+        fill: Optional[Union[Color, Var[Union[Color, str]], str]] = None,
+        stroke: Optional[Union[Color, Var[Union[Color, str]], str]] = None,
         style: Optional[Style] = None,
         key: Optional[Any] = None,
         id: Optional[Any] = None,

+ 1 - 1
reflex/components/recharts/polar.py

@@ -73,7 +73,7 @@ class Pie(Recharts):
     data: Var[List[Dict[str, Any]]]
 
     # Valid children components
-    _valid_children: List[str] = ["Cell", "LabelList"]
+    _valid_children: List[str] = ["Cell", "LabelList", "Bare"]
 
     # Stoke color. Default: rx.color("accent", 9)
     stroke: Var[Union[str, Color]] = LiteralVar.create(Color("accent", 9))

+ 4 - 4
reflex/components/recharts/recharts.py

@@ -1,6 +1,6 @@
 """A component that wraps a recharts lib."""
 
-from typing import Dict, Literal
+from typing import Literal
 
 from reflex.components.component import Component, MemoizationLeaf, NoSSRComponent
 
@@ -8,16 +8,16 @@ from reflex.components.component import Component, MemoizationLeaf, NoSSRCompone
 class Recharts(Component):
     """A component that wraps a recharts lib."""
 
-    library = "recharts@2.13.0"
+    library = "recharts@2.15.0"
 
-    def _get_style(self) -> Dict:
+    def _get_style(self) -> dict:
         return {"wrapperStyle": self.style}
 
 
 class RechartsCharts(NoSSRComponent, MemoizationLeaf):
     """A component that wraps a recharts lib."""
 
-    library = "recharts@2.13.0"
+    library = "recharts@2.15.0"
 
 
 LiteralAnimationEasing = Literal["ease", "ease-in", "ease-out", "ease-in-out", "linear"]

+ 1 - 1
reflex/components/sonner/toast.py

@@ -167,7 +167,7 @@ class ToastProps(PropsBase, NoExtrasAllowedProps):
 class Toaster(Component):
     """A Toaster Component for displaying toast notifications."""
 
-    library: str = "sonner@1.7.1"
+    library: str = "sonner@1.7.2"
 
     tag = "Toaster"
 

+ 4 - 7
reflex/config.py

@@ -390,7 +390,7 @@ class EnvVar(Generic[T]):
             os.environ[self.name] = str(value)
 
 
-class env_var:  # type: ignore
+class env_var:  # type: ignore # noqa: N801
     """Descriptor for environment variables."""
 
     name: str
@@ -556,9 +556,6 @@ class EnvironmentVariables:
     # Arguments to pass to the app harness driver.
     APP_HARNESS_DRIVER_ARGS: EnvVar[str] = env_var("")
 
-    # Where to save screenshots when tests fail.
-    SCREENSHOT_DIR: EnvVar[Optional[Path]] = env_var(None)
-
     # Whether to check for outdated package versions.
     REFLEX_CHECK_LATEST_VERSION: EnvVar[bool] = env_var(True)
 
@@ -826,16 +823,16 @@ class Config(Base):
         if "api_url" not in self._non_default_attributes:
             # If running in Github Codespaces, override API_URL
             codespace_name = os.getenv("CODESPACE_NAME")
-            GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = os.getenv(
+            github_codespaces_port_forwarding_domain = os.getenv(
                 "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"
             )
             # If running on Replit.com interactively, override API_URL to ensure we maintain the backend_port
             replit_dev_domain = os.getenv("REPLIT_DEV_DOMAIN")
             backend_port = kwargs.get("backend_port", self.backend_port)
-            if codespace_name and GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN:
+            if codespace_name and github_codespaces_port_forwarding_domain:
                 self.api_url = (
                     f"https://{codespace_name}-{kwargs.get('backend_port', self.backend_port)}"
-                    f".{GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
+                    f".{github_codespaces_port_forwarding_domain}"
                 )
             elif replit_dev_domain and backend_port:
                 self.api_url = f"https://{replit_dev_domain}:{backend_port}"

+ 2 - 0
reflex/constants/compiler.py

@@ -28,6 +28,8 @@ class Ext(SimpleNamespace):
     ZIP = ".zip"
     # The extension for executable files on Windows.
     EXE = ".exe"
+    # The extension for markdown files.
+    MD = ".md"
 
 
 class CompileVars(SimpleNamespace):

+ 9 - 9
reflex/constants/installer.py

@@ -37,10 +37,10 @@ class Bun(SimpleNamespace):
     """Bun constants."""
 
     # The Bun version.
-    VERSION = "1.1.29"
+    VERSION = "1.2.0"
 
     # Min Bun Version
-    MIN_VERSION = "0.7.0"
+    MIN_VERSION = "1.1.0"
 
     # URL to bun install script.
     INSTALL_URL = "https://raw.githubusercontent.com/reflex-dev/reflex/main/scripts/bun_install.sh"
@@ -178,21 +178,21 @@ class PackageJson(SimpleNamespace):
     PATH = "package.json"
 
     DEPENDENCIES = {
-        "@babel/standalone": "7.26.0",
-        "@emotion/react": "11.13.3",
-        "axios": "1.7.7",
+        "@babel/standalone": "7.26.6",
+        "@emotion/react": "11.14.0",
+        "axios": "1.7.9",
         "json5": "2.2.3",
-        "next": "15.1.4",
+        "next": "15.1.6",
         "next-sitemap": "4.2.3",
-        "next-themes": "0.4.3",
+        "next-themes": "0.4.4",
         "react": "18.3.1",
         "react-dom": "18.3.1",
-        "react-focus-lock": "2.13.2",
+        "react-focus-lock": "2.13.5",
         "socket.io-client": "4.8.1",
         "universal-cookie": "7.2.2",
     }
     DEV_DEPENDENCIES = {
         "autoprefixer": "10.4.20",
-        "postcss": "8.4.49",
+        "postcss": "8.5.1",
         "postcss-import": "16.1.0",
     }

+ 1 - 1
reflex/constants/style.py

@@ -7,7 +7,7 @@ class Tailwind(SimpleNamespace):
     """Tailwind constants."""
 
     # The Tailwindcss version
-    VERSION = "tailwindcss@3.4.15"
+    VERSION = "tailwindcss@3.4.17"
     # The Tailwind config.
     CONFIG = "tailwind.config.js"
     # Default Tailwind content paths

+ 4 - 4
reflex/event.py

@@ -536,10 +536,10 @@ class JavasciptKeyboardEvent:
     """Interface for a Javascript KeyboardEvent https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent."""
 
     key: str = ""
-    altKey: bool = False
-    ctrlKey: bool = False
-    metaKey: bool = False
-    shiftKey: bool = False
+    altKey: bool = False  # noqa: N815
+    ctrlKey: bool = False  # noqa: N815
+    metaKey: bool = False  # noqa: N815
+    shiftKey: bool = False  # noqa: N815
 
 
 def input_event(e: Var[JavascriptInputEvent]) -> Tuple[Var[str]]:

+ 51 - 23
reflex/experimental/client_state.py

@@ -34,6 +34,18 @@ def _client_state_ref(var_name: str) -> str:
     return f"refs['_client_state_{var_name}']"
 
 
+def _client_state_ref_dict(var_name: str) -> str:
+    """Get the ref path for a ClientStateVar.
+
+    Args:
+        var_name: The name of the variable.
+
+    Returns:
+        An accessor for ClientStateVar ref as a string.
+    """
+    return f"refs['_client_state_dict_{var_name}']"
+
+
 @dataclasses.dataclass(
     eq=False,
     frozen=True,
@@ -115,10 +127,41 @@ class ClientStateVar(Var):
             "react": [ImportVar(tag="useState"), ImportVar(tag="useId")],
         }
         if global_ref:
-            hooks[f"{_client_state_ref(var_name)} ??= {{}}"] = None
-            hooks[f"{_client_state_ref(setter_name)} ??= {{}}"] = None
-            hooks[f"{_client_state_ref(var_name)}[{id_name}] = {var_name}"] = None
-            hooks[f"{_client_state_ref(setter_name)}[{id_name}] = {setter_name}"] = None
+            arg_name = get_unique_variable_name()
+            func = ArgsFunctionOperationBuilder.create(
+                args_names=(arg_name,),
+                return_expr=Var("Array.prototype.forEach.call")
+                .to(FunctionVar)
+                .call(
+                    (
+                        Var("Object.values")
+                        .to(FunctionVar)
+                        .call(Var(_client_state_ref_dict(setter_name)))
+                        .to(list)
+                        .to(list)
+                    )
+                    + Var.create(
+                        [
+                            Var(
+                                f"(value) => {{ {_client_state_ref(var_name)} = value; }}"
+                            )
+                        ]
+                    ).to(list),
+                    ArgsFunctionOperationBuilder.create(
+                        args_names=("setter",),
+                        return_expr=Var("setter").to(FunctionVar).call(Var(arg_name)),
+                    ),
+                ),
+            )
+
+            hooks[f"{_client_state_ref(setter_name)} = {func!s}"] = None
+            hooks[f"{_client_state_ref(var_name)} ??= {var_name!s}"] = None
+            hooks[f"{_client_state_ref_dict(var_name)} ??= {{}}"] = None
+            hooks[f"{_client_state_ref_dict(setter_name)} ??= {{}}"] = None
+            hooks[f"{_client_state_ref_dict(var_name)}[{id_name}] = {var_name}"] = None
+            hooks[
+                f"{_client_state_ref_dict(setter_name)}[{id_name}] = {setter_name}"
+            ] = None
             imports.update(_refs_import)
         return cls(
             _js_expr="",
@@ -150,7 +193,7 @@ class ClientStateVar(Var):
         return (
             Var(
                 _js_expr=(
-                    _client_state_ref(self._getter_name) + f"[{self._id_name}]"
+                    _client_state_ref_dict(self._getter_name) + f"[{self._id_name}]"
                     if self._global_ref
                     else self._getter_name
                 ),
@@ -179,26 +222,11 @@ class ClientStateVar(Var):
         """
         _var_data = VarData(imports=_refs_import if self._global_ref else {})
 
-        arg_name = get_unique_variable_name()
         setter = (
-            ArgsFunctionOperationBuilder.create(
-                args_names=(arg_name,),
-                return_expr=Var("Array.prototype.forEach.call")
-                .to(FunctionVar)
-                .call(
-                    Var("Object.values")
-                    .to(FunctionVar)
-                    .call(Var(_client_state_ref(self._setter_name))),
-                    ArgsFunctionOperationBuilder.create(
-                        args_names=("setter",),
-                        return_expr=Var("setter").to(FunctionVar).call(Var(arg_name)),
-                    ),
-                ),
-                _var_data=_var_data,
-            )
+            Var(_client_state_ref(self._setter_name))
             if self._global_ref
-            else Var(self._setter_name, _var_data=_var_data).to(FunctionVar)
-        )
+            else Var(self._setter_name, _var_data=_var_data)
+        ).to(FunctionVar)
 
         if value is not NoValue:
             # This is a hack to make it work like an EventSpec taking an arg

+ 4 - 4
reflex/experimental/hooks.py

@@ -26,7 +26,7 @@ def const(name, value) -> Var:
     return Var(_js_expr=f"const {name} = {value}")
 
 
-def useCallback(func, deps) -> Var:
+def useCallback(func, deps) -> Var:  # noqa: N802
     """Create a useCallback hook with a function and dependencies.
 
     Args:
@@ -42,7 +42,7 @@ def useCallback(func, deps) -> Var:
     )
 
 
-def useContext(context) -> Var:
+def useContext(context) -> Var:  # noqa: N802
     """Create a useContext hook with a context.
 
     Args:
@@ -57,7 +57,7 @@ def useContext(context) -> Var:
     )
 
 
-def useRef(default) -> Var:
+def useRef(default) -> Var:  # noqa: N802
     """Create a useRef hook with a default value.
 
     Args:
@@ -72,7 +72,7 @@ def useRef(default) -> Var:
     )
 
 
-def useState(var_name, default=None) -> Var:
+def useState(var_name, default=None) -> Var:  # noqa: N802
     """Create a useState hook with a variable name and setter name.
 
     Args:

+ 1 - 1
reflex/experimental/layout.pyi

@@ -109,7 +109,7 @@ class DrawerSidebar(DrawerRoot):
         snap_points: Optional[List[Union[float, str]]] = None,
         fade_from_index: Optional[Union[Var[int], int]] = None,
         scroll_lock_timeout: Optional[Union[Var[int], int]] = None,
-        preventScrollRestoration: Optional[Union[Var[bool], bool]] = None,
+        prevent_scroll_restoration: Optional[Union[Var[bool], bool]] = None,
         should_scale_background: Optional[Union[Var[bool], bool]] = None,
         close_threshold: Optional[Union[Var[float], float]] = None,
         as_child: Optional[Union[Var[bool], bool]] = None,

+ 14 - 0
reflex/reflex.py

@@ -573,6 +573,20 @@ def deploy(
     )
 
 
+@cli.command()
+def rename(
+    new_name: str = typer.Argument(..., help="The new name for the app."),
+    loglevel: constants.LogLevel = typer.Option(
+        config.loglevel, help="The log level to use."
+    ),
+):
+    """Rename the app in the current directory."""
+    from reflex.utils import prerequisites
+
+    prerequisites.validate_app_name(new_name)
+    prerequisites.rename_app(new_name, loglevel)
+
+
 cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
 cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.")
 cli.add_typer(

+ 23 - 24
reflex/state.py

@@ -93,14 +93,14 @@ from reflex.event import (
 )
 from reflex.utils import console, format, path_ops, prerequisites, types
 from reflex.utils.exceptions import (
-    ComputedVarShadowsBaseVars,
-    ComputedVarShadowsStateVar,
-    DynamicComponentInvalidSignature,
-    DynamicRouteArgShadowsStateVar,
-    EventHandlerShadowsBuiltInStateMethod,
+    ComputedVarShadowsBaseVarsError,
+    ComputedVarShadowsStateVarError,
+    DynamicComponentInvalidSignatureError,
+    DynamicRouteArgShadowsStateVarError,
+    EventHandlerShadowsBuiltInStateMethodError,
     ImmutableStateError,
     InvalidLockWarningThresholdError,
-    InvalidStateManagerMode,
+    InvalidStateManagerModeError,
     LockExpiredError,
     ReflexRuntimeError,
     SetUndefinedStateVarError,
@@ -815,7 +815,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
         """Check for shadow methods and raise error if any.
 
         Raises:
-            EventHandlerShadowsBuiltInStateMethod: When an event handler shadows an inbuilt state method.
+            EventHandlerShadowsBuiltInStateMethodError: When an event handler shadows an inbuilt state method.
         """
         overridden_methods = set()
         state_base_functions = cls._get_base_functions()
@@ -829,7 +829,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
                 overridden_methods.add(method.__name__)
 
         for method_name in overridden_methods:
-            raise EventHandlerShadowsBuiltInStateMethod(
+            raise EventHandlerShadowsBuiltInStateMethodError(
                 f"The event handler name `{method_name}` shadows a builtin State method; use a different name instead"
             )
 
@@ -838,11 +838,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
         """Check for shadow base vars and raise error if any.
 
         Raises:
-            ComputedVarShadowsBaseVars: When a computed var shadows a base var.
+            ComputedVarShadowsBaseVarsError: When a computed var shadows a base var.
         """
         for computed_var_ in cls._get_computed_vars():
             if computed_var_._js_expr in cls.__annotations__:
-                raise ComputedVarShadowsBaseVars(
+                raise ComputedVarShadowsBaseVarsError(
                     f"The computed var name `{computed_var_._js_expr}` shadows a base var in {cls.__module__}.{cls.__name__}; use a different name instead"
                 )
 
@@ -851,14 +851,14 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
         """Check for shadow computed vars and raise error if any.
 
         Raises:
-            ComputedVarShadowsStateVar: When a computed var shadows another.
+            ComputedVarShadowsStateVarError: When a computed var shadows another.
         """
         for name, cv in cls.__dict__.items():
             if not is_computed_var(cv):
                 continue
             name = cv._js_expr
             if name in cls.inherited_vars or name in cls.inherited_backend_vars:
-                raise ComputedVarShadowsStateVar(
+                raise ComputedVarShadowsStateVarError(
                     f"The computed var name `{cv._js_expr}` shadows a var in {cls.__module__}.{cls.__name__}; use a different name instead"
                 )
 
@@ -1218,14 +1218,14 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
             args: a dict of args
 
         Raises:
-            DynamicRouteArgShadowsStateVar: If a dynamic arg is shadowing an existing var.
+            DynamicRouteArgShadowsStateVarError: If a dynamic arg is shadowing an existing var.
         """
         for arg in args:
             if (
                 arg in cls.computed_vars
                 and not isinstance(cls.computed_vars[arg], DynamicRouteVar)
             ) or arg in cls.base_vars:
-                raise DynamicRouteArgShadowsStateVar(
+                raise DynamicRouteArgShadowsStateVarError(
                     f"Dynamic route arg '{arg}' is shadowing an existing var in {cls.__module__}.{cls.__name__}"
                 )
         for substate in cls.get_substates():
@@ -2353,8 +2353,7 @@ def dynamic(func: Callable[[T], Component]):
         The dynamically generated component.
 
     Raises:
-        DynamicComponentInvalidSignature: If the function does not have exactly one parameter.
-        DynamicComponentInvalidSignature: If the function does not have a type hint for the state class.
+        DynamicComponentInvalidSignatureError: If the function does not have exactly one parameter or a type hint for the state class.
     """
     number_of_parameters = len(inspect.signature(func).parameters)
 
@@ -2366,12 +2365,12 @@ def dynamic(func: Callable[[T], Component]):
     values = list(func_signature.values())
 
     if number_of_parameters != 1:
-        raise DynamicComponentInvalidSignature(
+        raise DynamicComponentInvalidSignatureError(
             "The function must have exactly one parameter, which is the state class."
         )
 
     if len(values) != 1:
-        raise DynamicComponentInvalidSignature(
+        raise DynamicComponentInvalidSignatureError(
             "You must provide a type hint for the state class in the function."
         )
 
@@ -2878,7 +2877,7 @@ class StateManager(Base, ABC):
             state: The state class to use.
 
         Raises:
-            InvalidStateManagerMode: If the state manager mode is invalid.
+            InvalidStateManagerModeError: If the state manager mode is invalid.
 
         Returns:
             The state manager (either disk, memory or redis).
@@ -2901,7 +2900,7 @@ class StateManager(Base, ABC):
                     lock_expiration=config.redis_lock_expiration,
                     lock_warning_threshold=config.redis_lock_warning_threshold,
                 )
-        raise InvalidStateManagerMode(
+        raise InvalidStateManagerModeError(
             f"Expected one of: DISK, MEMORY, REDIS, got {config.state_manager_mode}"
         )
 
@@ -4057,10 +4056,10 @@ def serialize_mutable_proxy(mp: MutableProxy):
     return mp.__wrapped__
 
 
-_orig_json_JSONEncoder_default = json.JSONEncoder.default
+_orig_json_encoder_default = json.JSONEncoder.default
 
 
-def _json_JSONEncoder_default_wrapper(self: json.JSONEncoder, o: Any) -> Any:
+def _json_encoder_default_wrapper(self: json.JSONEncoder, o: Any) -> Any:
     """Wrap JSONEncoder.default to handle MutableProxy objects.
 
     Args:
@@ -4074,10 +4073,10 @@ def _json_JSONEncoder_default_wrapper(self: json.JSONEncoder, o: Any) -> Any:
         return o.__wrapped__
     except AttributeError:
         pass
-    return _orig_json_JSONEncoder_default(self, o)
+    return _orig_json_encoder_default(self, o)
 
 
-json.JSONEncoder.default = _json_JSONEncoder_default_wrapper
+json.JSONEncoder.default = _json_encoder_default_wrapper
 
 
 class ImmutableMutableProxy(MutableProxy):

+ 6 - 6
reflex/testing.py

@@ -85,7 +85,7 @@ else:
 
 
 # borrowed from py3.11
-class chdir(contextlib.AbstractContextManager):
+class chdir(contextlib.AbstractContextManager):  # noqa: N801
     """Non thread-safe context manager to change the current working directory."""
 
     def __init__(self, path):
@@ -296,9 +296,9 @@ class AppHarness:
             raise RuntimeError("App was not initialized.")
         if isinstance(self.app_instance._state_manager, StateManagerRedis):
             # Create our own redis connection for testing.
-            if self.app_instance.state is None:
+            if self.app_instance._state is None:
                 raise RuntimeError("App state is not initialized.")
-            self.state_manager = StateManagerRedis.create(self.app_instance.state)
+            self.state_manager = StateManagerRedis.create(self.app_instance._state)
         else:
             self.state_manager = self.app_instance._state_manager
 
@@ -326,7 +326,7 @@ class AppHarness:
         return _shutdown_redis
 
     def _start_backend(self, port=0):
-        if self.app_instance is None:
+        if self.app_instance is None or self.app_instance.api is None:
             raise RuntimeError("App was not initialized.")
         self.backend = uvicorn.Server(
             uvicorn.Config(
@@ -355,12 +355,12 @@ class AppHarness:
                 self.app_instance.state_manager,
                 StateManagerRedis,
             )
-            and self.app_instance.state is not None
+            and self.app_instance._state is not None
         ):
             with contextlib.suppress(RuntimeError):
                 await self.app_instance.state_manager.close()
             self.app_instance._state_manager = StateManagerRedis.create(
-                state=self.app_instance.state,
+                state=self.app_instance._state,
             )
             if not isinstance(self.app_instance.state_manager, StateManagerRedis):
                 raise RuntimeError("Failed to reset state manager.")

+ 1 - 4
reflex/utils/codespaces.py

@@ -42,10 +42,7 @@ def codespaces_port_forwarding_domain() -> str | None:
     Returns:
         The domain for port forwarding in Github Codespaces, or None if not running in Codespaces.
     """
-    GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = os.getenv(
-        "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"
-    )
-    return GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN
+    return os.getenv("GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN")
 
 
 def is_running_in_codespaces() -> bool:

+ 10 - 10
reflex/utils/exceptions.py

@@ -11,7 +11,7 @@ class ConfigError(ReflexError):
     """Custom exception for config related errors."""
 
 
-class InvalidStateManagerMode(ReflexError, ValueError):
+class InvalidStateManagerModeError(ReflexError, ValueError):
     """Raised when an invalid state manager mode is provided."""
 
 
@@ -143,35 +143,35 @@ class EventFnArgMismatchError(ReflexError, TypeError):
     """Raised when the number of args required by an event handler is more than provided by the event trigger."""
 
 
-class DynamicRouteArgShadowsStateVar(ReflexError, NameError):
+class DynamicRouteArgShadowsStateVarError(ReflexError, NameError):
     """Raised when a dynamic route arg shadows a state var."""
 
 
-class ComputedVarShadowsStateVar(ReflexError, NameError):
+class ComputedVarShadowsStateVarError(ReflexError, NameError):
     """Raised when a computed var shadows a state var."""
 
 
-class ComputedVarShadowsBaseVars(ReflexError, NameError):
+class ComputedVarShadowsBaseVarsError(ReflexError, NameError):
     """Raised when a computed var shadows a base var."""
 
 
-class EventHandlerShadowsBuiltInStateMethod(ReflexError, NameError):
+class EventHandlerShadowsBuiltInStateMethodError(ReflexError, NameError):
     """Raised when an event handler shadows a built-in state method."""
 
 
-class GeneratedCodeHasNoFunctionDefs(ReflexError):
+class GeneratedCodeHasNoFunctionDefsError(ReflexError):
     """Raised when refactored code generated with flexgen has no functions defined."""
 
 
-class PrimitiveUnserializableToJSON(ReflexError, ValueError):
+class PrimitiveUnserializableToJSONError(ReflexError, ValueError):
     """Raised when a primitive type is unserializable to JSON. Usually with NaN and Infinity."""
 
 
-class InvalidLifespanTaskType(ReflexError, TypeError):
+class InvalidLifespanTaskTypeError(ReflexError, TypeError):
     """Raised when an invalid task type is registered as a lifespan task."""
 
 
-class DynamicComponentMissingLibrary(ReflexError, ValueError):
+class DynamicComponentMissingLibraryError(ReflexError, ValueError):
     """Raised when a dynamic component is missing a library."""
 
 
@@ -187,7 +187,7 @@ class EnvironmentVarValueError(ReflexError, ValueError):
     """Raised when an environment variable is set to an invalid value."""
 
 
-class DynamicComponentInvalidSignature(ReflexError, TypeError):
+class DynamicComponentInvalidSignatureError(ReflexError, TypeError):
     """Raised when a dynamic component has an invalid signature."""
 
 

+ 4 - 4
reflex/utils/exec.py

@@ -364,11 +364,11 @@ def run_uvicorn_backend_prod(host, port, loglevel):
 
     app_module = get_app_module()
 
-    RUN_BACKEND_PROD = f"gunicorn --worker-class {config.gunicorn_worker_class} --max-requests {config.gunicorn_max_requests} --max-requests-jitter {config.gunicorn_max_requests_jitter} --preload --timeout {config.timeout} --log-level critical".split()
-    RUN_BACKEND_PROD_WINDOWS = f"uvicorn --limit-max-requests {config.gunicorn_max_requests} --timeout-keep-alive {config.timeout}".split()
+    run_backend_prod = f"gunicorn --worker-class {config.gunicorn_worker_class} --max-requests {config.gunicorn_max_requests} --max-requests-jitter {config.gunicorn_max_requests_jitter} --preload --timeout {config.timeout} --log-level critical".split()
+    run_backend_prod_windows = f"uvicorn --limit-max-requests {config.gunicorn_max_requests} --timeout-keep-alive {config.timeout}".split()
     command = (
         [
-            *RUN_BACKEND_PROD_WINDOWS,
+            *run_backend_prod_windows,
             "--host",
             host,
             "--port",
@@ -377,7 +377,7 @@ def run_uvicorn_backend_prod(host, port, loglevel):
         ]
         if constants.IS_WINDOWS
         else [
-            *RUN_BACKEND_PROD,
+            *run_backend_prod,
             "--bind",
             f"{host}:{port}",
             "--threads",

+ 166 - 3
reflex/utils/prerequisites.py

@@ -7,6 +7,7 @@ import dataclasses
 import functools
 import importlib
 import importlib.metadata
+import importlib.util
 import json
 import os
 import platform
@@ -37,7 +38,7 @@ from reflex.compiler import templates
 from reflex.config import Config, environment, get_config
 from reflex.utils import console, net, path_ops, processes, redir
 from reflex.utils.exceptions import (
-    GeneratedCodeHasNoFunctionDefs,
+    GeneratedCodeHasNoFunctionDefsError,
     SystemPackageMissingError,
 )
 from reflex.utils.format import format_library_name
@@ -463,6 +464,167 @@ def validate_app_name(app_name: str | None = None) -> str:
     return app_name
 
 
+def rename_path_up_tree(full_path: str | Path, old_name: str, new_name: str) -> Path:
+    """Rename all instances of `old_name` in the path (file and directories) to `new_name`.
+    The renaming stops when we reach the directory containing `rxconfig.py`.
+
+    Args:
+        full_path: The full path to start renaming from.
+        old_name: The name to be replaced.
+        new_name: The replacement name.
+
+    Returns:
+         The updated path after renaming.
+    """
+    current_path = Path(full_path)
+    new_path = None
+
+    while True:
+        directory, base = current_path.parent, current_path.name
+        # Stop renaming when we reach the root dir (which contains rxconfig.py)
+        if current_path.is_dir() and (current_path / "rxconfig.py").exists():
+            new_path = current_path
+            break
+
+        if old_name == base.removesuffix(constants.Ext.PY):
+            new_base = base.replace(old_name, new_name)
+            new_path = directory / new_base
+            current_path.rename(new_path)
+            console.debug(f"Renamed {current_path} -> {new_path}")
+            current_path = new_path
+        else:
+            new_path = current_path
+
+        # Move up the directory tree
+        current_path = directory
+
+    return new_path
+
+
+def rename_app(new_app_name: str, loglevel: constants.LogLevel):
+    """Rename the app directory.
+
+    Args:
+        new_app_name: The new name for the app.
+        loglevel: The log level to use.
+
+    Raises:
+        Exit: If the command is not ran in the root dir or the app module cannot be imported.
+    """
+    # Set the log level.
+    console.set_log_level(loglevel)
+
+    if not constants.Config.FILE.exists():
+        console.error(
+            "No rxconfig.py found. Make sure you are in the root directory of your app."
+        )
+        raise typer.Exit(1)
+
+    sys.path.insert(0, str(Path.cwd()))
+
+    config = get_config()
+    module_path = importlib.util.find_spec(config.module)
+    if module_path is None:
+        console.error(f"Could not find module {config.module}.")
+        raise typer.Exit(1)
+
+    if not module_path.origin:
+        console.error(f"Could not find origin for module {config.module}.")
+        raise typer.Exit(1)
+    console.info(f"Renaming app directory to {new_app_name}.")
+    process_directory(
+        Path.cwd(),
+        config.app_name,
+        new_app_name,
+        exclude_dirs=[constants.Dirs.WEB, constants.Dirs.APP_ASSETS],
+    )
+
+    rename_path_up_tree(Path(module_path.origin), config.app_name, new_app_name)
+
+    console.success(f"App directory renamed to [bold]{new_app_name}[/bold].")
+
+
+def rename_imports_and_app_name(file_path: str | Path, old_name: str, new_name: str):
+    """Rename imports the file using string replacement as well as app_name in rxconfig.py.
+
+    Args:
+        file_path: The file to process.
+        old_name: The old name to replace.
+        new_name: The new name to use.
+    """
+    file_path = Path(file_path)
+    content = file_path.read_text()
+
+    # Replace `from old_name.` or `from old_name` with `from new_name`
+    content = re.sub(
+        rf"\bfrom {re.escape(old_name)}(\b|\.|\s)",
+        lambda match: f"from {new_name}{match.group(1)}",
+        content,
+    )
+
+    # Replace `import old_name` with `import new_name`
+    content = re.sub(
+        rf"\bimport {re.escape(old_name)}\b",
+        f"import {new_name}",
+        content,
+    )
+
+    # Replace `app_name="old_name"` in rx.Config
+    content = re.sub(
+        rf'\bapp_name\s*=\s*["\']{re.escape(old_name)}["\']',
+        f'app_name="{new_name}"',
+        content,
+    )
+
+    # Replace positional argument `"old_name"` in rx.Config
+    content = re.sub(
+        rf'\brx\.Config\(\s*["\']{re.escape(old_name)}["\']',
+        f'rx.Config("{new_name}"',
+        content,
+    )
+
+    file_path.write_text(content)
+
+
+def process_directory(
+    directory: str | Path,
+    old_name: str,
+    new_name: str,
+    exclude_dirs: list | None = None,
+    extensions: list | None = None,
+):
+    """Process files with specified extensions in a directory, excluding specified directories.
+
+    Args:
+        directory: The root directory to process.
+        old_name: The old name to replace.
+        new_name: The new name to use.
+        exclude_dirs: List of directory names to exclude. Defaults to None.
+        extensions: List of file extensions to process.
+    """
+    exclude_dirs = exclude_dirs or []
+    extensions = extensions or [
+        constants.Ext.PY,
+        constants.Ext.MD,
+    ]  # include .md files, typically used in reflex-web.
+    extensions_set = {ext.lstrip(".") for ext in extensions}
+    directory = Path(directory)
+
+    root_exclude_dirs = {directory / exclude_dir for exclude_dir in exclude_dirs}
+
+    files = (
+        p.resolve()
+        for p in directory.glob("**/*")
+        if p.is_file() and p.suffix.lstrip(".") in extensions_set
+    )
+
+    for file_path in files:
+        if not any(
+            file_path.is_relative_to(exclude_dir) for exclude_dir in root_exclude_dirs
+        ):
+            rename_imports_and_app_name(file_path, old_name, new_name)
+
+
 def create_config(app_name: str):
     """Create a new rxconfig file.
 
@@ -921,6 +1083,7 @@ def install_bun():
             constants.Bun.INSTALL_URL,
             f"bun-v{constants.Bun.VERSION}",
             BUN_INSTALL=str(constants.Bun.ROOT_PATH),
+            BUN_VERSION=str(constants.Bun.VERSION),
         )
 
 
@@ -1653,7 +1816,7 @@ def initialize_main_module_index_from_generation(app_name: str, generation_hash:
         generation_hash: The generation hash from reflex.build.
 
     Raises:
-        GeneratedCodeHasNoFunctionDefs: If the fetched code has no function definitions
+        GeneratedCodeHasNoFunctionDefsError: If the fetched code has no function definitions
             (the refactored reflex code is expected to have at least one root function defined).
     """
     # Download the reflex code for the generation.
@@ -1670,7 +1833,7 @@ def initialize_main_module_index_from_generation(app_name: str, generation_hash:
     # Determine the name of the last function, which renders the generated code.
     defined_funcs = re.findall(r"def ([a-zA-Z_]+)\(", resp.text)
     if not defined_funcs:
-        raise GeneratedCodeHasNoFunctionDefs(
+        raise GeneratedCodeHasNoFunctionDefsError(
             f"No function definitions found in generated code from {url!r}."
         )
     render_func_name = defined_funcs[-1]

+ 1 - 1
reflex/vars/base.py

@@ -1909,7 +1909,7 @@ def figure_out_type(value: Any) -> types.GenericType:
     return type(value)
 
 
-class cached_property_no_lock(functools.cached_property):
+class cached_property_no_lock(functools.cached_property):  # noqa: N801
     """A special version of functools.cached_property that does not use a lock."""
 
     def __init__(self, func):

+ 2 - 2
reflex/vars/function.py

@@ -1802,13 +1802,13 @@ def _generate_overloads_for_function_var_call(maximum_args: int = 4) -> str:
                     )
                 ]
                 function_type_hint = f"""FunctionVar[ReflexCallable[[{", ".join(required_params + optional_params)}], {return_type}]]"""
-                NEWLINE = "\n"
+                new_line = "\n"
                 overloads.append(
                     f"""
     @overload
     def call(
         self: {function_type_hint},
-        {"," + NEWLINE + "        ".join(required_args + optional_args)}
+        {"," + new_line + "        ".join(required_args + optional_args)}
     ) -> {return_type_var}: ...
     """
                 )

+ 3 - 3
reflex/vars/number.py

@@ -22,7 +22,7 @@ from typing import (
 from typing_extensions import Unpack
 
 from reflex.constants.base import Dirs
-from reflex.utils.exceptions import PrimitiveUnserializableToJSON, VarTypeError
+from reflex.utils.exceptions import PrimitiveUnserializableToJSONError, VarTypeError
 from reflex.utils.imports import ImportDict, ImportVar
 
 from .base import (
@@ -915,10 +915,10 @@ class LiteralNumberVar(LiteralVar, NumberVar):
             The JSON representation of the var.
 
         Raises:
-            PrimitiveUnserializableToJSON: If the var is unserializable to JSON.
+            PrimitiveUnserializableToJSONError: If the var is unserializable to JSON.
         """
         if math.isinf(self._var_value) or math.isnan(self._var_value):
-            raise PrimitiveUnserializableToJSON(
+            raise PrimitiveUnserializableToJSONError(
                 f"No valid JSON representation for {self}"
             )
         return json.dumps(self._var_value)

+ 20 - 9
scripts/bun_install.sh

@@ -78,6 +78,14 @@ case $platform in
     ;;
 esac
 
+case "$target" in
+'linux'*)
+    if [ -f /etc/alpine-release ]; then
+        target="$target-musl"
+    fi
+    ;;
+esac
+
 if [[ $target = darwin-x64 ]]; then
     # Is this process running in Rosetta?
     # redirect stderr to devnull to avoid error message when not running in Rosetta
@@ -91,19 +99,20 @@ GITHUB=${GITHUB-"https://github.com"}
 
 github_repo="$GITHUB/oven-sh/bun"
 
-if [[ $target = darwin-x64 ]]; then
-    # If AVX2 isn't supported, use the -baseline build
+# If AVX2 isn't supported, use the -baseline build
+case "$target" in
+'darwin-x64'*)
     if [[ $(sysctl -a | grep machdep.cpu | grep AVX2) == '' ]]; then
-        target=darwin-x64-baseline
+        target="$target-baseline"
     fi
-fi
-
-if [[ $target = linux-x64 ]]; then
+    ;;
+'linux-x64'*)
     # If AVX2 isn't supported, use the -baseline build
     if [[ $(cat /proc/cpuinfo | grep avx2) = '' ]]; then
-        target=linux-x64-baseline
+        target="$target-baseline"
     fi
-fi
+    ;;
+esac
 
 exe_name=bun
 
@@ -113,8 +122,10 @@ if [[ $# = 2 && $2 = debug-info ]]; then
     info "You requested a debug build of bun. More information will be shown if a crash occurs."
 fi
 
+bun_version=BUN_VERSION
+
 if [[ $# = 0 ]]; then
-    bun_uri=$github_repo/releases/latest/download/bun-$target.zip
+    bun_uri=$github_repo/releases/download/bun-v$bun_version/bun-$target.zip
 else
     bun_uri=$github_repo/releases/download/$1/bun-$target.zip
 fi

+ 4 - 0
scripts/install.ps1

@@ -214,8 +214,12 @@ function Install-Bun {
   # http://community.sqlbackupandftp.com/t/error-1073741515-solved/1305
   if (($LASTEXITCODE -eq 3221225781) -or ($LASTEXITCODE -eq -1073741515)) # STATUS_DLL_NOT_FOUND
   { 
+    # TODO: as of July 2024, Bun has no external dependencies.
+    # I want to keep this error message in for a few months to ensure that
+    # if someone somehow runs into this, it can be reported.
     Write-Output "Install Failed - You are missing a DLL required to run bun.exe"
     Write-Output "This can be solved by installing the Visual C++ Redistributable from Microsoft:`nSee https://learn.microsoft.com/cpp/windows/latest-supported-vc-redist`nDirect Download -> https://aka.ms/vs/17/release/vc_redist.x64.exe`n`n"
+    Write-Output "The error above should be unreachable as Bun does not depend on this library. Please comment in https://github.com/oven-sh/bun/issues/8598 or open a new issue.`n`n"
     Write-Output "The command '${BunBin}\bun.exe --revision' exited with code ${LASTEXITCODE}`n"
     return 1
   }

+ 0 - 30
tests/integration/conftest.py

@@ -1,8 +1,6 @@
 """Shared conftest for all integration tests."""
 
 import os
-import re
-from pathlib import Path
 
 import pytest
 
@@ -36,34 +34,6 @@ def xvfb():
         yield None
 
 
-def pytest_exception_interact(node, call, report):
-    """Take and upload screenshot when tests fail.
-
-    Args:
-        node: The pytest item that failed.
-        call: The pytest call describing when/where the test was invoked.
-        report: The pytest log report object.
-    """
-    screenshot_dir = environment.SCREENSHOT_DIR.get()
-    if DISPLAY is None or screenshot_dir is None:
-        return
-
-    screenshot_dir = Path(screenshot_dir)
-    screenshot_dir.mkdir(parents=True, exist_ok=True)
-    safe_filename = re.sub(
-        r"(?u)[^-\w.]",
-        "_",
-        str(node.nodeid).strip().replace(" ", "_").replace(":", "_").replace(".py", ""),
-    )
-
-    try:
-        DISPLAY.waitgrab().save(
-            (Path(screenshot_dir) / safe_filename).with_suffix(".png"),
-        )
-    except Exception as e:
-        print(f"Failed to take screenshot for {node}: {e}")
-
-
 @pytest.fixture(
     scope="session", params=[AppHarness, AppHarnessProd], ids=["dev", "prod"]
 )

+ 2 - 2
tests/integration/test_background_task.py

@@ -172,7 +172,7 @@ def BackgroundTask():
             rx.button("Reset", on_click=State.reset_counter, id="reset"),
         )
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
     app.add_page(index)
 
 
@@ -288,7 +288,7 @@ def test_background_task(
     assert background_task._poll_for(lambda: counter.text == "620", timeout=40)
     # all tasks should have exited and cleaned up
     assert background_task._poll_for(
-        lambda: not background_task.app_instance.background_tasks  # type: ignore
+        lambda: not background_task.app_instance._background_tasks  # type: ignore
     )
 
 

+ 1 - 1
tests/integration/test_call_script.py

@@ -188,7 +188,7 @@ def CallScript():
             yield rx.call_script("inline_counter = 0; external_counter = 0")
             self.reset()
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
     Path("assets/external.js").write_text(external_scripts)
 
     @app.add_page

+ 3 - 1
tests/integration/test_client_storage.py

@@ -127,7 +127,7 @@ def ClientSide():
             rx.box(ClientSideSubSubState.s1s, id="s1s"),
         )
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
     app.add_page(index)
     app.add_page(index, route="/foo")
 
@@ -321,6 +321,7 @@ async def test_client_side_state(
     assert not driver.get_cookies()
     local_storage_items = local_storage.items()
     local_storage_items.pop("last_compiled_time", None)
+    local_storage_items.pop("theme", None)
     assert not local_storage_items
 
     # set some cookies and local storage values
@@ -436,6 +437,7 @@ async def test_client_side_state(
 
     local_storage_items = local_storage.items()
     local_storage_items.pop("last_compiled_time", None)
+    local_storage_items.pop("theme", None)
     assert local_storage_items.pop(f"{sub_state_name}.l1") == "l1 value"
     assert local_storage_items.pop(f"{sub_state_name}.l2") == "l2 value"
     assert local_storage_items.pop("l3") == "l3 value"

+ 1 - 1
tests/integration/test_component_state.py

@@ -72,7 +72,7 @@ def ComponentStateApp():
             State=_Counter,
         )
 
-    app = rx.App(state=rx.State)  # noqa
+    app = rx.App(_state=rx.State)  # noqa
 
     @rx.page()
     def index():

+ 1 - 1
tests/integration/test_connection_banner.py

@@ -36,7 +36,7 @@ def ConnectionBanner():
             rx.button("Delay", id="delay", on_click=State.delay),
         )
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
     app.add_page(index)
 
 

+ 1 - 1
tests/integration/test_deploy_url.py

@@ -26,7 +26,7 @@ def DeployUrlSample() -> None:
             rx.button("GOTO SELF", on_click=State.goto_self, id="goto_self")
         )
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
     app.add_page(index)
 
 

+ 1 - 1
tests/integration/test_dynamic_routes.py

@@ -138,7 +138,7 @@ def DynamicRoute():
     def redirect_page():
         return rx.fragment(rx.text("redirecting..."))
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
     app.add_page(index, route="/page/[page_id]", on_load=DynamicState.on_load)  # type: ignore
     app.add_page(index, route="/static/x", on_load=DynamicState.on_load)  # type: ignore
     app.add_page(index)

+ 1 - 1
tests/integration/test_event_actions.py

@@ -158,7 +158,7 @@ def TestEventAction():
             on_click=EventActionState.on_click("outer"),  # type: ignore
         )
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
     app.add_page(index)
 
 

+ 1 - 1
tests/integration/test_event_chain.py

@@ -144,7 +144,7 @@ def EventChain():
             time.sleep(0.5)
             self.interim_value = "final"
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
 
     token_input = rx.input(
         value=State.router.session.client_token, is_read_only=True, id="token"

+ 1 - 1
tests/integration/test_exception_handlers.py

@@ -39,7 +39,7 @@ def TestApp():
             """
             print(1 / number)
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
 
     @app.add_page
     def index():

+ 2 - 2
tests/integration/test_form_submit.py

@@ -30,7 +30,7 @@ def FormSubmit(form_component):
         def form_submit(self, form_data: Dict):
             self.form_data = form_data
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
 
     @app.add_page
     def index():
@@ -90,7 +90,7 @@ def FormSubmitName(form_component):
         def form_submit(self, form_data: Dict):
             self.form_data = form_data
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
 
     @app.add_page
     def index():

+ 1 - 1
tests/integration/test_input.py

@@ -16,7 +16,7 @@ def FullyControlledInput():
     class State(rx.State):
         text: str = "initial"
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
 
     @app.add_page
     def index():

+ 1 - 1
tests/integration/test_login_flow.py

@@ -45,7 +45,7 @@ def LoginSample():
             rx.button("Do it", on_click=State.login, id="doit"),
         )
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
     app.add_page(index)
     app.add_page(login)
 

+ 1 - 1
tests/integration/test_server_side_event.py

@@ -38,7 +38,7 @@ def ServerSideEvent():
         def set_value_return_c(self):
             return rx.set_value("c", "")
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
 
     @app.add_page
     def index():

+ 1 - 1
tests/integration/test_upload.py

@@ -166,7 +166,7 @@ def UploadFile():
             rx.text(UploadState.event_order.to_string(), id="event-order"),
         )
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
     app.add_page(index)
 
 

+ 1 - 1
tests/integration/test_var_operations.py

@@ -41,7 +41,7 @@ def VarOperations():
         dict2: rx.Field[Dict[int, int]] = rx.field({3: 4})
         html_str: rx.Field[str] = rx.field("<div>hello</div>")
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
 
     @rx.memo
     def memo_comp(list1: List[int], int_var1: int, id: str):

+ 1 - 1
tests/integration/tests_playwright/test_datetime_operations.py

@@ -16,7 +16,7 @@ def DatetimeOperationsApp():
         date2: datetime = datetime(2031, 1, 1)
         date3: datetime = datetime(2021, 1, 1)
 
-    app = rx.App(state=DtOperationsState)
+    app = rx.App(_state=DtOperationsState)
 
     @app.add_page
     def index():

+ 1 - 1
tests/integration/tests_playwright/test_table.py

@@ -20,7 +20,7 @@ def Table():
     """App using table component."""
     import reflex as rx
 
-    app = rx.App(state=rx.State)
+    app = rx.App(_state=rx.State)
 
     @app.add_page
     def index():

+ 1 - 30
tests/units/conftest.py

@@ -1,11 +1,8 @@
 """Test fixtures."""
 
 import asyncio
-import contextlib
-import os
 import platform
 import uuid
-from pathlib import Path
 from typing import Dict, Generator, Type
 from unittest import mock
 
@@ -14,6 +11,7 @@ import pytest
 from reflex.app import App
 from reflex.event import EventSpec
 from reflex.model import ModelRegistry
+from reflex.testing import chdir
 from reflex.utils import prerequisites
 
 from .states import (
@@ -191,33 +189,6 @@ def router_data(router_data_headers) -> Dict[str, str]:
     }
 
 
-# borrowed from py3.11
-class chdir(contextlib.AbstractContextManager):
-    """Non thread-safe context manager to change the current working directory."""
-
-    def __init__(self, path):
-        """Prepare contextmanager.
-
-        Args:
-            path: the path to change to
-        """
-        self.path = path
-        self._old_cwd = []
-
-    def __enter__(self):
-        """Save current directory and perform chdir."""
-        self._old_cwd.append(Path.cwd())
-        os.chdir(self.path)
-
-    def __exit__(self, *excinfo):
-        """Change back to previous directory on stack.
-
-        Args:
-            excinfo: sys.exc_info captured in the context block
-        """
-        os.chdir(self._old_cwd.pop())
-
-
 @pytest.fixture
 def tmp_working_dir(tmp_path):
     """Create a temporary directory and chdir to it.

+ 1 - 1
tests/units/middleware/test_hydrate_middleware.py

@@ -41,7 +41,7 @@ async def test_preprocess_no_events(hydrate_middleware, event1, mocker):
     mocker.patch("reflex.state.State.class_subclasses", {TestState})
     state = State()
     update = await hydrate_middleware.preprocess(
-        app=App(state=State),
+        app=App(_state=State),
         event=event1,
         state=state,
     )

+ 50 - 50
tests/units/test_app.py

@@ -235,14 +235,14 @@ def test_add_page_default_route(app: App, index_page, about_page):
         index_page: The index page.
         about_page: The about page.
     """
-    assert app.pages == {}
-    assert app.unevaluated_pages == {}
+    assert app._pages == {}
+    assert app._unevaluated_pages == {}
     app.add_page(index_page)
     app._compile_page("index")
-    assert app.pages.keys() == {"index"}
+    assert app._pages.keys() == {"index"}
     app.add_page(about_page)
     app._compile_page("about")
-    assert app.pages.keys() == {"index", "about"}
+    assert app._pages.keys() == {"index", "about"}
 
 
 def test_add_page_set_route(app: App, index_page, windows_platform: bool):
@@ -254,10 +254,10 @@ def test_add_page_set_route(app: App, index_page, windows_platform: bool):
         windows_platform: Whether the system is windows.
     """
     route = "test" if windows_platform else "/test"
-    assert app.unevaluated_pages == {}
+    assert app._unevaluated_pages == {}
     app.add_page(index_page, route=route)
     app._compile_page("test")
-    assert app.pages.keys() == {"test"}
+    assert app._pages.keys() == {"test"}
 
 
 def test_add_page_set_route_dynamic(index_page, windows_platform: bool):
@@ -267,18 +267,18 @@ def test_add_page_set_route_dynamic(index_page, windows_platform: bool):
         index_page: The index page.
         windows_platform: Whether the system is windows.
     """
-    app = App(state=EmptyState)
-    assert app.state is not None
+    app = App(_state=EmptyState)
+    assert app._state is not None
     route = "/test/[dynamic]"
-    assert app.unevaluated_pages == {}
+    assert app._unevaluated_pages == {}
     app.add_page(index_page, route=route)
     app._compile_page("test/[dynamic]")
-    assert app.pages.keys() == {"test/[dynamic]"}
-    assert "dynamic" in app.state.computed_vars
-    assert app.state.computed_vars["dynamic"]._deps(objclass=EmptyState) == {
+    assert app._pages.keys() == {"test/[dynamic]"}
+    assert "dynamic" in app._state.computed_vars
+    assert app._state.computed_vars["dynamic"]._deps(objclass=EmptyState) == {
         constants.ROUTER
     }
-    assert constants.ROUTER in app.state()._computed_var_dependencies
+    assert constants.ROUTER in app._state()._computed_var_dependencies
 
 
 def test_add_page_set_route_nested(app: App, index_page, windows_platform: bool):
@@ -290,9 +290,9 @@ def test_add_page_set_route_nested(app: App, index_page, windows_platform: bool)
         windows_platform: Whether the system is windows.
     """
     route = "test\\nested" if windows_platform else "/test/nested"
-    assert app.unevaluated_pages == {}
+    assert app._unevaluated_pages == {}
     app.add_page(index_page, route=route)
-    assert app.unevaluated_pages.keys() == {route.strip(os.path.sep)}
+    assert app._unevaluated_pages.keys() == {route.strip(os.path.sep)}
 
 
 def test_add_page_invalid_api_route(app: App, index_page):
@@ -412,8 +412,8 @@ async def test_initialize_with_state(test_state: Type[ATestState], token: str):
         test_state: The default state.
         token: a Token.
     """
-    app = App(state=test_state)
-    assert app.state == test_state
+    app = App(_state=test_state)
+    assert app._state == test_state
 
     # Get a state for a given token.
     state = await app.state_manager.get_state(_substate_key(token, test_state))
@@ -431,7 +431,7 @@ async def test_set_and_get_state(test_state):
     Args:
         test_state: The default state.
     """
-    app = App(state=test_state)
+    app = App(_state=test_state)
 
     # Create two tokens.
     token1 = str(uuid.uuid4()) + f"_{test_state.get_full_name()}"
@@ -826,7 +826,7 @@ async def test_upload_file_without_annotation(state, tmp_path, token):
         token: a Token.
     """
     state._tmp_path = tmp_path
-    app = App(state=State)
+    app = App(_state=State)
 
     request_mock = unittest.mock.Mock()
     request_mock.headers = {
@@ -860,7 +860,7 @@ async def test_upload_file_background(state, tmp_path, token):
         token: a Token.
     """
     state._tmp_path = tmp_path
-    app = App(state=State)
+    app = App(_state=State)
 
     request_mock = unittest.mock.Mock()
     request_mock.headers = {
@@ -937,8 +937,8 @@ def test_dynamic_arg_shadow(
     """
     arg_name = "counter"
     route = f"/test/[{arg_name}]"
-    app = app_module_mock.app = App(state=DynamicState)
-    assert app.state is not None
+    app = app_module_mock.app = App(_state=DynamicState)
+    assert app._state is not None
     with pytest.raises(NameError):
         app.add_page(index_page, route=route, on_load=DynamicState.on_load)  # type: ignore
 
@@ -962,7 +962,7 @@ def test_multiple_dynamic_args(
     arg_name = "my_arg"
     route = f"/test/[{arg_name}]"
     route2 = f"/test2/[{arg_name}]"
-    app = app_module_mock.app = App(state=EmptyState)
+    app = app_module_mock.app = App(_state=EmptyState)
     app.add_page(index_page, route=route)
     app.add_page(index_page, route=route2)
 
@@ -989,16 +989,16 @@ async def test_dynamic_route_var_route_change_completed_on_load(
     """
     arg_name = "dynamic"
     route = f"/test/[{arg_name}]"
-    app = app_module_mock.app = App(state=DynamicState)
-    assert app.state is not None
-    assert arg_name not in app.state.vars
+    app = app_module_mock.app = App(_state=DynamicState)
+    assert app._state is not None
+    assert arg_name not in app._state.vars
     app.add_page(index_page, route=route, on_load=DynamicState.on_load)  # type: ignore
-    assert arg_name in app.state.vars
-    assert arg_name in app.state.computed_vars
-    assert app.state.computed_vars[arg_name]._deps(objclass=DynamicState) == {
+    assert arg_name in app._state.vars
+    assert arg_name in app._state.computed_vars
+    assert app._state.computed_vars[arg_name]._deps(objclass=DynamicState) == {
         constants.ROUTER
     }
-    assert constants.ROUTER in app.state()._computed_var_dependencies
+    assert constants.ROUTER in app._state()._computed_var_dependencies
 
     substate_token = _substate_key(token, DynamicState)
     sid = "mock_sid"
@@ -1173,7 +1173,7 @@ async def test_process_events(mocker, token: str):
         "headers": {},
         "ip": "127.0.0.1",
     }
-    app = App(state=GenState)
+    app = App(_state=GenState)
 
     mocker.patch.object(app, "_postprocess", AsyncMock())
     event = Event(
@@ -1219,7 +1219,7 @@ def test_overlay_component(
         overlay_component: The overlay_component to pass to App.
         exp_page_child: The type of the expected child in the page fragment.
     """
-    app = App(state=state, overlay_component=overlay_component)
+    app = App(_state=state, overlay_component=overlay_component)
     app._setup_overlay_component()
     if exp_page_child is None:
         assert app.overlay_component is None
@@ -1238,7 +1238,7 @@ def test_overlay_component(
     # overlay components are wrapped during compile only
     app._compile_page("test")
     app._setup_overlay_component()
-    page = app.pages["test"]
+    page = app._pages["test"]
 
     if exp_page_child is not None:
         assert len(page.children) == 3
@@ -1356,52 +1356,52 @@ def test_app_wrap_priority(compilable_app: tuple[App, Path]):
 def test_app_state_determination():
     """Test that the stateless status of an app is determined correctly."""
     a1 = App()
-    assert a1.state is None
+    assert a1._state is None
 
     # No state, no router, no event handlers.
     a1.add_page(rx.box("Index"), route="/")
-    assert a1.state is None
+    assert a1._state is None
 
     # Add a page with `on_load` enables state.
     a1.add_page(rx.box("About"), route="/about", on_load=rx.console_log(""))
     a1._compile_page("about")
-    assert a1.state is not None
+    assert a1._state is not None
 
     a2 = App()
-    assert a2.state is None
+    assert a2._state is None
 
     # Referencing a state Var enables state.
     a2.add_page(rx.box(rx.text(GenState.value)), route="/")
     a2._compile_page("index")
-    assert a2.state is not None
+    assert a2._state is not None
 
     a3 = App()
-    assert a3.state is None
+    assert a3._state is None
 
     # Referencing router enables state.
     a3.add_page(rx.box(rx.text(State.router.page.full_path)), route="/")
     a3._compile_page("index")
-    assert a3.state is not None
+    assert a3._state is not None
 
     a4 = App()
-    assert a4.state is None
+    assert a4._state is None
 
     a4.add_page(rx.box(rx.button("Click", on_click=rx.console_log(""))), route="/")
-    assert a4.state is None
+    assert a4._state is None
 
     a4.add_page(
         rx.box(rx.button("Click", on_click=DynamicState.on_counter)), route="/page2"
     )
     a4._compile_page("page2")
-    assert a4.state is not None
+    assert a4._state is not None
 
 
 def test_raise_on_state():
     """Test that the state is set."""
     # state kwargs is deprecated, we just make sure the app is created anyway.
-    _app = App(state=State)
-    assert _app.state is not None
-    assert issubclass(_app.state, State)
+    _app = App(_state=State)
+    assert _app._state is not None
+    assert issubclass(_app._state, State)
 
 
 def test_call_app():
@@ -1468,7 +1468,7 @@ def test_add_page_component_returning_tuple():
     app._compile_page("index")
     app._compile_page("page2")
 
-    fragment_wrapper = app.pages["index"].children[0]
+    fragment_wrapper = app._pages["index"].children[0]
     assert isinstance(fragment_wrapper, Fragment)
     first_text = fragment_wrapper.children[0]
     assert isinstance(first_text, Text)
@@ -1478,7 +1478,7 @@ def test_add_page_component_returning_tuple():
     assert str(second_text.children[0].contents) == '"second"'  # type: ignore
 
     # Test page with trailing comma.
-    page2_fragment_wrapper = app.pages["page2"].children[0]
+    page2_fragment_wrapper = app._pages["page2"].children[0]
     assert isinstance(page2_fragment_wrapper, Fragment)
     third_text = page2_fragment_wrapper.children[0]
     assert isinstance(third_text, Text)
@@ -1552,7 +1552,7 @@ def test_app_with_valid_var_dependencies(compilable_app: tuple[App, Path]):
         def bar(self) -> str:
             return "bar"
 
-    app.state = ValidDepState
+    app._state = ValidDepState
     app._compile()
 
 
@@ -1564,7 +1564,7 @@ def test_app_with_invalid_var_dependencies(compilable_app: tuple[App, Path]):
         def bar(self) -> str:
             return "bar"
 
-    app.state = InvalidDepState
+    app._state = InvalidDepState
     with pytest.raises(exceptions.VarDependencyError):
         app._compile()
 

+ 161 - 0
tests/units/test_prerequisites.py

@@ -1,20 +1,28 @@
 import json
 import re
+import shutil
 import tempfile
+from pathlib import Path
 from unittest.mock import Mock, mock_open
 
 import pytest
+from typer.testing import CliRunner
 
 from reflex import constants
 from reflex.config import Config
+from reflex.reflex import cli
+from reflex.testing import chdir
 from reflex.utils.prerequisites import (
     CpuInfo,
     _update_next_config,
     cached_procedure,
     get_cpu_info,
     initialize_requirements_txt,
+    rename_imports_and_app_name,
 )
 
+runner = CliRunner()
+
 
 @pytest.mark.parametrize(
     "config, export, expected_output",
@@ -224,3 +232,156 @@ def test_get_cpu_info():
     for attr in ("manufacturer_id", "model_name", "address_width"):
         value = getattr(cpu_info, attr)
         assert value.strip() if attr != "address_width" else value
+
+
+@pytest.fixture
+def temp_directory():
+    temp_dir = tempfile.mkdtemp()
+    yield Path(temp_dir)
+    shutil.rmtree(temp_dir)
+
+
+@pytest.mark.parametrize(
+    "config_code,expected",
+    [
+        ("rx.Config(app_name='old_name')", 'rx.Config(app_name="new_name")'),
+        ('rx.Config(app_name="old_name")', 'rx.Config(app_name="new_name")'),
+        ("rx.Config('old_name')", 'rx.Config("new_name")'),
+        ('rx.Config("old_name")', 'rx.Config("new_name")'),
+    ],
+)
+def test_rename_imports_and_app_name(temp_directory, config_code, expected):
+    file_path = temp_directory / "rxconfig.py"
+    content = f"""
+config = {config_code}
+"""
+    file_path.write_text(content)
+
+    rename_imports_and_app_name(file_path, "old_name", "new_name")
+
+    updated_content = file_path.read_text()
+    expected_content = f"""
+config = {expected}
+"""
+    assert updated_content == expected_content
+
+
+def test_regex_edge_cases(temp_directory):
+    file_path = temp_directory / "example.py"
+    content = """
+from old_name.module import something
+import old_name
+from old_name import something_else as alias
+from old_name
+"""
+    file_path.write_text(content)
+
+    rename_imports_and_app_name(file_path, "old_name", "new_name")
+
+    updated_content = file_path.read_text()
+    expected_content = """
+from new_name.module import something
+import new_name
+from new_name import something_else as alias
+from new_name
+"""
+    assert updated_content == expected_content
+
+
+def test_cli_rename_command(temp_directory):
+    foo_dir = temp_directory / "foo"
+    foo_dir.mkdir()
+    (foo_dir / "__init__").touch()
+    (foo_dir / ".web").mkdir()
+    (foo_dir / "assets").mkdir()
+    (foo_dir / "foo").mkdir()
+    (foo_dir / "foo" / "__init__.py").touch()
+    (foo_dir / "rxconfig.py").touch()
+    (foo_dir / "rxconfig.py").write_text(
+        """
+import reflex as rx
+
+config = rx.Config(
+    app_name="foo",
+)
+"""
+    )
+    (foo_dir / "foo" / "components").mkdir()
+    (foo_dir / "foo" / "components" / "__init__.py").touch()
+    (foo_dir / "foo" / "components" / "base.py").touch()
+    (foo_dir / "foo" / "components" / "views.py").touch()
+    (foo_dir / "foo" / "components" / "base.py").write_text(
+        """
+import reflex as rx
+from foo.components import views
+from foo.components.views import *
+from .base import *
+
+def random_component():
+    return rx.fragment()
+"""
+    )
+    (foo_dir / "foo" / "foo.py").touch()
+    (foo_dir / "foo" / "foo.py").write_text(
+        """
+import reflex as rx
+import foo.components.base
+from foo.components.base import random_component
+
+class State(rx.State):
+  pass
+
+
+def index():
+   return rx.text("Hello, World!")
+
+app = rx.App()
+app.add_page(index)
+"""
+    )
+
+    with chdir(temp_directory / "foo"):
+        result = runner.invoke(cli, ["rename", "bar"])
+
+    assert result.exit_code == 0
+    assert (foo_dir / "rxconfig.py").read_text() == (
+        """
+import reflex as rx
+
+config = rx.Config(
+    app_name="bar",
+)
+"""
+    )
+    assert (foo_dir / "bar").exists()
+    assert not (foo_dir / "foo").exists()
+    assert (foo_dir / "bar" / "components" / "base.py").read_text() == (
+        """
+import reflex as rx
+from bar.components import views
+from bar.components.views import *
+from .base import *
+
+def random_component():
+    return rx.fragment()
+"""
+    )
+    assert (foo_dir / "bar" / "bar.py").exists()
+    assert not (foo_dir / "bar" / "foo.py").exists()
+    assert (foo_dir / "bar" / "bar.py").read_text() == (
+        """
+import reflex as rx
+import bar.components.base
+from bar.components.base import random_component
+
+class State(rx.State):
+  pass
+
+
+def index():
+   return rx.text("Hello, World!")
+
+app = rx.App()
+app.add_page(index)
+"""
+    )

+ 2 - 2
tests/units/test_route.py

@@ -89,7 +89,7 @@ def app():
     ],
 )
 def test_check_routes_conflict_invalid(mocker, app, route1, route2):
-    mocker.patch.object(app, "pages", {route1: []})
+    mocker.patch.object(app, "_pages", {route1: []})
     with pytest.raises(ValueError):
         app._check_routes_conflict(route2)
 
@@ -117,6 +117,6 @@ def test_check_routes_conflict_invalid(mocker, app, route1, route2):
     ],
 )
 def test_check_routes_conflict_valid(mocker, app, route1, route2):
-    mocker.patch.object(app, "pages", {route1: []})
+    mocker.patch.object(app, "_pages", {route1: []})
     # test that running this does not throw an error.
     app._check_routes_conflict(route2)

+ 14 - 14
tests/units/test_state.py

@@ -1912,12 +1912,12 @@ def mock_app_simple(monkeypatch) -> rx.App:
     Returns:
         The app, after mocking out prerequisites.get_app()
     """
-    app = App(state=TestState)
+    app = App(_state=TestState)
 
     app_module = Mock()
 
     setattr(app_module, CompileVars.APP, app)
-    app.state = TestState
+    app._state = TestState
     app.event_namespace.emit = CopyingAsyncMock()  # type: ignore
 
     def _mock_get_app(*args, **kwargs):
@@ -2161,7 +2161,7 @@ async def test_background_task_no_block(mock_app: rx.App, token: str):
         token: A token.
     """
     router_data = {"query": {}}
-    mock_app.state_manager.state = mock_app.state = BackgroundTaskState
+    mock_app.state_manager.state = mock_app._state = BackgroundTaskState
     async for update in rx.app.process(  # type: ignore
         mock_app,
         Event(
@@ -2179,7 +2179,7 @@ async def test_background_task_no_block(mock_app: rx.App, token: str):
 
     # wait for the coroutine to start
     await asyncio.sleep(0.5 if CI else 0.1)
-    assert len(mock_app.background_tasks) == 1
+    assert len(mock_app._background_tasks) == 1
 
     # Process another normal event
     async for update in rx.app.process(  # type: ignore
@@ -2211,9 +2211,9 @@ async def test_background_task_no_block(mock_app: rx.App, token: str):
         )
 
     # Explicit wait for background tasks
-    for task in tuple(mock_app.background_tasks):
+    for task in tuple(mock_app._background_tasks):
         await task
-    assert not mock_app.background_tasks
+    assert not mock_app._background_tasks
 
     exp_order = [
         "background_task:start",
@@ -2292,7 +2292,7 @@ async def test_background_task_reset(mock_app: rx.App, token: str):
         token: A token.
     """
     router_data = {"query": {}}
-    mock_app.state_manager.state = mock_app.state = BackgroundTaskState
+    mock_app.state_manager.state = mock_app._state = BackgroundTaskState
     async for update in rx.app.process(  # type: ignore
         mock_app,
         Event(
@@ -2309,9 +2309,9 @@ async def test_background_task_reset(mock_app: rx.App, token: str):
         assert update == StateUpdate()
 
     # Explicit wait for background tasks
-    for task in tuple(mock_app.background_tasks):
+    for task in tuple(mock_app._background_tasks):
         await task
-    assert not mock_app.background_tasks
+    assert not mock_app._background_tasks
 
     assert (
         await mock_app.state_manager.get_state(
@@ -2896,7 +2896,7 @@ async def test_preprocess(app_module_mock, token, test_state, expected, mocker):
         "reflex.state.State.class_subclasses", {test_state, OnLoadInternalState}
     )
     app = app_module_mock.app = App(
-        state=State, load_events={"index": [test_state.test_handler]}
+        _state=State, _load_events={"index": [test_state.test_handler]}
     )
     async with app.state_manager.modify_state(_substate_key(token, State)) as state:
         state.router_data = {"simulate": "hydrate"}
@@ -2943,8 +2943,8 @@ async def test_preprocess_multiple_load_events(app_module_mock, token, mocker):
         "reflex.state.State.class_subclasses", {OnLoadState, OnLoadInternalState}
     )
     app = app_module_mock.app = App(
-        state=State,
-        load_events={"index": [OnLoadState.test_handler, OnLoadState.test_handler]},
+        _state=State,
+        _load_events={"index": [OnLoadState.test_handler, OnLoadState.test_handler]},
     )
     async with app.state_manager.modify_state(_substate_key(token, State)) as state:
         state.router_data = {"simulate": "hydrate"}
@@ -2989,7 +2989,7 @@ async def test_get_state(mock_app: rx.App, token: str):
         mock_app: An app that will be returned by `get_app()`
         token: A token.
     """
-    mock_app.state_manager.state = mock_app.state = TestState
+    mock_app.state_manager.state = mock_app._state = TestState
 
     # Get instance of ChildState2.
     test_state = await mock_app.state_manager.get_state(
@@ -3159,7 +3159,7 @@ async def test_get_state_from_sibling_not_cached(mock_app: rx.App, token: str):
 
         pass
 
-    mock_app.state_manager.state = mock_app.state = Parent
+    mock_app.state_manager.state = mock_app._state = Parent
 
     # Get the top level state via unconnected sibling.
     root = await mock_app.state_manager.get_state(_substate_key(token, Child))

+ 1 - 1
tests/units/test_state_tree.py

@@ -222,7 +222,7 @@ async def state_manager_redis(
     Yields:
         A state manager instance
     """
-    app_module_mock.app = rx.App(state=Root)
+    app_module_mock.app = rx.App(_state=Root)
     state_manager = app_module_mock.app.state_manager
 
     if not isinstance(state_manager, StateManagerRedis):

+ 1 - 1
tests/units/test_testing.py

@@ -23,7 +23,7 @@ def test_app_harness(tmp_path):
         class State(rx.State):
             pass
 
-        app = rx.App(state=State)
+        app = rx.App(_state=State)
         app.add_page(lambda: rx.text("Basic App"), route="/", title="index")
         app._compile()
 

+ 2 - 2
tests/units/test_var.py

@@ -12,7 +12,7 @@ from reflex.base import Base
 from reflex.constants.base import REFLEX_VAR_CLOSING_TAG, REFLEX_VAR_OPENING_TAG
 from reflex.state import BaseState
 from reflex.utils.exceptions import (
-    PrimitiveUnserializableToJSON,
+    PrimitiveUnserializableToJSONError,
     UntypedComputedVarError,
 )
 from reflex.utils.imports import ImportVar
@@ -1234,7 +1234,7 @@ def test_inf_and_nan(var, expected_js):
     assert str(var) == expected_js
     assert isinstance(var, NumberVar)
     assert isinstance(var, LiteralVar)
-    with pytest.raises(PrimitiveUnserializableToJSON):
+    with pytest.raises(PrimitiveUnserializableToJSONError):
         var.json()