Merge branch 'main' into lendemor/fix_duplicate_tab_issue

This commit is contained in:
Lendemor 2025-01-08 17:20:06 +01:00
commit 41d8cfae57
29 changed files with 479 additions and 284 deletions

View File

@ -5,7 +5,7 @@ on:
types: types:
- closed - closed
paths-ignore: paths-ignore:
- '**/*.md' - "**/*.md"
permissions: permissions:
contents: read contents: read
@ -15,21 +15,21 @@ defaults:
shell: bash shell: bash
env: env:
PYTHONIOENCODING: 'utf8' PYTHONIOENCODING: "utf8"
TELEMETRY_ENABLED: false TELEMETRY_ENABLED: false
NODE_OPTIONS: '--max_old_space_size=8192' NODE_OPTIONS: "--max_old_space_size=8192"
PR_TITLE: ${{ github.event.pull_request.title }} PR_TITLE: ${{ github.event.pull_request.title }}
jobs: jobs:
reflex-web: reflex-web:
# if: github.event.pull_request.merged == true # if: github.event.pull_request.merged == true
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
# Show OS combos first in GUI # Show OS combos first in GUI
os: [ubuntu-latest] os: [ubuntu-latest]
python-version: ['3.11.4'] python-version: ["3.12.8"]
node-version: ['18.x'] node-version: ["18.x"]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
@ -81,24 +81,24 @@ 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.9.18', '3.10.13', '3.11.5', '3.12.0'] python-version: ["3.9.21", "3.10.16", "3.11.11", "3.12.8"]
exclude: exclude:
- os: windows-latest - os: windows-latest
python-version: '3.10.13' python-version: "3.10.16"
- os: windows-latest - os: windows-latest
python-version: '3.9.18' python-version: "3.9.21"
# keep only one python version for MacOS # keep only one python version for MacOS
- os: macos-latest - os: macos-latest
python-version: '3.9.18' python-version: "3.9.21"
- os: macos-latest - os: macos-latest
python-version: '3.10.13' python-version: "3.10.16"
- os: macos-latest - os: macos-latest
python-version: '3.12.0' 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 - os: windows-latest
python-version: '3.9.13' python-version: "3.9.13"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
@ -123,7 +123,7 @@ jobs:
--event-type "${{ github.event_name }}" --pr-id "${{ github.event.pull_request.id }}" --event-type "${{ github.event_name }}" --pr-id "${{ github.event.pull_request.id }}"
reflex-dist-size: # This job is used to calculate the size of the Reflex distribution (wheel file) reflex-dist-size: # This job is used to calculate the size of the Reflex distribution (wheel file)
if: github.event.pull_request.merged == true if: github.event.pull_request.merged == true
timeout-minutes: 30 timeout-minutes: 30
strategy: strategy:
# Prioritize getting more information out of the workflow (even if something fails) # Prioritize getting more information out of the workflow (even if something fails)
@ -133,7 +133,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: ./.github/actions/setup_build_env - uses: ./.github/actions/setup_build_env
with: with:
python-version: 3.11.5 python-version: 3.12.8
run-poetry-install: true run-poetry-install: true
create-venv-at-path: .venv create-venv-at-path: .venv
- name: Build reflex - name: Build reflex
@ -143,12 +143,12 @@ jobs:
# Only run if the database creds are available in this context. # Only run if the database creds are available in this context.
run: run:
poetry run python benchmarks/benchmark_package_size.py --os ubuntu-latest poetry run python benchmarks/benchmark_package_size.py --os ubuntu-latest
--python-version 3.11.5 --commit-sha "${{ github.sha }}" --pr-id "${{ github.event.pull_request.id }}" --python-version 3.12.8 --commit-sha "${{ github.sha }}" --pr-id "${{ github.event.pull_request.id }}"
--branch-name "${{ github.head_ref || github.ref_name }}" --branch-name "${{ github.head_ref || github.ref_name }}"
--path ./dist --path ./dist
reflex-venv-size: # This job calculates the total size of Reflex and its dependencies reflex-venv-size: # This job calculates the total size of Reflex and its dependencies
if: github.event.pull_request.merged == true if: github.event.pull_request.merged == true
timeout-minutes: 30 timeout-minutes: 30
strategy: strategy:
# Prioritize getting more information out of the workflow (even if something fails) # Prioritize getting more information out of the workflow (even if something fails)
@ -156,7 +156,7 @@ 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.11.5'] python-version: ["3.12.8"]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
@ -186,6 +186,6 @@ jobs:
run: run:
poetry run python benchmarks/benchmark_package_size.py --os "${{ matrix.os }}" poetry run python benchmarks/benchmark_package_size.py --os "${{ matrix.os }}"
--python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
--pr-id "${{ github.event.pull_request.id }}" --pr-id "${{ github.event.pull_request.id }}"
--branch-name "${{ github.head_ref || github.ref_name }}" --branch-name "${{ github.head_ref || github.ref_name }}"
--path ./.venv --path ./.venv

View File

@ -6,16 +6,16 @@ concurrency:
on: on:
push: push:
branches: ['main'] branches: ["main"]
# We don't just trigger on make_pyi.py and the components dir, because # We don't just trigger on make_pyi.py and the components dir, because
# there are other things that can change the generator output # there are other things that can change the generator output
# e.g. black version, reflex.Component, reflex.Var. # e.g. black version, reflex.Component, reflex.Var.
paths-ignore: paths-ignore:
- '**/*.md' - "**/*.md"
pull_request: pull_request:
branches: ['main'] branches: ["main"]
paths-ignore: paths-ignore:
- '**/*.md' - "**/*.md"
jobs: jobs:
check-generated-pyi-components: check-generated-pyi-components:
@ -25,7 +25,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: ./.github/actions/setup_build_env - uses: ./.github/actions/setup_build_env
with: with:
python-version: '3.11.5' python-version: "3.12.8"
run-poetry-install: true run-poetry-install: true
create-venv-at-path: .venv create-venv-at-path: .venv
- run: | - run: |

View File

@ -1,43 +1,40 @@
name: integration-node-latest name: integration-node-latest
on: on:
push: push:
branches: branches:
- main - main
pull_request: pull_request:
branches: branches:
- main - main
env: env:
TELEMETRY_ENABLED: false TELEMETRY_ENABLED: false
REFLEX_USE_SYSTEM_NODE: true REFLEX_USE_SYSTEM_NODE: true
jobs: jobs:
check_latest_node: check_latest_node:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
strategy: strategy:
matrix: matrix:
python-version: ['3.12'] python-version: ["3.12.8"]
split_index: [1, 2] split_index: [1, 2]
node-version: ['node'] node-version: ["node"]
fail-fast: false fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup_build_env
with:
python-version: ${{ matrix.python-version }}
run-poetry-install: true
create-venv-at-path: .venv
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: |
poetry run uv pip install pyvirtualdisplay pillow pytest-split
poetry run playwright install --with-deps
- run: |
poetry run pytest tests/test_node_version.py
poetry run pytest tests/integration --splits 2 --group ${{matrix.split_index}}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup_build_env
with:
python-version: ${{ matrix.python-version }}
run-poetry-install: true
create-venv-at-path: .venv
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: |
poetry run uv pip install pyvirtualdisplay pillow pytest-split
poetry run playwright install --with-deps
- run: |
poetry run pytest tests/test_node_version.py
poetry run pytest tests/integration --splits 2 --group ${{matrix.split_index}}

