Przeglądaj źródła

Fix project hash and modernize type annotations (#1704)

Nikhil Rao 1 rok temu
rodzic
commit
1d9f25be6d

+ 2 - 3
reflex/.templates/web/utils/state.js

@@ -9,9 +9,8 @@ import Router, { useRouter } from "next/router";
 
 
 // Endpoint URLs.
-const PINGURL = env.pingUrl
-const EVENTURL = env.eventUrl
-const UPLOADURL = env.uploadUrl
+const EVENTURL = env.EVENT
+const UPLOADURL = env.UPLOAD
 
 // Global variable to hold the token.
 let token;

+ 18 - 17
reflex/app.py

@@ -13,7 +13,6 @@ from typing import (
     Dict,
     List,
     Optional,
-    Tuple,
     Type,
     Union,
 )
@@ -210,7 +209,7 @@ class App(Base):
             allow_origins=["*"],
         )
 
-    async def preprocess(self, state: State, event: Event) -> Optional[StateUpdate]:
+    async def preprocess(self, state: State, event: Event) -> StateUpdate | None:
         """Preprocess the event.
 
         This is where middleware can modify the event before it is processed.
@@ -263,7 +262,7 @@ class App(Base):
                 return out  # type: ignore
         return update
 
-    def add_middleware(self, middleware: Middleware, index: Optional[int] = None):
+    def add_middleware(self, middleware: Middleware, index: int | None = None):
         """Add middleware to the app.
 
         Args:
@@ -302,16 +301,17 @@ class App(Base):
 
     def add_page(
         self,
-        component: Union[Component, ComponentCallable],
-        route: Optional[str] = None,
+        component: Component | ComponentCallable,
+        route: str | None = None,
         title: str = constants.DEFAULT_TITLE,
         description: str = constants.DEFAULT_DESCRIPTION,
         image=constants.DEFAULT_IMAGE,
-        on_load: Optional[
-            Union[EventHandler, EventSpec, List[Union[EventHandler, EventSpec]]]
-        ] = None,
-        meta: List[Dict] = constants.DEFAULT_META_LIST,
-        script_tags: Optional[List[Component]] = None,
+        on_load: EventHandler
+        | EventSpec
+        | list[EventHandler | EventSpec]
+        | None = None,
+        meta: list[dict[str, str]] = constants.DEFAULT_META_LIST,
+        script_tags: list[Component] | None = None,
     ):
         """Add a page to the app.
 
@@ -379,7 +379,7 @@ class App(Base):
                 on_load = [on_load]
             self.load_events[route] = on_load
 
-    def get_load_events(self, route: str) -> List[Union[EventHandler, EventSpec]]:
+    def get_load_events(self, route: str) -> list[EventHandler | EventSpec]:
         """Get the load events for a route.
 
         Args:
@@ -428,14 +428,15 @@ class App(Base):
 
     def add_custom_404_page(
         self,
-        component: Optional[Union[Component, ComponentCallable]] = None,
+        component: Component | ComponentCallable | None = None,
         title: str = constants.TITLE_404,
         image: str = constants.FAVICON_404,
         description: str = constants.DESCRIPTION_404,
-        on_load: Optional[
-            Union[EventHandler, EventSpec, List[Union[EventHandler, EventSpec]]]
-        ] = None,
-        meta: List[Dict] = constants.DEFAULT_META_LIST,
+        on_load: EventHandler
+        | EventSpec
+        | list[EventHandler | EventSpec]
+        | None = None,
+        meta: list[dict[str, str]] = constants.DEFAULT_META_LIST,
     ):
         """Define a custom 404 page for any url having no match.
 
@@ -694,7 +695,7 @@ def upload(app: App):
         # get the current state(parent state/substate)
         path = handler.split(".")[:-1]
         current_state = state.get_substate(path)
-        handler_upload_param: Tuple = ()
+        handler_upload_param = ()
 
         # get handler function
         func = getattr(current_state, handler.split(".")[-1])

+ 2 - 2
reflex/base.py

@@ -1,7 +1,7 @@
 """Define the base Reflex class."""
 from __future__ import annotations
 
-from typing import Any, Dict
+from typing import Any
 
 import pydantic
 from pydantic.fields import ModelField
@@ -46,7 +46,7 @@ class Base(pydantic.BaseModel):
         return self
 
     @classmethod
-    def get_fields(cls) -> Dict[str, Any]:
+    def get_fields(cls) -> dict[str, Any]:
         """Get the fields of the object.
 
         Returns:

