Compare commits

..

7 Commits

Author SHA1 Message Date
Lendemor
bebe49a6a5 Merge branch 'main' into lendemor/add_custom_palette_function 2024-09-16 09:39:10 -07:00
Lendemor
be58a4c17e Merge branch 'main' into lendemor/add_custom_palette_function 2024-09-10 14:17:38 +02:00
Lendemor
68999a6f7c make coloraide dev dependencies 2024-09-06 18:30:20 +02:00
Lendemor
f36d1a0226 Merge branch 'main' into lendemor/add_custom_palette_function 2024-09-03 19:07:00 +02:00
Lendemor
d56e2ae532 remove coloraide dep 2024-09-03 19:00:57 +02:00
Lendemor
e93279890c add coloraide dependency for custom palette 2024-08-24 13:00:09 +02:00
Lendemor
d885cf6a66 add generate palette function in experimental 2024-08-24 12:54:14 +02:00
507 changed files with 35278 additions and 79024 deletions

View File

@ -11,7 +11,7 @@ omit =
[report] [report]
show_missing = true show_missing = true
# TODO bump back to 79 # TODO bump back to 79
fail_under = 70 fail_under = 60
precision = 2 precision = 2
# Regexes for lines to exclude from consideration # Regexes for lines to exclude from consideration

1
.github/CODEOWNERS vendored
View File

@ -1 +0,0 @@
@reflex-dev/reflex-team

View File

@ -2,6 +2,7 @@
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ''
labels: bug
assignees: '' assignees: ''
--- ---

View File

@ -1,19 +0,0 @@
---
name: Enhancement Request
about: Suggest an enhancement for an existing Reflex feature.
title: ''
labels: 'enhancement'
assignees: ''
---
**Describe the Enhancement you want**
A clear and concise description of what the improvement does.
- Which feature do you want to improve? (and what problem does it have)
- What is the benefit of the enhancement?
- Show an example/usecase were the improvement are needed.
**Additional context**
Add any other context here.

View File

@ -1,18 +0,0 @@
---
name: Feature Request
about: Suggest a new feature for Reflex
title: ''
labels: 'feature request'
assignees: ''
---
**Describe the Features**
A clear and concise description of what the features does.
- What is the purpose of the feature?
- Show an example / use cases for the new feature.
**Additional context**
Add any other context here.

View File

@ -6,7 +6,7 @@
# #
# Exit conditions: # Exit conditions:
# - Python of version `python-version` is ready to be invoked as `python`. # - Python of version `python-version` is ready to be invoked as `python`.
# - Poetry of version `poetry-version` is ready to be invoked as `poetry`. # - Poetry of version `poetry-version` is ready ot be invoked as `poetry`.
# - If `run-poetry-install` is true, deps as defined in `pyproject.toml` will have been installed into the venv at `create-venv-at-path`. # - If `run-poetry-install` is true, deps as defined in `pyproject.toml` will have been installed into the venv at `create-venv-at-path`.
name: 'Setup Reflex build environment' name: 'Setup Reflex build environment'
@ -18,7 +18,7 @@ inputs:
poetry-version: poetry-version:
description: 'Poetry version to install' description: 'Poetry version to install'
required: false required: false
default: '1.8.3' default: '1.3.1'
run-poetry-install: run-poetry-install:
description: 'Whether to run poetry install on current dir' description: 'Whether to run poetry install on current dir'
required: false required: false

View File

@ -1,2 +0,0 @@
paths-ignore:
- "**/tests/**"

View File

@ -5,7 +5,7 @@ on:
types: types:
- closed - closed
paths-ignore: paths-ignore:
- "**/*.md" - '**/*.md'
permissions: permissions:
contents: read contents: read
@ -15,21 +15,21 @@ defaults:
shell: bash shell: bash
env: env:
PYTHONIOENCODING: "utf8" PYTHONIOENCODING: 'utf8'
TELEMETRY_ENABLED: false TELEMETRY_ENABLED: false
NODE_OPTIONS: "--max_old_space_size=8192" NODE_OPTIONS: '--max_old_space_size=8192'
PR_TITLE: ${{ github.event.pull_request.title }} PR_TITLE: ${{ github.event.pull_request.title }}
jobs: jobs:
reflex-web: reflex-web:
# if: github.event.pull_request.merged == true # if: github.event.pull_request.merged == true
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
# Show OS combos first in GUI # Show OS combos first in GUI
os: [ubuntu-latest] os: [ubuntu-latest]
python-version: ["3.12.8"] python-version: ['3.11.4']
node-version: ["18.x"] node-version: ['18.x']
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
@ -70,8 +70,66 @@ jobs:
env: env:
GITHUB_SHA: ${{ github.sha }} GITHUB_SHA: ${{ github.sha }}
reflex-dist-size: # This job is used to calculate the size of the Reflex distribution (wheel file) simple-apps-benchmarks: # This app tests the compile times of various compoonents and pages
if: github.event.pull_request.merged == true if: github.event.pull_request.merged == true
env:
OUTPUT_FILE: benchmarks.json
timeout-minutes: 50
strategy:
# Prioritize getting more information out of the workflow (even if something fails)
fail-fast: false
matrix:
# Show OS combos first in GUI
os: [ubuntu-latest, windows-latest, macos-12]
python-version: ['3.8.18', '3.9.18', '3.10.13', '3.11.5', '3.12.0']
exclude:
- os: windows-latest
python-version: '3.10.13'
- os: windows-latest
python-version: '3.9.18'
- os: windows-latest
python-version: '3.8.18'
# keep only one python version for MacOS
- os: macos-latest
python-version: '3.8.18'
- os: macos-latest
python-version: '3.9.18'
- os: macos-latest
python-version: '3.10.13'
- os: macos-12
python-version: '3.12.0'
include:
- os: windows-latest
python-version: '3.10.11'
- os: windows-latest
python-version: '3.9.13'
- os: windows-latest
python-version: '3.8.10'
runs-on: ${{ matrix.os }}
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
- name: Run benchmark tests
env:
APP_HARNESS_HEADLESS: 1
PYTHONUNBUFFERED: 1
run: |
poetry run pytest -v benchmarks/ --benchmark-json=${{ env.OUTPUT_FILE }} -s
- name: Upload benchmark results
# Only run if the database creds are available in this context.
run:
poetry run python benchmarks/benchmark_compile_times.py --os "${{ matrix.os }}"
--python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
--benchmark-json "${{ env.OUTPUT_FILE }}" --branch-name "${{ github.head_ref || github.ref_name }}"
--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
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)
@ -81,7 +139,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.12.8 python-version: 3.11.5
run-poetry-install: true run-poetry-install: true
create-venv-at-path: .venv create-venv-at-path: .venv
- name: Build reflex - name: Build reflex
@ -91,29 +149,25 @@ 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.12.8 --commit-sha "${{ github.sha }}" --pr-id "${{ github.event.pull_request.id }}" --python-version 3.11.5 --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)
fail-fast: false fail-fast: false
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-12]
python-version: ["3.12.8"] python-version: ['3.11.5']
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up python
id: setup-python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install Poetry - name: Install Poetry
uses: snok/install-poetry@v1 uses: snok/install-poetry@v1
with: with:
@ -138,6 +192,6 @@ jobs:
run: run:
poetry run python benchmarks/benchmark_package_size.py --os "${{ matrix.os }}" poetry run python benchmarks/benchmark_package_size.py --os "${{ matrix.os }}"
--python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
--pr-id "${{ github.event.pull_request.id }}" --pr-id "${{ github.event.pull_request.id }}"
--branch-name "${{ github.head_ref || github.ref_name }}" --branch-name "${{ github.head_ref || github.ref_name }}"
--path ./.venv --path ./.venv

View File

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

View File

@ -1,40 +0,0 @@
name: integration-node-latest
on:
push:
branches:
- main
pull_request:
branches:
- main
env:
TELEMETRY_ENABLED: false
REFLEX_USE_SYSTEM_NODE: true
jobs:
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}}

View File

@ -1,86 +0,0 @@
name: check-outdated-dependencies
on:
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.
jobs:
backend:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- uses: ./.github/actions/setup_build_env
with:
python-version: '3.10'
run-poetry-install: true
create-venv-at-path: .venv
- 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
frontend:
runs-on: ubuntu-latest
steps:
- 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 $(grep -ivE "reflex " requirements.txt)
- name: Install additional dependencies for DB access
run: poetry run uv pip install psycopg
- name: Init Website for reflex-web
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|ag-grid' || 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

View File

@ -1,103 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
schedule:
- cron: "36 7 * * 4"
jobs:
analyze:
name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions:
# required for all workflows
security-events: write
# required to fetch internal or private CodeQL packs
packages: read
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
include:
- language: javascript-typescript
build-mode: none
- language: python
build-mode: none
- language: actions
build-mode: none
# CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds.
# - name: Setup runtime (example)
# uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
config-file: .github/codeql-config.yml
build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step
# to build your code.
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual'
shell: bash
run: |
echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \
'your code, for example:'
echo ' make bootstrap'
echo ' make release'
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View File

@ -6,13 +6,13 @@ concurrency:
on: on:
push: push:
branches: ["main"] branches: ['main']
paths-ignore: paths-ignore:
- "**/*.md" - '**/*.md'
pull_request: pull_request:
branches: ["main"] branches: ['main']
paths-ignore: paths-ignore:
- "**/*.md" - '**/*.md'
permissions: permissions:
contents: read contents: read
@ -22,11 +22,9 @@ jobs:
timeout-minutes: 30 timeout-minutes: 30
strategy: strategy:
matrix: matrix:
state_manager: ["redis", "memory"] state_manager: ['redis', 'memory']
python-version: ["3.11.11", "3.12.8", "3.13.1"] python-version: ['3.8.18', '3.11.5', '3.12.0']
split_index: [1, 2] runs-on: ubuntu-latest
fail-fast: false
runs-on: ubuntu-22.04
services: services:
# Label used to access the service container # Label used to access the service container
redis: redis:
@ -47,10 +45,16 @@ jobs:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
run-poetry-install: true run-poetry-install: true
create-venv-at-path: .venv create-venv-at-path: .venv
- run: poetry run uv pip install pyvirtualdisplay pillow pytest-split pytest-retry - run: poetry run uv pip install pyvirtualdisplay pillow
- name: Run app harness tests - name: Run app harness tests
env: env:
SCREENSHOT_DIR: /tmp/screenshots
REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }} REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }}
run: | run: |
poetry run playwright install chromium poetry run pytest integration
poetry run pytest tests/integration --retries 3 --maxfail=5 --splits 2 --group ${{matrix.split_index}} - uses: actions/upload-artifact@v4
name: Upload failed test screenshots
if: always()
with:
name: failed_test_screenshots
path: /tmp/screenshots

View File

@ -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,13 +27,13 @@ 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:
example-counter-and-nba-proxy: example-counter:
env: env:
OUTPUT_FILE: import_benchmark.json OUTPUT_FILE: import_benchmark.json
timeout-minutes: 30 timeout-minutes: 30
@ -42,18 +42,22 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
# Show OS combos first in GUI # Show OS combos first in GUI
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest, macos-12]
python-version: ['3.10.16', '3.11.11', '3.12.8', '3.13.1'] python-version: ['3.8.18', '3.9.18', '3.10.13', '3.11.5', '3.12.0']
exclude: exclude:
- os: windows-latest - os: windows-latest
python-version: "3.11.11" python-version: '3.10.13'
- os: windows-latest - os: windows-latest
python-version: '3.10.16' python-version: '3.9.18'
- os: windows-latest
python-version: '3.8.18'
include: include:
- os: windows-latest
python-version: "3.11.9"
- os: windows-latest - os: windows-latest
python-version: '3.10.11' python-version: '3.10.11'
- os: windows-latest
python-version: '3.9.13'
- os: windows-latest
python-version: '3.8.10'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
@ -73,7 +77,7 @@ jobs:
run: | run: |
poetry run uv pip install -r requirements.txt 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 psycopg2-binary
- name: Check export --backend-only before init for counter example - name: Check export --backend-only before init for counter example
working-directory: ./reflex-examples/counter working-directory: ./reflex-examples/counter
run: | run: |
@ -94,25 +98,27 @@ jobs:
# Check that npm is home # Check that npm is home
npm -v npm -v
poetry run bash scripts/integration.sh ./reflex-examples/counter dev poetry run bash scripts/integration.sh ./reflex-examples/counter dev
- name: Install requirements for nba proxy example - name: Measure and upload .web size
working-directory: ./reflex-examples/nba-proxy run:
run: | poetry run python benchmarks/benchmark_web_size.py --os "${{ matrix.os }}"
poetry run uv pip install -r requirements.txt --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
- name: Install additional dependencies for DB access --pr-id "${{ github.event.pull_request.id }}"
run: poetry run uv pip install psycopg --branch-name "${{ github.head_ref || github.ref_name }}"
- name: Check export --backend-only before init for nba-proxy example --path ./reflex-examples/counter/.web
working-directory: ./reflex-examples/nba-proxy --app-name "counter"
run: | - name: Install hyperfine
poetry run reflex export --backend-only run: cargo install hyperfine
- name: Init Website for nba-proxy example - name: Benchmark imports
working-directory: ./reflex-examples/nba-proxy working-directory: ./reflex-examples/counter
run: | run: hyperfine --warmup 3 "export POETRY_VIRTUALENVS_PATH=../../.venv; poetry run python counter/counter.py" --show-output --export-json "${{ env.OUTPUT_FILE }}" --shell bash
poetry run reflex init --loglevel debug - name: Upload Benchmarks
- name: Run Website and Check for errors run:
run: | poetry run python benchmarks/benchmark_imports.py --os "${{ matrix.os }}"
# Check that npm is home --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
npm -v --benchmark-json "./reflex-examples/counter/${{ env.OUTPUT_FILE }}"
poetry run bash scripts/integration.sh ./reflex-examples/nba-proxy dev --branch-name "${{ github.head_ref || github.ref_name }}" --pr-id "${{ github.event.pull_request.id }}"
--app-name "counter"
reflex-web: reflex-web:
@ -120,11 +126,11 @@ jobs:
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, windows-latest, macos-12]
python-version: ["3.11.11", "3.12.8"] python-version: ['3.10.11', '3.11.4']
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
@ -143,72 +149,9 @@ jobs:
- 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 $(grep -ivE "reflex " 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 psycopg2-binary
- name: Init Website for reflex-web
working-directory: ./reflex-web
run: poetry run reflex init
- name: Run Website and Check for errors
run: |
# Check that npm is home
npm -v
poetry run bash scripts/integration.sh ./reflex-web prod
rx-shout-from-template:
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup_build_env
with:
python-version: "3.11.11"
run-poetry-install: true
create-venv-at-path: .venv
- name: Create app directory
run: mkdir rx-shout-from-template
- name: Init reflex-web from template
run: poetry run reflex init --template https://github.com/masenf/rx_shout
working-directory: ./rx-shout-from-template
- name: ignore reflex pin in requirements
run: sed -i -e '/reflex==/d' requirements.txt
working-directory: ./rx-shout-from-template
- name: Install additional dependencies
run: poetry run uv pip install -r requirements.txt
working-directory: ./rx-shout-from-template
- name: Run Website and Check for errors
run: |
# 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:
# 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
- uses: ./.github/actions/setup_build_env
with:
python-version: ${{ matrix.python-version }}
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 - name: Init Website for reflex-web
working-directory: ./reflex-web working-directory: ./reflex-web
run: poetry run reflex init run: poetry run reflex init
@ -217,3 +160,9 @@ jobs:
# Check that npm is home # Check that npm is home
npm -v npm -v
poetry run bash scripts/integration.sh ./reflex-web prod poetry run bash scripts/integration.sh ./reflex-web prod
- name: Measure and upload .web size
run:
poetry run python benchmarks/benchmark_web_size.py --os "${{ matrix.os }}"
--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

View File

@ -37,8 +37,6 @@ jobs:
path: reflex-examples path: reflex-examples
- uses: Vampire/setup-wsl@v3 - uses: Vampire/setup-wsl@v3
with:
distribution: Ubuntu-24.04
- name: Install Python - name: Install Python
shell: wsl-bash {0} shell: wsl-bash {0}

View File

@ -1,34 +0,0 @@
name: performance-tests
on:
push:
branches:
- "main" # or "master"
paths-ignore:
- "**/*.md"
pull_request:
workflow_dispatch:
env:
TELEMETRY_ENABLED: false
NODE_OPTIONS: "--max_old_space_size=8192"
PR_TITLE: ${{ github.event.pull_request.title }}
APP_HARNESS_HEADLESS: 1
PYTHONUNBUFFERED: 1
jobs:
benchmarks:
name: Run benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup_build_env
with:
python-version: 3.12.8
run-poetry-install: true
create-venv-at-path: .venv
- name: Run benchmarks
uses: CodSpeedHQ/action@v3
with:
token: ${{ secrets.CODSPEED_TOKEN }}
run: poetry run pytest tests/benchmarks --codspeed

View File

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

View File

@ -28,5 +28,5 @@ jobs:
# Run reflex init in a docker container # Run reflex init in a docker container
# cwd is repo root # cwd is repo root
docker build -f tests/integration/init-test/Dockerfile -t reflex-init-test tests/integration/init-test docker build -f integration/init-test/Dockerfile -t reflex-init-test integration/init-test
docker run --rm -v $(pwd):/reflex-repo/ reflex-init-test /reflex-repo/tests/integration/init-test/in_docker_test_script.sh docker run --rm -v $(pwd):/reflex-repo/ reflex-init-test /reflex-repo/integration/init-test/in_docker_test_script.sh

View File

@ -6,13 +6,13 @@ concurrency:
on: on:
push: push:
branches: ["main"] branches: ['main']
paths-ignore: paths-ignore:
- "**/*.md" - '**/*.md'
pull_request: pull_request:
branches: ["main"] branches: ['main']
paths-ignore: paths-ignore:
- "**/*.md" - '**/*.md'
permissions: permissions:
contents: read contents: read
@ -27,21 +27,24 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, windows-latest] os: [ubuntu-latest, windows-latest, macos-12]
python-version: ["3.10.16", "3.11.11", "3.12.8", "3.13.1"] python-version: ['3.8.18', '3.9.18', '3.10.13', '3.11.5', '3.12.0']
# 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.11.11" python-version: '3.10.13'
- os: windows-latest - os: windows-latest
python-version: "3.10.16" python-version: '3.9.18'
- os: windows-latest
python-version: '3.8.18'
include: include:
- os: windows-latest - os: windows-latest
python-version: "3.11.9" python-version: '3.10.11'
- os: windows-latest - os: windows-latest
python-version: "3.10.11" python-version: '3.9.13'
- os: windows-latest
python-version: '3.8.10'
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
# Service containers to run with `runner-job` # Service containers to run with `runner-job`
services: services:
# Label used to access the service container # Label used to access the service container
@ -66,44 +69,17 @@ jobs:
- name: Run unit tests - name: Run unit tests
run: | run: |
export PYTHONUNBUFFERED=1 export PYTHONUNBUFFERED=1
poetry run pytest tests/units --cov --no-cov-on-fail --cov-report= poetry run pytest tests --cov --no-cov-on-fail --cov-report=
- name: Run unit tests w/ redis - name: Run unit tests w/ redis
if: ${{ matrix.os == 'ubuntu-latest' }} if: ${{ matrix.os == 'ubuntu-latest' }}
run: | run: |
export PYTHONUNBUFFERED=1 export PYTHONUNBUFFERED=1
export REDIS_URL=redis://localhost:6379 export REDIS_URL=redis://localhost:6379
poetry run pytest tests/units --cov --no-cov-on-fail --cov-report= poetry run pytest tests --cov --no-cov-on-fail --cov-report=
# Change to explicitly install v1 when reflex-hosting-cli is compatible with v2 # Change to explicitly install v1 when reflex-hosting-cli is compatible with v2
- name: Run unit tests w/ pydantic v1 - name: Run unit tests w/ pydantic v1
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 --cov --no-cov-on-fail --cov-report=
- name: Generate coverage report - run: poetry run coverage html
run: poetry run coverage html
unit-tests-macos:
timeout-minutes: 30
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
strategy:
fail-fast: false
matrix:
# Note: py310, py311 versions chosen due to available arm64 darwin builds.
python-version: ["3.10.11", "3.11.9", "3.12.8", "3.13.1"]
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup_build_env
with:
python-version: ${{ matrix.python-version }}
run-poetry-install: true
create-venv-at-path: .venv
- name: Run unit tests
run: |
export PYTHONUNBUFFERED=1
poetry run pytest tests/units --cov --no-cov-on-fail --cov-report=
- name: Run unit tests w/ pydantic v1
run: |
export PYTHONUNBUFFERED=1
poetry run uv pip install "pydantic~=1.10"
poetry run pytest tests/units --cov --no-cov-on-fail --cov-report=

2
.gitignore vendored
View File

