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:
|
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
|
||||||
|
10
.github/workflows/check_generated_pyi.yml
vendored
10
.github/workflows/check_generated_pyi.yml
vendored
@ -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: |
|
||||||
|
67
.github/workflows/check_node_latest.yml
vendored
67
.github/workflows/check_node_latest.yml
vendored
@ -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}}
|
||||||
|
130
.github/workflows/check_outdated_dependencies.yml
vendored
130
.github/workflows/check_outdated_dependencies.yml
vendored
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
40
.github/workflows/integration_tests.yml
vendored
40
.github/workflows/integration_tests.yml
vendored
@ -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
|
||||||
|
|
6
.github/workflows/pre-commit.yml
vendored
6
.github/workflows/pre-commit.yml
vendored
@ -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)
|
||||||
|
28
.github/workflows/unit_tests.yml
vendored
28
.github/workflows/unit_tests.yml
vendored
@ -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=
|
||||||
|
@ -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"
|
||||||
|
@ -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)}}
|
||||||
|
@ -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)}}
|
||||||
)
|
)
|
||||||
|
@ -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)}}
|
||||||
|
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 %}
|
{% 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)}}
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
)
|
)
|
||||||
|
@ -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"
|
||||||
|
@ -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,
|
||||||
|
@ -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.
|
||||||
|
@ -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}
|
||||||
)
|
)
|
||||||
|
@ -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(),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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]:
|
||||||
|
@ -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}
|
||||||
|
@ -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)
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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}
|
||||||
|
@ -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.")
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
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