+ 4 - 2
reflex/components/base/meta.py

@@ -1,6 +1,8 @@
 """Display the title of the current page."""
 
-from typing import Dict, Optional
+from __future__ import annotations
+
+from typing import Optional
 
 from reflex.components.base.bare import Bare
 from reflex.components.component import Component
@@ -11,7 +13,7 @@ class Title(Component):
 
     tag = "title"
 
-    def render(self) -> Dict:
+    def render(self) -> dict:
         """Render the title component.
 
         Returns:

+ 2 - 2
reflex/components/base/script.py

@@ -2,7 +2,7 @@
 
 https://nextjs.org/docs/app/api-reference/components/script
 """
-from typing import Set
+from __future__ import annotations
 
 from reflex.components.component import Component
 from reflex.event import EventChain
@@ -57,7 +57,7 @@ class Script(Component):
             raise ValueError("Must provide inline script or `src` prop.")
         return super().create(*children, **props)
 
-    def get_triggers(self) -> Set[str]:
+    def get_triggers(self) -> set[str]:
         """Get the event triggers for the component.
 
         Returns:

+ 7 - 7
reflex/config.py

@@ -28,9 +28,9 @@ class DBConfig(Base):
         cls,
         database: str,
         username: str,
-        password: Optional[str] = None,
-        host: Optional[str] = None,
-        port: Optional[int] = 5432,
+        password: str | None = None,
+        host: str | None = None,
+        port: int | None = 5432,
     ) -> DBConfig:
         """Create an instance with postgresql engine.
 
@@ -58,9 +58,9 @@ class DBConfig(Base):
         cls,
         database: str,
         username: str,
-        password: Optional[str] = None,
-        host: Optional[str] = None,
-        port: Optional[int] = 5432,
+        password: str | None = None,
+        host: str | None = None,
+        port: int | None = 5432,
     ) -> DBConfig:
         """Create an instance with postgresql+psycopg2 engine.
 
@@ -259,7 +259,7 @@ class Config(Base):
                 # Set the value.
                 setattr(self, key, env_var)
 
-    def get_event_namespace(self) -> Optional[str]:
+    def get_event_namespace(self) -> str | None:
         """Get the websocket event namespace.
 
         Returns:

+ 1 - 2
reflex/constants.py

@@ -6,7 +6,6 @@ import platform
 import re
 from enum import Enum
 from types import SimpleNamespace
-from typing import Optional
 
 from platformdirs import PlatformDirs
 
@@ -19,7 +18,7 @@ except ImportError:
 IS_WINDOWS = platform.system() == "Windows"
 
 
-def get_fnm_name() -> Optional[str]:
+def get_fnm_name() -> str | None:
     """Get the appropriate fnm executable name based on the current platform.
 
     Returns:

+ 9 - 11
reflex/event.py

@@ -2,7 +2,7 @@
 from __future__ import annotations
 
 import inspect
-from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
+from typing import Any, Callable, Dict, List, Tuple
 
 from reflex import constants
 from reflex.base import Base
@@ -162,7 +162,7 @@ def server_side(name: str, sig: inspect.Signature, **kwargs) -> EventSpec:
     )
 
 
-def redirect(path: Union[str, Var[str]]) -> EventSpec:
+def redirect(path: str | Var[str]) -> EventSpec:
     """Redirect to a new path.
 
     Args:
@@ -174,7 +174,7 @@ def redirect(path: Union[str, Var[str]]) -> EventSpec:
     return server_side("_redirect", get_fn_signature(redirect), path=path)
 
 