@ -4,7 +4,6 @@ assets/external/*
dist/* dist/*
examples/ examples/
.web .web
.states
.idea .idea
.vscode .vscode
.coverage .coverage
@ -15,4 +14,3 @@ requirements.txt
.pyi_generator_last_run .pyi_generator_last_run
.pyi_generator_diff .pyi_generator_diff
reflex.db reflex.db
.codspeed

View File

@ -3,20 +3,14 @@ fail_fast: true
repos: repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.9.6 rev: v0.4.10
hooks: hooks:
- id: ruff-format - id: ruff-format
args: [reflex, tests] args: [integration, reflex, tests]
- id: ruff - id: ruff
args: ["--fix", "--exit-non-zero-on-fix"] args: ["--fix", "--exit-non-zero-on-fix"]
exclude: '^integration/benchmarks/' exclude: '^integration/benchmarks/'
- repo: https://github.com/codespell-project/codespell
rev: v2.3.0
hooks:
- id: codespell
args: ["reflex"]
# Run pyi check before pyright because pyright can fail if pyi files are wrong. # Run pyi check before pyright because pyright can fail if pyi files are wrong.
- repo: local - repo: local
hooks: hooks:
@ -24,15 +18,14 @@ repos:
name: update-pyi-files name: update-pyi-files
always_run: true always_run: true
language: system language: system
require_serial: true
description: 'Update pyi files as needed' description: 'Update pyi files as needed'
entry: python3 scripts/make_pyi.py entry: python3 scripts/make_pyi.py
- repo: https://github.com/RobertCraigie/pyright-python - repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.393 rev: v1.1.313
hooks: hooks:
- id: pyright - id: pyright
args: [reflex, tests] args: [integration, reflex, tests]
language: system language: system
- repo: https://github.com/terrencepreilly/darglint - repo: https://github.com/terrencepreilly/darglint

View File

@ -5,7 +5,7 @@
We as members, contributors, and leaders pledge to make participation in our We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socioeconomic status, identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity nationality, personal appearance, race, religion, or sexual identity
and orientation. and orientation.

View File

@ -8,7 +8,7 @@ Here is a quick guide on how to run Reflex repo locally so you can start contrib
**Prerequisites:** **Prerequisites:**
- Python >= 3.10 - Python >= 3.8
- Poetry version >= 1.4.0 and add it to your path (see [Poetry Docs](https://python-poetry.org/docs/#installation) for more info). - Poetry version >= 1.4.0 and add it to your path (see [Poetry Docs](https://python-poetry.org/docs/#installation) for more info).
**1. Fork this repository:** **1. Fork this repository:**
@ -69,7 +69,7 @@ In your `reflex` directory run make sure all the unit tests are still passing us
This will fail if code coverage is below 70%. This will fail if code coverage is below 70%.
``` bash ``` bash
poetry run pytest tests/units --cov --no-cov-on-fail --cov-report= poetry run pytest tests --cov --no-cov-on-fail --cov-report=
``` ```
Next make sure all the following tests pass. This ensures that every new change has proper documentation and type checking. Next make sure all the following tests pass. This ensures that every new change has proper documentation and type checking.
@ -87,7 +87,7 @@ poetry run ruff format .
``` ```
Consider installing git pre-commit hooks so Ruff, Pyright, Darglint and `make_pyi` will run automatically before each commit. Consider installing git pre-commit hooks so Ruff, Pyright, Darglint and `make_pyi` will run automatically before each commit.
Note that pre-commit will only be installed when you use a Python version >= 3.10. Note that pre-commit will only be installed when you use a Python version >= 3.8.
``` bash ``` bash
pre-commit install pre-commit install

View File

@ -10,6 +10,7 @@
### **✨ Performant, customizable web apps in pure Python. Deploy in seconds. ✨** ### **✨ Performant, customizable web apps in pure Python. Deploy in seconds. ✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -17,7 +18,7 @@
--- ---
[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md) [English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md)
--- ---
@ -34,7 +35,7 @@ See our [architecture page](https://reflex.dev/blog/2024-03-21-reflex-architectu
## ⚙️ Installation ## ⚙️ Installation
Open a terminal and run (Requires Python 3.10+): Open a terminal and run (Requires Python 3.8+):
```bash ```bash
pip install reflex pip install reflex
@ -228,7 +229,7 @@ You can create a multi-page app by adding more pages.
<div align="center"> <div align="center">
📑 [Docs](https://reflex.dev/docs/getting-started/introduction) &nbsp; | &nbsp; 🗞️ [Blog](https://reflex.dev/blog) &nbsp; | &nbsp; 📱 [Component Library](https://reflex.dev/docs/library) &nbsp; | &nbsp; 🖼️ [Templates](https://reflex.dev/templates/) &nbsp; | &nbsp; 🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start) &nbsp; 📑 [Docs](https://reflex.dev/docs/getting-started/introduction) &nbsp; | &nbsp; 🗞️ [Blog](https://reflex.dev/blog) &nbsp; | &nbsp; 📱 [Component Library](https://reflex.dev/docs/library) &nbsp; | &nbsp; 🖼️ [Gallery](https://reflex.dev/docs/gallery) &nbsp; | &nbsp; 🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start) &nbsp;
</div> </div>
@ -249,7 +250,7 @@ We welcome contributions of any size! Below are some good ways to get started in
- **GitHub Discussions**: A great way to talk about features you want added or things that are confusing/need clarification. - **GitHub Discussions**: A great way to talk about features you want added or things that are confusing/need clarification.
- **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues) are an excellent way to report bugs. Additionally, you can try and solve an existing issue and submit a PR. - **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues) are an excellent way to report bugs. Additionally, you can try and solve an existing issue and submit a PR.
We are actively looking for contributors, no matter your skill level or experience. To contribute check out [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) We are actively looking for contributors, no matter your skill level or experience. To contribute check out [CONTIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)
## All Thanks To Our Contributors: ## All Thanks To Our Contributors:

View File

@ -5,7 +5,6 @@ from __future__ import annotations
import argparse import argparse
import json import json
import os import os
from pathlib import Path
from utils import send_data_to_posthog from utils import send_data_to_posthog
@ -19,7 +18,7 @@ def extract_stats_from_json(json_file: str) -> list[dict]:
Returns: Returns:
list[dict]: The stats for each test. list[dict]: The stats for each test.
""" """
with Path(json_file).open() as file: with open(json_file, "r") as file:
json_data = json.load(file) json_data = json.load(file)
# Load the JSON data if it is a string, otherwise assume it's already a dictionary # Load the JSON data if it is a string, otherwise assume it's already a dictionary

View File

@ -5,7 +5,6 @@ from __future__ import annotations
import argparse import argparse
import json import json
import os import os
from pathlib import Path
from utils import send_data_to_posthog from utils import send_data_to_posthog
@ -19,7 +18,7 @@ def extract_stats_from_json(json_file: str) -> dict:
Returns: Returns:
dict: The stats for each test. dict: The stats for each test.
""" """
with Path(json_file).open() as file: with open(json_file, "r") as file:
json_data = json.load(file) json_data = json.load(file)
# Load the JSON data if it is a string, otherwise assume it's already a dictionary # Load the JSON data if it is a string, otherwise assume it's already a dictionary

View File

@ -3,8 +3,8 @@
from __future__ import annotations from __future__ import annotations
import json import json
import os
import sys import sys
from pathlib import Path
from utils import send_data_to_posthog from utils import send_data_to_posthog
@ -28,7 +28,7 @@ def insert_benchmarking_data(
send_data_to_posthog("lighthouse_benchmark", properties) send_data_to_posthog("lighthouse_benchmark", properties)
def get_lighthouse_scores(directory_path: str | Path) -> dict: def get_lighthouse_scores(directory_path: str) -> dict:
"""Extracts the Lighthouse scores from the JSON files in the specified directory. """Extracts the Lighthouse scores from the JSON files in the specified directory.
Args: Args:
@ -38,20 +38,24 @@ def get_lighthouse_scores(directory_path: str | Path) -> dict:
dict: The Lighthouse scores. dict: The Lighthouse scores.
""" """
scores = {} scores = {}
directory_path = Path(directory_path)
try: try:
for filename in directory_path.iterdir(): for filename in os.listdir(directory_path):
if filename.suffix == ".json" and filename.stem != "manifest": if filename.endswith(".json") and filename != "manifest.json":
data = json.loads(filename.read_text()) file_path = os.path.join(directory_path, filename)
# Extract scores and add them to the dictionary with the filename as key with open(file_path, "r") as file:
scores[data["finalUrl"].replace("http://localhost:3000/", "/")] = { data = json.load(file)
"performance_score": data["categories"]["performance"]["score"], # Extract scores and add them to the dictionary with the filename as key
"accessibility_score": data["categories"]["accessibility"]["score"], scores[data["finalUrl"].replace("http://localhost:3000/", "/")] = {
"best_practices_score": data["categories"]["best-practices"][ "performance_score": data["categories"]["performance"]["score"],
"score" "accessibility_score": data["categories"]["accessibility"][
], "score"
"seo_score": data["categories"]["seo"]["score"], ],
} "best_practices_score": data["categories"]["best-practices"][
"score"
],
"seo_score": data["categories"]["seo"]["score"],
}
except Exception as e: except Exception as e:
return {"error": e} return {"error": e}

View File

@ -2,12 +2,11 @@
import argparse import argparse
import os import os
from pathlib import Path
from utils import get_directory_size, get_python_version, send_data_to_posthog from utils import get_directory_size, get_python_version, send_data_to_posthog
def get_package_size(venv_path: Path, os_name): def get_package_size(venv_path, os_name):
"""Get the size of a specified package. """Get the size of a specified package.
Args: Args:
@ -27,12 +26,14 @@ def get_package_size(venv_path: Path, os_name):
is_windows = "windows" in os_name is_windows = "windows" in os_name
package_dir: Path = ( full_path = (
venv_path / "lib" / f"python{python_version}" / "site-packages" ["lib", f"python{python_version}", "site-packages"]
if not is_windows if not is_windows
else venv_path / "Lib" / "site-packages" else ["Lib", "site-packages"]
) )
if not package_dir.exists():
package_dir = os.path.join(venv_path, *full_path)
if not os.path.exists(package_dir):
raise ValueError( raise ValueError(
"Error: Virtual environment does not exist or is not activated." "Error: Virtual environment does not exist or is not activated."
) )
@ -62,9 +63,9 @@ def insert_benchmarking_data(
path: The path to the dir or file to check size. path: The path to the dir or file to check size.
""" """
if "./dist" in path: if "./dist" in path:
size = get_directory_size(Path(path)) size = get_directory_size(path)
else: else:
size = get_package_size(Path(path), os_type_version) size = get_package_size(path, os_type_version)
# Prepare the event data # Prepare the event data
properties = { properties = {

View File

@ -2,7 +2,6 @@
import argparse import argparse
import os import os
from pathlib import Path
from utils import get_directory_size, send_data_to_posthog from utils import get_directory_size, send_data_to_posthog
@ -29,7 +28,7 @@ def insert_benchmarking_data(
pr_id: The id of the PR. pr_id: The id of the PR.
path: The path to the dir or file to check size. path: The path to the dir or file to check size.
""" """
size = get_directory_size(Path(path)) size = get_directory_size(path)
# Prepare the event data # Prepare the event data
properties = { properties = {

View File

@ -0,0 +1,376 @@
"""Benchmark tests for apps with varying component numbers."""
from __future__ import annotations
import functools
import time
from typing import Generator
import pytest
from benchmarks import WINDOWS_SKIP_REASON
from reflex import constants
from reflex.compiler import utils
from reflex.testing import AppHarness, chdir
from reflex.utils import build
from reflex.utils.prerequisites import get_web_dir
web_pages = get_web_dir() / constants.Dirs.PAGES
def render_component(num: int):
"""Generate a number of components based on num.
Args:
num: number of components to produce.
Returns:
The rendered number of components.
"""
import reflex as rx
return [
rx.fragment(
rx.box(
rx.accordion.root(
rx.accordion.item(
header="Full Ingredients", # type: ignore
content="Yes. It's built with accessibility in mind.", # type: ignore
font_size="3em",
),
rx.accordion.item(
header="Applications", # type: ignore
content="Yes. It's unstyled by default, giving you freedom over the look and feel.", # type: ignore
),
collapsible=True,
variant="ghost",
width="25rem",
),
padding_top="20px",
),
rx.box(
rx.drawer.root(
rx.drawer.trigger(
rx.button("Open Drawer with snap points"), as_child=True
),
rx.drawer.overlay(),
rx.drawer.portal(
rx.drawer.content(
rx.flex(
rx.drawer.title("Drawer Content"),
rx.drawer.description("Drawer description"),
rx.drawer.close(
rx.button("Close Button"),
as_child=True,
),
direction="column",
margin="5em",
align_items="center",
),
top="auto",
height="100%",
flex_direction="column",
background_color="var(--green-3)",
),
),
snap_points=["148px", "355px", 1],
),
),
rx.box(
rx.callout(
"You will need admin privileges to install and access this application.",
icon="info",
size="3",
),
),
rx.box(
rx.table.root(
rx.table.header(
rx.table.row(
rx.table.column_header_cell("Full name"),
rx.table.column_header_cell("Email"),
rx.table.column_header_cell("Group"),
),
),
rx.table.body(
rx.table.row(
rx.table.row_header_cell("Danilo Sousa"),
rx.table.cell("danilo@example.com"),
rx.table.cell("Developer"),
),
rx.table.row(
rx.table.row_header_cell("Zahra Ambessa"),
rx.table.cell("zahra@example.com"),
rx.table.cell("Admin"),
),
rx.table.row(
rx.table.row_header_cell("Jasper Eriksson"),
rx.table.cell("jasper@example.com"),
rx.table.cell("Developer"),
),
),
)
),
)
] * num
def AppWithTenComponentsOnePage():
"""A reflex app with roughly 10 components on one page."""
import reflex as rx
def index() -> rx.Component:
return rx.center(rx.vstack(*render_component(1)))
app = rx.App(state=rx.State)
app.add_page(index)
def AppWithHundredComponentOnePage():
"""A reflex app with roughly 100 components on one page."""
import reflex as rx
def index() -> rx.Component:
return rx.center(rx.vstack(*render_component(100)))
app = rx.App(state=rx.State)
app.add_page(index)
def AppWithThousandComponentsOnePage():
"""A reflex app with roughly 1000 components on one page."""
import reflex as rx
def index() -> rx.Component:
return rx.center(rx.vstack(*render_component(1000)))
app = rx.App(state=rx.State)
app.add_page(index)
@pytest.fixture(scope="session")
def app_with_10_components(
tmp_path_factory,
) -> Generator[AppHarness, None, None]:
"""Start Blank Template app at tmp_path via AppHarness.
Args:
tmp_path_factory: pytest tmp_path_factory fixture
Yields:
running AppHarness instance
"""
root = tmp_path_factory.mktemp("app10components")
yield AppHarness.create(
root=root,
app_source=functools.partial(
AppWithTenComponentsOnePage,
render_component=render_component, # type: ignore
),
) # type: ignore
@pytest.fixture(scope="session")
def app_with_100_components(
tmp_path_factory,
) -> Generator[AppHarness, None, None]:
"""Start Blank Template app at tmp_path via AppHarness.
Args:
tmp_path_factory: pytest tmp_path_factory fixture
Yields:
running AppHarness instance
"""
root = tmp_path_factory.mktemp("app100components")
yield AppHarness.create(
root=root,
app_source=functools.partial(
AppWithHundredComponentOnePage,
render_component=render_component, # type: ignore
),
) # type: ignore
@pytest.fixture(scope="session")
def app_with_1000_components(
tmp_path_factory,
) -> Generator[AppHarness, None, None]:
"""Create an app with 1000 components at tmp_path via AppHarness.
Args:
tmp_path_factory: pytest tmp_path_factory fixture
Yields:
an AppHarness instance
"""
root = tmp_path_factory.mktemp("app1000components")
yield AppHarness.create(
root=root,
app_source=functools.partial(
AppWithThousandComponentsOnePage,
render_component=render_component, # type: ignore
),
) # type: ignore
@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON)
@pytest.mark.benchmark(
group="Compile time of varying component numbers",
timer=time.perf_counter,
disable_gc=True,
warmup=False,
)
def test_app_10_compile_time_cold(benchmark, app_with_10_components):
"""Test the compile time on a cold start for an app with roughly 10 components.
Args:
benchmark: The benchmark fixture.
app_with_10_components: The app harness.
"""
def setup():
with chdir(app_with_10_components.app_path):
utils.empty_dir(web_pages, ["_app.js"])
app_with_10_components._initialize_app()
build.setup_frontend(app_with_10_components.app_path)
def benchmark_fn():
with chdir(app_with_10_components.app_path):
app_with_10_components.app_instance._compile()
benchmark.pedantic(benchmark_fn, setup=setup, rounds=10)
@pytest.mark.benchmark(
group="Compile time of varying component numbers",
min_rounds=5,
timer=time.perf_counter,
disable_gc=True,
warmup=False,
)
def test_app_10_compile_time_warm(benchmark, app_with_10_components):
"""Test the compile time on a warm start for an app with roughly 10 components.
Args:
benchmark: The benchmark fixture.
app_with_10_components: The app harness.
"""
with chdir(app_with_10_components.app_path):
app_with_10_components._initialize_app()
build.setup_frontend(app_with_10_components.app_path)
def benchmark_fn():
with chdir(app_with_10_components.app_path):
app_with_10_components.app_instance._compile()
benchmark(benchmark_fn)
@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON)
@pytest.mark.benchmark(
group="Compile time of varying component numbers",
timer=time.perf_counter,
disable_gc=True,
warmup=False,
)
def test_app_100_compile_time_cold(benchmark, app_with_100_components):
"""Test the compile time on a cold start for an app with roughly 100 components.
Args:
benchmark: The benchmark fixture.
app_with_100_components: The app harness.
"""
def setup():
with chdir(app_with_100_components.app_path):
utils.empty_dir(web_pages, ["_app.js"])
app_with_100_components._initialize_app()
build.setup_frontend(app_with_100_components.app_path)
def benchmark_fn():
with chdir(app_with_100_components.app_path):
app_with_100_components.app_instance._compile()
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
@pytest.mark.benchmark(
group="Compile time of varying component numbers",
min_rounds=5,
timer=time.perf_counter,
disable_gc=True,
warmup=False,
)
def test_app_100_compile_time_warm(benchmark, app_with_100_components):
"""Test the compile time on a warm start for an app with roughly 100 components.
Args:
benchmark: The benchmark fixture.
app_with_100_components: The app harness.
"""
with chdir(app_with_100_components.app_path):
app_with_100_components._initialize_app()
build.setup_frontend(app_with_100_components.app_path)
def benchmark_fn():
with chdir(app_with_100_components.app_path):
app_with_100_components.app_instance._compile()
benchmark(benchmark_fn)
@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON)
@pytest.mark.benchmark(
group="Compile time of varying component numbers",
timer=time.perf_counter,
disable_gc=True,
warmup=False,
)
def test_app_1000_compile_time_cold(benchmark, app_with_1000_components):
"""Test the compile time on a cold start for an app with roughly 1000 components.
Args:
benchmark: The benchmark fixture.
app_with_1000_components: The app harness.
"""
def setup():
with chdir(app_with_1000_components.app_path):
utils.empty_dir(web_pages, keep_files=["_app.js"])
app_with_1000_components._initialize_app()
build.setup_frontend(app_with_1000_components.app_path)
def benchmark_fn():
with chdir(app_with_1000_components.app_path):
app_with_1000_components.app_instance._compile()
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
@pytest.mark.benchmark(
group="Compile time of varying component numbers",
min_rounds=5,
timer=time.perf_counter,
disable_gc=True,
warmup=False,
)
def test_app_1000_compile_time_warm(benchmark, app_with_1000_components):
"""Test the compile time on a warm start for an app with roughly 1000 components.
Args:
benchmark: The benchmark fixture.
app_with_1000_components: The app harness.
"""
with chdir(app_with_1000_components.app_path):
app_with_1000_components._initialize_app()
build.setup_frontend(app_with_1000_components.app_path)
def benchmark_fn():
with chdir(app_with_1000_components.app_path):
app_with_1000_components.app_instance._compile()
benchmark(benchmark_fn)

View File

@ -0,0 +1,579 @@
"""Benchmark tests for apps with varying page numbers."""
from __future__ import annotations
import functools
import time
from typing import Generator
import pytest
from benchmarks import WINDOWS_SKIP_REASON
from reflex import constants
from reflex.compiler import utils
from reflex.testing import AppHarness, chdir
from reflex.utils import build
from reflex.utils.prerequisites import get_web_dir
web_pages = get_web_dir() / constants.Dirs.PAGES
def render_multiple_pages(app, num: int):
"""Add multiple pages based on num.
Args:
app: The App object.
num: number of pages to render.
"""
from typing import Tuple
from rxconfig import config # type: ignore
import reflex as rx
docs_url = "https://reflex.dev/docs/getting-started/introduction/"
filename = f"{config.app_name}/{config.app_name}.py"
college = [
"Stanford University",
"Arizona",
"Arizona state",
"Baylor",
"Boston College",
"Boston University",
]
class State(rx.State):
"""The app state."""
position: str
college: str
age: Tuple[int, int] = (18, 50)
salary: Tuple[int, int] = (0, 25000000)
comp1 = rx.center(
rx.theme_panel(),
rx.vstack(
rx.heading("Welcome to Reflex!", size="9"),
rx.text("Get started by editing ", rx.code(filename)),
rx.button(
"Check out our docs!",
on_click=lambda: rx.redirect(docs_url),
size="4",
),
align="center",
spacing="7",
font_size="2em",
),
height="100vh",
)
comp2 = rx.vstack(
rx.hstack(
rx.vstack(
rx.select(
["C", "PF", "SF", "PG", "SG"],
placeholder="Select a position. (All)",
on_change=State.set_position, # type: ignore
size="3",
),
rx.select(
college,
placeholder="Select a college. (All)",
on_change=State.set_college, # type: ignore
size="3",
),
),
rx.vstack(
rx.vstack(
rx.hstack(
rx.badge("Min Age: ", State.age[0]),
rx.divider(orientation="vertical"),
rx.badge("Max Age: ", State.age[1]),
),
rx.slider(
default_value=[18, 50],
min=18,
max=50,
on_value_commit=State.set_age, # type: ignore
),
align_items="left",
width="100%",
),
rx.vstack(
rx.hstack(
rx.badge("Min Sal: ", State.salary[0] // 1000000, "M"),
rx.divider(orientation="vertical"),
rx.badge("Max Sal: ", State.salary[1] // 1000000, "M"),
),
rx.slider(
default_value=[0, 25000000],
min=0,
max=25000000,
on_value_commit=State.set_salary, # type: ignore
),
align_items="left",
width="100%",
),
),
spacing="4",
),
width="100%",
)
for i in range(1, num + 1):
if i % 2 == 1:
app.add_page(comp1, route=f"page{i}")
else:
app.add_page(comp2, route=f"page{i}")
def AppWithOnePage():
"""A reflex app with one page."""
from rxconfig import config # type: ignore
import reflex as rx
docs_url = "https://reflex.dev/docs/getting-started/introduction/"
filename = f"{config.app_name}/{config.app_name}.py"
class State(rx.State):
"""The app state."""
pass
def index() -> rx.Component:
return rx.center(
rx.input(
id="token", value=State.router.session.client_token, is_read_only=True
),
rx.vstack(
rx.heading("Welcome to Reflex!", size="9"),
rx.text("Get started by editing ", rx.code(filename)),
rx.button(
"Check out our docs!",
on_click=lambda: rx.redirect(docs_url),
size="4",
),
align="center",
spacing="7",
font_size="2em",
),
height="100vh",
)
app = rx.App(state=rx.State)
app.add_page(index)
def AppWithTenPages():
"""A reflex app with 10 pages."""
import reflex as rx
app = rx.App(state=rx.State)
render_multiple_pages(app, 10)
def AppWithHundredPages():
"""A reflex app with 100 pages."""
import reflex as rx
app = rx.App(state=rx.State)
render_multiple_pages(app, 100)
def AppWithThousandPages():
"""A reflex app with Thousand pages."""
import reflex as rx
app = rx.App(state=rx.State)
render_multiple_pages(app, 1000)
def AppWithTenThousandPages():
"""A reflex app with ten thousand pages."""
import reflex as rx
app = rx.App(state=rx.State)
render_multiple_pages(app, 10000)
@pytest.fixture(scope="session")
def app_with_one_page(
tmp_path_factory,
) -> Generator[AppHarness, None, None]:
"""Create an app with 10000 pages at tmp_path via AppHarness.
Args:
tmp_path_factory: pytest tmp_path_factory fixture
Yields:
an AppHarness instance
"""
root = tmp_path_factory.mktemp(f"app1")
yield AppHarness.create(root=root, app_source=AppWithOnePage) # type: ignore
@pytest.fixture(scope="session")
def app_with_ten_pages(
tmp_path_factory,
) -> Generator[AppHarness, None, None]:
"""Create an app with 10 pages at tmp_path via AppHarness.
Args:
tmp_path_factory: pytest tmp_path_factory fixture
Yields:
an AppHarness instance
"""
root = tmp_path_factory.mktemp(f"app10")
yield AppHarness.create(
root=root,
app_source=functools.partial(
AppWithTenPages,
render_comp=render_multiple_pages, # type: ignore
),
)
@pytest.fixture(scope="session")
def app_with_hundred_pages(
tmp_path_factory,
) -> Generator[AppHarness, None, None]:
"""Create an app with 100 pages at tmp_path via AppHarness.
Args:
tmp_path_factory: pytest tmp_path_factory fixture
Yields:
an AppHarness instance
"""
root = tmp_path_factory.mktemp(f"app100")
yield AppHarness.create(
root=root,
app_source=functools.partial(
AppWithHundredPages,
render_comp=render_multiple_pages, # type: ignore
),
) # type: ignore
@pytest.fixture(scope="session")
def app_with_thousand_pages(
tmp_path_factory,
) -> Generator[AppHarness, None, None]:
"""Create an app with 1000 pages at tmp_path via AppHarness.
Args:
tmp_path_factory: pytest tmp_path_factory fixture
Yields:
an AppHarness instance
"""
root = tmp_path_factory.mktemp(f"app1000")
yield AppHarness.create(
root=root,
app_source=functools.partial( # type: ignore
AppWithThousandPages,
render_comp=render_multiple_pages, # type: ignore
),
) # type: ignore
@pytest.fixture(scope="session")
def app_with_ten_thousand_pages(
tmp_path_factory,
) -> Generator[AppHarness, None, None]:
"""Create an app with 10000 pages at tmp_path via AppHarness.
Args:
tmp_path_factory: pytest tmp_path_factory fixture
Yields:
running AppHarness instance
"""
root = tmp_path_factory.mktemp(f"app10000")
yield AppHarness.create(
root=root,
app_source=functools.partial(
AppWithTenThousandPages,
render_comp=render_multiple_pages, # type: ignore
),
) # type: ignore
@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON)
@pytest.mark.benchmark(
group="Compile time of varying page numbers",
timer=time.perf_counter,
disable_gc=True,
warmup=False,
)
def test_app_1_compile_time_cold(benchmark, app_with_one_page):
"""Test the compile time on a cold start for an app with 1 page.
Args:
benchmark: The benchmark fixture.
app_with_one_page: The app harness.
"""
def setup():
with chdir(app_with_one_page.app_path):
utils.empty_dir(web_pages, keep_files=["_app.js"])
app_with_one_page._initialize_app()
build.setup_frontend(app_with_one_page.app_path)
def benchmark_fn():
with chdir(app_with_one_page.app_path):
app_with_one_page.app_instance._compile()
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
app_with_one_page._reload_state_module()
@pytest.mark.benchmark(
group="Compile time of varying page numbers",
min_rounds=5,
timer=time.perf_counter,
disable_gc=True,
warmup=False,
)
def test_app_1_compile_time_warm(benchmark, app_with_one_page):
"""Test the compile time on a warm start for an app with 1 page.
Args:
benchmark: The benchmark fixture.
app_with_one_page: The app harness.
"""
with chdir(app_with_one_page.app_path):
app_with_one_page._initialize_app()
build.setup_frontend(app_with_one_page.app_path)
def benchmark_fn():
with chdir(app_with_one_page.app_path):
app_with_one_page.app_instance._compile()
benchmark(benchmark_fn)
app_with_one_page._reload_state_module()
@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON)
@pytest.mark.benchmark(
group="Compile time of varying page numbers",
timer=time.perf_counter,
disable_gc=True,
warmup=False,
)
def test_app_10_compile_time_cold(benchmark, app_with_ten_pages):
"""Test the compile time on a cold start for an app with 10 page.
Args:
benchmark: The benchmark fixture.
app_with_ten_pages: The app harness.
"""
def setup():
with chdir(app_with_ten_pages.app_path):
utils.empty_dir(web_pages, keep_files=["_app.js"])
app_with_ten_pages._initialize_app()
build.setup_frontend(app_with_ten_pages.app_path)
def benchmark_fn():
with chdir(app_with_ten_pages.app_path):
app_with_ten_pages.app_instance._compile()
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
app_with_ten_pages._reload_state_module()
@pytest.mark.benchmark(
group="Compile time of varying page numbers",
min_rounds=5,
timer=time.perf_counter,
disable_gc=True,
warmup=False,
)
def test_app_10_compile_time_warm(benchmark, app_with_ten_pages):
"""Test the compile time on a warm start for an app with 10 page.
Args:
benchmark: The benchmark fixture.
app_with_ten_pages: The app harness.
"""
with chdir(app_with_ten_pages.app_path):
app_with_ten_pages._initialize_app()
build.setup_frontend(app_with_ten_pages.app_path)
def benchmark_fn():
with chdir(app_with_ten_pages.app_path):
app_with_ten_pages.app_instance._compile()
benchmark(benchmark_fn)
app_with_ten_pages._reload_state_module()
@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON)
@pytest.mark.benchmark(
group="Compile time of varying page numbers",
timer=time.perf_counter,
disable_gc=True,
warmup=False,
)
def test_app_100_compile_time_cold(benchmark, app_with_hundred_pages):
"""Test the compile time on a cold start for an app with 100 page.
Args:
benchmark: The benchmark fixture.
app_with_hundred_pages: The app harness.
"""
def setup():
with chdir(app_with_hundred_pages.app_path):
utils.empty_dir(web_pages, keep_files=["_app.js"])
app_with_hundred_pages._initialize_app()
build.setup_frontend(app_with_hundred_pages.app_path)
def benchmark_fn():
with chdir(app_with_hundred_pages.app_path):
app_with_hundred_pages.app_instance._compile()
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
app_with_hundred_pages._reload_state_module()
@pytest.mark.benchmark(
group="Compile time of varying page numbers",
min_rounds=5,
timer=time.perf_counter,
disable_gc=True,
warmup=False,
)
def test_app_100_compile_time_warm(benchmark, app_with_hundred_pages):
"""Test the compile time on a warm start for an app with 100 page.
Args:
benchmark: The benchmark fixture.
app_with_hundred_pages: The app harness.
"""
with chdir(app_with_hundred_pages.app_path):
app_with_hundred_pages._initialize_app()
build.setup_frontend(app_with_hundred_pages.app_path)
def benchmark_fn():
with chdir(app_with_hundred_pages.app_path):
app_with_hundred_pages.app_instance._compile()
benchmark(benchmark_fn)
app_with_hundred_pages._reload_state_module()
@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON)
@pytest.mark.benchmark(
group="Compile time of varying page numbers",
timer=time.perf_counter,
disable_gc=True,
warmup=False,
)
def test_app_1000_compile_time_cold(benchmark, app_with_thousand_pages):
"""Test the compile time on a cold start for an app with 1000 page.
Args:
benchmark: The benchmark fixture.
app_with_thousand_pages: The app harness.
"""
def setup():
with chdir(app_with_thousand_pages.app_path):
utils.empty_dir(web_pages, keep_files=["_app.js"])
app_with_thousand_pages._initialize_app()
build.setup_frontend(app_with_thousand_pages.app_path)
def benchmark_fn():
with chdir(app_with_thousand_pages.app_path):
app_with_thousand_pages.app_instance._compile()
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
app_with_thousand_pages._reload_state_module()
@pytest.mark.benchmark(
group="Compile time of varying page numbers",
min_rounds=5,
timer=time.perf_counter,
disable_gc=True,
warmup=False,
)
def test_app_1000_compile_time_warm(benchmark, app_with_thousand_pages):
"""Test the compile time on a warm start for an app with 1000 page.
Args:
benchmark: The benchmark fixture.
app_with_thousand_pages: The app harness.
"""
with chdir(app_with_thousand_pages.app_path):
app_with_thousand_pages._initialize_app()
build.setup_frontend(app_with_thousand_pages.app_path)
def benchmark_fn():
with chdir(app_with_thousand_pages.app_path):
app_with_thousand_pages.app_instance._compile()
benchmark(benchmark_fn)
app_with_thousand_pages._reload_state_module()
@pytest.mark.skip
@pytest.mark.benchmark(
group="Compile time of varying page numbers",
timer=time.perf_counter,
disable_gc=True,
warmup=False,
)
def test_app_10000_compile_time_cold(benchmark, app_with_ten_thousand_pages):
"""Test the compile time on a cold start for an app with 10000 page.
Args:
benchmark: The benchmark fixture.
app_with_ten_thousand_pages: The app harness.
"""
def setup():
with chdir(app_with_ten_thousand_pages.app_path):
utils.empty_dir(web_pages, keep_files=["_app.js"])
app_with_ten_thousand_pages._initialize_app()
build.setup_frontend(app_with_ten_thousand_pages.app_path)
def benchmark_fn():
with chdir(app_with_ten_thousand_pages.app_path):
app_with_ten_thousand_pages.app_instance._compile()
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
app_with_ten_thousand_pages._reload_state_module()
@pytest.mark.skip
@pytest.mark.benchmark(
group="Compile time of varying page numbers",
min_rounds=5,
timer=time.perf_counter,
disable_gc=True,
warmup=False,
)
def test_app_10000_compile_time_warm(benchmark, app_with_ten_thousand_pages):
"""Test the compile time on a warm start for an app with 10000 page.
Args:
benchmark: The benchmark fixture.
app_with_ten_thousand_pages: The app harness.
"""
def benchmark_fn():
with chdir(app_with_ten_thousand_pages.app_path):
app_with_ten_thousand_pages.app_instance._compile()
benchmark(benchmark_fn)
app_with_ten_thousand_pages._reload_state_module()

View File

@ -2,13 +2,12 @@
import os import os
import subprocess import subprocess
from pathlib import Path
import httpx import httpx
from httpx import HTTPError from httpx import HTTPError
def get_python_version(venv_path: Path, os_name): def get_python_version(venv_path, os_name):
"""Get the python version of python in a virtual env. """Get the python version of python in a virtual env.
Args: Args:
@ -19,13 +18,13 @@ def get_python_version(venv_path: Path, os_name):
The python version. The python version.
""" """
python_executable = ( python_executable = (
venv_path / "bin" / "python" os.path.join(venv_path, "bin", "python")
if "windows" not in os_name if "windows" not in os_name
else venv_path / "Scripts" / "python.exe" else os.path.join(venv_path, "Scripts", "python.exe")
) )
try: try:
output = subprocess.check_output( output = subprocess.check_output(
[str(python_executable), "--version"], stderr=subprocess.STDOUT [python_executable, "--version"], stderr=subprocess.STDOUT
) )
python_version = output.decode("utf-8").strip().split()[1] python_version = output.decode("utf-8").strip().split()[1]
return ".".join(python_version.split(".")[:-1]) return ".".join(python_version.split(".")[:-1])
@ -33,7 +32,7 @@ def get_python_version(venv_path: Path, os_name):
return None return None
def get_directory_size(directory: Path): def get_directory_size(directory):
"""Get the size of a directory in bytes. """Get the size of a directory in bytes.
Args: Args:
@ -45,8 +44,8 @@ def get_directory_size(directory: Path):
total_size = 0 total_size = 0
for dirpath, _, filenames in os.walk(directory): for dirpath, _, filenames in os.walk(directory):
for f in filenames: for f in filenames:
fp = Path(dirpath) / f fp = os.path.join(dirpath, f)
total_size += fp.stat().st_size total_size += os.path.getsize(fp)
return total_size return total_size

View File

@ -23,11 +23,11 @@
# for example, pass `docker build --platform=linux/amd64 ...` # for example, pass `docker build --platform=linux/amd64 ...`
# Stage 1: init # Stage 1: init
FROM python:3.13 as init FROM python:3.11 as init
ARG uv=/root/.local/bin/uv ARG uv=/root/.cargo/bin/uv
# Install `uv` for faster package bootstrapping # Install `uv` for faster package boostrapping
ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh
RUN /install.sh && rm /install.sh RUN /install.sh && rm /install.sh
@ -48,11 +48,11 @@ RUN $uv pip install -r requirements.txt
RUN reflex init RUN reflex init
# Stage 2: copy artifacts into slim image # Stage 2: copy artifacts into slim image
FROM python:3.13-slim FROM python:3.11-slim
WORKDIR /app WORKDIR /app
RUN adduser --disabled-password --home /app reflex RUN adduser --disabled-password --home /app reflex
COPY --chown=reflex --from=init /app /app COPY --chown=reflex --from=init /app /app
# Install libpq-dev for psycopg (skip if not using postgres). # Install libpq-dev for psycopg2 (skip if not using postgres).
RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/* RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
USER reflex USER reflex
ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1 ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1

View File

@ -2,11 +2,11 @@
# instance of a Reflex app. # instance of a Reflex app.
# Stage 1: init # Stage 1: init
FROM python:3.13 as init FROM python:3.11 as init
ARG uv=/root/.local/bin/uv ARG uv=/root/.cargo/bin/uv
# Install `uv` for faster package bootstrapping # Install `uv` for faster package boostrapping
ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh
RUN /install.sh && rm /install.sh RUN /install.sh && rm /install.sh
@ -35,11 +35,11 @@ RUN rm -rf .web && mkdir .web
RUN mv /tmp/_static .web/_static RUN mv /tmp/_static .web/_static
# Stage 2: copy artifacts into slim image # Stage 2: copy artifacts into slim image
FROM python:3.13-slim FROM python:3.11-slim
WORKDIR /app WORKDIR /app
RUN adduser --disabled-password --home /app reflex RUN adduser --disabled-password --home /app reflex
COPY --chown=reflex --from=init /app /app COPY --chown=reflex --from=init /app /app
# Install libpq-dev for psycopg (skip if not using postgres). # Install libpq-dev for psycopg2 (skip if not using postgres).
RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/* RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
USER reflex USER reflex
ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1 ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1

View File

@ -15,7 +15,7 @@ services:
app: app:
environment: environment:
DB_URL: postgresql+psycopg://postgres:secret@db/postgres DB_URL: postgresql+psycopg2://postgres:secret@db/postgres
REDIS_URL: redis://redis:6379 REDIS_URL: redis://redis:6379
depends_on: depends_on:
- db - db

View File

@ -1,3 +0,0 @@
.web
!.web/bun.lockb
!.web/package.json

View File

@ -1,14 +0,0 @@
:{$PORT}
encode gzip
@backend_routes path /_event/* /ping /_upload /_upload/*
handle @backend_routes {
reverse_proxy localhost:8000
}
root * /srv
route {
try_files {path} {path}/ /404.html
file_server
}

View File

@ -1,62 +0,0 @@
# This Dockerfile is used to deploy a single-container Reflex app instance
# to services like Render, Railway, Heroku, GCP, and others.
# If the service expects a different port, provide it here (f.e Render expects port 10000)
ARG PORT=8080
# Only set for local/direct access. When TLS is used, the API_URL is assumed to be the same as the frontend.
ARG API_URL
# It uses a reverse proxy to serve the frontend statically and proxy to backend
# from a single exposed port, expecting TLS termination to be handled at the
# edge by the given platform.
FROM python:3.13 as builder
RUN mkdir -p /app/.web
RUN python -m venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"
WORKDIR /app
# Install python app requirements and reflex in the container
COPY requirements.txt .
RUN pip install -r requirements.txt
# Install reflex helper utilities like bun/fnm/node
COPY rxconfig.py ./
RUN reflex init
# Install pre-cached frontend dependencies (if exist)
COPY *.web/bun.lockb *.web/package.json .web/
RUN if [ -f .web/bun.lockb ]; then cd .web && ~/.local/share/reflex/bun/bin/bun install --frozen-lockfile; fi
# Copy local context to `/app` inside container (see .dockerignore)
COPY . .
ARG PORT API_URL
# Download other npm dependencies and compile frontend
RUN API_URL=${API_URL:-http://localhost:$PORT} reflex export --loglevel debug --frontend-only --no-zip && mv .web/_static/* /srv/ && rm -rf .web
# Final image with only necessary files
FROM python:3.13-slim
# Install Caddy and redis server inside image
RUN apt-get update -y && apt-get install -y caddy redis-server && rm -rf /var/lib/apt/lists/*
ARG PORT API_URL
ENV PATH="/app/.venv/bin:$PATH" PORT=$PORT API_URL=${API_URL:-http://localhost:$PORT} REDIS_URL=redis://localhost PYTHONUNBUFFERED=1
WORKDIR /app
COPY --from=builder /app /app
COPY --from=builder /srv /srv
# Needed until Reflex properly passes SIGTERM on backend.
STOPSIGNAL SIGKILL
EXPOSE $PORT
# Apply migrations before starting the backend.
CMD [ -d alembic ] && reflex db migrate; \
caddy start && \
redis-server --daemonize yes && \
exec reflex run --env prod --backend-only

View File

@ -1,37 +0,0 @@
# production-one-port
This docker deployment runs Reflex in prod mode, exposing a single HTTP port:
* `8080` (`$PORT`) - Caddy server hosting the frontend statically and proxying requests to the backend.
The deployment also runs a local Redis server to store state for each user.
Conceptually it is similar to the `simple-one-port` example except it:
* has layer caching for python, reflex, and node dependencies
* uses multi-stage build to reduce the size of the final image
Using this method may be preferable for deploying in memory constrained
environments, because it serves a static frontend export, rather than running
the NextJS server via node.
## Build
```console
docker build -t reflex-production-one-port .
```
## Run
```console
docker run -p 8080:8080 reflex-production-one-port
```
Note that this container has _no persistence_ and will lose all data when
stopped. You can use bind mounts or named volumes to persist the database and
uploaded_files directories as needed.
## Usage
This container should be used with an existing load balancer or reverse proxy to
terminate TLS.
It is also useful for deploying to simple app platforms, such as Render or Heroku.

View File

@ -11,4 +11,4 @@ root * /srv
route { route {
try_files {path} {path}/ /404.html try_files {path} {path}/ /404.html
file_server file_server
} }

View File

@ -4,7 +4,7 @@
# It uses a reverse proxy to serve the frontend statically and proxy to backend # It uses a reverse proxy to serve the frontend statically and proxy to backend
# from a single exposed port, expecting TLS termination to be handled at the # from a single exposed port, expecting TLS termination to be handled at the
# edge by the given platform. # edge by the given platform.
FROM python:3.13 FROM python:3.11
# If the service expects a different port, provide it here (f.e Render expects port 10000) # If the service expects a different port, provide it here (f.e Render expects port 10000)
ARG PORT=8080 ARG PORT=8080
@ -38,4 +38,4 @@ EXPOSE $PORT
CMD [ -d alembic ] && reflex db migrate; \ CMD [ -d alembic ] && reflex db migrate; \
caddy start && \ caddy start && \
redis-server --daemonize yes && \ redis-server --daemonize yes && \
exec reflex run --env prod --backend-only exec reflex run --env prod --backend-only

View File

@ -1,5 +1,5 @@
# This Dockerfile is used to deploy a simple single-container Reflex app instance. # This Dockerfile is used to deploy a simple single-container Reflex app instance.
FROM python:3.13 FROM python:3.12
RUN apt-get update && apt-get install -y redis-server && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y redis-server && rm -rf /var/lib/apt/lists/*
ENV REDIS_URL=redis://localhost PYTHONUNBUFFERED=1 ENV REDIS_URL=redis://localhost PYTHONUNBUFFERED=1

View File

@ -10,6 +10,7 @@
### **✨ Performante, anpassbare Web-Apps in purem Python. Bereitstellung in Sekunden. ✨** ### **✨ Performante, anpassbare Web-Apps in purem Python. Bereitstellung in Sekunden. ✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -34,7 +35,7 @@ Auf unserer [Architektur-Seite](https://reflex.dev/blog/2024-03-21-reflex-archit
## ⚙️ Installation ## ⚙️ Installation
Öffne ein Terminal und führe den folgenden Befehl aus (benötigt Python 3.10+): Öffne ein Terminal und führe den folgenden Befehl aus (benötigt Python 3.8+):
```bash ```bash
pip install reflex pip install reflex

View File

@ -35,7 +35,7 @@ Consulta nuestra [página de arquitectura](https://reflex.dev/blog/2024-03-21-re
## ⚙️ Instalación ## ⚙️ Instalación
Abra un terminal y ejecute (Requiere Python 3.10+): Abra un terminal y ejecute (Requiere Python 3.8+):
```bash ```bash
pip install reflex pip install reflex
@ -239,7 +239,7 @@ Reflex se lanzó en diciembre de 2022 con el nombre de Pynecone.
- **Discusiones de GitHub**: Una excelente manera de hablar sobre las características que deseas agregar o las cosas que te resultan confusas o necesitan aclaración. - **Discusiones de GitHub**: Una excelente manera de hablar sobre las características que deseas agregar o las cosas que te resultan confusas o necesitan aclaración.
- **GitHub Issues**: Las incidencias son una forma excelente de informar de errores. Además, puedes intentar resolver un problema existente y enviar un PR. - **GitHub Issues**: Las incidencias son una forma excelente de informar de errores. Además, puedes intentar resolver un problema existente y enviar un PR.
Buscamos colaboradores, sin importar su nivel o experiencia. Para contribuir consulta [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) Buscamos colaboradores, sin importar su nivel o experiencia. Para contribuir consulta [CONTIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)
## Licencia ## Licencia

View File

@ -11,6 +11,7 @@ Pynecone की तलाश हैं? आप सही रेपो में
### **✨ प्रदर्शनकारी, अनुकूलित वेब ऐप्स, शुद्ध Python में। सेकंडों में तैनात करें। ✨** ### **✨ प्रदर्शनकारी, अनुकूलित वेब ऐप्स, शुद्ध Python में। सेकंडों में तैनात करें। ✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -35,7 +36,7 @@ Reflex के अंदर के कामकाज को जानने क
## ⚙️ इंस्टॉलेशन (Installation) ## ⚙️ इंस्टॉलेशन (Installation)
एक टर्मिनल खोलें और चलाएं (Python 3.10+ की आवश्यकता है): एक टर्मिनल खोलें और चलाएं (Python 3.8+ की आवश्यकता है):
```bash ```bash
pip install reflex pip install reflex
@ -239,7 +240,7 @@ Reflex में हर सप्ताह नए रिलीज़ और फ
- **GitHub Discussions** (गिटहब चर्चाएँ): उन सुविधाओं के बारे में बात करने का एक शानदार तरीका जिन्हें आप जोड़ना चाहते हैं या ऐसी चीज़ें जो भ्रमित करने वाली हैं/स्पष्टीकरण की आवश्यकता है। - **GitHub Discussions** (गिटहब चर्चाएँ): उन सुविधाओं के बारे में बात करने का एक शानदार तरीका जिन्हें आप जोड़ना चाहते हैं या ऐसी चीज़ें जो भ्रमित करने वाली हैं/स्पष्टीकरण की आवश्यकता है।
- **GitHub Issues** (गिटहब समस्याएं): ये [बग](https://github.com/reflex-dev/reflex/issues) की रिपोर्ट करने का एक शानदार तरीका है। इसके अतिरिक्त, आप किसी मौजूदा समस्या को हल करने का प्रयास कर सकते हैं और एक पीआर सबमिट कर सकते हैं। - **GitHub Issues** (गिटहब समस्याएं): ये [बग](https://github.com/reflex-dev/reflex/issues) की रिपोर्ट करने का एक शानदार तरीका है। इसके अतिरिक्त, आप किसी मौजूदा समस्या को हल करने का प्रयास कर सकते हैं और एक पीआर सबमिट कर सकते हैं।
हम सक्रिय रूप से योगदानकर्ताओं की तलाश कर रहे हैं, चाहे आपका कौशल स्तर या अनुभव कुछ भी हो।योगदान करने के लिए [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) देखें। हम सक्रिय रूप से योगदानकर्ताओं की तलाश कर रहे हैं, चाहे आपका कौशल स्तर या अनुभव कुछ भी हो।योगदान करने के लिए [CONTIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) देखें।
## हमारे सभी योगदानकर्ताओं का धन्यवाद: ## हमारे सभी योगदानकर्ताओं का धन्यवाद:

View File

@ -10,6 +10,7 @@
### **✨ App web performanti e personalizzabili in puro Python. Distribuisci in pochi secondi. ✨** ### **✨ App web performanti e personalizzabili in puro Python. Distribuisci in pochi secondi. ✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -22,7 +23,7 @@
## ⚙️ Installazione ## ⚙️ Installazione
Apri un terminale ed esegui (Richiede Python 3.10+): Apri un terminale ed esegui (Richiede Python 3.8+):
```bash ```bash
pip install reflex pip install reflex

View File

@ -11,6 +11,7 @@
### **✨ 即時デプロイが可能な、Pure Python で作ったパフォーマンスと汎用性が高い Web アプリケーション ✨** ### **✨ 即時デプロイが可能な、Pure Python で作ったパフォーマンスと汎用性が高い Web アプリケーション ✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -37,7 +38,7 @@ Reflex がどのように動作しているかを知るには、[アーキテク
## ⚙️ インストール ## ⚙️ インストール
ターミナルを開いて以下のコマンドを実行してください。Python 3.10 以上が必要です。): ターミナルを開いて以下のコマンドを実行してください。Python 3.8 以上が必要です。):
```bash ```bash
pip install reflex pip install reflex
@ -222,7 +223,7 @@ app.add_page(index, title="DALL-E")
<div align="center"> <div align="center">
📑 [Docs](https://reflex.dev/docs/getting-started/introduction) &nbsp; | &nbsp; 🗞️ [Blog](https://reflex.dev/blog) &nbsp; | &nbsp; 📱 [Component Library](https://reflex.dev/docs/library) &nbsp; | &nbsp; 🖼️ [Templates](https://reflex.dev/templates/) &nbsp; | &nbsp; 🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start) &nbsp; 📑 [Docs](https://reflex.dev/docs/getting-started/introduction) &nbsp; | &nbsp; 🗞️ [Blog](https://reflex.dev/blog) &nbsp; | &nbsp; 📱 [Component Library](https://reflex.dev/docs/library) &nbsp; | &nbsp; 🖼️ [Gallery](https://reflex.dev/docs/gallery) &nbsp; | &nbsp; 🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start) &nbsp;
</div> </div>
@ -242,7 +243,7 @@ Reflex は毎週、新しいリリースや機能追加を行っています!
- **GitHub Discussions**: GitHub Discussions では、追加したい機能や、複雑で解明が必要な事柄についての議論に適している場所です。 - **GitHub Discussions**: GitHub Discussions では、追加したい機能や、複雑で解明が必要な事柄についての議論に適している場所です。
- **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues)はバグの報告に適している場所です。また、課題を解決した PR のサブミットにチャレンジしていただくことも、可能です。 - **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues)はバグの報告に適している場所です。また、課題を解決した PR のサブミットにチャレンジしていただくことも、可能です。
CONTスキルや経験に関わらず、私たちはコントリビュータを積極的に探しています。コントリビュートするために、[CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)をご覧ください。 スキルや経験に関わらず、私たちはコントリビュータを積極的に探しています。コントリビュートするために、[CONTIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)をご覧ください。
## 私たちのコントリビュータに感謝!: ## 私たちのコントリビュータに感謝!:

View File

@ -10,6 +10,7 @@
### **✨ 순수 Python으로 고성능 사용자 정의 웹앱을 만들어 보세요. 몇 초만에 배포 가능합니다. ✨** ### **✨ 순수 Python으로 고성능 사용자 정의 웹앱을 만들어 보세요. 몇 초만에 배포 가능합니다. ✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -20,7 +21,7 @@
--- ---
## ⚙️ 설치 ## ⚙️ 설치
터미널을 열고 실행하세요. (Python 3.10+ 필요): 터미널을 열고 실행하세요. (Python 3.8+ 필요):
```bash ```bash
pip install reflex pip install reflex

View File

@ -10,6 +10,7 @@
### **✨ برنامه های تحت وب قابل تنظیم، کارآمد تماما پایتونی که در چند ثانیه مستقر(دپلوی) می‎شود. ✨** ### **✨ برنامه های تحت وب قابل تنظیم، کارآمد تماما پایتونی که در چند ثانیه مستقر(دپلوی) می‎شود. ✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -34,7 +35,7 @@
## ⚙️ Installation - نصب و راه اندازی ## ⚙️ Installation - نصب و راه اندازی
یک ترمینال را باز کنید و اجرا کنید (نیازمند Python 3.10+): یک ترمینال را باز کنید و اجرا کنید (نیازمند Python 3.8+):
```bash ```bash
pip install reflex pip install reflex
@ -249,7 +250,7 @@ app.add_page(index, title="DALL-E")
- **بحث های GitHub**: راهی عالی برای صحبت در مورد ویژگی هایی که می خواهید اضافه کنید یا چیزهایی که گیج کننده هستند/نیاز به توضیح دارند. - **بحث های GitHub**: راهی عالی برای صحبت در مورد ویژگی هایی که می خواهید اضافه کنید یا چیزهایی که گیج کننده هستند/نیاز به توضیح دارند.
- **قسمت مشکلات GitHub**: [قسمت مشکلات](https://github.com/reflex-dev/reflex/issues) یک راه عالی برای گزارش اشکال هستند. علاوه بر این، می توانید یک مشکل موجود را حل کنید و یک PR(pull request) ارسال کنید. - **قسمت مشکلات GitHub**: [قسمت مشکلات](https://github.com/reflex-dev/reflex/issues) یک راه عالی برای گزارش اشکال هستند. علاوه بر این، می توانید یک مشکل موجود را حل کنید و یک PR(pull request) ارسال کنید.
ما فعالانه به دنبال مشارکت کنندگان هستیم، فارغ از سطح مهارت یا تجربه شما. برای مشارکت [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) را بررسی کنید. ما فعالانه به دنبال مشارکت کنندگان هستیم، فارغ از سطح مهارت یا تجربه شما. برای مشارکت [CONTIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) را بررسی کنید.
## All Thanks To Our Contributors - با تشکر از همکاران ما: ## All Thanks To Our Contributors - با تشکر از همکاران ما:

View File

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

View File

@ -11,6 +11,7 @@
### **✨ Saf Python'da performanslı, özelleştirilebilir web uygulamaları. Saniyeler içinde dağıtın. ✨** ### **✨ Saf Python'da performanslı, özelleştirilebilir web uygulamaları. Saniyeler içinde dağıtın. ✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -24,7 +25,7 @@
## ⚙️ Kurulum ## ⚙️ Kurulum
Bir terminal açın ve çalıştırın (Python 3.10+ gerekir): Bir terminal açın ve çalıştırın (Python 3.8+ gerekir):
```bash ```bash
pip install reflex pip install reflex
@ -200,7 +201,7 @@ Daha fazla sayfa ekleyerek çok sayfalı bir uygulama oluşturabilirsiniz.
<div align="center"> <div align="center">
📑 [Docs](https://reflex.dev/docs/getting-started/introduction) &nbsp; | &nbsp; 🗞️ [Blog](https://reflex.dev/blog) &nbsp; | &nbsp; 📱 [Component Library](https://reflex.dev/docs/library) &nbsp; | &nbsp; 🖼️ [Templates](https://reflex.dev/templates/) &nbsp; | &nbsp; 🛸 [Deployment](https://reflex.dev/docs/hosting/deploy) &nbsp; 📑 [Docs](https://reflex.dev/docs/getting-started/introduction) &nbsp; | &nbsp; 🗞️ [Blog](https://reflex.dev/blog) &nbsp; | &nbsp; 📱 [Component Library](https://reflex.dev/docs/library) &nbsp; | &nbsp; 🖼️ [Gallery](https://reflex.dev/docs/gallery) &nbsp; | &nbsp; 🛸 [Deployment](https://reflex.dev/docs/hosting/deploy) &nbsp;
</div> </div>
@ -229,7 +230,7 @@ Her boyuttaki katkıları memnuniyetle karşılıyoruz! Aşağıda Reflex toplul
- **GitHub Discussions**: Eklemek istediğiniz özellikler veya kafa karıştırıcı, açıklığa kavuşturulması gereken şeyler hakkında konuşmanın harika bir yolu. - **GitHub Discussions**: Eklemek istediğiniz özellikler veya kafa karıştırıcı, açıklığa kavuşturulması gereken şeyler hakkında konuşmanın harika bir yolu.
- **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues) hataları bildirmenin mükemmel bir yoludur. Ayrıca mevcut bir sorunu deneyip çözebilir ve bir PR (Pull Requests) gönderebilirsiniz. - **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues) hataları bildirmenin mükemmel bir yoludur. Ayrıca mevcut bir sorunu deneyip çözebilir ve bir PR (Pull Requests) gönderebilirsiniz.
Beceri düzeyiniz veya deneyiminiz ne olursa olsun aktif olarak katkıda bulunacak kişiler arıyoruz. Katkı sağlamak için katkı sağlama rehberimize bakabilirsiniz: [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) Beceri düzeyiniz veya deneyiminiz ne olursa olsun aktif olarak katkıda bulunacak kişiler arıyoruz. Katkı sağlamak için katkı sağlama rehberimize bakabilirsiniz: [CONTIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)
## Hepsi Katkıda Bulunanlar Sayesinde: ## Hepsi Katkıda Bulunanlar Sayesinde:

View File

@ -1,267 +0,0 @@
```diff
+ Bạn đang tìm kiếm Pynecone? Bạn đã tìm đúng. Pynecone đã được đổi tên thành Reflex. +
```
<div align="center">
<img src="https://raw.githubusercontent.com/reflex-dev/reflex/main/docs/images/reflex_dark.svg#gh-light-mode-only" alt="Reflex Logo" width="300px">
<img src="https://raw.githubusercontent.com/reflex-dev/reflex/main/docs/images/reflex_light.svg#gh-dark-mode-only" alt="Reflex Logo" width="300px">
<hr>
### **✨ Ứng dụng web hiệu suất cao, tùy chỉnh bằng Python thuần. Deploy trong vài giây. ✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentation](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
</div>
---
[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md)
---
# Reflex
Reflex là một thư viện để xây dựng ứng dụng web toàn bộ bằng Python thuần.
Các tính năng chính:
* **Python thuần tuý** - Viết toàn bộ ứng dụng cả backend và frontend hoàn toàn bằng Python, không cần học JavaScript.
* **Full Flexibility** - Reflex dễ dàng để bắt đầu, nhưng cũng có thể mở rộng lên các ứng dụng phức tạp.
* **Deploy Instantly** - Sau khi xây dựng ứng dụng, bạn có thể triển khai bằng [một dòng lệnh](https://reflex.dev/docs/hosting/deploy-quick-start/) hoặc triển khai trên server của riêng bạn.
Đọc [bài viết về kiến trúc hệ thống](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture) để hiểu rõ các hoạt động của Reflex.
## ⚙️ Cài đặt
Mở cửa sổ lệnh và chạy (Yêu cầu Python phiên bản 3.10+):
```bash
pip install reflex
```
## 🥳 Tạo ứng dụng đầu tiên
Cài đặt `reflex` cũng như cài đặt công cụ dòng lệnh `reflex`.
Kiểm tra việc cài đặt đã thành công hay chưa bằng cách tạo mới một ứng dụng. (Thay `my_app_name` bằng tên ứng dụng của bạn):
```bash
mkdir my_app_name
cd my_app_name
reflex init
```
Lệnh này tạo ra một ứng dụng mẫu trong một thư mục mới.
Bạn có thể chạy ứng dụng ở chế độ phát triển.
```bash
reflex run
```
Bạn có thể xem ứng dụng của bạn ở địa chỉ http://localhost:3000.
Bạn có thể thay đổi mã nguồn ở `my_app_name/my_app_name.py`. Reflex nhanh chóng làm mới và bạn có thể thấy thay đổi trên ứng dụng của bạn ngay lập tức khi bạn lưu file.
## 🫧 Ứng dụng ví dụ
Bắt đầu với ví dụ: tạo một ứng dụng tạo ảnh bằng [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node). Để cho đơn giản, chúng ta sẽ sử dụng [OpenAI API](https://platform.openai.com/docs/api-reference/authentication), nhưng bạn có thể sử dụng model của chính bạn được triển khai trên local.
&nbsp;
<div align="center">
<img src="https://raw.githubusercontent.com/reflex-dev/reflex/main/docs/images/dalle.gif" alt="A frontend wrapper for DALL·E, shown in the process of generating an image." width="550" />
</div>
&nbsp;
Đây là toàn bộ đoạn mã để xây dựng ứng dụng trên. Nó được viết hoàn toàn trong một file Python!
```python
import reflex as rx
import openai
openai_client = openai.OpenAI()
class State(rx.State):
"""The app state."""
prompt = ""
image_url = ""
processing = False
complete = False
def get_image(self):
"""Get the image from the prompt."""
if self.prompt == "":
return rx.window_alert("Prompt Empty")
self.processing, self.complete = True, False
yield
response = openai_client.images.generate(
prompt=self.prompt, n=1, size="1024x1024"
)
self.image_url = response.data[0].url
self.processing, self.complete = False, True
def index():
return rx.center(
rx.vstack(
rx.heading("DALL-E", font_size="1.5em"),
rx.input(
placeholder="Enter a prompt..",
on_blur=State.set_prompt,
width="25em",
),
rx.button(
"Generate Image",
on_click=State.get_image,
width="25em",
loading=State.processing
),
rx.cond(
State.complete,
rx.image(src=State.image_url, width="20em"),
),
align="center",
),
width="100%",
height="100vh",
)
# Add state and page to the app.
app = rx.App()
app.add_page(index, title="Reflex:DALL-E")
```
## Hãy phân tích chi tiết.
<div align="center">
<img src="../images/dalle_colored_code_example.png" alt="Explaining the differences between backend and frontend parts of the DALL-E app." width="900" />
</div>
### **Reflex UI**
Bắt đầu với giao diện chính.
```python
def index():
return rx.center(
...
)
```
Hàm `index` định nghĩa phần giao diện chính của ứng dụng.
Chúng tôi sử dụng các component (thành phần) khác nhau như `center`, `vstack`, `input``button` để xây dựng giao diện phía trước.
Các component có thể được lồng vào nhau để tạo ra các bố cục phức tạp. Và bạn cũng có thể sử dụng từ khoá `args` để tận dụng đầy đủ sức mạnh của CSS.
Reflex có đến hơn [60 component được xây dựng sẵn](https://reflex.dev/docs/library) để giúp bạn bắt đầu. Chúng ta có thể tạo ra một component mới khá dễ dàng, thao khảo: [xây dựng component của riêng bạn](https://reflex.dev/docs/wrapping-react/overview/).
### **State**
Reflex biểu diễn giao diện bằng các hàm của state (trạng thái).
```python
class State(rx.State):
"""The app state."""
prompt = ""
image_url = ""
processing = False
complete = False
```
Một state định nghĩa các biến (được gọi là vars) có thể thay đổi trong một ứng dụng và cho phép các hàm có thể thay đổi chúng.
Tại đây state được cấu thành từ một `prompt``image_url`.
Có cũng những biến boolean `processing``complete`
để chỉ ra khi nào tắt nút (trong quá trình tạo hình ảnh)
và khi nào hiển thị hình ảnh kết quả.
### **Event Handlers**
```python
def get_image(self):
"""Get the image from the prompt."""
if self.prompt == "":
return rx.window_alert("Prompt Empty")
self.processing, self.complete = True, False
yield
response = openai_client.images.generate(
prompt=self.prompt, n=1, size="1024x1024"
)
self.image_url = response.data[0].url
self.processing, self.complete = False, True
```
Với các state, chúng ta định nghĩa các hàm có thể thay đổi state vars được gọi là event handlers. Event handler là cách chúng ta có thể thay đổi state trong Reflex. Chúng có thể là phản hồi khi người dùng thao tác, chằng hạn khi nhấn vào nút hoặc khi đang nhập trong text box. Các hành động này được gọi là event.
Ứng dụng DALL·E. của chúng ta có một event handler, `get_image` để lấy hình ảnh từ OpenAI API. Sử dụng từ khoá `yield` in ở giữa event handler để cập nhật giao diện. Hoặc giao diện có thể cập nhật ở cuối event handler.
### **Routing**
Cuối cùng, chúng ta định nghĩa một ứng dụng.
```python
app = rx.App()
```
Chúng ta thêm một trang ở đầu ứng dụng bằng index component. Chúng ta cũng thêm tiêu đề của ứng dụng để hiển thị lên trình duyệt.
```python
app.add_page(index, title="DALL-E")
```
Bạn có thể tạo một ứng dụng nhiều trang bằng cách thêm trang.
## 📑 Tài liệu
<div align="center">
📑 [Docs](https://reflex.dev/docs/getting-started/introduction) &nbsp; | &nbsp; 🗞️ [Blog](https://reflex.dev/blog) &nbsp; | &nbsp; 📱 [Component Library](https://reflex.dev/docs/library) &nbsp; | &nbsp; 🖼️ [Templates](https://reflex.dev/templates/) &nbsp; | &nbsp; 🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start) &nbsp;
</div>
## ✅ Status
Reflex phát hành vào tháng 12/2022 với tên là Pynecone.
Đến tháng 02/2024, chúng tôi tạo ra dịch vụ dưới phiên bản alpha! Trong thời gian này mọi người có thể triển khai ứng dụng hoàn toàn miễn phí. Xem [roadmap](https://github.com/reflex-dev/reflex/issues/2727) để biết thêm chi tiết.
Reflex ra phiên bản mới với các tính năng mới hàng tuần! Hãy :star: star và :eyes: watch repo này để thấy các cập nhật mới nhất.
## Contributing
Chúng tôi chào đón mọi đóng góp dù lớn hay nhỏ. Dưới đây là các cách để bắt đầu với cộng đồng Reflex.
- **Discord**: [Discord](https://discord.gg/T5WSbC2YtQ) của chúng tôi là nơi tốt nhất để nhờ sự giúp đỡ và thảo luận các bạn có thể đóng góp.
- **GitHub Discussions**: Là cách tốt nhất để thảo luận về các tính năng mà bạn có thể đóng góp hoặc những điều bạn chưa rõ.
- **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues) là nơi tốt nhất để thông báo. Ngoài ra bạn có thể sửa chữa các vấn đề bằng cách tạo PR.
Chúng tôi luôn sẵn sàng tìm kiếm các contributor, bất kể kinh nghiệm. Để tham gia đóng góp, xin mời xem
[CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)
## Xin cảm ơn các Contributors:
<a href="https://github.com/reflex-dev/reflex/graphs/contributors">
<img src="https://contrib.rocks/image?repo=reflex-dev/reflex" />
</a>
## License
Reflex là mã nguồn mở và sử dụng giấy phép [Apache License 2.0](LICENSE).

View File

@ -10,6 +10,7 @@
### **✨ 使用 Python 创建高效且可自定义的网页应用程序,几秒钟内即可部署.✨** ### **✨ 使用 Python 创建高效且可自定义的网页应用程序,几秒钟内即可部署.✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -34,7 +35,7 @@ Reflex 是一个使用纯Python构建全栈web应用的库。
## ⚙️ 安装 ## ⚙️ 安装
打开一个终端并且运行(要求Python3.10+): 打开一个终端并且运行(要求Python3.8+):
```bash ```bash
pip install reflex pip install reflex

View File

@ -11,6 +11,7 @@
**✨ 使用 Python 建立高效且可自訂的網頁應用程式,幾秒鐘內即可部署。✨** **✨ 使用 Python 建立高效且可自訂的網頁應用程式,幾秒鐘內即可部署。✨**
[![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex) [![PyPI version](https://badge.fury.io/py/reflex.svg)](https://badge.fury.io/py/reflex)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.svg)
![versions](https://img.shields.io/pypi/pyversions/reflex.svg) ![versions](https://img.shields.io/pypi/pyversions/reflex.svg)
[![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction) [![Documentaiton](https://img.shields.io/badge/Documentation%20-Introduction%20-%20%23007ec6)](https://reflex.dev/docs/getting-started/introduction)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ) [![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -36,7 +37,7 @@ Reflex 是一個可以用純 Python 構建全端網頁應用程式的函式庫
## ⚙️ 安裝 ## ⚙️ 安裝
開啟一個終端機並且執行 (需要 Python 3.10+): 開啟一個終端機並且執行 (需要 Python 3.8+):
```bash ```bash
pip install reflex pip install reflex
@ -229,7 +230,7 @@ app.add_page(index, title="DALL-E")
<div align="center"> <div align="center">
📑 [Docs](https://reflex.dev/docs/getting-started/introduction) &nbsp; | &nbsp; 🗞️ [Blog](https://reflex.dev/blog) &nbsp; | &nbsp; 📱 [Component Library](https://reflex.dev/docs/library) &nbsp; | &nbsp; 🖼️ [Templates](https://reflex.dev/templates/) &nbsp; | &nbsp; 🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start) &nbsp; 📑 [Docs](https://reflex.dev/docs/getting-started/introduction) &nbsp; | &nbsp; 🗞️ [Blog](https://reflex.dev/blog) &nbsp; | &nbsp; 📱 [Component Library](https://reflex.dev/docs/library) &nbsp; | &nbsp; 🖼️ [Gallery](https://reflex.dev/docs/gallery) &nbsp; | &nbsp; 🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start) &nbsp;
</div> </div>
@ -251,7 +252,7 @@ Reflex 每周都有新功能和釋出新版本! 確保你按下 :star: 和 :eyes
- **GitHub Discussions**: 這是一個討論您想新增的功能或對於一些困惑/需要澄清事項的好方法。 - **GitHub Discussions**: 這是一個討論您想新增的功能或對於一些困惑/需要澄清事項的好方法。
- **GitHub Issues**: 在 [Issues](https://github.com/reflex-dev/reflex/issues) 頁面報告錯誤是一個絕佳的方式。此外,您也可以嘗試解決現有 Issue 並提交 PR。 - **GitHub Issues**: 在 [Issues](https://github.com/reflex-dev/reflex/issues) 頁面報告錯誤是一個絕佳的方式。此外,您也可以嘗試解決現有 Issue 並提交 PR。
我們積極尋找貢獻者,不論您的技能水平或經驗如何。要貢獻,請查看 [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) 我們積極尋找貢獻者,不論您的技能水平或經驗如何。要貢獻,請查看 [CONTIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)
## 感謝所有貢獻者: ## 感謝所有貢獻者:

View File

@ -1,11 +1,11 @@
"""Shared conftest for all integration tests.""" """Shared conftest for all integration tests."""
import os import os
import re
from pathlib import Path
import pytest import pytest
import reflex.app
from reflex.config import environment
from reflex.testing import AppHarness, AppHarnessProd from reflex.testing import AppHarness, AppHarnessProd
DISPLAY = None DISPLAY = None
@ -21,7 +21,7 @@ def xvfb():
Yields: Yields:
the pyvirtualdisplay object that the browser will be open on the pyvirtualdisplay object that the browser will be open on
""" """
if os.environ.get("GITHUB_ACTIONS") and not environment.APP_HARNESS_HEADLESS.get(): if os.environ.get("GITHUB_ACTIONS") and not os.environ.get("APP_HARNESS_HEADLESS"):
from pyvirtualdisplay.smartdisplay import ( # pyright: ignore [reportMissingImports] from pyvirtualdisplay.smartdisplay import ( # pyright: ignore [reportMissingImports]
SmartDisplay, SmartDisplay,
) )
@ -34,6 +34,34 @@ def xvfb():
yield None yield None
def pytest_exception_interact(node, call, report):
"""Take and upload screenshot when tests fail.
Args:
node: The pytest item that failed.
call: The pytest call describing when/where the test was invoked.
report: The pytest log report object.
"""
screenshot_dir = os.environ.get("SCREENSHOT_DIR")
if DISPLAY is None or screenshot_dir is None:
return
screenshot_dir = Path(screenshot_dir)
screenshot_dir.mkdir(parents=True, exist_ok=True)
safe_filename = re.sub(
r"(?u)[^-\w.]",
"_",
str(node.nodeid).strip().replace(" ", "_").replace(":", "_").replace(".py", ""),
)
try:
DISPLAY.waitgrab().save(
(Path(screenshot_dir) / safe_filename).with_suffix(".png"),
)
except Exception as e:
print(f"Failed to take screenshot for {node}: {e}")
@pytest.fixture( @pytest.fixture(
scope="session", params=[AppHarness, AppHarnessProd], ids=["dev", "prod"] scope="session", params=[AppHarness, AppHarnessProd], ids=["dev", "prod"]
) )
@ -47,25 +75,3 @@ def app_harness_env(request):
The AppHarness class to use for the test. The AppHarness class to use for the test.
""" """
return request.param return request.param
@pytest.fixture(autouse=True)
def raise_console_error(request, mocker):
"""Spy on calls to `console.error` used by the framework.
Help catch spurious error conditions that might otherwise go unnoticed.
If a test is marked with `ignore_console_error`, the spy will be ignored
after the test.
Args:
request: The pytest request object.
mocker: The pytest mocker object.
Yields:
control to the test function.
"""
spy = mocker.spy(reflex.app.console, "error")
yield
if "ignore_console_error" not in request.keywords:
spy.assert_not_called()

View File

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

View File

@ -13,7 +13,7 @@ function do_export () {
reflex init --template "$template" reflex init --template "$template"
reflex export reflex export
( (
cd "$SCRIPTPATH/../../.." cd "$SCRIPTPATH/../.."
scripts/integration.sh ~/"$template" dev scripts/integration.sh ~/"$template" dev
pkill -9 -f 'next-server|python3' || true pkill -9 -f 'next-server|python3' || true
sleep 10 sleep 10

View File

@ -1,4 +1,4 @@
"""Test @rx.event(background=True) task functionality.""" """Test @rx.background task functionality."""
from typing import Generator from typing import Generator
@ -20,13 +20,9 @@ def BackgroundTask():
class State(rx.State): class State(rx.State):
counter: int = 0 counter: int = 0
_task_id: int = 0 _task_id: int = 0
iterations: rx.Field[int] = rx.field(10) iterations: int = 10
@rx.event @rx.background
def set_iterations(self, value: str):
self.iterations = int(value)
@rx.event(background=True)
async def handle_event(self): async def handle_event(self):
async with self: async with self:
self._task_id += 1 self._task_id += 1
@ -35,35 +31,32 @@ def BackgroundTask():
self.counter += 1 self.counter += 1
await asyncio.sleep(0.005) await asyncio.sleep(0.005)
@rx.event(background=True) @rx.background
async def handle_event_yield_only(self): async def handle_event_yield_only(self):
async with self: async with self:
self._task_id += 1 self._task_id += 1
for ix in range(int(self.iterations)): for ix in range(int(self.iterations)):
if ix % 2 == 0: if ix % 2 == 0:
yield State.increment_arbitrary(1) yield State.increment_arbitrary(1) # type: ignore
else: else:
yield State.increment() yield State.increment() # type: ignore
await asyncio.sleep(0.005) await asyncio.sleep(0.005)
@rx.event
def increment(self): def increment(self):
self.counter += 1 self.counter += 1
@rx.event(background=True) @rx.background
async def increment_arbitrary(self, amount: int): async def increment_arbitrary(self, amount: int):
async with self: async with self:
self.counter += int(amount) self.counter += int(amount)
@rx.event
def reset_counter(self): def reset_counter(self):
self.counter = 0 self.counter = 0
@rx.event
async def blocking_pause(self): async def blocking_pause(self):
await asyncio.sleep(0.02) await asyncio.sleep(0.02)
@rx.event(background=True) @rx.background
async def non_blocking_pause(self): async def non_blocking_pause(self):
await asyncio.sleep(0.02) await asyncio.sleep(0.02)
@ -75,13 +68,13 @@ def BackgroundTask():
self.counter += 1 self.counter += 1
await asyncio.sleep(0.005) await asyncio.sleep(0.005)
@rx.event(background=True) @rx.background
async def handle_racy_event(self): async def handle_racy_event(self):
await asyncio.gather( await asyncio.gather(
self.racy_task(), self.racy_task(), self.racy_task(), self.racy_task() self.racy_task(), self.racy_task(), self.racy_task(), self.racy_task()
) )
@rx.event(background=True) @rx.background
async def nested_async_with_self(self): async def nested_async_with_self(self):
async with self: async with self:
self.counter += 1 self.counter += 1
@ -93,7 +86,7 @@ def BackgroundTask():
third_state = await self.get_state(ThirdState) third_state = await self.get_state(ThirdState)
await third_state._triple_count() await third_state._triple_count()
@rx.event(background=True) @rx.background
async def yield_in_async_with_self(self): async def yield_in_async_with_self(self):
async with self: async with self:
self.counter += 1 self.counter += 1
@ -101,7 +94,7 @@ def BackgroundTask():
self.counter += 1 self.counter += 1
class OtherState(rx.State): class OtherState(rx.State):
@rx.event(background=True) @rx.background
async def get_other_state(self): async def get_other_state(self):
async with self: async with self:
state = await self.get_state(State) state = await self.get_state(State)
@ -129,8 +122,8 @@ def BackgroundTask():
rx.input( rx.input(
id="iterations", id="iterations",
placeholder="Iterations", placeholder="Iterations",
value=State.iterations.to_string(), value=State.iterations.to_string(), # type: ignore
on_change=State.set_iterations, on_change=State.set_iterations, # type: ignore
), ),
rx.button( rx.button(
"Delayed Increment", "Delayed Increment",
@ -176,7 +169,7 @@ def BackgroundTask():
rx.button("Reset", on_click=State.reset_counter, id="reset"), rx.button("Reset", on_click=State.reset_counter, id="reset"),
) )
app = rx.App(_state=rx.State) app = rx.App(state=rx.State)
app.add_page(index) app.add_page(index)
@ -193,8 +186,8 @@ def background_task(
running AppHarness instance running AppHarness instance
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path_factory.mktemp("background_task"), root=tmp_path_factory.mktemp(f"background_task"),
app_source=BackgroundTask, app_source=BackgroundTask, # type: ignore
) as harness: ) as harness:
yield harness yield harness
@ -292,7 +285,7 @@ def test_background_task(
assert background_task._poll_for(lambda: counter.text == "620", timeout=40) assert background_task._poll_for(lambda: counter.text == "620", timeout=40)
# all tasks should have exited and cleaned up # all tasks should have exited and cleaned up
assert background_task._poll_for( assert background_task._poll_for(
lambda: not background_task.app_instance._background_tasks # pyright: ignore [reportOptionalMemberAccess] lambda: not background_task.app_instance.background_tasks # type: ignore
) )

View File

@ -15,8 +15,7 @@ from .utils import SessionStorage
def CallScript(): def CallScript():
"""A test app for browser javascript integration.""" """A test app for browser javascript integration."""
from pathlib import Path from typing import Dict, List, Optional, Union
from typing import Optional, Union
import reflex as rx import reflex as rx
@ -43,32 +42,26 @@ def CallScript():
external_scripts = inline_scripts.replace("inline", "external") external_scripts = inline_scripts.replace("inline", "external")
class CallScriptState(rx.State): class CallScriptState(rx.State):
results: rx.Field[list[Optional[Union[str, dict, list]]]] = rx.field([]) results: List[Optional[Union[str, Dict, List]]] = []
inline_counter: rx.Field[int] = rx.field(0) inline_counter: int = 0
external_counter: rx.Field[int] = rx.field(0) external_counter: int = 0
value: str = "Initial" value: str = "Initial"
last_result: int = 0
@rx.event
def call_script_callback(self, result): def call_script_callback(self, result):
self.results.append(result) self.results.append(result)
@rx.event
def call_script_callback_other_arg(self, result, other_arg): def call_script_callback_other_arg(self, result, other_arg):
self.results.append([other_arg, result]) self.results.append([other_arg, result])
@rx.event
def call_scripts_inline_yield(self): def call_scripts_inline_yield(self):
yield rx.call_script("inline1()") yield rx.call_script("inline1()")
yield rx.call_script("inline2()") yield rx.call_script("inline2()")
yield rx.call_script("inline3()") yield rx.call_script("inline3()")
yield rx.call_script("inline4()") yield rx.call_script("inline4()")
@rx.event
def call_script_inline_return(self): def call_script_inline_return(self):
return rx.call_script("inline2()") return rx.call_script("inline2()")
@rx.event
def call_scripts_inline_yield_callback(self): def call_scripts_inline_yield_callback(self):
yield rx.call_script( yield rx.call_script(
"inline1()", callback=CallScriptState.call_script_callback "inline1()", callback=CallScriptState.call_script_callback
@ -83,40 +76,34 @@ def CallScript():
"inline4()", callback=CallScriptState.call_script_callback "inline4()", callback=CallScriptState.call_script_callback
) )
@rx.event
def call_script_inline_return_callback(self): def call_script_inline_return_callback(self):
return rx.call_script( return rx.call_script(
"inline3()", callback=CallScriptState.call_script_callback "inline3()", callback=CallScriptState.call_script_callback
) )
@rx.event
def call_script_inline_return_lambda(self): def call_script_inline_return_lambda(self):
return rx.call_script( return rx.call_script(
"inline2()", "inline2()",
callback=lambda result: CallScriptState.call_script_callback_other_arg( callback=lambda result: CallScriptState.call_script_callback_other_arg( # type: ignore
result, "lambda" result, "lambda"
), ),
) )
@rx.event
def get_inline_counter(self): def get_inline_counter(self):
return rx.call_script( return rx.call_script(
"inline_counter", "inline_counter",
callback=CallScriptState.setvar("inline_counter"), callback=CallScriptState.set_inline_counter, # type: ignore
) )
@rx.event
def call_scripts_external_yield(self): def call_scripts_external_yield(self):
yield rx.call_script("external1()") yield rx.call_script("external1()")
yield rx.call_script("external2()") yield rx.call_script("external2()")
yield rx.call_script("external3()") yield rx.call_script("external3()")
yield rx.call_script("external4()") yield rx.call_script("external4()")
@rx.event
def call_script_external_return(self): def call_script_external_return(self):
return rx.call_script("external2()") return rx.call_script("external2()")
@rx.event
def call_scripts_external_yield_callback(self): def call_scripts_external_yield_callback(self):
yield rx.call_script( yield rx.call_script(
"external1()", callback=CallScriptState.call_script_callback "external1()", callback=CallScriptState.call_script_callback
@ -131,81 +118,48 @@ def CallScript():
"external4()", callback=CallScriptState.call_script_callback "external4()", callback=CallScriptState.call_script_callback
) )
@rx.event
def call_script_external_return_callback(self): def call_script_external_return_callback(self):
return rx.call_script( return rx.call_script(
"external3()", callback=CallScriptState.call_script_callback "external3()", callback=CallScriptState.call_script_callback
) )
@rx.event
def call_script_external_return_lambda(self): def call_script_external_return_lambda(self):
return rx.call_script( return rx.call_script(
"external2()", "external2()",
callback=lambda result: CallScriptState.call_script_callback_other_arg( callback=lambda result: CallScriptState.call_script_callback_other_arg( # type: ignore
result, "lambda" result, "lambda"
), ),
) )
@rx.event
def get_external_counter(self): def get_external_counter(self):
return rx.call_script( return rx.call_script(
"external_counter", "external_counter",
callback=CallScriptState.setvar("external_counter"), callback=CallScriptState.set_external_counter, # type: ignore
) )
@rx.event
def call_with_var_f_string(self):
return rx.call_script(
f"{rx.Var('inline_counter')} + {rx.Var('external_counter')}",
callback=CallScriptState.setvar("last_result"),
)
@rx.event
def call_with_var_str_cast(self):
return rx.call_script(
f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}",
callback=CallScriptState.setvar("last_result"),
)
@rx.event
def call_with_var_f_string_wrapped(self):
return rx.call_script(
rx.Var(f"{rx.Var('inline_counter')} + {rx.Var('external_counter')}"),
callback=CallScriptState.setvar("last_result"),
)
@rx.event
def call_with_var_str_cast_wrapped(self):
return rx.call_script(
rx.Var(
f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}"
),
callback=CallScriptState.setvar("last_result"),
)
@rx.event
def reset_(self): def reset_(self):
yield rx.call_script("inline_counter = 0; external_counter = 0") yield rx.call_script("inline_counter = 0; external_counter = 0")
self.reset() self.reset()
app = rx.App(_state=rx.State) app = rx.App(state=rx.State)
Path("assets/external.js").write_text(external_scripts) with open("assets/external.js", "w") as f:
f.write(external_scripts)
@app.add_page @app.add_page
def index(): def index():
return rx.vstack( return rx.vstack(
rx.input( rx.input(
value=CallScriptState.inline_counter.to(str), value=CallScriptState.inline_counter.to(str), # type: ignore
id="inline_counter", id="inline_counter",
read_only=True, read_only=True,
), ),
rx.input( rx.input(
value=CallScriptState.external_counter.to(str), value=CallScriptState.external_counter.to(str), # type: ignore
id="external_counter", id="external_counter",
read_only=True, read_only=True,
), ),
rx.text_area( rx.text_area(
value=CallScriptState.results.to_string(), value=CallScriptState.results.to_string(), # type: ignore
id="results", id="results",
read_only=True, read_only=True,
), ),
@ -275,73 +229,11 @@ def CallScript():
CallScriptState.value, CallScriptState.value,
on_click=rx.call_script( on_click=rx.call_script(
"'updated'", "'updated'",
callback=CallScriptState.setvar("value"), callback=CallScriptState.set_value, # type: ignore
), ),
id="update_value", id="update_value",
), ),
rx.button("Reset", id="reset", on_click=CallScriptState.reset_), rx.button("Reset", id="reset", on_click=CallScriptState.reset_),
rx.input(
value=CallScriptState.last_result,
id="last_result",
read_only=True,
on_click=CallScriptState.setvar("last_result", 0),
),
rx.button(
"call_with_var_f_string",
on_click=CallScriptState.call_with_var_f_string,
id="call_with_var_f_string",
),
rx.button(
"call_with_var_str_cast",
on_click=CallScriptState.call_with_var_str_cast,
id="call_with_var_str_cast",
),
rx.button(
"call_with_var_f_string_wrapped",
on_click=CallScriptState.call_with_var_f_string_wrapped,
id="call_with_var_f_string_wrapped",
),
rx.button(
"call_with_var_str_cast_wrapped",
on_click=CallScriptState.call_with_var_str_cast_wrapped,
id="call_with_var_str_cast_wrapped",
),
rx.button(
"call_with_var_f_string_inline",
on_click=rx.call_script(
f"{rx.Var('inline_counter')} + {CallScriptState.last_result}",
callback=CallScriptState.setvar("last_result"),
),
id="call_with_var_f_string_inline",
),
rx.button(
"call_with_var_str_cast_inline",
on_click=rx.call_script(
f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}",
callback=CallScriptState.setvar("last_result"),
),
id="call_with_var_str_cast_inline",
),
rx.button(
"call_with_var_f_string_wrapped_inline",
on_click=rx.call_script(
rx.Var(
f"{rx.Var('inline_counter')} + {CallScriptState.last_result}"
),
callback=CallScriptState.setvar("last_result"),
),
id="call_with_var_f_string_wrapped_inline",
),
rx.button(
"call_with_var_str_cast_wrapped_inline",
on_click=rx.call_script(
rx.Var(
f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}"
),
callback=CallScriptState.setvar("last_result"),
),
id="call_with_var_str_cast_wrapped_inline",
),
) )
@ -357,7 +249,7 @@ def call_script(tmp_path_factory) -> Generator[AppHarness, None, None]:
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path_factory.mktemp("call_script"), root=tmp_path_factory.mktemp("call_script"),
app_source=CallScript, app_source=CallScript, # type: ignore
) as harness: ) as harness:
yield harness yield harness
@ -471,73 +363,3 @@ def test_call_script(
call_script.poll_for_content(update_value_button, exp_not_equal="Initial") call_script.poll_for_content(update_value_button, exp_not_equal="Initial")
== "updated" == "updated"
) )
def test_call_script_w_var(
call_script: AppHarness,
driver: WebDriver,
):
"""Test evaluating javascript expressions containing Vars.
Args:
call_script: harness for CallScript app.
driver: WebDriver instance.
"""
assert_token(driver)
last_result = driver.find_element(By.ID, "last_result")
assert last_result.get_attribute("value") == "0"
inline_return_button = driver.find_element(By.ID, "inline_return")
call_with_var_f_string_button = driver.find_element(By.ID, "call_with_var_f_string")
call_with_var_str_cast_button = driver.find_element(By.ID, "call_with_var_str_cast")
call_with_var_f_string_wrapped_button = driver.find_element(
By.ID, "call_with_var_f_string_wrapped"
)
call_with_var_str_cast_wrapped_button = driver.find_element(
By.ID, "call_with_var_str_cast_wrapped"
)
call_with_var_f_string_inline_button = driver.find_element(
By.ID, "call_with_var_f_string_inline"
)
call_with_var_str_cast_inline_button = driver.find_element(
By.ID, "call_with_var_str_cast_inline"
)
call_with_var_f_string_wrapped_inline_button = driver.find_element(
By.ID, "call_with_var_f_string_wrapped_inline"
)
call_with_var_str_cast_wrapped_inline_button = driver.find_element(
By.ID, "call_with_var_str_cast_wrapped_inline"
)
inline_return_button.click()
call_with_var_f_string_button.click()
assert call_script.poll_for_value(last_result, exp_not_equal="") == "1"
inline_return_button.click()
call_with_var_str_cast_button.click()
assert call_script.poll_for_value(last_result, exp_not_equal="1") == "2"
inline_return_button.click()
call_with_var_f_string_wrapped_button.click()
assert call_script.poll_for_value(last_result, exp_not_equal="2") == "3"
inline_return_button.click()
call_with_var_str_cast_wrapped_button.click()
assert call_script.poll_for_value(last_result, exp_not_equal="3") == "4"
inline_return_button.click()
call_with_var_f_string_inline_button.click()
assert call_script.poll_for_value(last_result, exp_not_equal="4") == "9"
inline_return_button.click()
call_with_var_str_cast_inline_button.click()
assert call_script.poll_for_value(last_result, exp_not_equal="9") == "6"
inline_return_button.click()
call_with_var_f_string_wrapped_inline_button.click()
assert call_script.poll_for_value(last_result, exp_not_equal="6") == "13"
inline_return_button.click()
call_with_var_str_cast_wrapped_inline_button.click()
assert call_script.poll_for_value(last_result, exp_not_equal="13") == "8"

View File

@ -10,13 +10,6 @@ from selenium.webdriver import Firefox
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.remote.webdriver import WebDriver
from reflex.state import (
State,
StateManagerDisk,
StateManagerMemory,
StateManagerRedis,
_substate_key,
)
from reflex.testing import AppHarness from reflex.testing import AppHarness
from . import utils from . import utils
@ -33,18 +26,18 @@ def ClientSide():
class ClientSideSubState(ClientSideState): class ClientSideSubState(ClientSideState):
# cookies with default settings # cookies with default settings
c1: str = rx.Cookie() c1: str = rx.Cookie()
c2: str = rx.Cookie("c2 default") c2: rx.Cookie = "c2 default" # type: ignore
# cookies with custom settings # cookies with custom settings
c3: str = rx.Cookie(max_age=2) # expires after 2 second c3: str = rx.Cookie(max_age=2) # expires after 2 second
c4: str = rx.Cookie(same_site="strict") c4: rx.Cookie = rx.Cookie(same_site="strict")
c5: str = rx.Cookie(path="/foo/") # only accessible on `/foo/` c5: str = rx.Cookie(path="/foo/") # only accessible on `/foo/`
c6: str = rx.Cookie(name="c6") c6: str = rx.Cookie(name="c6")
c7: str = rx.Cookie("c7 default") c7: str = rx.Cookie("c7 default")
# local storage with default settings # local storage with default settings
l1: str = rx.LocalStorage() l1: str = rx.LocalStorage()
l2: str = rx.LocalStorage("l2 default") l2: rx.LocalStorage = "l2 default" # type: ignore
# local storage with custom settings # local storage with custom settings
l3: str = rx.LocalStorage(name="l3") l3: str = rx.LocalStorage(name="l3")
@ -56,13 +49,12 @@ def ClientSide():
# Session storage # Session storage
s1: str = rx.SessionStorage() s1: str = rx.SessionStorage()
s2: str = rx.SessionStorage("s2 default") s2: rx.SessionStorage = "s2 default" # type: ignore
s3: str = rx.SessionStorage(name="s3") s3: str = rx.SessionStorage(name="s3")
def set_l6(self, my_param: str): def set_l6(self, my_param: str):
self.l6 = my_param self.l6 = my_param
@rx.event
def set_var(self): def set_var(self):
setattr(self, self.state_var, self.input_value) setattr(self, self.state_var, self.input_value)
self.state_var = self.input_value = "" self.state_var = self.input_value = ""
@ -72,7 +64,6 @@ def ClientSide():
l1s: str = rx.LocalStorage() l1s: str = rx.LocalStorage()
s1s: str = rx.SessionStorage() s1s: str = rx.SessionStorage()
@rx.event
def set_var(self): def set_var(self):
setattr(self, self.state_var, self.input_value) setattr(self, self.state_var, self.input_value)
self.state_var = self.input_value = "" self.state_var = self.input_value = ""
@ -81,19 +72,19 @@ def ClientSide():
return rx.fragment( return rx.fragment(
rx.input( rx.input(
value=ClientSideState.router.session.client_token, value=ClientSideState.router.session.client_token,
read_only=True, is_read_only=True,
id="token", id="token",
), ),
rx.input( rx.input(
placeholder="state var", placeholder="state var",
value=ClientSideState.state_var, value=ClientSideState.state_var,
on_change=ClientSideState.setvar("state_var"), on_change=ClientSideState.set_state_var, # type: ignore
id="state_var", id="state_var",
), ),
rx.input( rx.input(
placeholder="input value", placeholder="input value",
value=ClientSideState.input_value, value=ClientSideState.input_value,
on_change=ClientSideState.setvar("input_value"), on_change=ClientSideState.set_input_value, # type: ignore
id="input_value", id="input_value",
), ),
rx.button( rx.button(
@ -127,7 +118,7 @@ def ClientSide():
rx.box(ClientSideSubSubState.s1s, id="s1s"), rx.box(ClientSideSubSubState.s1s, id="s1s"),
) )
app = rx.App(_state=rx.State) app = rx.App(state=rx.State)
app.add_page(index) app.add_page(index)
app.add_page(index, route="/foo") app.add_page(index, route="/foo")
@ -144,7 +135,7 @@ def client_side(tmp_path_factory) -> Generator[AppHarness, None, None]:
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path_factory.mktemp("client_side"), root=tmp_path_factory.mktemp("client_side"),
app_source=ClientSide, app_source=ClientSide, # type: ignore
) as harness: ) as harness:
yield harness yield harness
@ -321,7 +312,6 @@ async def test_client_side_state(
assert not driver.get_cookies() assert not driver.get_cookies()
local_storage_items = local_storage.items() local_storage_items = local_storage.items()
local_storage_items.pop("last_compiled_time", None) local_storage_items.pop("last_compiled_time", None)
local_storage_items.pop("theme", None)
assert not local_storage_items assert not local_storage_items
# set some cookies and local storage values # set some cookies and local storage values
@ -437,7 +427,6 @@ async def test_client_side_state(
local_storage_items = local_storage.items() local_storage_items = local_storage.items()
local_storage_items.pop("last_compiled_time", None) local_storage_items.pop("last_compiled_time", None)
local_storage_items.pop("theme", None)
assert local_storage_items.pop(f"{sub_state_name}.l1") == "l1 value" assert local_storage_items.pop(f"{sub_state_name}.l1") == "l1 value"
assert local_storage_items.pop(f"{sub_state_name}.l2") == "l2 value" assert local_storage_items.pop(f"{sub_state_name}.l2") == "l2 value"
assert local_storage_items.pop("l3") == "l3 value" assert local_storage_items.pop("l3") == "l3 value"
@ -613,109 +602,6 @@ async def test_client_side_state(
assert s2.text == "s2 value" assert s2.text == "s2 value"
assert s3.text == "s3 value" assert s3.text == "s3 value"
# Simulate state expiration
if isinstance(client_side.state_manager, StateManagerRedis):
await client_side.state_manager.redis.delete(
_substate_key(token, State.get_full_name())
)
await client_side.state_manager.redis.delete(_substate_key(token, state_name))
await client_side.state_manager.redis.delete(
_substate_key(token, sub_state_name)
)
await client_side.state_manager.redis.delete(
_substate_key(token, sub_sub_state_name)
)
elif isinstance(client_side.state_manager, (StateManagerMemory, StateManagerDisk)):
del client_side.state_manager.states[token]
if isinstance(client_side.state_manager, StateManagerDisk):
client_side.state_manager.token_expiration = 0
client_side.state_manager._purge_expired_states()
# Ensure the state is gone (not hydrated)
async def poll_for_not_hydrated():
state = await client_side.get_state(_substate_key(token or "", state_name))
return not state.is_hydrated
assert await AppHarness._poll_for_async(poll_for_not_hydrated)
# Trigger event to get a new instance of the state since the old was expired.
set_sub("c1", "c1 post expire")
# get new references to all cookie and local storage elements (again)
c1 = driver.find_element(By.ID, "c1")
c2 = driver.find_element(By.ID, "c2")
c3 = driver.find_element(By.ID, "c3")
c4 = driver.find_element(By.ID, "c4")
c5 = driver.find_element(By.ID, "c5")
c6 = driver.find_element(By.ID, "c6")
c7 = driver.find_element(By.ID, "c7")
l1 = driver.find_element(By.ID, "l1")
l2 = driver.find_element(By.ID, "l2")
l3 = driver.find_element(By.ID, "l3")
l4 = driver.find_element(By.ID, "l4")
s1 = driver.find_element(By.ID, "s1")
s2 = driver.find_element(By.ID, "s2")
s3 = driver.find_element(By.ID, "s3")
c1s = driver.find_element(By.ID, "c1s")
l1s = driver.find_element(By.ID, "l1s")
s1s = driver.find_element(By.ID, "s1s")
assert c1.text == "c1 post expire"
assert c2.text == "c2 value"
assert c3.text == "" # temporary cookie expired after reset state!
assert c4.text == "c4 value"
assert c5.text == "c5 value"
assert c6.text == "c6 value"
assert c7.text == "c7 value"
assert l1.text == "l1 value"
assert l2.text == "l2 value"
assert l3.text == "l3 value"
assert l4.text == "l4 value"
assert s1.text == "s1 value"
assert s2.text == "s2 value"
assert s3.text == "s3 value"
assert c1s.text == "c1s value"
assert l1s.text == "l1s value"
assert s1s.text == "s1s value"
# Get the backend state and ensure the values are still set
async def get_sub_state():
root_state = await client_side.get_state(
_substate_key(token or "", sub_state_name)
)
state = root_state.substates[client_side.get_state_name("_client_side_state")]
sub_state = state.substates[
client_side.get_state_name("_client_side_sub_state")
]
return sub_state
async def poll_for_c1_set():
sub_state = await get_sub_state()
return sub_state.c1 == "c1 post expire"
assert await AppHarness._poll_for_async(poll_for_c1_set)
sub_state = await get_sub_state()
assert sub_state.c1 == "c1 post expire"
assert sub_state.c2 == "c2 value"
assert sub_state.c3 == ""
assert sub_state.c4 == "c4 value"
assert sub_state.c5 == "c5 value"
assert sub_state.c6 == "c6 value"
assert sub_state.c7 == "c7 value"
assert sub_state.l1 == "l1 value"
assert sub_state.l2 == "l2 value"
assert sub_state.l3 == "l3 value"
assert sub_state.l4 == "l4 value"
assert sub_state.s1 == "s1 value"
assert sub_state.s2 == "s2 value"
assert sub_state.s3 == "s3 value"
sub_sub_state = sub_state.substates[
client_side.get_state_name("_client_side_sub_sub_state")
]
assert sub_sub_state.c1s == "c1s value"
assert sub_sub_state.l1s == "l1s value"
assert sub_sub_state.s1s == "s1s value"
# clear the cookie jar and local storage, ensure state reset to default # clear the cookie jar and local storage, ensure state reset to default
driver.delete_all_cookies() driver.delete_all_cookies()
local_storage.clear() local_storage.clear()

View File

@ -0,0 +1,107 @@
"""Test that per-component state scaffold works and operates independently."""
from typing import Generator
import pytest
from selenium.webdriver.common.by import By
from reflex.testing import AppHarness
from . import utils
def ComponentStateApp():
"""App using per component state."""
import reflex as rx
class MultiCounter(rx.ComponentState):
count: int = 0
def increment(self):
self.count += 1
@classmethod
def get_component(cls, *children, **props):
return rx.vstack(
*children,
rx.heading(cls.count, id=f"count-{props.get('id', 'default')}"),
rx.button(
"Increment",
on_click=cls.increment,
id=f"button-{props.get('id', 'default')}",
),
**props,
)
app = rx.App(state=rx.State) # noqa
@rx.page()
def index():
mc_a = MultiCounter.create(id="a")
mc_b = MultiCounter.create(id="b")
assert mc_a.State != mc_b.State
return rx.vstack(
mc_a,
mc_b,
rx.button(
"Inc A",
on_click=mc_a.State.increment, # type: ignore
id="inc-a",
),
)
@pytest.fixture()
def component_state_app(tmp_path) -> Generator[AppHarness, None, None]:
"""Start ComponentStateApp app at tmp_path via AppHarness.
Args:
tmp_path: pytest tmp_path fixture
Yields:
running AppHarness instance
"""
with AppHarness.create(
root=tmp_path,
app_source=ComponentStateApp, # type: ignore
) as harness:
yield harness
@pytest.mark.asyncio
async def test_component_state_app(component_state_app: AppHarness):
"""Increment counters independently.
Args:
component_state_app: harness for ComponentStateApp app
"""
assert component_state_app.app_instance is not None, "app is not running"
driver = component_state_app.frontend()
ss = utils.SessionStorage(driver)
assert AppHarness._poll_for(lambda: ss.get("token") is not None), "token not found"
count_a = driver.find_element(By.ID, "count-a")
count_b = driver.find_element(By.ID, "count-b")
button_a = driver.find_element(By.ID, "button-a")
button_b = driver.find_element(By.ID, "button-b")
button_inc_a = driver.find_element(By.ID, "inc-a")
assert count_a.text == "0"
button_a.click()
assert component_state_app.poll_for_content(count_a, exp_not_equal="0") == "1"
button_a.click()
assert component_state_app.poll_for_content(count_a, exp_not_equal="1") == "2"
button_inc_a.click()
assert component_state_app.poll_for_content(count_a, exp_not_equal="2") == "3"
assert count_b.text == "0"
button_b.click()
assert component_state_app.poll_for_content(count_b, exp_not_equal="0") == "1"
button_b.click()
assert component_state_app.poll_for_content(count_b, exp_not_equal="1") == "2"

View File

@ -22,22 +22,22 @@ def ComputedVars():
count: int = 0 count: int = 0
# cached var with dep on count # cached var with dep on count
@rx.var(interval=15) @rx.var(cache=True, interval=15)
def count1(self) -> int: def count1(self) -> int:
return self.count return self.count
# cached backend var with dep on count # cached backend var with dep on count
@rx.var(interval=15, backend=True) @rx.var(cache=True, interval=15, backend=True)
def count1_backend(self) -> int: def count1_backend(self) -> int:
return self.count return self.count
# same as above but implicit backend with `_` prefix # same as above but implicit backend with `_` prefix
@rx.var(interval=15) @rx.var(cache=True, interval=15)
def _count1_backend(self) -> int: def _count1_backend(self) -> int:
return self.count return self.count
# explicit disabled auto_deps # explicit disabled auto_deps
@rx.var(interval=15, auto_deps=False) @rx.var(interval=15, cache=True, auto_deps=False)
def count3(self) -> int: def count3(self) -> int:
# this will not add deps, because auto_deps is False # this will not add deps, because auto_deps is False
print(self.count1) print(self.count1)
@ -45,32 +45,22 @@ def ComputedVars():
return self.count return self.count
# explicit dependency on count var # explicit dependency on count var
@rx.var(deps=["count"], auto_deps=False) @rx.var(cache=True, deps=["count"], auto_deps=False)
def depends_on_count(self) -> int: def depends_on_count(self) -> int:
return self.count return self.count
# explicit dependency on count1 var # explicit dependency on count1 var
@rx.var(deps=[count1], auto_deps=False) @rx.var(cache=True, deps=[count1], auto_deps=False)
def depends_on_count1(self) -> int: def depends_on_count1(self) -> int:
return self.count return self.count
@rx.var( @rx.var(deps=[count3], auto_deps=False, cache=True)
deps=[count3],
auto_deps=False,
)
def depends_on_count3(self) -> int: def depends_on_count3(self) -> int:
return self.count return self.count
# special floats should be properly decoded on the frontend
@rx.var(cache=True, initial_value=[])
def special_floats(self) -> list[float]:
return [42.9, float("nan"), float("inf"), float("-inf")]
@rx.event
def increment(self): def increment(self):
self.count += 1 self.count += 1
@rx.event
def mark_dirty(self): def mark_dirty(self):
self._mark_dirty() self._mark_dirty()
@ -111,14 +101,10 @@ def ComputedVars():
State.depends_on_count3, State.depends_on_count3,
id="depends_on_count3", id="depends_on_count3",
), ),
rx.text("special_floats:"),
rx.text(
State.special_floats.join(", "),
id="special_floats",
),
), ),
) )
# raise Exception(State.count3._deps(objclass=State))
app = rx.App() app = rx.App()
app.add_page(index) app.add_page(index)
@ -136,8 +122,8 @@ def computed_vars(
running AppHarness instance running AppHarness instance
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path_factory.mktemp("computed_vars"), root=tmp_path_factory.mktemp(f"computed_vars"),
app_source=ComputedVars, app_source=ComputedVars, # type: ignore
) as harness: ) as harness:
yield harness yield harness
@ -237,10 +223,6 @@ async def test_computed_vars(
assert depends_on_count3 assert depends_on_count3
assert depends_on_count3.text == "0" assert depends_on_count3.text == "0"
special_floats = driver.find_element(By.ID, "special_floats")
assert special_floats
assert special_floats.text == "42.9, NaN, Infinity, -Infinity"
increment = driver.find_element(By.ID, "increment") increment = driver.find_element(By.ID, "increment")
assert increment.is_enabled() assert increment.is_enabled()

View File

@ -1,14 +1,11 @@
"""Test case for displaying the connection banner when the websocket drops.""" """Test case for displaying the connection banner when the websocket drops."""
import functools
from typing import Generator from typing import Generator
import pytest import pytest
from selenium.common.exceptions import NoSuchElementException from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from reflex import constants
from reflex.config import environment
from reflex.testing import AppHarness, WebDriver from reflex.testing import AppHarness, WebDriver
from .utils import SessionStorage from .utils import SessionStorage
@ -23,7 +20,6 @@ def ConnectionBanner():
class State(rx.State): class State(rx.State):
foo: int = 0 foo: int = 0
@rx.event
async def delay(self): async def delay(self):
await asyncio.sleep(5) await asyncio.sleep(5)
@ -34,55 +30,28 @@ def ConnectionBanner():
rx.button( rx.button(
"Increment", "Increment",
id="increment", id="increment",
on_click=State.set_foo(State.foo + 1), # pyright: ignore [reportAttributeAccessIssue] on_click=State.set_foo(State.foo + 1), # type: ignore
), ),
rx.button("Delay", id="delay", on_click=State.delay), rx.button("Delay", id="delay", on_click=State.delay),
) )
app = rx.App(_state=rx.State) app = rx.App(state=rx.State)
app.add_page(index) app.add_page(index)
@pytest.fixture(
params=[constants.CompileContext.RUN, constants.CompileContext.DEPLOY],
ids=["compile_context_run", "compile_context_deploy"],
)
def simulate_compile_context(request) -> constants.CompileContext:
"""Fixture to simulate reflex cloud deployment.
Args:
request: pytest request fixture.
Returns:
The context to run the app with.
"""
return request.param
@pytest.fixture() @pytest.fixture()
def connection_banner( def connection_banner(tmp_path) -> Generator[AppHarness, None, None]:
tmp_path,
simulate_compile_context: constants.CompileContext,
) -> Generator[AppHarness, None, None]:
"""Start ConnectionBanner app at tmp_path via AppHarness. """Start ConnectionBanner app at tmp_path via AppHarness.
Args: Args:
tmp_path: pytest tmp_path fixture tmp_path: pytest tmp_path fixture
simulate_compile_context: Which context to run the app with.
Yields: Yields:
running AppHarness instance running AppHarness instance
""" """
environment.REFLEX_COMPILE_CONTEXT.set(simulate_compile_context)
with AppHarness.create( with AppHarness.create(
root=tmp_path, root=tmp_path,
app_source=functools.partial(ConnectionBanner), app_source=ConnectionBanner, # type: ignore
app_name=(
"connection_banner_reflex_cloud"
if simulate_compile_context == constants.CompileContext.DEPLOY
else "connection_banner"
),
) as harness: ) as harness:
yield harness yield harness
@ -101,40 +70,9 @@ def has_error_modal(driver: WebDriver) -> bool:
""" """
try: try:
driver.find_element(By.XPATH, CONNECTION_ERROR_XPATH) driver.find_element(By.XPATH, CONNECTION_ERROR_XPATH)
return True
except NoSuchElementException: except NoSuchElementException:
return False return False
else:
return True
def has_cloud_banner(driver: WebDriver) -> bool:
"""Check if the cloud banner is displayed.
Args:
driver: Selenium webdriver instance.
Returns:
True if the banner is displayed, False otherwise.
"""
try:
driver.find_element(By.XPATH, "//*[ contains(text(), 'This app is paused') ]")
except NoSuchElementException:
return False
else:
return True
def _assert_token(connection_banner, driver):
"""Poll for backend to be up.
Args:
connection_banner: AppHarness instance.
driver: Selenium webdriver instance.
"""
ss = SessionStorage(driver)
assert connection_banner._poll_for(lambda: ss.get("token") is not None), (
"token not found"
)
@pytest.mark.asyncio @pytest.mark.asyncio
@ -148,7 +86,11 @@ async def test_connection_banner(connection_banner: AppHarness):
assert connection_banner.backend is not None assert connection_banner.backend is not None
driver = connection_banner.frontend() driver = connection_banner.frontend()
_assert_token(connection_banner, driver) ss = SessionStorage(driver)
assert connection_banner._poll_for(
lambda: ss.get("token") is not None
), "token not found"
assert connection_banner._poll_for(lambda: not has_error_modal(driver)) assert connection_banner._poll_for(lambda: not has_error_modal(driver))
delay_button = driver.find_element(By.ID, "delay") delay_button = driver.find_element(By.ID, "delay")
@ -188,36 +130,3 @@ async def test_connection_banner(connection_banner: AppHarness):
# Count should have incremented after coming back up # Count should have incremented after coming back up
assert connection_banner.poll_for_value(counter_element, exp_not_equal="1") == "2" assert connection_banner.poll_for_value(counter_element, exp_not_equal="1") == "2"
@pytest.mark.asyncio
async def test_cloud_banner(
connection_banner: AppHarness, simulate_compile_context: constants.CompileContext
):
"""Test that the connection banner is displayed when the websocket drops.
Args:
connection_banner: AppHarness instance.
simulate_compile_context: Which context to set for the app.
"""
assert connection_banner.app_instance is not None
assert connection_banner.backend is not None
driver = connection_banner.frontend()
driver.add_cookie({"name": "backend-enabled", "value": "truly"})
driver.refresh()
_assert_token(connection_banner, driver)
assert connection_banner._poll_for(lambda: not has_cloud_banner(driver))
driver.add_cookie({"name": "backend-enabled", "value": "false"})
driver.refresh()
if simulate_compile_context == constants.CompileContext.DEPLOY:
assert connection_banner._poll_for(lambda: has_cloud_banner(driver))
else:
_assert_token(connection_banner, driver)
assert connection_banner._poll_for(lambda: not has_cloud_banner(driver))
driver.delete_cookie("backend-enabled")
driver.refresh()
_assert_token(connection_banner, driver)
assert connection_banner._poll_for(lambda: not has_cloud_banner(driver))

View File

@ -17,16 +17,15 @@ def DeployUrlSample() -> None:
import reflex as rx import reflex as rx
class State(rx.State): class State(rx.State):
@rx.event
def goto_self(self): def goto_self(self):
return rx.redirect(rx.config.get_config().deploy_url) # pyright: ignore [reportArgumentType] return rx.redirect(rx.config.get_config().deploy_url) # type: ignore
def index(): def index():
return rx.fragment( return rx.fragment(
rx.button("GOTO SELF", on_click=State.goto_self, id="goto_self") rx.button("GOTO SELF", on_click=State.goto_self, id="goto_self")
) )
app = rx.App(_state=rx.State) app = rx.App(state=rx.State)
app.add_page(index) app.add_page(index)
@ -44,7 +43,7 @@ def deploy_url_sample(
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path_factory.mktemp("deploy_url_sample"), root=tmp_path_factory.mktemp("deploy_url_sample"),
app_source=DeployUrlSample, app_source=DeployUrlSample, # type: ignore
) as harness: ) as harness:
yield harness yield harness

View File

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
import time
from typing import Callable, Coroutine, Generator, Type from typing import Callable, Coroutine, Generator, Type
from urllib.parse import urlsplit from urllib.parse import urlsplit
@ -23,18 +22,12 @@ def DynamicRoute():
class DynamicState(rx.State): class DynamicState(rx.State):
order: List[str] = [] order: List[str] = []
@rx.event
def on_load(self): def on_load(self):
page_data = f"{self.router.page.path}-{self.page_id or 'no page id'}" self.order.append(f"{self.router.page.path}-{self.page_id or 'no page id'}")
print(f"on_load: {page_data}")
self.order.append(page_data)
@rx.event
def on_load_redir(self): def on_load_redir(self):
query_params = self.router.page.params query_params = self.router.page.params
page_data = f"on_load_redir-{query_params}" self.order.append(f"on_load_redir-{query_params}")
print(f"on_load_redir: {page_data}")
self.order.append(page_data)
return rx.redirect(f"/page/{query_params['page_id']}") return rx.redirect(f"/page/{query_params['page_id']}")
@rx.var @rx.var
@ -48,13 +41,13 @@ def DynamicRoute():
return rx.fragment( return rx.fragment(
rx.input( rx.input(
value=DynamicState.router.session.client_token, value=DynamicState.router.session.client_token,
read_only=True, is_read_only=True,
id="token", id="token",
), ),
rx.input(value=rx.State.page_id, read_only=True, id="page_id"), # pyright: ignore [reportAttributeAccessIssue] rx.input(value=rx.State.page_id, is_read_only=True, id="page_id"), # type: ignore
rx.input( rx.input(
value=DynamicState.router.page.raw_path, value=DynamicState.router.page.raw_path,
read_only=True, is_read_only=True,
id="raw_path", id="raw_path",
), ),
rx.link("index", href="/", id="link_index"), rx.link("index", href="/", id="link_index"),
@ -62,89 +55,26 @@ def DynamicRoute():
rx.link( rx.link(
"next", "next",
href="/page/" + DynamicState.next_page, href="/page/" + DynamicState.next_page,
id="link_page_next", id="link_page_next", # type: ignore
), ),
rx.link("missing", href="/missing", id="link_missing"), rx.link("missing", href="/missing", id="link_missing"),
rx.list( # pyright: ignore [reportAttributeAccessIssue] rx.list( # type: ignore
rx.foreach( rx.foreach(
DynamicState.order, # pyright: ignore [reportAttributeAccessIssue] DynamicState.order, # type: ignore
lambda i: rx.list_item(rx.text(i)), lambda i: rx.list_item(rx.text(i)),
), ),
), ),
) )
class ArgState(rx.State): @rx.page(route="/redirect-page/[page_id]", on_load=DynamicState.on_load_redir) # type: ignore
"""The app state."""
@rx.var(cache=False)
def arg(self) -> int:
return int(self.arg_str or 0)
class ArgSubState(ArgState):
@rx.var
def cached_arg(self) -> int:
return self.arg
@rx.var
def cached_arg_str(self) -> str:
return self.arg_str
@rx.page(route="/arg/[arg_str]")
def arg() -> rx.Component:
return rx.vstack(
rx.input(
value=DynamicState.router.session.client_token,
read_only=True,
id="token",
),
rx.data_list.root(
rx.data_list.item(
rx.data_list.label("rx.State.arg_str (dynamic)"),
rx.data_list.value(rx.State.arg_str, id="state-arg_str"), # pyright: ignore [reportAttributeAccessIssue]
),
rx.data_list.item(
rx.data_list.label("ArgState.arg_str (dynamic) (inherited)"),
rx.data_list.value(ArgState.arg_str, id="argstate-arg_str"), # pyright: ignore [reportAttributeAccessIssue]
),
rx.data_list.item(
rx.data_list.label("ArgState.arg"),
rx.data_list.value(ArgState.arg, id="argstate-arg"),
),
rx.data_list.item(
rx.data_list.label("ArgSubState.arg_str (dynamic) (inherited)"),
rx.data_list.value(ArgSubState.arg_str, id="argsubstate-arg_str"), # pyright: ignore [reportAttributeAccessIssue]
),
rx.data_list.item(
rx.data_list.label("ArgSubState.arg (inherited)"),
rx.data_list.value(ArgSubState.arg, id="argsubstate-arg"),
),
rx.data_list.item(
rx.data_list.label("ArgSubState.cached_arg"),
rx.data_list.value(
ArgSubState.cached_arg, id="argsubstate-cached_arg"
),
),
rx.data_list.item(
rx.data_list.label("ArgSubState.cached_arg_str"),
rx.data_list.value(
ArgSubState.cached_arg_str, id="argsubstate-cached_arg_str"
),
),
),
rx.link("+", href=f"/arg/{ArgState.arg + 1}", id="next-page"),
align="center",
height="100vh",
)
@rx.page(route="/redirect-page/[page_id]", on_load=DynamicState.on_load_redir)
def redirect_page(): def redirect_page():
return rx.fragment(rx.text("redirecting...")) return rx.fragment(rx.text("redirecting..."))
app = rx.App(_state=rx.State) app = rx.App(state=rx.State)
app.add_page(index, route="/page/[page_id]", on_load=DynamicState.on_load) app.add_page(index, route="/page/[page_id]", on_load=DynamicState.on_load) # type: ignore
app.add_page(index, route="/static/x", on_load=DynamicState.on_load) app.add_page(index, route="/static/x", on_load=DynamicState.on_load) # type: ignore
app.add_page(index) app.add_page(index)
app.add_custom_404_page(on_load=DynamicState.on_load) app.add_custom_404_page(on_load=DynamicState.on_load) # type: ignore
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@ -161,9 +91,9 @@ def dynamic_route(
running AppHarness instance running AppHarness instance
""" """
with app_harness_env.create( with app_harness_env.create(
root=tmp_path_factory.mktemp("dynamic_route"), root=tmp_path_factory.mktemp(f"dynamic_route"),
app_name=f"dynamicroute_{app_harness_env.__name__.lower()}", app_name=f"dynamicroute_{app_harness_env.__name__.lower()}",
app_source=DynamicRoute, app_source=DynamicRoute, # type: ignore
) as harness: ) as harness:
yield harness yield harness
@ -180,8 +110,6 @@ def driver(dynamic_route: AppHarness) -> Generator[WebDriver, None, None]:
""" """
assert dynamic_route.app_instance is not None, "app is not running" assert dynamic_route.app_instance is not None, "app is not running"
driver = dynamic_route.frontend() driver = dynamic_route.frontend()
# TODO: drop after flakiness is resolved
driver.implicitly_wait(30)
try: try:
yield driver yield driver
finally: finally:
@ -235,11 +163,8 @@ def poll_for_order(
dynamic_state_name dynamic_state_name
].order == exp_order ].order == exp_order
await AppHarness._poll_for_async(_check, timeout=60) await AppHarness._poll_for_async(_check)
assert ( assert (await _backend_state()).substates[dynamic_state_name].order == exp_order
list((await _backend_state()).substates[dynamic_state_name].order)
== exp_order
)
return _poll_for_order return _poll_for_order
@ -377,56 +302,3 @@ async def test_on_load_navigate_non_dynamic(
link.click() link.click()
assert urlsplit(driver.current_url).path == "/static/x/" assert urlsplit(driver.current_url).path == "/static/x/"
await poll_for_order(["/static/x-no page id", "/static/x-no page id"]) await poll_for_order(["/static/x-no page id", "/static/x-no page id"])
@pytest.mark.asyncio
async def test_render_dynamic_arg(
dynamic_route: AppHarness,
driver: WebDriver,
token: str,
):
"""Assert that dynamic arg var is rendered correctly in different contexts.
Args:
dynamic_route: harness for DynamicRoute app.
driver: WebDriver instance.
token: The token visible in the driver browser.
"""
assert dynamic_route.app_instance is not None
with poll_for_navigation(driver):
driver.get(f"{dynamic_route.frontend_url}/arg/0")
# TODO: drop after flakiness is resolved
time.sleep(3)
def assert_content(expected: str, expect_not: str):
ids = [
"state-arg_str",
"argstate-arg",
"argstate-arg_str",
"argsubstate-arg_str",
"argsubstate-arg",
"argsubstate-cached_arg",
"argsubstate-cached_arg_str",
]
for id in ids:
el = driver.find_element(By.ID, id)
assert el
assert (
dynamic_route.poll_for_content(el, timeout=30, exp_not_equal=expect_not)
== expected
)
assert_content("0", "")
next_page_link = driver.find_element(By.ID, "next-page")
assert next_page_link
with poll_for_navigation(driver):
next_page_link.click()
assert driver.current_url == f"{dynamic_route.frontend_url}/arg/1/"
assert_content("1", "0")
next_page_link = driver.find_element(By.ID, "next-page")
assert next_page_link
with poll_for_navigation(driver):
next_page_link.click()
assert driver.current_url == f"{dynamic_route.frontend_url}/arg/2/"
assert_content("2", "1")

View File

@ -24,7 +24,6 @@ def TestEventAction():
def on_click(self, ev): def on_click(self, ev):
self.order.append(f"on_click:{ev}") self.order.append(f"on_click:{ev}")
@rx.event
def on_click2(self): def on_click2(self):
self.order.append("on_click2") self.order.append("on_click2")
@ -63,16 +62,16 @@ def TestEventAction():
rx.button( rx.button(
"Stop Prop Only", "Stop Prop Only",
id="btn-stop-prop-only", id="btn-stop-prop-only",
on_click=rx.stop_propagation, # pyright: ignore [reportArgumentType] on_click=rx.stop_propagation, # type: ignore
), ),
rx.button( rx.button(
"Click event", "Click event",
on_click=EventActionState.on_click("no_event_actions"), # pyright: ignore [reportCallIssue] on_click=EventActionState.on_click("no_event_actions"), # type: ignore
id="btn-click-event", id="btn-click-event",
), ),
rx.button( rx.button(
"Click stop propagation", "Click stop propagation",
on_click=EventActionState.on_click("stop_propagation").stop_propagation, # pyright: ignore [reportCallIssue] on_click=EventActionState.on_click("stop_propagation").stop_propagation, # type: ignore
id="btn-click-stop-propagation", id="btn-click-stop-propagation",
), ),
rx.button( rx.button(
@ -88,13 +87,13 @@ def TestEventAction():
rx.link( rx.link(
"Link", "Link",
href="#", href="#",
on_click=EventActionState.on_click("link_no_event_actions"), # pyright: ignore [reportCallIssue] on_click=EventActionState.on_click("link_no_event_actions"), # type: ignore
id="link", id="link",
), ),
rx.link( rx.link(
"Link Stop Propagation", "Link Stop Propagation",
href="#", href="#",
on_click=EventActionState.on_click( # pyright: ignore [reportCallIssue] on_click=EventActionState.on_click( # type: ignore
"link_stop_propagation" "link_stop_propagation"
).stop_propagation, ).stop_propagation,
id="link-stop-propagation", id="link-stop-propagation",
@ -102,13 +101,13 @@ def TestEventAction():
rx.link( rx.link(
"Link Prevent Default Only", "Link Prevent Default Only",
href="/invalid", href="/invalid",
on_click=rx.prevent_default, # pyright: ignore [reportArgumentType] on_click=rx.prevent_default, # type: ignore
id="link-prevent-default-only", id="link-prevent-default-only",
), ),
rx.link( rx.link(
"Link Prevent Default", "Link Prevent Default",
href="/invalid", href="/invalid",
on_click=EventActionState.on_click( # pyright: ignore [reportCallIssue] on_click=EventActionState.on_click( # type: ignore
"link_prevent_default" "link_prevent_default"
).prevent_default, ).prevent_default,
id="link-prevent-default", id="link-prevent-default",
@ -116,47 +115,47 @@ def TestEventAction():
rx.link( rx.link(
"Link Both", "Link Both",
href="/invalid", href="/invalid",
on_click=EventActionState.on_click( # pyright: ignore [reportCallIssue] on_click=EventActionState.on_click( # type: ignore
"link_both" "link_both"
).stop_propagation.prevent_default, ).stop_propagation.prevent_default,
id="link-stop-propagation-prevent-default", id="link-stop-propagation-prevent-default",
), ),
EventFiringComponent.create( EventFiringComponent.create(
id="custom-stop-propagation", id="custom-stop-propagation",
on_click=EventActionState.on_click( # pyright: ignore [reportCallIssue] on_click=EventActionState.on_click( # type: ignore
"custom-stop-propagation" "custom-stop-propagation"
).stop_propagation, ).stop_propagation,
), ),
EventFiringComponent.create( EventFiringComponent.create(
id="custom-prevent-default", id="custom-prevent-default",
on_click=EventActionState.on_click( # pyright: ignore [reportCallIssue] on_click=EventActionState.on_click( # type: ignore
"custom-prevent-default" "custom-prevent-default"
).prevent_default, ).prevent_default,
), ),
rx.button( rx.button(
"Throttle", "Throttle",
id="btn-throttle", id="btn-throttle",
on_click=lambda: EventActionState.on_click_throttle.throttle( # pyright: ignore [reportFunctionMemberAccess] on_click=lambda: EventActionState.on_click_throttle.throttle(
200 200
).stop_propagation, ).stop_propagation,
), ),
rx.button( rx.button(
"Debounce", "Debounce",
id="btn-debounce", id="btn-debounce",
on_click=EventActionState.on_click_debounce.debounce( # pyright: ignore [reportFunctionMemberAccess] on_click=EventActionState.on_click_debounce.debounce(
200 200
).stop_propagation, ).stop_propagation,
), ),
rx.list( # pyright: ignore [reportAttributeAccessIssue] rx.list( # type: ignore
rx.foreach( rx.foreach(
EventActionState.order, EventActionState.order, # type: ignore
rx.list_item, rx.list_item,
), ),
), ),
on_click=EventActionState.on_click("outer"), # pyright: ignore [reportCallIssue] on_click=EventActionState.on_click("outer"), # type: ignore
) )
app = rx.App(_state=rx.State) app = rx.App(state=rx.State)
app.add_page(index) app.add_page(index)
@ -171,8 +170,8 @@ def event_action(tmp_path_factory) -> Generator[AppHarness, None, None]:
running AppHarness instance running AppHarness instance
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path_factory.mktemp("event_action"), root=tmp_path_factory.mktemp(f"event_action"),
app_source=TestEventAction, app_source=TestEventAction, # type: ignore
) as harness: ) as harness:
yield harness yield harness

View File

@ -27,124 +27,105 @@ def EventChain():
event_order: List[str] = [] event_order: List[str] = []
interim_value: str = "" interim_value: str = ""
@rx.event
def event_no_args(self): def event_no_args(self):
self.event_order.append("event_no_args") self.event_order.append("event_no_args")
@rx.event
def event_arg(self, arg): def event_arg(self, arg):
self.event_order.append(f"event_arg:{arg}") self.event_order.append(f"event_arg:{arg}")
@rx.event
def event_arg_repr_type(self, arg): def event_arg_repr_type(self, arg):
self.event_order.append(f"event_arg_repr:{arg!r}_{type(arg).__name__}") self.event_order.append(f"event_arg_repr:{arg!r}_{type(arg).__name__}")
@rx.event
def event_nested_1(self): def event_nested_1(self):
self.event_order.append("event_nested_1") self.event_order.append("event_nested_1")
yield State.event_nested_2 yield State.event_nested_2
yield State.event_arg("nested_1") yield State.event_arg("nested_1") # type: ignore
@rx.event
def event_nested_2(self): def event_nested_2(self):
self.event_order.append("event_nested_2") self.event_order.append("event_nested_2")
yield State.event_nested_3 yield State.event_nested_3
yield rx.console_log("event_nested_2") yield rx.console_log("event_nested_2")
yield State.event_arg("nested_2") yield State.event_arg("nested_2") # type: ignore
@rx.event
def event_nested_3(self): def event_nested_3(self):
self.event_order.append("event_nested_3") self.event_order.append("event_nested_3")
yield State.event_no_args yield State.event_no_args
yield State.event_arg("nested_3") yield State.event_arg("nested_3") # type: ignore
@rx.event
def on_load_return_chain(self): def on_load_return_chain(self):
self.event_order.append("on_load_return_chain") self.event_order.append("on_load_return_chain")
return [State.event_arg(1), State.event_arg(2), State.event_arg(3)] return [State.event_arg(1), State.event_arg(2), State.event_arg(3)] # type: ignore
@rx.event
def on_load_yield_chain(self): def on_load_yield_chain(self):
self.event_order.append("on_load_yield_chain") self.event_order.append("on_load_yield_chain")
yield State.event_arg(4) yield State.event_arg(4) # type: ignore
yield State.event_arg(5) yield State.event_arg(5) # type: ignore
yield State.event_arg(6) yield State.event_arg(6) # type: ignore
@rx.event
def click_return_event(self): def click_return_event(self):
self.event_order.append("click_return_event") self.event_order.append("click_return_event")
return State.event_no_args return State.event_no_args
@rx.event
def click_return_events(self): def click_return_events(self):
self.event_order.append("click_return_events") self.event_order.append("click_return_events")
return [ return [
State.event_arg(7), State.event_arg(7), # type: ignore
rx.console_log("click_return_events"), rx.console_log("click_return_events"),
State.event_arg(8), State.event_arg(8), # type: ignore
State.event_arg(9), State.event_arg(9), # type: ignore
] ]
@rx.event
def click_yield_chain(self): def click_yield_chain(self):
self.event_order.append("click_yield_chain:0") self.event_order.append("click_yield_chain:0")
yield State.event_arg(10) yield State.event_arg(10) # type: ignore
self.event_order.append("click_yield_chain:1") self.event_order.append("click_yield_chain:1")
yield rx.console_log("click_yield_chain") yield rx.console_log("click_yield_chain")
yield State.event_arg(11) yield State.event_arg(11) # type: ignore
self.event_order.append("click_yield_chain:2") self.event_order.append("click_yield_chain:2")
yield State.event_arg(12) yield State.event_arg(12) # type: ignore
self.event_order.append("click_yield_chain:3") self.event_order.append("click_yield_chain:3")
@rx.event
def click_yield_many_events(self): def click_yield_many_events(self):
self.event_order.append("click_yield_many_events") self.event_order.append("click_yield_many_events")
for ix in range(MANY_EVENTS): for ix in range(MANY_EVENTS):
yield State.event_arg(ix) yield State.event_arg(ix) # type: ignore
yield rx.console_log(f"many_events_{ix}") yield rx.console_log(f"many_events_{ix}")
self.event_order.append("click_yield_many_events_done") self.event_order.append("click_yield_many_events_done")
@rx.event
def click_yield_nested(self): def click_yield_nested(self):
self.event_order.append("click_yield_nested") self.event_order.append("click_yield_nested")
yield State.event_nested_1 yield State.event_nested_1
yield State.event_arg("yield_nested") yield State.event_arg("yield_nested") # type: ignore
@rx.event
def redirect_return_chain(self): def redirect_return_chain(self):
self.event_order.append("redirect_return_chain") self.event_order.append("redirect_return_chain")
yield rx.redirect("/on-load-return-chain") yield rx.redirect("/on-load-return-chain")
@rx.event
def redirect_yield_chain(self): def redirect_yield_chain(self):
self.event_order.append("redirect_yield_chain") self.event_order.append("redirect_yield_chain")
yield rx.redirect("/on-load-yield-chain") yield rx.redirect("/on-load-yield-chain")
@rx.event
def click_return_int_type(self): def click_return_int_type(self):
self.event_order.append("click_return_int_type") self.event_order.append("click_return_int_type")
return State.event_arg_repr_type(1) return State.event_arg_repr_type(1) # type: ignore
@rx.event
def click_return_dict_type(self): def click_return_dict_type(self):
self.event_order.append("click_return_dict_type") self.event_order.append("click_return_dict_type")
return State.event_arg_repr_type({"a": 1}) return State.event_arg_repr_type({"a": 1}) # type: ignore
@rx.event
async def click_yield_interim_value_async(self): async def click_yield_interim_value_async(self):
self.interim_value = "interim" self.interim_value = "interim"
yield yield
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
self.interim_value = "final" self.interim_value = "final"
@rx.event
def click_yield_interim_value(self): def click_yield_interim_value(self):
self.interim_value = "interim" self.interim_value = "interim"
yield yield
time.sleep(0.5) time.sleep(0.5)
self.interim_value = "final" self.interim_value = "final"
app = rx.App(_state=rx.State) app = rx.App(state=rx.State)
token_input = rx.input( token_input = rx.input(
value=State.router.session.client_token, is_read_only=True, id="token" value=State.router.session.client_token, is_read_only=True, id="token"
@ -193,12 +174,12 @@ def EventChain():
rx.button( rx.button(
"Click Int Type", "Click Int Type",
id="click_int_type", id="click_int_type",
on_click=lambda: State.event_arg_repr_type(1), on_click=lambda: State.event_arg_repr_type(1), # type: ignore
), ),
rx.button( rx.button(
"Click Dict Type", "Click Dict Type",
id="click_dict_type", id="click_dict_type",
on_click=lambda: State.event_arg_repr_type({"a": 1}), on_click=lambda: State.event_arg_repr_type({"a": 1}), # type: ignore
), ),
rx.button( rx.button(
"Return Chain Int Type", "Return Chain Int Type",
@ -239,7 +220,7 @@ def EventChain():
rx.text( rx.text(
"return", "return",
on_mount=State.on_load_return_chain, on_mount=State.on_load_return_chain,
on_unmount=lambda: State.event_arg("unmount"), on_unmount=lambda: State.event_arg("unmount"), # type: ignore
), ),
token_input, token_input,
rx.button("Unmount", on_click=rx.redirect("/"), id="unmount"), rx.button("Unmount", on_click=rx.redirect("/"), id="unmount"),
@ -251,7 +232,7 @@ def EventChain():
"yield", "yield",
on_mount=[ on_mount=[
State.on_load_yield_chain, State.on_load_yield_chain,
lambda: State.event_arg("mount"), lambda: State.event_arg("mount"), # type: ignore
], ],
on_unmount=State.event_no_args, on_unmount=State.event_no_args,
), ),
@ -259,8 +240,8 @@ def EventChain():
rx.button("Unmount", on_click=rx.redirect("/"), id="unmount"), rx.button("Unmount", on_click=rx.redirect("/"), id="unmount"),
) )
app.add_page(on_load_return_chain, on_load=State.on_load_return_chain) app.add_page(on_load_return_chain, on_load=State.on_load_return_chain) # type: ignore
app.add_page(on_load_yield_chain, on_load=State.on_load_yield_chain) app.add_page(on_load_yield_chain, on_load=State.on_load_yield_chain) # type: ignore
app.add_page(on_mount_return_chain) app.add_page(on_mount_return_chain)
app.add_page(on_mount_yield_chain) app.add_page(on_mount_yield_chain)
@ -277,7 +258,7 @@ def event_chain(tmp_path_factory) -> Generator[AppHarness, None, None]:
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path_factory.mktemp("event_chain"), root=tmp_path_factory.mktemp("event_chain"),
app_source=EventChain, app_source=EventChain, # type: ignore
) as harness: ) as harness:
yield harness yield harness
@ -493,6 +474,11 @@ async def test_event_chain_on_load(
"/on-mount-return-chain", "/on-mount-return-chain",
[ [
"on_load_return_chain", "on_load_return_chain",
"event_arg:unmount",
"on_load_return_chain",
"event_arg:1",
"event_arg:2",
"event_arg:3",
"event_arg:1", "event_arg:1",
"event_arg:2", "event_arg:2",
"event_arg:3", "event_arg:3",
@ -504,6 +490,12 @@ async def test_event_chain_on_load(
[ [
"on_load_yield_chain", "on_load_yield_chain",
"event_arg:mount", "event_arg:mount",
"event_no_args",
"on_load_yield_chain",
"event_arg:mount",
"event_arg:4",
"event_arg:5",
"event_arg:6",
"event_arg:4", "event_arg:4",
"event_arg:5", "event_arg:5",
"event_arg:6", "event_arg:6",

View File

@ -11,9 +11,7 @@ from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from reflex.testing import AppHarness, AppHarnessProd from reflex.testing import AppHarness
pytestmark = [pytest.mark.ignore_console_error]
def TestApp(): def TestApp():
@ -28,8 +26,6 @@ def TestApp():
class TestAppState(rx.State): class TestAppState(rx.State):
"""State for the TestApp app.""" """State for the TestApp app."""
react_error: bool = False
def divide_by_number(self, number: int): def divide_by_number(self, number: int):
"""Divide by number and print the result. """Divide by number and print the result.
@ -39,7 +35,7 @@ def TestApp():
""" """
print(1 / number) print(1 / number)
app = rx.App(_state=rx.State) app = rx.App(state=rx.State)
@app.add_page @app.add_page
def index(): def index():
@ -51,21 +47,9 @@ def TestApp():
), ),
rx.button( rx.button(
"induce_backend_error", "induce_backend_error",
on_click=lambda: TestAppState.divide_by_number(0), # pyright: ignore [reportCallIssue] on_click=lambda: TestAppState.divide_by_number(0), # type: ignore
id="induce-backend-error-btn", id="induce-backend-error-btn",
), ),
rx.button(
"induce_react_error",
on_click=TestAppState.set_react_error(True), # pyright: ignore [reportAttributeAccessIssue]
id="induce-react-error-btn",
),
rx.box(
rx.cond(
TestAppState.react_error,
rx.Var.create({"invalid": "cannot have object as child"}),
"",
),
),
) )
@ -86,7 +70,7 @@ def test_app(
with app_harness_env.create( with app_harness_env.create(
root=tmp_path_factory.mktemp("test_app"), root=tmp_path_factory.mktemp("test_app"),
app_name=f"testapp_{app_harness_env.__name__.lower()}", app_name=f"testapp_{app_harness_env.__name__.lower()}",
app_source=TestApp, app_source=TestApp, # type: ignore
) as harness: ) as harness:
yield harness yield harness
@ -168,37 +152,3 @@ def test_backend_exception_handler_during_runtime(
"divide_by_number" in captured_default_handler_output.out "divide_by_number" in captured_default_handler_output.out
and "ZeroDivisionError" in captured_default_handler_output.out and "ZeroDivisionError" in captured_default_handler_output.out
) )
def test_frontend_exception_handler_with_react(
test_app: AppHarness,
driver: WebDriver,
capsys,
):
"""Test calling frontend exception handler during runtime.
Render an object as a react child, which is invalid.
Args:
test_app: harness for TestApp app
driver: WebDriver instance.
capsys: pytest fixture for capturing stdout and stderr.
"""
reset_button = WebDriverWait(driver, 20).until(
EC.element_to_be_clickable((By.ID, "induce-react-error-btn"))
)
reset_button.click()
# Wait for the error to be logged
time.sleep(2)
captured_default_handler_output = capsys.readouterr()
if isinstance(test_app, AppHarnessProd):
assert "Error: Minified React error #31" in captured_default_handler_output.out
else:
assert (
"Error: Objects are not valid as a React child (found: object with keys \n{invalid})"
in captured_default_handler_output.out
)

View File

@ -30,7 +30,7 @@ def FormSubmit(form_component):
def form_submit(self, form_data: Dict): def form_submit(self, form_data: Dict):
self.form_data = form_data self.form_data = form_data
app = rx.App(_state=rx.State) app = rx.App(state=rx.State)
@app.add_page @app.add_page
def index(): def index():
@ -90,7 +90,7 @@ def FormSubmitName(form_component):
def form_submit(self, form_data: Dict): def form_submit(self, form_data: Dict):
self.form_data = form_data self.form_data = form_data
app = rx.App(_state=rx.State) app = rx.App(state=rx.State)
@app.add_page @app.add_page
def index(): def index():
@ -121,7 +121,7 @@ def FormSubmitName(form_component):
on_change=rx.console_log, on_change=rx.console_log,
), ),
rx.button("Submit", type_="submit"), rx.button("Submit", type_="submit"),
rx.icon_button(rx.icon(tag="plus")), rx.icon_button(FormState.val, icon=rx.icon(tag="plus")),
), ),
on_submit=FormState.form_submit, on_submit=FormState.form_submit,
custom_attrs={"action": "/invalid"}, custom_attrs={"action": "/invalid"},
@ -159,7 +159,7 @@ def form_submit(request, tmp_path_factory) -> Generator[AppHarness, None, None]:
param_id = request._pyfuncitem.callspec.id.replace("-", "_") param_id = request._pyfuncitem.callspec.id.replace("-", "_")
with AppHarness.create( with AppHarness.create(
root=tmp_path_factory.mktemp("form_submit"), root=tmp_path_factory.mktemp("form_submit"),
app_source=request.param, app_source=request.param, # type: ignore
app_name=request.param.func.__name__ + f"_{param_id}", app_name=request.param.func.__name__ + f"_{param_id}",
) as harness: ) as harness:
assert harness.app_instance is not None, "app is not running" assert harness.app_instance is not None, "app is not running"

View File

@ -16,7 +16,7 @@ def FullyControlledInput():
class State(rx.State): class State(rx.State):
text: str = "initial" text: str = "initial"
app = rx.App(_state=rx.State) app = rx.App(state=rx.State)
@app.add_page @app.add_page
def index(): def index():
@ -26,11 +26,11 @@ def FullyControlledInput():
), ),
rx.input( rx.input(
id="debounce_input_input", id="debounce_input_input",
on_change=State.set_text, # pyright: ignore [reportAttributeAccessIssue] on_change=State.set_text, # type: ignore
value=State.text, value=State.text,
), ),
rx.input(value=State.text, id="value_input", is_read_only=True), rx.input(value=State.text, id="value_input", is_read_only=True),
rx.input(on_change=State.set_text, id="on_change_input"), # pyright: ignore [reportAttributeAccessIssue] rx.input(on_change=State.set_text, id="on_change_input"), # type: ignore
rx.el.input( rx.el.input(
value=State.text, value=State.text,
id="plain_value_input", id="plain_value_input",
@ -63,7 +63,7 @@ def fully_controlled_input(tmp_path) -> Generator[AppHarness, None, None]:
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path, root=tmp_path,
app_source=FullyControlledInput, app_source=FullyControlledInput, # type: ignore
) as harness: ) as harness:
yield harness yield harness
@ -183,6 +183,6 @@ async def test_fully_controlled_input(fully_controlled_input: AppHarness):
clear_button.click() clear_button.click()
assert AppHarness._poll_for(lambda: on_change_input.get_attribute("value") == "") assert AppHarness._poll_for(lambda: on_change_input.get_attribute("value") == "")
# potential bug: clearing the on_change field doesn't itself trigger on_change # potential bug: clearing the on_change field doesn't itself trigger on_change
# assert backend_state.text == "" #noqa: ERA001 # assert backend_state.text == ""
# assert debounce_input.get_attribute("value") == "" #noqa: ERA001 # assert debounce_input.get_attribute("value") == ""
# assert value_input.get_attribute("value") == "" #noqa: ERA001 # assert value_input.get_attribute("value") == ""

View File

@ -58,7 +58,7 @@ def test_large_state(var_count: int, tmp_path_factory, benchmark):
large_state_rendered = template.render(var_count=var_count) large_state_rendered = template.render(var_count=var_count)
with AppHarness.create( with AppHarness.create(
root=tmp_path_factory.mktemp("large_state"), root=tmp_path_factory.mktemp(f"large_state"),
app_source=large_state_rendered, app_source=large_state_rendered,
app_name="large_state", app_name="large_state",
) as large_state: ) as large_state:

View File

@ -36,24 +36,21 @@ def LifespanApp():
print("Lifespan global started.") print("Lifespan global started.")
try: try:
while True: while True:
lifespan_task_global += inc # pyright: ignore[reportUnboundVariable, reportPossiblyUnboundVariable] lifespan_task_global += inc # pyright: ignore[reportUnboundVariable]
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
except asyncio.CancelledError as ce: except asyncio.CancelledError as ce:
print(f"Lifespan global cancelled: {ce}.") print(f"Lifespan global cancelled: {ce}.")
lifespan_task_global = 0 lifespan_task_global = 0
class LifespanState(rx.State): class LifespanState(rx.State):
interval: int = 100 @rx.var
@rx.var(cache=False)
def task_global(self) -> int: def task_global(self) -> int:
return lifespan_task_global return lifespan_task_global
@rx.var(cache=False) @rx.var
def context_global(self) -> int: def context_global(self) -> int:
return lifespan_context_global return lifespan_context_global
@rx.event
def tick(self, date): def tick(self, date):
pass pass
@ -61,15 +58,7 @@ 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.button( rx.moment(interval=100, on_change=LifespanState.tick),
rx.moment(
interval=LifespanState.interval, on_change=LifespanState.tick
),
on_click=LifespanState.set_interval( # pyright: ignore [reportAttributeAccessIssue]
rx.cond(LifespanState.interval, 0, 100)
),
id="toggle-tick",
),
) )
app = rx.App() app = rx.App()
@ -90,7 +79,7 @@ def lifespan_app(tmp_path) -> Generator[AppHarness, None, None]:
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path, root=tmp_path,
app_source=LifespanApp, app_source=LifespanApp, # type: ignore
) as harness: ) as harness:
yield harness yield harness
@ -113,13 +102,12 @@ async def test_lifespan(lifespan_app: AppHarness):
task_global = driver.find_element(By.ID, "task_global") task_global = driver.find_element(By.ID, "task_global")
assert context_global.text == "2" assert context_global.text == "2"
assert lifespan_app.app_module.lifespan_context_global == 2 assert lifespan_app.app_module.lifespan_context_global == 2 # type: ignore
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
assert int(task_global.text) > original_task_global_value assert int(task_global.text) > original_task_global_value
# Kill the backend # Kill the backend

View File

@ -21,18 +21,16 @@ def LoginSample():
class State(rx.State): class State(rx.State):
auth_token: str = rx.LocalStorage("") auth_token: str = rx.LocalStorage("")
@rx.event
def logout(self): def logout(self):
self.set_auth_token("") self.set_auth_token("")
@rx.event
def login(self): def login(self):
self.set_auth_token("12345") self.set_auth_token("12345")
yield rx.redirect("/") yield rx.redirect("/")
def index(): def index():
return rx.cond( # pyright: ignore [reportCallIssue] return rx.cond(
State.is_hydrated & State.auth_token, # pyright: ignore [reportOperatorIssue] State.is_hydrated & State.auth_token, # type: ignore
rx.vstack( rx.vstack(
rx.heading(State.auth_token, id="auth-token"), rx.heading(State.auth_token, id="auth-token"),
rx.button("Logout", on_click=State.logout, id="logout"), rx.button("Logout", on_click=State.logout, id="logout"),
@ -45,7 +43,7 @@ def LoginSample():
rx.button("Do it", on_click=State.login, id="doit"), rx.button("Do it", on_click=State.login, id="doit"),
) )
app = rx.App(_state=rx.State) app = rx.App(state=rx.State)
app.add_page(index) app.add_page(index)
app.add_page(login) app.add_page(login)
@ -62,7 +60,7 @@ def login_sample(tmp_path_factory) -> Generator[AppHarness, None, None]:
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path_factory.mktemp("login_sample"), root=tmp_path_factory.mktemp("login_sample"),
app_source=LoginSample, app_source=LoginSample, # type: ignore
) as harness: ) as harness:
yield harness yield harness

View File

@ -19,38 +19,38 @@ def MediaApp():
def _blue(self, format=None) -> Image.Image: def _blue(self, format=None) -> Image.Image:
img = Image.new("RGB", (200, 200), "blue") img = Image.new("RGB", (200, 200), "blue")
if format is not None: if format is not None:
img.format = format img.format = format # type: ignore
return img return img
@rx.var @rx.var(cache=True)
def img_default(self) -> Image.Image: def img_default(self) -> Image.Image:
return self._blue() return self._blue()
@rx.var @rx.var(cache=True)
def img_bmp(self) -> Image.Image: def img_bmp(self) -> Image.Image:
return self._blue(format="BMP") return self._blue(format="BMP")
@rx.var @rx.var(cache=True)
def img_jpg(self) -> Image.Image: def img_jpg(self) -> Image.Image:
return self._blue(format="JPEG") return self._blue(format="JPEG")
@rx.var @rx.var(cache=True)
def img_png(self) -> Image.Image: def img_png(self) -> Image.Image:
return self._blue(format="PNG") return self._blue(format="PNG")
@rx.var @rx.var(cache=True)
def img_gif(self) -> Image.Image: def img_gif(self) -> Image.Image:
return self._blue(format="GIF") return self._blue(format="GIF")
@rx.var @rx.var(cache=True)
def img_webp(self) -> Image.Image: def img_webp(self) -> Image.Image:
return self._blue(format="WEBP") return self._blue(format="WEBP")
@rx.var @rx.var(cache=True)
def img_from_url(self) -> Image.Image: def img_from_url(self) -> Image.Image:
img_url = "https://picsum.photos/id/1/200/300" img_url = "https://picsum.photos/id/1/200/300"
img_resp = httpx.get(img_url, follow_redirects=True) img_resp = httpx.get(img_url, follow_redirects=True)
return Image.open(img_resp) # pyright: ignore [reportArgumentType] return Image.open(img_resp) # type: ignore
app = rx.App() app = rx.App()
@ -84,7 +84,7 @@ def media_app(tmp_path) -> Generator[AppHarness, None, None]:
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path, root=tmp_path,
app_source=MediaApp, app_source=MediaApp, # type: ignore
) as harness: ) as harness:
yield harness yield harness

View File

@ -52,7 +52,7 @@ def navigation_app(tmp_path) -> Generator[AppHarness, None, None]:
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path, root=tmp_path,
app_source=NavigationApp, app_source=NavigationApp, # type: ignore
) as harness: ) as harness:
yield harness yield harness
@ -74,7 +74,7 @@ async def test_navigation_app(navigation_app: AppHarness):
with poll_for_navigation(driver): with poll_for_navigation(driver):
internal_link.click() internal_link.click()
assert urlsplit(driver.current_url).path == "/internal/" assert urlsplit(driver.current_url).path == f"/internal/"
with poll_for_navigation(driver): with poll_for_navigation(driver):
driver.back() driver.back()

View File

@ -14,19 +14,16 @@ def ServerSideEvent():
import reflex as rx import reflex as rx
class SSState(rx.State): class SSState(rx.State):
@rx.event
def set_value_yield(self): def set_value_yield(self):
yield rx.set_value("a", "") yield rx.set_value("a", "")
yield rx.set_value("b", "") yield rx.set_value("b", "")
yield rx.set_value("c", "") yield rx.set_value("c", "")
@rx.event
def set_value_yield_return(self): def set_value_yield_return(self):
yield rx.set_value("a", "") yield rx.set_value("a", "")
yield rx.set_value("b", "") yield rx.set_value("b", "")
return rx.set_value("c", "") return rx.set_value("c", "")
@rx.event
def set_value_return(self): def set_value_return(self):
return [ return [
rx.set_value("a", ""), rx.set_value("a", ""),
@ -34,11 +31,10 @@ def ServerSideEvent():
rx.set_value("c", ""), rx.set_value("c", ""),
] ]
@rx.event
def set_value_return_c(self): def set_value_return_c(self):
return rx.set_value("c", "") return rx.set_value("c", "")
app = rx.App(_state=rx.State) app = rx.App(state=rx.State)
@app.add_page @app.add_page
def index(): def index():
@ -93,7 +89,7 @@ def server_side_event(tmp_path_factory) -> Generator[AppHarness, None, None]:
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path_factory.mktemp("server_side_event"), root=tmp_path_factory.mktemp("server_side_event"),
app_source=ServerSideEvent, app_source=ServerSideEvent, # type: ignore
) as harness: ) as harness:
yield harness yield harness
@ -102,6 +98,7 @@ def server_side_event(tmp_path_factory) -> Generator[AppHarness, None, None]:
def driver(server_side_event: AppHarness): def driver(server_side_event: AppHarness):
"""Get an instance of the browser open to the server_side_event app. """Get an instance of the browser open to the server_side_event app.
Args: Args:
server_side_event: harness for ServerSideEvent app server_side_event: harness for ServerSideEvent app

View File

@ -12,7 +12,7 @@ from reflex.testing import AppHarness, WebDriver
def SharedStateApp(): def SharedStateApp():
"""Test that shared state works as expected.""" """Test that shared state works as expected."""
import reflex as rx import reflex as rx
from tests.integration.shared.state import SharedState from integration.shared.state import SharedState
class State(SharedState): class State(SharedState):
pass pass
@ -39,7 +39,7 @@ def shared_state(
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path_factory.mktemp("shared_state"), root=tmp_path_factory.mktemp("shared_state"),
app_source=SharedStateApp, app_source=SharedStateApp, # type: ignore
) as harness: ) as harness:
yield harness yield harness

View File

@ -59,7 +59,6 @@ def StateInheritance():
def computed_mixin(self) -> str: def computed_mixin(self) -> str:
return "computed_mixin" return "computed_mixin"
@rx.event
def on_click_mixin(self): def on_click_mixin(self):
return rx.call_script("alert('clicked')") return rx.call_script("alert('clicked')")
@ -71,11 +70,10 @@ def StateInheritance():
def computed_other_mixin(self) -> str: def computed_other_mixin(self) -> str:
return self.other_mixin return self.other_mixin
@rx.event
def on_click_other_mixin(self): def on_click_other_mixin(self):
self.other_mixin_clicks += 1 self.other_mixin_clicks += 1
self.other_mixin = ( self.other_mixin = (
f"{type(self).__name__}.clicked.{self.other_mixin_clicks}" f"{self.__class__.__name__}.clicked.{self.other_mixin_clicks}"
) )
class Base1(Mixin, rx.State): class Base1(Mixin, rx.State):
@ -133,7 +131,7 @@ def StateInheritance():
rx.heading(Base1.child_mixin, id="base1-child-mixin"), rx.heading(Base1.child_mixin, id="base1-child-mixin"),
rx.button( rx.button(
"Base1.on_click_mixin", "Base1.on_click_mixin",
on_click=Base1.on_click_mixin, on_click=Base1.on_click_mixin, # type: ignore
id="base1-mixin-btn", id="base1-mixin-btn",
), ),
rx.heading( rx.heading(
@ -155,7 +153,7 @@ def StateInheritance():
rx.heading(Child1.child_mixin, id="child1-child-mixin"), rx.heading(Child1.child_mixin, id="child1-child-mixin"),
rx.button( rx.button(
"Child1.on_click_other_mixin", "Child1.on_click_other_mixin",
on_click=Child1.on_click_other_mixin, on_click=Child1.on_click_other_mixin, # type: ignore
id="child1-other-mixin-btn", id="child1-other-mixin-btn",
), ),
# Child 2 (Mixin, ChildMixin, OtherMixin) # Child 2 (Mixin, ChildMixin, OtherMixin)
@ -168,12 +166,12 @@ def StateInheritance():
rx.heading(Child2.child_mixin, id="child2-child-mixin"), rx.heading(Child2.child_mixin, id="child2-child-mixin"),
rx.button( rx.button(
"Child2.on_click_mixin", "Child2.on_click_mixin",
on_click=Child2.on_click_mixin, on_click=Child2.on_click_mixin, # type: ignore
id="child2-mixin-btn", id="child2-mixin-btn",
), ),
rx.button( rx.button(
"Child2.on_click_other_mixin", "Child2.on_click_other_mixin",
on_click=Child2.on_click_other_mixin, on_click=Child2.on_click_other_mixin, # type: ignore
id="child2-other-mixin-btn", id="child2-other-mixin-btn",
), ),
# Child 3 (Mixin, ChildMixin, OtherMixin) # Child 3 (Mixin, ChildMixin, OtherMixin)
@ -188,12 +186,12 @@ def StateInheritance():
rx.heading(Child3.child_mixin, id="child3-child-mixin"), rx.heading(Child3.child_mixin, id="child3-child-mixin"),
rx.button( rx.button(
"Child3.on_click_mixin", "Child3.on_click_mixin",
on_click=Child3.on_click_mixin, on_click=Child3.on_click_mixin, # type: ignore
id="child3-mixin-btn", id="child3-mixin-btn",
), ),
rx.button( rx.button(
"Child3.on_click_other_mixin", "Child3.on_click_other_mixin",
on_click=Child3.on_click_other_mixin, on_click=Child3.on_click_other_mixin, # type: ignore
id="child3-other-mixin-btn", id="child3-other-mixin-btn",
), ),
rx.heading( rx.heading(
@ -218,8 +216,8 @@ def state_inheritance(
running AppHarness instance running AppHarness instance
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path_factory.mktemp("state_inheritance"), root=tmp_path_factory.mktemp(f"state_inheritance"),
app_source=StateInheritance, app_source=StateInheritance, # type: ignore
) as harness: ) as harness:
yield harness yield harness

View File

@ -3,28 +3,25 @@
from typing import Generator from typing import Generator
import pytest import pytest
from playwright.sync_api import Page, expect from selenium.webdriver.common.by import By
from reflex.testing import AppHarness from reflex.testing import AppHarness
expected_col_headers = ["Name", "Age", "Location"]
expected_row_headers = ["John", "Jane", "Joe"]
expected_cells_data = [
["30", "New York"],
["31", "San Fransisco"],
["32", "Los Angeles"],
]
def Table(): def Table():
"""App using table component.""" """App using table component."""
import reflex as rx import reflex as rx
app = rx.App(_state=rx.State) app = rx.App(state=rx.State)
@app.add_page @app.add_page
def index(): def index():
return rx.center( return rx.center(
rx.input(
id="token",
value=rx.State.router.session.client_token,
is_read_only=True,
),
rx.table.root( rx.table.root(
rx.table.header( rx.table.header(
rx.table.row( rx.table.row(
@ -56,7 +53,7 @@ def Table():
@pytest.fixture() @pytest.fixture()
def table_app(tmp_path_factory) -> Generator[AppHarness, None, None]: def table(tmp_path_factory) -> Generator[AppHarness, None, None]:
"""Start Table app at tmp_path via AppHarness. """Start Table app at tmp_path via AppHarness.
Args: Args:
@ -68,36 +65,53 @@ def table_app(tmp_path_factory) -> Generator[AppHarness, None, None]:
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path_factory.mktemp("table"), root=tmp_path_factory.mktemp("table"),
app_source=Table, app_source=Table, # type: ignore
) as harness: ) as harness:
assert harness.app_instance is not None, "app is not running" assert harness.app_instance is not None, "app is not running"
yield harness yield harness
def test_table(page: Page, table_app: AppHarness): @pytest.fixture
def driver(table: AppHarness):
"""GEt an instance of the browser open to the table app.
Args:
table: harness for Table app
Yields:
WebDriver instance.
"""
driver = table.frontend()
try:
token_input = driver.find_element(By.ID, "token")
assert token_input
# wait for the backend connection to send the token
token = table.poll_for_value(token_input)
assert token is not None
yield driver
finally:
driver.quit()
def test_table(driver, table: AppHarness):
"""Test that a table component is rendered properly. """Test that a table component is rendered properly.
Args: Args:
table_app: Harness for Table app driver: Selenium WebDriver open to the app
page: Playwright page instance table: Harness for Table app
""" """
assert table_app.frontend_url is not None, "frontend url is not available" assert table.app_instance is not None, "app is not running"
page.goto(table_app.frontend_url) thead = driver.find_element(By.TAG_NAME, "thead")
table = page.get_by_role("table") # poll till page is fully loaded.
table.poll_for_content(element=thead)
# Check column headers # check headers
headers = table.get_by_role("columnheader") assert thead.find_element(By.TAG_NAME, "tr").text == "Name Age Location"
for header, exp_value in zip(headers.all(), expected_col_headers, strict=True): # check first row value
expect(header).to_have_text(exp_value) assert (
driver.find_element(By.TAG_NAME, "tbody")
# Check rows headers .find_elements(By.TAG_NAME, "tr")[0]
rows = table.get_by_role("rowheader") .text
for row, expected_row in zip(rows.all(), expected_row_headers, strict=True): == "John 30 New York"
expect(row).to_have_text(expected_row) )
# Check cells
rows = table.get_by_role("cell").all_inner_texts()
for i, expected_row in enumerate(expected_cells_data):
idx = i * 2
assert [rows[idx], rows[idx + 1]] == expected_row

View File

@ -78,7 +78,7 @@ def tailwind_app(tmp_path, tailwind_disabled) -> Generator[AppHarness, None, Non
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path, root=tmp_path,
app_source=functools.partial(TailwindApp, tailwind_disabled=tailwind_disabled), app_source=functools.partial(TailwindApp, tailwind_disabled=tailwind_disabled), # type: ignore
app_name="tailwind_disabled_app" if tailwind_disabled else "tailwind_app", app_name="tailwind_disabled_app" if tailwind_disabled else "tailwind_app",
) as harness: ) as harness:
yield harness yield harness

View File

@ -4,18 +4,13 @@ from __future__ import annotations
import asyncio import asyncio
import time import time
from pathlib import Path
from typing import Generator from typing import Generator
from urllib.parse import urlsplit
import pytest import pytest
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from reflex.constants.event import Endpoint
from reflex.testing import AppHarness, WebDriver from reflex.testing import AppHarness, WebDriver
from .utils import poll_for_navigation
def UploadFile(): def UploadFile():
"""App for testing dynamic routes.""" """App for testing dynamic routes."""
@ -23,14 +18,10 @@ def UploadFile():
import reflex as rx import reflex as rx
LARGE_DATA = "DUMMY" * 1024 * 512
class UploadState(rx.State): class UploadState(rx.State):
_file_data: Dict[str, str] = {} _file_data: Dict[str, str] = {}
event_order: rx.Field[List[str]] = rx.field([]) event_order: List[str] = []
progress_dicts: List[dict] = [] progress_dicts: List[dict] = []
disabled: bool = False
large_data: str = ""
async def handle_upload(self, files: List[rx.UploadFile]): async def handle_upload(self, files: List[rx.UploadFile]):
for file in files: for file in files:
@ -41,7 +32,6 @@ def UploadFile():
for file in files: for file in files:
upload_data = await file.read() upload_data = await file.read()
self._file_data[file.filename or ""] = upload_data.decode("utf-8") self._file_data[file.filename or ""] = upload_data.decode("utf-8")
self.large_data = LARGE_DATA
yield UploadState.chain_event yield UploadState.chain_event
def upload_progress(self, progress): def upload_progress(self, progress):
@ -50,26 +40,13 @@ def UploadFile():
self.progress_dicts.append(progress) self.progress_dicts.append(progress)
def chain_event(self): def chain_event(self):
assert self.large_data == LARGE_DATA
self.large_data = ""
self.event_order.append("chain_event") self.event_order.append("chain_event")
@rx.event
async def handle_upload_tertiary(self, files: List[rx.UploadFile]):
for file in files:
(rx.get_upload_dir() / (file.filename or "INVALID")).write_bytes(
await file.read()
)
@rx.event
def do_download(self):
return rx.download(rx.get_upload_url("test.txt"))
def index(): def index():
return rx.vstack( return rx.vstack(
rx.input( rx.input(
value=UploadState.router.session.client_token, value=UploadState.router.session.client_token,
read_only=True, is_read_only=True,
id="token", id="token",
), ),
rx.heading("Default Upload"), rx.heading("Default Upload"),
@ -78,16 +55,15 @@ def UploadFile():
rx.button("Select File"), rx.button("Select File"),
rx.text("Drag and drop files here or click to select files"), rx.text("Drag and drop files here or click to select files"),
), ),
disabled=UploadState.disabled,
), ),
rx.button( rx.button(
"Upload", "Upload",
on_click=lambda: UploadState.handle_upload(rx.upload_files()), # pyright: ignore [reportCallIssue] on_click=lambda: UploadState.handle_upload(rx.upload_files()), # type: ignore
id="upload_button", id="upload_button",
), ),
rx.box( rx.box(
rx.foreach( rx.foreach(
rx.selected_files(), rx.selected_files,
lambda f: rx.text(f, as_="p"), lambda f: rx.text(f, as_="p"),
), ),
id="selected_files", id="selected_files",
@ -107,7 +83,7 @@ def UploadFile():
), ),
rx.button( rx.button(
"Upload", "Upload",
on_click=UploadState.handle_upload_secondary( # pyright: ignore [reportCallIssue] on_click=UploadState.handle_upload_secondary( # type: ignore
rx.upload_files( rx.upload_files(
upload_id="secondary", upload_id="secondary",
on_upload_progress=UploadState.upload_progress, on_upload_progress=UploadState.upload_progress,
@ -129,7 +105,7 @@ def UploadFile():
), ),
rx.vstack( rx.vstack(
rx.foreach( rx.foreach(
UploadState.progress_dicts, UploadState.progress_dicts, # type: ignore
lambda d: rx.text(d.to_string()), lambda d: rx.text(d.to_string()),
) )
), ),
@ -138,37 +114,9 @@ def UploadFile():
on_click=rx.cancel_upload("secondary"), on_click=rx.cancel_upload("secondary"),
id="cancel_button_secondary", id="cancel_button_secondary",
), ),
rx.heading("Tertiary Upload/Download"),
rx.upload.root(
rx.vstack(
rx.button("Select File"),
rx.text("Drag and drop files here or click to select files"),
),
id="tertiary",
),
rx.button(
"Upload",
on_click=UploadState.handle_upload_tertiary(
rx.upload_files( # pyright: ignore [reportArgumentType]
upload_id="tertiary",
),
),
id="upload_button_tertiary",
),
rx.button(
"Download - Frontend",
on_click=rx.download(rx.get_upload_url("test.txt")),
id="download-frontend",
),
rx.button(
"Download - Backend",
on_click=UploadState.do_download,
id="download-backend",
),
rx.text(UploadState.event_order.to_string(), id="event-order"),
) )
app = rx.App(_state=rx.State) app = rx.App(state=rx.State)
app.add_page(index) app.add_page(index)
@ -184,7 +132,7 @@ def upload_file(tmp_path_factory) -> Generator[AppHarness, None, None]:
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path_factory.mktemp("upload_file"), root=tmp_path_factory.mktemp("upload_file"),
app_source=UploadFile, app_source=UploadFile, # type: ignore
) as harness: ) as harness:
yield harness yield harness
@ -207,24 +155,6 @@ def driver(upload_file: AppHarness):
driver.quit() driver.quit()
def poll_for_token(driver: WebDriver, upload_file: AppHarness) -> str:
"""Poll for the token input to be populated.
Args:
driver: WebDriver instance.
upload_file: harness for UploadFile app.
Returns:
token value
"""
token_input = driver.find_element(By.ID, "token")
assert token_input
# wait for the backend connection to send the token
token = upload_file.poll_for_value(token_input)
assert token is not None
return token
@pytest.mark.parametrize("secondary", [False, True]) @pytest.mark.parametrize("secondary", [False, True])
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_upload_file( async def test_upload_file(
@ -239,7 +169,11 @@ async def test_upload_file(
secondary: whether to use the secondary upload form secondary: whether to use the secondary upload form
""" """
assert upload_file.app_instance is not None assert upload_file.app_instance is not None
token = poll_for_token(driver, upload_file) token_input = driver.find_element(By.ID, "token")
assert token_input
# wait for the backend connection to send the token
token = upload_file.poll_for_value(token_input)
assert token is not None
full_state_name = upload_file.get_full_state_name(["_upload_state"]) full_state_name = upload_file.get_full_state_name(["_upload_state"])
state_name = upload_file.get_state_name("_upload_state") state_name = upload_file.get_state_name("_upload_state")
substate_token = f"{token}_{full_state_name}" substate_token = f"{token}_{full_state_name}"
@ -261,19 +195,6 @@ async def test_upload_file(
upload_box.send_keys(str(target_file)) upload_box.send_keys(str(target_file))
upload_button.click() upload_button.click()
# check that the selected files are displayed
selected_files = driver.find_element(By.ID, f"selected_files{suffix}")
assert Path(selected_files.text).name == Path(exp_name).name
if secondary:
event_order_displayed = driver.find_element(By.ID, "event-order")
AppHarness._poll_for(lambda: "chain_event" in event_order_displayed.text)
state = await upload_file.get_state(substate_token)
# only the secondary form tracks progress and chain events
assert state.substates[state_name].event_order.count("upload_progress") == 1
assert state.substates[state_name].event_order.count("chain_event") == 1
# look up the backend state and assert on uploaded contents # look up the backend state and assert on uploaded contents
async def get_file_data(): async def get_file_data():
return ( return (
@ -284,8 +205,17 @@ async def test_upload_file(
file_data = await AppHarness._poll_for_async(get_file_data) file_data = await AppHarness._poll_for_async(get_file_data)
assert isinstance(file_data, dict) assert isinstance(file_data, dict)
normalized_file_data = {Path(k).name: v for k, v in file_data.items()} assert file_data[exp_name] == exp_contents
assert normalized_file_data[Path(exp_name).name] == exp_contents
# check that the selected files are displayed
selected_files = driver.find_element(By.ID, f"selected_files{suffix}")
assert selected_files.text == exp_name
state = await upload_file.get_state(substate_token)
if secondary:
# only the secondary form tracks progress and chain events
assert state.substates[state_name].event_order.count("upload_progress") == 1
assert state.substates[state_name].event_order.count("chain_event") == 1
@pytest.mark.asyncio @pytest.mark.asyncio
@ -298,7 +228,11 @@ async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver):
driver: WebDriver instance. driver: WebDriver instance.
""" """
assert upload_file.app_instance is not None assert upload_file.app_instance is not None
token = poll_for_token(driver, upload_file) token_input = driver.find_element(By.ID, "token")
assert token_input
# wait for the backend connection to send the token
token = upload_file.poll_for_value(token_input)
assert token is not None
full_state_name = upload_file.get_full_state_name(["_upload_state"]) full_state_name = upload_file.get_full_state_name(["_upload_state"])
state_name = upload_file.get_state_name("_upload_state") state_name = upload_file.get_state_name("_upload_state")
substate_token = f"{token}_{full_state_name}" substate_token = f"{token}_{full_state_name}"
@ -322,9 +256,7 @@ async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver):
# check that the selected files are displayed # check that the selected files are displayed
selected_files = driver.find_element(By.ID, "selected_files") selected_files = driver.find_element(By.ID, "selected_files")
assert [Path(name).name for name in selected_files.text.split("\n")] == [ assert selected_files.text == "\n".join(exp_files)
Path(name).name for name in exp_files
]
# do the upload # do the upload
upload_button.click() upload_button.click()
@ -339,9 +271,8 @@ async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver):
file_data = await AppHarness._poll_for_async(get_file_data) file_data = await AppHarness._poll_for_async(get_file_data)
assert isinstance(file_data, dict) assert isinstance(file_data, dict)
normalized_file_data = {Path(k).name: v for k, v in file_data.items()}
for exp_name, exp_contents in exp_files.items(): for exp_name, exp_contents in exp_files.items():
assert normalized_file_data[Path(exp_name).name] == exp_contents assert file_data[exp_name] == exp_contents
@pytest.mark.parametrize("secondary", [False, True]) @pytest.mark.parametrize("secondary", [False, True])
@ -357,7 +288,11 @@ def test_clear_files(
secondary: whether to use the secondary upload form. secondary: whether to use the secondary upload form.
""" """
assert upload_file.app_instance is not None assert upload_file.app_instance is not None
poll_for_token(driver, upload_file) token_input = driver.find_element(By.ID, "token")
assert token_input
# wait for the backend connection to send the token
token = upload_file.poll_for_value(token_input)
assert token is not None
suffix = "_secondary" if secondary else "" suffix = "_secondary" if secondary else ""
@ -382,9 +317,7 @@ def test_clear_files(
# check that the selected files are displayed # check that the selected files are displayed
selected_files = driver.find_element(By.ID, f"selected_files{suffix}") selected_files = driver.find_element(By.ID, f"selected_files{suffix}")
assert [Path(name).name for name in selected_files.text.split("\n")] == [ assert selected_files.text == "\n".join(exp_files)
Path(name).name for name in exp_files
]
clear_button = driver.find_element(By.ID, f"clear_button{suffix}") clear_button = driver.find_element(By.ID, f"clear_button{suffix}")
assert clear_button assert clear_button
@ -409,14 +342,18 @@ async def test_cancel_upload(tmp_path, upload_file: AppHarness, driver: WebDrive
driver: WebDriver instance. driver: WebDriver instance.
""" """
assert upload_file.app_instance is not None assert upload_file.app_instance is not None
token = poll_for_token(driver, upload_file) token_input = driver.find_element(By.ID, "token")
assert token_input
# wait for the backend connection to send the token
token = upload_file.poll_for_value(token_input)
assert token is not None
state_name = upload_file.get_state_name("_upload_state") state_name = upload_file.get_state_name("_upload_state")
state_full_name = upload_file.get_full_state_name(["_upload_state"]) state_full_name = upload_file.get_full_state_name(["_upload_state"])
substate_token = f"{token}_{state_full_name}" substate_token = f"{token}_{state_full_name}"
upload_box = driver.find_elements(By.XPATH, "//input[@type='file']")[1] upload_box = driver.find_elements(By.XPATH, "//input[@type='file']")[1]
upload_button = driver.find_element(By.ID, "upload_button_secondary") upload_button = driver.find_element(By.ID, f"upload_button_secondary")
cancel_button = driver.find_element(By.ID, "cancel_button_secondary") cancel_button = driver.find_element(By.ID, f"cancel_button_secondary")
exp_name = "large.txt" exp_name = "large.txt"
target_file = tmp_path / exp_name target_file = tmp_path / exp_name
@ -429,77 +366,9 @@ async def test_cancel_upload(tmp_path, upload_file: AppHarness, driver: WebDrive
await asyncio.sleep(0.3) await asyncio.sleep(0.3)
cancel_button.click() cancel_button.click()
# Wait a bit for the upload to get cancelled. # look up the backend state and assert on progress
await asyncio.sleep(0.5)
# Get interim progress dicts saved in the on_upload_progress handler.
async def _progress_dicts():
state = await upload_file.get_state(substate_token)
return state.substates[state_name].progress_dicts
# We should have _some_ progress
assert await AppHarness._poll_for_async(_progress_dicts)
# But there should never be a final progress record for a cancelled upload.
for p in await _progress_dicts():
assert p["progress"] != 1
state = await upload_file.get_state(substate_token) state = await upload_file.get_state(substate_token)
file_data = state.substates[state_name]._file_data assert state.substates[state_name].progress_dicts
assert isinstance(file_data, dict) assert exp_name not in state.substates[state_name]._file_data
normalized_file_data = {Path(k).name: v for k, v in file_data.items()}
assert Path(exp_name).name not in normalized_file_data
target_file.unlink() target_file.unlink()
@pytest.mark.asyncio
async def test_upload_download_file(
tmp_path,
upload_file: AppHarness,
driver: WebDriver,
):
"""Submit a file upload and then fetch it with rx.download.
This checks the special case `getBackendURL` logic in the _download event
handler in state.js.
Args:
tmp_path: pytest tmp_path fixture
upload_file: harness for UploadFile app.
driver: WebDriver instance.
"""
assert upload_file.app_instance is not None
poll_for_token(driver, upload_file)
upload_box = driver.find_elements(By.XPATH, "//input[@type='file']")[2]
assert upload_box
upload_button = driver.find_element(By.ID, "upload_button_tertiary")
assert upload_button
exp_name = "test.txt"
exp_contents = "test file contents!"
target_file = tmp_path / exp_name
target_file.write_text(exp_contents)
upload_box.send_keys(str(target_file))
upload_button.click()
# Download via event embedded in frontend code.
download_frontend = driver.find_element(By.ID, "download-frontend")
with poll_for_navigation(driver):
download_frontend.click()
assert urlsplit(driver.current_url).path == f"/{Endpoint.UPLOAD.value}/test.txt"
assert driver.find_element(by=By.TAG_NAME, value="body").text == exp_contents
# Go back and wait for the app to reload.
with poll_for_navigation(driver):
driver.back()
poll_for_token(driver, upload_file)
# Download via backend event handler.
download_backend = driver.find_element(By.ID, "download-backend")
with poll_for_navigation(driver):
download_backend.click()
assert urlsplit(driver.current_url).path == f"/{Endpoint.UPLOAD.value}/test.txt"
assert driver.find_element(by=By.TAG_NAME, value="body").text == exp_contents

68
integration/test_urls.py Executable file
View File

@ -0,0 +1,68 @@
"""Integration tests for all urls in Reflex."""
import os
import re
from pathlib import Path
import pytest
import requests
def check_urls(repo_dir):
"""Check that all URLs in the repo are valid and secure.
Args:
repo_dir: The directory of the repo.
Returns:
A list of errors.
"""
url_pattern = re.compile(r'http[s]?://reflex\.dev[^\s")]*')
errors = []
for root, _dirs, files in os.walk(repo_dir):
if "__pycache__" in root:
continue
for file_name in files:
if not file_name.endswith(".py") and not file_name.endswith(".md"):
continue
file_path = os.path.join(root, file_name)
try:
with open(file_path, "r", encoding="utf-8", errors="ignore") as file:
for line in file:
urls = url_pattern.findall(line)
for url in set(urls):
if url.startswith("http://"):
errors.append(
f"Found insecure HTTP URL: {url} in {file_path}"
)
url = url.strip('"\n')
try:
response = requests.head(
url, allow_redirects=True, timeout=5
)
response.raise_for_status()
except requests.RequestException as e:
errors.append(
f"Error accessing URL: {url} in {file_path} | Error: {e}, , Check your path ends with a /"
)
except Exception as e:
errors.append(f"Error reading file: {file_path} | Error: {e}")
return errors
@pytest.mark.parametrize(
"repo_dir",
[Path(__file__).resolve().parent.parent / "reflex"],
)
def test_find_and_check_urls(repo_dir):
"""Test that all URLs in the repo are valid and secure.
Args:
repo_dir: The directory of the repo.
"""
errors = check_urls(repo_dir)
assert not errors, "\n".join(errors)

View File

@ -7,47 +7,42 @@ from selenium.webdriver.common.by import By
from reflex.testing import AppHarness from reflex.testing import AppHarness
# pyright: reportOptionalMemberAccess=false, reportGeneralTypeIssues=false, reportUnknownMemberType=false
def VarOperations(): def VarOperations():
"""App with var operations.""" """App with var operations."""
from typing import TypedDict from typing import Dict, List
import reflex as rx import reflex as rx
from reflex.vars.base import LiteralVar from reflex.vars.base import LiteralVar
from reflex.vars.sequence import ArrayVar from reflex.vars.sequence import ArrayVar
class Object(rx.Base): class Object(rx.Base):
name: str = "hello" str: str = "hello"
class Person(TypedDict):
name: str
age: int
class VarOperationState(rx.State): class VarOperationState(rx.State):
int_var1: rx.Field[int] = rx.field(10) int_var1: int = 10
int_var2: rx.Field[int] = rx.field(5) int_var2: int = 5
int_var3: rx.Field[int] = rx.field(7) int_var3: int = 7
float_var1: rx.Field[float] = rx.field(10.5) float_var1: float = 10.5
float_var2: rx.Field[float] = rx.field(5.5) float_var2: float = 5.5
list1: rx.Field[list] = rx.field([1, 2]) list1: List = [1, 2]
list2: rx.Field[list] = rx.field([3, 4]) list2: List = [3, 4]
list3: rx.Field[list] = rx.field(["first", "second", "third"]) list3: List = ["first", "second", "third"]
list4: rx.Field[list] = rx.field([Object(name="obj_1"), Object(name="obj_2")]) list4: List = [Object(name="obj_1"), Object(name="obj_2")]
str_var1: rx.Field[str] = rx.field("first") str_var1: str = "first"
str_var2: rx.Field[str] = rx.field("second") str_var2: str = "second"
str_var3: rx.Field[str] = rx.field("ThIrD") str_var3: str = "ThIrD"
str_var4: rx.Field[str] = rx.field("a long string") str_var4: str = "a long string"
dict1: rx.Field[dict[int, int]] = rx.field({1: 2}) dict1: Dict[int, int] = {1: 2}
dict2: rx.Field[dict[int, int]] = rx.field({3: 4}) dict2: Dict[int, int] = {3: 4}
html_str: rx.Field[str] = rx.field("<div>hello</div>") html_str: str = "<div>hello</div>"
people: rx.Field[list[Person]] = rx.field(
[{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
)
app = rx.App(_state=rx.State) app = rx.App(state=rx.State)
@rx.memo @rx.memo
def memo_comp(list1: list[int], int_var1: int, id: str): def memo_comp(list1: List[int], int_var1: int, id: str):
return rx.text(list1, int_var1, id=id) return rx.text(list1, int_var1, id=id)
@rx.memo @rx.memo
@ -383,8 +378,7 @@ def VarOperations():
id="str_contains", id="str_contains",
), ),
rx.text( rx.text(
VarOperationState.str_var1 | VarOperationState.str_var1, VarOperationState.str_var1 | VarOperationState.str_var1, id="str_or_str"
id="str_or_str",
), ),
rx.text( rx.text(
VarOperationState.str_var1 & VarOperationState.str_var2, VarOperationState.str_var1 & VarOperationState.str_var2,
@ -400,8 +394,7 @@ def VarOperations():
id="str_and_int", id="str_and_int",
), ),
rx.text( rx.text(
VarOperationState.str_var1 | VarOperationState.int_var2, VarOperationState.str_var1 | VarOperationState.int_var2, id="str_or_int"
id="str_or_int",
), ),
rx.text( rx.text(
(VarOperationState.str_var1 == VarOperationState.int_var1).to_string(), (VarOperationState.str_var1 == VarOperationState.int_var1).to_string(),
@ -413,8 +406,7 @@ def VarOperations():
), ),
# STR, LIST # STR, LIST
rx.text( rx.text(
VarOperationState.str_var1 | VarOperationState.list1, VarOperationState.str_var1 | VarOperationState.list1, id="str_or_list"
id="str_or_list",
), ),
rx.text( rx.text(
(VarOperationState.str_var1 & VarOperationState.list1).to_string(), (VarOperationState.str_var1 & VarOperationState.list1).to_string(),
@ -430,8 +422,7 @@ def VarOperations():
), ),
# STR, DICT # STR, DICT
rx.text( rx.text(
VarOperationState.str_var1 | VarOperationState.dict1, VarOperationState.str_var1 | VarOperationState.dict1, id="str_or_dict"
id="str_or_dict",
), ),
rx.text( rx.text(
(VarOperationState.str_var1 & VarOperationState.dict1).to_string(), (VarOperationState.str_var1 & VarOperationState.dict1).to_string(),
@ -483,8 +474,7 @@ def VarOperations():
id="list_neq_list", id="list_neq_list",
), ),
rx.text( rx.text(
VarOperationState.list1.contains(1).to_string(), VarOperationState.list1.contains(1).to_string(), id="list_contains"
id="list_contains",
), ),
rx.text(VarOperationState.list4.pluck("name").to_string(), id="list_pluck"), rx.text(VarOperationState.list4.pluck("name").to_string(), id="list_pluck"),
rx.text(VarOperationState.list1.reverse().to_string(), id="list_reverse"), rx.text(VarOperationState.list1.reverse().to_string(), id="list_reverse"),
@ -544,8 +534,7 @@ def VarOperations():
id="dict_neq_dict", id="dict_neq_dict",
), ),
rx.text( rx.text(
VarOperationState.dict1.contains(1).to_string(), VarOperationState.dict1.contains(1).to_string(), id="dict_contains"
id="dict_contains",
), ),
rx.text(VarOperationState.str_var3.lower(), id="str_lower"), rx.text(VarOperationState.str_var3.lower(), id="str_lower"),
rx.text(VarOperationState.str_var3.upper(), id="str_upper"), rx.text(VarOperationState.str_var3.upper(), id="str_upper"),
@ -582,7 +571,7 @@ def VarOperations():
), ),
rx.box( rx.box(
rx.foreach( rx.foreach(
LiteralVar.create(list(range(0, 3))).to(ArrayVar, list[int]), LiteralVar.create(list(range(0, 3))).to(ArrayVar, List[int]),
lambda x: rx.foreach( lambda x: rx.foreach(
ArrayVar.range(x), ArrayVar.range(x),
lambda y: rx.text(VarOperationState.list1[y], as_="p"), lambda y: rx.text(VarOperationState.list1[y], as_="p"),
@ -609,42 +598,6 @@ def VarOperations():
), ),
id="foreach_in_match", id="foreach_in_match",
), ),
# Literal range var in a foreach
rx.box(rx.foreach(range(42, 80, 27), rx.text.span), id="range_in_foreach1"),
rx.box(rx.foreach(range(42, 80, 3), rx.text.span), id="range_in_foreach2"),
rx.box(rx.foreach(range(42, 20, -6), rx.text.span), id="range_in_foreach3"),
rx.box(rx.foreach(range(42, 43, 5), rx.text.span), id="range_in_foreach4"),
# Literal dict in a foreach
rx.box(rx.foreach({"a": 1, "b": 2}, rx.text.span), id="dict_in_foreach1"),
# State Var dict in a foreach
rx.box(
rx.foreach(VarOperationState.dict1, rx.text.span),
id="dict_in_foreach2",
),
rx.box(
rx.foreach(
VarOperationState.dict1.merge(VarOperationState.dict2),
rx.text.span,
),
id="dict_in_foreach3",
),
rx.box(
rx.foreach("abcdef", lambda x: rx.text.span(x + " ")),
id="str_in_foreach",
),
rx.box(
rx.foreach(VarOperationState.str_var1, lambda x: rx.text.span(x + " ")),
id="str_var_in_foreach",
),
rx.box(
rx.foreach(
VarOperationState.people,
lambda person: rx.text.span(
"Hello " + person["name"], person["age"] + 3
),
),
id="typed_dict_in_foreach",
),
) )
@ -660,7 +613,7 @@ def var_operations(tmp_path_factory) -> Generator[AppHarness, None, None]:
""" """
with AppHarness.create( with AppHarness.create(
root=tmp_path_factory.mktemp("var_operations"), root=tmp_path_factory.mktemp("var_operations"),
app_source=VarOperations, app_source=VarOperations, # type: ignore
) as harness: ) as harness:
assert harness.app_instance is not None, "app is not running" assert harness.app_instance is not None, "app is not running"
yield harness yield harness
@ -844,17 +797,6 @@ def test_var_operations(driver, var_operations: AppHarness):
("memo_comp_nested", "345"), ("memo_comp_nested", "345"),
# foreach in a match # foreach in a match
("foreach_in_match", "first\nsecond\nthird"), ("foreach_in_match", "first\nsecond\nthird"),
# literal range in a foreach
("range_in_foreach1", "4269"),
("range_in_foreach2", "42454851545760636669727578"),
("range_in_foreach3", "42363024"),
("range_in_foreach4", "42"),
("dict_in_foreach1", "a1b2"),
("dict_in_foreach2", "12"),
("dict_in_foreach3", "1234"),
("str_in_foreach", "a b c d e f"),
("str_var_in_foreach", "f i r s t"),
("typed_dict_in_foreach", "Hello Alice33Hello Bob28"),
] ]
for tag, expected in tests: for tag, expected in tests:

2858
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,74 +1,88 @@
[tool.poetry] [tool.poetry]
name = "reflex" name = "reflex"
version = "0.7.2dev1" version = "0.6.0a1"
description = "Web apps in pure Python." description = "Web apps in pure Python."
license = "Apache-2.0" license = "Apache-2.0"
authors = [ authors = [
"Nikhil Rao <nikhil@reflex.dev>", "Nikhil Rao <nikhil@reflex.dev>",
"Alek Petuskey <alek@reflex.dev>", "Alek Petuskey <alek@reflex.dev>",
"Masen Furer <masen@reflex.dev>", "Masen Furer <masen@reflex.dev>",
"Elijah Ahianyo <elijah@reflex.dev>", "Elijah Ahianyo <elijah@reflex.dev>",
"Thomas Brandého <thomas@reflex.dev>", "Thomas Brandého <thomas@reflex.dev>",
] ]
readme = "README.md" readme = "README.md"
homepage = "https://reflex.dev" homepage = "https://reflex.dev"
repository = "https://github.com/reflex-dev/reflex" 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 = [
classifiers = ["Development Status :: 4 - Beta"] "web",
"framework",
]
classifiers = [
"Development Status :: 4 - Beta",
]
packages = [
{include = "reflex"}
]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.10, <4.0" python = "^3.8"
dill = ">=0.3.8,<0.4"
fastapi = ">=0.96.0,!=0.111.0,!=0.111.1" fastapi = ">=0.96.0,!=0.111.0,!=0.111.1"
gunicorn = ">=20.1.0,<24.0" gunicorn = ">=20.1.0,<24.0"
jinja2 = ">=3.1.2,<4.0" jinja2 = ">=3.1.2,<4.0"
psutil = ">=5.9.4,<7.0" psutil = ">=5.9.4,<7.0"
pydantic = ">=1.10.21,<3.0" pydantic = ">=1.10.2,<3.0"
python-multipart = ">=0.0.5,<0.1" python-multipart = ">=0.0.5,<0.1"
python-socketio = ">=5.7.0,<6.0" python-socketio = ">=5.7.0,<6.0"
redis = ">=4.3.5,<6.0" redis = ">=4.3.5,<6.0"
rich = ">=13.0.0,<14.0" rich = ">=13.0.0,<14.0"
sqlmodel = ">=0.0.14,<0.1" sqlmodel = ">=0.0.14,<0.1"
typer = ">=0.15.1,<1.0" typer = ">=0.4.2,<1.0"
uvicorn = ">=0.20.0" uvicorn = ">=0.20.0"
starlette-admin = ">=0.11.0,<1.0" starlette-admin = ">=0.11.0,<1.0"
alembic = ">=1.11.1,<2.0" alembic = ">=1.11.1,<2.0"
platformdirs = ">=3.10.0,<5.0" platformdirs = ">=3.10.0,<5.0"
distro = { version = ">=1.8.0,<2.0", platform = "linux" } distro = {version = ">=1.8.0,<2.0", platform = "linux"}
python-engineio = "!=4.6.0" python-engineio = "!=4.6.0"
wrapt = ">=1.17.0,<2.0" wrapt = [
{version = ">=1.14.0,<2.0", python = ">=3.11"},
{version = ">=1.11.0,<2.0", python = "<3.11"},
]
packaging = ">=23.1,<25.0" packaging = ">=23.1,<25.0"
reflex-hosting-cli = ">=0.1.29" reflex-hosting-cli = ">=0.1.2,<2.0"
charset-normalizer = ">=3.3.2,<4.0" charset-normalizer = ">=3.3.2,<4.0"
wheel = ">=0.42.0,<1.0" wheel = ">=0.42.0,<1.0"
build = ">=1.0.3,<2.0" build = ">=1.0.3,<2.0"
setuptools = ">=75.0" setuptools = ">=69.1.1,<70.2"
httpx = ">=0.25.1,<1.0" httpx = ">=0.25.1,<1.0"
twine = ">=4.0.0,<7.0" twine = ">=4.0.0,<6.0"
tomlkit = ">=0.12.4,<1.0" tomlkit = ">=0.12.4,<1.0"
lazy_loader = ">=0.4" lazy_loader = ">=0.4"
typing_extensions = ">=4.6.0" reflex-chakra = ">=0.6.0a6"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest = ">=7.1.2,<9.0" pytest = ">=7.1.2,<8.0"
pytest-mock = ">=3.10.0,<4.0" pytest-mock = ">=3.10.0,<4.0"
pyright = ">=1.1.394, <1.2" pyright = ">=1.1.229,<1.1.335"
darglint = ">=1.8.1,<2.0" darglint = ">=1.8.1,<2.0"
dill = ">=0.3.8"
toml = ">=0.10.2,<1.0" toml = ">=0.10.2,<1.0"
pytest-asyncio = ">=0.24.0" pytest-asyncio = ">=0.20.1,<0.22.0" # https://github.com/pytest-dev/pytest-asyncio/issues/706
pytest-cov = ">=4.0.0,<7.0" pytest-cov = ">=4.0.0,<5.0"
ruff = "0.9.6" ruff = "^0.4.9"
pandas = ">=2.1.1,<3.0" pandas = [
pillow = ">=10.0.0,<12.0" {version = ">=2.1.1,<3.0", python = ">=3.9,<3.13"},
{version = ">=1.5.3,<2.0", python = ">=3.8,<3.9"},
]
pillow = [
{version = ">=10.0.0,<11.0", python = ">=3.8,<4.0"}
]
plotly = ">=5.13.0,<6.0" plotly = ">=5.13.0,<6.0"
asynctest = ">=0.13.0,<1.0" asynctest = ">=0.13.0,<1.0"
pre-commit = ">=3.2.1" pre-commit = {version = ">=3.2.1", python = ">=3.8,<4.0"}
selenium = ">=4.11.0,<5.0" selenium = ">=4.11.0,<5.0"
pytest-benchmark = ">=4.0.0,<6.0" pytest-benchmark = ">=4.0.0,<5.0"
playwright = ">=1.46.0" coloraide=">=3.3.1"
pytest-playwright = ">=0.5.1"
pytest-codspeed = "^3.1.2"
[tool.poetry.scripts] [tool.poetry.scripts]
reflex = "reflex.reflex:cli" reflex = "reflex.reflex:cli"
@ -78,60 +92,16 @@ requires = ["poetry-core>=1.5.1"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.pyright] [tool.pyright]
reportIncompatibleMethodOverride = false
[tool.ruff] [tool.ruff]
target-version = "py310" target-version = "py38"
output-format = "concise" lint.select = ["B", "D", "E", "F", "I", "SIM", "W"]
lint.isort.split-on-trailing-comma = false lint.ignore = ["B008", "D203", "D205", "D213", "D401", "D406", "D407", "E501", "F403", "F405", "F541"]
lint.select = [
"ANN001",
"B",
"C4",
"D",
"E",
"ERA",
"F",
"FURB",
"I",
"N",
"PERF",
"PGH",
"PTH",
"RUF",
"SIM",
"T",
"TRY",
"W",
]
lint.ignore = [
"B008",
"D205",
"E501",
"F403",
"SIM115",
"RUF006",
"RUF008",
"RUF012",
"TRY0",
]
lint.pydocstyle.convention = "google" lint.pydocstyle.convention = "google"
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"] "__init__.py" = ["F401"]
"tests/*.py" = ["ANN001", "D100", "D103", "D104", "B018", "PERF", "T", "N"] "tests/*.py" = ["D100", "D103", "D104", "B018"]
"benchmarks/*.py" = ["ANN001", "D100", "D103", "D104", "B018", "PERF", "T", "N"]
"reflex/.templates/*.py" = ["D100", "D103", "D104"] "reflex/.templates/*.py" = ["D100", "D103", "D104"]
"*.pyi" = ["D301", "D415", "D417", "D418", "E742", "N", "PGH"] "*.pyi" = ["D301", "D415", "D417", "D418", "E742"]
"pyi_generator.py" = ["N802"]
"reflex/constants/*.py" = ["N"]
"*/blank.py" = ["I001"] "*/blank.py" = ["I001"]
[tool.pytest.ini_options]
filterwarnings = "ignore:fields may not start with an underscore:RuntimeWarning"
asyncio_default_fixture_loop_scope = "function"
asyncio_mode = "auto"
[tool.codespell]
skip = "docs/*,*.html,examples/*, *.pyi, poetry.lock"
ignore-words-list = "te, TreeE"