View File

@ -1,88 +1,86 @@
name: check-outdated-dependencies name: check-outdated-dependencies
on: on:
push: # This will trigger the action when a pull request is opened or updated. push: # This will trigger the action when a pull request is opened or updated.
branches: branches:
- 'release/**' # This will trigger the action when any branch starting with "release/" is created. - "release/**" # This will trigger the action when any branch starting with "release/" is created.
workflow_dispatch: # Allow manual triggering if needed. workflow_dispatch: # Allow manual triggering if needed.
jobs: jobs:
backend: backend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
- uses: ./.github/actions/setup_build_env - uses: ./.github/actions/setup_build_env
with: with:
python-version: '3.9' python-version: "3.9.21"
run-poetry-install: true run-poetry-install: true
create-venv-at-path: .venv create-venv-at-path: .venv
- name: Check outdated backend dependencies - name: Check outdated backend dependencies
run: | run: |
outdated=$(poetry show -oT) outdated=$(poetry show -oT)
echo "Outdated:" echo "Outdated:"
echo "$outdated" echo "$outdated"
filtered_outdated=$(echo "$outdated" | grep -vE 'pyright|ruff' || true) filtered_outdated=$(echo "$outdated" | grep -vE 'pyright|ruff' || true)
if [ ! -z "$filtered_outdated" ]; then
echo "Outdated dependencies found:"
echo "$filtered_outdated"
exit 1
else
echo "All dependencies are up to date. (pyright and ruff are ignored)"
fi
if [ ! -z "$filtered_outdated" ]; then
echo "Outdated dependencies found:"
echo "$filtered_outdated"
exit 1
else
echo "All dependencies are up to date. (pyright and ruff are ignored)"
fi
frontend: frontend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: ./.github/actions/setup_build_env - uses: ./.github/actions/setup_build_env
with: with:
python-version: '3.10.11' python-version: "3.10.16"
run-poetry-install: true run-poetry-install: true
create-venv-at-path: .venv create-venv-at-path: .venv
- name: Clone Reflex Website Repo - name: Clone Reflex Website Repo
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
repository: reflex-dev/reflex-web repository: reflex-dev/reflex-web
ref: main ref: main
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 -r 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
working-directory: ./reflex-web working-directory: ./reflex-web
run: poetry run reflex init run: poetry run reflex init
- name: Run Website and Check for errors - name: Run Website and Check for errors
run: | run: |
poetry run bash scripts/integration.sh ./reflex-web dev poetry run bash scripts/integration.sh ./reflex-web dev
- name: Check outdated frontend dependencies - name: Check outdated frontend dependencies
working-directory: ./reflex-web/.web working-directory: ./reflex-web/.web
run: | run: |
raw_outdated=$(/home/runner/.local/share/reflex/bun/bin/bun outdated) raw_outdated=$(/home/runner/.local/share/reflex/bun/bin/bun outdated)
outdated=$(echo "$raw_outdated" | grep -vE '\|\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\|' || true) outdated=$(echo "$raw_outdated" | grep -vE '\|\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\|' || true)
echo "Outdated:" echo "Outdated:"
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' || true)
no_extra=$(echo "$filtered_outdated" | grep -vE '\|\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-' || true) no_extra=$(echo "$filtered_outdated" | grep -vE '\|\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-' || true)
if [ ! -z "$no_extra" ]; then if [ ! -z "$no_extra" ]; then
echo "Outdated dependencies found:" echo "Outdated dependencies found:"
echo "$filtered_outdated" echo "$filtered_outdated"
exit 1 exit 1
else else
echo "All dependencies are up to date. (3rd party packages are ignored)" echo "All dependencies are up to date. (3rd party packages are ignored)"
fi fi

View File

@ -22,8 +22,8 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
strategy: strategy:
matrix: matrix:
state_manager: ['redis', 'memory'] state_manager: ["redis", "memory"]
python-version: ['3.11.5', '3.12.0', '3.13.0'] python-version: ["3.11.11", "3.12.8", "3.13.1"]
split_index: [1, 2] split_index: [1, 2]
fail-fast: false fail-fast: false
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
@ -53,7 +53,7 @@ jobs:
SCREENSHOT_DIR: /tmp/screenshots/${{ matrix.state_manager }}/${{ matrix.python-version }}/${{ matrix.split_index }} 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 --with-deps poetry run playwright install chromium
poetry run pytest tests/integration --splits 2 --group ${{matrix.split_index}} poetry run pytest tests/integration --splits 2 --group ${{matrix.split_index}}
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
name: Upload failed test screenshots name: Upload failed test screenshots

View File

@ -2,13 +2,13 @@ name: integration-tests
on: on:
push: push:
branches: ['main'] branches: ["main"]
paths-ignore: paths-ignore:
- '**/*.md' - "**/*.md"
pull_request: pull_request:
branches: ['main'] branches: ["main"]
paths-ignore: paths-ignore:
- '**/*.md' - "**/*.md"
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.id }} group: ${{ github.workflow }}-${{ github.event.pull_request.id }}
@ -27,9 +27,9 @@ env:
# TODO: can we fix windows encoding natively within reflex? Bug above can hit real users too (less common, but possible) # TODO: can we fix windows encoding natively within reflex? Bug above can hit real users too (less common, but possible)
# - Catch encoding errors when printing logs # - Catch encoding errors when printing logs
# - Best effort print lines that contain illegal chars (map to some default char, etc.) # - Best effort print lines that contain illegal chars (map to some default char, etc.)
PYTHONIOENCODING: 'utf8' PYTHONIOENCODING: "utf8"
TELEMETRY_ENABLED: false TELEMETRY_ENABLED: false
NODE_OPTIONS: '--max_old_space_size=8192' NODE_OPTIONS: "--max_old_space_size=8192"
PR_TITLE: ${{ github.event.pull_request.title }} PR_TITLE: ${{ github.event.pull_request.title }}
jobs: jobs:
@ -43,17 +43,22 @@ jobs:
matrix: matrix:
# Show OS combos first in GUI # Show OS combos first in GUI
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest]
python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0', '3.13.0'] 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
exclude: exclude:
- os: windows-latest - os: windows-latest
python-version: '3.10.13' python-version: "3.11.11"
- os: windows-latest - os: windows-latest
python-version: '3.9.18' python-version: "3.10.16"
- os: windows-latest
python-version: "3.9.21"
include: include:
- os: windows-latest - os: windows-latest
python-version: '3.10.11' python-version: "3.11.9"
- os: windows-latest - os: windows-latest
python-version: '3.9.13' python-version: "3.10.11"
- os: windows-latest
python-version: "3.9.13"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
@ -117,18 +122,16 @@ jobs:
--branch-name "${{ github.head_ref || github.ref_name }}" --pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}" --pr-id "${{ github.event.pull_request.id }}"
--app-name "counter" --app-name "counter"
reflex-web: reflex-web:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
# Show OS combos first in GUI # Show OS combos first in GUI
os: [ubuntu-latest] os: [ubuntu-latest]
python-version: ['3.10.11', '3.11.4'] python-version: ["3.11.11", "3.12.8"]
env: env:
REFLEX_WEB_WINDOWS_OVERRIDE: '1' REFLEX_WEB_WINDOWS_OVERRIDE: "1"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -173,7 +176,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: ./.github/actions/setup_build_env - uses: ./.github/actions/setup_build_env
with: with:
python-version: '3.11.4' python-version: "3.11.11"
run-poetry-install: true run-poetry-install: true
create-venv-at-path: .venv create-venv-at-path: .venv
- name: Create app directory - name: Create app directory
@ -192,14 +195,14 @@ jobs:
# Check that npm is home # Check that npm is home
npm -v npm -v
poetry run bash scripts/integration.sh ./rx-shout-from-template prod poetry run bash scripts/integration.sh ./rx-shout-from-template prod
reflex-web-macos: reflex-web-macos:
if: github.event_name == 'push' && github.ref == 'refs/heads/main' if: github.event_name == 'push' && github.ref == 'refs/heads/main'
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ['3.11.5', '3.12.0'] # Note: py311 version chosen due to available arm64 darwin builds.
python-version: ["3.11.9", "3.12.8"]
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -233,4 +236,3 @@ jobs:
--python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
--pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}" --pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}"
--app-name "reflex-web" --path ./reflex-web/.web --app-name "reflex-web" --path ./reflex-web/.web

