Merge remote-tracking branch 'origin/main' into lendemor/rename_private_fields

This commit is contained in:
Masen Furer 2025-01-22 15:29:46 -08:00
commit 35f71cbfc3
No known key found for this signature in database
GPG Key ID: B0008AD22B3B3A95
72 changed files with 1567 additions and 1554 deletions

View File

@ -81,24 +81,18 @@ jobs:
matrix:
# Show OS combos first in GUI
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.9.21", "3.10.16", "3.11.11", "3.12.8"]
python-version: ['3.10.16', '3.11.11', '3.12.8']
exclude:
- os: windows-latest
python-version: "3.10.16"
- os: windows-latest
python-version: "3.9.21"
python-version: '3.10.16'
# keep only one python version for MacOS
- os: macos-latest
python-version: "3.9.21"
- os: macos-latest
python-version: "3.10.16"
python-version: '3.10.16'
- os: macos-latest
python-version: "3.11.11"
include:
- os: windows-latest
python-version: "3.10.11"
- os: windows-latest
python-version: "3.9.13"
python-version: '3.10.11'
runs-on: ${{ matrix.os }}
steps:

View File

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

View File

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

View File

@ -43,22 +43,17 @@ jobs:
matrix:
# Show OS combos first in GUI
os: [ubuntu-latest, windows-latest]
python-version: ["3.9.21", "3.10.16", "3.11.11", "3.12.8", "3.13.1"]
# Windows is a bit behind on Python version availability in Github
python-version: ['3.10.16', '3.11.11', '3.12.8', '3.13.1']
exclude:
- os: windows-latest
python-version: "3.11.11"
- os: windows-latest
python-version: "3.10.16"
- os: windows-latest
python-version: "3.9.21"
python-version: '3.10.16'
include:
- os: windows-latest
python-version: "3.11.9"
- os: windows-latest
python-version: "3.10.11"
- os: windows-latest
python-version: "3.9.13"
python-version: '3.10.11'
runs-on: ${{ matrix.os }}
steps:

View File

@ -28,22 +28,18 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.9.21", "3.10.16", "3.11.11", "3.12.8", "3.13.1"]
python-version: ["3.10.16", "3.11.11", "3.12.8", "3.13.1"]
# Windows is a bit behind on Python version availability in Github
exclude:
- os: windows-latest
python-version: "3.11.11"
- os: windows-latest
python-version: "3.10.16"
- os: windows-latest
python-version: "3.9.21"
include:
- os: windows-latest
python-version: "3.11.9"
- os: windows-latest
python-version: "3.10.11"
- os: windows-latest
python-version: "3.9.13"
runs-on: ${{ matrix.os }}
# Service containers to run with `runner-job`
@ -92,8 +88,8 @@ jobs:
strategy:
fail-fast: false
matrix:
# Note: py39, py310, py311 versions chosen due to available arm64 darwin builds.
python-version: ["3.9.13", "3.10.11", "3.11.9", "3.12.8", "3.13.1"]
# Note: py310, py311 versions chosen due to available arm64 darwin builds.
python-version: ["3.10.11", "3.11.9", "3.12.8", "3.13.1"]
runs-on: macos-latest
steps:
- uses: actions/checkout@v4

View File

@ -28,7 +28,7 @@ repos:
entry: python3 scripts/make_pyi.py
- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.313
rev: v1.1.334
hooks:
- id: pyright
args: [reflex, tests]

View File

