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

This commit is contained in:
Khaleel Al-Adhami 2025-01-23 10:26:04 -08:00
commit 24f341d125
79 changed files with 765 additions and 397 deletions

View File

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

View File

@ -55,7 +55,7 @@ jobs:
path: reflex-web path: reflex-web
- name: Install Requirements for reflex-web - name: Install Requirements for reflex-web
working-directory: ./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 - name: Install additional dependencies for DB access
run: poetry run uv pip install psycopg run: poetry run uv pip install psycopg
- name: Init Website for reflex-web - name: Init Website for reflex-web
@ -73,7 +73,7 @@ jobs:
echo "$outdated" echo "$outdated"
# Ignore 3rd party dependencies that are not updated. # 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) no_extra=$(echo "$filtered_outdated" | grep -vE '\|\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-' || true)

View File

@ -50,14 +50,7 @@ jobs:
- run: poetry run uv pip install pyvirtualdisplay pillow pytest-split pytest-retry - run: poetry run uv pip install pyvirtualdisplay pillow pytest-split pytest-retry
- name: Run app harness tests - name: Run app harness tests
env: env:
SCREENSHOT_DIR: /tmp/screenshots/${{ matrix.state_manager }}/${{ matrix.python-version }}/${{ matrix.split_index }}
REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }} REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }}
run: | run: |
poetry run playwright install chromium poetry run playwright install chromium
poetry run pytest tests/integration --retries 3 --maxfail=5 --splits 2 --group ${{matrix.split_index}} 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

View File

@ -21,7 +21,7 @@ def get_package_size(venv_path: Path, os_name):
ValueError: when venv does not exist or python version is None. ValueError: when venv does not exist or python version is None.
""" """
python_version = get_python_version(venv_path, os_name) 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: if python_version is None:
raise ValueError("Error: Failed to determine Python version.") raise ValueError("Error: Failed to determine Python version.")

View File

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

View File

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

2
poetry.lock generated
View File

@ -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_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_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_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_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-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"},
{file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, {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_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_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_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_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-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"},
{file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"},

View File

@ -87,15 +87,18 @@ reportIncompatibleVariableOverride = false
target-version = "py39" target-version = "py39"
output-format = "concise" output-format = "concise"
lint.isort.split-on-trailing-comma = false 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.ignore = ["B008", "D205", "E501", "F403", "SIM115", "RUF006", "RUF012", "TRY0"]
lint.pydocstyle.convention = "google" lint.pydocstyle.convention = "google"
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"] "__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"] "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"] "*/blank.py" = ["I001"]
[tool.pytest.ini_options] [tool.pytest.ini_options]

View File

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

View File

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

View File

@ -12,7 +12,7 @@ from typing import Callable, Coroutine, Set, Union
from fastapi import FastAPI from fastapi import FastAPI
from reflex.utils import console from reflex.utils import console
from reflex.utils.exceptions import InvalidLifespanTaskType from reflex.utils.exceptions import InvalidLifespanTaskTypeError
from .mixin import AppMixin from .mixin import AppMixin
@ -64,10 +64,10 @@ class LifespanMixin(AppMixin):
task_kwargs: The kwargs of the task. task_kwargs: The kwargs of the task.
Raises: 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): 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." f"Task {task.__name__} of type generator must be decorated with contextlib.asynccontextmanager."
) )

View File

