Compare commits

..

2 Commits

Author SHA1 Message Date
Elijah
212fa0c49f Imports Benchmark Times Not representative 2024-05-30 17:22:44 +00:00
Elijah
2ea59b6363 Imports Benchmark Times Not representative 2024-05-30 17:16:24 +00:00
756 changed files with 75488 additions and 104928 deletions

1
.github/CODEOWNERS vendored
View File

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

View File

@ -2,6 +2,7 @@
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
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:
# - 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`.
name: 'Setup Reflex build environment'
@ -18,7 +18,7 @@ inputs:
poetry-version:
description: 'Poetry version to install'
required: false
default: '1.8.3'
default: '1.3.1'
run-poetry-install:
description: 'Whether to run poetry install on current dir'
required: false

View File

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

View File

@ -5,7 +5,7 @@ on:
types:
- closed
paths-ignore:
- "**/*.md"
- '**/*.md'
permissions:
contents: read
@ -15,21 +15,22 @@ defaults:
shell: bash
env:
PYTHONIOENCODING: "utf8"
PYTHONIOENCODING: 'utf8'
TELEMETRY_ENABLED: false
NODE_OPTIONS: "--max_old_space_size=8192"
NODE_OPTIONS: '--max_old_space_size=4096'
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PR_TITLE: ${{ github.event.pull_request.title }}
jobs:
reflex-web:
# if: github.event.pull_request.merged == true
# if: github.event.pull_request.merged == true
strategy:
fail-fast: false
matrix:
# Show OS combos first in GUI
os: [ubuntu-latest]
python-version: ["3.12.8"]
node-version: ["18.x"]
python-version: ['3.11.4']
node-version: ['16.x']
runs-on: ${{ matrix.os }}
steps:
@ -48,7 +49,7 @@ jobs:
uses: actions/checkout@v4
with:
repository: reflex-dev/reflex-web
ref: main
ref: reflex-ci
path: reflex-web
- name: Install Requirements for reflex-web
@ -61,16 +62,79 @@ jobs:
run: |
# Check that npm is home
npm -v
poetry run bash benchmarks/lighthouse.sh ./reflex-web prod
poetry run bash scripts/benchmarks/benchmarks.sh ./reflex-web prod
env:
LHCI_GITHUB_APP_TOKEN: $
- name: Run Benchmarks
# Only run if the database creds are available in this context.
run: poetry run python benchmarks/benchmark_lighthouse.py "$GITHUB_SHA" ./integration/benchmarks/.lighthouseci
if: ${{ env.DATABASE_URL }}
run: poetry run python scripts/benchmarks/lighthouse_score_upload.py "$GITHUB_SHA" ./integration/benchmarks/.lighthouseci
env:
GITHUB_SHA: ${{ github.sha }}
reflex-dist-size: # This job is used to calculate the size of the Reflex distribution (wheel file)
simple-apps-benchmarks:
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: Install additional dependencies for DB access
run: poetry run uv pip install psycopg2-binary
- 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.
if: ${{ env.DATABASE_URL }}
run:
poetry run python scripts/benchmarks/simple_app_benchmark_upload.py --os "${{ matrix.os }}"
--python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
--benchmark-json "${{ env.OUTPUT_FILE }}"
--db-url "${{ env.DATABASE_URL }}" --branch-name "${{ github.head_ref || github.ref_name }}"
--event-type "${{ github.event_name }}" --actor "${{ github.actor }}" --pr-id "${{ github.event.pull_request.id }}"
reflex-build-size:
if: github.event.pull_request.merged == true
timeout-minutes: 30
strategy:
@ -81,21 +145,24 @@ jobs:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup_build_env
with:
python-version: 3.12.8
python-version: 3.11.5
run-poetry-install: true
create-venv-at-path: .venv
- name: Install additional dependencies for DB access
run: poetry run uv pip install psycopg2-binary
- name: Build reflex
run: |
poetry build
- name: Upload benchmark results
# Only run if the database creds are available in this context.
if: ${{ env.DATABASE_URL }}
run:
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 }}"
--branch-name "${{ github.head_ref || github.ref_name }}"
--path ./dist
poetry run python scripts/benchmarks/benchmark_reflex_size.py --os ubuntu-latest
--python-version 3.11.5 --commit-sha "${{ github.sha }}" --pr-id "${{ github.event.pull_request.id }}"
--db-url "${{ env.DATABASE_URL }}" --branch-name "${{ github.head_ref || github.ref_name }}"
--measurement-type "reflex-build" --path ./dist
reflex-venv-size: # This job calculates the total size of Reflex and its dependencies
reflex-plus-dependency-size:
if: github.event.pull_request.merged == true
timeout-minutes: 30
strategy:
@ -103,17 +170,13 @@ jobs:
fail-fast: false
matrix:
# Show OS combos first in GUI
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.12.8"]
os: [ubuntu-latest, windows-latest, macos-12]
python-version: ['3.11.5']
runs-on: ${{ matrix.os }}
steps:
- 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
uses: snok/install-poetry@v1
with:
@ -134,10 +197,14 @@ jobs:
run: |
poetry run pip install uv
- name: calculate and upload size
- name: Install additional dependencies for DB access
run: poetry run uv pip install psycopg2-binary
- if: ${{ env.DATABASE_URL }}
name: calculate and upload size
run:
poetry run python benchmarks/benchmark_package_size.py --os "${{ matrix.os }}"
poetry run python scripts/benchmarks/benchmark_reflex_size.py --os "${{ matrix.os }}"
--python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
--pr-id "${{ github.event.pull_request.id }}"
--pr-id "${{ github.event.pull_request.id }}" --db-url "${{ env.DATABASE_URL }}"
--branch-name "${{ github.head_ref || github.ref_name }}"
--path ./.venv
--measurement-type "reflex-package" --path ./.venv

View File

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

@ -13,5 +13,4 @@ jobs:
- name: 'Dependency Review'
uses: actions/dependency-review-action@v4
with:
allow-licenses: Apache-2.0, BSD-2-Clause, BSD-3-Clause, HPND, ISC, MIT, MPL-2.0, Unlicense, Python-2.0, Python-2.0.1, Apache-2.0 AND MIT, BSD-2-Clause AND BSD-3-Clause, Apache-2.0 AND BSD-3-Clause
allow-dependencies-licenses: 'pkg:pypi/lazy-loader'
allow-licenses: Apache-2.0, BSD-2-Clause, BSD-3-Clause, HPND, ISC, MIT, MPL-2.0, PSF-2.0, Unlicense

View File

@ -6,13 +6,13 @@ concurrency:
on:
push:
branches: ["main"]
branches: ['main']
paths-ignore:
- "**/*.md"
- '**/*.md'
pull_request:
branches: ["main"]
branches: ['main']
paths-ignore:
- "**/*.md"
- '**/*.md'
permissions:
contents: read
@ -22,11 +22,9 @@ jobs:
timeout-minutes: 30
strategy:
matrix:
state_manager: ["redis", "memory"]
python-version: ["3.11.11", "3.12.8", "3.13.1"]
split_index: [1, 2]
fail-fast: false
runs-on: ubuntu-22.04
state_manager: ['redis', 'memory']
python-version: ['3.8.18', '3.11.5', '3.12.0']
runs-on: ubuntu-latest
services:
# Label used to access the service container
redis:
@ -47,10 +45,16 @@ jobs:
python-version: ${{ matrix.python-version }}
run-poetry-install: true
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
env:
SCREENSHOT_DIR: /tmp/screenshots
REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }}
run: |
poetry run playwright install chromium
poetry run pytest tests/integration --retries 3 --maxfail=5 --splits 2 --group ${{matrix.split_index}}
poetry run pytest integration
- 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:
push:
branches: ["main"]
branches: ['main']
paths-ignore:
- "**/*.md"
- '**/*.md'
pull_request:
branches: ["main"]
branches: ['main']
paths-ignore:
- "**/*.md"
- '**/*.md'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.id }}
@ -27,13 +27,14 @@ env:
# TODO: can we fix windows encoding natively within reflex? Bug above can hit real users too (less common, but possible)
# - Catch encoding errors when printing logs
# - Best effort print lines that contain illegal chars (map to some default char, etc.)
PYTHONIOENCODING: "utf8"
PYTHONIOENCODING: 'utf8'
TELEMETRY_ENABLED: false
NODE_OPTIONS: "--max_old_space_size=8192"
NODE_OPTIONS: '--max_old_space_size=4096'
DATABASE_URL: ${{ secrets.DATABASE_URL }}
PR_TITLE: ${{ github.event.pull_request.title }}
jobs:
example-counter-and-nba-proxy:
example-counter:
env:
OUTPUT_FILE: import_benchmark.json
timeout-minutes: 30
@ -42,18 +43,22 @@ jobs:
fail-fast: false
matrix:
# Show OS combos first in GUI
os: [ubuntu-latest, windows-latest]
python-version: ['3.10.16', '3.11.11', '3.12.8', '3.13.1']
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.11.11"
python-version: '3.10.13'
- os: windows-latest
python-version: '3.10.16'
python-version: '3.9.18'
- os: windows-latest
python-version: '3.8.18'
include:
- os: windows-latest
python-version: "3.11.9"
- os: windows-latest
python-version: '3.10.11'
- os: windows-latest
python-version: '3.9.13'
- os: windows-latest
python-version: '3.8.10'
runs-on: ${{ matrix.os }}
steps:
@ -73,7 +78,7 @@ jobs:
run: |
poetry run uv pip install -r requirements.txt
- 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
working-directory: ./reflex-examples/counter
run: |
@ -94,25 +99,29 @@ jobs:
# Check that npm is home
npm -v
poetry run bash scripts/integration.sh ./reflex-examples/counter dev
- name: Install requirements for nba proxy example
working-directory: ./reflex-examples/nba-proxy
run: |
poetry run uv pip install -r requirements.txt
- name: Install additional dependencies for DB access
run: poetry run uv pip install psycopg
- name: Check export --backend-only before init for nba-proxy example
working-directory: ./reflex-examples/nba-proxy
run: |
poetry run reflex export --backend-only
- name: Init Website for nba-proxy example
working-directory: ./reflex-examples/nba-proxy
run: |
poetry run reflex init --loglevel debug
- name: Run Website and Check for errors
run: |
# Check that npm is home
npm -v
poetry run bash scripts/integration.sh ./reflex-examples/nba-proxy dev
- name: Measure and upload .web size
if: ${{ env.DATABASE_URL}}
run:
poetry run python scripts/benchmarks/benchmark_reflex_size.py --os "${{ matrix.os }}"
--python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
--pr-id "${{ github.event.pull_request.id }}" --db-url "${{ env.DATABASE_URL }}"
--branch-name "${{ github.head_ref || github.ref_name }}"
--measurement-type "counter-app-dot-web" --path ./reflex-examples/counter/.web
- name: Install hyperfine
run: cargo install hyperfine
- name: Benchmark imports
working-directory: ./reflex-examples/counter
run: hyperfine --warmup 10 "export POETRY_VIRTUALENVS_PATH=../../.venv; poetry run python counter/counter.py" --show-output --export-json "${{ env.OUTPUT_FILE }}" --shell bash
- name: Upload Benchmarks
if : ${{ env.DATABASE_URL }}
run:
poetry run python scripts/benchmarks/benchmark_imports.py --os "${{ matrix.os }}"
--python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
--benchmark-json "./reflex-examples/counter/${{ env.OUTPUT_FILE }}"
--db-url "${{ env.DATABASE_URL }}" --branch-name "${{ github.head_ref || github.ref_name }}"
--event-type "${{ github.event_name }}" --actor "${{ github.actor }}" --pr-id "${{ github.event.pull_request.id }}"
reflex-web:
@ -120,11 +129,11 @@ jobs:
fail-fast: false
matrix:
# Show OS combos first in GUI
os: [ubuntu-latest]
python-version: ["3.11.11", "3.12.8"]
os: [ubuntu-latest, windows-latest, macos-12]
python-version: ['3.10.11', '3.11.4']
env:
REFLEX_WEB_WINDOWS_OVERRIDE: "1"
REFLEX_WEB_WINDOWS_OVERRIDE: '1'
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
@ -143,72 +152,9 @@ jobs:
- name: Install Requirements for 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
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: |
# 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
run: poetry run uv pip install psycopg2-binary
- name: Init Website for reflex-web
working-directory: ./reflex-web
run: poetry run reflex init
@ -217,3 +163,11 @@ jobs:
# Check that npm is home
npm -v
poetry run bash scripts/integration.sh ./reflex-web prod
- name: Measure and upload .web size
if: ${{ env.DATABASE_URL}}
run:
poetry run python scripts/benchmarks/benchmark_reflex_size.py --os "${{ matrix.os }}"
--python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
--pr-id "${{ github.event.pull_request.id }}"
--db-url "${{ env.DATABASE_URL }}" --branch-name "${{ github.head_ref || github.ref_name }}"
--measurement-type "reflex-web-dot-web" --path ./reflex-web/.web

View File

@ -37,8 +37,6 @@ jobs:
path: reflex-examples
- uses: Vampire/setup-wsl@v3
with:
distribution: Ubuntu-24.04
- name: Install Python
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:
pull_request:
branches: ["main"]
branches: ['main']
push:
# Note even though this job is called "pre-commit" and runs "pre-commit", this job will run
# also POST-commit on main also! In case there are mishandled merge conflicts / bad auto-resolves
# when merging into main branch.
branches: ["main"]
branches: ['main']
jobs:
pre-commit:
@ -23,7 +23,7 @@ jobs:
with:
# running vs. one version of Python is OK
# i.e. ruff, black, etc.
python-version: 3.12.8
python-version: 3.11.5
run-poetry-install: true
create-venv-at-path: .venv
# 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
# cwd is repo root
docker build -f tests/integration/init-test/Dockerfile -t reflex-init-test tests/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 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/integration/init-test/in_docker_test_script.sh

View File

@ -6,13 +6,13 @@ concurrency:
on:
push:
branches: ["main"]
branches: ['main']
paths-ignore:
- "**/*.md"
- '**/*.md'
pull_request:
branches: ["main"]
branches: ['main']
paths-ignore:
- "**/*.md"
- '**/*.md'
permissions:
contents: read
@ -27,21 +27,24 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.10.16", "3.11.11", "3.12.8", "3.13.1"]
os: [ubuntu-latest, windows-latest, macos-12]
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
exclude:
- os: windows-latest
python-version: "3.11.11"
python-version: '3.10.13'
- os: windows-latest
python-version: "3.10.16"
python-version: '3.9.18'
- os: windows-latest
python-version: '3.8.18'
include:
- os: windows-latest
python-version: "3.11.9"
python-version: '3.10.11'
- 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 }}
# Service containers to run with `runner-job`
services:
# Label used to access the service container
@ -66,44 +69,17 @@ jobs:
- name: Run unit tests
run: |
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
if: ${{ matrix.os == 'ubuntu-latest' }}
run: |
export PYTHONUNBUFFERED=1
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
- 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=
- name: Generate coverage report
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=
poetry run pytest tests --cov --no-cov-on-fail --cov-report=
- run: poetry run coverage html

4
.gitignore vendored
View File

