Merge branch 'main' into lendemor/fix_duplicate_tab_issue
This commit is contained in:
commit
41d8cfae57
42
.github/workflows/benchmarks.yml
vendored
42
.github/workflows/benchmarks.yml
vendored
@ -5,7 +5,7 @@ on:
|
||||
types:
|
||||
- closed
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- "**/*.md"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@ -15,21 +15,21 @@ defaults:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
PYTHONIOENCODING: 'utf8'
|
||||
PYTHONIOENCODING: "utf8"
|
||||
TELEMETRY_ENABLED: false
|
||||
NODE_OPTIONS: '--max_old_space_size=8192'
|
||||
NODE_OPTIONS: "--max_old_space_size=8192"
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
|
||||
jobs:
|
||||
reflex-web:
|
||||
# if: github.event.pull_request.merged == true
|
||||
# if: github.event.pull_request.merged == true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Show OS combos first in GUI
|
||||
os: [ubuntu-latest]
|
||||
python-version: ['3.11.4']
|
||||
node-version: ['18.x']
|
||||
python-version: ["3.12.8"]
|
||||
node-version: ["18.x"]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@ -81,24 +81,24 @@ jobs:
|
||||
matrix:
|
||||
# Show OS combos first in GUI
|
||||
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:
|
||||
- os: windows-latest
|
||||
python-version: '3.10.13'
|
||||
python-version: "3.10.16"
|
||||
- os: windows-latest
|
||||
python-version: '3.9.18'
|
||||
python-version: "3.9.21"
|
||||
# keep only one python version for MacOS
|
||||
- os: macos-latest
|
||||
python-version: '3.9.18'
|
||||
python-version: "3.9.21"
|
||||
- os: macos-latest
|
||||
python-version: '3.10.13'
|
||||
python-version: "3.10.16"
|
||||
- os: macos-latest
|
||||
python-version: '3.12.0'
|
||||
python-version: "3.11.11"
|
||||
include:
|
||||
- os: windows-latest
|
||||
python-version: '3.10.11'
|
||||
python-version: "3.10.11"
|
||||
- os: windows-latest
|
||||
python-version: '3.9.13'
|
||||
python-version: "3.9.13"
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@ -123,7 +123,7 @@ jobs:
|
||||
--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)
|
||||
if: github.event.pull_request.merged == true
|
||||
if: github.event.pull_request.merged == true
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
# Prioritize getting more information out of the workflow (even if something fails)
|
||||
@ -133,7 +133,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup_build_env
|
||||
with:
|
||||
python-version: 3.11.5
|
||||
python-version: 3.12.8
|
||||
run-poetry-install: true
|
||||
create-venv-at-path: .venv
|
||||
- name: Build reflex
|
||||
@ -143,12 +143,12 @@ jobs:
|
||||
# Only run if the database creds are available in this context.
|
||||
run:
|
||||
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 }}"
|
||||
--path ./dist
|
||||
|
||||
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
|
||||
strategy:
|
||||
# Prioritize getting more information out of the workflow (even if something fails)
|
||||
@ -156,7 +156,7 @@ jobs:
|
||||
matrix:
|
||||
# Show OS combos first in GUI
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
python-version: ['3.11.5']
|
||||
python-version: ["3.12.8"]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@ -186,6 +186,6 @@ jobs:
|
||||
run:
|
||||
poetry run python benchmarks/benchmark_package_size.py --os "${{ matrix.os }}"
|
||||
--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 }}"
|
||||
--path ./.venv
|
||||
--path ./.venv
|
||||
|
10
.github/workflows/check_generated_pyi.yml
vendored
10
.github/workflows/check_generated_pyi.yml
vendored
@ -6,16 +6,16 @@ concurrency:
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
branches: ["main"]
|
||||
# We don't just trigger on make_pyi.py and the components dir, because
|
||||
# there are other things that can change the generator output
|
||||
# e.g. black version, reflex.Component, reflex.Var.
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- "**/*.md"
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
branches: ["main"]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- "**/*.md"
|
||||
|
||||
jobs:
|
||||
check-generated-pyi-components:
|
||||
@ -25,7 +25,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup_build_env
|
||||
with:
|
||||
python-version: '3.11.5'
|
||||
python-version: "3.12.8"
|
||||
run-poetry-install: true
|
||||
create-venv-at-path: .venv
|
||||
- run: |
|
||||
|
67
.github/workflows/check_node_latest.yml
vendored
67
.github/workflows/check_node_latest.yml
vendored
@ -1,43 +1,40 @@
|
||||
name: integration-node-latest
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
TELEMETRY_ENABLED: false
|
||||
REFLEX_USE_SYSTEM_NODE: true
|
||||
TELEMETRY_ENABLED: false
|
||||
REFLEX_USE_SYSTEM_NODE: true
|
||||
|
||||
jobs:
|
||||
check_latest_node:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ['3.12']
|
||||
split_index: [1, 2]
|
||||
node-version: ['node']
|
||||
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}}
|
||||
|
||||
|
||||
check_latest_node:
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.12.8"]
|
||||
split_index: [1, 2]
|
||||
node-version: ["node"]
|
||||
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}}
|
||||
|
130
.github/workflows/check_outdated_dependencies.yml
vendored
130
.github/workflows/check_outdated_dependencies.yml
vendored
@ -1,88 +1,86 @@
|
||||
name: check-outdated-dependencies
|
||||
|
||||
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:
|
||||
- 'release/**' # This will trigger the action when any branch starting with "release/" is created.
|
||||
workflow_dispatch: # Allow manual triggering if needed.
|
||||
- "release/**" # This will trigger the action when any branch starting with "release/" is created.
|
||||
workflow_dispatch: # Allow manual triggering if needed.
|
||||
|
||||
jobs:
|
||||
backend:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- uses: ./.github/actions/setup_build_env
|
||||
with:
|
||||
python-version: '3.9'
|
||||
run-poetry-install: true
|
||||
create-venv-at-path: .venv
|
||||
- uses: ./.github/actions/setup_build_env
|
||||
with:
|
||||
python-version: "3.9.21"
|
||||
run-poetry-install: true
|
||||
create-venv-at-path: .venv
|
||||
|
||||
- name: Check outdated backend dependencies
|
||||
run: |
|
||||
outdated=$(poetry show -oT)
|
||||
echo "Outdated:"
|
||||
echo "$outdated"
|
||||
- name: Check outdated backend dependencies
|
||||
run: |
|
||||
outdated=$(poetry show -oT)
|
||||
echo "Outdated:"
|
||||
echo "$outdated"
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
frontend:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup_build_env
|
||||
with:
|
||||
python-version: '3.10.11'
|
||||
run-poetry-install: true
|
||||
create-venv-at-path: .venv
|
||||
- name: Clone Reflex Website Repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: reflex-dev/reflex-web
|
||||
ref: main
|
||||
path: reflex-web
|
||||
- name: Install Requirements for reflex-web
|
||||
working-directory: ./reflex-web
|
||||
run: poetry run uv pip install -r requirements.txt
|
||||
- name: Install additional dependencies for DB access
|
||||
run: poetry run uv pip install psycopg
|
||||
- name: Init Website for reflex-web
|
||||
working-directory: ./reflex-web
|
||||
run: poetry run reflex init
|
||||
- name: Run Website and Check for errors
|
||||
run: |
|
||||
poetry run bash scripts/integration.sh ./reflex-web dev
|
||||
- name: Check outdated frontend dependencies
|
||||
working-directory: ./reflex-web/.web
|
||||
run: |
|
||||
raw_outdated=$(/home/runner/.local/share/reflex/bun/bin/bun outdated)
|
||||
outdated=$(echo "$raw_outdated" | grep -vE '\|\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\|' || true)
|
||||
echo "Outdated:"
|
||||
echo "$outdated"
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup_build_env
|
||||
with:
|
||||
python-version: "3.10.16"
|
||||
run-poetry-install: true
|
||||
create-venv-at-path: .venv
|
||||
- name: Clone Reflex Website Repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: reflex-dev/reflex-web
|
||||
ref: main
|
||||
path: reflex-web
|
||||
- name: Install Requirements for reflex-web
|
||||
working-directory: ./reflex-web
|
||||
run: poetry run uv pip install -r requirements.txt
|
||||
- name: Install additional dependencies for DB access
|
||||
run: poetry run uv pip install psycopg
|
||||
- name: Init Website for reflex-web
|
||||
working-directory: ./reflex-web
|
||||
run: poetry run reflex init
|
||||
- name: Run Website and Check for errors
|
||||
run: |
|
||||
poetry run bash scripts/integration.sh ./reflex-web dev
|
||||
- name: Check outdated frontend dependencies
|
||||
working-directory: ./reflex-web/.web
|
||||
run: |
|
||||
raw_outdated=$(/home/runner/.local/share/reflex/bun/bin/bun outdated)
|
||||
outdated=$(echo "$raw_outdated" | grep -vE '\|\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\|' || true)
|
||||
echo "Outdated:"
|
||||
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)
|
||||
no_extra=$(echo "$filtered_outdated" | grep -vE '\|\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-' || true)
|
||||
# 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)
|
||||
no_extra=$(echo "$filtered_outdated" | grep -vE '\|\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-' || true)
|
||||
|
||||
|
||||
if [ ! -z "$no_extra" ]; then
|
||||
echo "Outdated dependencies found:"
|
||||
echo "$filtered_outdated"
|
||||
exit 1
|
||||
else
|
||||
echo "All dependencies are up to date. (3rd party packages are ignored)"
|
||||
fi
|
||||
|
||||
if [ ! -z "$no_extra" ]; then
|
||||
echo "Outdated dependencies found:"
|
||||
echo "$filtered_outdated"
|
||||
exit 1
|
||||
else
|
||||
echo "All dependencies are up to date. (3rd party packages are ignored)"
|
||||
fi
|
||||
|
@ -22,8 +22,8 @@ jobs:
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
matrix:
|
||||
state_manager: ['redis', 'memory']
|
||||
python-version: ['3.11.5', '3.12.0', '3.13.0']
|
||||
state_manager: ["redis", "memory"]
|
||||
python-version: ["3.11.11", "3.12.8", "3.13.1"]
|
||||
split_index: [1, 2]
|
||||
fail-fast: false
|
||||
runs-on: ubuntu-22.04
|
||||
@ -53,7 +53,7 @@ jobs:
|
||||
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 --with-deps
|
||||
poetry run playwright install chromium
|
||||
poetry run pytest tests/integration --splits 2 --group ${{matrix.split_index}}
|
||||
- uses: actions/upload-artifact@v4
|
||||
name: Upload failed test screenshots
|
||||
|
40
.github/workflows/integration_tests.yml
vendored
40
.github/workflows/integration_tests.yml
vendored
@ -2,13 +2,13 @@ name: integration-tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
branches: ["main"]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- "**/*.md"
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
branches: ["main"]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- "**/*.md"
|
||||
|
||||
concurrency:
|
||||
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)
|
||||
# - Catch encoding errors when printing logs
|
||||
# - Best effort print lines that contain illegal chars (map to some default char, etc.)
|
||||
PYTHONIOENCODING: 'utf8'
|
||||
PYTHONIOENCODING: "utf8"
|
||||
TELEMETRY_ENABLED: false
|
||||
NODE_OPTIONS: '--max_old_space_size=8192'
|
||||
NODE_OPTIONS: "--max_old_space_size=8192"
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
|
||||
jobs:
|
||||
@ -43,17 +43,22 @@ jobs:
|
||||
matrix:
|
||||
# Show OS combos first in GUI
|
||||
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:
|
||||
- os: windows-latest
|
||||
python-version: '3.10.13'
|
||||
python-version: "3.11.11"
|
||||
- os: windows-latest
|
||||
python-version: '3.9.18'
|
||||
python-version: "3.10.16"
|
||||
- os: windows-latest
|
||||
python-version: "3.9.21"
|
||||
include:
|
||||
- os: windows-latest
|
||||
python-version: '3.10.11'
|
||||
python-version: "3.11.9"
|
||||
- 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 }}
|
||||
steps:
|
||||
@ -117,18 +122,16 @@ jobs:
|
||||
--branch-name "${{ github.head_ref || github.ref_name }}" --pr-id "${{ github.event.pull_request.id }}"
|
||||
--app-name "counter"
|
||||
|
||||
|
||||
|
||||
reflex-web:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Show OS combos first in GUI
|
||||
os: [ubuntu-latest]
|
||||
python-version: ['3.10.11', '3.11.4']
|
||||
python-version: ["3.11.11", "3.12.8"]
|
||||
|
||||
env:
|
||||
REFLEX_WEB_WINDOWS_OVERRIDE: '1'
|
||||
REFLEX_WEB_WINDOWS_OVERRIDE: "1"
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@ -173,7 +176,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup_build_env
|
||||
with:
|
||||
python-version: '3.11.4'
|
||||
python-version: "3.11.11"
|
||||
run-poetry-install: true
|
||||
create-venv-at-path: .venv
|
||||
- name: Create app directory
|
||||
@ -192,14 +195,14 @@ jobs:
|
||||
# Check that npm is home
|
||||
npm -v
|
||||
poetry run bash scripts/integration.sh ./rx-shout-from-template prod
|
||||
|
||||
|
||||
reflex-web-macos:
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
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
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@ -233,4 +236,3 @@ jobs:
|
||||
--python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
|
||||
--pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}"
|
||||
--app-name "reflex-web" --path ./reflex-web/.web
|
||||
|
6
.github/workflows/pre-commit.yml
vendored
6
.github/workflows/pre-commit.yml
vendored
@ -6,12 +6,12 @@ concurrency:
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
branches: ["main"]
|
||||
push:
|
||||
# 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
|
||||
# when merging into main branch.
|
||||
branches: ['main']
|
||||
branches: ["main"]
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
@ -23,7 +23,7 @@ jobs:
|
||||
with:
|
||||
# running vs. one version of Python is OK
|
||||
# i.e. ruff, black, etc.
|
||||
python-version: 3.11.5
|
||||
python-version: 3.12.8
|
||||
run-poetry-install: true
|
||||
create-venv-at-path: .venv
|
||||
# TODO pre-commit related stuff can be cached too (not a bottleneck yet)
|
||||
|
28
.github/workflows/unit_tests.yml
vendored
28
.github/workflows/unit_tests.yml
vendored
@ -6,13 +6,13 @@ concurrency:
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['main']
|
||||
branches: ["main"]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- "**/*.md"
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
branches: ["main"]
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- "**/*.md"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@ -28,18 +28,22 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
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:
|
||||
- os: windows-latest
|
||||
python-version: '3.10.13'
|
||||
python-version: "3.11.11"
|
||||
- os: windows-latest
|
||||
python-version: '3.9.18'
|
||||
python-version: "3.10.16"
|
||||
- os: windows-latest
|
||||
python-version: "3.9.21"
|
||||
include:
|
||||
- os: windows-latest
|
||||
python-version: '3.10.11'
|
||||
python-version: "3.11.9"
|
||||
- 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 }}
|
||||
|
||||
# Service containers to run with `runner-job`
|
||||
@ -88,8 +92,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Note: py39, py310 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']
|
||||
# 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"]
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@ -106,4 +110,4 @@ jobs:
|
||||
run: |
|
||||
export PYTHONUNBUFFERED=1
|
||||
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=
|
||||
|
@ -16,7 +16,6 @@ repository = "https://github.com/reflex-dev/reflex"
|
||||
documentation = "https://reflex.dev/docs/getting-started/introduction"
|
||||
keywords = ["web", "framework"]
|
||||
classifiers = ["Development Status :: 4 - Beta"]
|
||||
packages = [{ include = "reflex" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
|
@ -1,4 +1,5 @@
|
||||
{% extends "web/pages/base_page.js.jinja2" %}
|
||||
{% from "web/pages/macros.js.jinja2" import renderHooks %}
|
||||
|
||||
{% block early_imports %}
|
||||
import '$/styles/styles.css'
|
||||
@ -18,10 +19,7 @@ import * as {{library_alias}} from "{{library_path}}";
|
||||
|
||||
{% block export %}
|
||||
function AppWrap({children}) {
|
||||
|
||||
{% for hook in hooks %}
|
||||
{{ hook }}
|
||||
{% endfor %}
|
||||
{{ renderHooks(hooks) }}
|
||||
|
||||
return (
|
||||
{{utils.render(render, indent_width=0)}}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% extends "web/pages/base_page.js.jinja2" %}
|
||||
|
||||
{% from "web/pages/macros.js.jinja2" import renderHooks %}
|
||||
{% block export %}
|
||||
{% for component in components %}
|
||||
|
||||
@ -8,9 +8,8 @@
|
||||
{% endfor %}
|
||||
|
||||
export const {{component.name}} = memo(({ {{-component.props|join(", ")-}} }) => {
|
||||
{% for hook in component.hooks %}
|
||||
{{ hook }}
|
||||
{% endfor %}
|
||||
{{ renderHooks(component.hooks) }}
|
||||
|
||||
return(
|
||||
{{utils.render(component.render)}}
|
||||
)
|
||||
|
@ -1,4 +1,5 @@
|
||||
{% extends "web/pages/base_page.js.jinja2" %}
|
||||
{% from "web/pages/macros.js.jinja2" import renderHooks %}
|
||||
|
||||
{% block declaration %}
|
||||
{% for custom_code in custom_codes %}
|
||||
@ -8,9 +9,7 @@
|
||||
|
||||
{% block export %}
|
||||
export default function Component() {
|
||||
{% for hook in hooks %}
|
||||
{{ hook }}
|
||||
{% endfor %}
|
||||
{{ renderHooks(hooks)}}
|
||||
|
||||
return (
|
||||
{{utils.render(render, indent_width=0)}}
|
||||
|
38
reflex/.templates/jinja/web/pages/macros.js.jinja2
Normal file
38
reflex/.templates/jinja/web/pages/macros.js.jinja2
Normal 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 %}
|
@ -1,22 +1,10 @@
|
||||
{% 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}} () {
|
||||
{% for hook in component._get_all_hooks_internal() %}
|
||||
{{ 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 %}
|
||||
|
||||
{{ renderHooksWithMemo(all_hooks, memo_trigger_hooks) }}
|
||||
|
||||
return (
|
||||
{{utils.render(component.render(), indent_width=0)}}
|
||||
)
|
||||
|
@ -75,7 +75,7 @@ def _compile_app(app_root: Component) -> str:
|
||||
return templates.APP_ROOT.render(
|
||||
imports=utils.compile_imports(app_root._get_all_imports()),
|
||||
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,
|
||||
render=app_root.render(),
|
||||
)
|
||||
@ -149,7 +149,7 @@ def _compile_page(
|
||||
imports=imports,
|
||||
dynamic_imports=component._get_all_dynamic_imports(),
|
||||
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(),
|
||||
**kwargs,
|
||||
)
|
||||
|
@ -1,9 +1,46 @@
|
||||
"""Templates to use in the reflex compiler."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader, Template
|
||||
|
||||
from reflex import constants
|
||||
from reflex.constants import Hooks
|
||||
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):
|
||||
@ -47,6 +84,7 @@ class ReflexJinjaEnvironment(Environment):
|
||||
"frontend_exception_state": constants.CompileVars.FRONTEND_EXCEPTION_STATE_FULL,
|
||||
"hook_position": constants.Hooks.HookPosition,
|
||||
}
|
||||
self.globals["sort_hooks"] = _sort_hooks
|
||||
|
||||
|
||||
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
|
||||
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.
|
||||
CUSTOM_COMPONENTS_PYPROJECT_TOML = get_template(
|
||||
"custom_components/pyproject.toml.jinja2"
|
||||
|
@ -290,7 +290,7 @@ def compile_custom_component(
|
||||
"name": component.tag,
|
||||
"props": props,
|
||||
"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(),
|
||||
},
|
||||
imports,
|
||||
|
@ -9,6 +9,7 @@ from reflex.components.tags import Tag
|
||||
from reflex.components.tags.tagless import Tagless
|
||||
from reflex.utils.imports import ParsedImportDict
|
||||
from reflex.vars import BooleanVar, ObjectVar, Var
|
||||
from reflex.vars.base import VarData
|
||||
|
||||
|
||||
class Bare(Component):
|
||||
@ -32,7 +33,7 @@ class Bare(Component):
|
||||
contents = str(contents) if contents is not None else ""
|
||||
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.
|
||||
|
||||
Returns:
|
||||
@ -43,7 +44,7 @@ class Bare(Component):
|
||||
hooks |= self.contents._var_value._get_all_hooks_internal()
|
||||
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.
|
||||
|
||||
Returns:
|
||||
@ -107,11 +108,14 @@ class Bare(Component):
|
||||
return Tagless(contents=f"{{{self.contents!s}}}")
|
||||
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.
|
||||
|
||||
Args:
|
||||
include_children: Whether to include Vars from children.
|
||||
ignore_ids: The ids to ignore.
|
||||
|
||||
Yields:
|
||||
The contents if it is a Var, otherwise nothing.
|
||||
|
@ -102,7 +102,7 @@ class BaseComponent(Base, ABC):
|
||||
"""
|
||||
|
||||
@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.
|
||||
|
||||
Returns:
|
||||
@ -110,7 +110,7 @@ class BaseComponent(Base, ABC):
|
||||
"""
|
||||
|
||||
@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.
|
||||
|
||||
Returns:
|
||||
@ -1020,18 +1020,22 @@ class Component(BaseComponent, ABC):
|
||||
event_args.append(spec)
|
||||
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.
|
||||
|
||||
Args:
|
||||
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).
|
||||
"""
|
||||
vars = getattr(self, "__vars", None)
|
||||
ignore_ids = ignore_ids or set()
|
||||
vars: List[Var] | None = getattr(self, "__vars", None)
|
||||
if vars is not None:
|
||||
return vars
|
||||
yield from vars
|
||||
vars = self.__vars = []
|
||||
# Get Vars associated with event trigger arguments.
|
||||
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.
|
||||
if include_children:
|
||||
for child in self.children:
|
||||
if not isinstance(child, Component):
|
||||
if not isinstance(child, Component) or id(child) in ignore_ids:
|
||||
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)
|
||||
|
||||
return vars
|
||||
yield from vars
|
||||
|
||||
def _event_trigger_values_use_state(self) -> bool:
|
||||
"""Check if the values of a component's event trigger use state.
|
||||
@ -1272,7 +1279,7 @@ class Component(BaseComponent, ABC):
|
||||
"""
|
||||
_imports = {}
|
||||
|
||||
if self._get_ref_hook():
|
||||
if self._get_ref_hook() is not None:
|
||||
# Handle hooks needed for attaching react refs to DOM nodes.
|
||||
_imports.setdefault("react", set()).add(ImportVar(tag="useRef"))
|
||||
_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.
|
||||
|
||||
Returns:
|
||||
@ -1396,11 +1403,12 @@ class Component(BaseComponent, ABC):
|
||||
"""
|
||||
ref = self.get_ref()
|
||||
if ref is not None:
|
||||
return (
|
||||
f"const {ref} = useRef(null); {Var(_js_expr=ref)._as_ref()!s} = {ref};"
|
||||
return Var(
|
||||
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.
|
||||
|
||||
Returns:
|
||||
@ -1413,27 +1421,38 @@ class Component(BaseComponent, ABC):
|
||||
vars_hooks.update(
|
||||
var_data.hooks
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
Returns:
|
||||
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.
|
||||
|
||||
Returns:
|
||||
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.
|
||||
|
||||
Downstream components should NOT override this method to avoid breaking
|
||||
@ -1444,7 +1463,7 @@ class Component(BaseComponent, ABC):
|
||||
"""
|
||||
return {
|
||||
**{
|
||||
hook: None
|
||||
str(hook): VarData(position=Hooks.HookPosition.INTERNAL)
|
||||
for hook in [self._get_ref_hook(), self._get_mount_lifecycle_hook()]
|
||||
if hook is not None
|
||||
},
|
||||
@ -1493,7 +1512,7 @@ class Component(BaseComponent, ABC):
|
||||
"""
|
||||
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.
|
||||
|
||||
Returns:
|
||||
@ -1508,7 +1527,7 @@ class Component(BaseComponent, ABC):
|
||||
|
||||
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.
|
||||
|
||||
Returns:
|
||||
@ -1516,6 +1535,9 @@ class Component(BaseComponent, ABC):
|
||||
"""
|
||||
code = {}
|
||||
|
||||
# Add the internal hooks for this component.
|
||||
code.update(self._get_hooks_internal())
|
||||
|
||||
# Add the hook code for this component.
|
||||
hooks = self._get_hooks()
|
||||
if hooks is not None:
|
||||
@ -1796,19 +1818,25 @@ class CustomComponent(Component):
|
||||
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.
|
||||
|
||||
Args:
|
||||
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).
|
||||
"""
|
||||
return (
|
||||
super()._get_vars(include_children=include_children)
|
||||
+ [prop for prop in self.props.values() if isinstance(prop, Var)]
|
||||
+ self.get_component(self)._get_vars(include_children=include_children)
|
||||
ignore_ids = ignore_ids or set()
|
||||
yield from super()._get_vars(
|
||||
include_children=include_children, ignore_ids=ignore_ids
|
||||
)
|
||||
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
|
||||
@ -2211,7 +2239,7 @@ class StatefulComponent(BaseComponent):
|
||||
)
|
||||
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.
|
||||
|
||||
Returns:
|
||||
@ -2219,7 +2247,7 @@ class StatefulComponent(BaseComponent):
|
||||
"""
|
||||
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.
|
||||
|
||||
Returns:
|
||||
@ -2337,7 +2365,7 @@ class MemoizationLeaf(Component):
|
||||
The memoization leaf
|
||||
"""
|
||||
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(
|
||||
update={"disposition": MemoizationDisposition.ALWAYS}
|
||||
)
|
||||
|
@ -502,8 +502,8 @@ class CodeBlock(Component, MarkdownComponentMap):
|
||||
|
||||
theme = self.theme
|
||||
|
||||
out.add_props(style=theme).remove_props("theme", "code", "language").add_props(
|
||||
children=self.code, language=_LANGUAGE
|
||||
out.add_props(style=theme).remove_props("theme", "code").add_props(
|
||||
children=self.code,
|
||||
)
|
||||
|
||||
return out
|
||||
@ -512,20 +512,25 @@ class CodeBlock(Component, MarkdownComponentMap):
|
||||
return ["can_copy", "copy_button"]
|
||||
|
||||
@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.
|
||||
|
||||
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:
|
||||
The hook to register the language.
|
||||
"""
|
||||
return f"""
|
||||
if ({_LANGUAGE!s}) {{
|
||||
if ({language_var!s}) {{
|
||||
(async () => {{
|
||||
try {{
|
||||
const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${{{_LANGUAGE!s}}}`);
|
||||
SyntaxHighlighter.registerLanguage({_LANGUAGE!s}, module.default);
|
||||
const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${{{language_var!s}}}`);
|
||||
SyntaxHighlighter.registerLanguage({language_var!s}, module.default);
|
||||
}} 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.
|
||||
"""
|
||||
return [
|
||||
f"const {_LANGUAGE!s} = {self.language!s}",
|
||||
self._get_language_registration_hook(),
|
||||
self._get_language_registration_hook(language_var=self.language),
|
||||
]
|
||||
|
||||
|
||||
|
@ -182,9 +182,7 @@ class Form(BaseHTML):
|
||||
props["handle_submit_unique_name"] = ""
|
||||
form = super().create(*children, **props)
|
||||
form.handle_submit_unique_name = md5(
|
||||
str({**form._get_all_hooks_internal(), **form._get_all_hooks()}).encode(
|
||||
"utf-8"
|
||||
)
|
||||
str(form._get_all_hooks()).encode("utf-8")
|
||||
).hexdigest()
|
||||
return form
|
||||
|
||||
@ -252,8 +250,12 @@ class Form(BaseHTML):
|
||||
)
|
||||
return form_refs
|
||||
|
||||
def _get_vars(self, include_children: bool = True) -> Iterator[Var]:
|
||||
yield from super()._get_vars(include_children=include_children)
|
||||
def _get_vars(
|
||||
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()
|
||||
|
||||
def _exclude_props(self) -> list[str]:
|
||||
|
@ -420,11 +420,12 @@ const {_LANGUAGE!s} = match ? match[1] : '';
|
||||
|
||||
def _get_custom_code(self) -> str | None:
|
||||
hooks = {}
|
||||
from reflex.compiler.templates import MACROS
|
||||
|
||||
for _component in self.component_map.values():
|
||||
comp = _component(_MOCK_ARG)
|
||||
hooks.update(comp._get_all_hooks_internal())
|
||||
hooks.update(comp._get_all_hooks())
|
||||
formatted_hooks = "\n".join(hooks.keys())
|
||||
formatted_hooks = MACROS.module.renderHooks(hooks) # type: ignore
|
||||
return f"""
|
||||
function {self._get_component_map_name()} () {{
|
||||
{formatted_hooks}
|
||||
|
@ -76,7 +76,7 @@ class Link(RadixThemesComponent, A, MemoizationLeaf, MarkdownComponentMap):
|
||||
Returns:
|
||||
Component: The link component
|
||||
"""
|
||||
props.setdefault(":hover", {"color": color("accent", 8)})
|
||||
props.setdefault("_hover", {"color": color("accent", 8)})
|
||||
href = props.get("href")
|
||||
|
||||
is_external = props.pop("is_external", None)
|
||||
|
@ -135,6 +135,7 @@ class Hooks(SimpleNamespace):
|
||||
class HookPosition(enum.Enum):
|
||||
"""The position of the hook in the component."""
|
||||
|
||||
INTERNAL = "internal"
|
||||
PRE_TRIGGER = "pre_trigger"
|
||||
POST_TRIGGER = "post_trigger"
|
||||
|
||||
|
@ -12,7 +12,7 @@ from reflex.event import EventChain, EventHandler, EventSpec, run_script
|
||||
from reflex.utils.imports import ImportVar
|
||||
from reflex.vars import VarData, get_unique_variable_name
|
||||
from reflex.vars.base import LiteralVar, Var
|
||||
from reflex.vars.function import FunctionVar
|
||||
from reflex.vars.function import ArgsFunctionOperationBuilder, FunctionVar
|
||||
|
||||
NoValue = object()
|
||||
|
||||
@ -45,6 +45,7 @@ class ClientStateVar(Var):
|
||||
# Track the names of the getters and setters
|
||||
_setter_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.
|
||||
_global_ref: bool = dataclasses.field(default=True)
|
||||
@ -96,6 +97,7 @@ class ClientStateVar(Var):
|
||||
"""
|
||||
if var_name is None:
|
||||
var_name = get_unique_variable_name()
|
||||
id_name = "id_" + get_unique_variable_name()
|
||||
if not isinstance(var_name, str):
|
||||
raise ValueError("var_name must be a string.")
|
||||
if default is NoValue:
|
||||
@ -105,20 +107,24 @@ class ClientStateVar(Var):
|
||||
else:
|
||||
default_var = default
|
||||
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,
|
||||
}
|
||||
imports = {
|
||||
"react": [ImportVar(tag="useState")],
|
||||
"react": [ImportVar(tag="useState"), ImportVar(tag="useId")],
|
||||
}
|
||||
if global_ref:
|
||||
hooks[f"{_client_state_ref(var_name)} = {var_name}"] = None
|
||||
hooks[f"{_client_state_ref(setter_name)} = {setter_name}"] = None
|
||||
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
|
||||
imports.update(_refs_import)
|
||||
return cls(
|
||||
_js_expr="",
|
||||
_setter_name=setter_name,
|
||||
_getter_name=var_name,
|
||||
_id_name=id_name,
|
||||
_global_ref=global_ref,
|
||||
_var_type=default_var._var_type,
|
||||
_var_data=VarData.merge(
|
||||
@ -144,10 +150,11 @@ class ClientStateVar(Var):
|
||||
return (
|
||||
Var(
|
||||
_js_expr=(
|
||||
_client_state_ref(self._getter_name)
|
||||
_client_state_ref(self._getter_name) + f"[{self._id_name}]"
|
||||
if self._global_ref
|
||||
else self._getter_name
|
||||
)
|
||||
),
|
||||
_var_data=self._var_data,
|
||||
)
|
||||
.to(self._var_type)
|
||||
._replace(
|
||||
@ -170,28 +177,43 @@ class ClientStateVar(Var):
|
||||
Returns:
|
||||
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 {})
|
||||
|
||||
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:
|
||||
# This is a hack to make it work like an EventSpec taking an arg
|
||||
value_var = LiteralVar.create(value)
|
||||
_var_data = VarData.merge(_var_data, value_var._get_all_var_data())
|
||||
value_str = str(value_var)
|
||||
|
||||
if value_str.startswith("_"):
|
||||
setter = ArgsFunctionOperationBuilder.create(
|
||||
# remove patterns of ["*"] from the value_str using regex
|
||||
arg = re.sub(r"\[\".*\"\]", "", value_str)
|
||||
setter = f"(({arg}) => {setter}({value_str}))"
|
||||
else:
|
||||
setter = f"(() => {setter}({value_str}))"
|
||||
return Var(
|
||||
_js_expr=setter,
|
||||
_var_data=_var_data,
|
||||
).to(FunctionVar, EventChain)
|
||||
args_names=(re.sub(r"\[\".*\"\]", "", value_str),)
|
||||
if value_str.startswith("_")
|
||||
else (),
|
||||
return_expr=setter.call(value_var),
|
||||
)
|
||||
|
||||
return setter.to(FunctionVar, EventChain)
|
||||
|
||||
@property
|
||||
def set(self) -> Var:
|
||||
|
@ -28,8 +28,8 @@ import typer
|
||||
from alembic.util.exc import CommandError
|
||||
from packaging import version
|
||||
from redis import Redis as RedisSync
|
||||
from redis import exceptions
|
||||
from redis.asyncio import Redis
|
||||
from redis.exceptions import RedisError
|
||||
|
||||
from reflex import constants, model
|
||||
from reflex.compiler import templates
|
||||
@ -333,10 +333,11 @@ def get_redis() -> Redis | None:
|
||||
Returns:
|
||||
The asynchronous redis client.
|
||||
"""
|
||||
if isinstance((redis_url_or_options := parse_redis_url()), str):
|
||||
return Redis.from_url(redis_url_or_options)
|
||||
elif isinstance(redis_url_or_options, dict):
|
||||
return Redis(**redis_url_or_options)
|
||||
if (redis_url := parse_redis_url()) is not None:
|
||||
return Redis.from_url(
|
||||
redis_url,
|
||||
retry_on_error=[RedisError],
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@ -346,14 +347,15 @@ def get_redis_sync() -> RedisSync | None:
|
||||
Returns:
|
||||
The synchronous redis client.
|
||||
"""
|
||||
if isinstance((redis_url_or_options := parse_redis_url()), str):
|
||||
return RedisSync.from_url(redis_url_or_options)
|
||||
elif isinstance(redis_url_or_options, dict):
|
||||
return RedisSync(**redis_url_or_options)
|
||||
if (redis_url := parse_redis_url()) is not None:
|
||||
return RedisSync.from_url(
|
||||
redis_url,
|
||||
retry_on_error=[RedisError],
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def parse_redis_url() -> str | dict | None:
|
||||
def parse_redis_url() -> str | None:
|
||||
"""Parse the REDIS_URL in config if applicable.
|
||||
|
||||
Returns:
|
||||
@ -387,7 +389,7 @@ async def get_redis_status() -> dict[str, bool | None]:
|
||||
redis_client.ping()
|
||||
else:
|
||||
status = None
|
||||
except exceptions.RedisError:
|
||||
except RedisError:
|
||||
status = False
|
||||
|
||||
return {"redis": status}
|
||||
|
@ -127,7 +127,7 @@ class VarData:
|
||||
state: str = "",
|
||||
field_name: str = "",
|
||||
imports: ImportDict | ParsedImportDict | None = None,
|
||||
hooks: dict[str, None] | None = None,
|
||||
hooks: dict[str, VarData | None] | None = None,
|
||||
deps: list[Var] | 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), ""
|
||||
)
|
||||
|
||||
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(
|
||||
*(var_data.imports for var_data in all_var_datas)
|
||||
@ -2276,7 +2278,7 @@ def computed_var(
|
||||
def computed_var(
|
||||
fget: Callable[[BASE_STATE], Any] | None = None,
|
||||
initial_value: Any | types.Unset = types.Unset(),
|
||||
cache: bool = False,
|
||||
cache: Optional[bool] = None,
|
||||
deps: Optional[List[Union[str, Var]]] = None,
|
||||
auto_deps: bool = True,
|
||||
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.
|
||||
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:
|
||||
raise ValueError("Cannot set update interval without caching.")
|
||||
|
||||
|
@ -43,6 +43,8 @@ def LifespanApp():
|
||||
lifespan_task_global = 0
|
||||
|
||||
class LifespanState(rx.State):
|
||||
interval: int = 100
|
||||
|
||||
@rx.var
|
||||
def task_global(self) -> int:
|
||||
return lifespan_task_global
|
||||
@ -59,7 +61,15 @@ def LifespanApp():
|
||||
return rx.vstack(
|
||||
rx.text(LifespanState.task_global, id="task_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()
|
||||
@ -108,6 +118,7 @@ async def test_lifespan(lifespan_app: AppHarness):
|
||||
original_task_global_text = 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)
|
||||
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 int(task_global.text) > original_task_global_value
|
||||
|
||||
|
46
tests/integration/tests_playwright/test_link_hover.py
Normal file
46
tests/integration/tests_playwright/test_link_hover.py
Normal 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)")
|
Loading…
Reference in New Issue
Block a user