View File

@ -8,11 +8,11 @@ version = "0.0.1"
description = "Reflex custom component {{ module_name }}" description = "Reflex custom component {{ module_name }}"
readme = "README.md" readme = "README.md"
license = { text = "Apache-2.0" } license = { text = "Apache-2.0" }
requires-python = ">=3.10" requires-python = ">=3.8"
authors = [{ name = "", email = "YOUREMAIL@domain.com" }] authors = [{ name = "", email = "YOUREMAIL@domain.com" }]
keywords = ["reflex","reflex-custom-components"] keywords = ["reflex","reflex-custom-components"]
dependencies = ["reflex>={{ reflex_version }}"] dependencies = ["reflex>=0.4.2"]
classifiers = ["Development Status :: 4 - Beta"] classifiers = ["Development Status :: 4 - Beta"]

View File

@ -15,13 +15,7 @@
"devDependencies": { "devDependencies": {
{% for package, version in dev_dependencies.items() %} {% for package, version in dev_dependencies.items() %}
"{{ package }}": "{{ version }}"{% if not loop.last %},{% endif %} "{{ package }}": "{{ version }}"{% if not loop.last %},{% endif %}
{% endfor %}
},
"overrides": {
{% for package, version in overrides.items() %}
"{{ package }}": "{{ version }}"{% if not loop.last %},{% endif %}
{% endfor %} {% endfor %}
} }
} }