@ -3,8 +3,6 @@
assets/external/*
dist/*
examples/
.web
.states
.idea
.vscode
.coverage
@ -14,5 +12,3 @@ venv
requirements.txt
.pyi_generator_last_run
.pyi_generator_diff
reflex.db
.codspeed

View File

@ -3,36 +3,19 @@ fail_fast: true
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.9.6
rev: v0.1.0
hooks:
- id: ruff-format
args: [reflex, tests]
args: [integration, reflex, tests]
- id: ruff
args: ["--fix", "--exit-non-zero-on-fix"]
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.
- repo: local
hooks:
- id: update-pyi-files
name: update-pyi-files
always_run: true
language: system
require_serial: true
description: 'Update pyi files as needed'
entry: python3 scripts/make_pyi.py
- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.393
rev: v1.1.313
hooks:
- id: pyright
args: [reflex, tests]
args: [integration, reflex, tests]
language: system
- repo: https://github.com/terrencepreilly/darglint
@ -41,3 +24,11 @@ repos:
- id: darglint
exclude: '^reflex/reflex.py'
- repo: local
hooks:
- id: update-pyi-files
name: update-pyi-files
always_run: true
language: system
description: 'Update pyi files as needed'
entry: python scripts/make_pyi.py

View File

@ -5,7 +5,7 @@
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
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
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:**
- 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).
**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%.
``` 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.
@ -80,14 +80,14 @@ poetry run pyright reflex tests
find reflex tests -name "*.py" -not -path reflex/reflex.py | xargs poetry run darglint
```
Finally, run `ruff` to format your code.
Finally, run `black` to format your code.
``` bash
poetry run ruff format .
poetry run black reflex tests
```
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.
Consider installing git pre-commit hooks so Ruff, Pyright, Darglint and Black will run automatically before each commit.
Note that pre-commit will only be installed when you use a Python version >= 3.8.
``` bash
pre-commit install

View File

@ -10,6 +10,7 @@
### **✨ 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)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.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)
[![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)
---
@ -34,7 +35,7 @@ See our [architecture page](https://reflex.dev/blog/2024-03-21-reflex-architectu
## ⚙️ Installation
Open a terminal and run (Requires Python 3.10+):
Open a terminal and run (Requires Python 3.8+):
```bash
pip install reflex
@ -228,7 +229,7 @@ You can create a multi-page app by adding more pages.
<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>
@ -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 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:

View File

@ -1,75 +0,0 @@
"""Extracts the Lighthouse scores from the JSON files in the specified directory and inserts them into the database."""
from __future__ import annotations
import json
import sys
from pathlib import Path
from utils import send_data_to_posthog
def insert_benchmarking_data(
lighthouse_data: dict,
commit_sha: str,
):
"""Insert the benchmarking data into the database.
Args:
lighthouse_data: The Lighthouse data to insert.
commit_sha: The commit SHA to insert.
"""
properties = {
"distinct_id": commit_sha,
"lighthouse_data": lighthouse_data,
}
# Send the data to PostHog
send_data_to_posthog("lighthouse_benchmark", properties)
def get_lighthouse_scores(directory_path: str | Path) -> dict:
"""Extracts the Lighthouse scores from the JSON files in the specified directory.
Args:
directory_path (str): The path to the directory containing the JSON files.
Returns:
dict: The Lighthouse scores.
"""
scores = {}
directory_path = Path(directory_path)
try:
for filename in directory_path.iterdir():
if filename.suffix == ".json" and filename.stem != "manifest":
data = json.loads(filename.read_text())
# Extract scores and add them to the dictionary with the filename as key
scores[data["finalUrl"].replace("http://localhost:3000/", "/")] = {
"performance_score": data["categories"]["performance"]["score"],
"accessibility_score": data["categories"]["accessibility"]["score"],
"best_practices_score": data["categories"]["best-practices"][
"score"
],
"seo_score": data["categories"]["seo"]["score"],
}
except Exception as e:
return {"error": e}
return scores
def main():
"""Runs the benchmarks and inserts the results into the database."""
# Get the commit SHA and JSON directory from the command line arguments
commit_sha = sys.argv[1]
json_dir = sys.argv[2]
# Get the Lighthouse scores
lighthouse_scores = get_lighthouse_scores(json_dir)
# Insert the data into the database
insert_benchmarking_data(lighthouse_scores, commit_sha)
if __name__ == "__main__":
main()

View File

@ -1,135 +0,0 @@
"""Checks the size of a specific directory and uploads result to Posthog."""
import argparse
import os
from pathlib import Path
from utils import get_directory_size, get_python_version, send_data_to_posthog
def get_package_size(venv_path: Path, os_name):
"""Get the size of a specified package.
Args:
venv_path: The path to the venv.
os_name: Name of os.
Returns:
The total size of the package in bytes.
Raises:
ValueError: when venv does not exist or python version is None.
"""
python_version = get_python_version(venv_path, os_name)
print("Python version:", python_version)
if python_version is None:
raise ValueError("Error: Failed to determine Python version.")
is_windows = "windows" in os_name
package_dir: Path = (
venv_path / "lib" / f"python{python_version}" / "site-packages"
if not is_windows
else venv_path / "Lib" / "site-packages"
)
if not package_dir.exists():
raise ValueError(
"Error: Virtual environment does not exist or is not activated."
)
total_size = get_directory_size(package_dir)
return total_size
def insert_benchmarking_data(
os_type_version: str,
python_version: str,
commit_sha: str,
pr_title: str,
branch_name: str,
pr_id: str,
path: str,
):
"""Insert the benchmarking data into PostHog.
Args:
os_type_version: The OS type and version to insert.
python_version: The Python version to insert.
commit_sha: The commit SHA to insert.
pr_title: The PR title to insert.
branch_name: The name of the branch.
pr_id: The id of the PR.
path: The path to the dir or file to check size.
"""
if "./dist" in path:
size = get_directory_size(Path(path))
else:
size = get_package_size(Path(path), os_type_version)
# Prepare the event data
properties = {
"path": path,
"os": os_type_version,
"python_version": python_version,
"distinct_id": commit_sha,
"pr_title": pr_title,
"branch_name": branch_name,
"pr_id": pr_id,
"size_mb": round(
size / (1024 * 1024), 3
), # save size in MB and round to 3 places
}
send_data_to_posthog("package_size", properties)
def main():
"""Runs the benchmarks and inserts the results."""
parser = argparse.ArgumentParser(description="Run benchmarks and process results.")
parser.add_argument(
"--os", help="The OS type and version to insert into the database."
)
parser.add_argument(
"--python-version", help="The Python version to insert into the database."
)
parser.add_argument(
"--commit-sha", help="The commit SHA to insert into the database."
)
parser.add_argument(
"--pr-title",
help="The PR title to insert into the database.",
)
parser.add_argument(
"--branch-name",
help="The current branch",
required=True,
)
parser.add_argument(
"--pr-id",
help="The pr id",
required=True,
)
parser.add_argument(
"--path",
help="The path to the vnenv.",
required=True,
)
args = parser.parse_args()
# Get the PR title from env or the args. For the PR merge or push event, there is no PR title, leaving it empty.
pr_title = args.pr_title or os.getenv("PR_TITLE", "")
# Insert the data into the database
insert_benchmarking_data(
os_type_version=args.os,
python_version=args.python_version,
commit_sha=args.commit_sha,
pr_title=pr_title,
branch_name=args.branch_name,
pr_id=args.pr_id,
path=args.path,
)
if __name__ == "__main__":
main()

View File

@ -1,106 +0,0 @@
"""Checks the size of a specific directory and uploads result to Posthog."""
import argparse
import os
from pathlib import Path
from utils import get_directory_size, send_data_to_posthog
def insert_benchmarking_data(
os_type_version: str,
python_version: str,
app_name: str,
commit_sha: str,
pr_title: str,
branch_name: str,
pr_id: str,
path: str,
):
"""Insert the benchmarking data into PostHog.
Args:
app_name: The name of the app being measured.
os_type_version: The OS type and version to insert.
python_version: The Python version to insert.
commit_sha: The commit SHA to insert.
pr_title: The PR title to insert.
branch_name: The name of the branch.
pr_id: The id of the PR.
path: The path to the dir or file to check size.
"""
size = get_directory_size(Path(path))
# Prepare the event data
properties = {
"app_name": app_name,
"os": os_type_version,
"python_version": python_version,
"distinct_id": commit_sha,
"pr_title": pr_title,
"branch_name": branch_name,
"pr_id": pr_id,
"size_mb": round(
size / (1024 * 1024), 3
), # save size in MB and round to 3 places
}
send_data_to_posthog("web-size", properties)
def main():
"""Runs the benchmarks and inserts the results."""
parser = argparse.ArgumentParser(description="Run benchmarks and process results.")
parser.add_argument(
"--os", help="The OS type and version to insert into the database."
)
parser.add_argument(
"--python-version", help="The Python version to insert into the database."
)
parser.add_argument(
"--commit-sha", help="The commit SHA to insert into the database."
)
parser.add_argument(
"--pr-title",
help="The PR title to insert into the database.",
)
parser.add_argument(
"--branch-name",
help="The current branch",
required=True,
)
parser.add_argument(
"--app-name",
help="The name of the app measured.",
required=True,
)
parser.add_argument(
"--pr-id",
help="The pr id",
required=True,
)
parser.add_argument(
"--path",
help="The current path to app to check.",
required=True,
)
args = parser.parse_args()
# Get the PR title from env or the args. For the PR merge or push event, there is no PR title, leaving it empty.
pr_title = args.pr_title or os.getenv("PR_TITLE", "")
# Insert the data into the database
insert_benchmarking_data(
app_name=args.app_name,
os_type_version=args.os,
python_version=args.python_version,
commit_sha=args.commit_sha,
pr_title=pr_title,
branch_name=args.branch_name,
pr_id=args.pr_id,
path=args.path,
)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,373 @@
"""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
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(constants.Dirs.WEB_PAGES, keep_files=["_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(constants.Dirs.WEB_PAGES, keep_files=["_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(constants.Dirs.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,576 @@
"""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
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.chakra.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(constants.Dirs.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(constants.Dirs.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(constants.Dirs.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(constants.Dirs.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(constants.Dirs.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

@ -1,74 +0,0 @@
"""Utility functions for the benchmarks."""
import os
import subprocess
from pathlib import Path
import httpx
from httpx import HTTPError
def get_python_version(venv_path: Path, os_name):
"""Get the python version of python in a virtual env.
Args:
venv_path: Path to virtual environment.
os_name: Name of os.
Returns:
The python version.
"""
python_executable = (
venv_path / "bin" / "python"
if "windows" not in os_name
else venv_path / "Scripts" / "python.exe"
)
try:
output = subprocess.check_output(
[str(python_executable), "--version"], stderr=subprocess.STDOUT
)
python_version = output.decode("utf-8").strip().split()[1]
return ".".join(python_version.split(".")[:-1])
except subprocess.CalledProcessError:
return None
def get_directory_size(directory: Path):
"""Get the size of a directory in bytes.
Args:
directory: The directory to check.
Returns:
The size of the dir in bytes.
"""
total_size = 0
for dirpath, _, filenames in os.walk(directory):
for f in filenames:
fp = Path(dirpath) / f
total_size += fp.stat().st_size
return total_size
def send_data_to_posthog(event, properties):
"""Send data to PostHog.
Args:
event: The event to send.
properties: The properties to send.
Raises:
HTTPError: When there is an error sending data to PostHog.
"""
event_data = {
"api_key": "phc_JoMo0fOyi0GQAooY3UyO9k0hebGkMyFJrrCw1Gt5SGb",
"event": event,
"properties": properties,
}
with httpx.Client() as client:
response = client.post("https://app.posthog.com/capture/", json=event_data)
if response.status_code != 200:
raise HTTPError(
f"Error sending data to PostHog: {response.status_code} - {response.text}"
)

View File

@ -1,8 +1,5 @@
# This Dockerfile is used to deploy a simple single-container Reflex app instance.
FROM python:3.13
RUN apt-get update && apt-get install -y redis-server && rm -rf /var/lib/apt/lists/*
ENV REDIS_URL=redis://localhost PYTHONUNBUFFERED=1
FROM python:3.11
# Copy local context to `/app` inside container (see .dockerignore)
WORKDIR /app
@ -21,6 +18,4 @@ RUN reflex export --frontend-only --no-zip
STOPSIGNAL SIGKILL
# Always apply migrations before starting the backend.
CMD [ -d alembic ] && reflex db migrate; \
redis-server --daemonize yes && \
exec reflex run --env prod
CMD [ -d alembic ] && reflex db migrate; reflex run --env prod

View File

@ -1,30 +1,133 @@
# Reflex Docker Examples
# Reflex Docker Container
This directory contains several examples of how to deploy Reflex apps using docker.
This example describes how to create and use a container image for Reflex with your own code.
In all cases, ensure that your `requirements.txt` file is up to date and
includes the `reflex` package.
## Update Requirements
## `simple-two-port`
The `requirements.txt` includes the reflex package which is needed to install
Reflex framework. If you use additional packages in your project you have to add
this in the `requirements.txt` first. Copy the `Dockerfile`, `.dockerignore` and
the `requirements.txt` file in your project folder.
The most basic production deployment exposes two HTTP ports and relies on an
existing load balancer to forward the traffic appropriately.
## Build Simple Reflex Container Image
## `simple-one-port`
The main `Dockerfile` is intended to build a very simple, single container deployment that runs
the Reflex frontend and backend together, exposing ports 3000 and 8000.
This deployment exports the frontend statically and serves it via a single HTTP
port using Caddy. This is useful for platforms that only support a single port
or where running a node server in the container is undesirable.
To build your container image run the following command:
## `production-compose`
```bash
docker build -t reflex-app:latest .
```
This deployment is intended for use with a standalone VPS that is only hosting a
single Reflex app. It provides the entire stack in a single `compose.yaml`
including a webserver, one or more backend instances, redis, and a postgres
database.
## Start Container Service
## `production-app-platform`
Finally, you can start your Reflex container service as follows:
This example deployment is intended for use with App hosting platforms, like
Azure, AWS, or Google Cloud Run. It is the backend of the deployment, which
depends on a separately hosted redis instance and static frontend deployment.
```bash
docker run -it --rm -p 3000:3000 -p 8000:8000 --name app reflex-app:latest
```
It may take a few seconds for the service to become available.
Access your app at http://localhost:3000.
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.
# Production Service with Docker Compose and Caddy
An example production deployment uses automatic TLS with Caddy serving static files
for the frontend and proxying requests to both the frontend and backend.
Copy the following files to your project directory:
* `compose.yaml`
* `compose.prod.yaml`
* `compose.tools.yaml`
* `prod.Dockerfile`
* `Caddy.Dockerfile`
* `Caddyfile`
The production app container, based on `prod.Dockerfile`, builds and exports the
frontend statically (to be served by Caddy). The resulting image only runs the
backend service.
The `webserver` service, based on `Caddy.Dockerfile`, copies the static frontend
and `Caddyfile` into the container to configure the reverse proxy routes that will
forward requests to the backend service. Caddy will automatically provision TLS
for localhost or the domain specified in the environment variable `DOMAIN`.
This type of deployment should use less memory and be more performant since
nodejs is not required at runtime.
## Customize `Caddyfile` (optional)
If the app uses additional backend API routes, those should be added to the
`@backend_routes` path matcher to ensure they are forwarded to the backend.
## Build Reflex Production Service
During build, set `DOMAIN` environment variable to the domain where the app will
be hosted! (Do not include http or https, it will always use https).
**If `DOMAIN` is not provided, the service will default to `localhost`.**
```bash
DOMAIN=example.com docker compose build
```
This will build both the `app` service from the `prod.Dockerfile` and the `webserver`
service via `Caddy.Dockerfile`.
## Run Reflex Production Service
```bash
DOMAIN=example.com docker compose up
```
The app should be available at the specified domain via HTTPS. Certificate
provisioning will occur automatically and may take a few minutes.
### Data Persistence
Named docker volumes are used to persist the app database (`db-data`),
uploaded_files (`upload-data`), and caddy TLS keys and certificates
(`caddy-data`).
## More Robust Deployment
For a more robust deployment, consider bringing the service up with
`compose.prod.yaml` which includes postgres database and redis cache, allowing
the backend to run with multiple workers and service more requests.
```bash
DOMAIN=example.com docker compose -f compose.yaml -f compose.prod.yaml up -d
```
Postgres uses its own named docker volume for data persistence.
## Admin Tools
When needed, the services in `compose.tools.yaml` can be brought up, providing
graphical database administration (Adminer on http://localhost:8080) and a
redis cache browser (redis-commander on http://localhost:8081). It is not recommended
to deploy these services if they are not in active use.
```bash
DOMAIN=example.com docker compose -f compose.yaml -f compose.prod.yaml -f compose.tools.yaml up -d
```
# Container Hosting
Most container hosting services automatically terminate TLS and expect the app
to be listening on a single port (typically `$PORT`).
To host a Reflex app on one of these platforms, like Google Cloud Run, Render,
Railway, etc, use `app.Dockerfile` to build a single image containing a reverse
proxy that will serve that frontend as static files and proxy requests to the
backend for specific endpoints.
If the chosen platform does not support buildx and thus heredoc, you can copy
the Caddyfile configuration into a separate Caddyfile in the root of the
project.

View File

@ -4,19 +4,37 @@
# 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
FROM python:3.11
# 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
ENV PORT=$PORT API_URL=${API_URL:-http://localhost:$PORT} REDIS_URL=redis://localhost PYTHONUNBUFFERED=1
ENV PORT=$PORT API_URL=${API_URL:-http://localhost:$PORT}
# 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/*
# Install Caddy server inside image
RUN apt-get update -y && apt-get install -y caddy && rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Create a simple Caddyfile to serve as reverse proxy
RUN cat > Caddyfile <<EOF
:{\$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
}
EOF
# Copy local context to `/app` inside container (see .dockerignore)
COPY . .
@ -36,6 +54,4 @@ 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
caddy start && reflex run --env prod --backend-only --loglevel debug

View File

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

View File

@ -12,6 +12,7 @@ services:
DB_URL: sqlite:///data/reflex.db
build:
context: .
dockerfile: prod.Dockerfile
volumes:
- db-data:/app/data
- upload-data:/app/uploaded_files

View File

@ -2,11 +2,11 @@
# instance of a Reflex app.
# 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
RUN /install.sh && rm /install.sh
@ -35,18 +35,17 @@ RUN rm -rf .web && mkdir .web
RUN mv /tmp/_static .web/_static
# Stage 2: copy artifacts into slim image
FROM python:3.13-slim
FROM python:3.11-slim
WORKDIR /app
RUN adduser --disabled-password --home /app reflex
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/*
USER reflex
ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1
ENV PATH="/app/.venv/bin:$PATH"
# Needed until Reflex properly passes SIGTERM on backend.
STOPSIGNAL SIGKILL
# Always apply migrations before starting the backend.
CMD [ -d alembic ] && reflex db migrate; \
exec reflex run --env prod --backend-only
CMD reflex db migrate && reflex run --env prod --backend-only

View File

@ -1,5 +0,0 @@
.web
.git
__pycache__/*
Dockerfile
uploaded_files

View File

@ -1,65 +0,0 @@
# This docker file is intended to be used with container hosting services
#
# After deploying this image, get the URL pointing to the backend service
# and run API_URL=https://path-to-my-container.example.com reflex export frontend
# then copy the contents of `frontend.zip` to your static file server (github pages, s3, etc).
#
# Azure Static Web App example:
# npx @azure/static-web-apps-cli deploy --env production --app-location .web/_static
#
# For dynamic routes to function properly, ensure that 404s are redirected to /404 on the
# static file host (for github pages, this works out of the box; remember to create .nojekyll).
#
# For azure static web apps, add `staticwebapp.config.json` to to `.web/_static` with the following:
# {
# "responseOverrides": {
# "404": {
# "rewrite": "/404.html"
# }
# }
# }
#
# Note: many container hosting platforms require amd64 images, so when building on an M1 Mac
# for example, pass `docker build --platform=linux/amd64 ...`
# Stage 1: init
FROM python:3.13 as init
ARG uv=/root/.local/bin/uv
# Install `uv` for faster package bootstrapping
ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh
RUN /install.sh && rm /install.sh
# Copy local context to `/app` inside container (see .dockerignore)
WORKDIR /app
COPY . .
RUN mkdir -p /app/data /app/uploaded_files
# Create virtualenv which will be copied into final container
ENV VIRTUAL_ENV=/app/.venv
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
RUN $uv venv
# Install app requirements and reflex inside virtualenv
RUN $uv pip install -r requirements.txt
# Deploy templates and prepare app
RUN reflex init
# Stage 2: copy artifacts into slim image
FROM python:3.13-slim
WORKDIR /app
RUN adduser --disabled-password --home /app reflex
COPY --chown=reflex --from=init /app /app
# Install libpq-dev for psycopg (skip if not using postgres).
RUN apt-get update -y && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*
USER reflex
ENV PATH="/app/.venv/bin:$PATH" PYTHONUNBUFFERED=1
# Needed until Reflex properly passes SIGTERM on backend.
STOPSIGNAL SIGKILL
# Always apply migrations before starting the backend.
CMD [ -d alembic ] && reflex db migrate; \
exec reflex run --env prod --backend-only --backend-port ${PORT:-8000}

View File

@ -1,113 +0,0 @@
# production-app-platform
This example deployment is intended for use with App hosting platforms, like
Azure, AWS, or Google Cloud Run.
## Architecture
The production deployment consists of a few pieces:
* Backend container - built by `Dockerfile` Runs the Reflex backend
service on port 8000 and is scalable to multiple instances.
* Redis container - A single instance the standard `redis` docker image should
share private networking with the backend
* Static frontend - HTML/CSS/JS files that are hosted via a CDN or static file
server. This is not included in the docker image.
## Deployment
These general steps do not cover the specifics of each platform, but all platforms should
support the concepts described here.
### Vnet
All containers in the deployment should be hooked up to the same virtual private
network so they can access the redis service and optionally the database server.
The vnet should not be exposed to the internet, use an ingress rule to terminate
TLS at the load balancer and forward the traffic to a backend service replica.
### Redis
Deploy a `redis` instance on the vnet.
### Backend
The backend is built by the `Dockerfile` in this directory. When deploying the
backend, be sure to set REDIS_URL=redis://internal-redis-hostname to connect to
the redis service.
### Ingress
Configure the load balancer for the app to forward traffic to port 8000 on the
backend service replicas. Most platforms will generate an ingress hostname
automatically. Make sure when you access the ingress endpoint on `/ping` that it
returns "pong", indicating that the backend is up an available.
### Frontend
The frontend should be hosted on a static file server or CDN.
**Important**: when exporting the frontend, set the API_URL environment variable
to the ingress hostname of the backend service.
If you will host the frontend from a path other than the root, set the
`FRONTEND_PATH` environment variable appropriately when exporting the frontend.
Most static hosts will automatically use the `/404.html` file to handle 404
errors. _This is essential for dynamic routes to work correctly._ Ensure that
missing routes return the `/404.html` content to the user if this is not the
default behavior.
_For Github Pages_: ensure the file `.nojekyll` is present in the root of the repo
to avoid special processing of underscore-prefix directories, like `_next`.
## Platform Notes
The following sections are currently a work in progress and may be incomplete.
### Azure
In the Azure load balancer, per-message deflate is not supported. Add the following
to your `rxconfig.py` to workaround this issue.
```python
import uvicorn.workers
import reflex as rx
class NoWSPerMessageDeflate(uvicorn.workers.UvicornH11Worker):
CONFIG_KWARGS = {
**uvicorn.workers.UvicornH11Worker.CONFIG_KWARGS,
"ws_per_message_deflate": False,
}
config = rx.Config(
app_name="my_app",
gunicorn_worker_class="rxconfig.NoWSPerMessageDeflate",
)
```
#### Persistent Storage
If you need to use a database or upload files, you cannot save them to the
container volume. Use Azure Files and mount it into the container at /app/uploaded_files.
#### Resource Types
* Create a new vnet with 10.0.0.0/16
* Create a new subnet for redis, database, and containers
* Deploy redis as a Container Instances
* Deploy database server as "Azure Database for PostgreSQL"
* Create a new database for the app
* Set db-url as a secret containing the db user/password connection string
* Deploy Storage account for uploaded files
* Enable access from the vnet and container subnet
* Create a new file share
* In the environment, create a new files share (get the storage key)
* Deploy the backend as a Container App
* Create a custom Container App Environment linked up to the same vnet as the redis container.
* Set REDIS_URL and DB_URL environment variables
* Add the volume from the environment
* Add the volume mount to the container
* Deploy the frontend as a Static Web App

View File

@ -1,75 +0,0 @@
# production-compose
This example production deployment uses automatic TLS with Caddy serving static
files for the frontend and proxying requests to both the frontend and backend.
It is intended for use with a standalone VPS that is only hosting a single
Reflex app.
The production app container (`Dockerfile`), builds and exports the frontend
statically (to be served by Caddy). The resulting image only runs the backend
service.
The `webserver` service, based on `Caddy.Dockerfile`, copies the static frontend
and `Caddyfile` into the container to configure the reverse proxy routes that will
forward requests to the backend service. Caddy will automatically provision TLS
for localhost or the domain specified in the environment variable `DOMAIN`.
This type of deployment should use less memory and be more performant since
nodejs is not required at runtime.
## Customize `Caddyfile` (optional)
If the app uses additional backend API routes, those should be added to the
`@backend_routes` path matcher to ensure they are forwarded to the backend.
## Build Reflex Production Service
During build, set `DOMAIN` environment variable to the domain where the app will
be hosted! (Do not include http or https, it will always use https).
**If `DOMAIN` is not provided, the service will default to `localhost`.**
```bash
DOMAIN=example.com docker compose build
```
This will build both the `app` service from the `prod.Dockerfile` and the `webserver`
service via `Caddy.Dockerfile`.
## Run Reflex Production Service
```bash
DOMAIN=example.com docker compose up
```
The app should be available at the specified domain via HTTPS. Certificate
provisioning will occur automatically and may take a few minutes.
### Data Persistence
Named docker volumes are used to persist the app database (`db-data`),
uploaded_files (`upload-data`), and caddy TLS keys and certificates
(`caddy-data`).
## More Robust Deployment
For a more robust deployment, consider bringing the service up with
`compose.prod.yaml` which includes postgres database and redis cache, allowing
the backend to run with multiple workers and service more requests.
```bash
DOMAIN=example.com docker compose -f compose.yaml -f compose.prod.yaml up -d
```
Postgres uses its own named docker volume for data persistence.
## Admin Tools
When needed, the services in `compose.tools.yaml` can be brought up, providing
graphical database administration (Adminer on http://localhost:8080) and a
redis cache browser (redis-commander on http://localhost:8081). It is not recommended
to deploy these services if they are not in active use.
```bash
DOMAIN=example.com docker compose -f compose.yaml -f compose.prod.yaml -f compose.tools.yaml up -d
```

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

@ -0,0 +1 @@
reflex

View File

@ -1,5 +0,0 @@
.web
.git
__pycache__/*
Dockerfile
uploaded_files

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,36 +0,0 @@
# simple-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.
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.
For platforms which only terminate TLS to a single port, this container can be
deployed instead of the `simple-two-port` example.
## Build
```console
docker build -t reflex-simple-one-port .
```
## Run
```console
docker run -p 8080:8080 reflex-simple-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

@ -1,5 +0,0 @@
.web
.git
__pycache__/*
Dockerfile
uploaded_files

View File

@ -1,44 +0,0 @@
# simple-two-port
This docker deployment runs Reflex in prod mode, exposing two HTTP ports:
* `3000` - node NextJS server using optimized production build
* `8000` - python gunicorn server hosting the Reflex backend
The deployment also runs a local Redis server to store state for each user.
## Build
```console
docker build -t reflex-simple-two-port .
```
## Run
```console
docker run -p 3000:3000 -p 8000:8000 reflex-simple-two-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
route traffic to the appropriate port inside the container.
For example, the following Caddyfile can be used to terminate TLS and forward
traffic to the frontend and backend from outside the container.
```
my-domain.com
encode gzip
@backend_routes path /_event/* /ping /_upload /_upload/*
handle @backend_routes {
reverse_proxy localhost:8000
}
reverse_proxy localhost:3000
```

View File

@ -1,261 +0,0 @@
```diff
+ Suchst du nach Pynecone? Dann bist du hier in der richtigen Repository. Pynecone wurde in Reflex umbenannt. +
```
<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>
### **✨ 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)
![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)
---
# Reflex
Reflex ist eine Bibliothek, mit der man Full-Stack-Web-Applikationen in purem Python erstellen kann.
Wesentliche Merkmale:
* **Pures Python** - Schreibe dein Front- und Backend in Python, es gibt also keinen Grund, JavaScript zu lernen.
* **Volle Flexibilität** - Reflex ist einfach zu handhaben, kann aber auch für komplexe Anwendungen skaliert werden.
* **Sofortige Bereitstellung** - Nach dem Erstellen kannst du deine App mit einem [einzigen Befehl](https://reflex.dev/docs/hosting/deploy-quick-start/) bereitstellen oder auf deinem eigenen Server hosten.
Auf unserer [Architektur-Seite](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture) erfahren Sie, wie Reflex unter der Haube funktioniert.
## ⚙️ Installation
Öffne ein Terminal und führe den folgenden Befehl aus (benötigt Python 3.10+):
```bash
pip install reflex
```
## 🥳 Erstelle deine erste App
Die Installation von `reflex` installiert auch das `reflex`-Kommandozeilen-Tool.
Teste, ob die Installation erfolgreich war, indem du ein neues Projekt erstellst. (Ersetze `my_app_name` durch deinen Projektnamen):
```bash
mkdir my_app_name
cd my_app_name
reflex init
```
Dieser Befehl initialisiert eine Vorlage in deinem neuen Verzeichnis.
Du kannst diese App im Entwicklungsmodus ausführen:
```bash
reflex run
```
Du solltest deine App unter http://localhost:3000 laufen sehen.
Nun kannst du den Quellcode in `my_app_name/my_app_name.py` ändern. Reflex hat schnelle Aktualisierungen, sodass du deine Änderungen sofort siehst, wenn du deinen Code speicherst.
## 🫧 Beispiel-App
Lass uns ein Beispiel durchgehen: die Erstellung einer Benutzeroberfläche für die Bildgenerierung mit [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node). Zur Vereinfachung rufen wir einfach die [OpenAI-API](https://platform.openai.com/docs/api-reference/authentication) auf, aber du könntest dies auch durch ein lokal ausgeführtes ML-Modell ersetzen.
&nbsp;
<div align="center">
<img src="https://raw.githubusercontent.com/reflex-dev/reflex/main/docs/images/dalle.gif" alt="Eine Benutzeroberfläche für DALL·E, die im Prozess der Bildgenerierung gezeigt wird." width="550" />
</div>
&nbsp;
Hier ist der komplette Code, um dies zu erstellen. Das alles wird in einer Python-Datei gemacht!
```python
import reflex as rx
import openai
openai_client = openai.OpenAI()
class State(rx.State):
"""Der Zustand der App."""
prompt = ""
image_url = ""
processing = False
complete = False
def get_image(self):
"""Hole das Bild aus dem 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",
)
# Füge Zustand und Seite zur App hinzu.
app = rx.App()
app.add_page(index, title="Reflex:DALL-E")
```
## Schauen wir uns das mal genauer an.
<div align="center">
<img src="docs/images/dalle_colored_code_example.png" alt="Erläuterung der Unterschiede zwischen Backend- und Frontend-Teilen der DALL-E-App." width="900" />
</div>
### **Reflex-UI**
Fangen wir mit der Benutzeroberfläche an.
```python
def index():
return rx.center(
...
)
```
Diese `index`-Funktion definiert das Frontend der App.
Wir verwenden verschiedene Komponenten wie `center`, `vstack`, `input` und `button`, um das Frontend zu erstellen. Komponenten können ineinander verschachtelt werden, um komplexe Layouts zu erstellen. Und du kannst Schlüsselwortargumente verwenden, um sie mit der vollen Kraft von CSS zu stylen.
Reflex wird mit [über 60 eingebauten Komponenten](https://reflex.dev/docs/library) geliefert, die dir den Einstieg erleichtern. Wir fügen aktiv weitere Komponenten hinzu, und es ist einfach, [eigene Komponenten zu erstellen](https://reflex.dev/docs/wrapping-react/overview/).
### **State**
Reflex stellt deine Benutzeroberfläche als Funktion deines Zustands dar.
```python
class State(rx.State):
"""Der Zustand der App."""
prompt = ""
image_url = ""
processing = False
complete = False
```
Der Zustand definiert alle Variablen (genannt Vars) in einer App, die sich ändern können, und die Funktionen, die sie ändern.
Hier besteht der Zustand aus einem `prompt` und einer `image_url`. Es gibt auch die Booleans `processing` und `complete`, um anzuzeigen, wann der Button deaktiviert werden soll (während der Bildgenerierung) und wann das resultierende Bild angezeigt werden soll.
### **Event-Handler**
```python
def get_image(self):
"""Hole das Bild aus dem 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
```
Innerhalb des Zustands definieren wir Funktionen, die als Event-Handler bezeichnet werden und die Zustand-Variablen ändern. Event-Handler sind die Art und Weise, wie wir den Zustand in Reflex ändern können. Sie können als Reaktion auf Benutzeraktionen aufgerufen werden, z.B. beim Klicken auf eine Schaltfläche oder bei der Eingabe in ein Textfeld. Diese Aktionen werden als Ereignisse bezeichnet.
Unsere DALL-E.-App hat einen Event-Handler, `get_image`, der dieses Bild von der OpenAI-API abruft. Die Verwendung von `yield` in der Mitte eines Event-Handlers führt zu einer Aktualisierung der Benutzeroberfläche. Andernfalls wird die Benutzeroberfläche am Ende des Ereignishandlers aktualisiert.
### **Routing**
Schließlich definieren wir unsere App.
```python
app = rx.App()
```
Wir fügen der Indexkomponente eine Seite aus dem Stammverzeichnis der Anwendung hinzu. Wir fügen auch einen Titel hinzu, der in der Seitenvorschau/Browser-Registerkarte angezeigt wird.
```python
app.add_page(index, title="DALL-E")
```
Du kannst eine mehrseitige App erstellen, indem du weitere Seiten hinzufügst.
## 📑 Ressourcen
<div align="center">
📑 [Docs](https://reflex.dev/docs/getting-started/introduction) &nbsp; | &nbsp; 🗞️ [Blog](https://reflex.dev/blog) &nbsp; | &nbsp; 📱 [Komponentenbibliothek](https://reflex.dev/docs/library) &nbsp; | &nbsp; 🖼️ [Galerie](https://reflex.dev/docs/gallery) &nbsp; | &nbsp; 🛸 [Bereitstellung](https://reflex.dev/docs/hosting/deploy-quick-start) &nbsp;
</div>
## ✅ Status
Reflex wurde im Dezember 2022 unter dem Namen Pynecone gestartet.
Ab Februar 2024 befindet sich unser Hosting-Service in der Alpha-Phase! In dieser Zeit kann jeder seine Apps kostenlos bereitstellen. Siehe unsere [Roadmap](https://github.com/reflex-dev/reflex/issues/2727), um zu sehen, was geplant ist.
Reflex hat wöchentliche Veröffentlichungen und neue Features! Stelle sicher, dass du dieses Repository mit einem :star: Stern markierst und :eyes: beobachtest, um auf dem Laufenden zu bleiben.
## Beitragende
Wir begrüßen Beiträge jeder Größe! Hier sind einige gute Möglichkeiten, um in der Reflex-Community zu starten.
- **Tritt unserem Discord bei**: Unser [Discord](https://discord.gg/T5WSbC2YtQ) ist der beste Ort, um Hilfe für dein Reflex-Projekt zu bekommen und zu besprechen, wie du beitragen kannst.
- **GitHub-Diskussionen**: Eine großartige Möglichkeit, über Funktionen zu sprechen, die du hinzugefügt haben möchtest oder Dinge, die verwirrend sind/geklärt werden müssen.
- **GitHub-Issues**: [Issues](https://github.com/reflex-dev/reflex/issues) sind eine ausgezeichnete Möglichkeit, Bugs zu melden. Außerdem kannst du versuchen, ein bestehendes Problem zu lösen und eine PR einzureichen.
Wir suchen aktiv nach Mitwirkenden, unabhängig von deinem Erfahrungslevel oder deiner Erfahrung. Um beizutragen, sieh dir [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) an.
## Vielen Dank an unsere Mitwirkenden:
<a href="https://github.com/reflex-dev/reflex/graphs/contributors">
<img src="https://contrib.rocks/image?repo=reflex-dev/reflex" />
</a>
## Lizenz
Reflex ist Open-Source und lizenziert unter der [Apache License 2.0](LICENSE).

View File

@ -18,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)
[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)
---
@ -35,7 +35,7 @@ Consulta nuestra [página de arquitectura](https://reflex.dev/blog/2024-03-21-re
## ⚙️ Instalación
Abra un terminal y ejecute (Requiere Python 3.10+):
Abra un terminal y ejecute (Requiere Python 3.8+):
```bash
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.
- **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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 KiB

After

Width:  |  Height:  |  Size: 455 KiB

View File

@ -11,6 +11,7 @@ Pynecone की तलाश हैं? आप सही रेपो में
### **✨ प्रदर्शनकारी, अनुकूलित वेब ऐप्स, शुद्ध Python में। सेकंडों में तैनात करें। ✨**
[![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)
[![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)
@ -19,7 +20,7 @@ Pynecone की तलाश हैं? आप सही रेपो में
---
## [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) | [한국어](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)
## [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) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md)
# Reflex
@ -35,7 +36,7 @@ Reflex के अंदर के कामकाज को जानने क
## ⚙️ इंस्टॉलेशन (Installation)
एक टर्मिनल खोलें और चलाएं (Python 3.10+ की आवश्यकता है):
एक टर्मिनल खोलें और चलाएं (Python 3.8+ की आवश्यकता है):
```bash
pip install reflex
@ -239,7 +240,7 @@ Reflex में हर सप्ताह नए रिलीज़ और फ
- **GitHub Discussions** (गिटहब चर्चाएँ): उन सुविधाओं के बारे में बात करने का एक शानदार तरीका जिन्हें आप जोड़ना चाहते हैं या ऐसी चीज़ें जो भ्रमित करने वाली हैं/स्पष्टीकरण की आवश्यकता है।
- **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. ✨**
[![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)
[![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)
@ -17,12 +18,12 @@
---
[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) | [한국어](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)
[Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/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)
---
## ⚙️ Installazione
Apri un terminale ed esegui (Richiede Python 3.10+):
Apri un terminale ed esegui (Richiede Python 3.8+):
```bash
pip install reflex

View File

@ -8,36 +8,34 @@
<hr>
### **✨ 即時デプロイが可能な、Pure Python で作ったパフォーマンスと汎用性が高い Web アプリケーション ✨**
### **✨ 即時デプロイが可能な、Pure Pythonで作ったパフォーマンスと汎用性が高いWebアプリケーション✨**
[![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)
[![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)
[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)
---
# Reflex
Reflex Python のみでフルスタック Web アプリケーションを作成できるライブラリです。
ReflexはPythonのみでフルスタックWebアプリケーションを作成できるライブラリです。
主な特徴:
* **Pure Python** - WebアプリケーションのフロントエンドとバックエンドをPythonのみで実装できるため、Javascriptを学ぶ必要がありません。
* **高い柔軟性** - Reflexは簡単に始められて、複雑なアプリケーションまで作成できます。
* **即時デプロイ** - ビルド後、すぐにデプロイが可能です。[単純なCLIコマンド](https://reflex.dev/docs/hosting/deploy-quick-start/)を使ったアプリケーションのデプロイや、自身のサーバーへのホストができます。
- **Pure Python** - Web アプリケーションのフロントエンドとバックエンドを Python のみで実装できるため、Javascript を学ぶ必要がありません。
- **高い柔軟性** - Reflex は簡単に始められて、複雑なアプリケーションまで作成できます。
- **即時デプロイ** - ビルド後、すぐにデプロイが可能です。[単純な CLI コマンド](https://reflex.dev/docs/hosting/deploy-quick-start/)を使ったアプリケーションのデプロイや、自身のサーバーへのホストができます。
Reflex がどのように動作しているかを知るには、[アーキテクチャページ](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture)をご覧ください。
Reflexがどのように動作しているかを知るには、[アーキテクチャページ](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture)をご覧ください。
## ⚙️ インストール
ターミナルを開いて以下のコマンドを実行してください。Python 3.10 以上が必要です。):
ターミナルを開いて以下のコマンドを実行してください。Python 3.8以上が必要です。):
```bash
pip install reflex
@ -45,7 +43,7 @@ pip install reflex
## 🥳 最初のアプリケーションを作ろう
`reflex`をインストールすると、`reflex`の CLI ツールが自動でインストールされます。
`reflex`をインストールすると、`reflex`のCLIツールが自動でインストールされます。
新しいプロジェクトを作成して、インストールが成功しているかを確認しましょう。(`my_app_name`を自身のプロジェクト名に書き換えて実行ください。):
@ -65,11 +63,11 @@ reflex run
http://localhost:3000 にアクセスしてアプリの動作を見ることができます。
`my_app_name/my_app_name.py`のソースコードを編集してみましょうReflex fast refresh なので、ソースを保存した直後に変更が Web ページに反映されます。
`my_app_name/my_app_name.py`のソースコードを編集してみましょうReflexはfast refreshなので、ソースを保存した直後に変更がWebページに反映されます。
## 🫧 実装例
実装例を見てみましょう: [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node)を中心とした画像生成 UI を作成しました。説明を簡単にするためにここでは[OpenAI API](https://platform.openai.com/docs/api-reference/authentication)を呼んでいますが、ローカルで動作している機械学習モデルに置き換えることも可能です。
実装例を見てみましょう: [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node)を中心とした画像生成UIを作成しました。説明を簡単にするためにここでは[OpenAI API](https://platform.openai.com/docs/api-reference/authentication)を呼んでいますが、ローカルで動作している機械学習モデルに置き換えることも可能です。
&nbsp;
@ -79,7 +77,7 @@ http://localhost:3000 にアクセスしてアプリの動作を見ることが
&nbsp;
画像生成 UI のソースコードの全貌を見てみましょう。下記のように、単一の Python ファイルで作れます!
画像生成UIのソースコードの全貌を見てみましょう。下記のように、単一のPythonファイルで作れます
```python
import reflex as rx
@ -119,15 +117,14 @@ def index():
on_blur=State.set_prompt,
width="25em",
),
rx.button(
"Generate Image",
on_click=State.get_image,
width="25em",
loading=State.processing
),
rx.button("Generate Image", on_click=State.get_image, width="25em"),
rx.cond(
State.complete,
rx.image(src=State.image_url, width="20em"),
State.processing,
rx.chakra.circular_progress(is_indeterminate=True),
rx.cond(
State.complete,
rx.image(src=State.image_url, width="20em"),
),
),
align="center",
),
@ -140,15 +137,17 @@ app = rx.App()
app.add_page(index, title="Reflex:DALL-E")
```
## それぞれの実装を見てみましょう
<div align="center">
<img src="../../docs/images/dalle_colored_code_example.png" alt="DALL-E appのフロントエンドとバックエンドのパーツの違いを説明しています。" width="900" />
</div>
### **Reflex UI**
UI から見てみましょう。
UIから見てみましょう。
```python
def index():
@ -159,13 +158,13 @@ def index():
`index`関数において、アプリのフロントエンドを定義しています。
フロントエンドを実装するにあたり、`center`、`vstack`、`input`、`button`など異なるコンポーネントを使用しています。コンポーネントはお互いにネストが可能であり、複雑なレイアウトを作成できます。また、keyword args を使うことで、CSS の機能をすべて使ったスタイルが可能です。
フロントエンドを実装するにあたり、`center`、`vstack`、`input`、`button`など異なるコンポーネントを使用しています。コンポーネントはお互いにネストが可能であり、複雑なレイアウトを作成できます。また、keyword argsを使うことで、CSSの機能をすべて使ったスタイルが可能です。
Reflex は[60 を超える内臓コンポーネント](https://reflex.dev/docs/library)があるため、すぐに始められます。私たちは、積極的にコンポーネントを追加していますが、簡単に[自身のコンポーネントを追加](https://reflex.dev/docs/wrapping-react/overview/)することも可能です。
Reflexは[60を超える内臓コンポーネント](https://reflex.dev/docs/library)があるため、すぐに始められます。私たちは、積極的にコンポーネントを追加していますが、簡単に[自身のコンポーネントを追加](https://reflex.dev/docs/wrapping-react/overview/)することも可能です。
### **ステート**
Reflex はステートの関数を用いて UI を表示します。
Reflexはステートの関数を用いてUIを表示します。
```python
class State(rx.State):
@ -177,9 +176,9 @@ class State(rx.State):
```
ステートでは、アプリで変更が可能な全ての変数vars と呼びますと、vars の変更が可能な関数を定義します。
ステートでは、アプリで変更が可能な全ての変数varsと呼びますと、varsの変更が可能な関数を定義します。
この例では、ステートを`prompt`と`image_url`で構成しています。そして、ブール型の`processing`と`complete`を用いて、ボタンを無効にするタイミング(画像生成中)や生成された画像を表示するタイミングを示しています。
この例では、ステートを`prompt`と`image_url`で構成しています。そして、ブール型の`processing`と`complete`を用いて、プログレスサークルと画像の表示を切り替えています。
### **イベントハンドラ**
@ -198,9 +197,9 @@ def get_image(self):
self.processing, self.complete = False, True
```
ステートにおいて、ステートの vars を変更できるイベントハンドラ関数を定義しています。イベントハンドラは Reflex において、ステートの vars を変更する方法です。ボタンクリックやテキストボックスの入力など、ユーザのアクションに応じてイベントハンドラが呼ばれます。
ステートにおいて、ステートのvarsを変更できるイベントハンドラ関数を定義しています。イベントハンドラはReflexにおいて、ステートのvarsを変更する方法です。ボタンクリックやテキストボックスの入力など、ユーザのアクションに応じてイベントハンドラが呼ばれます。
DALL·E.アプリには、OpenAI API からイメージを取得する`get_image`関数があります。イベントハンドラの最後で UI の更新がかかるため、関数の途中に`yield`を入れることで先に UI を更新しています。
DALL·E.アプリには、OpenAI APIからイメージを取得する`get_image`関数があります。イベントハンドラの最後でUIの更新がかかるため、関数の途中に`yield`を入れることで先にUIを更新しています。
### **ルーティング**
@ -210,7 +209,7 @@ DALL·E.アプリには、OpenAI API からイメージを取得する`get_image
app = rx.App()
```
アプリにページを追加し、ドキュメントルートを index コンポーネントにルーティングしています。更に、ページのプレビューやブラウザタブに表示されるタイトルを記載しています。
アプリにページを追加し、ドキュメントルートをindexコンポーネントにルーティングしています。更に、ページのプレビューやブラウザタブに表示されるタイトルを記載しています。
```python
app.add_page(index, title="DALL-E")
@ -222,34 +221,35 @@ app.add_page(index, title="DALL-E")
<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>
## ✅ ステータス
2022 12 月に、Reflex Pynecone という名前でローンチしました。
2022年12月に、ReflexはPyneconeという名前でローンチしました。
2024 2 月に、ホスティングサービスをアルファ版でリリースしました!アルファ版では、だれでも Reflex アプリケーションを無料でデプロイできます。今後の予定は[ロードマップ](https://github.com/reflex-dev/reflex/issues/2727)において見れます。
2024年2月に、ホスティングサービスをアルファ版でリリースしましたアルファ版では、だれでもReflexアプリケーションを無料でデプロイできます。今後の予定は[ロードマップ](https://github.com/reflex-dev/reflex/issues/2727)において見れます。
Reflex は毎週、新しいリリースや機能追加を行っています!最新情報を逃さないために、 :star: Star や :eyes: Watch をお願いします。
Reflexは毎週、新しいリリースや機能追加を行っています最新情報を逃さないために、 :star: Starや :eyes: Watchをお願いします。
## コントリビュート
様々なサイズのコントリビュートを歓迎していますReflex コミュニティに入るための方法を、いくつかリストアップします。
様々なサイズのコントリビュートを歓迎していますReflexコミュニティに入るための方法を、いくつかリストアップします。
- **Discord に参加**: [Discord](https://discord.gg/T5WSbC2YtQ)は、Reflex プロジェクトの相談や、コントリビュートについての話し合いをするための、最適な場所です。
- **GitHub Discussions**: GitHub Discussions では、追加したい機能や、複雑で解明が必要な事柄についての議論に適している場所です。
- **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues)はバグの報告に適している場所です。また、課題を解決した PR のサブミットにチャレンジしていただくことも、可能です。
- **Discordに参加**: [Discord](https://discord.gg/T5WSbC2YtQ)は、Reflexプロジェクトの相談や、コントリビュートについての話し合いをするための、最適な場所です。
- **GitHub Discussions**: GitHub Discussionsでは、追加したい機能や、複雑で解明が必要な事柄についての議論に適している場所です。
- **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues)はバグの報告に適している場所です。また、課題を解決したPRのサブミットにチャレンジしていただくことも、可能です。
スキルや経験に関わらず、私たちはコントリビュータを積極的に探しています。コントリビュートするために、[CONTIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)をご覧ください。
CONTスキルや経験に関わらず、私たちはコントリビュータを積極的に探しています。コントリビュートするために、[CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)をご覧ください。
## 私たちのコントリビュータに感謝!:
<a href="https://github.com/reflex-dev/reflex/graphs/contributors">
<img src="https://contrib.rocks/image?repo=reflex-dev/reflex" />
</a>
## ライセンス
Reflex はオープンソースであり、[Apache License 2.0](LICENSE)に基づいてライセンス供与されます。
Reflexはオープンソースであり、[Apache License 2.0](LICENSE)に基づいてライセンス供与されます。

View File

@ -10,17 +10,18 @@
### **✨ 순수 Python으로 고성능 사용자 정의 웹앱을 만들어 보세요. 몇 초만에 배포 가능합니다. ✨**
[![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)
[![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)
</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) | [한국어](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)
[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) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md)
---
## ⚙️ 설치
터미널을 열고 실행하세요. (Python 3.10+ 필요):
터미널을 열고 실행하세요. (Python 3.8+ 필요):
```bash
pip install reflex

View File

@ -1,262 +0,0 @@
```diff
+ دنبال Pynecone میگردی؟ شما در مخزن درستی قرار داری. Pynecone به 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>
### **✨ برنامه های تحت وب قابل تنظیم، کارآمد تماما پایتونی که در چند ثانیه مستقر(دپلوی) می‎شود. ✨**
[![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)
---
# Reflex - رفلکس
رفلکس(Reflex) یک کتابخانه برای ساخت برنامه های وب فول استک تماما پایتونی است.
ویژگی های کلیدی:
* **تماما پایتونی** - فرانت اند و بک اند برنامه خود را همه در پایتون بنویسید، بدون نیاز به یادگیری جاوا اسکریپت.
* **انعطاف پذیری کامل** - شروع به کار با Reflex آسان است، اما می تواند به برنامه های پیچیده نیز تبدیل شود.
* **دپلوی فوری** - پس از ساخت، برنامه خود را با [یک دستور](https://reflex.dev/docs/hosting/deploy-quick-start/) دپلوی کنید یا آن را روی سرور خود میزبانی کنید.
برای آشنایی با نحوه عملکرد Reflex [صفحه معماری](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture) را ببینید.
## ⚙️ Installation - نصب و راه اندازی
یک ترمینال را باز کنید و اجرا کنید (نیازمند Python 3.10+):
```bash
pip install reflex
```
## 🥳 اولین برنامه خود را ایجاد کنید
نصب `reflex` همچنین `reflex` در خط فرمان را نصب میکند.
با ایجاد یک پروژه جدید موفقیت آمیز بودن نصب را تست کنید. (`my_app_name` را با اسم پروژه خودتان جایگزین کنید):
```bash
mkdir my_app_name
cd my_app_name
reflex init
```
این دستور یک برنامه الگو(تمپلیت) را در فهرست(دایرکتوری) جدید شما مقداردهی اولیه می کند
می توانید این برنامه را در حالت توسعه(development) اجرا کنید:
```bash
reflex run
```
باید برنامه خود را در حال اجرا ببینید در http://localhost:3000.
اکنون می‌توانید کد منبع را در «my_app_name/my_app_name.py» تغییر دهید. Reflex دارای تازه‌سازی‌های سریعی است، بنابراین می‌توانید تغییرات خود را بلافاصله پس از ذخیره کد خود مشاهده کنید.
## 🫧 Example App - برنامه نمونه
بیایید یک مثال بزنیم: ایجاد یک رابط کاربری برای تولید تصویر [DALL·E](https://platform.openai.com/docs/guides/images/image-generation?context=node). برای سادگی، ما فراخوانی میکنیم [OpenAI API](https://platform.openai.com/docs/api-reference/authentication), اما می توانید آن را با یک مدل ML که به صورت محلی اجرا می شود جایگزین کنید.
&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;
در اینجا کد کامل برای ایجاد این پروژه آمده است. همه اینها در یک فایل پایتون انجام می شود!
```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")
```
## بیاید سادش کنیم
<div align="center">
<img src="docs/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 - رابط کاربری رفلکس**
بیایید با رابط کاربری شروع کنیم.
```python
def index():
return rx.center(
...
)
```
تابع `index` قسمت فرانت اند برنامه را تعریف می کند.
ما از اجزای مختلفی مثل `center`, `vstack`, `input` و `button` استفاده میکنیم تا فرانت اند را بسازیم. اجزاء را می توان درون یکدیگر قرار داد
برای ایجاد طرح بندی های پیچیده می توانید از args کلمات کلیدی برای استایل دادن به آنها از CSS استفاده کنید.
رفلکس دارای [بیش از 60 جزء](https://reflex.dev/docs/library) برای کمک به شما برای شروع. ما به طور فعال اجزای بیشتری را اضافه می کنیم, و این خیلی آسان است که [اجزا خود را بسازید](https://reflex.dev/docs/wrapping-react/overview/).
### **State - حالت**
رفلکس رابط کاربری شما را به عنوان تابعی از وضعیت شما نشان می دهد.
```python
class State(rx.State):
"""The app state."""
prompt = ""
image_url = ""
processing = False
complete = False
```
حالت تمام متغیرها(variables) (به نام vars) را در یک برنامه که می توانند تغییر دهند و توابعی که آنها را تغییر می دهند تعریف می کند..
در اینجا حالت از یک `prompt` و `image_url` تشکیل شده است. همچنین دو بولی `processing` و `complete` برای نشان دادن زمان غیرفعال کردن دکمه (در طول تولید تصویر) و زمان نمایش تصویر نتیجه وجود دارد.
### **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
```
در داخل حالت، توابعی به نام کنترل کننده رویداد تعریف می کنیم که متغیرهای حالت را تغییر می دهند. کنترل کننده های رویداد راهی هستند که می توانیم وضعیت را در Reflex تغییر دهیم. آنها را می توان در پاسخ به اقدامات کاربر، مانند کلیک کردن روی یک دکمه یا تایپ کردن در یک متن، فراخوانی کرد. به این اعمال وقایع می گویند.
برنامه DALL·E ما دارای یک کنترل کننده رویداد، `get_image` است که این تصویر را از OpenAI API دریافت می کند. استفاده از `yield` در وسط کنترل‌کننده رویداد باعث به‌روزرسانی رابط کاربری می‌شود. در غیر این صورت رابط کاربری در پایان کنترل کننده رویداد به روز می شود.
### **Routing - مسیریابی**
بالاخره اپلیکیشن خود را تعریف می کنیم.
```python
app = rx.App()
```
ما یک صفحه از root برنامه را به جزء index اضافه می کنیم. ما همچنین عنوانی را اضافه می کنیم که در برگه پیش نمایش/مرورگر صفحه نمایش داده می شود.
```python
app.add_page(index, title="DALL-E")
```
با افزودن صفحات بیشتر می توانید یک برنامه چند صفحه ای ایجاد کنید.
## 📑 Resources - منابع
<div align="center">
📑 [اسناد](https://reflex.dev/docs/getting-started/introduction) &nbsp; | &nbsp; 🗞️ [وبلاگ](https://reflex.dev/blog) &nbsp; | &nbsp; 📱 [کتابخانه جزء](https://reflex.dev/docs/library) &nbsp; | &nbsp; 🖼️ [گالری](https://reflex.dev/docs/gallery) &nbsp; | &nbsp; 🛸 [استقرار](https://reflex.dev/docs/hosting/deploy-quick-start) &nbsp;
</div>
## ✅ Status - وضعیت
رفلکس(reflex) در دسامبر 2022 با نام Pynecone راه اندازی شد.
از فوریه 2024، سرویس میزبانی ما در آلفا است! در این مدت هر کسی می‌تواند برنامه‌های خود را به صورت رایگان اجرا کند. [نقشه راه](https://github.com/reflex-dev/reflex/issues/2727) را ببینید تا متوجه شوید چه برنامه‌ریزی شده است.
رفلکس(reflex) هر هفته نسخه ها و ویژگی های جدیدی دارد! مطمئن شوید که :star: ستاره و :eyes: این مخزن را تماشا کنید تا به روز بمانید.
## Contributing - مشارکت کردن
ما از مشارکت در هر اندازه استقبال می کنیم! در زیر چند راه خوب برای شروع در انجمن رفلکس آورده شده است.
- **به Discord ما بپیوندید**: [Discord](https://discord.gg/T5WSbC2YtQ) ما بهترین مکان برای دریافت کمک در مورد پروژه Reflex و بحث در مورد اینکه چگونه می توانید کمک کنید است.
- **بحث های GitHub**: راهی عالی برای صحبت در مورد ویژگی هایی که می خواهید اضافه کنید یا چیزهایی که گیج کننده هستند/نیاز به توضیح دارند.
- **قسمت مشکلات GitHub**: [قسمت مشکلات](https://github.com/reflex-dev/reflex/issues) یک راه عالی برای گزارش اشکال هستند. علاوه بر این، می توانید یک مشکل موجود را حل کنید و یک PR(pull request) ارسال کنید.
ما فعالانه به دنبال مشارکت کنندگان هستیم، فارغ از سطح مهارت یا تجربه شما. برای مشارکت [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) را بررسی کنید.
## All Thanks To Our Contributors - با تشکر از همکاران ما:
<a href="https://github.com/reflex-dev/reflex/graphs/contributors">
<img src="https://contrib.rocks/image?repo=reflex-dev/reflex" />
</a>
## License - مجوز
رفلکس متن باز و تحت مجوز [Apache License 2.0](LICENSE) است.

View File

@ -17,11 +17,11 @@
</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) | [한국어](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)
[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) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md)
---
## ⚙️ Instalação
Abra um terminal e execute (Requer Python 3.10+):
Abra um terminal e execute (Requer Python 3.8+):
```bash
pip install reflex

View File

@ -11,6 +11,7 @@
### **✨ 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)
![tests](https://github.com/pynecone-io/pynecone/actions/workflows/integration.yml/badge.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)
[![Discord](https://img.shields.io/discord/1029853095527727165?color=%237289da&label=Discord)](https://discord.gg/T5WSbC2YtQ)
@ -18,13 +19,13 @@
---
[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)
[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)
---
## ⚙️ 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
pip install reflex
@ -200,7 +201,7 @@ Daha fazla sayfa ekleyerek çok sayfalı bir uygulama oluşturabilirsiniz.
<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>
@ -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 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:

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 创建高效且可自定义的网页应用程序,几秒钟内即可部署.✨**
[![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)
[![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)
@ -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)
[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/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md)
---
@ -34,7 +35,7 @@ Reflex 是一个使用纯Python构建全栈web应用的库。
## ⚙️ 安装
打开一个终端并且运行(要求Python3.10+):
打开一个终端并且运行(要求Python3.8+):
```bash
pip install reflex
@ -147,7 +148,7 @@ app.add_page(index, title="Reflex:DALL-E")
## 让我们分解以上步骤.
<div align="center">
<img src="../../images/dalle_colored_code_example.png" alt="解释 DALL-E app 的前端和后端部分的区别。" width="900" />
<img src="docs/images/dalle_colored_code_example.png" alt="Explaining the differences between backend and frontend parts of the DALL-E app." width="900" />
</div>

View File

@ -11,32 +11,18 @@
**✨ 使用 Python 建立高效且可自訂的網頁應用程式,幾秒鐘內即可部署。✨**
[![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)
[![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)
</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)
[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/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md)
---
# Reflex
Reflex 是一個可以用純 Python 構建全端網頁應用程式的函式庫。
主要特色:
* **純 Python** - 您可以用 Python 撰寫應用程式的前端和後端,無需學習 Javascript。
* **完全靈活性** - Reflex 易於上手,但也可以擴展到複雜的應用程式。
* **立即部署** - 構建後,只需使用[單一指令](https://reflex.dev/docs/hosting/deploy-quick-start/)即可部署您的應用程式,或在您自己的伺服器上託管。
請參閱我們的[架構頁面](https://reflex.dev/blog/2024-03-21-reflex-architecture/#the-reflex-architecture)了解 Reflex 如何在底層運作。
## ⚙️ 安裝
開啟一個終端機並且執行 (需要 Python 3.10+):
開啟一個終端機並且執行 (需要 Python 3.8+):
```bash
pip install reflex
@ -84,8 +70,7 @@ reflex run
import reflex as rx
import openai
openai_client = openai.OpenAI()
openai.api_key = "YOUR_API_KEY"
class State(rx.State):
"""應用程式狀態"""
@ -101,33 +86,33 @@ class State(rx.State):
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
response = openai.Image.create(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.heading("DALL·E"),
rx.input(placeholder="Enter a prompt", on_blur=State.set_prompt),
rx.button(
"Generate Image",
"Generate Image",
on_click=State.get_image,
width="25em",
loading=State.processing
is_loading=State.processing,
width="100%",
),
rx.cond(
State.complete,
rx.image(src=State.image_url, width="20em"),
rx.image(
src=State.image_url,
height="25em",
width="25em",
)
),
align="center",
padding="2em",
shadow="lg",
border_radius="lg",
),
width="100%",
height="100vh",
@ -135,22 +120,10 @@ def index():
# 把狀態跟頁面添加到應用程式。
app = rx.App()
app.add_page(index, title="Reflex:DALL-E")
app.add_page(index, title="reflex:DALL·E")
```
## 讓我們來拆解一下。
<div align="center">
<img src="../../images/dalle_colored_code_example.png" alt="解釋 DALL-E app 的前端和後端部分的區別。" width="900" />
</div>
### **Reflex 使用者介面**
讓我們從使用介面開始。
@ -177,9 +150,8 @@ class State(rx.State):
"""應用程式狀態"""
prompt = ""
image_url = ""
processing = False
complete = False
image_processing = False
image_made = False
```
應用程式狀態定義了應用程式中所有可以更改的變數及變更他們的函式 (稱為 vars)。
@ -196,10 +168,8 @@ def get_image(self):
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
response = openai.Image.create(prompt=self.prompt, n=1, size="1024x1024")
self.image_url = response["data"][0]["url"]
self.processing, self.complete = False, True
```
@ -229,35 +199,34 @@ app.add_page(index, title="DALL-E")
<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) &nbsp;
</div>
## ✅ 產品狀態
## ✅ Reflex 狀態
Reflex 在 2022 年 12 月以 Pynecone 的名字推出
Reflex 於 2022 年 12 月推出,當時名為 Pynecone
截至 2024 年 2 月,我們的託管服務已進入 alpha 階段!在此期間,任何人都可以免費部署他們的應用程式。請參閱我們的[產品地圖](https://github.com/reflex-dev/reflex/issues/2727)了解未來的計劃。
截至 2023 年 7 月,我們處於 **Public Beta** 階段。
- :white_check_mark: **Public Alpha**: 任何人都可以安裝與使用 Reflex或許包含問題 但我們正在積極的解決他們。
- :large_orange_diamond: **Public Beta**: 對於不涉及商業目的使用情境來說足夠穩定。
- **Public Hosting Beta**: _Optionally_, 部屬跟託管你的 Reflex!
- **Public**: 這版本的 Reflex 是可用於軟體產品的。
Reflex 每周都有新功能和釋出新版本! 確保你按下 :star: 和 :eyes: watch 這個 repository 來確保知道最新資訊。
## 貢獻
我們歡迎任何大小的貢獻,以下是一些加入 Reflex 社群的好方法
我們歡迎任何大小的貢獻,以下是幾個好的方法來加入 Reflex 社群
- **加入我們的 Discord**: 我們的 [Discord](https://discord.gg/T5WSbC2YtQ) 是獲取 Reflex 專案幫助和討論如何貢獻的最佳地方。
- **GitHub Discussions**: 這是一個討論您想新增的功能或對於一些困惑/需要澄清事項的好方法
- **GitHub Issues**: 在 [Issues](https://github.com/reflex-dev/reflex/issues) 頁面報告錯誤是一個絕佳的方式。此外,您也可以嘗試解決現有 Issue 並提交 PR。
- **加入我們的 Discord**: 我們的 [Discord](https://discord.gg/T5WSbC2YtQ) 是幫助你加入 Reflex 專案和討論或貢獻最棒的地方。
- **GitHub Discussions**: 一個來討論你想要添加的功能或是需要澄清的事情的好地方
- **GitHub Issues**: 報告錯誤的絕佳地方,另外你可以試著解決一些 issue 和送出 PR。
我們積極尋找貢獻者,不論您的技能水平或經驗如何。要貢獻,請查看 [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)
## 感謝所有貢獻者:
<a href="https://github.com/reflex-dev/reflex/graphs/contributors">
<img src="https://contrib.rocks/image?repo=reflex-dev/reflex" />
</a>
我們正在積極尋找貢獻者,無關你的技能水平或經驗。
## 授權

View File

@ -1,11 +1,11 @@
"""Shared conftest for all integration tests."""
import os
import re
from pathlib import Path
import pytest
import reflex.app
from reflex.config import environment
from reflex.testing import AppHarness, AppHarnessProd
DISPLAY = None
@ -21,7 +21,7 @@ def xvfb():
Yields:
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]
SmartDisplay,
)
@ -34,6 +34,34 @@ def xvfb():
yield None
def pytest_exception_interact(node, call, report):
"""Take and upload screenshot when tests fail.
Args:
node: The pytest item that failed.
call: The pytest call describing when/where the test was invoked.
report: The pytest log report object.
"""
screenshot_dir = 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(
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.
"""
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
RUN useradd -m $USERNAME

View File

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

View File

@ -1,5 +1,4 @@
"""Simple module which contains one reusable reflex state class."""
import reflex as rx

View File

@ -0,0 +1,211 @@
"""Test @rx.background task functionality."""
from typing import Generator
import pytest
from selenium.webdriver.common.by import By
from reflex.testing import DEFAULT_TIMEOUT, AppHarness, WebDriver
def BackgroundTask():
"""Test that background tasks work as expected."""
import asyncio
import reflex as rx
class State(rx.State):
counter: int = 0
_task_id: int = 0
iterations: int = 10
@rx.background
async def handle_event(self):
async with self:
self._task_id += 1
for _ix in range(int(self.iterations)):
async with self:
self.counter += 1
await asyncio.sleep(0.005)
@rx.background
async def handle_event_yield_only(self):
async with self:
self._task_id += 1
for ix in range(int(self.iterations)):
if ix % 2 == 0:
yield State.increment_arbitrary(1) # type: ignore
else:
yield State.increment() # type: ignore
await asyncio.sleep(0.005)
def increment(self):
self.counter += 1
@rx.background
async def increment_arbitrary(self, amount: int):
async with self:
self.counter += int(amount)
def reset_counter(self):
self.counter = 0
async def blocking_pause(self):
await asyncio.sleep(0.02)
@rx.background
async def non_blocking_pause(self):
await asyncio.sleep(0.02)
def index() -> rx.Component:
return rx.vstack(
rx.chakra.input(
id="token", value=State.router.session.client_token, is_read_only=True
),
rx.heading(State.counter, id="counter"),
rx.chakra.input(
id="iterations",
placeholder="Iterations",
value=State.iterations.to_string(), # type: ignore
on_change=State.set_iterations, # type: ignore
),
rx.button(
"Delayed Increment",
on_click=State.handle_event,
id="delayed-increment",
),
rx.button(
"Yield Increment",
on_click=State.handle_event_yield_only,
id="yield-increment",
),
rx.button("Increment 1", on_click=State.increment, id="increment"),
rx.button(
"Blocking Pause",
on_click=State.blocking_pause,
id="blocking-pause",
),
rx.button(
"Non-Blocking Pause",
on_click=State.non_blocking_pause,
id="non-blocking-pause",
),
rx.button("Reset", on_click=State.reset_counter, id="reset"),
)
app = rx.App(state=rx.State)
app.add_page(index)
@pytest.fixture(scope="module")
def background_task(
tmp_path_factory,
) -> Generator[AppHarness, None, None]:
"""Start BackgroundTask app at tmp_path via AppHarness.
Args:
tmp_path_factory: pytest tmp_path_factory fixture
Yields:
running AppHarness instance
"""
with AppHarness.create(
root=tmp_path_factory.mktemp(f"background_task"),
app_source=BackgroundTask, # type: ignore
) as harness:
yield harness
@pytest.fixture
def driver(background_task: AppHarness) -> Generator[WebDriver, None, None]:
"""Get an instance of the browser open to the background_task app.
Args:
background_task: harness for BackgroundTask app
Yields:
WebDriver instance.
"""
assert background_task.app_instance is not None, "app is not running"
driver = background_task.frontend()
try:
yield driver
finally:
driver.quit()
@pytest.fixture()
def token(background_task: AppHarness, driver: WebDriver) -> str:
"""Get a function that returns the active token.
Args:
background_task: harness for BackgroundTask app.
driver: WebDriver instance.
Returns:
The token for the connected client
"""
assert background_task.app_instance is not None
token_input = driver.find_element(By.ID, "token")
assert token_input
# wait for the backend connection to send the token
token = background_task.poll_for_value(token_input, timeout=DEFAULT_TIMEOUT * 2)
assert token is not None
return token
def test_background_task(
background_task: AppHarness,
driver: WebDriver,
token: str,
):
"""Test that background tasks work as expected.
Args:
background_task: harness for BackgroundTask app.
driver: WebDriver instance.
token: The token for the connected client.
"""
assert background_task.app_instance is not None
# get a reference to all buttons
delayed_increment_button = driver.find_element(By.ID, "delayed-increment")
yield_increment_button = driver.find_element(By.ID, "yield-increment")
increment_button = driver.find_element(By.ID, "increment")
blocking_pause_button = driver.find_element(By.ID, "blocking-pause")
non_blocking_pause_button = driver.find_element(By.ID, "non-blocking-pause")
driver.find_element(By.ID, "reset")
# get a reference to the counter
counter = driver.find_element(By.ID, "counter")
# get a reference to the iterations input
iterations_input = driver.find_element(By.ID, "iterations")
# kick off background tasks
iterations_input.clear()
iterations_input.send_keys("50")
delayed_increment_button.click()
blocking_pause_button.click()
delayed_increment_button.click()
for _ in range(10):
increment_button.click()
blocking_pause_button.click()
delayed_increment_button.click()
delayed_increment_button.click()
yield_increment_button.click()
non_blocking_pause_button.click()
yield_increment_button.click()
blocking_pause_button.click()
yield_increment_button.click()
for _ in range(10):
increment_button.click()
yield_increment_button.click()
blocking_pause_button.click()
assert background_task._poll_for(lambda: counter.text == "420", timeout=40)
# all tasks should have exited and cleaned up
assert background_task._poll_for(
lambda: not background_task.app_instance.background_tasks # type: ignore
)

View File

@ -1,5 +1,4 @@
"""Integration tests for client side storage."""
from __future__ import annotations
from typing import Generator
@ -10,13 +9,10 @@ from selenium.webdriver.remote.webdriver import WebDriver
from reflex.testing import AppHarness
from .utils import SessionStorage
def CallScript():
"""A test app for browser javascript integration."""
from pathlib import Path
from typing import Optional, Union
from typing import Dict, List, Optional, Union
import reflex as rx
@ -43,32 +39,25 @@ def CallScript():
external_scripts = inline_scripts.replace("inline", "external")
class CallScriptState(rx.State):
results: rx.Field[list[Optional[Union[str, dict, list]]]] = rx.field([])
inline_counter: rx.Field[int] = rx.field(0)
external_counter: rx.Field[int] = rx.field(0)
value: str = "Initial"
last_result: int = 0
results: List[Optional[Union[str, Dict, List]]] = []
inline_counter: int = 0
external_counter: int = 0
@rx.event
def call_script_callback(self, result):
self.results.append(result)
@rx.event
def call_script_callback_other_arg(self, result, other_arg):
self.results.append([other_arg, result])
@rx.event
def call_scripts_inline_yield(self):
yield rx.call_script("inline1()")
yield rx.call_script("inline2()")
yield rx.call_script("inline3()")
yield rx.call_script("inline4()")
@rx.event
def call_script_inline_return(self):
return rx.call_script("inline2()")
@rx.event
def call_scripts_inline_yield_callback(self):
yield rx.call_script(
"inline1()", callback=CallScriptState.call_script_callback
@ -83,40 +72,34 @@ def CallScript():
"inline4()", callback=CallScriptState.call_script_callback
)
@rx.event
def call_script_inline_return_callback(self):
return rx.call_script(
"inline3()", callback=CallScriptState.call_script_callback
)
@rx.event
def call_script_inline_return_lambda(self):
return rx.call_script(
"inline2()",
callback=lambda result: CallScriptState.call_script_callback_other_arg(
callback=lambda result: CallScriptState.call_script_callback_other_arg( # type: ignore
result, "lambda"
),
)
@rx.event
def get_inline_counter(self):
return rx.call_script(
"inline_counter",
callback=CallScriptState.setvar("inline_counter"),
callback=CallScriptState.set_inline_counter, # type: ignore
)
@rx.event
def call_scripts_external_yield(self):
yield rx.call_script("external1()")
yield rx.call_script("external2()")
yield rx.call_script("external3()")
yield rx.call_script("external4()")
@rx.event
def call_script_external_return(self):
return rx.call_script("external2()")
@rx.event
def call_scripts_external_yield_callback(self):
yield rx.call_script(
"external1()", callback=CallScriptState.call_script_callback
@ -131,83 +114,55 @@ def CallScript():
"external4()", callback=CallScriptState.call_script_callback
)
@rx.event
def call_script_external_return_callback(self):
return rx.call_script(
"external3()", callback=CallScriptState.call_script_callback
)
@rx.event
def call_script_external_return_lambda(self):
return rx.call_script(
"external2()",
callback=lambda result: CallScriptState.call_script_callback_other_arg(
callback=lambda result: CallScriptState.call_script_callback_other_arg( # type: ignore
result, "lambda"
),
)
@rx.event
def get_external_counter(self):
return rx.call_script(
"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):
yield rx.call_script("inline_counter = 0; external_counter = 0")
self.reset()
app = rx.App(_state=rx.State)
Path("assets/external.js").write_text(external_scripts)
app = rx.App(state=rx.State)
with open("assets/external.js", "w") as f:
f.write(external_scripts)
@app.add_page
def index():
return rx.vstack(
rx.input(
value=CallScriptState.inline_counter.to(str),
id="inline_counter",
read_only=True,
rx.chakra.input(
value=CallScriptState.router.session.client_token,
is_read_only=True,
id="token",
),
rx.input(
value=CallScriptState.external_counter.to(str),
rx.chakra.input(
value=CallScriptState.inline_counter.to(str), # type: ignore
id="inline_counter",
is_read_only=True,
),
rx.chakra.input(
value=CallScriptState.external_counter.to(str), # type: ignore
id="external_counter",
read_only=True,
is_read_only=True,
),
rx.text_area(
value=CallScriptState.results.to_string(),
value=CallScriptState.results.to_string(), # type: ignore
id="results",
read_only=True,
is_read_only=True,
),
rx.script(inline_scripts),
rx.script(src="/external.js"),
@ -271,77 +226,7 @@ def CallScript():
on_click=CallScriptState.get_external_counter,
id="update_external_counter",
),
rx.button(
CallScriptState.value,
on_click=rx.call_script(
"'updated'",
callback=CallScriptState.setvar("value"),
),
id="update_value",
),
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 +242,7 @@ def call_script(tmp_path_factory) -> Generator[AppHarness, None, None]:
"""
with AppHarness.create(
root=tmp_path_factory.mktemp("call_script"),
app_source=CallScript,
app_source=CallScript, # type: ignore
) as harness:
yield harness
@ -380,18 +265,25 @@ def driver(call_script: AppHarness) -> Generator[WebDriver, None, None]:
driver.quit()
def assert_token(driver: WebDriver) -> str:
def assert_token(call_script: AppHarness, driver: WebDriver) -> str:
"""Get the token associated with backend state.
Args:
call_script: harness for CallScript app.
driver: WebDriver instance.
Returns:
The token visible in the driver browser.
"""
ss = SessionStorage(driver)
assert AppHarness._poll_for(lambda: ss.get("token") is not None), "token not found"
return ss.get("token")
assert call_script.app_instance is not None
token_input = driver.find_element(By.ID, "token")
assert token_input
# wait for the backend connection to send the token
token = call_script.poll_for_value(token_input)
assert token is not None
return token
@pytest.mark.parametrize("script", ["inline", "external"])
@ -407,7 +299,7 @@ def test_call_script(
driver: WebDriver instance.
script: The type of script to test.
"""
assert_token(driver)
assert_token(call_script, driver)
reset_button = driver.find_element(By.ID, "reset")
update_counter_button = driver.find_element(By.ID, f"update_{script}_counter")
counter = driver.find_element(By.ID, f"{script}_counter")
@ -462,82 +354,3 @@ def test_call_script(
)
reset_button.click()
assert call_script.poll_for_value(counter, exp_not_equal="1") == "0"
# Check that triggering script from event trigger calls callback
update_value_button = driver.find_element(By.ID, "update_value")
update_value_button.click()
assert (
call_script.poll_for_content(update_value_button, exp_not_equal="Initial")
== "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

@ -1,5 +1,4 @@
"""Integration tests for client side storage."""
from __future__ import annotations
import time
@ -10,13 +9,6 @@ from selenium.webdriver import Firefox
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webdriver import WebDriver
from reflex.state import (
State,
StateManagerDisk,
StateManagerMemory,
StateManagerRedis,
_substate_key,
)
from reflex.testing import AppHarness
from . import utils
@ -33,18 +25,18 @@ def ClientSide():
class ClientSideSubState(ClientSideState):
# cookies with default settings
c1: str = rx.Cookie()
c2: str = rx.Cookie("c2 default")
c2: rx.Cookie = "c2 default" # type: ignore
# cookies with custom settings
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/`
c6: str = rx.Cookie(name="c6")
c7: str = rx.Cookie("c7 default")
# local storage with default settings
l1: str = rx.LocalStorage()
l2: str = rx.LocalStorage("l2 default")
l2: rx.LocalStorage = "l2 default" # type: ignore
# local storage with custom settings
l3: str = rx.LocalStorage(name="l3")
@ -54,15 +46,9 @@ def ClientSide():
l5: str = rx.LocalStorage(sync=True)
l6: str = rx.LocalStorage(sync=True, name="l6")
# Session storage
s1: str = rx.SessionStorage()
s2: str = rx.SessionStorage("s2 default")
s3: str = rx.SessionStorage(name="s3")
def set_l6(self, my_param: str):
self.l6 = my_param
@rx.event
def set_var(self):
setattr(self, self.state_var, self.input_value)
self.state_var = self.input_value = ""
@ -70,30 +56,28 @@ def ClientSide():
class ClientSideSubSubState(ClientSideSubState):
c1s: str = rx.Cookie()
l1s: str = rx.LocalStorage()
s1s: str = rx.SessionStorage()
@rx.event
def set_var(self):
setattr(self, self.state_var, self.input_value)
self.state_var = self.input_value = ""
def index():
return rx.fragment(
rx.input(
rx.chakra.input(
value=ClientSideState.router.session.client_token,
read_only=True,
is_read_only=True,
id="token",
),
rx.input(
rx.chakra.input(
placeholder="state var",
value=ClientSideState.state_var,
on_change=ClientSideState.setvar("state_var"),
on_change=ClientSideState.set_state_var, # type: ignore
id="state_var",
),
rx.input(
rx.chakra.input(
placeholder="input value",
value=ClientSideState.input_value,
on_change=ClientSideState.setvar("input_value"),
on_change=ClientSideState.set_input_value, # type: ignore
id="input_value",
),
rx.button(
@ -119,15 +103,11 @@ def ClientSide():
rx.box(ClientSideSubState.l4, id="l4"),
rx.box(ClientSideSubState.l5, id="l5"),
rx.box(ClientSideSubState.l6, id="l6"),
rx.box(ClientSideSubState.s1, id="s1"),
rx.box(ClientSideSubState.s2, id="s2"),
rx.box(ClientSideSubState.s3, id="s3"),
rx.box(ClientSideSubSubState.c1s, id="c1s"),
rx.box(ClientSideSubSubState.l1s, id="l1s"),
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, route="/foo")
@ -144,7 +124,7 @@ def client_side(tmp_path_factory) -> Generator[AppHarness, None, None]:
"""
with AppHarness.create(
root=tmp_path_factory.mktemp("client_side"),
app_source=ClientSide,
app_source=ClientSide, # type: ignore
) as harness:
yield harness
@ -182,21 +162,6 @@ def local_storage(driver: WebDriver) -> Generator[utils.LocalStorage, None, None
ls.clear()
@pytest.fixture()
def session_storage(driver: WebDriver) -> Generator[utils.SessionStorage, None, None]:
"""Get an instance of the session storage helper.
Args:
driver: WebDriver instance.
Yields:
Session storage helper.
"""
ss = utils.SessionStorage(driver)
yield ss
ss.clear()
@pytest.fixture(autouse=True)
def delete_all_cookies(driver: WebDriver) -> Generator[None, None, None]:
"""Delete all cookies after each test.
@ -225,10 +190,7 @@ def cookie_info_map(driver: WebDriver) -> dict[str, dict[str, str]]:
@pytest.mark.asyncio
async def test_client_side_state(
client_side: AppHarness,
driver: WebDriver,
local_storage: utils.LocalStorage,
session_storage: utils.SessionStorage,
client_side: AppHarness, driver: WebDriver, local_storage: utils.LocalStorage
):
"""Test client side state.
@ -236,10 +198,8 @@ async def test_client_side_state(
client_side: harness for ClientSide app.
driver: WebDriver instance.
local_storage: Local storage helper.
session_storage: Session storage helper.
"""
app = client_side.app_instance
assert app is not None
assert client_side.app_instance is not None
assert client_side.frontend_url is not None
def poll_for_token():
@ -291,12 +251,8 @@ async def test_client_side_state(
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 on defaults where present
assert c1.text == ""
@ -310,18 +266,13 @@ async def test_client_side_state(
assert l2.text == "l2 default"
assert l3.text == ""
assert l4.text == "l4 default"
assert s1.text == ""
assert s2.text == "s2 default"
assert s3.text == ""
assert c1s.text == ""
assert l1s.text == ""
assert s1s.text == ""
# no cookies should be set yet!
assert not driver.get_cookies()
local_storage_items = local_storage.items()
local_storage_items.pop("last_compiled_time", None)
local_storage_items.pop("theme", None)
local_storage_items.pop("chakra-ui-color-mode", None)
assert not local_storage_items
# set some cookies and local storage values
@ -336,44 +287,32 @@ async def test_client_side_state(
set_sub("l2", "l2 value")
set_sub("l3", "l3 value")
set_sub("l4", "l4 value")
set_sub("s1", "s1 value")
set_sub("s2", "s2 value")
set_sub("s3", "s3 value")
set_sub_sub("c1s", "c1s value")
set_sub_sub("l1s", "l1s value")
set_sub_sub("s1s", "s1s value")
state_name = client_side.get_full_state_name(["_client_side_state"])
sub_state_name = client_side.get_full_state_name(
["_client_side_state", "_client_side_sub_state"]
)
sub_sub_state_name = client_side.get_full_state_name(
["_client_side_state", "_client_side_sub_state", "_client_side_sub_sub_state"]
)
exp_cookies = {
f"{sub_state_name}.c1": {
"state.client_side_state.client_side_sub_state.c1": {
"domain": "localhost",
"httpOnly": False,
"name": f"{sub_state_name}.c1",
"name": "state.client_side_state.client_side_sub_state.c1",
"path": "/",
"sameSite": "Lax",
"secure": False,
"value": "c1%20value",
},
f"{sub_state_name}.c2": {
"state.client_side_state.client_side_sub_state.c2": {
"domain": "localhost",
"httpOnly": False,
"name": f"{sub_state_name}.c2",
"name": "state.client_side_state.client_side_sub_state.c2",
"path": "/",
"sameSite": "Lax",
"secure": False,
"value": "c2%20value",
},
f"{sub_state_name}.c4": {
"state.client_side_state.client_side_sub_state.c4": {
"domain": "localhost",
"httpOnly": False,
"name": f"{sub_state_name}.c4",
"name": "state.client_side_state.client_side_sub_state.c4",
"path": "/",
"sameSite": "Strict",
"secure": False,
@ -388,19 +327,19 @@ async def test_client_side_state(
"secure": False,
"value": "c6%20value",
},
f"{sub_state_name}.c7": {
"state.client_side_state.client_side_sub_state.c7": {
"domain": "localhost",
"httpOnly": False,
"name": f"{sub_state_name}.c7",
"name": "state.client_side_state.client_side_sub_state.c7",
"path": "/",
"sameSite": "Lax",
"secure": False,
"value": "c7%20value",
},
f"{sub_sub_state_name}.c1s": {
"state.client_side_state.client_side_sub_state.client_side_sub_sub_state.c1s": {
"domain": "localhost",
"httpOnly": False,
"name": f"{sub_sub_state_name}.c1s",
"name": "state.client_side_state.client_side_sub_state.client_side_sub_sub_state.c1s",
"path": "/",
"sameSite": "Lax",
"secure": False,
@ -418,13 +357,18 @@ async def test_client_side_state(
# Test cookie with expiry by itself to avoid timing flakiness
set_sub("c3", "c3 value")
AppHarness._poll_for(lambda: f"{sub_state_name}.c3" in cookie_info_map(driver))
c3_cookie = cookie_info_map(driver)[f"{sub_state_name}.c3"]
AppHarness._poll_for(
lambda: "state.client_side_state.client_side_sub_state.c3"
in cookie_info_map(driver)
)
c3_cookie = cookie_info_map(driver)[
"state.client_side_state.client_side_sub_state.c3"
]
assert c3_cookie.pop("expiry") is not None
assert c3_cookie == {
"domain": "localhost",
"httpOnly": False,
"name": f"{sub_state_name}.c3",
"name": "state.client_side_state.client_side_sub_state.c3",
"path": "/",
"sameSite": "Lax",
"secure": False,
@ -433,26 +377,34 @@ async def test_client_side_state(
time.sleep(2) # wait for c3 to expire
if not isinstance(driver, Firefox):
# Note: Firefox does not remove expired cookies Bug 576347
assert f"{sub_state_name}.c3" not in cookie_info_map(driver)
assert (
"state.client_side_state.client_side_sub_state.c3"
not in cookie_info_map(driver)
)
local_storage_items = local_storage.items()
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}.l2") == "l2 value"
local_storage_items.pop("chakra-ui-color-mode", None)
assert (
local_storage_items.pop("state.client_side_state.client_side_sub_state.l1")
== "l1 value"
)
assert (
local_storage_items.pop("state.client_side_state.client_side_sub_state.l2")
== "l2 value"
)
assert local_storage_items.pop("l3") == "l3 value"
assert local_storage_items.pop(f"{sub_state_name}.l4") == "l4 value"
assert local_storage_items.pop(f"{sub_sub_state_name}.l1s") == "l1s value"
assert (
local_storage_items.pop("state.client_side_state.client_side_sub_state.l4")
== "l4 value"
)
assert (
local_storage_items.pop(
"state.client_side_state.client_side_sub_state.client_side_sub_sub_state.l1s"
)
== "l1s value"
)
assert not local_storage_items
session_storage_items = session_storage.items()
session_storage_items.pop("token", None)
assert session_storage_items.pop(f"{sub_state_name}.s1") == "s1 value"
assert session_storage_items.pop(f"{sub_state_name}.s2") == "s2 value"
assert session_storage_items.pop("s3") == "s3 value"
assert session_storage_items.pop(f"{sub_sub_state_name}.s1s") == "s1s value"
assert not session_storage_items
assert c1.text == "c1 value"
assert c2.text == "c2 value"
assert c3.text == "c3 value"
@ -464,12 +416,8 @@ async def test_client_side_state(
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"
# navigate to the /foo route
with utils.poll_for_navigation(driver):
@ -487,12 +435,8 @@ async def test_client_side_state(
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 value"
assert c2.text == "c2 value"
@ -505,15 +449,11 @@ async def test_client_side_state(
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"
# reset the backend state to force refresh from client storage
async with client_side.modify_state(f"{token}_{state_name}") as state:
async with client_side.modify_state(f"{token}_state.client_side_state") as state:
state.reset()
driver.refresh()
@ -535,12 +475,8 @@ async def test_client_side_state(
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 value"
assert c2.text == "c2 value"
@ -553,19 +489,20 @@ async def test_client_side_state(
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"
# make sure c5 cookie shows up on the `/foo` route
AppHarness._poll_for(lambda: f"{sub_state_name}.c5" in cookie_info_map(driver))
assert cookie_info_map(driver)[f"{sub_state_name}.c5"] == {
AppHarness._poll_for(
lambda: "state.client_side_state.client_side_sub_state.c5"
in cookie_info_map(driver)
)
assert cookie_info_map(driver)[
"state.client_side_state.client_side_sub_state.c5"
] == {
"domain": "localhost",
"httpOnly": False,
"name": f"{sub_state_name}.c5",
"name": "state.client_side_state.client_side_sub_state.c5",
"path": "/foo/",
"sameSite": "Lax",
"secure": False,
@ -588,15 +525,6 @@ async def test_client_side_state(
assert AppHarness._poll_for(lambda: l6.text == "l6 value")
assert l5.text == "l5 value"
# Set session storage values in the new tab
set_sub("s1", "other tab s1")
s1 = driver.find_element(By.ID, "s1")
s2 = driver.find_element(By.ID, "s2")
s3 = driver.find_element(By.ID, "s3")
assert AppHarness._poll_for(lambda: s1.text == "other tab s1")
assert s2.text == "s2 default"
assert s3.text == ""
# Switch back to main window.
driver.switch_to.window(main_tab)
@ -606,116 +534,6 @@ async def test_client_side_state(
assert AppHarness._poll_for(lambda: l6.text == "l6 value")
assert l5.text == "l5 value"
s1 = driver.find_element(By.ID, "s1")
s2 = driver.find_element(By.ID, "s2")
s3 = driver.find_element(By.ID, "s3")
assert AppHarness._poll_for(lambda: s1.text == "s1 value")
assert s2.text == "s2 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
driver.delete_all_cookies()
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

@ -15,67 +15,43 @@ def ComputedVars():
"""Test app for computed vars."""
import reflex as rx
class StateMixin(rx.State, mixin=True):
pass
class State(StateMixin, rx.State):
class State(rx.State):
count: int = 0
# cached var with dep on count
@rx.var(interval=15)
@rx.cached_var(interval=15)
def count1(self) -> int:
return self.count
# cached backend var with dep on count
@rx.var(interval=15, backend=True)
def count1_backend(self) -> int:
return self.count
# same as above but implicit backend with `_` prefix
@rx.var(interval=15)
def _count1_backend(self) -> int:
# same as above, different notation
@rx.var(interval=15, cache=True)
def count2(self) -> int:
return self.count
# explicit disabled auto_deps
@rx.var(interval=15, auto_deps=False)
@rx.var(interval=15, cache=True, auto_deps=False)
def count3(self) -> int:
# this will not add deps, because auto_deps is False
print(self.count1)
print(self.count2)
return self.count
# explicit dependency on count var
@rx.var(deps=["count"], auto_deps=False)
def depends_on_count(self) -> int:
return self.count
# explicit dependency on count1 var
@rx.var(deps=[count1], auto_deps=False)
@rx.cached_var(deps=[count1], auto_deps=False)
def depends_on_count1(self) -> int:
return self.count
@rx.var(
deps=[count3],
auto_deps=False,
)
@rx.var(deps=[count3], auto_deps=False, cache=True)
def depends_on_count3(self) -> int:
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):
self.count += 1
@rx.event
def mark_dirty(self):
self._mark_dirty()
assert State.backend_vars == {}
def index() -> rx.Component:
return rx.center(
rx.vstack(
@ -90,17 +66,10 @@ def ComputedVars():
rx.text(State.count, id="count"),
rx.text("count1:"),
rx.text(State.count1, id="count1"),
rx.text("count1_backend:"),
rx.text(State.count1_backend, id="count1_backend"),
rx.text("_count1_backend:"),
rx.text(State._count1_backend, id="_count1_backend"),
rx.text("count2:"),
rx.text(State.count2, id="count2"),
rx.text("count3:"),
rx.text(State.count3, id="count3"),
rx.text("depends_on_count:"),
rx.text(
State.depends_on_count,
id="depends_on_count",
),
rx.text("depends_on_count1:"),
rx.text(
State.depends_on_count1,
@ -111,21 +80,17 @@ def ComputedVars():
State.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.add_page(index)
@pytest.fixture(scope="module")
def computed_vars(
tmp_path_factory: pytest.TempPathFactory,
tmp_path_factory,
) -> Generator[AppHarness, None, None]:
"""Start ComputedVars app at tmp_path via AppHarness.
@ -136,8 +101,8 @@ def computed_vars(
running AppHarness instance
"""
with AppHarness.create(
root=tmp_path_factory.mktemp("computed_vars"),
app_source=ComputedVars,
root=tmp_path_factory.mktemp(f"computed_vars"),
app_source=ComputedVars, # type: ignore
) as harness:
yield harness
@ -182,8 +147,7 @@ def token(computed_vars: AppHarness, driver: WebDriver) -> str:
return token
@pytest.mark.asyncio
async def test_computed_vars(
def test_computed_vars(
computed_vars: AppHarness,
driver: WebDriver,
token: str,
@ -197,22 +161,6 @@ async def test_computed_vars(
"""
assert computed_vars.app_instance is not None
state_name = computed_vars.get_state_name("_state")
full_state_name = computed_vars.get_full_state_name(["_state"])
token = f"{token}_{full_state_name}"
state = (await computed_vars.get_state(token)).substates[state_name]
assert state is not None
assert state.count1_backend == 0
assert state._count1_backend == 0
# test that backend var is not rendered
count1_backend = driver.find_element(By.ID, "count1_backend")
assert count1_backend
assert count1_backend.text == ""
_count1_backend = driver.find_element(By.ID, "_count1_backend")
assert _count1_backend
assert _count1_backend.text == ""
count = driver.find_element(By.ID, "count")
assert count
assert count.text == "0"
@ -221,14 +169,14 @@ async def test_computed_vars(
assert count1
assert count1.text == "0"
count2 = driver.find_element(By.ID, "count2")
assert count2
assert count2.text == "0"
count3 = driver.find_element(By.ID, "count3")
assert count3
assert count3.text == "0"
depends_on_count = driver.find_element(By.ID, "depends_on_count")
assert depends_on_count
assert depends_on_count.text == "0"
depends_on_count1 = driver.find_element(By.ID, "depends_on_count1")
assert depends_on_count1
assert depends_on_count1.text == "0"
@ -237,10 +185,6 @@ async def test_computed_vars(
assert depends_on_count3
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")
assert increment.is_enabled()
@ -252,20 +196,11 @@ async def test_computed_vars(
increment.click()
assert computed_vars.poll_for_content(count, timeout=2, exp_not_equal="0") == "1"
assert computed_vars.poll_for_content(count1, timeout=2, exp_not_equal="0") == "1"
assert (
computed_vars.poll_for_content(depends_on_count, timeout=2, exp_not_equal="0")
== "1"
)
state = (await computed_vars.get_state(token)).substates[state_name]
assert state is not None
assert state.count1_backend == 1
assert count1_backend.text == ""
assert state._count1_backend == 1
assert _count1_backend.text == ""
assert computed_vars.poll_for_content(count2, timeout=2, exp_not_equal="0") == "1"
mark_dirty.click()
with pytest.raises(TimeoutError):
_ = computed_vars.poll_for_content(count3, timeout=5, exp_not_equal="0")
computed_vars.poll_for_content(count3, timeout=5, exp_not_equal="0")
time.sleep(10)
assert count3.text == "0"

View File

@ -0,0 +1,89 @@
"""Test case for displaying the connection banner when the websocket drops."""
from typing import Generator
import pytest
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.by import By
from reflex.testing import AppHarness, WebDriver
def ConnectionBanner():
"""App with a connection banner."""
import reflex as rx
class State(rx.State):
foo: int = 0
def index():
return rx.text("Hello World")
app = rx.App(state=rx.State)
app.add_page(index)
@pytest.fixture()
def connection_banner(tmp_path) -> Generator[AppHarness, None, None]:
"""Start ConnectionBanner 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=ConnectionBanner, # type: ignore
) as harness:
yield harness
CONNECTION_ERROR_XPATH = "//*[ text() = 'Connection Error' ]"
def has_error_modal(driver: WebDriver) -> bool:
"""Check if the connection error modal is displayed.
Args:
driver: Selenium webdriver instance.
Returns:
True if the modal is displayed, False otherwise.
"""
try:
driver.find_element(By.XPATH, CONNECTION_ERROR_XPATH)
return True
except NoSuchElementException:
return False
def test_connection_banner(connection_banner: AppHarness):
"""Test that the connection banner is displayed when the websocket drops.
Args:
connection_banner: AppHarness instance.
"""
assert connection_banner.app_instance is not None
assert connection_banner.backend is not None
driver = connection_banner.frontend()
connection_banner._poll_for(lambda: not has_error_modal(driver))
# Get the backend port
backend_port = connection_banner._poll_for_servers().getsockname()[1]
# Kill the backend
connection_banner.backend.should_exit = True
if connection_banner.backend_thread is not None:
connection_banner.backend_thread.join()
# Error modal should now be displayed
connection_banner._poll_for(lambda: has_error_modal(driver))
# Bring the backend back up
connection_banner._start_backend(port=backend_port)
# Banner should be gone now
connection_banner._poll_for(lambda: not has_error_modal(driver))

View File

@ -17,16 +17,15 @@ def DeployUrlSample() -> None:
import reflex as rx
class State(rx.State):
@rx.event
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():
return rx.fragment(
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)
@ -44,7 +43,7 @@ def deploy_url_sample(
"""
with AppHarness.create(
root=tmp_path_factory.mktemp("deploy_url_sample"),
app_source=DeployUrlSample,
app_source=DeployUrlSample, # type: ignore
) as harness:
yield harness

View File

@ -1,8 +1,6 @@
"""Integration tests for dynamic route page behavior."""
from __future__ import annotations
import time
from typing import Callable, Coroutine, Generator, Type
from urllib.parse import urlsplit
@ -22,19 +20,14 @@ def DynamicRoute():
class DynamicState(rx.State):
order: List[str] = []
page_id: str = ""
@rx.event
def on_load(self):
page_data = f"{self.router.page.path}-{self.page_id or 'no page id'}"
print(f"on_load: {page_data}")
self.order.append(page_data)
self.order.append(f"{self.router.page.path}-{self.page_id or 'no page id'}")
@rx.event
def on_load_redir(self):
query_params = self.router.page.params
page_data = f"on_load_redir-{query_params}"
print(f"on_load_redir: {page_data}")
self.order.append(page_data)
self.order.append(f"on_load_redir-{query_params}")
return rx.redirect(f"/page/{query_params['page_id']}")
@rx.var
@ -46,15 +39,17 @@ def DynamicRoute():
def index():
return rx.fragment(
rx.input(
rx.chakra.input(
value=DynamicState.router.session.client_token,
read_only=True,
is_read_only=True,
id="token",
),
rx.input(value=rx.State.page_id, read_only=True, id="page_id"), # pyright: ignore [reportAttributeAccessIssue]
rx.input(
rx.chakra.input(
value=DynamicState.page_id, is_read_only=True, id="page_id"
),
rx.chakra.input(
value=DynamicState.router.page.raw_path,
read_only=True,
is_read_only=True,
id="raw_path",
),
rx.link("index", href="/", id="link_index"),
@ -62,89 +57,26 @@ def DynamicRoute():
rx.link(
"next",
href="/page/" + DynamicState.next_page,
id="link_page_next",
id="link_page_next", # type: ignore
),
rx.link("missing", href="/missing", id="link_missing"),
rx.list( # pyright: ignore [reportAttributeAccessIssue]
rx.chakra.list(
rx.foreach(
DynamicState.order, # pyright: ignore [reportAttributeAccessIssue]
lambda i: rx.list_item(rx.text(i)),
DynamicState.order, # type: ignore
lambda i: rx.chakra.list_item(rx.text(i)),
),
),
)
class ArgState(rx.State):
"""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)
@rx.page(route="/redirect-page/[page_id]", on_load=DynamicState.on_load_redir) # type: ignore
def redirect_page():
return rx.fragment(rx.text("redirecting..."))
app = rx.App(_state=rx.State)
app.add_page(index, route="/page/[page_id]", on_load=DynamicState.on_load)
app.add_page(index, route="/static/x", on_load=DynamicState.on_load)
app = rx.App(state=rx.State)
app.add_page(index)
app.add_custom_404_page(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) # type: ignore
app.add_custom_404_page(on_load=DynamicState.on_load) # type: ignore
@pytest.fixture(scope="module")
@ -161,9 +93,9 @@ def dynamic_route(
running AppHarness instance
"""
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_source=DynamicRoute,
app_source=DynamicRoute, # type: ignore
) as harness:
yield harness
@ -180,8 +112,6 @@ def driver(dynamic_route: AppHarness) -> Generator[WebDriver, None, None]:
"""
assert dynamic_route.app_instance is not None, "app is not running"
driver = dynamic_route.frontend()
# TODO: drop after flakiness is resolved
driver.implicitly_wait(30)
try:
yield driver
finally:
@ -223,23 +153,18 @@ def poll_for_order(
Returns:
An async function that polls for the order list to match the expected order.
"""
dynamic_state_name = dynamic_route.get_state_name("_dynamic_state")
dynamic_state_full_name = dynamic_route.get_full_state_name(["_dynamic_state"])
async def _poll_for_order(exp_order: list[str]):
async def _backend_state():
return await dynamic_route.get_state(f"{token}_{dynamic_state_full_name}")
return await dynamic_route.get_state(f"{token}_state.dynamic_state")
async def _check():
return (await _backend_state()).substates[
dynamic_state_name
"dynamic_state"
].order == exp_order
await AppHarness._poll_for_async(_check, timeout=60)
assert (
list((await _backend_state()).substates[dynamic_state_name].order)
== exp_order
)
await AppHarness._poll_for_async(_check)
assert (await _backend_state()).substates["dynamic_state"].order == exp_order
return _poll_for_order
@ -259,7 +184,6 @@ async def test_on_load_navigate(
token: The token visible in the driver browser.
poll_for_order: function that polls for the order list to match the expected order.
"""
dynamic_state_full_name = dynamic_route.get_full_state_name(["_dynamic_state"])
assert dynamic_route.app_instance is not None
is_prod = isinstance(dynamic_route, AppHarnessProd)
link = driver.find_element(By.ID, "link_page_next")
@ -309,7 +233,7 @@ async def test_on_load_navigate(
driver.get(f"{driver.current_url}?foo=bar")
await poll_for_order(exp_order)
assert (
await dynamic_route.get_state(f"{token}_{dynamic_state_full_name}")
await dynamic_route.get_state(f"{token}_state.dynamic_state")
).router.page.params["foo"] == "bar"
# hit a 404 and ensure we still hydrate
@ -377,56 +301,3 @@ async def test_on_load_navigate_non_dynamic(
link.click()
assert urlsplit(driver.current_url).path == "/static/x/"
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

@ -1,5 +1,4 @@
"""Ensure stopPropagation and preventDefault work as expected."""
from __future__ import annotations
import asyncio
@ -24,7 +23,6 @@ def TestEventAction():
def on_click(self, ev):
self.order.append(f"on_click:{ev}")
@rx.event
def on_click2(self):
self.order.append("on_click2")
@ -54,7 +52,7 @@ def TestEventAction():
def index():
return rx.vstack(
rx.input(
rx.chakra.input(
value=EventActionState.router.session.client_token,
is_read_only=True,
id="token",
@ -63,16 +61,16 @@ def TestEventAction():
rx.button(
"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(
"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",
),
rx.button(
"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",
),
rx.button(
@ -88,13 +86,13 @@ def TestEventAction():
rx.link(
"Link",
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",
),
rx.link(
"Link Stop Propagation",
href="#",
on_click=EventActionState.on_click( # pyright: ignore [reportCallIssue]
on_click=EventActionState.on_click( # type: ignore
"link_stop_propagation"
).stop_propagation,
id="link-stop-propagation",
@ -102,13 +100,13 @@ def TestEventAction():
rx.link(
"Link Prevent Default Only",
href="/invalid",
on_click=rx.prevent_default, # pyright: ignore [reportArgumentType]
on_click=rx.prevent_default, # type: ignore
id="link-prevent-default-only",
),
rx.link(
"Link Prevent Default",
href="/invalid",
on_click=EventActionState.on_click( # pyright: ignore [reportCallIssue]
on_click=EventActionState.on_click( # type: ignore
"link_prevent_default"
).prevent_default,
id="link-prevent-default",
@ -116,47 +114,47 @@ def TestEventAction():
rx.link(
"Link Both",
href="/invalid",
on_click=EventActionState.on_click( # pyright: ignore [reportCallIssue]
on_click=EventActionState.on_click( # type: ignore
"link_both"
).stop_propagation.prevent_default,
id="link-stop-propagation-prevent-default",
),
EventFiringComponent.create(
id="custom-stop-propagation",
on_click=EventActionState.on_click( # pyright: ignore [reportCallIssue]
on_click=EventActionState.on_click( # type: ignore
"custom-stop-propagation"
).stop_propagation,
),
EventFiringComponent.create(
id="custom-prevent-default",
on_click=EventActionState.on_click( # pyright: ignore [reportCallIssue]
on_click=EventActionState.on_click( # type: ignore
"custom-prevent-default"
).prevent_default,
),
rx.button(
"Throttle",
id="btn-throttle",
on_click=lambda: EventActionState.on_click_throttle.throttle( # pyright: ignore [reportFunctionMemberAccess]
on_click=lambda: EventActionState.on_click_throttle.throttle(
200
).stop_propagation,
),
rx.button(
"Debounce",
id="btn-debounce",
on_click=EventActionState.on_click_debounce.debounce( # pyright: ignore [reportFunctionMemberAccess]
on_click=EventActionState.on_click_debounce.debounce(
200
).stop_propagation,
),
rx.list( # pyright: ignore [reportAttributeAccessIssue]
rx.chakra.list(
rx.foreach(
EventActionState.order,
rx.list_item,
EventActionState.order, # type: ignore
rx.chakra.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)
@ -171,8 +169,8 @@ def event_action(tmp_path_factory) -> Generator[AppHarness, None, None]:
running AppHarness instance
"""
with AppHarness.create(
root=tmp_path_factory.mktemp("event_action"),
app_source=TestEventAction,
root=tmp_path_factory.mktemp(f"event_action"),
app_source=TestEventAction, # type: ignore
) as harness:
yield harness
@ -230,18 +228,20 @@ def poll_for_order(
Returns:
An async function that polls for the order list to match the expected order.
"""
state_name = event_action.get_state_name("_event_action_state")
state_full_name = event_action.get_full_state_name(["_event_action_state"])
async def _poll_for_order(exp_order: list[str]):
async def _backend_state():
return await event_action.get_state(f"{token}_{state_full_name}")
return await event_action.get_state(f"{token}_state.event_action_state")
async def _check():
return (await _backend_state()).substates[state_name].order == exp_order
return (await _backend_state()).substates[
"event_action_state"
].order == exp_order
await AppHarness._poll_for_async(_check)
assert (await _backend_state()).substates[state_name].order == exp_order
assert (await _backend_state()).substates[
"event_action_state"
].order == exp_order
return _poll_for_order

View File

@ -1,5 +1,4 @@
"""Ensure that Event Chains are properly queued and handled between frontend and backend."""
from __future__ import annotations
from typing import Generator
@ -27,126 +26,107 @@ def EventChain():
event_order: List[str] = []
interim_value: str = ""
@rx.event
def event_no_args(self):
self.event_order.append("event_no_args")
@rx.event
def event_arg(self, arg):
self.event_order.append(f"event_arg:{arg}")
@rx.event
def event_arg_repr_type(self, arg):
self.event_order.append(f"event_arg_repr:{arg!r}_{type(arg).__name__}")
@rx.event
def event_nested_1(self):
self.event_order.append("event_nested_1")
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):
self.event_order.append("event_nested_2")
yield State.event_nested_3
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):
self.event_order.append("event_nested_3")
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):
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):
self.event_order.append("on_load_yield_chain")
yield State.event_arg(4)
yield State.event_arg(5)
yield State.event_arg(6)
yield State.event_arg(4) # type: ignore
yield State.event_arg(5) # type: ignore
yield State.event_arg(6) # type: ignore
@rx.event
def click_return_event(self):
self.event_order.append("click_return_event")
return State.event_no_args
@rx.event
def click_return_events(self):
self.event_order.append("click_return_events")
return [
State.event_arg(7),
State.event_arg(7), # type: ignore
rx.console_log("click_return_events"),
State.event_arg(8),
State.event_arg(9),
State.event_arg(8), # type: ignore
State.event_arg(9), # type: ignore
]
@rx.event
def click_yield_chain(self):
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")
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")
yield State.event_arg(12)
yield State.event_arg(12) # type: ignore
self.event_order.append("click_yield_chain:3")
@rx.event
def click_yield_many_events(self):
self.event_order.append("click_yield_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}")
self.event_order.append("click_yield_many_events_done")
@rx.event
def click_yield_nested(self):
self.event_order.append("click_yield_nested")
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):
self.event_order.append("redirect_return_chain")
yield rx.redirect("/on-load-return-chain")
@rx.event
def redirect_yield_chain(self):
self.event_order.append("redirect_yield_chain")
yield rx.redirect("/on-load-yield-chain")
@rx.event
def click_return_int_type(self):
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):
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):
self.interim_value = "interim"
yield
await asyncio.sleep(0.5)
self.interim_value = "final"
@rx.event
def click_yield_interim_value(self):
self.interim_value = "interim"
yield
time.sleep(0.5)
self.interim_value = "final"
app = rx.App(_state=rx.State)
app = rx.App(state=rx.State)
token_input = rx.input(
token_input = rx.chakra.input(
value=State.router.session.client_token, is_read_only=True, id="token"
)
@ -154,7 +134,9 @@ def EventChain():
def index():
return rx.fragment(
token_input,
rx.input(value=State.interim_value, is_read_only=True, id="interim_value"),
rx.chakra.input(
value=State.interim_value, is_read_only=True, id="interim_value"
),
rx.button(
"Return Event",
id="return_event",
@ -193,12 +175,12 @@ def EventChain():
rx.button(
"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(
"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(
"Return Chain Int Type",
@ -239,7 +221,7 @@ def EventChain():
rx.text(
"return",
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,
rx.button("Unmount", on_click=rx.redirect("/"), id="unmount"),
@ -251,7 +233,7 @@ def EventChain():
"yield",
on_mount=[
State.on_load_yield_chain,
lambda: State.event_arg("mount"),
lambda: State.event_arg("mount"), # type: ignore
],
on_unmount=State.event_no_args,
),
@ -259,8 +241,8 @@ def EventChain():
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_yield_chain, on_load=State.on_load_yield_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) # type: ignore
app.add_page(on_mount_return_chain)
app.add_page(on_mount_yield_chain)
@ -277,7 +259,7 @@ def event_chain(tmp_path_factory) -> Generator[AppHarness, None, None]:
"""
with AppHarness.create(
root=tmp_path_factory.mktemp("event_chain"),
app_source=EventChain,
app_source=EventChain, # type: ignore
) as harness:
yield harness
@ -318,8 +300,7 @@ def assert_token(event_chain: AppHarness, driver: WebDriver) -> str:
token = event_chain.poll_for_value(token_input)
assert token is not None
state_name = event_chain.get_full_state_name(["_state"])
return f"{token}_{state_name}"
return f"{token}_state.state"
@pytest.mark.parametrize(
@ -418,17 +399,16 @@ async def test_event_chain_click(
exp_event_order: the expected events recorded in the State
"""
token = assert_token(event_chain, driver)
state_name = event_chain.get_state_name("_state")
btn = driver.find_element(By.ID, button_id)
btn.click()
async def _has_all_events():
return len(
(await event_chain.get_state(token)).substates[state_name].event_order
(await event_chain.get_state(token)).substates["state"].event_order
) == len(exp_event_order)
await AppHarness._poll_for_async(_has_all_events)
event_order = (await event_chain.get_state(token)).substates[state_name].event_order
event_order = (await event_chain.get_state(token)).substates["state"].event_order
assert event_order == exp_event_order
@ -473,15 +453,14 @@ async def test_event_chain_on_load(
assert event_chain.frontend_url is not None
driver.get(event_chain.frontend_url + uri)
token = assert_token(event_chain, driver)
state_name = event_chain.get_state_name("_state")
async def _has_all_events():
return len(
(await event_chain.get_state(token)).substates[state_name].event_order
(await event_chain.get_state(token)).substates["state"].event_order
) == len(exp_event_order)
await AppHarness._poll_for_async(_has_all_events)
backend_state = (await event_chain.get_state(token)).substates[state_name]
backend_state = (await event_chain.get_state(token)).substates["state"]
assert backend_state.event_order == exp_event_order
assert backend_state.is_hydrated is True
@ -493,6 +472,11 @@ async def test_event_chain_on_load(
"/on-mount-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:2",
"event_arg:3",
@ -504,6 +488,12 @@ async def test_event_chain_on_load(
[
"on_load_yield_chain",
"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:5",
"event_arg:6",
@ -535,7 +525,6 @@ async def test_event_chain_on_mount(
assert event_chain.frontend_url is not None
driver.get(event_chain.frontend_url + uri)
token = assert_token(event_chain, driver)
state_name = event_chain.get_state_name("_state")
unmount_button = driver.find_element(By.ID, "unmount")
assert unmount_button
@ -543,11 +532,11 @@ async def test_event_chain_on_mount(
async def _has_all_events():
return len(
(await event_chain.get_state(token)).substates[state_name].event_order
(await event_chain.get_state(token)).substates["state"].event_order
) == len(exp_event_order)
await AppHarness._poll_for_async(_has_all_events)
event_order = (await event_chain.get_state(token)).substates[state_name].event_order
event_order = (await event_chain.get_state(token)).substates["state"].event_order
assert event_order == exp_event_order

View File

@ -1,5 +1,4 @@
"""Integration tests for forms."""
import functools
import time
from typing import Generator
@ -30,34 +29,33 @@ def FormSubmit(form_component):
def form_submit(self, form_data: Dict):
self.form_data = form_data
app = rx.App(_state=rx.State)
app = rx.App(state=rx.State)
@app.add_page
def index():
return rx.vstack(
rx.input(
rx.chakra.input(
value=FormState.router.session.client_token,
is_read_only=True,
id="token",
),
eval(form_component)(
rx.vstack(
rx.input(id="name_input"),
rx.chakra.input(id="name_input"),
rx.hstack(rx.chakra.pin_input(length=4, id="pin_input")),
rx.chakra.number_input(id="number_input"),
rx.checkbox(id="bool_input"),
rx.switch(id="bool_input2"),
rx.checkbox(id="bool_input3"),
rx.switch(id="bool_input4"),
rx.slider(id="slider_input", default_value=[50], width="100%"),
rx.chakra.range_slider(id="range_input"),
rx.radio(["option1", "option2"], id="radio_input"),
rx.radio(FormState.var_options, id="radio_input_var"),
rx.select(
["option1", "option2"],
name="select_input",
default_value="option1",
),
rx.select(FormState.var_options, id="select_input_var"),
rx.chakra.select(["option1", "option2"], id="select_input"),
rx.chakra.select(FormState.var_options, id="select_input_var"),
rx.text_area(id="text_area_input"),
rx.input(
rx.chakra.input(
id="debounce_input",
debounce_timeout=0,
on_change=rx.console_log,
@ -90,24 +88,27 @@ def FormSubmitName(form_component):
def form_submit(self, form_data: Dict):
self.form_data = form_data
app = rx.App(_state=rx.State)
app = rx.App(state=rx.State)
@app.add_page
def index():
return rx.vstack(
rx.input(
rx.chakra.input(
value=FormState.router.session.client_token,
is_read_only=True,
id="token",
),
eval(form_component)(
rx.vstack(
rx.input(name="name_input"),
rx.chakra.input(name="name_input"),
rx.hstack(rx.chakra.pin_input(length=4, name="pin_input")),
rx.chakra.number_input(name="number_input"),
rx.checkbox(name="bool_input"),
rx.switch(name="bool_input2"),
rx.checkbox(name="bool_input3"),
rx.switch(name="bool_input4"),
rx.slider(name="slider_input", default_value=[50], width="100%"),
rx.chakra.range_slider(name="range_input"),
rx.radio(FormState.options, name="radio_input"),
rx.select(
FormState.options,
@ -115,13 +116,21 @@ def FormSubmitName(form_component):
default_value=FormState.options[0],
),
rx.text_area(name="text_area_input"),
rx.input(
name="debounce_input",
debounce_timeout=0,
on_change=rx.console_log,
rx.chakra.input_group(
rx.chakra.input_left_element(rx.icon(tag="chevron_right")),
rx.chakra.input(
name="debounce_input",
debounce_timeout=0,
on_change=rx.console_log,
),
rx.chakra.input_right_element(rx.icon(tag="chevron_left")),
),
rx.chakra.button_group(
rx.button("Submit", type_="submit"),
rx.icon_button(FormState.val, icon=rx.icon(tag="plus")),
variant="outline",
is_attached=True,
),
rx.button("Submit", type_="submit"),
rx.icon_button(rx.icon(tag="plus")),
),
on_submit=FormState.form_submit,
custom_attrs={"action": "/invalid"},
@ -138,12 +147,16 @@ def FormSubmitName(form_component):
functools.partial(FormSubmitName, form_component="rx.form.root"),
functools.partial(FormSubmit, form_component="rx.el.form"),
functools.partial(FormSubmitName, form_component="rx.el.form"),
functools.partial(FormSubmit, form_component="rx.chakra.form"),
functools.partial(FormSubmitName, form_component="rx.chakra.form"),
],
ids=[
"id-radix",
"name-radix",
"id-html",
"name-html",
"id-chakra",
"name-chakra",
],
)
def form_submit(request, tmp_path_factory) -> Generator[AppHarness, None, None]:
@ -159,7 +172,7 @@ def form_submit(request, tmp_path_factory) -> Generator[AppHarness, None, None]:
param_id = request._pyfuncitem.callspec.id.replace("-", "_")
with AppHarness.create(
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}",
) as harness:
assert harness.app_instance is not None, "app is not running"
@ -206,6 +219,16 @@ async def test_submit(driver, form_submit: AppHarness):
name_input = driver.find_element(by, "name_input")
name_input.send_keys("foo")
pin_inputs = driver.find_elements(By.CLASS_NAME, "chakra-pin-input")
pin_values = ["8", "1", "6", "4"]
for i, pin_input in enumerate(pin_inputs):
pin_input.send_keys(pin_values[i])
number_input = driver.find_element(By.CLASS_NAME, "chakra-numberinput")
buttons = number_input.find_elements(By.XPATH, "//div[@role='button']")
for _ in range(3):
buttons[1].click()
checkbox_input = driver.find_element(By.XPATH, "//button[@role='checkbox']")
checkbox_input.click()
@ -228,13 +251,10 @@ async def test_submit(driver, form_submit: AppHarness):
submit_input = driver.find_element(By.CLASS_NAME, "rt-Button")
submit_input.click()
state_name = form_submit.get_state_name("_form_state")
full_state_name = form_submit.get_full_state_name(["_form_state"])
async def get_form_data():
return (
(await form_submit.get_state(f"{token}_{full_state_name}"))
.substates[state_name]
(await form_submit.get_state(f"{token}_state.form_state"))
.substates["form_state"]
.form_data
)
@ -244,15 +264,16 @@ async def test_submit(driver, form_submit: AppHarness):
form_data = format.collect_form_dict_names(form_data)
print(form_data)
assert form_data["name_input"] == "foo"
assert form_data["pin_input"] == pin_values
assert form_data["number_input"] == "-3"
assert form_data["bool_input"]
assert form_data["bool_input2"]
assert not form_data.get("bool_input3", False)
assert not form_data.get("bool_input4", False)
assert form_data["slider_input"] == "50"
assert form_data["range_input"] == ["25", "75"]
assert form_data["radio_input"] == "option2"
assert form_data["select_input"] == "option1"
assert form_data["text_area_input"] == "Some\nText"

View File

@ -1,5 +1,4 @@
"""Integration tests for text input and related components."""
from typing import Generator
import pytest
@ -16,7 +15,7 @@ def FullyControlledInput():
class State(rx.State):
text: str = "initial"
app = rx.App(_state=rx.State)
app = rx.App(state=rx.State)
@app.add_page
def index():
@ -26,11 +25,11 @@ def FullyControlledInput():
),
rx.input(
id="debounce_input_input",
on_change=State.set_text, # pyright: ignore [reportAttributeAccessIssue]
on_change=State.set_text, # type: ignore
value=State.text,
),
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(
value=State.text,
id="plain_value_input",
@ -63,7 +62,7 @@ def fully_controlled_input(tmp_path) -> Generator[AppHarness, None, None]:
"""
with AppHarness.create(
root=tmp_path,
app_source=FullyControlledInput,
app_source=FullyControlledInput, # type: ignore
) as harness:
yield harness
@ -86,12 +85,9 @@ async def test_fully_controlled_input(fully_controlled_input: AppHarness):
token = fully_controlled_input.poll_for_value(token_input)
assert token
state_name = fully_controlled_input.get_state_name("_state")
full_state_name = fully_controlled_input.get_full_state_name(["_state"])
async def get_state_text():
state = await fully_controlled_input.get_state(f"{token}_{full_state_name}")
return state.substates[state_name].text
state = await fully_controlled_input.get_state(f"{token}_state.state")
return state.substates["state"].text
# ensure defaults are set correctly
assert (
@ -141,10 +137,8 @@ async def test_fully_controlled_input(fully_controlled_input: AppHarness):
assert fully_controlled_input.poll_for_value(plain_value_input) == "ifoonitial"
# clear the input on the backend
async with fully_controlled_input.modify_state(
f"{token}_{full_state_name}"
) as state:
state.substates[state_name].text = ""
async with fully_controlled_input.modify_state(f"{token}_state.state") as state:
state.substates["state"].text = ""
assert await get_state_text() == ""
assert (
fully_controlled_input.poll_for_value(
@ -183,6 +177,6 @@ async def test_fully_controlled_input(fully_controlled_input: AppHarness):
clear_button.click()
assert AppHarness._poll_for(lambda: on_change_input.get_attribute("value") == "")
# potential bug: clearing the on_change field doesn't itself trigger on_change
# assert backend_state.text == "" #noqa: ERA001
# assert debounce_input.get_attribute("value") == "" #noqa: ERA001
# assert value_input.get_attribute("value") == "" #noqa: ERA001
# assert backend_state.text == ""
# assert debounce_input.get_attribute("value") == ""
# assert value_input.get_attribute("value") == ""

View File

@ -1,5 +1,4 @@
"""Test large state."""
import time
import jinja2
@ -58,7 +57,7 @@ def test_large_state(var_count: int, tmp_path_factory, benchmark):
large_state_rendered = template.render(var_count=var_count)
with AppHarness.create(
root=tmp_path_factory.mktemp("large_state"),
root=tmp_path_factory.mktemp(f"large_state"),
app_source=large_state_rendered,
app_name="large_state",
) as large_state:

View File

@ -1,5 +1,4 @@
"""Test cases for the FastAPI lifespan integration."""
from typing import Generator
import pytest
@ -36,24 +35,21 @@ def LifespanApp():
print("Lifespan global started.")
try:
while True:
lifespan_task_global += inc # pyright: ignore[reportUnboundVariable, reportPossiblyUnboundVariable]
lifespan_task_global += inc # pyright: ignore[reportUnboundVariable]
await asyncio.sleep(0.1)
except asyncio.CancelledError as ce:
print(f"Lifespan global cancelled: {ce}.")
lifespan_task_global = 0
class LifespanState(rx.State):
interval: int = 100
@rx.var(cache=False)
@rx.var
def task_global(self) -> int:
return lifespan_task_global
@rx.var(cache=False)
@rx.var
def context_global(self) -> int:
return lifespan_context_global
@rx.event
def tick(self, date):
pass
@ -61,15 +57,7 @@ def LifespanApp():
return rx.vstack(
rx.text(LifespanState.task_global, id="task_global"),
rx.text(LifespanState.context_global, id="context_global"),
rx.button(
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",
),
rx.moment(interval=100, on_change=LifespanState.tick),
)
app = rx.App()
@ -90,7 +78,7 @@ def lifespan_app(tmp_path) -> Generator[AppHarness, None, None]:
"""
with AppHarness.create(
root=tmp_path,
app_source=LifespanApp,
app_source=LifespanApp, # type: ignore
) as harness:
yield harness
@ -113,13 +101,12 @@ async def test_lifespan(lifespan_app: AppHarness):
task_global = driver.find_element(By.ID, "task_global")
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_value = int(original_task_global_text)
lifespan_app.poll_for_content(task_global, exp_not_equal=original_task_global_text)
driver.find_element(By.ID, "toggle-tick").click() # avoid teardown errors
assert lifespan_app.app_module.lifespan_task_global > original_task_global_value
assert lifespan_app.app_module.lifespan_task_global > original_task_global_value # type: ignore
assert int(task_global.text) > original_task_global_value
# Kill the backend

View File

@ -1,5 +1,4 @@
"""Integration tests for client side storage."""
from __future__ import annotations
from typing import Generator
@ -21,18 +20,16 @@ def LoginSample():
class State(rx.State):
auth_token: str = rx.LocalStorage("")
@rx.event
def logout(self):
self.set_auth_token("")
@rx.event
def login(self):
self.set_auth_token("12345")
yield rx.redirect("/")
def index():
return rx.cond( # pyright: ignore [reportCallIssue]
State.is_hydrated & State.auth_token, # pyright: ignore [reportOperatorIssue]
return rx.cond(
State.is_hydrated & State.auth_token, # type: ignore
rx.vstack(
rx.heading(State.auth_token, id="auth-token"),
rx.button("Logout", on_click=State.logout, id="logout"),
@ -45,7 +42,7 @@ def LoginSample():
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(login)
@ -62,7 +59,7 @@ def login_sample(tmp_path_factory) -> Generator[AppHarness, None, None]:
"""
with AppHarness.create(
root=tmp_path_factory.mktemp("login_sample"),
app_source=LoginSample,
app_source=LoginSample, # type: ignore
) as harness:
yield harness
@ -139,9 +136,6 @@ def test_login_flow(
logout_button = driver.find_element(By.ID, "logout")
logout_button.click()
state_name = login_sample.get_full_state_name(["_state"])
assert login_sample._poll_for(
lambda: local_storage[f"{state_name}.auth_token"] == ""
)
assert login_sample._poll_for(lambda: local_storage["state.state.auth_token"] == "")
with pytest.raises(NoSuchElementException):
driver.find_element(By.ID, "auth-token")

View File

@ -1,5 +1,4 @@
"""Integration tests for media components."""
from typing import Generator
import pytest
@ -19,38 +18,38 @@ def MediaApp():
def _blue(self, format=None) -> Image.Image:
img = Image.new("RGB", (200, 200), "blue")
if format is not None:
img.format = format
img.format = format # type: ignore
return img
@rx.var
@rx.cached_var
def img_default(self) -> Image.Image:
return self._blue()
@rx.var
@rx.cached_var
def img_bmp(self) -> Image.Image:
return self._blue(format="BMP")
@rx.var
@rx.cached_var
def img_jpg(self) -> Image.Image:
return self._blue(format="JPEG")
@rx.var
@rx.cached_var
def img_png(self) -> Image.Image:
return self._blue(format="PNG")
@rx.var
@rx.cached_var
def img_gif(self) -> Image.Image:
return self._blue(format="GIF")
@rx.var
@rx.cached_var
def img_webp(self) -> Image.Image:
return self._blue(format="WEBP")
@rx.var
@rx.cached_var
def img_from_url(self) -> Image.Image:
img_url = "https://picsum.photos/id/1/200/300"
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()
@ -84,7 +83,7 @@ def media_app(tmp_path) -> Generator[AppHarness, None, None]:
"""
with AppHarness.create(
root=tmp_path,
app_source=MediaApp,
app_source=MediaApp, # type: ignore
) as harness:
yield harness

View File

@ -1,5 +1,4 @@
"""Integration tests for links and related components."""
from typing import Generator
from urllib.parse import urlsplit
@ -52,7 +51,7 @@ def navigation_app(tmp_path) -> Generator[AppHarness, None, None]:
"""
with AppHarness.create(
root=tmp_path,
app_source=NavigationApp,
app_source=NavigationApp, # type: ignore
) as harness:
yield harness
@ -74,7 +73,7 @@ async def test_navigation_app(navigation_app: AppHarness):
with poll_for_navigation(driver):
internal_link.click()
assert urlsplit(driver.current_url).path == "/internal/"
assert urlsplit(driver.current_url).path == f"/internal/"
with poll_for_navigation(driver):
driver.back()

View File

@ -1,5 +1,4 @@
"""Integration tests for special server side events."""
import time
from typing import Generator
@ -14,19 +13,16 @@ def ServerSideEvent():
import reflex as rx
class SSState(rx.State):
@rx.event
def set_value_yield(self):
yield rx.set_value("a", "")
yield rx.set_value("b", "")
yield rx.set_value("c", "")
@rx.event
def set_value_yield_return(self):
yield rx.set_value("a", "")
yield rx.set_value("b", "")
return rx.set_value("c", "")
@rx.event
def set_value_return(self):
return [
rx.set_value("a", ""),
@ -34,21 +30,20 @@ def ServerSideEvent():
rx.set_value("c", ""),
]
@rx.event
def set_value_return_c(self):
return rx.set_value("c", "")
app = rx.App(_state=rx.State)
app = rx.App(state=rx.State)
@app.add_page
def index():
return rx.fragment(
rx.input(
rx.chakra.input(
id="token", value=SSState.router.session.client_token, is_read_only=True
),
rx.input(default_value="a", id="a"),
rx.input(default_value="b", id="b"),
rx.input(default_value="c", id="c"),
rx.chakra.input(default_value="a", id="a"),
rx.chakra.input(default_value="b", id="b"),
rx.chakra.input(default_value="c", id="c"),
rx.button(
"Clear Immediate",
id="clear_immediate",
@ -93,7 +88,7 @@ def server_side_event(tmp_path_factory) -> Generator[AppHarness, None, None]:
"""
with AppHarness.create(
root=tmp_path_factory.mktemp("server_side_event"),
app_source=ServerSideEvent,
app_source=ServerSideEvent, # type: ignore
) as harness:
yield harness
@ -102,6 +97,7 @@ def server_side_event(tmp_path_factory) -> Generator[AppHarness, None, None]:
def driver(server_side_event: AppHarness):
"""Get an instance of the browser open to the server_side_event app.
Args:
server_side_event: harness for ServerSideEvent app

View File

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

View File

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

170
integration/test_table.py Normal file
View File

@ -0,0 +1,170 @@
"""Integration tests for table and related components."""
from typing import Generator
import pytest
from selenium.webdriver.common.by import By
from reflex.testing import AppHarness
def Table():
"""App using table component."""
from typing import List
import reflex as rx
class TableState(rx.State):
rows: List[List[str]] = [
["John", "30", "New York"],
["Jane", "31", "San Fransisco"],
["Joe", "32", "Los Angeles"],
]
headers: List[str] = ["Name", "Age", "Location"]
footers: List[str] = ["footer1", "footer2", "footer3"]
caption: str = "random caption"
app = rx.App(state=rx.State)
@app.add_page
def index():
return rx.center(
rx.chakra.input(
id="token",
value=TableState.router.session.client_token,
is_read_only=True,
),
rx.chakra.table_container(
rx.chakra.table(
headers=TableState.headers,
rows=TableState.rows,
footers=TableState.footers,
caption=TableState.caption,
variant="striped",
color_scheme="blue",
width="100%",
),
),
)
@app.add_page
def another():
return rx.center(
rx.chakra.table_container(
rx.chakra.table( # type: ignore
rx.chakra.thead( # type: ignore
rx.chakra.tr( # type: ignore
rx.chakra.th("Name"),
rx.chakra.th("Age"),
rx.chakra.th("Location"),
)
),
rx.chakra.tbody( # type: ignore
rx.chakra.tr( # type: ignore
rx.chakra.td("John"),
rx.chakra.td(30),
rx.chakra.td("New York"),
),
rx.chakra.tr( # type: ignore
rx.chakra.td("Jane"),
rx.chakra.td(31),
rx.chakra.td("San Francisco"),
),
rx.chakra.tr( # type: ignore
rx.chakra.td("Joe"),
rx.chakra.td(32),
rx.chakra.td("Los Angeles"),
),
),
rx.chakra.tfoot( # type: ignore
rx.chakra.tr(
rx.chakra.td("footer1"),
rx.chakra.td("footer2"),
rx.chakra.td("footer3"),
) # type: ignore
),
rx.chakra.table_caption("random caption"),
variant="striped",
color_scheme="teal",
)
)
)
@pytest.fixture()
def table(tmp_path_factory) -> Generator[AppHarness, None, None]:
"""Start Table app at tmp_path via AppHarness.
Args:
tmp_path_factory: pytest tmp_path_factory fixture
Yields:
running AppHarness instance
"""
with AppHarness.create(
root=tmp_path_factory.mktemp("table"),
app_source=Table, # type: ignore
) as harness:
assert harness.app_instance is not None, "app is not running"
yield harness
@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()
@pytest.mark.parametrize("route", ["", "/another"])
def test_table(driver, table: AppHarness, route):
"""Test that a table component is rendered properly.
Args:
driver: Selenium WebDriver open to the app
table: Harness for Table app
route: Page route or path.
"""
driver.get(f"{table.frontend_url}/{route}")
assert table.app_instance is not None, "app is not running"
thead = driver.find_element(By.TAG_NAME, "thead")
# poll till page is fully loaded.
table.poll_for_content(element=thead)
# check headers
assert thead.find_element(By.TAG_NAME, "tr").text == "NAME AGE LOCATION"
# check first row value
assert (
driver.find_element(By.TAG_NAME, "tbody")
.find_elements(By.TAG_NAME, "tr")[0]
.text
== "John 30 New York"
)
# check footer
assert (
driver.find_element(By.TAG_NAME, "tfoot")
.find_element(By.TAG_NAME, "tr")
.text.lower()
== "footer1 footer2 footer3"
)
# check caption
assert driver.find_element(By.TAG_NAME, "caption").text == "random caption"

View File

@ -25,27 +25,21 @@ def TailwindApp(
paragraph_text: Text for the paragraph.
paragraph_class_name: Tailwind class_name for the paragraph.
"""
from pathlib import Path
import reflex as rx
import reflex.components.radix.themes as rdxt
class UnusedState(rx.State):
pass
def index():
return rx.el.div(
rx.text(paragraph_text, class_name=paragraph_class_name),
rx.chakra.text(paragraph_text, class_name=paragraph_class_name),
rx.el.p(paragraph_text, class_name=paragraph_class_name),
rx.text(paragraph_text, as_="p", class_name=paragraph_class_name),
rx.el.div("Test external stylesheet", class_name="external"),
rdxt.text(paragraph_text, as_="p", class_name=paragraph_class_name),
id="p-content",
)
assets = Path(__file__).resolve().parent.parent / "assets"
assets.mkdir(exist_ok=True)
stylesheet = assets / "test_styles.css"
stylesheet.write_text(".external { color: rgba(0, 0, 255, 0.5) }")
app = rx.App(style={"font_family": "monospace"}, stylesheets=[stylesheet.name])
app = rx.App(style={"font_family": "monospace"})
app.add_page(index)
if tailwind_disabled:
config = rx.config.get_config()
@ -78,7 +72,7 @@ def tailwind_app(tmp_path, tailwind_disabled) -> Generator[AppHarness, None, Non
"""
with AppHarness.create(
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",
) as harness:
yield harness
@ -114,9 +108,3 @@ def test_tailwind_app(tailwind_app: AppHarness, tailwind_disabled: bool):
else:
# expect "text-red-500" from tailwind utility class
assert p.value_of_css_property("color") in TEXT_RED_500_COLOR
# Assert external stylesheet is applying rules
external = driver.find_elements(By.CLASS_NAME, "external")
assert len(external) == 1
for ext_div in external:
assert ext_div.value_of_css_property("color") == "rgba(0, 0, 255, 0.5)"

View File

@ -1,21 +1,15 @@
"""Integration tests for file upload."""
from __future__ import annotations
import asyncio
import time
from pathlib import Path
from typing import Generator
from urllib.parse import urlsplit
import pytest
from selenium.webdriver.common.by import By
from reflex.constants.event import Endpoint
from reflex.testing import AppHarness, WebDriver
from .utils import poll_for_navigation
def UploadFile():
"""App for testing dynamic routes."""
@ -23,14 +17,10 @@ def UploadFile():
import reflex as rx
LARGE_DATA = "DUMMY" * 1024 * 512
class UploadState(rx.State):
_file_data: Dict[str, str] = {}
event_order: rx.Field[List[str]] = rx.field([])
event_order: List[str] = []
progress_dicts: List[dict] = []
disabled: bool = False
large_data: str = ""
async def handle_upload(self, files: List[rx.UploadFile]):
for file in files:
@ -41,7 +31,6 @@ def UploadFile():
for file in files:
upload_data = await file.read()
self._file_data[file.filename or ""] = upload_data.decode("utf-8")
self.large_data = LARGE_DATA
yield UploadState.chain_event
def upload_progress(self, progress):
@ -50,26 +39,13 @@ def UploadFile():
self.progress_dicts.append(progress)
def chain_event(self):
assert self.large_data == LARGE_DATA
self.large_data = ""
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():
return rx.vstack(
rx.input(
rx.chakra.input(
value=UploadState.router.session.client_token,
read_only=True,
is_read_only=True,
id="token",
),
rx.heading("Default Upload"),
@ -78,16 +54,15 @@ def UploadFile():
rx.button("Select File"),
rx.text("Drag and drop files here or click to select files"),
),
disabled=UploadState.disabled,
),
rx.button(
"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",
),
rx.box(
rx.foreach(
rx.selected_files(),
rx.selected_files,
lambda f: rx.text(f, as_="p"),
),
id="selected_files",
@ -107,7 +82,7 @@ def UploadFile():
),
rx.button(
"Upload",
on_click=UploadState.handle_upload_secondary( # pyright: ignore [reportCallIssue]
on_click=UploadState.handle_upload_secondary( # type: ignore
rx.upload_files(
upload_id="secondary",
on_upload_progress=UploadState.upload_progress,
@ -129,7 +104,7 @@ def UploadFile():
),
rx.vstack(
rx.foreach(
UploadState.progress_dicts,
UploadState.progress_dicts, # type: ignore
lambda d: rx.text(d.to_string()),
)
),
@ -138,37 +113,9 @@ def UploadFile():
on_click=rx.cancel_upload("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)
@ -184,7 +131,7 @@ def upload_file(tmp_path_factory) -> Generator[AppHarness, None, None]:
"""
with AppHarness.create(
root=tmp_path_factory.mktemp("upload_file"),
app_source=UploadFile,
app_source=UploadFile, # type: ignore
) as harness:
yield harness
@ -207,24 +154,6 @@ def driver(upload_file: AppHarness):
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.asyncio
async def test_upload_file(
@ -239,10 +168,12 @@ async def test_upload_file(
secondary: whether to use the secondary upload form
"""
assert upload_file.app_instance is not None
token = poll_for_token(driver, upload_file)
full_state_name = upload_file.get_full_state_name(["_upload_state"])
state_name = upload_file.get_state_name("_upload_state")
substate_token = f"{token}_{full_state_name}"
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
substate_token = f"{token}_state.upload_state"
suffix = "_secondary" if secondary else ""
@ -261,31 +192,27 @@ async def test_upload_file(
upload_box.send_keys(str(target_file))
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
async def get_file_data():
return (
(await upload_file.get_state(substate_token))
.substates[state_name]
.substates["upload_state"]
._file_data
)
file_data = await AppHarness._poll_for_async(get_file_data)
assert isinstance(file_data, dict)
normalized_file_data = {Path(k).name: v for k, v in file_data.items()}
assert normalized_file_data[Path(exp_name).name] == exp_contents
assert file_data[exp_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["upload_state"].event_order.count("upload_progress") == 1
assert state.substates["upload_state"].event_order.count("chain_event") == 1
@pytest.mark.asyncio
@ -298,10 +225,12 @@ async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver):
driver: WebDriver instance.
"""
assert upload_file.app_instance is not None
token = poll_for_token(driver, upload_file)
full_state_name = upload_file.get_full_state_name(["_upload_state"])
state_name = upload_file.get_state_name("_upload_state")
substate_token = f"{token}_{full_state_name}"
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
substate_token = f"{token}_state.upload_state"
upload_box = driver.find_element(By.XPATH, "//input[@type='file']")
assert upload_box
@ -322,9 +251,7 @@ async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver):
# check that the selected files are displayed
selected_files = driver.find_element(By.ID, "selected_files")
assert [Path(name).name for name in selected_files.text.split("\n")] == [
Path(name).name for name in exp_files
]
assert selected_files.text == "\n".join(exp_files)
# do the upload
upload_button.click()
@ -333,15 +260,14 @@ async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver):
async def get_file_data():
return (
(await upload_file.get_state(substate_token))
.substates[state_name]
.substates["upload_state"]
._file_data
)
file_data = await AppHarness._poll_for_async(get_file_data)
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():
assert normalized_file_data[Path(exp_name).name] == exp_contents
assert file_data[exp_name] == exp_contents
@pytest.mark.parametrize("secondary", [False, True])
@ -357,7 +283,11 @@ def test_clear_files(
secondary: whether to use the secondary upload form.
"""
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 ""
@ -382,9 +312,7 @@ def test_clear_files(
# check that the selected files are displayed
selected_files = driver.find_element(By.ID, f"selected_files{suffix}")
assert [Path(name).name for name in selected_files.text.split("\n")] == [
Path(name).name for name in exp_files
]
assert selected_files.text == "\n".join(exp_files)
clear_button = driver.find_element(By.ID, f"clear_button{suffix}")
assert clear_button
@ -409,14 +337,16 @@ async def test_cancel_upload(tmp_path, upload_file: AppHarness, driver: WebDrive
driver: WebDriver instance.
"""
assert upload_file.app_instance is not None
token = poll_for_token(driver, upload_file)
state_name = upload_file.get_state_name("_upload_state")
state_full_name = upload_file.get_full_state_name(["_upload_state"])
substate_token = f"{token}_{state_full_name}"
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
substate_token = f"{token}_state.upload_state"
upload_box = driver.find_elements(By.XPATH, "//input[@type='file']")[1]
upload_button = driver.find_element(By.ID, "upload_button_secondary")
cancel_button = driver.find_element(By.ID, "cancel_button_secondary")
upload_button = driver.find_element(By.ID, f"upload_button_secondary")
cancel_button = driver.find_element(By.ID, f"cancel_button_secondary")
exp_name = "large.txt"
target_file = tmp_path / exp_name
@ -429,77 +359,9 @@ async def test_cancel_upload(tmp_path, upload_file: AppHarness, driver: WebDrive
await asyncio.sleep(0.3)
cancel_button.click()
# Wait a bit for the upload to get cancelled.
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
# look up the backend state and assert on progress
state = await upload_file.get_state(substate_token)
file_data = state.substates[state_name]._file_data
assert isinstance(file_data, dict)
normalized_file_data = {Path(k).name: v for k, v in file_data.items()}
assert Path(exp_name).name not in normalized_file_data
assert state.substates["upload_state"].progress_dicts
assert exp_name not in state.substates["upload_state"]._file_data
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

67
integration/test_urls.py Executable file
View File

@ -0,0 +1,67 @@
"""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

@ -1,5 +1,4 @@
"""Integration tests for var operations."""
from typing import Generator
import pytest
@ -7,47 +6,36 @@ from selenium.webdriver.common.by import By
from reflex.testing import AppHarness
# pyright: reportOptionalMemberAccess=false, reportGeneralTypeIssues=false, reportUnknownMemberType=false
def VarOperations():
"""App with var operations."""
from typing import TypedDict
from typing import Dict, List
import reflex as rx
from reflex.vars.base import LiteralVar
from reflex.vars.sequence import ArrayVar
class Object(rx.Base):
name: str = "hello"
class Person(TypedDict):
name: str
age: int
class VarOperationState(rx.State):
int_var1: rx.Field[int] = rx.field(10)
int_var2: rx.Field[int] = rx.field(5)
int_var3: rx.Field[int] = rx.field(7)
float_var1: rx.Field[float] = rx.field(10.5)
float_var2: rx.Field[float] = rx.field(5.5)
list1: rx.Field[list] = rx.field([1, 2])
list2: rx.Field[list] = rx.field([3, 4])
list3: rx.Field[list] = rx.field(["first", "second", "third"])
list4: rx.Field[list] = rx.field([Object(name="obj_1"), Object(name="obj_2")])
str_var1: rx.Field[str] = rx.field("first")
str_var2: rx.Field[str] = rx.field("second")
str_var3: rx.Field[str] = rx.field("ThIrD")
str_var4: rx.Field[str] = rx.field("a long string")
dict1: rx.Field[dict[int, int]] = rx.field({1: 2})
dict2: rx.Field[dict[int, int]] = rx.field({3: 4})
html_str: rx.Field[str] = rx.field("<div>hello</div>")
people: rx.Field[list[Person]] = rx.field(
[{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]
)
int_var1: int = 10
int_var2: int = 5
int_var3: int = 7
float_var1: float = 10.5
float_var2: float = 5.5
list1: List = [1, 2]
list2: List = [3, 4]
list3: List = ["first", "second", "third"]
str_var1: str = "first"
str_var2: str = "second"
str_var3: str = "ThIrD"
str_var4: str = "a long string"
dict1: Dict = {1: 2}
dict2: Dict = {3: 4}
html_str: str = "<div>hello</div>"
app = rx.App(_state=rx.State)
app = rx.App(state=rx.State)
@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)
@rx.memo
@ -383,8 +371,7 @@ def VarOperations():
id="str_contains",
),
rx.text(
VarOperationState.str_var1 | VarOperationState.str_var1,
id="str_or_str",
VarOperationState.str_var1 | VarOperationState.str_var1, id="str_or_str"
),
rx.text(
VarOperationState.str_var1 & VarOperationState.str_var2,
@ -400,8 +387,7 @@ def VarOperations():
id="str_and_int",
),
rx.text(
VarOperationState.str_var1 | VarOperationState.int_var2,
id="str_or_int",
VarOperationState.str_var1 | VarOperationState.int_var2, id="str_or_int"
),
rx.text(
(VarOperationState.str_var1 == VarOperationState.int_var1).to_string(),
@ -413,8 +399,7 @@ def VarOperations():
),
# STR, LIST
rx.text(
VarOperationState.str_var1 | VarOperationState.list1,
id="str_or_list",
VarOperationState.str_var1 | VarOperationState.list1, id="str_or_list"
),
rx.text(
(VarOperationState.str_var1 & VarOperationState.list1).to_string(),
@ -430,8 +415,7 @@ def VarOperations():
),
# STR, DICT
rx.text(
VarOperationState.str_var1 | VarOperationState.dict1,
id="str_or_dict",
VarOperationState.str_var1 | VarOperationState.dict1, id="str_or_dict"
),
rx.text(
(VarOperationState.str_var1 & VarOperationState.dict1).to_string(),
@ -483,10 +467,8 @@ def VarOperations():
id="list_neq_list",
),
rx.text(
VarOperationState.list1.contains(1).to_string(),
id="list_contains",
VarOperationState.list1.contains(1).to_string(), id="list_contains"
),
rx.text(VarOperationState.list4.pluck("name").to_string(), id="list_pluck"),
rx.text(VarOperationState.list1.reverse().to_string(), id="list_reverse"),
# LIST, INT
rx.text(
@ -544,8 +526,7 @@ def VarOperations():
id="dict_neq_dict",
),
rx.text(
VarOperationState.dict1.contains(1).to_string(),
id="dict_contains",
VarOperationState.dict1.contains(1).to_string(), id="dict_contains"
),
rx.text(VarOperationState.str_var3.lower(), id="str_lower"),
rx.text(VarOperationState.str_var3.upper(), id="str_upper"),
@ -561,30 +542,33 @@ def VarOperations():
VarOperationState.html_str,
id="html_str",
),
rx.el.mark("second"),
rx.text(ArrayVar.range(2, 5).join(","), id="list_join_range1"),
rx.text(ArrayVar.range(2, 10, 2).join(","), id="list_join_range2"),
rx.text(ArrayVar.range(5, 0, -1).join(","), id="list_join_range3"),
rx.text(ArrayVar.range(0, 3).join(","), id="list_join_range4"),
rx.chakra.highlight(
"second",
query=[VarOperationState.str_var2],
),
rx.text(rx.Var.range(2, 5).join(","), id="list_join_range1"),
rx.text(rx.Var.range(2, 10, 2).join(","), id="list_join_range2"),
rx.text(rx.Var.range(5, 0, -1).join(","), id="list_join_range3"),
rx.text(rx.Var.range(0, 3).join(","), id="list_join_range4"),
rx.box(
rx.foreach(
ArrayVar.range(0, 2),
rx.Var.range(0, 2),
lambda x: rx.text(VarOperationState.list1[x], as_="p"),
),
id="foreach_list_arg",
),
rx.box(
rx.foreach(
ArrayVar.range(0, 2),
rx.Var.range(0, 2),
lambda x, ix: rx.text(VarOperationState.list1[ix], as_="p"),
),
id="foreach_list_ix",
),
rx.box(
rx.foreach(
LiteralVar.create(list(range(0, 3))).to(ArrayVar, list[int]),
rx.Var.create_safe(list(range(0, 3))).to(List[int]),
lambda x: rx.foreach(
ArrayVar.range(x),
rx.Var.range(x),
lambda y: rx.text(VarOperationState.list1[y], as_="p"),
),
),
@ -599,52 +583,6 @@ def VarOperations():
int_var2=VarOperationState.int_var2,
id="memo_comp_nested",
),
# foreach in a match
rx.box(
rx.match(
VarOperationState.list3.length(),
(0, rx.text("No choices")),
(1, rx.text("One choice")),
rx.foreach(VarOperationState.list3, lambda choice: rx.text(choice)),
),
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 +598,7 @@ def var_operations(tmp_path_factory) -> Generator[AppHarness, None, None]:
"""
with AppHarness.create(
root=tmp_path_factory.mktemp("var_operations"),
app_source=VarOperations,
app_source=VarOperations, # type: ignore
) as harness:
assert harness.app_instance is not None, "app is not running"
yield harness
@ -806,7 +744,6 @@ def test_var_operations(driver, var_operations: AppHarness):
("list_and_list", "[3,4]"),
("list_or_list", "[1,2]"),
("list_contains", "true"),
("list_pluck", '["obj_1","obj_2"]'),
("list_reverse", "[2,1]"),
("list_join", "firstsecondthird"),
("list_join_comma", "first,second,third"),
@ -842,23 +779,9 @@ def test_var_operations(driver, var_operations: AppHarness):
# rx.memo component with state
("memo_comp", "1210"),
("memo_comp_nested", "345"),
# foreach in a match
("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:
print(tag)
assert driver.find_element(By.ID, tag).text == expected
# Highlight component with var query (does not plumb ID)

View File

@ -1,5 +1,4 @@
"""Helper utilities for integration tests."""
from __future__ import annotations
from contextlib import contextmanager

3308
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,74 +1,91 @@
[tool.poetry]
name = "reflex"
version = "0.7.2dev1"
version = "0.5.1"
description = "Web apps in pure Python."
license = "Apache-2.0"
authors = [
"Nikhil Rao <nikhil@reflex.dev>",
"Alek Petuskey <alek@reflex.dev>",
"Masen Furer <masen@reflex.dev>",
"Elijah Ahianyo <elijah@reflex.dev>",
"Thomas Brandého <thomas@reflex.dev>",
"Nikhil Rao <nikhil@reflex.dev>",
"Alek Petuskey <alek@reflex.dev>",
"Masen Furer <masen@reflex.dev>",
"Elijah Ahianyo <elijah@reflex.dev>",
"Thomas Brandého <thomas@reflex.dev>",
]
readme = "README.md"
homepage = "https://reflex.dev"
repository = "https://github.com/reflex-dev/reflex"
documentation = "https://reflex.dev/docs/getting-started/introduction"
keywords = ["web", "framework"]
classifiers = ["Development Status :: 4 - Beta"]
keywords = [
"web",
"framework",
]
classifiers = [
"Development Status :: 4 - Beta",
]
packages = [
{include = "reflex"}
]
[tool.poetry.dependencies]
python = ">=3.10, <4.0"
fastapi = ">=0.96.0,!=0.111.0,!=0.111.1"
gunicorn = ">=20.1.0,<24.0"
python = "^3.8"
dill = ">=0.3.8,<0.4"
fastapi = ">=0.96.0,<1.0"
gunicorn = ">=20.1.0,<23.0"
jinja2 = ">=3.1.2,<4.0"
psutil = ">=5.9.4,<7.0"
pydantic = ">=1.10.21,<3.0"
psutil = ">=5.9.4,<6.0"
pydantic = ">=1.10.2,<3.0"
python-multipart = ">=0.0.5,<0.1"
python-socketio = ">=5.7.0,<6.0"
redis = ">=4.3.5,<6.0"
rich = ">=13.0.0,<14.0"
sqlmodel = ">=0.0.14,<0.1"
typer = ">=0.15.1,<1.0"
uvicorn = ">=0.20.0"
typer = ">=0.4.2,<1.0"
uvicorn = [
{version = "^0.24.0", python = ">=3.12"},
{version = "^0.20.0", python = "<3.12"},
]
watchdog = ">=2.3.1,<5.0"
watchfiles = ">=0.19.0,<1.0"
starlette-admin = ">=0.11.0,<1.0"
alembic = ">=1.11.1,<2.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"
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"
reflex-hosting-cli = ">=0.1.29"
reflex-hosting-cli = ">=0.1.2,<2.0"
charset-normalizer = ">=3.3.2,<4.0"
wheel = ">=0.42.0,<1.0"
build = ">=1.0.3,<2.0"
setuptools = ">=75.0"
setuptools = ">=69.1.1,<70.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"
lazy_loader = ">=0.4"
typing_extensions = ">=4.6.0"
[tool.poetry.group.dev.dependencies]
pytest = ">=7.1.2,<9.0"
pytest = ">=7.1.2,<8.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"
dill = ">=0.3.8"
toml = ">=0.10.2,<1.0"
pytest-asyncio = ">=0.24.0"
pytest-cov = ">=4.0.0,<7.0"
ruff = "0.9.6"
pandas = ">=2.1.1,<3.0"
pillow = ">=10.0.0,<12.0"
pytest-asyncio = ">=0.20.1,<0.22.0" # https://github.com/pytest-dev/pytest-asyncio/issues/706
pytest-cov = ">=4.0.0,<5.0"
black = "^22.10.0,<23.0"
ruff = "0.1.0"
pandas = [
{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"
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"
pytest-benchmark = ">=4.0.0,<6.0"
playwright = ">=1.46.0"
pytest-playwright = ">=0.5.1"
pytest-codspeed = "^3.1.2"
pytest-benchmark = ">=4.0.0,<5.0"
[tool.poetry.scripts]
reflex = "reflex.reflex:cli"
@ -78,60 +95,15 @@ requires = ["poetry-core>=1.5.1"]
build-backend = "poetry.core.masonry.api"
[tool.pyright]
reportIncompatibleMethodOverride = false
[tool.ruff]
target-version = "py310"
output-format = "concise"
lint.isort.split-on-trailing-comma = false
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"
exclude = ["*.pyi"]
target-version = "py37"
lint.select = ["B", "D", "E", "F", "I", "SIM", "W"]
lint.ignore = ["B008", "D203", "D205", "D213", "D401", "D406", "D407", "E501", "F403", "F405", "F541"]
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["F401"]
"tests/*.py" = ["ANN001", "D100", "D103", "D104", "B018", "PERF", "T", "N"]
"benchmarks/*.py" = ["ANN001", "D100", "D103", "D104", "B018", "PERF", "T", "N"]
"tests/*.py" = ["D100", "D103", "D104", "B018"]
"reflex/.templates/*.py" = ["D100", "D103", "D104"]
"*.pyi" = ["D301", "D415", "D417", "D418", "E742", "N", "PGH"]
"pyi_generator.py" = ["N802"]
"reflex/constants/*.py" = ["N"]
"*.pyi" = ["ALL"]
"*/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"

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