View File

@ -6,12 +6,12 @@ concurrency:
on: on:
pull_request: pull_request:
branches: ['main'] branches: ["main"]
push: push:
# Note even though this job is called "pre-commit" and runs "pre-commit", this job will run # Note even though this job is called "pre-commit" and runs "pre-commit", this job will run
# also POST-commit on main also! In case there are mishandled merge conflicts / bad auto-resolves # also POST-commit on main also! In case there are mishandled merge conflicts / bad auto-resolves
# when merging into main branch. # when merging into main branch.
branches: ['main'] branches: ["main"]
jobs: jobs:
pre-commit: pre-commit:
@ -23,7 +23,7 @@ jobs:
with: with:
# running vs. one version of Python is OK # running vs. one version of Python is OK
# i.e. ruff, black, etc. # i.e. ruff, black, etc.
python-version: 3.11.5 python-version: 3.12.8
run-poetry-install: true run-poetry-install: true
create-venv-at-path: .venv create-venv-at-path: .venv
# TODO pre-commit related stuff can be cached too (not a bottleneck yet) # TODO pre-commit related stuff can be cached too (not a bottleneck yet)

View File

@ -6,13 +6,13 @@ concurrency:
on: on:
push: push:
branches: ['main'] branches: ["main"]
paths-ignore: paths-ignore:
- '**/*.md' - "**/*.md"
pull_request: pull_request:
branches: ['main'] branches: ["main"]
paths-ignore: paths-ignore:
- '**/*.md' - "**/*.md"
permissions: permissions:
contents: read contents: read
@ -28,18 +28,22 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest]
python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0', '3.13.0'] 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 # Windows is a bit behind on Python version availability in Github
exclude: exclude:
- os: windows-latest - os: windows-latest
python-version: '3.10.13' python-version: "3.11.11"
- os: windows-latest - os: windows-latest
python-version: '3.9.18' python-version: "3.10.16"
- os: windows-latest
python-version: "3.9.21"
include: include:
- os: windows-latest - os: windows-latest
python-version: '3.10.11' python-version: "3.11.9"
- os: windows-latest - os: windows-latest
python-version: '3.9.13' python-version: "3.10.11"
- os: windows-latest
python-version: "3.9.13"
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
# Service containers to run with `runner-job` # Service containers to run with `runner-job`
@ -88,8 +92,8 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
# Note: py39, py310 versions chosen due to available arm64 darwin builds. # Note: py39, py310, py311 versions chosen due to available arm64 darwin builds.
python-version: ['3.9.13', '3.10.11', '3.11.5', '3.12.0', '3.13.0'] python-version: ["3.9.13", "3.10.11", "3.11.9", "3.12.8", "3.13.1"]
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -106,4 +110,4 @@ jobs:
run: | run: |
export PYTHONUNBUFFERED=1 export PYTHONUNBUFFERED=1
poetry run uv pip install "pydantic~=1.10" poetry run uv pip install "pydantic~=1.10"
poetry run pytest tests/units --cov --no-cov-on-fail --cov-report= poetry run pytest tests/units --cov --no-cov-on-fail --cov-report=

View File

@ -16,7 +16,6 @@ repository = "https://github.com/reflex-dev/reflex"
documentation = "https://reflex.dev/docs/getting-started/introduction" documentation = "https://reflex.dev/docs/getting-started/introduction"
keywords = ["web", "framework"] keywords = ["web", "framework"]
classifiers = ["Development Status :: 4 - Beta"] classifiers = ["Development Status :: 4 - Beta"]
packages = [{ include = "reflex" }]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.9" python = "^3.9"

View File