-def console_log(message: Union[str, Var[str]]) -> EventSpec:
+def console_log(message: str | Var[str]) -> EventSpec:
     """Do a console.log on the browser.
 
     Args:
@@ -186,7 +186,7 @@ def console_log(message: Union[str, Var[str]]) -> EventSpec:
     return server_side("_console", get_fn_signature(console_log), message=message)
 
 
-def window_alert(message: Union[str, Var[str]]) -> EventSpec:
+def window_alert(message: str | Var[str]) -> EventSpec:
     """Create a window alert on the browser.
 
     Args:
@@ -250,7 +250,7 @@ def set_cookie(key: str, value: str) -> EventSpec:
     )
 
 
-def remove_cookie(key: str, options: Dict[str, Any] = {}) -> EventSpec:  # noqa: B006
+def remove_cookie(key: str, options: dict[str, Any] = {}) -> EventSpec:  # noqa: B006
     """Remove a cookie on the frontend.
 
     Args:
@@ -378,7 +378,7 @@ def call_event_handler(event_handler: EventHandler, arg: Var) -> EventSpec:
     return event_handler(arg)
 
 
-def call_event_fn(fn: Callable, arg: Var) -> List[EventSpec]:
+def call_event_fn(fn: Callable, arg: Var) -> list[EventSpec]:
     """Call a function to a list of event specs.
 
     The function should return either a single EventSpec or a list of EventSpecs.
@@ -434,7 +434,7 @@ def call_event_fn(fn: Callable, arg: Var) -> List[EventSpec]:
     return events
 
 
-def get_handler_args(event_spec: EventSpec, arg: Var) -> Tuple[Tuple[Var, Var], ...]:
+def get_handler_args(event_spec: EventSpec, arg: Var) -> tuple[tuple[Var, Var], ...]:
     """Get the handler args for the given event spec.
 
     Args:
@@ -449,9 +449,7 @@ def get_handler_args(event_spec: EventSpec, arg: Var) -> Tuple[Tuple[Var, Var],
     return event_spec.args if len(args) > 1 else tuple()
 
 
-def fix_events(
-    events: Optional[List[Union[EventHandler, EventSpec]]], token: str
-) -> List[Event]:
+def fix_events(events: list[EventHandler | EventSpec], token: str) -> list[Event]:
     """Fix a list of events returned by an event handler.
 
     Args:
@@ -510,7 +508,7 @@ def get_fn_signature(fn: Callable) -> inspect.Signature:
 
 
 # A set of common event triggers.