@ -5,14 +5,12 @@ Only the app attribute is explicitly exposed.
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from reflex import constants from reflex import constants
from reflex.utils import telemetry
from reflex.utils.exec import is_prod_mode from reflex.utils.exec import is_prod_mode
from reflex.utils.prerequisites import get_and_validate_app from reflex.utils.prerequisites import get_and_validate_app
if constants.CompileVars.APP != "app": if constants.CompileVars.APP != "app":
raise AssertionError("unexpected variable name for 'app'") raise AssertionError("unexpected variable name for 'app'")
telemetry.send("compile")
app, app_module = get_and_validate_app(reload=False) app, app_module = get_and_validate_app(reload=False)
# For py3.9 compatibility when redis is used, we MUST add any decorator pages # 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). # 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 compile_future
del get_and_validate_app del get_and_validate_app
del is_prod_mode del is_prod_mode
del telemetry
del constants del constants
del ThreadPoolExecutor del ThreadPoolExecutor

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Union
from reflex import constants from reflex import constants
from reflex.utils import imports 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.format import format_library_name
from reflex.utils.serializers import serializer from reflex.utils.serializers import serializer
from reflex.vars import Var, get_unique_variable_name 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. component: The component to bundle the library with.
Raises: 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): if isinstance(component, str):
bundled_libraries.add(component) bundled_libraries.add(component)
return return
if component.library is None: 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)) bundled_libraries.add(format_library_name(component.library))

View File

@ -3,11 +3,13 @@
from typing import Any, Literal, Optional, Union from typing import Any, Literal, Optional, Union
from reflex.event import EventHandler, no_args_event_spec 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 reflex.vars.base import Var
from .base import NextComponent from .base import NextComponent
DEFAULT_W_H = "100%"
class Image(NextComponent): class Image(NextComponent):
"""Display an image.""" """Display an image."""
@ -53,7 +55,7 @@ class Image(NextComponent):
loading: Var[Literal["lazy", "eager"]] 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". # 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. # Fires when the image has loaded.
on_load: EventHandler[no_args_event_spec] on_load: EventHandler[no_args_event_spec]
@ -80,8 +82,16 @@ class Image(NextComponent):
Returns: Returns:
_type_: _description_ _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", {}) style = props.get("style", {})
DEFAULT_W_H = "100%"
def check_prop_type(prop_name, prop_value): def check_prop_type(prop_name, prop_value):
if types.check_prop_in_allowed_types(prop_value, allowed_types=[int]): if types.check_prop_in_allowed_types(prop_value, allowed_types=[int]):

View File

@ -11,6 +11,8 @@ from reflex.vars.base import Var
from .base import NextComponent from .base import NextComponent
DEFAULT_W_H = "100%"
class Image(NextComponent): class Image(NextComponent):
@overload @overload
@classmethod @classmethod
@ -30,7 +32,7 @@ class Image(NextComponent):
loading: Optional[ loading: Optional[
Union[Literal["eager", "lazy"], Var[Literal["eager", "lazy"]]] Union[Literal["eager", "lazy"], Var[Literal["eager", "lazy"]]]
] = None, ] = None,
blurDataURL: Optional[Union[Var[str], str]] = None, blur_data_url: Optional[Union[Var[str], str]] = None,
style: Optional[Style] = None, style: Optional[Style] = None,
key: Optional[Any] = None, key: Optional[Any] = None,
id: 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. 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. 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. 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_load: Fires when the image has loaded.
on_error: Fires when the image has an error. on_error: Fires when the image has an error.
style: The style of the component. style: The style of the component.

View File

@ -95,7 +95,7 @@ class Plotly(NoSSRComponent):
library = "react-plotly.js@2.6.0" 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" tag = "Plot"

View File

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

View File

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

View File