@ -8,7 +8,7 @@ Here is a quick guide on how to run Reflex repo locally so you can start contrib
**Prerequisites:**
- Python >= 3.9
- Python >= 3.10
- Poetry version >= 1.4.0 and add it to your path (see [Poetry Docs](https://python-poetry.org/docs/#installation) for more info).
**1. Fork this repository:**
@ -87,7 +87,7 @@ poetry run ruff format .
```
Consider installing git pre-commit hooks so Ruff, Pyright, Darglint and `make_pyi` will run automatically before each commit.
Note that pre-commit will only be installed when you use a Python version >= 3.9.
Note that pre-commit will only be installed when you use a Python version >= 3.10.
``` bash
pre-commit install

View File

@ -34,7 +34,7 @@ See our [architecture page](https://reflex.dev/blog/2024-03-21-reflex-architectu
## ⚙️ Installation
Open a terminal and run (Requires Python 3.9+):
Open a terminal and run (Requires Python 3.10+):
```bash
pip install reflex

View File

@ -34,7 +34,7 @@ Auf unserer [Architektur-Seite](https://reflex.dev/blog/2024-03-21-reflex-archit
## ⚙️ Installation
Öffne ein Terminal und führe den folgenden Befehl aus (benötigt Python 3.9+):
Öffne ein Terminal und führe den folgenden Befehl aus (benötigt Python 3.10+):
```bash
pip install reflex

View File

@ -35,7 +35,7 @@ Consulta nuestra [página de arquitectura](https://reflex.dev/blog/2024-03-21-re
## ⚙️ Instalación
Abra un terminal y ejecute (Requiere Python 3.9+):
Abra un terminal y ejecute (Requiere Python 3.10+):
```bash
pip install reflex

View File

@ -35,7 +35,7 @@ Reflex के अंदर के कामकाज को जानने क
## ⚙️ इंस्टॉलेशन (Installation)
एक टर्मिनल खोलें और चलाएं (Python 3.9+ की आवश्यकता है):
एक टर्मिनल खोलें और चलाएं (Python 3.10+ की आवश्यकता है):
```bash
pip install reflex

View File

@ -22,7 +22,7 @@
## ⚙️ Installazione
Apri un terminale ed esegui (Richiede Python 3.9+):
Apri un terminale ed esegui (Richiede Python 3.10+):
```bash
pip install reflex

View File

@ -37,7 +37,7 @@ Reflex がどのように動作しているかを知るには、[アーキテク
## ⚙️ インストール
ターミナルを開いて以下のコマンドを実行してください。Python 3.9 以上が必要です。):
ターミナルを開いて以下のコマンドを実行してください。Python 3.10 以上が必要です。):
```bash
pip install reflex

View File

@ -20,7 +20,7 @@
---
## ⚙️ 설치
터미널을 열고 실행하세요. (Python 3.9+ 필요):
터미널을 열고 실행하세요. (Python 3.10+ 필요):
```bash
pip install reflex

View File

@ -34,7 +34,7 @@
## ⚙️ Installation - نصب و راه اندازی
یک ترمینال را باز کنید و اجرا کنید (نیازمند Python 3.9+):
یک ترمینال را باز کنید و اجرا کنید (نیازمند Python 3.10+):
```bash
pip install reflex

View File

@ -21,7 +21,7 @@
---
## ⚙️ Instalação
Abra um terminal e execute (Requer Python 3.9+):
Abra um terminal e execute (Requer Python 3.10+):
```bash
pip install reflex

View File

@ -24,7 +24,7 @@
## ⚙️ Kurulum
Bir terminal açın ve çalıştırın (Python 3.9+ gerekir):
Bir terminal açın ve çalıştırın (Python 3.10+ gerekir):
```bash
pip install reflex

View File

@ -34,7 +34,7 @@ Các tính năng chính:
## ⚙️ Cài đặt
Mở cửa sổ lệnh và chạy (Yêu cầu Python phiên bản 3.9+):
Mở cửa sổ lệnh và chạy (Yêu cầu Python phiên bản 3.10+):
```bash
pip install reflex

View File

@ -34,7 +34,7 @@ Reflex 是一个使用纯Python构建全栈web应用的库。
## ⚙️ 安装
打开一个终端并且运行(要求Python3.9+):
打开一个终端并且运行(要求Python3.10+):
```bash
pip install reflex

View File

@ -36,7 +36,7 @@ Reflex 是一個可以用純 Python 構建全端網頁應用程式的函式庫
## ⚙️ 安裝
開啟一個終端機並且執行 (需要 Python 3.9+):
開啟一個終端機並且執行 (需要 Python 3.10+):
```bash
pip install reflex

1676
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -18,7 +18,7 @@ keywords = ["web", "framework"]
classifiers = ["Development Status :: 4 - Beta"]
[tool.poetry.dependencies]
python = "^3.9"
python = "^3.10"
fastapi = ">=0.96.0,!=0.111.0,!=0.111.1"
gunicorn = ">=20.1.0,<24.0"
jinja2 = ">=3.1.2,<4.0"
@ -50,7 +50,6 @@ httpx = ">=0.25.1,<1.0"
twine = ">=4.0.0,<7.0"
tomlkit = ">=0.12.4,<1.0"
lazy_loader = ">=0.4"
reflex-chakra = ">=0.6.0"
typing_extensions = ">=4.6.0"
[tool.poetry.group.dev.dependencies]
@ -102,5 +101,5 @@ asyncio_default_fixture_loop_scope = "function"
asyncio_mode = "auto"
[tool.codespell]
skip = "docs/*,*.html,examples/*, *.pyi"
skip = "docs/*,*.html,examples/*, *.pyi, poetry.lock"
ignore-words-list = "te, TreeE"

View File

@ -8,7 +8,7 @@ version = "0.0.1"
description = "Reflex custom component {{ module_name }}"
readme = "README.md"
license = { text = "Apache-2.0" }
requires-python = ">=3.9"
requires-python = ">=3.10"
authors = [{ name = "", email = "YOUREMAIL@domain.com" }]
keywords = ["reflex","reflex-custom-components"]

View File

@ -303,7 +303,6 @@ _MAPPING: dict = {
"event": [
"EventChain",
"EventHandler",
"background",
"call_script",
"call_function",
"run_script",
@ -367,19 +366,4 @@ getattr, __dir__, __all__ = lazy_loader.attach(
def __getattr__(name):
if name == "chakra":
from reflex.utils import console
console.deprecate(
"rx.chakra",
reason="and moved to a separate package. "
"To continue using Chakra UI components, install the `reflex-chakra` package via `pip install "
"reflex-chakra`.",
deprecation_version="0.6.0",
removal_version="0.7.0",
dedupe=True,
)
import reflex_chakra as rc
return rc
return getattr(name)

View File

@ -156,7 +156,6 @@ from .constants import Env as Env
from .constants.colors import Color as Color
from .event import EventChain as EventChain
from .event import EventHandler as EventHandler
from .event import background as background
from .event import call_function as call_function
from .event import call_script as call_script
from .event import clear_local_storage as clear_local_storage

View File

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

View File

@ -23,8 +23,6 @@ from typing import (
Union,
)
from typing_extensions import deprecated
import reflex.state
from reflex.base import Base
from reflex.compiler.templates import STATEFUL_COMPONENT
@ -47,11 +45,10 @@ from reflex.event import (
EventChain,
EventHandler,
EventSpec,
EventVar,
no_args_event_spec,
)
from reflex.style import Style, format_as_emotion
from reflex.utils import console, format, imports, types
from reflex.utils import format, imports, types
from reflex.utils.imports import (
ImmutableParsedImportDict,
ImportDict,
@ -547,41 +544,6 @@ class Component(BaseComponent, ABC):
# Construct the component.
super().__init__(*args, **kwargs)
@deprecated("Use rx.EventChain.create instead.")
def _create_event_chain(
self,
args_spec: types.ArgsSpec | Sequence[types.ArgsSpec],
value: Union[
Var,
EventHandler,
EventSpec,
List[Union[EventHandler, EventSpec, EventVar]],
Callable,
],
key: Optional[str] = None,
) -> Union[EventChain, Var]:
"""Create an event chain from a variety of input types.
Args:
args_spec: The args_spec of the event trigger being bound.
value: The value to create the event chain from.
key: The key of the event trigger being bound.
Returns:
The event chain.
"""
console.deprecate(
"Component._create_event_chain",
"Use rx.EventChain.create instead.",
deprecation_version="0.6.8",
removal_version="0.7.0",
)
return EventChain.create(
value=value, # type: ignore
args_spec=args_spec,
key=key,
)
def get_event_triggers(
self,
) -> Dict[str, types.ArgsSpec | Sequence[types.ArgsSpec]]:

View File

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

View File

@ -14,7 +14,7 @@ from reflex.components.radix.themes.layout.box import Box
from reflex.constants.colors import Color
from reflex.event import set_clipboard
from reflex.style import Style
from reflex.utils import console, format
from reflex.utils import format
from reflex.utils.imports import ImportVar
from reflex.vars.base import LiteralVar, Var, VarData
@ -382,7 +382,7 @@ for theme_name in dir(Theme):
class CodeBlock(Component, MarkdownComponentMap):
"""A code block."""
library = "react-syntax-highlighter@15.6.0"
library = "react-syntax-highlighter@15.6.1"
tag = "PrismAsyncLight"
@ -438,6 +438,8 @@ class CodeBlock(Component, MarkdownComponentMap):
can_copy = props.pop("can_copy", False)
copy_button = props.pop("copy_button", None)
# react-syntax-highlighter doesn't have an explicit "light" or "dark" theme so we use one-light and one-dark
# themes respectively to ensure code compatibility.
if "theme" not in props:
# Default color scheme responds to global color mode.
props["theme"] = color_mode_cond(
@ -445,17 +447,6 @@ class CodeBlock(Component, MarkdownComponentMap):
dark=Theme.one_dark,
)
# react-syntax-highlighter doesn't have an explicit "light" or "dark" theme so we use one-light and one-dark
# themes respectively to ensure code compatibility.
if "theme" in props and not isinstance(props["theme"], Var):
props["theme"] = getattr(Theme, format.to_snake_case(props["theme"])) # type: ignore
console.deprecate(
feature_name="theme prop as string",
reason="Use code_block.themes instead.",
deprecation_version="0.6.0",
removal_version="0.7.0",
)
if can_copy:
code = children[0]
copy_button = ( # type: ignore

View File

@ -102,7 +102,7 @@ class Fieldset(Element):
name: Var[Union[str, int, bool]]
def on_submit_event_spec() -> Tuple[Var[Dict[str, Any]]]:
def on_submit_event_spec() -> Tuple[Var[dict[str, Any]]]:
"""Event handler spec for the on_submit event.
Returns:
@ -111,7 +111,7 @@ def on_submit_event_spec() -> Tuple[Var[Dict[str, Any]]]:
return (FORM_DATA,)
def on_submit_string_event_spec() -> Tuple[Var[Dict[str, str]]]:
def on_submit_string_event_spec() -> Tuple[Var[dict[str, str]]]:
"""Event handler spec for the on_submit event.
Returns:

View File

@ -270,8 +270,8 @@ class Fieldset(Element):
"""
...
def on_submit_event_spec() -> Tuple[Var[Dict[str, Any]]]: ...
def on_submit_string_event_spec() -> Tuple[Var[Dict[str, str]]]: ...
def on_submit_event_spec() -> Tuple[Var[dict[str, Any]]]: ...
def on_submit_string_event_spec() -> Tuple[Var[dict[str, str]]]: ...
class Form(BaseHTML):
@overload
@ -341,10 +341,10 @@ class Form(BaseHTML):
on_submit: Optional[
Union[
Union[
EventType[[], BASE_STATE], EventType[[Dict[str, Any]], BASE_STATE]
EventType[[], BASE_STATE], EventType[[dict[str, Any]], BASE_STATE]
],
Union[
EventType[[], BASE_STATE], EventType[[Dict[str, str]], BASE_STATE]
EventType[[], BASE_STATE], EventType[[dict[str, str]], BASE_STATE]
],
]
] = None,

View File

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

View File

@ -132,10 +132,10 @@ class FormRoot(FormComponent, HTMLForm):
on_submit: Optional[
Union[
Union[
EventType[[], BASE_STATE], EventType[[Dict[str, Any]], BASE_STATE]
EventType[[], BASE_STATE], EventType[[dict[str, Any]], BASE_STATE]
],
Union[
EventType[[], BASE_STATE], EventType[[Dict[str, str]], BASE_STATE]
EventType[[], BASE_STATE], EventType[[dict[str, str]], BASE_STATE]
],
]
] = None,
@ -608,10 +608,10 @@ class Form(FormRoot):
on_submit: Optional[
Union[
Union[
EventType[[], BASE_STATE], EventType[[Dict[str, Any]], BASE_STATE]
EventType[[], BASE_STATE], EventType[[dict[str, Any]], BASE_STATE]
],
Union[
EventType[[], BASE_STATE], EventType[[Dict[str, str]], BASE_STATE]
EventType[[], BASE_STATE], EventType[[dict[str, str]], BASE_STATE]
],
]
] = None,
@ -741,10 +741,10 @@ class FormNamespace(ComponentNamespace):
on_submit: Optional[
Union[
Union[
EventType[[], BASE_STATE], EventType[[Dict[str, Any]], BASE_STATE]
EventType[[], BASE_STATE], EventType[[dict[str, Any]], BASE_STATE]
],
Union[
EventType[[], BASE_STATE], EventType[[Dict[str, str]], BASE_STATE]
EventType[[], BASE_STATE], EventType[[dict[str, str]], BASE_STATE]
],
]
] = None,

View File

@ -88,11 +88,13 @@ class ChartBase(RechartsCharts):
"width": width if width is not None else "100%",
"height": height if height is not None else "100%",
}
# Provide min dimensions so the graph always appears, even if the outer container is zero-size.
if width is None:
dim_props["min_width"] = 200
if height is None:
dim_props["min_height"] = 100
# Ensure that the min_height and min_width are set to prevent the chart from collapsing.
# We are using small values so that height and width can still be used over min_height and min_width.
# Without this, sometimes the chart will not be visible. Causing confusion to the user.
# With this, the user will see a small chart and can adjust the height and width and can figure out that the issue is with the size.
dim_props["min_height"] = props.pop("min_height", 10)
dim_props["min_width"] = props.pop("min_width", 10)
return ResponsiveContainer.create(
super().create(*children, **props),

View File

@ -36,11 +36,11 @@ class ResponsiveContainer(Recharts, MemoizationLeaf):
# The height of chart container. Can be a number or string. Default: "100%"
height: Var[Union[int, str]]
# The minimum width of chart container. Number
min_width: Var[int]
# The minimum width of chart container. Number or string.
min_width: Var[Union[int, str]]
# The minimum height of chart container. Number
min_height: Var[int]
# The minimum height of chart container. Number or string.
min_height: Var[Union[int, str]]
# If specified a positive number, debounced function will be used to handle the resize event. Default: 0
debounce: Var[int]

View File

@ -22,8 +22,8 @@ class ResponsiveContainer(Recharts, MemoizationLeaf):
aspect: Optional[Union[Var[int], int]] = None,
width: Optional[Union[Var[Union[int, str]], int, str]] = None,
height: Optional[Union[Var[Union[int, str]], int, str]] = None,
min_width: Optional[Union[Var[int], int]] = None,
min_height: Optional[Union[Var[int], int]] = None,
min_width: Optional[Union[Var[Union[int, str]], int, str]] = None,
min_height: Optional[Union[Var[Union[int, str]], int, str]] = None,
debounce: Optional[Union[Var[int], int]] = None,
style: Optional[Style] = None,
key: Optional[Any] = None,
@ -56,8 +56,8 @@ class ResponsiveContainer(Recharts, MemoizationLeaf):
aspect: The aspect ratio of the container. The final aspect ratio of the SVG element will be (width / height) * aspect. Number
width: The width of chart container. Can be a number or string. Default: "100%"
height: The height of chart container. Can be a number or string. Default: "100%"
min_width: The minimum width of chart container. Number
min_height: The minimum height of chart container. Number
min_width: The minimum width of chart container. Number or string.
min_height: The minimum height of chart container. Number or string.
debounce: If specified a positive number, debounced function will be used to handle the resize event. Default: 0
on_resize: If specified provides a callback providing the updated chart width and height values.
style: The style of the component.

View File

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

View File

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

View File

@ -556,9 +556,6 @@ class EnvironmentVariables:
# Arguments to pass to the app harness driver.
APP_HARNESS_DRIVER_ARGS: EnvVar[str] = env_var("")
# Where to save screenshots when tests fail.
SCREENSHOT_DIR: EnvVar[Optional[Path]] = env_var(None)
# Whether to check for outdated package versions.
REFLEX_CHECK_LATEST_VERSION: EnvVar[bool] = env_var(True)

View File

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

View File

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

View File

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

View File

@ -25,7 +25,6 @@ from typing import (
overload,
)
import typing_extensions
from typing_extensions import (
Concatenate,
ParamSpec,
@ -40,7 +39,11 @@ from typing_extensions import (
from reflex import constants
from reflex.constants.state import FRONTEND_EVENT_STATE
from reflex.utils import console, format
from reflex.utils.exceptions import EventFnArgMismatch, EventHandlerArgTypeMismatch
from reflex.utils.exceptions import (
EventFnArgMismatchError,
EventHandlerArgTypeMismatchError,
MissingAnnotationError,
)
from reflex.utils.types import ArgsSpec, GenericType, typehint_issubclass
from reflex.vars import VarData
from reflex.vars.base import LiteralVar, Var
@ -96,32 +99,6 @@ _EVENT_FIELDS: set[str] = {f.name for f in dataclasses.fields(Event)}
BACKGROUND_TASK_MARKER = "_reflex_background_task"
def background(fn, *, __internal_reflex_call: bool = False):
"""Decorator to mark event handler as running in the background.
Args:
fn: The function to decorate.
Returns:
The same function, but with a marker set.
Raises:
TypeError: If the function is not a coroutine function or async generator.
"""
if not __internal_reflex_call:
console.deprecate(
"background-decorator",
"Use `rx.event(background=True)` instead.",
"0.6.5",
"0.7.0",
)
if not inspect.iscoroutinefunction(fn) and not inspect.isasyncgenfunction(fn):
raise TypeError("Background task must be async function or generator.")
setattr(fn, BACKGROUND_TASK_MARKER, True)
return fn
@dataclasses.dataclass(
init=True,
frozen=True,
@ -813,29 +790,10 @@ def server_side(name: str, sig: inspect.Signature, **kwargs) -> EventSpec:
)
@overload
def redirect(
path: str | Var[str],
is_external: Optional[bool] = None,
is_external: bool = False,
replace: bool = False,
) -> EventSpec: ...
@overload
@typing_extensions.deprecated("`external` is deprecated use `is_external` instead")
def redirect(
path: str | Var[str],
is_external: Optional[bool] = None,
replace: bool = False,
external: Optional[bool] = None,
) -> EventSpec: ...
def redirect(
path: str | Var[str],
is_external: Optional[bool] = None,
replace: bool = False,
external: Optional[bool] = None,
) -> EventSpec:
"""Redirect to a new path.
@ -843,26 +801,10 @@ def redirect(
path: The path to redirect to.
is_external: Whether to open in new tab or not.
replace: If True, the current page will not create a new history entry.
external(Deprecated): Whether to open in new tab or not.
Returns:
An event to redirect to the path.
"""
if external is not None:
console.deprecate(
"The `external` prop in `rx.redirect`",
"use `is_external` instead.",
"0.6.6",
"0.7.0",
)
# is_external should take precedence over external.
is_external = (
(False if external is None else external)
if is_external is None
else is_external
)
return server_side(
"_redirect",
get_fn_signature(redirect),
@ -1279,11 +1221,14 @@ def call_event_handler(
event_spec: The lambda that define the argument(s) to pass to the event handler.
key: The key to pass to the event handler.
Raises:
EventHandlerArgTypeMismatchError: If the event handler arguments do not match the event spec. #noqa: DAR402
TypeError: If the event handler arguments are invalid.
Returns:
The event spec from calling the event handler.
# noqa: DAR401 failure
#noqa: DAR401
"""
event_spec_args = parse_args_spec(event_spec) # type: ignore
@ -1320,10 +1265,15 @@ def call_event_handler(
),
)
)
type_match_found: dict[str, bool] = {}
delayed_exceptions: list[EventHandlerArgTypeMismatchError] = []
try:
type_hints_of_provided_callback = get_type_hints(event_callback.fn)
except NameError:
type_hints_of_provided_callback = {}
if event_spec_return_types:
failures = []
event_callback_spec = inspect.getfullargspec(event_callback.fn)
for event_spec_index, event_spec_return_type in enumerate(
@ -1335,43 +1285,35 @@ def call_event_handler(
arg if get_origin(arg) is not Var else get_args(arg)[0] for arg in args
]
try:
type_hints_of_provided_callback = get_type_hints(event_callback.fn)
except NameError:
type_hints_of_provided_callback = {}
failed_type_check = False
# check that args of event handler are matching the spec if type hints are provided
for i, arg in enumerate(event_callback_spec.args[1:]):
if arg not in type_hints_of_provided_callback:
continue
type_match_found.setdefault(arg, False)
try:
compare_result = typehint_issubclass(
args_types_without_vars[i], type_hints_of_provided_callback[arg]
)
except TypeError:
# TODO: In 0.7.0, remove this block and raise the exception
# raise TypeError(
# f"Could not compare types {args_types_without_vars[i]} and {type_hints_of_provided_callback[arg]} for argument {arg} of {event_handler.fn.__qualname__} provided for {key}." # noqa: ERA001
# ) from e
console.warn(
except TypeError as te:
raise TypeError(
f"Could not compare types {args_types_without_vars[i]} and {type_hints_of_provided_callback[arg]} for argument {arg} of {event_callback.fn.__qualname__} provided for {key}."
)
compare_result = False
) from te
if compare_result:
type_match_found[arg] = True
continue
else:
failure = EventHandlerArgTypeMismatch(
f"Event handler {key} expects {args_types_without_vars[i]} for argument {arg} but got {type_hints_of_provided_callback[arg]} as annotated in {event_callback.fn.__qualname__} instead."
type_match_found[arg] = False
delayed_exceptions.append(
EventHandlerArgTypeMismatchError(
f"Event handler {key} expects {args_types_without_vars[i]} for argument {arg} but got {type_hints_of_provided_callback[arg]} as annotated in {event_callback.fn.__qualname__} instead."
)
)
failures.append(failure)
failed_type_check = True
break
if not failed_type_check:
if all(type_match_found.values()):
delayed_exceptions.clear()
if event_spec_index:
args = get_args(event_spec_return_types[0])
@ -1393,15 +1335,10 @@ def call_event_handler(
f"Event handler {key} expects ({expect_string}) -> () but got ({given_string}) -> () as annotated in {event_callback.fn.__qualname__} instead. "
f"This may lead to unexpected behavior but is intentionally ignored for {key}."
)
return event_callback(*event_spec_args)
break
if failures:
console.deprecate(
"Mismatched event handler argument types",
"\n".join([str(f) for f in failures]),
"0.6.5",
"0.7.0",
)
if delayed_exceptions:
raise delayed_exceptions[0]
return event_callback(*event_spec_args) # type: ignore
@ -1420,26 +1357,26 @@ def unwrap_var_annotation(annotation: GenericType):
return annotation
def resolve_annotation(annotations: dict[str, Any], arg_name: str):
def resolve_annotation(annotations: dict[str, Any], arg_name: str, spec: ArgsSpec):
"""Resolve the annotation for the given argument name.
Args:
annotations: The annotations.
arg_name: The argument name.
spec: The specs which the annotations come from.
Raises:
MissingAnnotationError: If the annotation is missing for non-lambda methods.
Returns:
The resolved annotation.
"""
annotation = annotations.get(arg_name)
if annotation is None:
console.deprecate(
feature_name="Unannotated event handler arguments",
reason="Provide type annotations for event handler arguments.",
deprecation_version="0.6.3",
removal_version="0.7.0",
)
# Allow arbitrary attribute access two levels deep until removed.
return Dict[str, dict]
if not isinstance(spec, types.LambdaType):
raise MissingAnnotationError(var_name=arg_name)
else:
return dict[str, dict]
return annotation
@ -1461,7 +1398,13 @@ def parse_args_spec(arg_spec: ArgsSpec | Sequence[ArgsSpec]):
arg_spec(
*[
Var(f"_{l_arg}").to(
unwrap_var_annotation(resolve_annotation(annotations, l_arg))
unwrap_var_annotation(
resolve_annotation(
annotations,
l_arg,
spec=arg_spec,
)
)
)
for l_arg in spec.args
]
@ -1477,7 +1420,7 @@ def check_fn_match_arg_spec(
func_name: str | None = None,
):
"""Ensures that the function signature matches the passed argument specification
or raises an EventFnArgMismatch if they do not.
or raises an EventFnArgMismatchError if they do not.
Args:
user_func: The function to be validated.
@ -1487,7 +1430,7 @@ def check_fn_match_arg_spec(
func_name: The name of the function to be validated.
Raises:
EventFnArgMismatch: Raised if the number of mandatory arguments do not match
EventFnArgMismatchError: Raised if the number of mandatory arguments do not match
"""
user_args = inspect.getfullargspec(user_func).args
# Drop the first argument if it's a bound method
@ -1503,7 +1446,7 @@ def check_fn_match_arg_spec(
number_of_event_args = len(parsed_event_args)
if number_of_user_args - number_of_user_default_args > number_of_event_args:
raise EventFnArgMismatch(
raise EventFnArgMismatchError(
f"Event {key} only provides {number_of_event_args} arguments, but "
f"{func_name or user_func} requires at least {number_of_user_args - number_of_user_default_args} "
"arguments to be passed to the event handler.\n"
@ -1812,8 +1755,6 @@ V3 = TypeVar("V3")
V4 = TypeVar("V4")
V5 = TypeVar("V5")
background_event_decorator = background
class EventCallback(Generic[P, T]):
"""A descriptor that wraps a function to be used as an event."""
@ -1986,6 +1927,9 @@ class EventNamespace(types.SimpleNamespace):
func: The function to wrap.
background: Whether the event should be run in the background. Defaults to False.
Raises:
TypeError: If background is True and the function is not a coroutine or async generator. # noqa: DAR402
Returns:
The wrapped function.
"""
@ -1994,7 +1938,13 @@ class EventNamespace(types.SimpleNamespace):
func: Callable[Concatenate[BASE_STATE, P], T],
) -> EventCallback[P, T]:
if background is True:
return background_event_decorator(func, __internal_reflex_call=True) # type: ignore
if not inspect.iscoroutinefunction(
func
) and not inspect.isasyncgenfunction(func):
raise TypeError(
"Background task must be async function or generator."
)
setattr(func, BACKGROUND_TASK_MARKER, True)
return func # type: ignore
if func is not None:

View File

@ -9,7 +9,6 @@ from reflex.components.sonner.toast import toast as toast
from ..utils.console import warn
from . import hooks as hooks
from .assets import asset as asset
from .client_state import ClientStateVar as ClientStateVar
from .layout import layout as layout
from .misc import run_in_thread as run_in_thread
@ -62,7 +61,6 @@ class ExperimentalNamespace(SimpleNamespace):
_x = ExperimentalNamespace(
asset=asset,
client_state=ClientStateVar.create,
hooks=hooks,
layout=layout,

View File

@ -1,37 +0,0 @@
"""Helper functions for adding assets to the app."""
from typing import Optional
from reflex import assets
from reflex.utils import console
def asset(relative_filename: str, subfolder: Optional[str] = None) -> str:
"""DEPRECATED: use `rx.asset` with `shared=True` instead.
Add an asset to the app.
Place the file next to your including python file.
Copies the file to the app's external assets directory.
Example:
```python
rx.script(src=rx._x.asset("my_custom_javascript.js"))
rx.image(src=rx._x.asset("test_image.png","subfolder"))
```
Args:
relative_filename: The relative filename of the asset.
subfolder: The directory to place the asset in.
Returns:
The relative URL to the copied asset.
"""
console.deprecate(
feature_name="rx._x.asset",
reason="Use `rx.asset` with `shared=True` instead of `rx._x.asset`.",
deprecation_version="0.6.6",
removal_version="0.7.0",
)
return assets.asset(
relative_filename, shared=True, subfolder=subfolder, _stack_level=2
)

View File

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

View File

@ -306,6 +306,9 @@ def export(
help="Whether to exclude sqlite db files when exporting backend.",
hidden=True,
),
env: constants.Env = typer.Option(
constants.Env.PROD, help="The environment to export the app in."
),
loglevel: constants.LogLevel = typer.Option(
config.loglevel, help="The log level to use."
),
@ -323,6 +326,7 @@ def export(
backend=backend,
zip_dest_dir=zip_dest_dir,
upload_db_file=upload_db_file,
env=env,
loglevel=loglevel.subprocess_level(),
)
@ -501,6 +505,7 @@ def deploy(
),
):
"""Deploy the app to the Reflex hosting service."""
from reflex_cli.constants.base import LogLevel as HostingLogLevel
from reflex_cli.utils import dependency
from reflex_cli.v2 import cli as hosting_cli
@ -512,6 +517,21 @@ def deploy(
# Set the log level.
console.set_log_level(loglevel)
def convert_reflex_loglevel_to_reflex_cli_loglevel(
loglevel: constants.LogLevel,
) -> HostingLogLevel:
if loglevel == constants.LogLevel.DEBUG:
return HostingLogLevel.DEBUG
if loglevel == constants.LogLevel.INFO:
return HostingLogLevel.INFO
if loglevel == constants.LogLevel.WARNING:
return HostingLogLevel.WARNING
if loglevel == constants.LogLevel.ERROR:
return HostingLogLevel.ERROR
if loglevel == constants.LogLevel.CRITICAL:
return HostingLogLevel.CRITICAL
return HostingLogLevel.INFO
# Only check requirements if interactive.
# There is user interaction for requirements update.
if interactive:
@ -521,9 +541,7 @@ def deploy(
if prerequisites.needs_reinit(frontend=True):
_init(name=config.app_name, loglevel=loglevel)
prerequisites.check_latest_package_version(constants.ReflexHostingCLI.MODULE_NAME)
extra: dict[str, str] = (
{"config_path": config_path} if config_path is not None else {}
)
hosting_cli.deploy(
app_name=app_name,
app_id=app_id,
@ -547,15 +565,28 @@ def deploy(
envfile=envfile,
hostname=hostname,
interactive=interactive,
loglevel=type(loglevel).INFO, # type: ignore
loglevel=convert_reflex_loglevel_to_reflex_cli_loglevel(loglevel),
token=token,
project=project,
config_path=config_path,
project_name=project_name,
**extra,
**({"config_path": config_path} if config_path is not None else {}),
)
@cli.command()
def rename(
new_name: str = typer.Argument(..., help="The new name for the app."),
loglevel: constants.LogLevel = typer.Option(
config.loglevel, help="The log level to use."
),
):
"""Rename the app in the current directory."""
from reflex.utils import prerequisites
prerequisites.validate_app_name(new_name)
prerequisites.rename_app(new_name, loglevel)
cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.")
cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.")
cli.add_typer(

View File

@ -1341,12 +1341,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
if field.allow_none and not is_optional(field_type):
field_type = Union[field_type, None]
if not _isinstance(value, field_type):
console.deprecate(
"mismatched-type-assignment",
f"Tried to assign value {value} of type {type(value)} to field {type(self).__name__}.{name} of type {field_type}."
" This might lead to unexpected behavior.",
"0.6.5",
"0.7.0",
console.error(
f"Expected field '{type(self).__name__}.{name}' to receive type '{field_type}',"
f" but got '{value}' of type '{type(value)}'."
)
# Set the attribute.
@ -1831,7 +1828,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow):
if (
isinstance(value, dict)
and inspect.isclass(hinted_args)
and not types.is_generic_alias(hinted_args) # py3.9-py3.10
and not types.is_generic_alias(hinted_args) # py3.10
):
if issubclass(hinted_args, Model):
# Remove non-fields from the payload

View File

@ -287,7 +287,9 @@ class Style(dict):
_var = LiteralVar.create(value)
if _var is not None:
# Carry the imports/hooks when setting a Var as a value.
self._var_data = VarData.merge(self._var_data, _var._get_all_var_data())
self._var_data = VarData.merge(
getattr(self, "_var_data", None), _var._get_all_var_data()
)
super().__setitem__(key, value)

View File

@ -933,6 +933,7 @@ class AppHarnessProd(AppHarness):
frontend=True,
backend=False,
loglevel=reflex.constants.LogLevel.INFO,
env=reflex.constants.Env.PROD,
)
self.frontend_thread = threading.Thread(target=self._run_frontend)

View File

@ -51,20 +51,12 @@ def set_log_level(log_level: LogLevel):
log_level: The log level to set.
Raises:
ValueError: If the log level is invalid.
TypeError: If the log level is a string.
"""
if not isinstance(log_level, LogLevel):
deprecate(
feature_name="Passing a string to set_log_level",
reason="use reflex.constants.LogLevel enum instead",
deprecation_version="0.6.6",
removal_version="0.7.0",
raise TypeError(
f"log_level must be a LogLevel enum value, got {log_level} of type {type(log_level)} instead."
)
try:
log_level = getattr(LogLevel, log_level.upper())
except AttributeError as ae:
raise ValueError(f"Invalid log level: {log_level}") from ae
global _LOG_LEVEL
_LOG_LEVEL = log_level

View File

@ -1,6 +1,6 @@
"""Custom Exceptions."""
from typing import Any, NoReturn
from typing import Any
class ReflexError(Exception):
@ -75,6 +75,30 @@ class VarAttributeError(ReflexError, AttributeError):
"""Custom AttributeError for var related errors."""
class UntypedComputedVarError(ReflexError, TypeError):
"""Custom TypeError for untyped computed var errors."""
def __init__(self, var_name):
"""Initialize the UntypedComputedVarError.
Args:
var_name: The name of the computed var.
"""
super().__init__(f"Computed var '{var_name}' must have a type annotation.")
class MissingAnnotationError(ReflexError, TypeError):
"""Custom TypeError for missing annotations."""
def __init__(self, var_name):
"""Initialize the MissingAnnotationError.
Args:
var_name: The name of the var.
"""
super().__init__(f"Var '{var_name}' must have a type annotation.")
class UploadValueError(ReflexError, ValueError):
"""Custom ValueError for upload related errors."""
@ -111,11 +135,11 @@ class MatchTypeError(ReflexError, TypeError):
"""Raised when the return types of match cases are different."""
class EventHandlerArgTypeMismatch(ReflexError, TypeError):
class EventHandlerArgTypeMismatchError(ReflexError, TypeError):
"""Raised when the annotations of args accepted by an EventHandler differs from the spec of the event trigger."""
class EventFnArgMismatch(ReflexError, TypeError):
class EventFnArgMismatchError(ReflexError, TypeError):
"""Raised when the number of args required by an event handler is more than provided by the event trigger."""
@ -186,29 +210,27 @@ class StateMismatchError(ReflexError, ValueError):
class SystemPackageMissingError(ReflexError):
"""Raised when a system package is missing."""
def __init__(self, package: str):
"""Initialize the SystemPackageMissingError.
Args:
package: The missing package.
"""
from reflex.constants import IS_MACOS
extra = (
f" You can do so by running 'brew install {package}'." if IS_MACOS else ""
)
super().__init__(
f"System package '{package}' is missing."
f" Please install it through your system package manager.{extra}"
)
class EventDeserializationError(ReflexError, ValueError):
"""Raised when an event cannot be deserialized."""
def raise_system_package_missing_error(package: str) -> NoReturn:
"""Raise a SystemPackageMissingError.
Args:
package: The name of the missing system package.
Raises:
SystemPackageMissingError: The raised exception.
"""
from reflex.constants import IS_MACOS
raise SystemPackageMissingError(
f"System package '{package}' is missing."
" Please install it through your system package manager."
+ (f" You can do so by running 'brew install {package}'." if IS_MACOS else "")
)
class InvalidLockWarningThresholdError(ReflexError):
"""Raised when an invalid lock warning threshold is provided."""

View File

@ -240,25 +240,28 @@ def run_backend(
run_uvicorn_backend(host, port, loglevel)
def get_reload_dirs() -> list[str]:
def get_reload_dirs() -> list[Path]:
"""Get the reload directories for the backend.
Returns:
The reload directories for the backend.
"""
config = get_config()
reload_dirs = [config.app_name]
reload_dirs = [Path(config.app_name)]
if config.app_module is not None and config.app_module.__file__:
module_path = Path(config.app_module.__file__).resolve().parent
while module_path.parent.name:
for parent_file in module_path.parent.iterdir():
if parent_file == "__init__.py":
# go up a level to find dir without `__init__.py`
module_path = module_path.parent
break
if any(
sibling_file.name == "__init__.py"
for sibling_file in module_path.parent.iterdir()
):
# go up a level to find dir without `__init__.py`
module_path = module_path.parent
else:
break
reload_dirs.append(str(module_path))
reload_dirs = [module_path]
return reload_dirs
@ -278,7 +281,7 @@ def run_uvicorn_backend(host, port, loglevel: LogLevel):
port=port,
log_level=loglevel.value,
reload=True,
reload_dirs=get_reload_dirs(),
reload_dirs=list(map(str, get_reload_dirs())),
)
@ -526,48 +529,3 @@ def is_prod_mode() -> bool:
"""
current_mode = environment.REFLEX_ENV_MODE.get()
return current_mode == constants.Env.PROD
def is_frontend_only() -> bool:
"""Check if the app is running in frontend-only mode.
Returns:
True if the app is running in frontend-only mode.
"""
console.deprecate(
"is_frontend_only() is deprecated and will be removed in a future release.",
reason="Use `environment.REFLEX_FRONTEND_ONLY.get()` instead.",
deprecation_version="0.6.5",
removal_version="0.7.0",
)
return environment.REFLEX_FRONTEND_ONLY.get()
def is_backend_only() -> bool:
"""Check if the app is running in backend-only mode.
Returns:
True if the app is running in backend-only mode.
"""
console.deprecate(
"is_backend_only() is deprecated and will be removed in a future release.",
reason="Use `environment.REFLEX_BACKEND_ONLY.get()` instead.",
deprecation_version="0.6.5",
removal_version="0.7.0",
)
return environment.REFLEX_BACKEND_ONLY.get()
def should_skip_compile() -> bool:
"""Whether the app should skip compile.
Returns:
True if the app should skip compile.
"""
console.deprecate(
"should_skip_compile() is deprecated and will be removed in a future release.",
reason="Use `environment.REFLEX_SKIP_COMPILE.get()` instead.",
deprecation_version="0.6.5",
removal_version="0.7.0",
)
return environment.REFLEX_SKIP_COMPILE.get()

View File

@ -4,7 +4,7 @@ from pathlib import Path
from typing import Optional
from reflex import constants
from reflex.config import get_config
from reflex.config import environment, get_config
from reflex.utils import build, console, exec, prerequisites, telemetry
config = get_config()
@ -18,6 +18,7 @@ def export(
upload_db_file: bool = False,
api_url: Optional[str] = None,
deploy_url: Optional[str] = None,
env: constants.Env = constants.Env.PROD,
loglevel: constants.LogLevel = console._LOG_LEVEL,
):
"""Export the app to a zip file.
@ -30,11 +31,15 @@ def export(
upload_db_file: Whether to upload the database file. Defaults to False.
api_url: The API URL to use. Defaults to None.
deploy_url: The deploy URL to use. Defaults to None.
env: The environment to use. Defaults to constants.Env.PROD.
loglevel: The log level to use. Defaults to console._LOG_LEVEL.
"""
# Set the log level.
console.set_log_level(loglevel)
# Set env mode in the environment
environment.REFLEX_ENV_MODE.set(env)
# Override the config url values if provided.
if api_url is not None:
config.api_url = str(api_url)

View File

@ -11,7 +11,6 @@ from typing import TYPE_CHECKING, Any, List, Optional, Union
from reflex import constants
from reflex.constants.state import FRONTEND_EVENT_STATE
from reflex.utils import exceptions
from reflex.utils.console import deprecate
if TYPE_CHECKING:
from reflex.components.component import ComponentStyle
@ -502,37 +501,6 @@ if TYPE_CHECKING:
from reflex.vars import Var
def format_event_chain(
event_chain: EventChain | Var[EventChain],
event_arg: Var | None = None,
) -> str:
"""DEPRECATED: format an event chain as a javascript invocation.
Use str(rx.Var.create(event_chain)) instead.
Args:
event_chain: The event chain to format.
event_arg: this argument is ignored.
Returns:
Compiled javascript code to queue the given event chain on the frontend.
"""
deprecate(
feature_name="format_event_chain",
reason="Use str(rx.Var.create(event_chain)) instead",
deprecation_version="0.6.0",
removal_version="0.7.0",
)
from reflex.vars import Var
from reflex.vars.function import ArgsFunctionOperation
result = Var.create(event_chain)
if isinstance(result, ArgsFunctionOperation):
result = result._return_expr
return str(result)
def format_queue_events(
events: EventType | None = None,
args_spec: Optional[ArgsSpec] = None,

View File

@ -7,6 +7,7 @@ import dataclasses
import functools
import importlib
import importlib.metadata
import importlib.util
import json
import os
import platform
@ -38,7 +39,7 @@ from reflex.config import Config, environment, get_config
from reflex.utils import console, net, path_ops, processes, redir
from reflex.utils.exceptions import (
GeneratedCodeHasNoFunctionDefs,
raise_system_package_missing_error,
SystemPackageMissingError,
)
from reflex.utils.format import format_library_name
from reflex.utils.registry import _get_npm_registry
@ -86,18 +87,6 @@ def get_web_dir() -> Path:
return environment.REFLEX_WEB_WORKDIR.get()
def _python_version_check():
"""Emit deprecation warning for deprecated python versions."""
# Check for end-of-life python versions.
if sys.version_info < (3, 10):
console.deprecate(
feature_name="Support for Python 3.9 and older",
reason="please upgrade to Python 3.10 or newer",
deprecation_version="0.6.0",
removal_version="0.7.0",
)
def check_latest_package_version(package_name: str):
"""Check if the latest version of the package is installed.
@ -120,8 +109,6 @@ def check_latest_package_version(package_name: str):
console.warn(
f"Your version ({current_version}) of {package_name} is out of date. Upgrade to {latest_version} with 'pip install {package_name} --upgrade'"
)
# Check for deprecated python versions
_python_version_check()
except Exception:
pass
@ -477,6 +464,167 @@ def validate_app_name(app_name: str | None = None) -> str:
return app_name
def rename_path_up_tree(full_path: str | Path, old_name: str, new_name: str) -> Path:
"""Rename all instances of `old_name` in the path (file and directories) to `new_name`.
The renaming stops when we reach the directory containing `rxconfig.py`.
Args:
full_path: The full path to start renaming from.
old_name: The name to be replaced.
new_name: The replacement name.
Returns:
The updated path after renaming.
"""
current_path = Path(full_path)
new_path = None
while True:
directory, base = current_path.parent, current_path.name
# Stop renaming when we reach the root dir (which contains rxconfig.py)
if current_path.is_dir() and (current_path / "rxconfig.py").exists():
new_path = current_path
break
if old_name == base.removesuffix(constants.Ext.PY):
new_base = base.replace(old_name, new_name)
new_path = directory / new_base
current_path.rename(new_path)
console.debug(f"Renamed {current_path} -> {new_path}")
current_path = new_path
else:
new_path = current_path
# Move up the directory tree
current_path = directory
return new_path
def rename_app(new_app_name: str, loglevel: constants.LogLevel):
"""Rename the app directory.
Args:
new_app_name: The new name for the app.
loglevel: The log level to use.
Raises:
Exit: If the command is not ran in the root dir or the app module cannot be imported.
"""
# Set the log level.
console.set_log_level(loglevel)
if not constants.Config.FILE.exists():
console.error(
"No rxconfig.py found. Make sure you are in the root directory of your app."
)
raise typer.Exit(1)
sys.path.insert(0, str(Path.cwd()))
config = get_config()
module_path = importlib.util.find_spec(config.module)
if module_path is None:
console.error(f"Could not find module {config.module}.")
raise typer.Exit(1)
if not module_path.origin:
console.error(f"Could not find origin for module {config.module}.")
raise typer.Exit(1)
console.info(f"Renaming app directory to {new_app_name}.")
process_directory(
Path.cwd(),
config.app_name,
new_app_name,
exclude_dirs=[constants.Dirs.WEB, constants.Dirs.APP_ASSETS],
)
rename_path_up_tree(Path(module_path.origin), config.app_name, new_app_name)
console.success(f"App directory renamed to [bold]{new_app_name}[/bold].")
def rename_imports_and_app_name(file_path: str | Path, old_name: str, new_name: str):
"""Rename imports the file using string replacement as well as app_name in rxconfig.py.
Args:
file_path: The file to process.
old_name: The old name to replace.
new_name: The new name to use.
"""
file_path = Path(file_path)
content = file_path.read_text()
# Replace `from old_name.` or `from old_name` with `from new_name`
content = re.sub(
rf"\bfrom {re.escape(old_name)}(\b|\.|\s)",
lambda match: f"from {new_name}{match.group(1)}",
content,
)
# Replace `import old_name` with `import new_name`
content = re.sub(
rf"\bimport {re.escape(old_name)}\b",
f"import {new_name}",
content,
)
# Replace `app_name="old_name"` in rx.Config
content = re.sub(
rf'\bapp_name\s*=\s*["\']{re.escape(old_name)}["\']',
f'app_name="{new_name}"',
content,
)
# Replace positional argument `"old_name"` in rx.Config
content = re.sub(
rf'\brx\.Config\(\s*["\']{re.escape(old_name)}["\']',
f'rx.Config("{new_name}"',
content,
)
file_path.write_text(content)
def process_directory(
directory: str | Path,
old_name: str,
new_name: str,
exclude_dirs: list | None = None,
extensions: list | None = None,
):
"""Process files with specified extensions in a directory, excluding specified directories.
Args:
directory: The root directory to process.
old_name: The old name to replace.
new_name: The new name to use.
exclude_dirs: List of directory names to exclude. Defaults to None.
extensions: List of file extensions to process.
"""
exclude_dirs = exclude_dirs or []
extensions = extensions or [
constants.Ext.PY,
constants.Ext.MD,
] # include .md files, typically used in reflex-web.
extensions_set = {ext.lstrip(".") for ext in extensions}
directory = Path(directory)
root_exclude_dirs = {directory / exclude_dir for exclude_dir in exclude_dirs}
files = (
p.resolve()
for p in directory.glob("**/*")
if p.is_file() and p.suffix.lstrip(".") in extensions_set
)
for file_path in files:
if not any(
file_path.is_relative_to(exclude_dir) for exclude_dir in root_exclude_dirs
):
rename_imports_and_app_name(file_path, old_name, new_name)
def create_config(app_name: str):
"""Create a new rxconfig file.
@ -885,7 +1033,11 @@ def install_node():
def install_bun():
"""Install bun onto the user's system."""
"""Install bun onto the user's system.
Raises:
SystemPackageMissingError: If "unzip" is missing.
"""
win_supported = is_windows_bun_supported()
one_drive_in_path = windows_check_onedrive_in_path()
if constants.IS_WINDOWS and (not win_supported or one_drive_in_path):
@ -924,13 +1076,14 @@ def install_bun():
else:
unzip_path = path_ops.which("unzip")
if unzip_path is None:
raise_system_package_missing_error("unzip")
raise SystemPackageMissingError("unzip")
# Run the bun install script.
download_and_run(
constants.Bun.INSTALL_URL,
f"bun-v{constants.Bun.VERSION}",
BUN_INSTALL=str(constants.Bun.ROOT_PATH),
BUN_VERSION=str(constants.Bun.VERSION),
)

View File

@ -122,7 +122,7 @@ def _prepare_event(event: str, **kwargs) -> dict:
return {}
if UTC is None:
# for python 3.9 & 3.10
# for python 3.10
stamp = datetime.utcnow().isoformat()
else:
# for python 3.11 & 3.12

View File

@ -46,6 +46,7 @@ from reflex.base import Base
from reflex.constants.compiler import Hooks
from reflex.utils import console, exceptions, imports, serializers, types
from reflex.utils.exceptions import (
UntypedComputedVarError,
VarAttributeError,
VarDependencyError,
VarTypeError,
@ -545,52 +546,21 @@ class Var(Generic[VAR_TYPE]):
def create(
cls,
value: Any,
_var_is_local: bool | None = None,
_var_is_string: bool | None = None,
_var_data: VarData | None = None,
) -> Var:
"""Create a var from a value.
Args:
value: The value to create the var from.
_var_is_local: Whether the var is local. Deprecated.
_var_is_string: Whether the var is a string literal. Deprecated.
_var_data: Additional hooks and imports associated with the Var.
Returns:
The var.
"""
if _var_is_local is not None:
console.deprecate(
feature_name="_var_is_local",
reason="The _var_is_local argument is not supported for Var. "
"If you want to create a Var from a raw Javascript expression, use the constructor directly",
deprecation_version="0.6.0",
removal_version="0.7.0",
)
if _var_is_string is not None:
console.deprecate(
feature_name="_var_is_string",
reason="The _var_is_string argument is not supported for Var. "
"If you want to create a Var from a raw Javascript expression, use the constructor directly",
deprecation_version="0.6.0",
removal_version="0.7.0",
)
# If the value is already a var, do nothing.
if isinstance(value, Var):
return value
# Try to pull the imports and hooks from contained values.
if not isinstance(value, str):
return LiteralVar.create(value, _var_data=_var_data)
if _var_is_string is False or _var_is_local is True:
return cls(
_js_expr=value,
_var_data=_var_data,
)
return LiteralVar.create(value, _var_data=_var_data)
@classmethod
@ -1863,19 +1833,14 @@ class ComputedVar(Var[RETURN_TYPE]):
Raises:
TypeError: If the computed var dependencies are not Var instances or var names.
UntypedComputedVarError: If the computed var is untyped.
"""
hint = kwargs.pop("return_type", None) or get_type_hints(fget).get(
"return", Any
)
if hint is Any:
console.deprecate(
"untyped-computed-var",
"ComputedVar should have a return type annotation.",
"0.6.5",
"0.7.0",
)
raise UntypedComputedVarError(var_name=fget.__name__)
kwargs.setdefault("_js_expr", fget.__name__)
kwargs.setdefault("_var_type", hint)
@ -1948,6 +1913,7 @@ class ComputedVar(Var[RETURN_TYPE]):
"_var_data": kwargs.pop(
"_var_data", VarData.merge(self._var_data, merge_var_data)
),
"return_type": kwargs.pop("return_type", self._var_type),
}
if kwargs:
@ -2082,12 +2048,9 @@ class ComputedVar(Var[RETURN_TYPE]):
value = getattr(instance, self._cache_attr)
if not _isinstance(value, self._var_type):
console.deprecate(
"mismatched-computed-var-return",
f"Computed var {type(instance).__name__}.{self._js_expr} returned value of type {type(value)}, "
f"expected {self._var_type}. This might cause unexpected behavior.",
"0.6.5",
"0.7.0",
console.error(
f"Computed var '{type(instance).__name__}.{self._js_expr}' must return"
f" type '{self._var_type}', got '{type(value)}'."
)
return value

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
FROM python:3.9
FROM python:3.10
ARG USERNAME=kerrigan
RUN useradd -m $USERNAME

View File

@ -16,7 +16,7 @@ from .utils import SessionStorage
def CallScript():
"""A test app for browser javascript integration."""
from pathlib import Path
from typing import Dict, List, Optional, Union
from typing import Optional, Union
import reflex as rx
@ -43,15 +43,17 @@ def CallScript():
external_scripts = inline_scripts.replace("inline", "external")
class CallScriptState(rx.State):
results: List[Optional[Union[str, Dict, List]]] = []
inline_counter: int = 0
external_counter: int = 0
results: rx.Field[list[Optional[Union[str, dict, list]]]] = rx.field([])
inline_counter: rx.Field[int] = rx.field(0)
external_counter: rx.Field[int] = rx.field(0)
value: str = "Initial"
last_result: str = ""
last_result: int = 0
@rx.event
def call_script_callback(self, result):
self.results.append(result)
@rx.event
def call_script_callback_other_arg(self, result, other_arg):
self.results.append([other_arg, result])
@ -91,7 +93,7 @@ def CallScript():
def call_script_inline_return_lambda(self):
return rx.call_script(
"inline2()",
callback=lambda result: CallScriptState.call_script_callback_other_arg( # type: ignore
callback=lambda result: CallScriptState.call_script_callback_other_arg(
result, "lambda"
),
)
@ -100,7 +102,7 @@ def CallScript():
def get_inline_counter(self):
return rx.call_script(
"inline_counter",
callback=CallScriptState.set_inline_counter, # type: ignore
callback=CallScriptState.setvar("inline_counter"),
)
@rx.event
@ -139,7 +141,7 @@ def CallScript():
def call_script_external_return_lambda(self):
return rx.call_script(
"external2()",
callback=lambda result: CallScriptState.call_script_callback_other_arg( # type: ignore
callback=lambda result: CallScriptState.call_script_callback_other_arg(
result, "lambda"
),
)
@ -148,28 +150,28 @@ def CallScript():
def get_external_counter(self):
return rx.call_script(
"external_counter",
callback=CallScriptState.set_external_counter, # type: ignore
callback=CallScriptState.setvar("external_counter"),
)
@rx.event
def call_with_var_f_string(self):
return rx.call_script(
f"{rx.Var('inline_counter')} + {rx.Var('external_counter')}",
callback=CallScriptState.set_last_result, # type: ignore
callback=CallScriptState.setvar("last_result"),
)
@rx.event
def call_with_var_str_cast(self):
return rx.call_script(
f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}",
callback=CallScriptState.set_last_result, # type: ignore
callback=CallScriptState.setvar("last_result"),
)
@rx.event
def call_with_var_f_string_wrapped(self):
return rx.call_script(
rx.Var(f"{rx.Var('inline_counter')} + {rx.Var('external_counter')}"),
callback=CallScriptState.set_last_result, # type: ignore
callback=CallScriptState.setvar("last_result"),
)
@rx.event
@ -178,7 +180,7 @@ def CallScript():
rx.Var(
f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}"
),
callback=CallScriptState.set_last_result, # type: ignore
callback=CallScriptState.setvar("last_result"),
)
@rx.event
@ -193,17 +195,17 @@ def CallScript():
def index():
return rx.vstack(
rx.input(
value=CallScriptState.inline_counter.to(str), # type: ignore
value=CallScriptState.inline_counter.to(str),
id="inline_counter",
read_only=True,
),
rx.input(
value=CallScriptState.external_counter.to(str), # type: ignore
value=CallScriptState.external_counter.to(str),
id="external_counter",
read_only=True,
),
rx.text_area(
value=CallScriptState.results.to_string(), # type: ignore
value=CallScriptState.results.to_string(),
id="results",
read_only=True,
),
@ -273,7 +275,7 @@ def CallScript():
CallScriptState.value,
on_click=rx.call_script(
"'updated'",
callback=CallScriptState.set_value, # type: ignore
callback=CallScriptState.setvar("value"),
),
id="update_value",
),
@ -282,7 +284,7 @@ def CallScript():
value=CallScriptState.last_result,
id="last_result",
read_only=True,
on_click=CallScriptState.set_last_result(""), # type: ignore
on_click=CallScriptState.setvar("last_result", 0),
),
rx.button(
"call_with_var_f_string",
@ -308,7 +310,7 @@ def CallScript():
"call_with_var_f_string_inline",
on_click=rx.call_script(
f"{rx.Var('inline_counter')} + {CallScriptState.last_result}",
callback=CallScriptState.set_last_result, # type: ignore
callback=CallScriptState.setvar("last_result"),
),
id="call_with_var_f_string_inline",
),
@ -316,7 +318,7 @@ def CallScript():
"call_with_var_str_cast_inline",
on_click=rx.call_script(
f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}",
callback=CallScriptState.set_last_result, # type: ignore
callback=CallScriptState.setvar("last_result"),
),
id="call_with_var_str_cast_inline",
),
@ -326,7 +328,7 @@ def CallScript():
rx.Var(
f"{rx.Var('inline_counter')} + {CallScriptState.last_result}"
),
callback=CallScriptState.set_last_result, # type: ignore
callback=CallScriptState.setvar("last_result"),
),
id="call_with_var_f_string_wrapped_inline",
),
@ -336,7 +338,7 @@ def CallScript():
rx.Var(
f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}"
),
callback=CallScriptState.set_last_result, # type: ignore
callback=CallScriptState.setvar("last_result"),
),
id="call_with_var_str_cast_wrapped_inline",
),
@ -483,7 +485,7 @@ def test_call_script_w_var(
"""
assert_token(driver)
last_result = driver.find_element(By.ID, "last_result")
assert last_result.get_attribute("value") == ""
assert last_result.get_attribute("value") == "0"
inline_return_button = driver.find_element(By.ID, "inline_return")

View File

@ -33,18 +33,18 @@ def ClientSide():
class ClientSideSubState(ClientSideState):
# cookies with default settings
c1: str = rx.Cookie()
c2: rx.Cookie = "c2 default" # type: ignore
c2: str = rx.Cookie("c2 default")
# cookies with custom settings
c3: str = rx.Cookie(max_age=2) # expires after 2 second
c4: rx.Cookie = rx.Cookie(same_site="strict")
c4: str = rx.Cookie(same_site="strict")
c5: str = rx.Cookie(path="/foo/") # only accessible on `/foo/`
c6: str = rx.Cookie(name="c6")
c7: str = rx.Cookie("c7 default")
# local storage with default settings
l1: str = rx.LocalStorage()
l2: rx.LocalStorage = "l2 default" # type: ignore
l2: str = rx.LocalStorage("l2 default")
# local storage with custom settings
l3: str = rx.LocalStorage(name="l3")
@ -56,7 +56,7 @@ def ClientSide():
# Session storage
s1: str = rx.SessionStorage()
s2: rx.SessionStorage = "s2 default" # type: ignore
s2: str = rx.SessionStorage("s2 default")
s3: str = rx.SessionStorage(name="s3")
def set_l6(self, my_param: str):
@ -87,13 +87,13 @@ def ClientSide():
rx.input(
placeholder="state var",
value=ClientSideState.state_var,
on_change=ClientSideState.set_state_var, # type: ignore
on_change=ClientSideState.setvar("state_var"),
id="state_var",
),
rx.input(
placeholder="input value",
value=ClientSideState.input_value,
on_change=ClientSideState.set_input_value, # type: ignore
on_change=ClientSideState.setvar("input_value"),
id="input_value",
),
rx.button(

View File

@ -37,19 +37,6 @@ def test_shared_asset() -> None:
assert not Path(Path.cwd() / "assets/external").exists()
def test_deprecated_x_asset(capsys) -> None:
"""Test that the deprecated asset function raises a warning.
Args:
capsys: Pytest fixture that captures stdout and stderr.
"""
assert rx.asset("custom_script.js", shared=True) == rx._x.asset("custom_script.js")
assert (
"DeprecationWarning: rx._x.asset has been deprecated in version 0.6.6"
in capsys.readouterr().out
)
@pytest.mark.parametrize(
"path,shared",
[

View File

@ -1,7 +1,5 @@
"""Data display component tests fixtures."""
from typing import List
import pandas as pd
import pytest
@ -54,11 +52,11 @@ def data_table_state3():
"""
class DataTableState(BaseState):
_data: List = []
_columns: List = ["col1", "col2"]
_data: list = []
_columns: list = ["col1", "col2"]
@rx.var
def data(self) -> List:
def data(self) -> list:
return self._data
@rx.var
@ -77,15 +75,15 @@ def data_table_state4():
"""
class DataTableState(BaseState):
_data: List = []
_columns: List = ["col1", "col2"]
_data: list = []
_columns: list[str] = ["col1", "col2"]
@rx.var
def data(self):
return self._data
@rx.var
def columns(self) -> List:
def columns(self) -> list:
return self._columns
return DataTableState

View File

@ -4,6 +4,7 @@ import pytest
import reflex as rx
from reflex.components.gridjs.datatable import DataTable
from reflex.utils import types
from reflex.utils.exceptions import UntypedComputedVarError
from reflex.utils.serializers import serialize, serialize_dataframe
@ -75,17 +76,17 @@ def test_invalid_props(props):
[
(
"data_table_state2",
"Annotation of the computed var assigned to the data field should be provided.",
"Computed var 'data' must have a type annotation.",
True,
),
(
"data_table_state3",
"Annotation of the computed var assigned to the column field should be provided.",
"Computed var 'columns' must have a type annotation.",
False,
),
(
"data_table_state4",
"Annotation of the computed var assigned to the data field should be provided.",
"Computed var 'data' must have a type annotation.",
False,
),
],
@ -99,7 +100,7 @@ def test_computed_var_without_annotation(fixture, request, err_msg, is_data_fram
err_msg: expected error message.
is_data_frame: whether data field is a pandas dataframe.
"""
with pytest.raises(ValueError) as err:
with pytest.raises(UntypedComputedVarError) as err:
if is_data_frame:
DataTable.create(data=request.getfixturevalue(fixture).data)
else:

View File

@ -19,6 +19,7 @@ from reflex.constants import EventTriggers
from reflex.event import (
EventChain,
EventHandler,
JavascriptInputEvent,
input_event,
no_args_event_spec,
parse_args_spec,
@ -27,7 +28,11 @@ from reflex.event import (
from reflex.state import BaseState
from reflex.style import Style
from reflex.utils import imports
from reflex.utils.exceptions import ChildrenTypeError, EventFnArgMismatch
from reflex.utils.exceptions import (
ChildrenTypeError,
EventFnArgMismatchError,
EventHandlerArgTypeMismatchError,
)
from reflex.utils.imports import ImportDict, ImportVar, ParsedImportDict, parse_imports
from reflex.vars import VarData
from reflex.vars.base import LiteralVar, Var
@ -94,11 +99,14 @@ def component2() -> Type[Component]:
A test component.
"""
def on_prop_event_spec(e0: Any):
return [e0]
class TestComponent2(Component):
# A test list prop.
arr: Var[List[str]]
on_prop_event: EventHandler[lambda e0: [e0]]
on_prop_event: EventHandler[on_prop_event_spec]
def get_event_triggers(self) -> Dict[str, Any]:
"""Test controlled triggers.
@ -818,10 +826,14 @@ def test_component_create_unpack_tuple_child(test_component, element, expected):
assert fragment_wrapper.render() == expected
class _Obj(Base):
custom: int = 0
class C1State(BaseState):
"""State for testing C1 component."""
def mock_handler(self, _e, _bravo, _charlie):
def mock_handler(self, _e: JavascriptInputEvent, _bravo: dict, _charlie: _Obj):
"""Mock handler."""
pass
@ -829,10 +841,12 @@ class C1State(BaseState):
def test_component_event_trigger_arbitrary_args():
"""Test that we can define arbitrary types for the args of an event trigger."""
class Obj(Base):
custom: int = 0
def on_foo_spec(_e, alpha: str, bravo: Dict[str, Any], charlie: Obj):
def on_foo_spec(
_e: Var[JavascriptInputEvent],
alpha: Var[str],
bravo: dict[str, Any],
charlie: Var[_Obj],
):
return [_e.target.value, bravo["nested"], charlie.custom + 42]
class C1(Component):
@ -845,13 +859,7 @@ def test_component_event_trigger_arbitrary_args():
"on_foo": on_foo_spec,
}
comp = C1.create(on_foo=C1State.mock_handler)
assert comp.render()["props"][0] == (
"onFoo={((__e, _alpha, _bravo, _charlie) => (addEvents("
f'[(Event("{C1State.get_full_name()}.mock_handler", ({{ ["_e"] : __e["target"]["value"], ["_bravo"] : _bravo["nested"], ["_charlie"] : (_charlie["custom"] + 42) }}), ({{ }})))], '
"[__e, _alpha, _bravo, _charlie], ({ }))))}"
)
C1.create(on_foo=C1State.mock_handler)
def test_create_custom_component(my_component):
@ -908,30 +916,29 @@ def test_invalid_event_handler_args(component2, test_state):
test_state: A test state.
"""
# EventHandler args must match
with pytest.raises(EventFnArgMismatch):
with pytest.raises(EventFnArgMismatchError):
component2.create(on_click=test_state.do_something_arg)
# Multiple EventHandler args: all must match
with pytest.raises(EventFnArgMismatch):
with pytest.raises(EventFnArgMismatchError):
component2.create(
on_click=[test_state.do_something_arg, test_state.do_something]
)
# Enable when 0.7.0 happens
# # Event Handler types must match
# with pytest.raises(EventHandlerArgTypeMismatch):
# component2.create(
# on_user_visited_count_changed=test_state.do_something_with_bool # noqa: ERA001 RUF100
# ) # noqa: ERA001 RUF100
# with pytest.raises(EventHandlerArgTypeMismatch):
# component2.create(on_user_list_changed=test_state.do_something_with_int) #noqa: ERA001
# with pytest.raises(EventHandlerArgTypeMismatch):
# component2.create(on_user_list_changed=test_state.do_something_with_list_int) #noqa: ERA001
with pytest.raises(EventHandlerArgTypeMismatchError):
component2.create(
on_user_visited_count_changed=test_state.do_something_with_bool
)
with pytest.raises(EventHandlerArgTypeMismatchError):
component2.create(on_user_list_changed=test_state.do_something_with_int)
with pytest.raises(EventHandlerArgTypeMismatchError):
component2.create(on_user_list_changed=test_state.do_something_with_list_int)
# component2.create(on_open=test_state.do_something_with_int) #noqa: ERA001
# component2.create(on_open=test_state.do_something_with_bool) #noqa: ERA001
# component2.create(on_user_visited_count_changed=test_state.do_something_with_int) #noqa: ERA001
# component2.create(on_user_list_changed=test_state.do_something_with_list_str) #noqa: ERA001
component2.create(on_open=test_state.do_something_with_int)
component2.create(on_open=test_state.do_something_with_bool)
component2.create(on_user_visited_count_changed=test_state.do_something_with_int)
component2.create(on_user_list_changed=test_state.do_something_with_list_str)
# lambda cannot return weird values.
with pytest.raises(ValueError):
@ -944,15 +951,15 @@ def test_invalid_event_handler_args(component2, test_state):
)
# lambda signature must match event trigger.
with pytest.raises(EventFnArgMismatch):
with pytest.raises(EventFnArgMismatchError):
component2.create(on_click=lambda _: test_state.do_something_arg(1))
# lambda returning EventHandler must match spec
with pytest.raises(EventFnArgMismatch):
with pytest.raises(EventFnArgMismatchError):
component2.create(on_click=lambda: test_state.do_something_arg)
# Mixed EventSpec and EventHandler must match spec.
with pytest.raises(EventFnArgMismatch):
with pytest.raises(EventFnArgMismatchError):
component2.create(
on_click=lambda: [
test_state.do_something_arg(1),
@ -1801,21 +1808,15 @@ def test_custom_component_declare_event_handlers_in_fields():
"""
return {
**super().get_event_triggers(),
"on_a": lambda e0: [e0],
"on_b": input_event,
"on_c": lambda e0: [],
"on_d": lambda: [],
"on_e": lambda: [],
"on_f": lambda a, b, c: [c, b, a],
}
class TestComponent(Component):
on_a: EventHandler[lambda e0: [e0]]
on_b: EventHandler[input_event]
on_c: EventHandler[no_args_event_spec]
on_d: EventHandler[no_args_event_spec]
on_e: EventHandler
on_f: EventHandler[lambda a, b, c: [c, b, a]]
custom_component = ReferenceComponent.create()
test_component = TestComponent.create()

View File

@ -199,16 +199,15 @@ def test_event_redirect(input, output):
input: The input for running the test.
output: The expected output to validate the test.
"""
path, external, replace = input
path, is_external, replace = input
kwargs = {}
if external is not None:
kwargs["external"] = external
if is_external is not None:
kwargs["is_external"] = is_external
if replace is not None:
kwargs["replace"] = replace
spec = event.redirect(path, **kwargs)
assert isinstance(spec, EventSpec)
assert spec.handler.fn.__qualname__ == "_redirect"
assert format.format_event(spec) == output

View File

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

View File

@ -1144,7 +1144,7 @@ def test_child_state():
class ChildState(MainState):
@computed_var
def rendered_var(self):
def rendered_var(self) -> int:
return self.v
ms = MainState()
@ -1421,7 +1421,7 @@ def test_computed_var_dependencies():
return self.testprop
@rx.var
def comp_w(self):
def comp_w(self) -> Callable[[], int]:
"""Nested lambda.
Returns:
@ -1430,7 +1430,7 @@ def test_computed_var_dependencies():
return lambda: self.w
@rx.var
def comp_x(self):
def comp_x(self) -> Callable[[], int]:
"""Nested function.
Returns:
@ -1443,7 +1443,7 @@ def test_computed_var_dependencies():
return _
@rx.var
def comp_y(self) -> List[int]:
def comp_y(self) -> list[int]:
"""Comprehension iterating over attribute.
Returns:
@ -3128,7 +3128,7 @@ async def test_get_state_from_sibling_not_cached(mock_app: rx.App, token: str):
child3_var: int = 0
@rx.var(cache=False)
def v(self):
def v(self) -> None:
pass
class Grandchild3(Child3):

View File

@ -11,7 +11,10 @@ import reflex as rx
from reflex.base import Base
from reflex.constants.base import REFLEX_VAR_CLOSING_TAG, REFLEX_VAR_OPENING_TAG
from reflex.state import BaseState
from reflex.utils.exceptions import PrimitiveUnserializableToJSON
from reflex.utils.exceptions import (
PrimitiveUnserializableToJSON,
UntypedComputedVarError,
)
from reflex.utils.imports import ImportVar
from reflex.vars import VarData
from reflex.vars.base import (
@ -804,7 +807,7 @@ def test_shadow_computed_var_error(request: pytest.FixtureRequest, fixture: str)
request: Fixture Request.
fixture: The state fixture.
"""
with pytest.raises(NameError):
with pytest.raises(UntypedComputedVarError):
state = request.getfixturevalue(fixture)
state.var_without_annotation.foo