-EVENT_TRIGGERS: Set[str] = {
+EVENT_TRIGGERS: set[str] = {
     "on_focus",
     "on_blur",
     "on_click",

+ 6 - 4
reflex/model.py

@@ -1,5 +1,7 @@
 """Database built into Reflex."""
 
+from __future__ import annotations
+
 import os
 from collections import defaultdict
 from pathlib import Path
@@ -21,7 +23,7 @@ from reflex.config import get_config
 from reflex.utils import console
 
 
-def get_engine(url: Optional[str] = None):
+def get_engine(url: str | None = None):
     """Get the database engine.
 
     Args:
@@ -142,7 +144,7 @@ class Model(Base, sqlmodel.SQLModel):
     def alembic_autogenerate(
         cls,
         connection: sqlalchemy.engine.Connection,
-        message: Optional[str] = None,
+        message: str | None = None,
         write_migration_scripts: bool = True,
     ) -> bool:
         """Generate migration scripts for alembic-detectable changes.
@@ -233,7 +235,7 @@ class Model(Base, sqlmodel.SQLModel):
             env.run_migrations()
 
     @classmethod
-    def migrate(cls, autogenerate: bool = False) -> Optional[bool]:
+    def migrate(cls, autogenerate: bool = False) -> bool | None:
         """Execute alembic migrations for all sqlmodel Model classes.
 
         If alembic is not installed or has not been initialized for the project,
@@ -277,7 +279,7 @@ class Model(Base, sqlmodel.SQLModel):
         return sqlmodel.select(cls)
 
 
-def session(url: Optional[str] = None) -> sqlmodel.Session:
+def session(url: str | None = None) -> sqlmodel.Session:
     """Get a session to interact with the database.
 
     Args:

+ 7 - 10
reflex/page.py

@@ -2,8 +2,6 @@
 
 from __future__ import annotations
 
-from typing import List, Optional, Union
-
 from reflex.components.component import Component
 from reflex.event import EventHandler
 
@@ -11,13 +9,13 @@ DECORATED_PAGES = []
 
 
 def page(
-    route: Optional[str] = None,
-    title: Optional[str] = None,
-    image: Optional[str] = None,
-    description: Optional[str] = None,
-    meta: Optional[str] = None,
-    script_tags: Optional[List[Component]] = None,
-    on_load: Optional[Union[EventHandler, List[EventHandler]]] = None,
+    route: str | None = None,
+    title: str | None = None,
+    image: str | None = None,
+    description: str | None = None,
+    meta: str | None = None,
+    script_tags: list[Component] | None = None,
+    on_load: EventHandler | list[EventHandler] | None = None,
 ):
     """Decorate a function as a page.
 
@@ -40,7 +38,6 @@ def page(
     Returns:
         The decorated function.
     """
-    ...
 
     def decorator(render_fn):
         kwargs = {}

+ 0 - 1
reflex/reflex.py

@@ -81,7 +81,6 @@ def init(
     if not os.path.exists(constants.CONFIG_FILE):
         prerequisites.create_config(app_name)
         prerequisites.initialize_app_directory(app_name, template)
-        build.set_reflex_project_hash()
         telemetry.send("init", config.telemetry_enabled)
     else:
         telemetry.send("reinit", config.telemetry_enabled)

+ 6 - 7
reflex/route.py

@@ -3,7 +3,6 @@
 from __future__ import annotations
 
 import re
-from typing import Dict, List, Optional, Union
 
 from reflex import constants
 from reflex.event import EventHandler
@@ -12,11 +11,11 @@ from reflex.utils.console import deprecate
 
 
 def route(
-    route: Optional[str] = None,
-    title: Optional[str] = None,
-    image: Optional[str] = None,
-    description: Optional[str] = None,
-    on_load: Optional[Union[EventHandler, List[EventHandler]]] = None,
+    route: str | None = None,
+    title: str | None = None,
+    image: str | None = None,
+    description: str | None = None,
+    on_load: EventHandler | list[EventHandler] | None = None,
 ):
     """Decorate a function as a page.
 
@@ -62,7 +61,7 @@ def verify_route_validity(route: str) -> None:
         raise ValueError(f"Catch-all must be the last part of the URL: {route}")
 
 
-def get_route_args(route: str) -> Dict[str, str]:
+def get_route_args(route: str) -> dict[str, str]:
     """Get the dynamic arguments for the given route.
 
     Args:

+ 12 - 13
reflex/state.py

@@ -21,7 +21,6 @@ from typing import (
     Optional,
     Sequence,
     Set,
-    Tuple,
     Type,
     Union,
 )
@@ -87,7 +86,7 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
     # Per-instance copy of backend variable values
     _backend_vars: Dict[str, Any] = {}
 
-    def __init__(self, *args, parent_state: Optional[State] = None, **kwargs):
+    def __init__(self, *args, parent_state: State | None = None, **kwargs):
         """Initialize the state.
 
         Args:
@@ -287,7 +286,7 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
             )
 
     @classmethod
-    def get_skip_vars(cls) -> Set[str]:
+    def get_skip_vars(cls) -> set[str]:
         """Get the vars to skip when serializing.
 
         Returns:
@@ -306,7 +305,7 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
 
     @classmethod
     @functools.lru_cache()
-    def get_parent_state(cls) -> Optional[Type[State]]:
+    def get_parent_state(cls) -> Type[State] | None:
         """Get the parent state.
 
         Returns:
@@ -322,7 +321,7 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
 
     @classmethod
     @functools.lru_cache()
-    def get_substates(cls) -> Set[Type[State]]:
+    def get_substates(cls) -> set[Type[State]]:
         """Get the substates of the state.
 
         Returns:
@@ -493,7 +492,7 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
             field.default = default_value
 
     @staticmethod
-    def _get_base_functions() -> Dict[str, FunctionType]:
+    def _get_base_functions() -> dict[str, FunctionType]:
         """Get all functions of the state class excluding dunder methods.
 
         Returns:
@@ -551,7 +550,7 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
         else:
             return self.router_data.get(constants.RouteVar.PATH, "")
 
-    def get_query_params(self) -> Dict[str, str]:
+    def get_query_params(self) -> dict[str, str]:
         """Obtain the query parameters for the queried page.
 
         The query object contains both the URI parameters and the GET parameters.
@@ -561,7 +560,7 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
         """
         return self.router_data.get(constants.RouteVar.QUERY, {})
 
-    def get_cookies(self) -> Dict[str, str]:
+    def get_cookies(self) -> dict[str, str]:
         """Obtain the cookies of the client stored in the browser.
 
         Returns:
@@ -712,7 +711,7 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
         for substate in self.substates.values():
             substate._reset_client_storage()
 
-    def get_substate(self, path: Sequence[str]) -> Optional[State]:
+    def get_substate(self, path: Sequence[str]) -> State | None:
         """Get the substate.
 
         Args:
@@ -812,7 +811,7 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
 
     async def _process_event(
         self, handler: EventHandler, state: State, payload: Dict
-    ) -> AsyncIterator[Tuple[Optional[List[EventSpec]], bool]]:
+    ) -> AsyncIterator[tuple[list[EventSpec] | None, bool]]:
         """Process event.
 
         Args:
@@ -865,7 +864,7 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
             print(error)
             yield [window_alert("An error occurred. See logs for details.")], True
 
-    def _always_dirty_computed_vars(self) -> Set[str]:
+    def _always_dirty_computed_vars(self) -> set[str]:
         """The set of ComputedVars that always need to be recalculated.
 
         Returns:
@@ -889,7 +888,7 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
                 if actual_var:
                     actual_var.mark_dirty(instance=self)
 
-    def _dirty_computed_vars(self, from_vars: Optional[Set[str]] = None) -> Set[str]:
+    def _dirty_computed_vars(self, from_vars: set[str] | None = None) -> set[str]:
         """Determine ComputedVars that need to be recalculated based on the given vars.
 
         Args:
@@ -976,7 +975,7 @@ class State(Base, ABC, extra=pydantic.Extra.allow):
         self.dirty_vars = set()
         self.dirty_substates = set()
 
-    def dict(self, include_computed: bool = True, **kwargs) -> Dict[str, Any]:
+    def dict(self, include_computed: bool = True, **kwargs) -> dict[str, Any]:
         """Convert the object to a dictionary.
 
         Args:

+ 2 - 2
reflex/style.py

@@ -1,6 +1,6 @@
 """Handle styling."""
 
-from typing import Optional
+from __future__ import annotations
 
 from reflex import constants
 from reflex.event import EventChain
@@ -35,7 +35,7 @@ def convert(style_dict):
 class Style(dict):
     """A style dictionary."""
 
-    def __init__(self, style_dict: Optional[dict] = None):
+    def __init__(self, style_dict: dict | None = None):
         """Initialize the style.
 
         Args:

+ 3 - 36
reflex/utils/build.py

@@ -4,12 +4,10 @@ from __future__ import annotations
 
 import json
 import os
-import random
 import subprocess
 import zipfile
 from enum import Enum
 from pathlib import Path
-from typing import Optional, Union
 
 from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn
 
@@ -18,42 +16,11 @@ from reflex.config import get_config
 from reflex.utils import console, path_ops, prerequisites, processes
 
 
-def update_json_file(file_path: str, update_dict: dict[str, Union[int, str]]):
-    """Update the contents of a json file.
-
-    Args:
-        file_path: the path to the JSON file.
-        update_dict: object to update json.
-    """
-    fp = Path(file_path)
-    # create file if it doesn't exist
-    fp.touch(exist_ok=True)
-    # create an empty json object if file is empty
-    fp.write_text("{}") if fp.stat().st_size == 0 else None
-
-    with open(fp) as f:  # type: ignore
-        json_object: dict = json.load(f)
-        json_object.update(update_dict)
-    with open(fp, "w") as f:
-        json.dump(json_object, f, ensure_ascii=False)
-
-
-def set_reflex_project_hash():
-    """Write the hash of the Reflex project to a REFLEX_JSON."""
-    project_hash = random.getrandbits(128)
-    console.debug(f"Setting project hash to {project_hash}.")
-    update_json_file(constants.REFLEX_JSON, {"project_hash": project_hash})
-
-
 def set_env_json():
     """Write the upload url to a REFLEX_JSON."""
-    update_json_file(
+    path_ops.update_json_file(
         constants.ENV_JSON,
-        {
-            "uploadUrl": constants.Endpoint.UPLOAD.get_url(),
-            "eventUrl": constants.Endpoint.EVENT.get_url(),
-            "pingUrl": constants.Endpoint.PING.get_url(),
-        },
+        {endpoint.name: endpoint.get_url() for endpoint in constants.Endpoint},
     )
 
 
@@ -152,7 +119,7 @@ def export(
     backend: bool = True,
     frontend: bool = True,
     zip: bool = False,
-    deploy_url: Optional[str] = None,
+    deploy_url: str | None = None,
 ):
     """Export the app for deployment.
 

+ 7 - 7
reflex/utils/format.py

@@ -9,7 +9,7 @@ import os
 import os.path as op
 import re
 import sys
-from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type
+from typing import TYPE_CHECKING, Any, Type
 
 import plotly.graph_objects as go
 from plotly.io import to_json
@@ -33,7 +33,7 @@ WRAP_MAP = {
 }
 
 
-def get_close_char(open: str, close: Optional[str] = None) -> str:
+def get_close_char(open: str, close: str | None = None) -> str:
     """Check if the given character is a valid brace.
 
     Args:
@@ -53,7 +53,7 @@ def get_close_char(open: str, close: Optional[str] = None) -> str:
     return WRAP_MAP[open]
 
 
-def is_wrapped(text: str, open: str, close: Optional[str] = None) -> bool:
+def is_wrapped(text: str, open: str, close: str | None = None) -> bool:
     """Check if the given text is wrapped in the given open and close characters.
 
     Args:
@@ -71,7 +71,7 @@ def is_wrapped(text: str, open: str, close: Optional[str] = None) -> bool:
 def wrap(
     text: str,
     open: str,
-    close: Optional[str] = None,
+    close: str | None = None,
     check_first: bool = True,
     num: int = 1,
 ) -> str:
@@ -258,7 +258,7 @@ def format_cond(
     return wrap(f"{cond} ? {true_value} : {false_value}", "{")
 
 
-def get_event_handler_parts(handler: EventHandler) -> Tuple[str, str]:
+def get_event_handler_parts(handler: EventHandler) -> tuple[str, str]:
     """Get the state and function name of an event handler.
 
     Args:
@@ -370,7 +370,7 @@ def format_event_chain(
     )
 
 
-def format_query_params(router_data: Dict[str, Any]) -> Dict[str, str]:
+def format_query_params(router_data: dict[str, Any]) -> dict[str, str]:
     """Convert back query params name to python-friendly case.
 
     Args:
@@ -383,7 +383,7 @@ def format_query_params(router_data: Dict[str, Any]) -> Dict[str, str]:
     return {k.replace("-", "_"): v for k, v in params.items()}
 
 
-def format_dataframe_values(value: Type) -> List[Any]:
+def format_dataframe_values(value: Type) -> list[Any]:
     """Format dataframe values.
 
     Args:

+ 2 - 0
reflex/utils/imports.py

@@ -1,5 +1,7 @@
 """Import operations."""
 
+from __future__ import annotations
+
 from collections import defaultdict
 from typing import Dict, Set
 

+ 34 - 5
reflex/utils/path_ops.py

@@ -2,10 +2,10 @@
 
 from __future__ import annotations
 
+import json
 import os
 import shutil
 from pathlib import Path
-from typing import Optional
 
 from reflex import constants
 
@@ -100,7 +100,7 @@ def ln(src: str, dest: str, overwrite: bool = False) -> bool:
     return True
 
 
-def which(program: str) -> Optional[str]:
+def which(program: str) -> str | None:
     """Find the path to an executable.
 
     Args:
@@ -112,7 +112,7 @@ def which(program: str) -> Optional[str]:
     return shutil.which(program)
 
 
-def get_node_bin_path() -> Optional[str]:
+def get_node_bin_path() -> str | None:
     """Get the node binary dir path.
 
     Returns:
@@ -124,7 +124,7 @@ def get_node_bin_path() -> Optional[str]:
     return constants.NODE_BIN_PATH
 
 
-def get_node_path() -> Optional[str]:
+def get_node_path() -> str | None:
     """Get the node binary path.
 
     Returns:
@@ -135,7 +135,7 @@ def get_node_path() -> Optional[str]:
     return constants.NODE_PATH
 
 
-def get_npm_path() -> Optional[str]:
+def get_npm_path() -> str | None:
     """Get npm binary path.
 
     Returns:
@@ -144,3 +144,32 @@ def get_npm_path() -> Optional[str]:
     if not os.path.exists(constants.NODE_PATH):
         return which("npm")
     return constants.NPM_PATH
+
+
+def update_json_file(file_path: str, update_dict: dict[str, int | str]):
+    """Update the contents of a json file.
+
+    Args:
+        file_path: the path to the JSON file.
+        update_dict: object to update json.
+    """
+    fp = Path(file_path)
+
+    # Create the file if it doesn't exist.
+    fp.touch(exist_ok=True)
+
+    # Create an empty json object if file is empty
+    fp.write_text("{}") if fp.stat().st_size == 0 else None
+
+    # Read the existing json object from the file.
+    json_object = {}
+    if fp.stat().st_size == 0:
+        with open(fp) as f:
+            json_object = json.load(f)
+
+    # Update the json object with the new data.
+    json_object.update(update_dict)
+
+    # Write the updated json object to the file
+    with open(fp, "w") as f:
+        json.dump(json_object, f, ensure_ascii=False)

+ 24 - 12
reflex/utils/prerequisites.py

@@ -6,6 +6,7 @@ import glob
 import json
 import os
 import platform
+import random
 import re
 import stat
 import sys
@@ -14,7 +15,6 @@ import zipfile
 from fileinput import FileInput
 from pathlib import Path
 from types import ModuleType
-from typing import List, Optional
 
 import httpx
 import typer
@@ -44,7 +44,7 @@ def check_node_version() -> bool:
     return False
 
 
-def get_node_version() -> Optional[version.Version]:
+def get_node_version() -> version.Version | None:
     """Get the version of node.
 
     Returns:
@@ -58,7 +58,7 @@ def get_node_version() -> Optional[version.Version]:
         return None
 
 
-def get_bun_version() -> Optional[version.Version]:
+def get_bun_version() -> version.Version | None:
     """Get the version of bun.
 
     Returns:
@@ -72,7 +72,7 @@ def get_bun_version() -> Optional[version.Version]:
         return None
 
 
-def get_install_package_manager() -> Optional[str]:
+def get_install_package_manager() -> str | None:
     """Get the package manager executable for installation.
       Currently on unix systems, bun is used for installation only.
 
@@ -87,7 +87,7 @@ def get_install_package_manager() -> Optional[str]:
     return get_config().bun_path
 
 
-def get_package_manager() -> Optional[str]:
+def get_package_manager() -> str | None:
     """Get the package manager executable for running app.
       Currently on unix systems, npm is used for running the app only.
 
@@ -109,7 +109,7 @@ def get_app() -> ModuleType:
     return __import__(module, fromlist=(constants.APP_VAR,))
 
 
-def get_redis() -> Optional[Redis]:
+def get_redis() -> Redis | None:
     """Get the redis client.
 
     Returns:
@@ -227,10 +227,22 @@ def initialize_web_directory():
     with open(next_config_file, "w") as file:
         file.writelines(lines)
 
-    # Write the current version of distributed reflex package to a REFLEX_JSON."""
-    with open(constants.REFLEX_JSON, "w") as f:
-        reflex_json = {"version": constants.VERSION}
-        json.dump(reflex_json, f, ensure_ascii=False)
+    # Initialize the reflex json file.
+    init_reflex_json()
+
+
+def init_reflex_json():
+    """Write the hash of the Reflex project to a REFLEX_JSON."""
+    # Get a random project hash.
+    project_hash = random.getrandbits(128)
+    console.debug(f"Setting project hash to {project_hash}.")
+
+    # Write the hash and version to the reflex json file.
+    reflex_json = {
+        "version": constants.VERSION,
+        "project_hash": project_hash,
+    }
+    path_ops.update_json_file(constants.REFLEX_JSON, reflex_json)
 
 
 def remove_existing_bun_installation():
@@ -376,11 +388,11 @@ def install_bun():
     )
 
 
-def install_frontend_packages(packages: List[str]):
+def install_frontend_packages(packages: list[str]):
     """Installs the base and custom frontend packages.
 
     Args:
-        packages (List[str]): A list of package names to be installed.
+        packages: A list of package names to be installed.
 
     Example:
         >>> install_frontend_packages(["react", "react-dom"])

+ 2 - 2
reflex/utils/types.py

@@ -4,7 +4,7 @@ from __future__ import annotations
 
 import contextlib
 import typing
-from typing import Any, Callable, Tuple, Type, Union, _GenericAlias  # type: ignore
+from typing import Any, Callable, Type, Union, _GenericAlias  # type: ignore
 
 from reflex.base import Base
 
@@ -17,7 +17,7 @@ StateVar = Union[PrimitiveType, Base, None]
 StateIterVar = Union[list, set, tuple]
 
 
-def get_args(alias: _GenericAlias) -> Tuple[Type, ...]:
+def get_args(alias: _GenericAlias) -> tuple[Type, ...]:
     """Get the arguments of a type alias.
 
     Args:

+ 7 - 9
reflex/vars.py

@@ -73,7 +73,7 @@ class Var(ABC):
     @classmethod
     def create(
         cls, value: Any, is_local: bool = True, is_string: bool = False
-    ) -> Optional[Var]:
+    ) -> Var | None:
         """Create a var from a value.
 
         Args:
@@ -358,10 +358,10 @@ class Var(ABC):
     def operation(
         self,
         op: str = "",
-        other: Optional[Var] = None,
-        type_: Optional[Type] = None,
+        other: Var | None = None,
+        type_: Type | None = None,
         flip: bool = False,
-        fn: Optional[str] = None,
+        fn: str | None = None,
     ) -> Var:
         """Perform an operation on a var.
 
@@ -983,8 +983,8 @@ class ComputedVar(Var, property):
     def deps(
         self,
         objclass: Type,
-        obj: Optional[FunctionType] = None,
-    ) -> Set[str]:
+        obj: FunctionType | None = None,
+    ) -> set[str]:
         """Determine var dependencies of this ComputedVar.
 
         Save references to attributes accessed on "self".  Recursively called
@@ -1375,10 +1375,8 @@ class ImportVar(Base):
 class NoRenderImportVar(ImportVar):
     """A import that doesn't need to be rendered."""
 
-    ...
 
-
-def get_local_storage(key: Optional[Union[Var, str]] = None) -> BaseVar:
+def get_local_storage(key: Var | str | None = None) -> BaseVar:
     """Provide a base var as payload to get local storage item(s).
 
     Args:

+ 3 - 0
reflex/vars.pyi

@@ -160,4 +160,7 @@ class ImportVar(Base):
     def name(self) -> str: ...
     def __hash__(self) -> int: ...
 
+class NoRenderImportVar(ImportVar):
+    """A import that doesn't need to be rendered."""
+
 def get_local_storage(key: Optional[Union[Var, str]] = ...) -> BaseVar: ...