@ -81,7 +81,7 @@ class DrawerRoot(DrawerComponent):
snap_points: Optional[List[Union[float, str]]] = None, snap_points: Optional[List[Union[float, str]]] = None,
fade_from_index: Optional[Union[Var[int], int]] = None, fade_from_index: Optional[Union[Var[int], int]] = None,
scroll_lock_timeout: 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, should_scale_background: Optional[Union[Var[bool], bool]] = None,
close_threshold: Optional[Union[Var[float], float]] = None, close_threshold: Optional[Union[Var[float], float]] = None,
as_child: Optional[Union[Var[bool], bool]] = 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. 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. 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 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. 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. 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. 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, snap_points: Optional[List[Union[float, str]]] = None,
fade_from_index: Optional[Union[Var[int], int]] = None, fade_from_index: Optional[Union[Var[int], int]] = None,
scroll_lock_timeout: 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, should_scale_background: Optional[Union[Var[bool], bool]] = None,
close_threshold: Optional[Union[Var[float], float]] = None, close_threshold: Optional[Union[Var[float], float]] = None,
as_child: Optional[Union[Var[bool], bool]] = 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. 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. 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 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. 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. 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. as_child: Change the default rendered element for the one passed as a child.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -390,7 +390,7 @@ class EnvVar(Generic[T]):
os.environ[self.name] = str(value) os.environ[self.name] = str(value)
class env_var: # type: ignore class env_var: # type: ignore # noqa: N801
"""Descriptor for environment variables.""" """Descriptor for environment variables."""
name: str name: str
@ -556,9 +556,6 @@ class EnvironmentVariables:
# Arguments to pass to the app harness driver. # Arguments to pass to the app harness driver.
APP_HARNESS_DRIVER_ARGS: EnvVar[str] = env_var("") 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. # Whether to check for outdated package versions.
REFLEX_CHECK_LATEST_VERSION: EnvVar[bool] = env_var(True) 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 "api_url" not in self._non_default_attributes:
# If running in Github Codespaces, override API_URL # If running in Github Codespaces, override API_URL
codespace_name = os.getenv("CODESPACE_NAME") 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" "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN"
) )
# If running on Replit.com interactively, override API_URL to ensure we maintain the backend_port # If running on Replit.com interactively, override API_URL to ensure we maintain the backend_port
replit_dev_domain = os.getenv("REPLIT_DEV_DOMAIN") replit_dev_domain = os.getenv("REPLIT_DEV_DOMAIN")
backend_port = kwargs.get("backend_port", self.backend_port) 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 = ( self.api_url = (
f"https://{codespace_name}-{kwargs.get('backend_port', self.backend_port)}" 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: elif replit_dev_domain and backend_port:
self.api_url = f"https://{replit_dev_domain}:{backend_port}" self.api_url = f"https://{replit_dev_domain}:{backend_port}"

View File

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

View File

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

View File

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

View File

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

View File

@ -34,6 +34,18 @@ def _client_state_ref(var_name: str) -> str:
return f"refs['_client_state_{var_name}']" 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( @dataclasses.dataclass(
eq=False, eq=False,
frozen=True, frozen=True,
@ -115,10 +127,41 @@ class ClientStateVar(Var):
"react": [ImportVar(tag="useState"), ImportVar(tag="useId")], "react": [ImportVar(tag="useState"), ImportVar(tag="useId")],
} }
if global_ref: if global_ref:
hooks[f"{_client_state_ref(var_name)} ??= {{}}"] = None arg_name = get_unique_variable_name()
hooks[f"{_client_state_ref(setter_name)} ??= {{}}"] = None func = ArgsFunctionOperationBuilder.create(
hooks[f"{_client_state_ref(var_name)}[{id_name}] = {var_name}"] = None args_names=(arg_name,),
hooks[f"{_client_state_ref(setter_name)}[{id_name}] = {setter_name}"] = None 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) imports.update(_refs_import)
return cls( return cls(
_js_expr="", _js_expr="",
@ -150,7 +193,7 @@ class ClientStateVar(Var):
return ( return (
Var( Var(
_js_expr=( _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 if self._global_ref
else self._getter_name else self._getter_name
), ),
@ -179,26 +222,11 @@ class ClientStateVar(Var):
""" """
_var_data = VarData(imports=_refs_import if self._global_ref else {}) _var_data = VarData(imports=_refs_import if self._global_ref else {})
arg_name = get_unique_variable_name()
setter = ( setter = (
ArgsFunctionOperationBuilder.create( Var(_client_state_ref(self._setter_name))
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,
)
if self._global_ref 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: if value is not NoValue:
# This is a hack to make it work like an EventSpec taking an arg # This is a hack to make it work like an EventSpec taking an arg

View File

@ -26,7 +26,7 @@ def const(name, value) -> Var:
return Var(_js_expr=f"const {name} = {value}") 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. """Create a useCallback hook with a function and dependencies.
Args: 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. """Create a useContext hook with a context.
Args: 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. """Create a useRef hook with a default value.
Args: 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. """Create a useState hook with a variable name and setter name.
Args: Args:

View File

@ -109,7 +109,7 @@ class DrawerSidebar(DrawerRoot):
snap_points: Optional[List[Union[float, str]]] = None, snap_points: Optional[List[Union[float, str]]] = None,
fade_from_index: Optional[Union[Var[int], int]] = None, fade_from_index: Optional[Union[Var[int], int]] = None,
scroll_lock_timeout: 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, should_scale_background: Optional[Union[Var[bool], bool]] = None,
close_threshold: Optional[Union[Var[float], float]] = None, close_threshold: Optional[Union[Var[float], float]] = None,
as_child: Optional[Union[Var[bool], bool]] = None, as_child: Optional[Union[Var[bool], bool]] = None,

View File

@ -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(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(script_cli, name="script", help="Subcommands running helper scripts.")
cli.add_typer( cli.add_typer(

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ class ConfigError(ReflexError):
"""Custom exception for config related errors.""" """Custom exception for config related errors."""
class InvalidStateManagerMode(ReflexError, ValueError): class InvalidStateManagerModeError(ReflexError, ValueError):
"""Raised when an invalid state manager mode is provided.""" """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.""" """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.""" """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.""" """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.""" """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.""" """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.""" """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.""" """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.""" """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.""" """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.""" """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.""" """Raised when a dynamic component has an invalid signature."""

View File

@ -364,11 +364,11 @@ def run_uvicorn_backend_prod(host, port, loglevel):
app_module = get_app_module() 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 = 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_windows = f"uvicorn --limit-max-requests {config.gunicorn_max_requests} --timeout-keep-alive {config.timeout}".split()
command = ( command = (
[ [
*RUN_BACKEND_PROD_WINDOWS, *run_backend_prod_windows,
"--host", "--host",
host, host,
"--port", "--port",
@ -377,7 +377,7 @@ def run_uvicorn_backend_prod(host, port, loglevel):
] ]
if constants.IS_WINDOWS if constants.IS_WINDOWS
else [ else [
*RUN_BACKEND_PROD, *run_backend_prod,
"--bind", "--bind",
f"{host}:{port}", f"{host}:{port}",
"--threads", "--threads",

View File

@ -7,6 +7,7 @@ import dataclasses
import functools import functools
import importlib import importlib
import importlib.metadata import importlib.metadata
import importlib.util
import json import json
import os import os
import platform import platform
@ -37,7 +38,7 @@ from reflex.compiler import templates
from reflex.config import Config, environment, get_config from reflex.config import Config, environment, get_config
from reflex.utils import console, net, path_ops, processes, redir from reflex.utils import console, net, path_ops, processes, redir
from reflex.utils.exceptions import ( from reflex.utils.exceptions import (
GeneratedCodeHasNoFunctionDefs, GeneratedCodeHasNoFunctionDefsError,
SystemPackageMissingError, SystemPackageMissingError,
) )
from reflex.utils.format import format_library_name 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 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): def create_config(app_name: str):
"""Create a new rxconfig file. """Create a new rxconfig file.
@ -921,6 +1083,7 @@ def install_bun():
constants.Bun.INSTALL_URL, constants.Bun.INSTALL_URL,
f"bun-v{constants.Bun.VERSION}", f"bun-v{constants.Bun.VERSION}",
BUN_INSTALL=str(constants.Bun.ROOT_PATH), 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. generation_hash: The generation hash from reflex.build.
Raises: 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). (the refactored reflex code is expected to have at least one root function defined).
""" """
# Download the reflex code for the generation. # 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. # Determine the name of the last function, which renders the generated code.
defined_funcs = re.findall(r"def ([a-zA-Z_]+)\(", resp.text) defined_funcs = re.findall(r"def ([a-zA-Z_]+)\(", resp.text)
if not defined_funcs: if not defined_funcs:
raise GeneratedCodeHasNoFunctionDefs( raise GeneratedCodeHasNoFunctionDefsError(
f"No function definitions found in generated code from {url!r}." f"No function definitions found in generated code from {url!r}."
) )
render_func_name = defined_funcs[-1] render_func_name = defined_funcs[-1]

View File

@ -1909,7 +1909,7 @@ def figure_out_type(value: Any) -> types.GenericType:
return type(value) 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.""" """A special version of functools.cached_property that does not use a lock."""
def __init__(self, func): def __init__(self, func):

View File

@ -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}]]""" function_type_hint = f"""FunctionVar[ReflexCallable[[{", ".join(required_params + optional_params)}], {return_type}]]"""
NEWLINE = "\n" new_line = "\n"
overloads.append( overloads.append(
f""" f"""
@overload @overload
def call( def call(
self: {function_type_hint}, self: {function_type_hint},
{"," + NEWLINE + " ".join(required_args + optional_args)} {"," + new_line + " ".join(required_args + optional_args)}
) -> {return_type_var}: ... ) -> {return_type_var}: ...
""" """
) )

View File

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

View File

@ -78,6 +78,14 @@ case $platform in
;; ;;
esac esac
case "$target" in
'linux'*)
if [ -f /etc/alpine-release ]; then
target="$target-musl"
fi
;;
esac
if [[ $target = darwin-x64 ]]; then if [[ $target = darwin-x64 ]]; then
# Is this process running in Rosetta? # Is this process running in Rosetta?
# redirect stderr to devnull to avoid error message when not 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" 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 if [[ $(sysctl -a | grep machdep.cpu | grep AVX2) == '' ]]; then
target=darwin-x64-baseline target="$target-baseline"
fi fi
fi ;;
'linux-x64'*)
if [[ $target = linux-x64 ]]; then
# If AVX2 isn't supported, use the -baseline build # If AVX2 isn't supported, use the -baseline build
if [[ $(cat /proc/cpuinfo | grep avx2) = '' ]]; then if [[ $(cat /proc/cpuinfo | grep avx2) = '' ]]; then
target=linux-x64-baseline target="$target-baseline"
fi fi
fi ;;
esac
exe_name=bun 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." info "You requested a debug build of bun. More information will be shown if a crash occurs."
fi fi
bun_version=BUN_VERSION
if [[ $# = 0 ]]; then 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 else
bun_uri=$github_repo/releases/download/$1/bun-$target.zip bun_uri=$github_repo/releases/download/$1/bun-$target.zip
fi fi

View File

@ -214,8 +214,12 @@ function Install-Bun {
# http://community.sqlbackupandftp.com/t/error-1073741515-solved/1305 # http://community.sqlbackupandftp.com/t/error-1073741515-solved/1305
if (($LASTEXITCODE -eq 3221225781) -or ($LASTEXITCODE -eq -1073741515)) # STATUS_DLL_NOT_FOUND 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 "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 "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" Write-Output "The command '${BunBin}\bun.exe --revision' exited with code ${LASTEXITCODE}`n"
return 1 return 1
} }

View File

@ -1,8 +1,6 @@
"""Shared conftest for all integration tests.""" """Shared conftest for all integration tests."""
import os import os
import re
from pathlib import Path
import pytest import pytest
@ -36,34 +34,6 @@ def xvfb():
yield None 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( @pytest.fixture(
scope="session", params=[AppHarness, AppHarnessProd], ids=["dev", "prod"] scope="session", params=[AppHarness, AppHarnessProd], ids=["dev", "prod"]
) )

View File

@ -172,7 +172,7 @@ def BackgroundTask():
rx.button("Reset", on_click=State.reset_counter, id="reset"), 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) app.add_page(index)
@ -288,7 +288,7 @@ def test_background_task(
assert background_task._poll_for(lambda: counter.text == "620", timeout=40) assert background_task._poll_for(lambda: counter.text == "620", timeout=40)
# all tasks should have exited and cleaned up # all tasks should have exited and cleaned up
assert background_task._poll_for( 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
) )

View File

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

View File

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

View File

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

View File

@ -36,7 +36,7 @@ def ConnectionBanner():
rx.button("Delay", id="delay", on_click=State.delay), 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) app.add_page(index)

View File

@ -26,7 +26,7 @@ def DeployUrlSample() -> None:
rx.button("GOTO SELF", on_click=State.goto_self, id="goto_self") 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) app.add_page(index)

View File

@ -138,7 +138,7 @@ def DynamicRoute():
def redirect_page(): def redirect_page():
return rx.fragment(rx.text("redirecting...")) 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="/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, route="/static/x", on_load=DynamicState.on_load) # type: ignore
app.add_page(index) app.add_page(index)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -45,7 +45,7 @@ def LoginSample():
rx.button("Do it", on_click=State.login, id="doit"), 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(index)
app.add_page(login) app.add_page(login)

View File

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

View File

@ -166,7 +166,7 @@ def UploadFile():
rx.text(UploadState.event_order.to_string(), id="event-order"), 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) app.add_page(index)

View File

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

View File

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

View File

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

View File

@ -1,11 +1,8 @@
"""Test fixtures.""" """Test fixtures."""
import asyncio import asyncio
import contextlib
import os
import platform import platform
import uuid import uuid
from pathlib import Path
from typing import Dict, Generator, Type from typing import Dict, Generator, Type
from unittest import mock from unittest import mock
@ -14,6 +11,7 @@ import pytest
from reflex.app import App from reflex.app import App
from reflex.event import EventSpec from reflex.event import EventSpec
from reflex.model import ModelRegistry from reflex.model import ModelRegistry
from reflex.testing import chdir
from reflex.utils import prerequisites from reflex.utils import prerequisites
from .states import ( 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 @pytest.fixture
def tmp_working_dir(tmp_path): def tmp_working_dir(tmp_path):
"""Create a temporary directory and chdir to it. """Create a temporary directory and chdir to it.

View File

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

View File

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

View File

@ -1,20 +1,28 @@
import json import json
import re import re
import shutil
import tempfile import tempfile
from pathlib import Path
from unittest.mock import Mock, mock_open from unittest.mock import Mock, mock_open
import pytest import pytest
from typer.testing import CliRunner
from reflex import constants from reflex import constants
from reflex.config import Config from reflex.config import Config
from reflex.reflex import cli
from reflex.testing import chdir
from reflex.utils.prerequisites import ( from reflex.utils.prerequisites import (
CpuInfo, CpuInfo,
_update_next_config, _update_next_config,
cached_procedure, cached_procedure,
get_cpu_info, get_cpu_info,
initialize_requirements_txt, initialize_requirements_txt,
rename_imports_and_app_name,
) )
runner = CliRunner()
@pytest.mark.parametrize( @pytest.mark.parametrize(
"config, export, expected_output", "config, export, expected_output",
@ -224,3 +232,156 @@ def test_get_cpu_info():
for attr in ("manufacturer_id", "model_name", "address_width"): for attr in ("manufacturer_id", "model_name", "address_width"):
value = getattr(cpu_info, attr) value = getattr(cpu_info, attr)
assert value.strip() if attr != "address_width" else value 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)
"""
)

View File

@ -89,7 +89,7 @@ def app():
], ],
) )
def test_check_routes_conflict_invalid(mocker, app, route1, route2): 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): with pytest.raises(ValueError):
app._check_routes_conflict(route2) 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): 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. # test that running this does not throw an error.
app._check_routes_conflict(route2) app._check_routes_conflict(route2)

View File

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

View File

@ -222,7 +222,7 @@ async def state_manager_redis(
Yields: Yields:
A state manager instance 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 state_manager = app_module_mock.app.state_manager
if not isinstance(state_manager, StateManagerRedis): if not isinstance(state_manager, StateManagerRedis):

View File

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

View File

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