View File

@ -1,16 +1,12 @@
{% 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'
{% endblock %} {% endblock %}
{% block declaration %} {% block declaration %}
import { EventLoopProvider, StateProvider, defaultColorMode } from "$/utils/context.js"; import { EventLoopProvider, StateProvider, defaultColorMode } from "/utils/context.js";
import { ThemeProvider } from 'next-themes' import { ThemeProvider } from 'next-themes'
{% for library_alias, library_path in window_libraries %}
import * as {{library_alias}} from "{{library_path}}";
{% endfor %}
{% for custom_code in custom_codes %} {% for custom_code in custom_codes %}
{{custom_code}} {{custom_code}}
@ -19,7 +15,10 @@ 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)}}
@ -27,24 +26,15 @@ function AppWrap({children}) {
} }
export default function MyApp({ Component, pageProps }) { export default function MyApp({ Component, pageProps }) {
React.useEffect(() => {
// Make contexts and state objects available globally for dynamic eval'd components
let windowImports = {
{% for library_alias, library_path in window_libraries %}
"{{library_path}}": {{library_alias}},
{% endfor %}
};
window["__reflex"] = windowImports;
}, []);
return ( return (
<ThemeProvider defaultTheme={ defaultColorMode } attribute="class"> <ThemeProvider defaultTheme={ defaultColorMode } attribute="class">
<StateProvider> <AppWrap>
<EventLoopProvider> <StateProvider>
<AppWrap> <EventLoopProvider>
<Component {...pageProps} /> <Component {...pageProps} />
</AppWrap> </EventLoopProvider>
</EventLoopProvider> </StateProvider>
</StateProvider> </AppWrap>
</ThemeProvider> </ThemeProvider>
); );
} }