@ -1,4 +1,5 @@
{% extends "web/pages/base_page.js.jinja2" %} {% extends "web/pages/base_page.js.jinja2" %}
{% from "web/pages/macros.js.jinja2" import renderHooks %}
{% block early_imports %} {% block early_imports %}
import '$/styles/styles.css' import '$/styles/styles.css'
@ -18,10 +19,7 @@ import * as {{library_alias}} from "{{library_path}}";
{% block export %} {% block export %}
function AppWrap({children}) { function AppWrap({children}) {
{{ renderHooks(hooks) }}
{% for hook in hooks %}
{{ hook }}
{% endfor %}
return ( return (
{{utils.render(render, indent_width=0)}} {{utils.render(render, indent_width=0)}}

View File

@ -1,5 +1,5 @@
{% extends "web/pages/base_page.js.jinja2" %} {% extends "web/pages/base_page.js.jinja2" %}
{% from "web/pages/macros.js.jinja2" import renderHooks %}
{% block export %} {% block export %}
{% for component in components %} {% for component in components %}
@ -8,9 +8,8 @@
{% endfor %} {% endfor %}
export const {{component.name}} = memo(({ {{-component.props|join(", ")-}} }) => { export const {{component.name}} = memo(({ {{-component.props|join(", ")-}} }) => {
{% for hook in component.hooks %} {{ renderHooks(component.hooks) }}
{{ hook }}
{% endfor %}
return( return(
{{utils.render(component.render)}} {{utils.render(component.render)}}
) )

View File

@ -1,4 +1,5 @@
{% extends "web/pages/base_page.js.jinja2" %} {% extends "web/pages/base_page.js.jinja2" %}
{% from "web/pages/macros.js.jinja2" import renderHooks %}
{% block declaration %} {% block declaration %}
{% for custom_code in custom_codes %} {% for custom_code in custom_codes %}
@ -8,9 +9,7 @@
{% block export %} {% block export %}
export default function Component() { export default function Component() {
{% for hook in hooks %} {{ renderHooks(hooks)}}
{{ hook }}
{% endfor %}
return ( return (
{{utils.render(render, indent_width=0)}} {{utils.render(render, indent_width=0)}}

View File

@ -0,0 +1,38 @@
{% macro renderHooks(hooks) %}
{% set sorted_hooks = sort_hooks(hooks) %}
{# Render the grouped hooks #}
{% for hook, _ in sorted_hooks[const.hook_position.INTERNAL] %}
{{ hook }}
{% endfor %}
{% for hook, _ in sorted_hooks[const.hook_position.PRE_TRIGGER] %}
{{ hook }}
{% endfor %}
{% for hook, _ in sorted_hooks[const.hook_position.POST_TRIGGER] %}
{{ hook }}
{% endfor %}
{% endmacro %}
{% macro renderHooksWithMemo(hooks, memo)%}
{% set sorted_hooks = sort_hooks(hooks) %}
{# Render the grouped hooks #}
{% for hook, _ in sorted_hooks[const.hook_position.INTERNAL] %}
{{ hook }}
{% endfor %}
{% for hook, _ in sorted_hooks[const.hook_position.PRE_TRIGGER] %}
{{ hook }}
{% endfor %}
{% for hook in memo %}
{{ hook }}
{% endfor %}
{% for hook, _ in sorted_hooks[const.hook_position.POST_TRIGGER] %}
{{ hook }}
{% endfor %}
{% endmacro %}

View File

@ -1,22 +1,10 @@
{% import 'web/pages/utils.js.jinja2' as utils %} {% import 'web/pages/utils.js.jinja2' as utils %}
{% from 'web/pages/macros.js.jinja2' import renderHooksWithMemo %}
{% set all_hooks = component._get_all_hooks() %}
export function {{tag_name}} () { export function {{tag_name}} () {
{% for hook in component._get_all_hooks_internal() %} {{ renderHooksWithMemo(all_hooks, memo_trigger_hooks) }}
{{ hook }}
{% endfor %}
{% for hook, data in component._get_all_hooks().items() if not data.position or data.position == const.hook_position.PRE_TRIGGER %}
{{ hook }}
{% endfor %}
{% for hook in memo_trigger_hooks %}
{{ hook }}
{% endfor %}
{% for hook, data in component._get_all_hooks().items() if data.position and data.position == const.hook_position.POST_TRIGGER %}
{{ hook }}
{% endfor %}
return ( return (
{{utils.render(component.render(), indent_width=0)}} {{utils.render(component.render(), indent_width=0)}}
) )

View File

@ -75,7 +75,7 @@ def _compile_app(app_root: Component) -> str:
return templates.APP_ROOT.render( return templates.APP_ROOT.render(
imports=utils.compile_imports(app_root._get_all_imports()), imports=utils.compile_imports(app_root._get_all_imports()),
custom_codes=app_root._get_all_custom_code(), custom_codes=app_root._get_all_custom_code(),
hooks={**app_root._get_all_hooks_internal(), **app_root._get_all_hooks()}, hooks=app_root._get_all_hooks(),
window_libraries=window_libraries, window_libraries=window_libraries,
render=app_root.render(), render=app_root.render(),
) )
@ -149,7 +149,7 @@ def _compile_page(
imports=imports, imports=imports,
dynamic_imports=component._get_all_dynamic_imports(), dynamic_imports=component._get_all_dynamic_imports(),
custom_codes=component._get_all_custom_code(), custom_codes=component._get_all_custom_code(),
hooks={**component._get_all_hooks_internal(), **component._get_all_hooks()}, hooks=component._get_all_hooks(),
render=component.render(), render=component.render(),
**kwargs, **kwargs,
) )

View File

@ -1,9 +1,46 @@
"""Templates to use in the reflex compiler.""" """Templates to use in the reflex compiler."""
from __future__ import annotations
from jinja2 import Environment, FileSystemLoader, Template from jinja2 import Environment, FileSystemLoader, Template
from reflex import constants from reflex import constants
from reflex.constants import Hooks
from reflex.utils.format import format_state_name, json_dumps from reflex.utils.format import format_state_name, json_dumps
from reflex.vars.base import VarData
def _sort_hooks(hooks: dict[str, VarData | None]):
"""Sort the hooks by their position.
Args:
hooks: The hooks to sort.
Returns:
The sorted hooks.
"""
sorted_hooks = {
Hooks.HookPosition.INTERNAL: [],
Hooks.HookPosition.PRE_TRIGGER: [],
Hooks.HookPosition.POST_TRIGGER: [],
}
for hook, data in hooks.items():
if data and data.position and data.position == Hooks.HookPosition.INTERNAL:
sorted_hooks[Hooks.HookPosition.INTERNAL].append((hook, data))
elif not data or (
not data.position
or data.position == constants.Hooks.HookPosition.PRE_TRIGGER
):
sorted_hooks[Hooks.HookPosition.PRE_TRIGGER].append((hook, data))
elif (
data
and data.position
and data.position == constants.Hooks.HookPosition.POST_TRIGGER
):
sorted_hooks[Hooks.HookPosition.POST_TRIGGER].append((hook, data))
return sorted_hooks
class ReflexJinjaEnvironment(Environment): class ReflexJinjaEnvironment(Environment):
@ -47,6 +84,7 @@ class ReflexJinjaEnvironment(Environment):
"frontend_exception_state": constants.CompileVars.FRONTEND_EXCEPTION_STATE_FULL, "frontend_exception_state": constants.CompileVars.FRONTEND_EXCEPTION_STATE_FULL,
"hook_position": constants.Hooks.HookPosition, "hook_position": constants.Hooks.HookPosition,
} }
self.globals["sort_hooks"] = _sort_hooks
def get_template(name: str) -> Template: def get_template(name: str) -> Template:
@ -103,6 +141,9 @@ STYLE = get_template("web/styles/styles.css.jinja2")
# Code that generate the package json file # Code that generate the package json file
PACKAGE_JSON = get_template("web/package.json.jinja2") PACKAGE_JSON = get_template("web/package.json.jinja2")
# Template containing some macros used in the web pages.
MACROS = get_template("web/pages/macros.js.jinja2")
# Code that generate the pyproject.toml file for custom components. # Code that generate the pyproject.toml file for custom components.
CUSTOM_COMPONENTS_PYPROJECT_TOML = get_template( CUSTOM_COMPONENTS_PYPROJECT_TOML = get_template(
"custom_components/pyproject.toml.jinja2" "custom_components/pyproject.toml.jinja2"

View File

@ -290,7 +290,7 @@ def compile_custom_component(
"name": component.tag, "name": component.tag,
"props": props, "props": props,
"render": render.render(), "render": render.render(),
"hooks": {**render._get_all_hooks_internal(), **render._get_all_hooks()}, "hooks": render._get_all_hooks(),
"custom_code": render._get_all_custom_code(), "custom_code": render._get_all_custom_code(),
}, },
imports, imports,

View File

@ -9,6 +9,7 @@ from reflex.components.tags import Tag
from reflex.components.tags.tagless import Tagless from reflex.components.tags.tagless import Tagless
from reflex.utils.imports import ParsedImportDict from reflex.utils.imports import ParsedImportDict
from reflex.vars import BooleanVar, ObjectVar, Var from reflex.vars import BooleanVar, ObjectVar, Var
from reflex.vars.base import VarData
class Bare(Component): class Bare(Component):
@ -32,7 +33,7 @@ class Bare(Component):
contents = str(contents) if contents is not None else "" contents = str(contents) if contents is not None else ""
return cls(contents=contents) # type: ignore return cls(contents=contents) # type: ignore
def _get_all_hooks_internal(self) -> dict[str, None]: def _get_all_hooks_internal(self) -> dict[str, VarData | None]:
"""Include the hooks for the component. """Include the hooks for the component.
Returns: Returns:
@ -43,7 +44,7 @@ class Bare(Component):
hooks |= self.contents._var_value._get_all_hooks_internal() hooks |= self.contents._var_value._get_all_hooks_internal()
return hooks return hooks
def _get_all_hooks(self) -> dict[str, None]: def _get_all_hooks(self) -> dict[str, VarData | None]:
"""Include the hooks for the component. """Include the hooks for the component.
Returns: Returns:
@ -107,11 +108,14 @@ class Bare(Component):
return Tagless(contents=f"{{{self.contents!s}}}") return Tagless(contents=f"{{{self.contents!s}}}")
return Tagless(contents=str(self.contents)) return Tagless(contents=str(self.contents))
def _get_vars(self, include_children: bool = False) -> Iterator[Var]: def _get_vars(
self, include_children: bool = False, ignore_ids: set[int] | None = None
) -> Iterator[Var]:
"""Walk all Vars used in this component. """Walk all Vars used in this component.
Args: Args:
include_children: Whether to include Vars from children. include_children: Whether to include Vars from children.
ignore_ids: The ids to ignore.
Yields: Yields:
The contents if it is a Var, otherwise nothing. The contents if it is a Var, otherwise nothing.

View File

@ -102,7 +102,7 @@ class BaseComponent(Base, ABC):
""" """
@abstractmethod @abstractmethod
def _get_all_hooks_internal(self) -> dict[str, None]: def _get_all_hooks_internal(self) -> dict[str, VarData | None]:
"""Get the reflex internal hooks for the component and its children. """Get the reflex internal hooks for the component and its children.
Returns: Returns:
@ -110,7 +110,7 @@ class BaseComponent(Base, ABC):
""" """
@abstractmethod @abstractmethod
def _get_all_hooks(self) -> dict[str, None]: def _get_all_hooks(self) -> dict[str, VarData | None]:
"""Get the React hooks for this component. """Get the React hooks for this component.
Returns: Returns:
@ -1020,18 +1020,22 @@ class Component(BaseComponent, ABC):
event_args.append(spec) event_args.append(spec)
yield event_trigger, event_args yield event_trigger, event_args
def _get_vars(self, include_children: bool = False) -> list[Var]: def _get_vars(
self, include_children: bool = False, ignore_ids: set[int] | None = None
) -> Iterator[Var]:
"""Walk all Vars used in this component. """Walk all Vars used in this component.
Args: Args:
include_children: Whether to include Vars from children. include_children: Whether to include Vars from children.
ignore_ids: The ids to ignore.
Returns: Yields:
Each var referenced by the component (props, styles, event handlers). Each var referenced by the component (props, styles, event handlers).
""" """
vars = getattr(self, "__vars", None) ignore_ids = ignore_ids or set()
vars: List[Var] | None = getattr(self, "__vars", None)
if vars is not None: if vars is not None:
return vars yield from vars
vars = self.__vars = [] vars = self.__vars = []
# Get Vars associated with event trigger arguments. # Get Vars associated with event trigger arguments.
for _, event_vars in self._get_vars_from_event_triggers(self.event_triggers): for _, event_vars in self._get_vars_from_event_triggers(self.event_triggers):
@ -1075,12 +1079,15 @@ class Component(BaseComponent, ABC):
# Get Vars associated with children. # Get Vars associated with children.
if include_children: if include_children:
for child in self.children: for child in self.children:
if not isinstance(child, Component): if not isinstance(child, Component) or id(child) in ignore_ids:
continue continue
child_vars = child._get_vars(include_children=include_children) ignore_ids.add(id(child))
child_vars = child._get_vars(
include_children=include_children, ignore_ids=ignore_ids
)
vars.extend(child_vars) vars.extend(child_vars)
return vars yield from vars
def _event_trigger_values_use_state(self) -> bool: def _event_trigger_values_use_state(self) -> bool:
"""Check if the values of a component's event trigger use state. """Check if the values of a component's event trigger use state.
@ -1272,7 +1279,7 @@ class Component(BaseComponent, ABC):
""" """
_imports = {} _imports = {}
if self._get_ref_hook(): if self._get_ref_hook() is not None:
# Handle hooks needed for attaching react refs to DOM nodes. # Handle hooks needed for attaching react refs to DOM nodes.
_imports.setdefault("react", set()).add(ImportVar(tag="useRef")) _imports.setdefault("react", set()).add(ImportVar(tag="useRef"))
_imports.setdefault(f"$/{Dirs.STATE_PATH}", set()).add( _imports.setdefault(f"$/{Dirs.STATE_PATH}", set()).add(
@ -1388,7 +1395,7 @@ class Component(BaseComponent, ABC):
}} }}
}}, []);""" }}, []);"""
def _get_ref_hook(self) -> str | None: def _get_ref_hook(self) -> Var | None:
"""Generate the ref hook for the component. """Generate the ref hook for the component.
Returns: Returns:
@ -1396,11 +1403,12 @@ class Component(BaseComponent, ABC):
""" """
ref = self.get_ref() ref = self.get_ref()
if ref is not None: if ref is not None:
return ( return Var(
f"const {ref} = useRef(null); {Var(_js_expr=ref)._as_ref()!s} = {ref};" f"const {ref} = useRef(null); {Var(_js_expr=ref)._as_ref()!s} = {ref};",
_var_data=VarData(position=Hooks.HookPosition.INTERNAL),
) )
def _get_vars_hooks(self) -> dict[str, None]: def _get_vars_hooks(self) -> dict[str, VarData | None]:
"""Get the hooks required by vars referenced in this component. """Get the hooks required by vars referenced in this component.
Returns: Returns:
@ -1413,27 +1421,38 @@ class Component(BaseComponent, ABC):
vars_hooks.update( vars_hooks.update(
var_data.hooks var_data.hooks
if isinstance(var_data.hooks, dict) if isinstance(var_data.hooks, dict)
else {k: None for k in var_data.hooks} else {
k: VarData(position=Hooks.HookPosition.INTERNAL)
for k in var_data.hooks
}
) )
return vars_hooks return vars_hooks
def _get_events_hooks(self) -> dict[str, None]: def _get_events_hooks(self) -> dict[str, VarData | None]:
"""Get the hooks required by events referenced in this component. """Get the hooks required by events referenced in this component.
Returns: Returns:
The hooks for the events. The hooks for the events.
""" """
return {Hooks.EVENTS: None} if self.event_triggers else {} return (
{Hooks.EVENTS: VarData(position=Hooks.HookPosition.INTERNAL)}
if self.event_triggers
else {}
)
def _get_special_hooks(self) -> dict[str, None]: def _get_special_hooks(self) -> dict[str, VarData | None]:
"""Get the hooks required by special actions referenced in this component. """Get the hooks required by special actions referenced in this component.
Returns: Returns:
The hooks for special actions. The hooks for special actions.
""" """
return {Hooks.AUTOFOCUS: None} if self.autofocus else {} return (
{Hooks.AUTOFOCUS: VarData(position=Hooks.HookPosition.INTERNAL)}
if self.autofocus
else {}
)
def _get_hooks_internal(self) -> dict[str, None]: def _get_hooks_internal(self) -> dict[str, VarData | None]:
"""Get the React hooks for this component managed by the framework. """Get the React hooks for this component managed by the framework.
Downstream components should NOT override this method to avoid breaking Downstream components should NOT override this method to avoid breaking
@ -1444,7 +1463,7 @@ class Component(BaseComponent, ABC):
""" """
return { return {
**{ **{
hook: None str(hook): VarData(position=Hooks.HookPosition.INTERNAL)
for hook in [self._get_ref_hook(), self._get_mount_lifecycle_hook()] for hook in [self._get_ref_hook(), self._get_mount_lifecycle_hook()]
if hook is not None if hook is not None
}, },
@ -1493,7 +1512,7 @@ class Component(BaseComponent, ABC):
""" """
return return
def _get_all_hooks_internal(self) -> dict[str, None]: def _get_all_hooks_internal(self) -> dict[str, VarData | None]:
"""Get the reflex internal hooks for the component and its children. """Get the reflex internal hooks for the component and its children.
Returns: Returns:
@ -1508,7 +1527,7 @@ class Component(BaseComponent, ABC):
return code return code
def _get_all_hooks(self) -> dict[str, None]: def _get_all_hooks(self) -> dict[str, VarData | None]:
"""Get the React hooks for this component and its children. """Get the React hooks for this component and its children.
Returns: Returns:
@ -1516,6 +1535,9 @@ class Component(BaseComponent, ABC):
""" """
code = {} code = {}
# Add the internal hooks for this component.
code.update(self._get_hooks_internal())
# Add the hook code for this component. # Add the hook code for this component.
hooks = self._get_hooks() hooks = self._get_hooks()
if hooks is not None: if hooks is not None:
@ -1796,19 +1818,25 @@ class CustomComponent(Component):
for name, prop in self.props.items() for name, prop in self.props.items()
] ]
def _get_vars(self, include_children: bool = False) -> list[Var]: def _get_vars(
self, include_children: bool = False, ignore_ids: set[int] | None = None
) -> Iterator[Var]:
"""Walk all Vars used in this component. """Walk all Vars used in this component.
Args: Args:
include_children: Whether to include Vars from children. include_children: Whether to include Vars from children.
ignore_ids: The ids to ignore.
Returns: Yields:
Each var referenced by the component (props, styles, event handlers). Each var referenced by the component (props, styles, event handlers).
""" """
return ( ignore_ids = ignore_ids or set()
super()._get_vars(include_children=include_children) yield from super()._get_vars(
+ [prop for prop in self.props.values() if isinstance(prop, Var)] include_children=include_children, ignore_ids=ignore_ids
+ self.get_component(self)._get_vars(include_children=include_children) )
yield from filter(lambda prop: isinstance(prop, Var), self.props.values())
yield from self.get_component(self)._get_vars(
include_children=include_children, ignore_ids=ignore_ids
) )
@lru_cache(maxsize=None) # noqa @lru_cache(maxsize=None) # noqa
@ -2211,7 +2239,7 @@ class StatefulComponent(BaseComponent):
) )
return trigger_memo return trigger_memo
def _get_all_hooks_internal(self) -> dict[str, None]: def _get_all_hooks_internal(self) -> dict[str, VarData | None]:
"""Get the reflex internal hooks for the component and its children. """Get the reflex internal hooks for the component and its children.
Returns: Returns:
@ -2219,7 +2247,7 @@ class StatefulComponent(BaseComponent):
""" """
return {} return {}
def _get_all_hooks(self) -> dict[str, None]: def _get_all_hooks(self) -> dict[str, VarData | None]:
"""Get the React hooks for this component. """Get the React hooks for this component.
Returns: Returns:
@ -2337,7 +2365,7 @@ class MemoizationLeaf(Component):
The memoization leaf The memoization leaf
""" """
comp = super().create(*children, **props) comp = super().create(*children, **props)
if comp._get_all_hooks() or comp._get_all_hooks_internal(): if comp._get_all_hooks():
comp._memoization_mode = cls._memoization_mode.copy( comp._memoization_mode = cls._memoization_mode.copy(
update={"disposition": MemoizationDisposition.ALWAYS} update={"disposition": MemoizationDisposition.ALWAYS}
) )

View File

@ -502,8 +502,8 @@ class CodeBlock(Component, MarkdownComponentMap):
theme = self.theme theme = self.theme
out.add_props(style=theme).remove_props("theme", "code", "language").add_props( out.add_props(style=theme).remove_props("theme", "code").add_props(
children=self.code, language=_LANGUAGE children=self.code,
) )
return out return out
@ -512,20 +512,25 @@ class CodeBlock(Component, MarkdownComponentMap):
return ["can_copy", "copy_button"] return ["can_copy", "copy_button"]
@classmethod @classmethod
def _get_language_registration_hook(cls) -> str: def _get_language_registration_hook(cls, language_var: Var = _LANGUAGE) -> str:
"""Get the hook to register the language. """Get the hook to register the language.
Args:
language_var: The const/literal Var of the language module to import.
For markdown, uses the default placeholder _LANGUAGE. For direct use,
a LiteralStringVar should be passed via the language prop.
Returns: Returns:
The hook to register the language. The hook to register the language.
""" """
return f""" return f"""
if ({_LANGUAGE!s}) {{ if ({language_var!s}) {{
(async () => {{ (async () => {{
try {{ try {{
const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${{{_LANGUAGE!s}}}`); const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${{{language_var!s}}}`);
SyntaxHighlighter.registerLanguage({_LANGUAGE!s}, module.default); SyntaxHighlighter.registerLanguage({language_var!s}, module.default);
}} catch (error) {{ }} catch (error) {{
console.error(`Error importing language module for ${{{_LANGUAGE!s}}}:`, error); console.error(`Error importing language module for ${{{language_var!s}}}:`, error);
}} }}
}})(); }})();
}} }}
@ -547,8 +552,7 @@ class CodeBlock(Component, MarkdownComponentMap):
The hooks for the component. The hooks for the component.
""" """
return [ return [
f"const {_LANGUAGE!s} = {self.language!s}", self._get_language_registration_hook(language_var=self.language),
self._get_language_registration_hook(),
] ]

View File

@ -182,9 +182,7 @@ class Form(BaseHTML):
props["handle_submit_unique_name"] = "" props["handle_submit_unique_name"] = ""
form = super().create(*children, **props) form = super().create(*children, **props)
form.handle_submit_unique_name = md5( form.handle_submit_unique_name = md5(
str({**form._get_all_hooks_internal(), **form._get_all_hooks()}).encode( str(form._get_all_hooks()).encode("utf-8")
"utf-8"
)
).hexdigest() ).hexdigest()
return form return form
@ -252,8 +250,12 @@ class Form(BaseHTML):
) )
return form_refs return form_refs
def _get_vars(self, include_children: bool = True) -> Iterator[Var]: def _get_vars(
yield from super()._get_vars(include_children=include_children) self, include_children: bool = True, ignore_ids: set[int] | None = None
) -> Iterator[Var]:
yield from super()._get_vars(
include_children=include_children, ignore_ids=ignore_ids
)
yield from self._get_form_refs().values() yield from self._get_form_refs().values()
def _exclude_props(self) -> list[str]: def _exclude_props(self) -> list[str]:

View File

@ -420,11 +420,12 @@ const {_LANGUAGE!s} = match ? match[1] : '';
def _get_custom_code(self) -> str | None: def _get_custom_code(self) -> str | None:
hooks = {} hooks = {}
from reflex.compiler.templates import MACROS
for _component in self.component_map.values(): for _component in self.component_map.values():
comp = _component(_MOCK_ARG) comp = _component(_MOCK_ARG)
hooks.update(comp._get_all_hooks_internal())
hooks.update(comp._get_all_hooks()) hooks.update(comp._get_all_hooks())
formatted_hooks = "\n".join(hooks.keys()) formatted_hooks = MACROS.module.renderHooks(hooks) # type: ignore
return f""" return f"""
function {self._get_component_map_name()} () {{ function {self._get_component_map_name()} () {{
{formatted_hooks} {formatted_hooks}

View File

@ -76,7 +76,7 @@ class Link(RadixThemesComponent, A, MemoizationLeaf, MarkdownComponentMap):
Returns: Returns:
Component: The link component Component: The link component
""" """
props.setdefault(":hover", {"color": color("accent", 8)}) props.setdefault("_hover", {"color": color("accent", 8)})
href = props.get("href") href = props.get("href")
is_external = props.pop("is_external", None) is_external = props.pop("is_external", None)

View File

@ -135,6 +135,7 @@ class Hooks(SimpleNamespace):
class HookPosition(enum.Enum): class HookPosition(enum.Enum):
"""The position of the hook in the component.""" """The position of the hook in the component."""
INTERNAL = "internal"
PRE_TRIGGER = "pre_trigger" PRE_TRIGGER = "pre_trigger"
POST_TRIGGER = "post_trigger" POST_TRIGGER = "post_trigger"

View File

@ -12,7 +12,7 @@ from reflex.event import EventChain, EventHandler, EventSpec, run_script
from reflex.utils.imports import ImportVar from reflex.utils.imports import ImportVar
from reflex.vars import VarData, get_unique_variable_name from reflex.vars import VarData, get_unique_variable_name
from reflex.vars.base import LiteralVar, Var from reflex.vars.base import LiteralVar, Var
from reflex.vars.function import FunctionVar from reflex.vars.function import ArgsFunctionOperationBuilder, FunctionVar
NoValue = object() NoValue = object()
@ -45,6 +45,7 @@ class ClientStateVar(Var):
# Track the names of the getters and setters # Track the names of the getters and setters
_setter_name: str = dataclasses.field(default="") _setter_name: str = dataclasses.field(default="")
_getter_name: str = dataclasses.field(default="") _getter_name: str = dataclasses.field(default="")
_id_name: str = dataclasses.field(default="")
# Whether to add the var and setter to the global `refs` object for use in any Component. # Whether to add the var and setter to the global `refs` object for use in any Component.
_global_ref: bool = dataclasses.field(default=True) _global_ref: bool = dataclasses.field(default=True)
@ -96,6 +97,7 @@ class ClientStateVar(Var):
""" """
if var_name is None: if var_name is None:
var_name = get_unique_variable_name() var_name = get_unique_variable_name()
id_name = "id_" + get_unique_variable_name()
if not isinstance(var_name, str): if not isinstance(var_name, str):
raise ValueError("var_name must be a string.") raise ValueError("var_name must be a string.")
if default is NoValue: if default is NoValue:
@ -105,20 +107,24 @@ class ClientStateVar(Var):
else: else:
default_var = default default_var = default
setter_name = f"set{var_name.capitalize()}" setter_name = f"set{var_name.capitalize()}"
hooks = { hooks: dict[str, VarData | None] = {
f"const {id_name} = useId()": None,
f"const [{var_name}, {setter_name}] = useState({default_var!s})": None, f"const [{var_name}, {setter_name}] = useState({default_var!s})": None,
} }
imports = { imports = {
"react": [ImportVar(tag="useState")], "react": [ImportVar(tag="useState"), ImportVar(tag="useId")],
} }
if global_ref: if global_ref:
hooks[f"{_client_state_ref(var_name)} = {var_name}"] = None hooks[f"{_client_state_ref(var_name)} ??= {{}}"] = None
hooks[f"{_client_state_ref(setter_name)} = {setter_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
imports.update(_refs_import) imports.update(_refs_import)
return cls( return cls(
_js_expr="", _js_expr="",
_setter_name=setter_name, _setter_name=setter_name,
_getter_name=var_name, _getter_name=var_name,
_id_name=id_name,
_global_ref=global_ref, _global_ref=global_ref,
_var_type=default_var._var_type, _var_type=default_var._var_type,
_var_data=VarData.merge( _var_data=VarData.merge(
@ -144,10 +150,11 @@ class ClientStateVar(Var):
return ( return (
Var( Var(
_js_expr=( _js_expr=(
_client_state_ref(self._getter_name) _client_state_ref(self._getter_name) + f"[{self._id_name}]"
if self._global_ref if self._global_ref
else self._getter_name else self._getter_name
) ),
_var_data=self._var_data,
) )
.to(self._var_type) .to(self._var_type)
._replace( ._replace(
@ -170,28 +177,43 @@ class ClientStateVar(Var):
Returns: Returns:
A special EventChain Var which will set the value when triggered. A special EventChain Var which will set the value when triggered.
""" """
setter = (
_client_state_ref(self._setter_name)
if self._global_ref
else self._setter_name
)
_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 = (
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,
)
if self._global_ref
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
value_var = LiteralVar.create(value) value_var = LiteralVar.create(value)
_var_data = VarData.merge(_var_data, value_var._get_all_var_data())
value_str = str(value_var) value_str = str(value_var)
if value_str.startswith("_"): setter = ArgsFunctionOperationBuilder.create(
# remove patterns of ["*"] from the value_str using regex # remove patterns of ["*"] from the value_str using regex
arg = re.sub(r"\[\".*\"\]", "", value_str) args_names=(re.sub(r"\[\".*\"\]", "", value_str),)
setter = f"(({arg}) => {setter}({value_str}))" if value_str.startswith("_")
else: else (),
setter = f"(() => {setter}({value_str}))" return_expr=setter.call(value_var),
return Var( )
_js_expr=setter,
_var_data=_var_data, return setter.to(FunctionVar, EventChain)
).to(FunctionVar, EventChain)
@property @property
def set(self) -> Var: def set(self) -> Var:

View File

@ -28,8 +28,8 @@ import typer
from alembic.util.exc import CommandError from alembic.util.exc import CommandError
from packaging import version from packaging import version
from redis import Redis as RedisSync from redis import Redis as RedisSync
from redis import exceptions
from redis.asyncio import Redis from redis.asyncio import Redis
from redis.exceptions import RedisError
from reflex import constants, model from reflex import constants, model
from reflex.compiler import templates from reflex.compiler import templates
@ -333,10 +333,11 @@ def get_redis() -> Redis | None:
Returns: Returns:
The asynchronous redis client. The asynchronous redis client.
""" """
if isinstance((redis_url_or_options := parse_redis_url()), str): if (redis_url := parse_redis_url()) is not None:
return Redis.from_url(redis_url_or_options) return Redis.from_url(
elif isinstance(redis_url_or_options, dict): redis_url,
return Redis(**redis_url_or_options) retry_on_error=[RedisError],
)
return None return None
@ -346,14 +347,15 @@ def get_redis_sync() -> RedisSync | None:
Returns: Returns:
The synchronous redis client. The synchronous redis client.
""" """
if isinstance((redis_url_or_options := parse_redis_url()), str): if (redis_url := parse_redis_url()) is not None:
return RedisSync.from_url(redis_url_or_options) return RedisSync.from_url(
elif isinstance(redis_url_or_options, dict): redis_url,
return RedisSync(**redis_url_or_options) retry_on_error=[RedisError],
)
return None return None
def parse_redis_url() -> str | dict | None: def parse_redis_url() -> str | None:
"""Parse the REDIS_URL in config if applicable. """Parse the REDIS_URL in config if applicable.
Returns: Returns:
@ -387,7 +389,7 @@ async def get_redis_status() -> dict[str, bool | None]:
redis_client.ping() redis_client.ping()
else: else:
status = None status = None
except exceptions.RedisError: except RedisError:
status = False status = False
return {"redis": status} return {"redis": status}

View File

@ -127,7 +127,7 @@ class VarData:
state: str = "", state: str = "",
field_name: str = "", field_name: str = "",
imports: ImportDict | ParsedImportDict | None = None, imports: ImportDict | ParsedImportDict | None = None,
hooks: dict[str, None] | None = None, hooks: dict[str, VarData | None] | None = None,
deps: list[Var] | None = None, deps: list[Var] | None = None,
position: Hooks.HookPosition | None = None, position: Hooks.HookPosition | None = None,
): ):
@ -194,7 +194,9 @@ class VarData:
(var_data.state for var_data in all_var_datas if var_data.state), "" (var_data.state for var_data in all_var_datas if var_data.state), ""
) )
hooks = {hook: None for var_data in all_var_datas for hook in var_data.hooks} hooks: dict[str, VarData | None] = {
hook: None for var_data in all_var_datas for hook in var_data.hooks
}
_imports = imports.merge_imports( _imports = imports.merge_imports(
*(var_data.imports for var_data in all_var_datas) *(var_data.imports for var_data in all_var_datas)
@ -2276,7 +2278,7 @@ def computed_var(
def computed_var( def computed_var(
fget: Callable[[BASE_STATE], Any] | None = None, fget: Callable[[BASE_STATE], Any] | None = None,
initial_value: Any | types.Unset = types.Unset(), initial_value: Any | types.Unset = types.Unset(),
cache: bool = False, cache: Optional[bool] = None,
deps: Optional[List[Union[str, Var]]] = None, deps: Optional[List[Union[str, Var]]] = None,
auto_deps: bool = True, auto_deps: bool = True,
interval: Optional[Union[datetime.timedelta, int]] = None, interval: Optional[Union[datetime.timedelta, int]] = None,
@ -2302,6 +2304,15 @@ def computed_var(
ValueError: If caching is disabled and an update interval is set. ValueError: If caching is disabled and an update interval is set.
VarDependencyError: If user supplies dependencies without caching. VarDependencyError: If user supplies dependencies without caching.
""" """
if cache is None:
cache = False
console.deprecate(
"Default non-cached rx.var",
"the default value will be `@rx.var(cache=True)` in a future release. "
"To retain uncached var, explicitly pass `@rx.var(cache=False)`",
deprecation_version="0.6.8",
removal_version="0.7.0",
)
if cache is False and interval is not None: if cache is False and interval is not None:
raise ValueError("Cannot set update interval without caching.") raise ValueError("Cannot set update interval without caching.")

View File

@ -43,6 +43,8 @@ def LifespanApp():
lifespan_task_global = 0 lifespan_task_global = 0
class LifespanState(rx.State): class LifespanState(rx.State):
interval: int = 100
@rx.var @rx.var
def task_global(self) -> int: def task_global(self) -> int:
return lifespan_task_global return lifespan_task_global
@ -59,7 +61,15 @@ def LifespanApp():
return rx.vstack( return rx.vstack(
rx.text(LifespanState.task_global, id="task_global"), rx.text(LifespanState.task_global, id="task_global"),
rx.text(LifespanState.context_global, id="context_global"), rx.text(LifespanState.context_global, id="context_global"),
rx.moment(interval=100, on_change=LifespanState.tick), rx.button(
rx.moment(
interval=LifespanState.interval, on_change=LifespanState.tick
),
on_click=LifespanState.set_interval( # type: ignore
rx.cond(LifespanState.interval, 0, 100)
),
id="toggle-tick",
),
) )
app = rx.App() app = rx.App()
@ -108,6 +118,7 @@ async def test_lifespan(lifespan_app: AppHarness):
original_task_global_text = task_global.text original_task_global_text = task_global.text
original_task_global_value = int(original_task_global_text) original_task_global_value = int(original_task_global_text)
lifespan_app.poll_for_content(task_global, exp_not_equal=original_task_global_text) lifespan_app.poll_for_content(task_global, exp_not_equal=original_task_global_text)
driver.find_element(By.ID, "toggle-tick").click() # avoid teardown errors
assert lifespan_app.app_module.lifespan_task_global > original_task_global_value # type: ignore assert lifespan_app.app_module.lifespan_task_global > original_task_global_value # type: ignore
assert int(task_global.text) > original_task_global_value assert int(task_global.text) > original_task_global_value

View File

@ -0,0 +1,46 @@
from typing import Generator
import pytest
from playwright.sync_api import Page, expect
from reflex.testing import AppHarness
def LinkApp():
import reflex as rx
app = rx.App()
def index():
return rx.vstack(
rx.box(height="10em"), # spacer, so the link isn't hovered initially
rx.link(
"Click me",
href="#",
color="blue",
_hover=rx.Style({"color": "red"}),
),
)
app.add_page(index, "/")
@pytest.fixture()
def link_app(tmp_path_factory) -> Generator[AppHarness, None, None]:
with AppHarness.create(
root=tmp_path_factory.mktemp("link_app"),
app_source=LinkApp, # type: ignore
) as harness:
assert harness.app_instance is not None, "app is not running"
yield harness
def test_link_hover(link_app: AppHarness, page: Page):
assert link_app.frontend_url is not None
page.goto(link_app.frontend_url)
link = page.get_by_role("link")
expect(link).to_have_text("Click me")
expect(link).to_have_css("color", "rgb(0, 0, 255)")
link.hover()
expect(link).to_have_css("color", "rgb(255, 0, 0)")