View File

@ -1,5 +1,5 @@
{% extends "web/pages/base_page.js.jinja2" %} {% extends "web/pages/base_page.js.jinja2" %}
{% from "web/pages/macros.js.jinja2" import renderHooks %}
{% block export %} {% block export %}
{% for component in components %} {% for component in components %}
@ -8,8 +8,23 @@
{% endfor %} {% endfor %}
export const {{component.name}} = memo(({ {{-component.props|join(", ")-}} }) => { export const {{component.name}} = memo(({ {{-component.props|join(", ")-}} }) => {
{{ renderHooks(component.hooks) }} {% if component.name == "CodeBlock" and "language" in component.props %}
if (language) {
(async () => {
try {
const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${language}`);
SyntaxHighlighter.registerLanguage(language, module.default);
} catch (error) {
console.error(`Error importing language module for ${language}:`, error);
}
})();
}
{% endif %}
{% for hook in component.hooks %}
{{ hook }}
{% endfor %}
return( return(
{{utils.render(component.render)}} {{utils.render(component.render)}}
) )

View File

@ -1,5 +1,4 @@
{% 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 %}
@ -9,7 +8,9 @@
{% block export %} {% block export %}
export default function Component() { export default function Component() {
{{ renderHooks(hooks)}} {% for hook in hooks %}
{{ hook }}
{% endfor %}
return ( return (
{{utils.render(render, indent_width=0)}} {{utils.render(render, indent_width=0)}}

View File

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

View File

@ -1,10 +1,18 @@
{% 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}} () {
{{ renderHooksWithMemo(all_hooks, memo_trigger_hooks) }} {% for hook in component._get_all_hooks_internal() %}
{{ hook }}
{% endfor %}
{% for hook in memo_trigger_hooks %}
{{ hook }}
{% endfor %}
{% for hook in component._get_all_hooks() %}
{{ hook }}
{% endfor %}
return ( return (
{{utils.render(component.render(), indent_width=0)}} {{utils.render(component.render(), indent_width=0)}}
) )

View File

@ -36,10 +36,14 @@
{# component: component dictionary #} {# component: component dictionary #}
{% macro render_tag(component) %} {% macro render_tag(component) %}
<{{component.name}} {{- render_props(component.props) }}> <{{component.name}} {{- render_props(component.props) }}>
{{ component.contents }} {%- if component.args is not none -%}
{% for child in component.children %} {{- render_arg_content(component) }}
{{ render(child) }} {%- else -%}
{% endfor %} {{ component.contents }}
{% for child in component.children %}
{{ render(child) }}
{% endfor %}
{%- endif -%}
</{{component.name}}> </{{component.name}}>
{%- endmacro %} {%- endmacro %}
@ -60,7 +64,7 @@
{# Args: #} {# Args: #}
{# component: component dictionary #} {# component: component dictionary #}
{% macro render_iterable_tag(component) %} {% macro render_iterable_tag(component) %}
<>{ {{ component.iterable_state }}.map(({{ component.arg_name }}, {{ component.arg_index }}) => ( <>{ {%- if component.iterable_type == 'dict' -%}Object.entries({{- component.iterable_state }}){%- else -%}{{- component.iterable_state }}{%- endif -%}.map(({{ component.arg_name }}, {{ component.arg_index }}) => (
{% for child in component.children %} {% for child in component.children %}
{{ render(child) }} {{ render(child) }}
{% endfor %} {% endfor %}
@ -86,11 +90,11 @@
{% for condition in case[:-1] %} {% for condition in case[:-1] %}
case JSON.stringify({{ condition._js_expr }}): case JSON.stringify({{ condition._js_expr }}):
{% endfor %} {% endfor %}
return {{ render(case[-1]) }}; return {{ case[-1] }};
break; break;
{% endfor %} {% endfor %}
default: default:
return {{ render(component.default) }}; return {{ component.default }};
break; break;
} }
})() })()

View File

@ -1,5 +1,5 @@
import { createContext, useContext, useMemo, useReducer, useState } from "react" import { createContext, useContext, useMemo, useReducer, useState } from "react"
import { applyDelta, Event, hydrateClientStorage, useEventLoop, refs } from "$/utils/state.js" import { applyDelta, Event, hydrateClientStorage, useEventLoop, refs } from "/utils/state.js"
{% if initial_state %} {% if initial_state %}
export const initialState = {{ initial_state|json_dumps }} export const initialState = {{ initial_state|json_dumps }}
@ -28,7 +28,7 @@ export const state_name = "{{state_name}}"
export const exception_state_name = "{{const.frontend_exception_state}}" export const exception_state_name = "{{const.frontend_exception_state}}"
// These events are triggered on initial load and each page navigation. // Theses events are triggered on initial load and each page navigation.
export const onLoadInternalEvent = () => { export const onLoadInternalEvent = () => {
const internal_events = []; const internal_events = [];
@ -59,8 +59,6 @@ export const initialEvents = () => [
{% else %} {% else %}
export const state_name = undefined export const state_name = undefined
export const exception_state_name = undefined
export const onLoadInternalEvent = () => [] export const onLoadInternalEvent = () => []
export const initialEvents = () => [] export const initialEvents = () => []
@ -78,9 +76,9 @@ export function UploadFilesProvider({ children }) {
return newFilesById return newFilesById
}) })
return ( return (
<UploadFilesContext value={[filesById, setFilesById]}> <UploadFilesContext.Provider value={[filesById, setFilesById]}>
{children} {children}
</UploadFilesContext> </UploadFilesContext.Provider>
) )
} }
@ -92,9 +90,9 @@ export function EventLoopProvider({ children }) {
clientStorage, clientStorage,
) )
return ( return (
<EventLoopContext value={[addEvents, connectErrors]}> <EventLoopContext.Provider value={[addEvents, connectErrors]}>
{children} {children}
</EventLoopContext> </EventLoopContext.Provider>
) )
} }
@ -112,13 +110,13 @@ export function StateProvider({ children }) {
return ( return (
{% for state_name in initial_state %} {% for state_name in initial_state %}
<StateContexts.{{state_name|var_name}} value={ {{state_name|var_name}} }> <StateContexts.{{state_name|var_name}}.Provider value={ {{state_name|var_name}} }>
{% endfor %} {% endfor %}
<DispatchContext value={dispatchers}> <DispatchContext.Provider value={dispatchers}>
{children} {children}
</DispatchContext> </DispatchContext.Provider>
{% for state_name in initial_state|reverse %} {% for state_name in initial_state|reverse %}
</StateContexts.{{state_name|var_name}}> </StateContexts.{{state_name|var_name}}.Provider>
{% endfor %} {% endfor %}
) )
} }

View File

@ -4,21 +4,22 @@ import {
ColorModeContext, ColorModeContext,
defaultColorMode, defaultColorMode,
isDevMode, isDevMode,
lastCompiledTimeStamp, lastCompiledTimeStamp
} from "$/utils/context.js"; } from "/utils/context.js";
export default function RadixThemesColorModeProvider({ children }) { export default function RadixThemesColorModeProvider({ children }) {
const { theme, resolvedTheme, setTheme } = useTheme(); const { theme, resolvedTheme, setTheme } = useTheme();
const [rawColorMode, setRawColorMode] = useState(defaultColorMode); const [rawColorMode, setRawColorMode] = useState(defaultColorMode);
const [resolvedColorMode, setResolvedColorMode] = useState( const [resolvedColorMode, setResolvedColorMode] = useState("dark");
defaultColorMode === "dark" ? "dark" : "light"
);
useEffect(() => { useEffect(() => {
if (isDevMode) { if (isDevMode) {
const lastCompiledTimeInLocalStorage = const lastCompiledTimeInLocalStorage =
localStorage.getItem("last_compiled_time"); localStorage.getItem("last_compiled_time");
if (lastCompiledTimeInLocalStorage !== lastCompiledTimeStamp) { if (
lastCompiledTimeInLocalStorage &&
lastCompiledTimeInLocalStorage !== lastCompiledTimeStamp
) {
// on app startup, make sure the application color mode is persisted correctly. // on app startup, make sure the application color mode is persisted correctly.
setTheme(defaultColorMode); setTheme(defaultColorMode);
localStorage.setItem("last_compiled_time", lastCompiledTimeStamp); localStorage.setItem("last_compiled_time", lastCompiledTimeStamp);
@ -43,10 +44,10 @@ export default function RadixThemesColorModeProvider({ children }) {
setTheme(mode); setTheme(mode);
}; };
return ( return (
<ColorModeContext <ColorModeContext.Provider
value={{ rawColorMode, resolvedColorMode, toggleColorMode, setColorMode }} value={{ rawColorMode, resolvedColorMode, toggleColorMode, setColorMode }}
> >
{children} {children}
</ColorModeContext> </ColorModeContext.Provider>
); );
} }

View File

@ -1,34 +0,0 @@
import { useEffect, useState } from "react"
import { codeToHtml} from "shiki"
/**
* Code component that uses Shiki to convert code to HTML and render it.
*
* @param code - The code to be highlighted.
* @param theme - The theme to be used for highlighting.
* @param language - The language of the code.
* @param transformers - The transformers to be applied to the code.
* @param decorations - The decorations to be applied to the code.
* @param divProps - Additional properties to be passed to the div element.
* @returns The rendered code block.
*/
export function Code ({code, theme, language, transformers, decorations, ...divProps}) {
const [codeResult, setCodeResult] = useState("")
useEffect(() => {
async function fetchCode() {
const result = await codeToHtml(code, {
lang: language,
theme,
transformers,
decorations
});
setCodeResult(result);
}
fetchCode();
}, [code, language, theme, transformers, decorations]
)
return (
<div dangerouslySetInnerHTML={{__html: codeResult}} {...divProps} ></div>
)
}

View File

@ -2,8 +2,7 @@
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"$/*": ["*"],
"@/*": ["public/*"] "@/*": ["public/*"]
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More