Compare commits
4 Commits
main
...
masenf/pyr
Author | SHA1 | Date | |
---|---|---|---|
![]() |
19e10038b6 | ||
![]() |
4969df2dc2 | ||
![]() |
4fcfa1103b | ||
![]() |
29d2266cf7 |
@ -11,7 +11,7 @@ omit =
|
||||
[report]
|
||||
show_missing = true
|
||||
# TODO bump back to 79
|
||||
fail_under = 70
|
||||
fail_under = 60
|
||||
precision = 2
|
||||
|
||||
# Regexes for lines to exclude from consideration
|
||||
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -1 +0,0 @@
|
||||
@reflex-dev/reflex-team
|
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -2,6 +2,7 @@
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
19
.github/ISSUE_TEMPLATE/enhancement_request.md
vendored
19
.github/ISSUE_TEMPLATE/enhancement_request.md
vendored
@ -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.
|
18
.github/ISSUE_TEMPLATE/feature_request.md
vendored
18
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -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.
|
4
.github/actions/setup_build_env/action.yml
vendored
4
.github/actions/setup_build_env/action.yml
vendored
@ -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
|
||||
|
2
.github/codeql-config.yml
vendored
2
.github/codeql-config.yml
vendored
@ -1,2 +0,0 @@
|
||||
paths-ignore:
|
||||
- "**/tests/**"
|
92
.github/workflows/benchmarks.yml
vendored
92
.github/workflows/benchmarks.yml
vendored
@ -5,7 +5,7 @@ on:
|
||||
types:
|
||||
- closed
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- '**/*.md'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@ -15,21 +15,21 @@ defaults:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
PYTHONIOENCODING: "utf8"
|
||||
PYTHONIOENCODING: 'utf8'
|
||||
TELEMETRY_ENABLED: false
|
||||
NODE_OPTIONS: "--max_old_space_size=8192"
|
||||
NODE_OPTIONS: '--max_old_space_size=8192'
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
|
||||
jobs:
|
||||
reflex-web:
|
||||
# if: github.event.pull_request.merged == true
|
||||
# if: github.event.pull_request.merged == true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Show OS combos first in GUI
|
||||
os: [ubuntu-latest]
|
||||
python-version: ["3.12.8"]
|
||||
node-version: ["18.x"]
|
||||
python-version: ['3.11.4']
|
||||
node-version: ['18.x']
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@ -70,8 +70,66 @@ jobs:
|
||||
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: # This app tests the compile times of various compoonents and pages
|
||||
if: github.event.pull_request.merged == true
|
||||
env:
|
||||
OUTPUT_FILE: benchmarks.json
|
||||
timeout-minutes: 50
|
||||
strategy:
|
||||
# Prioritize getting more information out of the workflow (even if something fails)
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Show OS combos first in GUI
|
||||
os: [ubuntu-latest, windows-latest, macos-12]
|
||||
python-version: ['3.8.18', '3.9.18', '3.10.13', '3.11.5', '3.12.0']
|
||||
exclude:
|
||||
- os: windows-latest
|
||||
python-version: '3.10.13'
|
||||
- os: windows-latest
|
||||
python-version: '3.9.18'
|
||||
- os: windows-latest
|
||||
python-version: '3.8.18'
|
||||
# keep only one python version for MacOS
|
||||
- os: macos-latest
|
||||
python-version: '3.8.18'
|
||||
- os: macos-latest
|
||||
python-version: '3.9.18'
|
||||
- os: macos-latest
|
||||
python-version: '3.10.13'
|
||||
- os: macos-12
|
||||
python-version: '3.12.0'
|
||||
include:
|
||||
- os: windows-latest
|
||||
python-version: '3.10.11'
|
||||
- os: windows-latest
|
||||
python-version: '3.9.13'
|
||||
- os: windows-latest
|
||||
python-version: '3.8.10'
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup_build_env
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
run-poetry-install: true
|
||||
create-venv-at-path: .venv
|
||||
- name: Run benchmark tests
|
||||
env:
|
||||
APP_HARNESS_HEADLESS: 1
|
||||
PYTHONUNBUFFERED: 1
|
||||
run: |
|
||||
poetry run pytest -v benchmarks/ --benchmark-json=${{ env.OUTPUT_FILE }} -s
|
||||
- name: Upload benchmark results
|
||||
# Only run if the database creds are available in this context.
|
||||
run:
|
||||
poetry run python benchmarks/benchmark_compile_times.py --os "${{ matrix.os }}"
|
||||
--python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
|
||||
--benchmark-json "${{ env.OUTPUT_FILE }}" --branch-name "${{ github.head_ref || github.ref_name }}"
|
||||
--event-type "${{ github.event_name }}" --pr-id "${{ github.event.pull_request.id }}"
|
||||
|
||||
reflex-dist-size: # This job is used to calculate the size of the Reflex distribution (wheel file)
|
||||
if: github.event.pull_request.merged == true
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
# Prioritize getting more information out of the workflow (even if something fails)
|
||||
@ -81,7 +139,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
|
||||
- name: Build reflex
|
||||
@ -91,29 +149,25 @@ jobs:
|
||||
# Only run if the database creds are available in this context.
|
||||
run:
|
||||
poetry run python benchmarks/benchmark_package_size.py --os ubuntu-latest
|
||||
--python-version 3.12.8 --commit-sha "${{ github.sha }}" --pr-id "${{ github.event.pull_request.id }}"
|
||||
--python-version 3.11.5 --commit-sha "${{ github.sha }}" --pr-id "${{ github.event.pull_request.id }}"
|
||||
--branch-name "${{ github.head_ref || github.ref_name }}"
|
||||
--path ./dist
|
||||
|
||||
reflex-venv-size: # This job calculates the total size of Reflex and its dependencies
|
||||
if: github.event.pull_request.merged == true
|
||||
if: github.event.pull_request.merged == true
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
# Prioritize getting more information out of the workflow (even if something fails)
|
||||
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:
|
||||
@ -138,6 +192,6 @@ jobs:
|
||||
run:
|
||||
poetry run python benchmarks/benchmark_package_size.py --os "${{ matrix.os }}"
|
||||
--python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
|
||||
--pr-id "${{ github.event.pull_request.id }}"
|
||||
--pr-id "${{ github.event.pull_request.id }}"
|
||||
--branch-name "${{ github.head_ref || github.ref_name }}"
|
||||
--path ./.venv
|
||||
--path ./.venv
|
10
.github/workflows/check_generated_pyi.yml
vendored
10
.github/workflows/check_generated_pyi.yml
vendored
@ -6,16 +6,16 @@ concurrency:
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches: ['main']
|
||||
# We don't just trigger on make_pyi.py and the components dir, because
|
||||
# there are other things that can change the generator output
|
||||
# e.g. black version, reflex.Component, reflex.Var.
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- '**/*.md'
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
branches: ['main']
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- '**/*.md'
|
||||
|
||||
jobs:
|
||||
check-generated-pyi-components:
|
||||
@ -25,7 +25,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/setup_build_env
|
||||
with:
|
||||
python-version: "3.12.8"
|
||||
python-version: '3.11.5'
|
||||
run-poetry-install: true
|
||||
create-venv-at-path: .venv
|
||||
- run: |
|
||||
|
40
.github/workflows/check_node_latest.yml
vendored
40
.github/workflows/check_node_latest.yml
vendored
@ -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}}
|
@ -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
|
103
.github/workflows/codeql.yml
vendored
103
.github/workflows/codeql.yml
vendored
@ -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}}"
|
28
.github/workflows/integration_app_harness.yml
vendored
28
.github/workflows/integration_app_harness.yml
vendored
@ -6,13 +6,13 @@ concurrency:
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches: ['main']
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- '**/*.md'
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
branches: ['main']
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- '**/*.md'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@ -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
|
||||
|
151
.github/workflows/integration_tests.yml
vendored
151
.github/workflows/integration_tests.yml
vendored
@ -2,13 +2,13 @@ name: integration-tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches: ['main']
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- '**/*.md'
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
branches: ['main']
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- '**/*.md'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.id }}
|
||||
@ -27,13 +27,13 @@ env:
|
||||
# TODO: can we fix windows encoding natively within reflex? Bug above can hit real users too (less common, but possible)
|
||||
# - Catch encoding errors when printing logs
|
||||
# - Best effort print lines that contain illegal chars (map to some default char, etc.)
|
||||
PYTHONIOENCODING: "utf8"
|
||||
PYTHONIOENCODING: 'utf8'
|
||||
TELEMETRY_ENABLED: false
|
||||
NODE_OPTIONS: "--max_old_space_size=8192"
|
||||
NODE_OPTIONS: '--max_old_space_size=8192'
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
|
||||
jobs:
|
||||
example-counter-and-nba-proxy:
|
||||
example-counter:
|
||||
env:
|
||||
OUTPUT_FILE: import_benchmark.json
|
||||
timeout-minutes: 30
|
||||
@ -42,18 +42,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 +77,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 +98,27 @@ 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
|
||||
run:
|
||||
poetry run python benchmarks/benchmark_web_size.py --os "${{ matrix.os }}"
|
||||
--python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
|
||||
--pr-id "${{ github.event.pull_request.id }}"
|
||||
--branch-name "${{ github.head_ref || github.ref_name }}"
|
||||
--path ./reflex-examples/counter/.web
|
||||
--app-name "counter"
|
||||
- name: Install hyperfine
|
||||
run: cargo install hyperfine
|
||||
- name: Benchmark imports
|
||||
working-directory: ./reflex-examples/counter
|
||||
run: hyperfine --warmup 3 "export POETRY_VIRTUALENVS_PATH=../../.venv; poetry run python counter/counter.py" --show-output --export-json "${{ env.OUTPUT_FILE }}" --shell bash
|
||||
- name: Upload Benchmarks
|
||||
run:
|
||||
poetry run python benchmarks/benchmark_imports.py --os "${{ matrix.os }}"
|
||||
--python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
|
||||
--benchmark-json "./reflex-examples/counter/${{ env.OUTPUT_FILE }}"
|
||||
--branch-name "${{ github.head_ref || github.ref_name }}" --pr-id "${{ github.event.pull_request.id }}"
|
||||
--app-name "counter"
|
||||
|
||||
|
||||
|
||||
reflex-web:
|
||||
@ -120,11 +126,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 +149,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 +160,9 @@ jobs:
|
||||
# Check that npm is home
|
||||
npm -v
|
||||
poetry run bash scripts/integration.sh ./reflex-web prod
|
||||
- name: Measure and upload .web size
|
||||
run:
|
||||
poetry run python benchmarks/benchmark_web_size.py --os "${{ matrix.os }}"
|
||||
--python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}"
|
||||
--pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}"
|
||||
--app-name "reflex-web" --path ./reflex-web/.web
|
2
.github/workflows/integration_tests_wsl.yml
vendored
2
.github/workflows/integration_tests_wsl.yml
vendored
@ -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}
|
||||
|
34
.github/workflows/performance.yml
vendored
34
.github/workflows/performance.yml
vendored
@ -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
|
6
.github/workflows/pre-commit.yml
vendored
6
.github/workflows/pre-commit.yml
vendored
@ -6,12 +6,12 @@ concurrency:
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
branches: ['main']
|
||||
push:
|
||||
# Note even though this job is called "pre-commit" and runs "pre-commit", this job will run
|
||||
# also POST-commit on main also! In case there are mishandled merge conflicts / bad auto-resolves
|
||||
# when merging into main branch.
|
||||
branches: ["main"]
|
||||
branches: ['main']
|
||||
|
||||
jobs:
|
||||
pre-commit:
|
||||
@ -23,7 +23,7 @@ jobs:
|
||||
with:
|
||||
# running vs. one version of Python is OK
|
||||
# i.e. ruff, black, etc.
|
||||
python-version: 3.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)
|
||||
|
@ -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
|
||||
|
60
.github/workflows/unit_tests.yml
vendored
60
.github/workflows/unit_tests.yml
vendored
@ -6,13 +6,13 @@ concurrency:
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
branches: ['main']
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- '**/*.md'
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
branches: ['main']
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- '**/*.md'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@ -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
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -3,8 +3,6 @@
|
||||
assets/external/*
|
||||
dist/*
|
||||
examples/
|
||||
.web
|
||||
.states
|
||||
.idea
|
||||
.vscode
|
||||
.coverage
|
||||
@ -15,4 +13,3 @@ requirements.txt
|
||||
.pyi_generator_last_run
|
||||
.pyi_generator_diff
|
||||
reflex.db
|
||||
.codspeed
|
@ -3,20 +3,14 @@ fail_fast: true
|
||||
repos:
|
||||
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.9.6
|
||||
rev: v0.4.10
|
||||
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:
|
||||
@ -24,15 +18,14 @@ repos:
|
||||
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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
@ -87,7 +87,7 @@ poetry run ruff format .
|
||||
```
|
||||
|
||||
Consider installing git pre-commit hooks so Ruff, Pyright, Darglint and `make_pyi` will run automatically before each commit.
|
||||
Note that pre-commit will only be installed when you use a Python version >= 3.10.
|
||||
Note that pre-commit will only be installed when you use a Python version >= 3.8.
|
||||
|
||||
``` bash
|
||||
pre-commit install
|
||||
|
@ -10,6 +10,7 @@
|
||||
|
||||
### **✨ Performant, customizable web apps in pure Python. Deploy in seconds. ✨**
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
@ -17,7 +18,7 @@
|
||||
|
||||
---
|
||||
|
||||
[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md) | [Tiếng Việt](https://github.com/reflex-dev/reflex/blob/main/docs/vi/README.md)
|
||||
[English](https://github.com/reflex-dev/reflex/blob/main/README.md) | [简体中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_cn/README.md) | [繁體中文](https://github.com/reflex-dev/reflex/blob/main/docs/zh/zh_tw/README.md) | [Türkçe](https://github.com/reflex-dev/reflex/blob/main/docs/tr/README.md) | [हिंदी](https://github.com/reflex-dev/reflex/blob/main/docs/in/README.md) | [Português (Brasil)](https://github.com/reflex-dev/reflex/blob/main/docs/pt/pt_br/README.md) | [Italiano](https://github.com/reflex-dev/reflex/blob/main/docs/it/README.md) | [Español](https://github.com/reflex-dev/reflex/blob/main/docs/es/README.md) | [한국어](https://github.com/reflex-dev/reflex/blob/main/docs/kr/README.md) | [日本語](https://github.com/reflex-dev/reflex/blob/main/docs/ja/README.md) | [Deutsch](https://github.com/reflex-dev/reflex/blob/main/docs/de/README.md) | [Persian (پارسی)](https://github.com/reflex-dev/reflex/blob/main/docs/pe/README.md)
|
||||
|
||||
---
|
||||
|
||||
@ -34,7 +35,7 @@ See our [architecture page](https://reflex.dev/blog/2024-03-21-reflex-architectu
|
||||
|
||||
## ⚙️ Installation
|
||||
|
||||
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) | 🗞️ [Blog](https://reflex.dev/blog) | 📱 [Component Library](https://reflex.dev/docs/library) | 🖼️ [Templates](https://reflex.dev/templates/) | 🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)
|
||||
📑 [Docs](https://reflex.dev/docs/getting-started/introduction) | 🗞️ [Blog](https://reflex.dev/blog) | 📱 [Component Library](https://reflex.dev/docs/library) | 🖼️ [Gallery](https://reflex.dev/docs/gallery) | 🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)
|
||||
|
||||
</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:
|
||||
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from utils import send_data_to_posthog
|
||||
|
||||
@ -19,7 +18,7 @@ def extract_stats_from_json(json_file: str) -> list[dict]:
|
||||
Returns:
|
||||
list[dict]: The stats for each test.
|
||||
"""
|
||||
with Path(json_file).open() as file:
|
||||
with open(json_file, "r") as file:
|
||||
json_data = json.load(file)
|
||||
|
||||
# Load the JSON data if it is a string, otherwise assume it's already a dictionary
|
||||
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from utils import send_data_to_posthog
|
||||
|
||||
@ -19,7 +18,7 @@ def extract_stats_from_json(json_file: str) -> dict:
|
||||
Returns:
|
||||
dict: The stats for each test.
|
||||
"""
|
||||
with Path(json_file).open() as file:
|
||||
with open(json_file, "r") as file:
|
||||
json_data = json.load(file)
|
||||
|
||||
# Load the JSON data if it is a string, otherwise assume it's already a dictionary
|
||||
|
@ -3,8 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from utils import send_data_to_posthog
|
||||
|
||||
@ -28,7 +28,7 @@ def insert_benchmarking_data(
|
||||
send_data_to_posthog("lighthouse_benchmark", properties)
|
||||
|
||||
|
||||
def get_lighthouse_scores(directory_path: str | Path) -> dict:
|
||||
def get_lighthouse_scores(directory_path: str) -> dict:
|
||||
"""Extracts the Lighthouse scores from the JSON files in the specified directory.
|
||||
|
||||
Args:
|
||||
@ -38,20 +38,24 @@ def get_lighthouse_scores(directory_path: str | Path) -> dict:
|
||||
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"],
|
||||
}
|
||||
for filename in os.listdir(directory_path):
|
||||
if filename.endswith(".json") and filename != "manifest.json":
|
||||
file_path = os.path.join(directory_path, filename)
|
||||
with open(file_path, "r") as file:
|
||||
data = json.load(file)
|
||||
# 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}
|
||||
|
||||
|
@ -2,12 +2,11 @@
|
||||
|
||||
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):
|
||||
def get_package_size(venv_path, os_name):
|
||||
"""Get the size of a specified package.
|
||||
|
||||
Args:
|
||||
@ -27,12 +26,14 @@ def get_package_size(venv_path: Path, os_name):
|
||||
|
||||
is_windows = "windows" in os_name
|
||||
|
||||
package_dir: Path = (
|
||||
venv_path / "lib" / f"python{python_version}" / "site-packages"
|
||||
full_path = (
|
||||
["lib", f"python{python_version}", "site-packages"]
|
||||
if not is_windows
|
||||
else venv_path / "Lib" / "site-packages"
|
||||
else ["Lib", "site-packages"]
|
||||
)
|
||||
if not package_dir.exists():
|
||||
|
||||
package_dir = os.path.join(venv_path, *full_path)
|
||||
if not os.path.exists(package_dir):
|
||||
raise ValueError(
|
||||
"Error: Virtual environment does not exist or is not activated."
|
||||
)
|
||||
@ -62,9 +63,9 @@ def insert_benchmarking_data(
|
||||
path: The path to the dir or file to check size.
|
||||
"""
|
||||
if "./dist" in path:
|
||||
size = get_directory_size(Path(path))
|
||||
size = get_directory_size(path)
|
||||
else:
|
||||
size = get_package_size(Path(path), os_type_version)
|
||||
size = get_package_size(path, os_type_version)
|
||||
|
||||
# Prepare the event data
|
||||
properties = {
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from utils import get_directory_size, send_data_to_posthog
|
||||
|
||||
@ -29,7 +28,7 @@ def insert_benchmarking_data(
|
||||
pr_id: The id of the PR.
|
||||
path: The path to the dir or file to check size.
|
||||
"""
|
||||
size = get_directory_size(Path(path))
|
||||
size = get_directory_size(path)
|
||||
|
||||
# Prepare the event data
|
||||
properties = {
|
||||
|
376
benchmarks/test_benchmark_compile_components.py
Normal file
376
benchmarks/test_benchmark_compile_components.py
Normal file
@ -0,0 +1,376 @@
|
||||
"""Benchmark tests for apps with varying component numbers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import time
|
||||
from typing import Generator
|
||||
|
||||
import pytest
|
||||
|
||||
from benchmarks import WINDOWS_SKIP_REASON
|
||||
from reflex import constants
|
||||
from reflex.compiler import utils
|
||||
from reflex.testing import AppHarness, chdir
|
||||
from reflex.utils import build
|
||||
from reflex.utils.prerequisites import get_web_dir
|
||||
|
||||
web_pages = get_web_dir() / constants.Dirs.PAGES
|
||||
|
||||
|
||||
def render_component(num: int):
|
||||
"""Generate a number of components based on num.
|
||||
|
||||
Args:
|
||||
num: number of components to produce.
|
||||
|
||||
Returns:
|
||||
The rendered number of components.
|
||||
"""
|
||||
import reflex as rx
|
||||
|
||||
return [
|
||||
rx.fragment(
|
||||
rx.box(
|
||||
rx.accordion.root(
|
||||
rx.accordion.item(
|
||||
header="Full Ingredients", # type: ignore
|
||||
content="Yes. It's built with accessibility in mind.", # type: ignore
|
||||
font_size="3em",
|
||||
),
|
||||
rx.accordion.item(
|
||||
header="Applications", # type: ignore
|
||||
content="Yes. It's unstyled by default, giving you freedom over the look and feel.", # type: ignore
|
||||
),
|
||||
collapsible=True,
|
||||
variant="ghost",
|
||||
width="25rem",
|
||||
),
|
||||
padding_top="20px",
|
||||
),
|
||||
rx.box(
|
||||
rx.drawer.root(
|
||||
rx.drawer.trigger(
|
||||
rx.button("Open Drawer with snap points"), as_child=True
|
||||
),
|
||||
rx.drawer.overlay(),
|
||||
rx.drawer.portal(
|
||||
rx.drawer.content(
|
||||
rx.flex(
|
||||
rx.drawer.title("Drawer Content"),
|
||||
rx.drawer.description("Drawer description"),
|
||||
rx.drawer.close(
|
||||
rx.button("Close Button"),
|
||||
as_child=True,
|
||||
),
|
||||
direction="column",
|
||||
margin="5em",
|
||||
align_items="center",
|
||||
),
|
||||
top="auto",
|
||||
height="100%",
|
||||
flex_direction="column",
|
||||
background_color="var(--green-3)",
|
||||
),
|
||||
),
|
||||
snap_points=["148px", "355px", 1],
|
||||
),
|
||||
),
|
||||
rx.box(
|
||||
rx.callout(
|
||||
"You will need admin privileges to install and access this application.",
|
||||
icon="info",
|
||||
size="3",
|
||||
),
|
||||
),
|
||||
rx.box(
|
||||
rx.table.root(
|
||||
rx.table.header(
|
||||
rx.table.row(
|
||||
rx.table.column_header_cell("Full name"),
|
||||
rx.table.column_header_cell("Email"),
|
||||
rx.table.column_header_cell("Group"),
|
||||
),
|
||||
),
|
||||
rx.table.body(
|
||||
rx.table.row(
|
||||
rx.table.row_header_cell("Danilo Sousa"),
|
||||
rx.table.cell("danilo@example.com"),
|
||||
rx.table.cell("Developer"),
|
||||
),
|
||||
rx.table.row(
|
||||
rx.table.row_header_cell("Zahra Ambessa"),
|
||||
rx.table.cell("zahra@example.com"),
|
||||
rx.table.cell("Admin"),
|
||||
),
|
||||
rx.table.row(
|
||||
rx.table.row_header_cell("Jasper Eriksson"),
|
||||
rx.table.cell("jasper@example.com"),
|
||||
rx.table.cell("Developer"),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
] * num
|
||||
|
||||
|
||||
def AppWithTenComponentsOnePage():
|
||||
"""A reflex app with roughly 10 components on one page."""
|
||||
import reflex as rx
|
||||
|
||||
def index() -> rx.Component:
|
||||
return rx.center(rx.vstack(*render_component(1)))
|
||||
|
||||
app = rx.App(state=rx.State)
|
||||
app.add_page(index)
|
||||
|
||||
|
||||
def AppWithHundredComponentOnePage():
|
||||
"""A reflex app with roughly 100 components on one page."""
|
||||
import reflex as rx
|
||||
|
||||
def index() -> rx.Component:
|
||||
return rx.center(rx.vstack(*render_component(100)))
|
||||
|
||||
app = rx.App(state=rx.State)
|
||||
app.add_page(index)
|
||||
|
||||
|
||||
def AppWithThousandComponentsOnePage():
|
||||
"""A reflex app with roughly 1000 components on one page."""
|
||||
import reflex as rx
|
||||
|
||||
def index() -> rx.Component:
|
||||
return rx.center(rx.vstack(*render_component(1000)))
|
||||
|
||||
app = rx.App(state=rx.State)
|
||||
app.add_page(index)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app_with_10_components(
|
||||
tmp_path_factory,
|
||||
) -> Generator[AppHarness, None, None]:
|
||||
"""Start Blank Template app at tmp_path via AppHarness.
|
||||
|
||||
Args:
|
||||
tmp_path_factory: pytest tmp_path_factory fixture
|
||||
|
||||
Yields:
|
||||
running AppHarness instance
|
||||
"""
|
||||
root = tmp_path_factory.mktemp("app10components")
|
||||
|
||||
yield AppHarness.create(
|
||||
root=root,
|
||||
app_source=functools.partial(
|
||||
AppWithTenComponentsOnePage,
|
||||
render_component=render_component, # type: ignore
|
||||
),
|
||||
) # type: ignore
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app_with_100_components(
|
||||
tmp_path_factory,
|
||||
) -> Generator[AppHarness, None, None]:
|
||||
"""Start Blank Template app at tmp_path via AppHarness.
|
||||
|
||||
Args:
|
||||
tmp_path_factory: pytest tmp_path_factory fixture
|
||||
|
||||
Yields:
|
||||
running AppHarness instance
|
||||
"""
|
||||
root = tmp_path_factory.mktemp("app100components")
|
||||
|
||||
yield AppHarness.create(
|
||||
root=root,
|
||||
app_source=functools.partial(
|
||||
AppWithHundredComponentOnePage,
|
||||
render_component=render_component, # type: ignore
|
||||
),
|
||||
) # type: ignore
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app_with_1000_components(
|
||||
tmp_path_factory,
|
||||
) -> Generator[AppHarness, None, None]:
|
||||
"""Create an app with 1000 components at tmp_path via AppHarness.
|
||||
|
||||
Args:
|
||||
tmp_path_factory: pytest tmp_path_factory fixture
|
||||
|
||||
Yields:
|
||||
an AppHarness instance
|
||||
"""
|
||||
root = tmp_path_factory.mktemp("app1000components")
|
||||
|
||||
yield AppHarness.create(
|
||||
root=root,
|
||||
app_source=functools.partial(
|
||||
AppWithThousandComponentsOnePage,
|
||||
render_component=render_component, # type: ignore
|
||||
),
|
||||
) # type: ignore
|
||||
|
||||
|
||||
@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON)
|
||||
@pytest.mark.benchmark(
|
||||
group="Compile time of varying component numbers",
|
||||
timer=time.perf_counter,
|
||||
disable_gc=True,
|
||||
warmup=False,
|
||||
)
|
||||
def test_app_10_compile_time_cold(benchmark, app_with_10_components):
|
||||
"""Test the compile time on a cold start for an app with roughly 10 components.
|
||||
|
||||
Args:
|
||||
benchmark: The benchmark fixture.
|
||||
app_with_10_components: The app harness.
|
||||
"""
|
||||
|
||||
def setup():
|
||||
with chdir(app_with_10_components.app_path):
|
||||
utils.empty_dir(web_pages, ["_app.js"])
|
||||
app_with_10_components._initialize_app()
|
||||
build.setup_frontend(app_with_10_components.app_path)
|
||||
|
||||
def benchmark_fn():
|
||||
with chdir(app_with_10_components.app_path):
|
||||
app_with_10_components.app_instance._compile()
|
||||
|
||||
benchmark.pedantic(benchmark_fn, setup=setup, rounds=10)
|
||||
|
||||
|
||||
@pytest.mark.benchmark(
|
||||
group="Compile time of varying component numbers",
|
||||
min_rounds=5,
|
||||
timer=time.perf_counter,
|
||||
disable_gc=True,
|
||||
warmup=False,
|
||||
)
|
||||
def test_app_10_compile_time_warm(benchmark, app_with_10_components):
|
||||
"""Test the compile time on a warm start for an app with roughly 10 components.
|
||||
|
||||
Args:
|
||||
benchmark: The benchmark fixture.
|
||||
app_with_10_components: The app harness.
|
||||
"""
|
||||
with chdir(app_with_10_components.app_path):
|
||||
app_with_10_components._initialize_app()
|
||||
build.setup_frontend(app_with_10_components.app_path)
|
||||
|
||||
def benchmark_fn():
|
||||
with chdir(app_with_10_components.app_path):
|
||||
app_with_10_components.app_instance._compile()
|
||||
|
||||
benchmark(benchmark_fn)
|
||||
|
||||
|
||||
@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON)
|
||||
@pytest.mark.benchmark(
|
||||
group="Compile time of varying component numbers",
|
||||
timer=time.perf_counter,
|
||||
disable_gc=True,
|
||||
warmup=False,
|
||||
)
|
||||
def test_app_100_compile_time_cold(benchmark, app_with_100_components):
|
||||
"""Test the compile time on a cold start for an app with roughly 100 components.
|
||||
|
||||
Args:
|
||||
benchmark: The benchmark fixture.
|
||||
app_with_100_components: The app harness.
|
||||
"""
|
||||
|
||||
def setup():
|
||||
with chdir(app_with_100_components.app_path):
|
||||
utils.empty_dir(web_pages, ["_app.js"])
|
||||
app_with_100_components._initialize_app()
|
||||
build.setup_frontend(app_with_100_components.app_path)
|
||||
|
||||
def benchmark_fn():
|
||||
with chdir(app_with_100_components.app_path):
|
||||
app_with_100_components.app_instance._compile()
|
||||
|
||||
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
|
||||
|
||||
|
||||
@pytest.mark.benchmark(
|
||||
group="Compile time of varying component numbers",
|
||||
min_rounds=5,
|
||||
timer=time.perf_counter,
|
||||
disable_gc=True,
|
||||
warmup=False,
|
||||
)
|
||||
def test_app_100_compile_time_warm(benchmark, app_with_100_components):
|
||||
"""Test the compile time on a warm start for an app with roughly 100 components.
|
||||
|
||||
Args:
|
||||
benchmark: The benchmark fixture.
|
||||
app_with_100_components: The app harness.
|
||||
"""
|
||||
with chdir(app_with_100_components.app_path):
|
||||
app_with_100_components._initialize_app()
|
||||
build.setup_frontend(app_with_100_components.app_path)
|
||||
|
||||
def benchmark_fn():
|
||||
with chdir(app_with_100_components.app_path):
|
||||
app_with_100_components.app_instance._compile()
|
||||
|
||||
benchmark(benchmark_fn)
|
||||
|
||||
|
||||
@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON)
|
||||
@pytest.mark.benchmark(
|
||||
group="Compile time of varying component numbers",
|
||||
timer=time.perf_counter,
|
||||
disable_gc=True,
|
||||
warmup=False,
|
||||
)
|
||||
def test_app_1000_compile_time_cold(benchmark, app_with_1000_components):
|
||||
"""Test the compile time on a cold start for an app with roughly 1000 components.
|
||||
|
||||
Args:
|
||||
benchmark: The benchmark fixture.
|
||||
app_with_1000_components: The app harness.
|
||||
"""
|
||||
|
||||
def setup():
|
||||
with chdir(app_with_1000_components.app_path):
|
||||
utils.empty_dir(web_pages, keep_files=["_app.js"])
|
||||
app_with_1000_components._initialize_app()
|
||||
build.setup_frontend(app_with_1000_components.app_path)
|
||||
|
||||
def benchmark_fn():
|
||||
with chdir(app_with_1000_components.app_path):
|
||||
app_with_1000_components.app_instance._compile()
|
||||
|
||||
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
|
||||
|
||||
|
||||
@pytest.mark.benchmark(
|
||||
group="Compile time of varying component numbers",
|
||||
min_rounds=5,
|
||||
timer=time.perf_counter,
|
||||
disable_gc=True,
|
||||
warmup=False,
|
||||
)
|
||||
def test_app_1000_compile_time_warm(benchmark, app_with_1000_components):
|
||||
"""Test the compile time on a warm start for an app with roughly 1000 components.
|
||||
|
||||
Args:
|
||||
benchmark: The benchmark fixture.
|
||||
app_with_1000_components: The app harness.
|
||||
"""
|
||||
with chdir(app_with_1000_components.app_path):
|
||||
app_with_1000_components._initialize_app()
|
||||
build.setup_frontend(app_with_1000_components.app_path)
|
||||
|
||||
def benchmark_fn():
|
||||
with chdir(app_with_1000_components.app_path):
|
||||
app_with_1000_components.app_instance._compile()
|
||||
|
||||
benchmark(benchmark_fn)
|
580
benchmarks/test_benchmark_compile_pages.py
Normal file
580
benchmarks/test_benchmark_compile_pages.py
Normal file
@ -0,0 +1,580 @@
|
||||
"""Benchmark tests for apps with varying page numbers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import time
|
||||
from typing import Generator
|
||||
|
||||
import pytest
|
||||
|
||||
from benchmarks import WINDOWS_SKIP_REASON
|
||||
from reflex import constants
|
||||
from reflex.compiler import utils
|
||||
from reflex.testing import AppHarness, chdir
|
||||
from reflex.utils import build
|
||||
from reflex.utils.prerequisites import get_web_dir
|
||||
|
||||
web_pages = get_web_dir() / constants.Dirs.PAGES
|
||||
|
||||
|
||||
def render_multiple_pages(app, num: int):
|
||||
"""Add multiple pages based on num.
|
||||
|
||||
Args:
|
||||
app: The App object.
|
||||
num: number of pages to render.
|
||||
|
||||
"""
|
||||
from typing import Tuple
|
||||
|
||||
from rxconfig import config # type: ignore
|
||||
|
||||
import reflex as rx
|
||||
|
||||
docs_url = "https://reflex.dev/docs/getting-started/introduction/"
|
||||
filename = f"{config.app_name}/{config.app_name}.py"
|
||||
college = [
|
||||
"Stanford University",
|
||||
"Arizona",
|
||||
"Arizona state",
|
||||
"Baylor",
|
||||
"Boston College",
|
||||
"Boston University",
|
||||
]
|
||||
|
||||
class State(rx.State):
|
||||
"""The app state."""
|
||||
|
||||
position: str
|
||||
college: str
|
||||
age: Tuple[int, int] = (18, 50)
|
||||
salary: Tuple[int, int] = (0, 25000000)
|
||||
|
||||
comp1 = rx.center(
|
||||
rx.theme_panel(),
|
||||
rx.vstack(
|
||||
rx.heading("Welcome to Reflex!", size="9"),
|
||||
rx.text("Get started by editing ", rx.code(filename)),
|
||||
rx.button(
|
||||
"Check out our docs!",
|
||||
on_click=lambda: rx.redirect(docs_url),
|
||||
size="4",
|
||||
),
|
||||
align="center",
|
||||
spacing="7",
|
||||
font_size="2em",
|
||||
),
|
||||
height="100vh",
|
||||
)
|
||||
|
||||
comp2 = rx.vstack(
|
||||
rx.hstack(
|
||||
rx.vstack(
|
||||
rx.select(
|
||||
["C", "PF", "SF", "PG", "SG"],
|
||||
placeholder="Select a position. (All)",
|
||||
on_change=State.set_position, # type: ignore
|
||||
size="3",
|
||||
),
|
||||
rx.select(
|
||||
college,
|
||||
placeholder="Select a college. (All)",
|
||||
on_change=State.set_college, # type: ignore
|
||||
size="3",
|
||||
),
|
||||
),
|
||||
rx.vstack(
|
||||
rx.vstack(
|
||||
rx.hstack(
|
||||
rx.badge("Min Age: ", State.age[0]),
|
||||
rx.divider(orientation="vertical"),
|
||||
rx.badge("Max Age: ", State.age[1]),
|
||||
),
|
||||
rx.slider(
|
||||
default_value=[18, 50],
|
||||
min=18,
|
||||
max=50,
|
||||
on_value_commit=State.set_age, # type: ignore
|
||||
),
|
||||
align_items="left",
|
||||
width="100%",
|
||||
),
|
||||
rx.vstack(
|
||||
rx.hstack(
|
||||
rx.badge("Min Sal: ", State.salary[0] // 1000000, "M"),
|
||||
rx.divider(orientation="vertical"),
|
||||
rx.badge("Max Sal: ", State.salary[1] // 1000000, "M"),
|
||||
),
|
||||
rx.slider(
|
||||
default_value=[0, 25000000],
|
||||
min=0,
|
||||
max=25000000,
|
||||
on_value_commit=State.set_salary, # type: ignore
|
||||
),
|
||||
align_items="left",
|
||||
width="100%",
|
||||
),
|
||||
),
|
||||
spacing="4",
|
||||
),
|
||||
width="100%",
|
||||
)
|
||||
|
||||
for i in range(1, num + 1):
|
||||
if i % 2 == 1:
|
||||
app.add_page(comp1, route=f"page{i}")
|
||||
else:
|
||||
app.add_page(comp2, route=f"page{i}")
|
||||
|
||||
|
||||
def AppWithOnePage():
|
||||
"""A reflex app with one page."""
|
||||
import reflex_chakra as rc
|
||||
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(
|
||||
rc.input(
|
||||
id="token", value=State.router.session.client_token, is_read_only=True
|
||||
),
|
||||
rx.vstack(
|
||||
rx.heading("Welcome to Reflex!", size="9"),
|
||||
rx.text("Get started by editing ", rx.code(filename)),
|
||||
rx.button(
|
||||
"Check out our docs!",
|
||||
on_click=lambda: rx.redirect(docs_url),
|
||||
size="4",
|
||||
),
|
||||
align="center",
|
||||
spacing="7",
|
||||
font_size="2em",
|
||||
),
|
||||
height="100vh",
|
||||
)
|
||||
|
||||
app = rx.App(state=rx.State)
|
||||
app.add_page(index)
|
||||
|
||||
|
||||
def AppWithTenPages():
|
||||
"""A reflex app with 10 pages."""
|
||||
import reflex as rx
|
||||
|
||||
app = rx.App(state=rx.State)
|
||||
render_multiple_pages(app, 10)
|
||||
|
||||
|
||||
def AppWithHundredPages():
|
||||
"""A reflex app with 100 pages."""
|
||||
import reflex as rx
|
||||
|
||||
app = rx.App(state=rx.State)
|
||||
render_multiple_pages(app, 100)
|
||||
|
||||
|
||||
def AppWithThousandPages():
|
||||
"""A reflex app with Thousand pages."""
|
||||
import reflex as rx
|
||||
|
||||
app = rx.App(state=rx.State)
|
||||
render_multiple_pages(app, 1000)
|
||||
|
||||
|
||||
def AppWithTenThousandPages():
|
||||
"""A reflex app with ten thousand pages."""
|
||||
import reflex as rx
|
||||
|
||||
app = rx.App(state=rx.State)
|
||||
render_multiple_pages(app, 10000)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app_with_one_page(
|
||||
tmp_path_factory,
|
||||
) -> Generator[AppHarness, None, None]:
|
||||
"""Create an app with 10000 pages at tmp_path via AppHarness.
|
||||
|
||||
Args:
|
||||
tmp_path_factory: pytest tmp_path_factory fixture
|
||||
|
||||
Yields:
|
||||
an AppHarness instance
|
||||
"""
|
||||
root = tmp_path_factory.mktemp(f"app1")
|
||||
|
||||
yield AppHarness.create(root=root, app_source=AppWithOnePage) # type: ignore
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app_with_ten_pages(
|
||||
tmp_path_factory,
|
||||
) -> Generator[AppHarness, None, None]:
|
||||
"""Create an app with 10 pages at tmp_path via AppHarness.
|
||||
|
||||
Args:
|
||||
tmp_path_factory: pytest tmp_path_factory fixture
|
||||
|
||||
Yields:
|
||||
an AppHarness instance
|
||||
"""
|
||||
root = tmp_path_factory.mktemp(f"app10")
|
||||
yield AppHarness.create(
|
||||
root=root,
|
||||
app_source=functools.partial(
|
||||
AppWithTenPages,
|
||||
render_comp=render_multiple_pages, # type: ignore
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app_with_hundred_pages(
|
||||
tmp_path_factory,
|
||||
) -> Generator[AppHarness, None, None]:
|
||||
"""Create an app with 100 pages at tmp_path via AppHarness.
|
||||
|
||||
Args:
|
||||
tmp_path_factory: pytest tmp_path_factory fixture
|
||||
|
||||
Yields:
|
||||
an AppHarness instance
|
||||
"""
|
||||
root = tmp_path_factory.mktemp(f"app100")
|
||||
|
||||
yield AppHarness.create(
|
||||
root=root,
|
||||
app_source=functools.partial(
|
||||
AppWithHundredPages,
|
||||
render_comp=render_multiple_pages, # type: ignore
|
||||
),
|
||||
) # type: ignore
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app_with_thousand_pages(
|
||||
tmp_path_factory,
|
||||
) -> Generator[AppHarness, None, None]:
|
||||
"""Create an app with 1000 pages at tmp_path via AppHarness.
|
||||
|
||||
Args:
|
||||
tmp_path_factory: pytest tmp_path_factory fixture
|
||||
|
||||
Yields:
|
||||
an AppHarness instance
|
||||
"""
|
||||
root = tmp_path_factory.mktemp(f"app1000")
|
||||
|
||||
yield AppHarness.create(
|
||||
root=root,
|
||||
app_source=functools.partial( # type: ignore
|
||||
AppWithThousandPages,
|
||||
render_comp=render_multiple_pages, # type: ignore
|
||||
),
|
||||
) # type: ignore
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app_with_ten_thousand_pages(
|
||||
tmp_path_factory,
|
||||
) -> Generator[AppHarness, None, None]:
|
||||
"""Create an app with 10000 pages at tmp_path via AppHarness.
|
||||
|
||||
Args:
|
||||
tmp_path_factory: pytest tmp_path_factory fixture
|
||||
|
||||
Yields:
|
||||
running AppHarness instance
|
||||
"""
|
||||
root = tmp_path_factory.mktemp(f"app10000")
|
||||
|
||||
yield AppHarness.create(
|
||||
root=root,
|
||||
app_source=functools.partial(
|
||||
AppWithTenThousandPages,
|
||||
render_comp=render_multiple_pages, # type: ignore
|
||||
),
|
||||
) # type: ignore
|
||||
|
||||
|
||||
@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON)
|
||||
@pytest.mark.benchmark(
|
||||
group="Compile time of varying page numbers",
|
||||
timer=time.perf_counter,
|
||||
disable_gc=True,
|
||||
warmup=False,
|
||||
)
|
||||
def test_app_1_compile_time_cold(benchmark, app_with_one_page):
|
||||
"""Test the compile time on a cold start for an app with 1 page.
|
||||
|
||||
Args:
|
||||
benchmark: The benchmark fixture.
|
||||
app_with_one_page: The app harness.
|
||||
"""
|
||||
|
||||
def setup():
|
||||
with chdir(app_with_one_page.app_path):
|
||||
utils.empty_dir(web_pages, keep_files=["_app.js"])
|
||||
app_with_one_page._initialize_app()
|
||||
build.setup_frontend(app_with_one_page.app_path)
|
||||
|
||||
def benchmark_fn():
|
||||
with chdir(app_with_one_page.app_path):
|
||||
app_with_one_page.app_instance._compile()
|
||||
|
||||
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
|
||||
app_with_one_page._reload_state_module()
|
||||
|
||||
|
||||
@pytest.mark.benchmark(
|
||||
group="Compile time of varying page numbers",
|
||||
min_rounds=5,
|
||||
timer=time.perf_counter,
|
||||
disable_gc=True,
|
||||
warmup=False,
|
||||
)
|
||||
def test_app_1_compile_time_warm(benchmark, app_with_one_page):
|
||||
"""Test the compile time on a warm start for an app with 1 page.
|
||||
|
||||
Args:
|
||||
benchmark: The benchmark fixture.
|
||||
app_with_one_page: The app harness.
|
||||
"""
|
||||
with chdir(app_with_one_page.app_path):
|
||||
app_with_one_page._initialize_app()
|
||||
build.setup_frontend(app_with_one_page.app_path)
|
||||
|
||||
def benchmark_fn():
|
||||
with chdir(app_with_one_page.app_path):
|
||||
app_with_one_page.app_instance._compile()
|
||||
|
||||
benchmark(benchmark_fn)
|
||||
app_with_one_page._reload_state_module()
|
||||
|
||||
|
||||
@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON)
|
||||
@pytest.mark.benchmark(
|
||||
group="Compile time of varying page numbers",
|
||||
timer=time.perf_counter,
|
||||
disable_gc=True,
|
||||
warmup=False,
|
||||
)
|
||||
def test_app_10_compile_time_cold(benchmark, app_with_ten_pages):
|
||||
"""Test the compile time on a cold start for an app with 10 page.
|
||||
|
||||
Args:
|
||||
benchmark: The benchmark fixture.
|
||||
app_with_ten_pages: The app harness.
|
||||
"""
|
||||
|
||||
def setup():
|
||||
with chdir(app_with_ten_pages.app_path):
|
||||
utils.empty_dir(web_pages, keep_files=["_app.js"])
|
||||
app_with_ten_pages._initialize_app()
|
||||
build.setup_frontend(app_with_ten_pages.app_path)
|
||||
|
||||
def benchmark_fn():
|
||||
with chdir(app_with_ten_pages.app_path):
|
||||
app_with_ten_pages.app_instance._compile()
|
||||
|
||||
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
|
||||
app_with_ten_pages._reload_state_module()
|
||||
|
||||
|
||||
@pytest.mark.benchmark(
|
||||
group="Compile time of varying page numbers",
|
||||
min_rounds=5,
|
||||
timer=time.perf_counter,
|
||||
disable_gc=True,
|
||||
warmup=False,
|
||||
)
|
||||
def test_app_10_compile_time_warm(benchmark, app_with_ten_pages):
|
||||
"""Test the compile time on a warm start for an app with 10 page.
|
||||
|
||||
Args:
|
||||
benchmark: The benchmark fixture.
|
||||
app_with_ten_pages: The app harness.
|
||||
"""
|
||||
with chdir(app_with_ten_pages.app_path):
|
||||
app_with_ten_pages._initialize_app()
|
||||
build.setup_frontend(app_with_ten_pages.app_path)
|
||||
|
||||
def benchmark_fn():
|
||||
with chdir(app_with_ten_pages.app_path):
|
||||
app_with_ten_pages.app_instance._compile()
|
||||
|
||||
benchmark(benchmark_fn)
|
||||
app_with_ten_pages._reload_state_module()
|
||||
|
||||
|
||||
@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON)
|
||||
@pytest.mark.benchmark(
|
||||
group="Compile time of varying page numbers",
|
||||
timer=time.perf_counter,
|
||||
disable_gc=True,
|
||||
warmup=False,
|
||||
)
|
||||
def test_app_100_compile_time_cold(benchmark, app_with_hundred_pages):
|
||||
"""Test the compile time on a cold start for an app with 100 page.
|
||||
|
||||
Args:
|
||||
benchmark: The benchmark fixture.
|
||||
app_with_hundred_pages: The app harness.
|
||||
"""
|
||||
|
||||
def setup():
|
||||
with chdir(app_with_hundred_pages.app_path):
|
||||
utils.empty_dir(web_pages, keep_files=["_app.js"])
|
||||
app_with_hundred_pages._initialize_app()
|
||||
build.setup_frontend(app_with_hundred_pages.app_path)
|
||||
|
||||
def benchmark_fn():
|
||||
with chdir(app_with_hundred_pages.app_path):
|
||||
app_with_hundred_pages.app_instance._compile()
|
||||
|
||||
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
|
||||
app_with_hundred_pages._reload_state_module()
|
||||
|
||||
|
||||
@pytest.mark.benchmark(
|
||||
group="Compile time of varying page numbers",
|
||||
min_rounds=5,
|
||||
timer=time.perf_counter,
|
||||
disable_gc=True,
|
||||
warmup=False,
|
||||
)
|
||||
def test_app_100_compile_time_warm(benchmark, app_with_hundred_pages):
|
||||
"""Test the compile time on a warm start for an app with 100 page.
|
||||
|
||||
Args:
|
||||
benchmark: The benchmark fixture.
|
||||
app_with_hundred_pages: The app harness.
|
||||
"""
|
||||
with chdir(app_with_hundred_pages.app_path):
|
||||
app_with_hundred_pages._initialize_app()
|
||||
build.setup_frontend(app_with_hundred_pages.app_path)
|
||||
|
||||
def benchmark_fn():
|
||||
with chdir(app_with_hundred_pages.app_path):
|
||||
app_with_hundred_pages.app_instance._compile()
|
||||
|
||||
benchmark(benchmark_fn)
|
||||
app_with_hundred_pages._reload_state_module()
|
||||
|
||||
|
||||
@pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON)
|
||||
@pytest.mark.benchmark(
|
||||
group="Compile time of varying page numbers",
|
||||
timer=time.perf_counter,
|
||||
disable_gc=True,
|
||||
warmup=False,
|
||||
)
|
||||
def test_app_1000_compile_time_cold(benchmark, app_with_thousand_pages):
|
||||
"""Test the compile time on a cold start for an app with 1000 page.
|
||||
|
||||
Args:
|
||||
benchmark: The benchmark fixture.
|
||||
app_with_thousand_pages: The app harness.
|
||||
"""
|
||||
|
||||
def setup():
|
||||
with chdir(app_with_thousand_pages.app_path):
|
||||
utils.empty_dir(web_pages, keep_files=["_app.js"])
|
||||
app_with_thousand_pages._initialize_app()
|
||||
build.setup_frontend(app_with_thousand_pages.app_path)
|
||||
|
||||
def benchmark_fn():
|
||||
with chdir(app_with_thousand_pages.app_path):
|
||||
app_with_thousand_pages.app_instance._compile()
|
||||
|
||||
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
|
||||
app_with_thousand_pages._reload_state_module()
|
||||
|
||||
|
||||
@pytest.mark.benchmark(
|
||||
group="Compile time of varying page numbers",
|
||||
min_rounds=5,
|
||||
timer=time.perf_counter,
|
||||
disable_gc=True,
|
||||
warmup=False,
|
||||
)
|
||||
def test_app_1000_compile_time_warm(benchmark, app_with_thousand_pages):
|
||||
"""Test the compile time on a warm start for an app with 1000 page.
|
||||
|
||||
Args:
|
||||
benchmark: The benchmark fixture.
|
||||
app_with_thousand_pages: The app harness.
|
||||
"""
|
||||
with chdir(app_with_thousand_pages.app_path):
|
||||
app_with_thousand_pages._initialize_app()
|
||||
build.setup_frontend(app_with_thousand_pages.app_path)
|
||||
|
||||
def benchmark_fn():
|
||||
with chdir(app_with_thousand_pages.app_path):
|
||||
app_with_thousand_pages.app_instance._compile()
|
||||
|
||||
benchmark(benchmark_fn)
|
||||
app_with_thousand_pages._reload_state_module()
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
@pytest.mark.benchmark(
|
||||
group="Compile time of varying page numbers",
|
||||
timer=time.perf_counter,
|
||||
disable_gc=True,
|
||||
warmup=False,
|
||||
)
|
||||
def test_app_10000_compile_time_cold(benchmark, app_with_ten_thousand_pages):
|
||||
"""Test the compile time on a cold start for an app with 10000 page.
|
||||
|
||||
Args:
|
||||
benchmark: The benchmark fixture.
|
||||
app_with_ten_thousand_pages: The app harness.
|
||||
"""
|
||||
|
||||
def setup():
|
||||
with chdir(app_with_ten_thousand_pages.app_path):
|
||||
utils.empty_dir(web_pages, keep_files=["_app.js"])
|
||||
app_with_ten_thousand_pages._initialize_app()
|
||||
build.setup_frontend(app_with_ten_thousand_pages.app_path)
|
||||
|
||||
def benchmark_fn():
|
||||
with chdir(app_with_ten_thousand_pages.app_path):
|
||||
app_with_ten_thousand_pages.app_instance._compile()
|
||||
|
||||
benchmark.pedantic(benchmark_fn, setup=setup, rounds=5)
|
||||
app_with_ten_thousand_pages._reload_state_module()
|
||||
|
||||
|
||||
@pytest.mark.skip
|
||||
@pytest.mark.benchmark(
|
||||
group="Compile time of varying page numbers",
|
||||
min_rounds=5,
|
||||
timer=time.perf_counter,
|
||||
disable_gc=True,
|
||||
warmup=False,
|
||||
)
|
||||
def test_app_10000_compile_time_warm(benchmark, app_with_ten_thousand_pages):
|
||||
"""Test the compile time on a warm start for an app with 10000 page.
|
||||
|
||||
Args:
|
||||
benchmark: The benchmark fixture.
|
||||
app_with_ten_thousand_pages: The app harness.
|
||||
"""
|
||||
|
||||
def benchmark_fn():
|
||||
with chdir(app_with_ten_thousand_pages.app_path):
|
||||
app_with_ten_thousand_pages.app_instance._compile()
|
||||
|
||||
benchmark(benchmark_fn)
|
||||
app_with_ten_thousand_pages._reload_state_module()
|
@ -2,13 +2,12 @@
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
from httpx import HTTPError
|
||||
|
||||
|
||||
def get_python_version(venv_path: Path, os_name):
|
||||
def get_python_version(venv_path, os_name):
|
||||
"""Get the python version of python in a virtual env.
|
||||
|
||||
Args:
|
||||
@ -19,13 +18,13 @@ def get_python_version(venv_path: Path, os_name):
|
||||
The python version.
|
||||
"""
|
||||
python_executable = (
|
||||
venv_path / "bin" / "python"
|
||||
os.path.join(venv_path, "bin", "python")
|
||||
if "windows" not in os_name
|
||||
else venv_path / "Scripts" / "python.exe"
|
||||
else os.path.join(venv_path, "Scripts", "python.exe")
|
||||
)
|
||||
try:
|
||||
output = subprocess.check_output(
|
||||
[str(python_executable), "--version"], stderr=subprocess.STDOUT
|
||||
[python_executable, "--version"], stderr=subprocess.STDOUT
|
||||
)
|
||||
python_version = output.decode("utf-8").strip().split()[1]
|
||||
return ".".join(python_version.split(".")[:-1])
|
||||
@ -33,7 +32,7 @@ def get_python_version(venv_path: Path, os_name):
|
||||
return None
|
||||
|
||||
|
||||
def get_directory_size(directory: Path):
|
||||
def get_directory_size(directory):
|
||||
"""Get the size of a directory in bytes.
|
||||
|
||||
Args:
|
||||
@ -45,8 +44,8 @@ def get_directory_size(directory: Path):
|
||||
total_size = 0
|
||||
for dirpath, _, filenames in os.walk(directory):
|
||||
for f in filenames:
|
||||
fp = Path(dirpath) / f
|
||||
total_size += fp.stat().st_size
|
||||
fp = os.path.join(dirpath, f)
|
||||
total_size += os.path.getsize(fp)
|
||||
return total_size
|
||||
|
||||
|
||||
|
@ -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
|
@ -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.
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -1,5 +0,0 @@
|
||||
.web
|
||||
.git
|
||||
__pycache__/*
|
||||
Dockerfile
|
||||
uploaded_files
|
@ -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}
|
@ -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
|
@ -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
|
||||
```
|
@ -1,3 +0,0 @@
|
||||
.web
|
||||
!.web/bun.lockb
|
||||
!.web/package.json
|
@ -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
|
||||
}
|
@ -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
|
@ -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.
|
1
docker-example/requirements.txt
Normal file
1
docker-example/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
reflex
|
@ -1,5 +0,0 @@
|
||||
.web
|
||||
.git
|
||||
__pycache__/*
|
||||
Dockerfile
|
||||
uploaded_files
|
@ -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
|
||||
}
|
@ -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.
|
@ -1,5 +0,0 @@
|
||||
.web
|
||||
.git
|
||||
__pycache__/*
|
||||
Dockerfile
|
||||
uploaded_files
|
@ -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
|
||||
```
|
@ -10,6 +10,7 @@
|
||||
|
||||
### **✨ Performante, anpassbare Web-Apps in purem Python. Bereitstellung in Sekunden. ✨**
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
@ -34,7 +35,7 @@ Auf unserer [Architektur-Seite](https://reflex.dev/blog/2024-03-21-reflex-archit
|
||||
|
||||
## ⚙️ Installation
|
||||
|
||||
Öffne ein Terminal und führe den folgenden Befehl aus (benötigt Python 3.10+):
|
||||
Öffne ein Terminal und führe den folgenden Befehl aus (benötigt Python 3.8+):
|
||||
|
||||
```bash
|
||||
pip install reflex
|
||||
|
@ -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
|
||||
|
||||
|
@ -11,6 +11,7 @@ Pynecone की तलाश हैं? आप सही रेपो में
|
||||
### **✨ प्रदर्शनकारी, अनुकूलित वेब ऐप्स, शुद्ध Python में। सेकंडों में तैनात करें। ✨**
|
||||
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
@ -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) देखें।
|
||||
|
||||
## हमारे सभी योगदानकर्ताओं का धन्यवाद:
|
||||
|
||||
|
@ -10,6 +10,7 @@
|
||||
|
||||
### **✨ App web performanti e personalizzabili in puro Python. Distribuisci in pochi secondi. ✨**
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
@ -22,7 +23,7 @@
|
||||
|
||||
## ⚙️ Installazione
|
||||
|
||||
Apri un terminale ed esegui (Richiede Python 3.10+):
|
||||
Apri un terminale ed esegui (Richiede Python 3.8+):
|
||||
|
||||
```bash
|
||||
pip install reflex
|
||||
|
@ -11,6 +11,7 @@
|
||||
### **✨ 即時デプロイが可能な、Pure Python で作ったパフォーマンスと汎用性が高い Web アプリケーション ✨**
|
||||
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
@ -37,7 +38,7 @@ Reflex がどのように動作しているかを知るには、[アーキテク
|
||||
|
||||
## ⚙️ インストール
|
||||
|
||||
ターミナルを開いて以下のコマンドを実行してください。(Python 3.10 以上が必要です。):
|
||||
ターミナルを開いて以下のコマンドを実行してください。(Python 3.8 以上が必要です。):
|
||||
|
||||
```bash
|
||||
pip install reflex
|
||||
@ -222,7 +223,7 @@ app.add_page(index, title="DALL-E")
|
||||
|
||||
<div align="center">
|
||||
|
||||
📑 [Docs](https://reflex.dev/docs/getting-started/introduction) | 🗞️ [Blog](https://reflex.dev/blog) | 📱 [Component Library](https://reflex.dev/docs/library) | 🖼️ [Templates](https://reflex.dev/templates/) | 🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)
|
||||
📑 [Docs](https://reflex.dev/docs/getting-started/introduction) | 🗞️ [Blog](https://reflex.dev/blog) | 📱 [Component Library](https://reflex.dev/docs/library) | 🖼️ [Gallery](https://reflex.dev/docs/gallery) | 🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)
|
||||
|
||||
</div>
|
||||
|
||||
@ -242,7 +243,7 @@ Reflex は毎週、新しいリリースや機能追加を行っています!
|
||||
- **GitHub Discussions**: GitHub Discussions では、追加したい機能や、複雑で解明が必要な事柄についての議論に適している場所です。
|
||||
- **GitHub Issues**: [Issues](https://github.com/reflex-dev/reflex/issues)はバグの報告に適している場所です。また、課題を解決した PR のサブミットにチャレンジしていただくことも、可能です。
|
||||
|
||||
CONTスキルや経験に関わらず、私たちはコントリビュータを積極的に探しています。コントリビュートするために、[CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)をご覧ください。
|
||||
スキルや経験に関わらず、私たちはコントリビュータを積極的に探しています。コントリビュートするために、[CONTIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)をご覧ください。
|
||||
|
||||
## 私たちのコントリビュータに感謝!:
|
||||
|
||||
|
@ -10,6 +10,7 @@
|
||||
|
||||
### **✨ 순수 Python으로 고성능 사용자 정의 웹앱을 만들어 보세요. 몇 초만에 배포 가능합니다. ✨**
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
@ -20,7 +21,7 @@
|
||||
---
|
||||
## ⚙️ 설치
|
||||
|
||||
터미널을 열고 실행하세요. (Python 3.10+ 필요):
|
||||
터미널을 열고 실행하세요. (Python 3.8+ 필요):
|
||||
|
||||
```bash
|
||||
pip install reflex
|
||||
|
@ -10,6 +10,7 @@
|
||||
|
||||
### **✨ برنامه های تحت وب قابل تنظیم، کارآمد تماما پایتونی که در چند ثانیه مستقر(دپلوی) میشود. ✨**
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
@ -34,7 +35,7 @@
|
||||
|
||||
## ⚙️ Installation - نصب و راه اندازی
|
||||
|
||||
یک ترمینال را باز کنید و اجرا کنید (نیازمند Python 3.10+):
|
||||
یک ترمینال را باز کنید و اجرا کنید (نیازمند Python 3.8+):
|
||||
|
||||
```bash
|
||||
pip install reflex
|
||||
@ -249,7 +250,7 @@ app.add_page(index, title="DALL-E")
|
||||
- **بحث های GitHub**: راهی عالی برای صحبت در مورد ویژگی هایی که می خواهید اضافه کنید یا چیزهایی که گیج کننده هستند/نیاز به توضیح دارند.
|
||||
- **قسمت مشکلات GitHub**: [قسمت مشکلات](https://github.com/reflex-dev/reflex/issues) یک راه عالی برای گزارش اشکال هستند. علاوه بر این، می توانید یک مشکل موجود را حل کنید و یک PR(pull request) ارسال کنید.
|
||||
|
||||
ما فعالانه به دنبال مشارکت کنندگان هستیم، فارغ از سطح مهارت یا تجربه شما. برای مشارکت [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) را بررسی کنید.
|
||||
ما فعالانه به دنبال مشارکت کنندگان هستیم، فارغ از سطح مهارت یا تجربه شما. برای مشارکت [CONTIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) را بررسی کنید.
|
||||
|
||||
|
||||
## All Thanks To Our Contributors - با تشکر از همکاران ما:
|
||||
|
@ -21,7 +21,7 @@
|
||||
---
|
||||
## ⚙️ Instalação
|
||||
|
||||
Abra um terminal e execute (Requer Python 3.10+):
|
||||
Abra um terminal e execute (Requer Python 3.8+):
|
||||
|
||||
```bash
|
||||
pip install reflex
|
||||
|
@ -11,6 +11,7 @@
|
||||
### **✨ Saf Python'da performanslı, özelleştirilebilir web uygulamaları. Saniyeler içinde dağıtın. ✨**
|
||||
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
@ -24,7 +25,7 @@
|
||||
|
||||
## ⚙️ 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) | 🗞️ [Blog](https://reflex.dev/blog) | 📱 [Component Library](https://reflex.dev/docs/library) | 🖼️ [Templates](https://reflex.dev/templates/) | 🛸 [Deployment](https://reflex.dev/docs/hosting/deploy)
|
||||
📑 [Docs](https://reflex.dev/docs/getting-started/introduction) | 🗞️ [Blog](https://reflex.dev/blog) | 📱 [Component Library](https://reflex.dev/docs/library) | 🖼️ [Gallery](https://reflex.dev/docs/gallery) | 🛸 [Deployment](https://reflex.dev/docs/hosting/deploy)
|
||||
|
||||
</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:
|
||||
|
||||
|
@ -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. ✨**
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](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.
|
||||
|
||||
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
Đâ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` và `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` và `image_url`.
|
||||
Có cũng những biến boolean `processing` và `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) | 🗞️ [Blog](https://reflex.dev/blog) | 📱 [Component Library](https://reflex.dev/docs/library) | 🖼️ [Templates](https://reflex.dev/templates/) | 🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)
|
||||
|
||||
</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).
|
@ -10,6 +10,7 @@
|
||||
|
||||
### **✨ 使用 Python 创建高效且可自定义的网页应用程序,几秒钟内即可部署.✨**
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
@ -34,7 +35,7 @@ Reflex 是一个使用纯Python构建全栈web应用的库。
|
||||
|
||||
## ⚙️ 安装
|
||||
|
||||
打开一个终端并且运行(要求Python3.10+):
|
||||
打开一个终端并且运行(要求Python3.8+):
|
||||
|
||||
```bash
|
||||
pip install reflex
|
||||
|
@ -11,6 +11,7 @@
|
||||
**✨ 使用 Python 建立高效且可自訂的網頁應用程式,幾秒鐘內即可部署。✨**
|
||||
|
||||
[](https://badge.fury.io/py/reflex)
|
||||

|
||||

|
||||
[](https://reflex.dev/docs/getting-started/introduction)
|
||||
[](https://discord.gg/T5WSbC2YtQ)
|
||||
@ -36,7 +37,7 @@ Reflex 是一個可以用純 Python 構建全端網頁應用程式的函式庫
|
||||
|
||||
## ⚙️ 安裝
|
||||
|
||||
開啟一個終端機並且執行 (需要 Python 3.10+):
|
||||
開啟一個終端機並且執行 (需要 Python 3.8+):
|
||||
|
||||
```bash
|
||||
pip install reflex
|
||||
@ -229,7 +230,7 @@ app.add_page(index, title="DALL-E")
|
||||
|
||||
<div align="center">
|
||||
|
||||
📑 [Docs](https://reflex.dev/docs/getting-started/introduction) | 🗞️ [Blog](https://reflex.dev/blog) | 📱 [Component Library](https://reflex.dev/docs/library) | 🖼️ [Templates](https://reflex.dev/templates/) | 🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)
|
||||
📑 [Docs](https://reflex.dev/docs/getting-started/introduction) | 🗞️ [Blog](https://reflex.dev/blog) | 📱 [Component Library](https://reflex.dev/docs/library) | 🖼️ [Gallery](https://reflex.dev/docs/gallery) | 🛸 [Deployment](https://reflex.dev/docs/hosting/deploy-quick-start)
|
||||
|
||||
</div>
|
||||
|
||||
@ -251,7 +252,7 @@ Reflex 每周都有新功能和釋出新版本! 確保你按下 :star: 和 :eyes
|
||||
- **GitHub Discussions**: 這是一個討論您想新增的功能或對於一些困惑/需要澄清事項的好方法。
|
||||
- **GitHub Issues**: 在 [Issues](https://github.com/reflex-dev/reflex/issues) 頁面報告錯誤是一個絕佳的方式。此外,您也可以嘗試解決現有 Issue 並提交 PR。
|
||||
|
||||
我們積極尋找貢獻者,不論您的技能水平或經驗如何。要貢獻,請查看 [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)
|
||||
我們積極尋找貢獻者,不論您的技能水平或經驗如何。要貢獻,請查看 [CONTIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md)
|
||||
|
||||
|
||||
## 感謝所有貢獻者:
|
||||
|
@ -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()
|
@ -1,4 +1,4 @@
|
||||
FROM python:3.10
|
||||
FROM python:3.8
|
||||
|
||||
ARG USERNAME=kerrigan
|
||||
RUN useradd -m $USERNAME
|
@ -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
|
@ -1,4 +1,4 @@
|
||||
"""Test @rx.event(background=True) task functionality."""
|
||||
"""Test @rx.background task functionality."""
|
||||
|
||||
from typing import Generator
|
||||
|
||||
@ -13,6 +13,7 @@ def BackgroundTask():
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
from reflex.state import ImmutableStateError
|
||||
@ -20,13 +21,9 @@ def BackgroundTask():
|
||||
class State(rx.State):
|
||||
counter: int = 0
|
||||
_task_id: int = 0
|
||||
iterations: rx.Field[int] = rx.field(10)
|
||||
iterations: int = 10
|
||||
|
||||
@rx.event
|
||||
def set_iterations(self, value: str):
|
||||
self.iterations = int(value)
|
||||
|
||||
@rx.event(background=True)
|
||||
@rx.background
|
||||
async def handle_event(self):
|
||||
async with self:
|
||||
self._task_id += 1
|
||||
@ -35,35 +32,32 @@ def BackgroundTask():
|
||||
self.counter += 1
|
||||
await asyncio.sleep(0.005)
|
||||
|
||||
@rx.event(background=True)
|
||||
@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)
|
||||
yield State.increment_arbitrary(1) # type: ignore
|
||||
else:
|
||||
yield State.increment()
|
||||
yield State.increment() # type: ignore
|
||||
await asyncio.sleep(0.005)
|
||||
|
||||
@rx.event
|
||||
def increment(self):
|
||||
self.counter += 1
|
||||
|
||||
@rx.event(background=True)
|
||||
@rx.background
|
||||
async def increment_arbitrary(self, amount: int):
|
||||
async with self:
|
||||
self.counter += int(amount)
|
||||
|
||||
@rx.event
|
||||
def reset_counter(self):
|
||||
self.counter = 0
|
||||
|
||||
@rx.event
|
||||
async def blocking_pause(self):
|
||||
await asyncio.sleep(0.02)
|
||||
|
||||
@rx.event(background=True)
|
||||
@rx.background
|
||||
async def non_blocking_pause(self):
|
||||
await asyncio.sleep(0.02)
|
||||
|
||||
@ -75,13 +69,13 @@ def BackgroundTask():
|
||||
self.counter += 1
|
||||
await asyncio.sleep(0.005)
|
||||
|
||||
@rx.event(background=True)
|
||||
@rx.background
|
||||
async def handle_racy_event(self):
|
||||
await asyncio.gather(
|
||||
self.racy_task(), self.racy_task(), self.racy_task(), self.racy_task()
|
||||
)
|
||||
|
||||
@rx.event(background=True)
|
||||
@rx.background
|
||||
async def nested_async_with_self(self):
|
||||
async with self:
|
||||
self.counter += 1
|
||||
@ -93,15 +87,8 @@ def BackgroundTask():
|
||||
third_state = await self.get_state(ThirdState)
|
||||
await third_state._triple_count()
|
||||
|
||||
@rx.event(background=True)
|
||||
async def yield_in_async_with_self(self):
|
||||
async with self:
|
||||
self.counter += 1
|
||||
yield
|
||||
self.counter += 1
|
||||
|
||||
class OtherState(rx.State):
|
||||
@rx.event(background=True)
|
||||
@rx.background
|
||||
async def get_other_state(self):
|
||||
async with self:
|
||||
state = await self.get_state(State)
|
||||
@ -122,15 +109,15 @@ def BackgroundTask():
|
||||
|
||||
def index() -> rx.Component:
|
||||
return rx.vstack(
|
||||
rx.input(
|
||||
rc.input(
|
||||
id="token", value=State.router.session.client_token, is_read_only=True
|
||||
),
|
||||
rx.heading(State.counter, id="counter"),
|
||||
rx.input(
|
||||
rc.input(
|
||||
id="iterations",
|
||||
placeholder="Iterations",
|
||||
value=State.iterations.to_string(),
|
||||
on_change=State.set_iterations,
|
||||
value=State.iterations.to_string(), # type: ignore
|
||||
on_change=State.set_iterations, # type: ignore
|
||||
),
|
||||
rx.button(
|
||||
"Delayed Increment",
|
||||
@ -168,15 +155,10 @@ def BackgroundTask():
|
||||
on_click=OtherState.get_other_state,
|
||||
id="increment-from-other-state",
|
||||
),
|
||||
rx.button(
|
||||
"Yield in Async with Self",
|
||||
on_click=State.yield_in_async_with_self,
|
||||
id="yield-in-async-with-self",
|
||||
),
|
||||
rx.button("Reset", on_click=State.reset_counter, id="reset"),
|
||||
)
|
||||
|
||||
app = rx.App(_state=rx.State)
|
||||
app = rx.App(state=rx.State)
|
||||
app.add_page(index)
|
||||
|
||||
|
||||
@ -193,8 +175,8 @@ def background_task(
|
||||
running AppHarness instance
|
||||
"""
|
||||
with AppHarness.create(
|
||||
root=tmp_path_factory.mktemp("background_task"),
|
||||
app_source=BackgroundTask,
|
||||
root=tmp_path_factory.mktemp(f"background_task"),
|
||||
app_source=BackgroundTask, # type: ignore
|
||||
) as harness:
|
||||
yield harness
|
||||
|
||||
@ -292,7 +274,7 @@ def test_background_task(
|
||||
assert background_task._poll_for(lambda: counter.text == "620", timeout=40)
|
||||
# all tasks should have exited and cleaned up
|
||||
assert background_task._poll_for(
|
||||
lambda: not background_task.app_instance._background_tasks # pyright: ignore [reportOptionalMemberAccess]
|
||||
lambda: not background_task.app_instance.background_tasks # type: ignore
|
||||
)
|
||||
|
||||
|
||||
@ -352,30 +334,3 @@ def test_get_state(
|
||||
|
||||
increment_button.click()
|
||||
assert background_task._poll_for(lambda: counter.text == "13", timeout=5)
|
||||
|
||||
|
||||
def test_yield_in_async_with_self(
|
||||
background_task: AppHarness,
|
||||
driver: WebDriver,
|
||||
token: str,
|
||||
):
|
||||
"""Test that yielding inside async with self does not disable mutability.
|
||||
|
||||
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
|
||||
yield_in_async_with_self_button = driver.find_element(
|
||||
By.ID, "yield-in-async-with-self"
|
||||
)
|
||||
|
||||
# get a reference to the counter
|
||||
counter = driver.find_element(By.ID, "counter")
|
||||
assert background_task._poll_for(lambda: counter.text == "0", timeout=5)
|
||||
|
||||
yield_in_async_with_self_button.click()
|
||||
assert background_task._poll_for(lambda: counter.text == "2", timeout=5)
|
@ -15,8 +15,7 @@ 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 +42,26 @@ 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)
|
||||
results: List[Optional[Union[str, Dict, List]]] = []
|
||||
inline_counter: int = 0
|
||||
external_counter: int = 0
|
||||
value: str = "Initial"
|
||||
last_result: 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 +76,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,81 +118,48 @@ 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),
|
||||
value=CallScriptState.inline_counter.to(str), # type: ignore
|
||||
id="inline_counter",
|
||||
read_only=True,
|
||||
),
|
||||
rx.input(
|
||||
value=CallScriptState.external_counter.to(str),
|
||||
value=CallScriptState.external_counter.to(str), # type: ignore
|
||||
id="external_counter",
|
||||
read_only=True,
|
||||
),
|
||||
rx.text_area(
|
||||
value=CallScriptState.results.to_string(),
|
||||
value=CallScriptState.results.to_string(), # type: ignore
|
||||
id="results",
|
||||
read_only=True,
|
||||
),
|
||||
@ -275,73 +229,11 @@ def CallScript():
|
||||
CallScriptState.value,
|
||||
on_click=rx.call_script(
|
||||
"'updated'",
|
||||
callback=CallScriptState.setvar("value"),
|
||||
callback=CallScriptState.set_value, # type: ignore
|
||||
),
|
||||
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 +249,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
|
||||
|
||||
@ -471,73 +363,3 @@ def test_call_script(
|
||||
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"
|
@ -10,13 +10,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
|
||||
@ -24,6 +17,8 @@ from . import utils
|
||||
|
||||
def ClientSide():
|
||||
"""App for testing client-side state."""
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
|
||||
class ClientSideState(rx.State):
|
||||
@ -33,18 +28,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")
|
||||
@ -56,13 +51,12 @@ def ClientSide():
|
||||
|
||||
# Session storage
|
||||
s1: str = rx.SessionStorage()
|
||||
s2: str = rx.SessionStorage("s2 default")
|
||||
s2: rx.SessionStorage = "s2 default" # type: ignore
|
||||
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 = ""
|
||||
@ -72,28 +66,27 @@ def ClientSide():
|
||||
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(
|
||||
rc.input(
|
||||
value=ClientSideState.router.session.client_token,
|
||||
read_only=True,
|
||||
is_read_only=True,
|
||||
id="token",
|
||||
),
|
||||
rx.input(
|
||||
rc.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(
|
||||
rc.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(
|
||||
@ -127,7 +120,7 @@ def ClientSide():
|
||||
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 +137,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
|
||||
|
||||
@ -320,8 +313,8 @@ async def test_client_side_state(
|
||||
# no cookies should be set yet!
|
||||
assert not driver.get_cookies()
|
||||
local_storage_items = local_storage.items()
|
||||
local_storage_items.pop("chakra-ui-color-mode", None)
|
||||
local_storage_items.pop("last_compiled_time", None)
|
||||
local_storage_items.pop("theme", None)
|
||||
assert not local_storage_items
|
||||
|
||||
# set some cookies and local storage values
|
||||
@ -436,8 +429,8 @@ async def test_client_side_state(
|
||||
assert f"{sub_state_name}.c3" not in cookie_info_map(driver)
|
||||
|
||||
local_storage_items = local_storage.items()
|
||||
local_storage_items.pop("chakra-ui-color-mode", None)
|
||||
local_storage_items.pop("last_compiled_time", None)
|
||||
local_storage_items.pop("theme", None)
|
||||
assert local_storage_items.pop(f"{sub_state_name}.l1") == "l1 value"
|
||||
assert local_storage_items.pop(f"{sub_state_name}.l2") == "l2 value"
|
||||
assert local_storage_items.pop("l3") == "l3 value"
|
||||
@ -613,109 +606,6 @@ async def test_client_side_state(
|
||||
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()
|
107
integration/test_component_state.py
Normal file
107
integration/test_component_state.py
Normal 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"
|
@ -22,22 +22,22 @@ def ComputedVars():
|
||||
count: int = 0
|
||||
|
||||
# cached var with dep on count
|
||||
@rx.var(interval=15)
|
||||
@rx.var(cache=True, interval=15)
|
||||
def count1(self) -> int:
|
||||
return self.count
|
||||
|
||||
# cached backend var with dep on count
|
||||
@rx.var(interval=15, backend=True)
|
||||
@rx.var(cache=True, interval=15, backend=True)
|
||||
def count1_backend(self) -> int:
|
||||
return self.count
|
||||
|
||||
# same as above but implicit backend with `_` prefix
|
||||
@rx.var(interval=15)
|
||||
@rx.var(cache=True, interval=15)
|
||||
def _count1_backend(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)
|
||||
@ -45,32 +45,22 @@ def ComputedVars():
|
||||
return self.count
|
||||
|
||||
# explicit dependency on count var
|
||||
@rx.var(deps=["count"], auto_deps=False)
|
||||
@rx.var(cache=True, deps=["count"], auto_deps=False)
|
||||
def depends_on_count(self) -> int:
|
||||
return self.count
|
||||
|
||||
# explicit dependency on count1 var
|
||||
@rx.var(deps=[count1], auto_deps=False)
|
||||
@rx.var(cache=True, deps=[count1], auto_deps=False)
|
||||
def depends_on_count1(self) -> int:
|
||||
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()
|
||||
|
||||
@ -111,14 +101,10 @@ 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)
|
||||
|
||||
@ -136,8 +122,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
|
||||
|
||||
@ -237,10 +223,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()
|
||||
|
@ -1,14 +1,11 @@
|
||||
"""Test case for displaying the connection banner when the websocket drops."""
|
||||
|
||||
import functools
|
||||
from typing import Generator
|
||||
|
||||
import pytest
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
from reflex import constants
|
||||
from reflex.config import environment
|
||||
from reflex.testing import AppHarness, WebDriver
|
||||
|
||||
from .utils import SessionStorage
|
||||
@ -23,7 +20,6 @@ def ConnectionBanner():
|
||||
class State(rx.State):
|
||||
foo: int = 0
|
||||
|
||||
@rx.event
|
||||
async def delay(self):
|
||||
await asyncio.sleep(5)
|
||||
|
||||
@ -34,55 +30,28 @@ def ConnectionBanner():
|
||||
rx.button(
|
||||
"Increment",
|
||||
id="increment",
|
||||
on_click=State.set_foo(State.foo + 1), # pyright: ignore [reportAttributeAccessIssue]
|
||||
on_click=State.set_foo(State.foo + 1), # type: ignore
|
||||
),
|
||||
rx.button("Delay", id="delay", on_click=State.delay),
|
||||
)
|
||||
|
||||
app = rx.App(_state=rx.State)
|
||||
app = rx.App(state=rx.State)
|
||||
app.add_page(index)
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
params=[constants.CompileContext.RUN, constants.CompileContext.DEPLOY],
|
||||
ids=["compile_context_run", "compile_context_deploy"],
|
||||
)
|
||||
def simulate_compile_context(request) -> constants.CompileContext:
|
||||
"""Fixture to simulate reflex cloud deployment.
|
||||
|
||||
Args:
|
||||
request: pytest request fixture.
|
||||
|
||||
Returns:
|
||||
The context to run the app with.
|
||||
"""
|
||||
return request.param
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def connection_banner(
|
||||
tmp_path,
|
||||
simulate_compile_context: constants.CompileContext,
|
||||
) -> Generator[AppHarness, None, None]:
|
||||
def connection_banner(tmp_path) -> Generator[AppHarness, None, None]:
|
||||
"""Start ConnectionBanner app at tmp_path via AppHarness.
|
||||
|
||||
Args:
|
||||
tmp_path: pytest tmp_path fixture
|
||||
simulate_compile_context: Which context to run the app with.
|
||||
|
||||
Yields:
|
||||
running AppHarness instance
|
||||
"""
|
||||
environment.REFLEX_COMPILE_CONTEXT.set(simulate_compile_context)
|
||||
|
||||
with AppHarness.create(
|
||||
root=tmp_path,
|
||||
app_source=functools.partial(ConnectionBanner),
|
||||
app_name=(
|
||||
"connection_banner_reflex_cloud"
|
||||
if simulate_compile_context == constants.CompileContext.DEPLOY
|
||||
else "connection_banner"
|
||||
),
|
||||
app_source=ConnectionBanner, # type: ignore
|
||||
) as harness:
|
||||
yield harness
|
||||
|
||||
@ -101,40 +70,9 @@ def has_error_modal(driver: WebDriver) -> bool:
|
||||
"""
|
||||
try:
|
||||
driver.find_element(By.XPATH, CONNECTION_ERROR_XPATH)
|
||||
return True
|
||||
except NoSuchElementException:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def has_cloud_banner(driver: WebDriver) -> bool:
|
||||
"""Check if the cloud banner is displayed.
|
||||
|
||||
Args:
|
||||
driver: Selenium webdriver instance.
|
||||
|
||||
Returns:
|
||||
True if the banner is displayed, False otherwise.
|
||||
"""
|
||||
try:
|
||||
driver.find_element(By.XPATH, "//*[ contains(text(), 'This app is paused') ]")
|
||||
except NoSuchElementException:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def _assert_token(connection_banner, driver):
|
||||
"""Poll for backend to be up.
|
||||
|
||||
Args:
|
||||
connection_banner: AppHarness instance.
|
||||
driver: Selenium webdriver instance.
|
||||
"""
|
||||
ss = SessionStorage(driver)
|
||||
assert connection_banner._poll_for(lambda: ss.get("token") is not None), (
|
||||
"token not found"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -148,7 +86,11 @@ async def test_connection_banner(connection_banner: AppHarness):
|
||||
assert connection_banner.backend is not None
|
||||
driver = connection_banner.frontend()
|
||||
|
||||
_assert_token(connection_banner, driver)
|
||||
ss = SessionStorage(driver)
|
||||
assert connection_banner._poll_for(
|
||||
lambda: ss.get("token") is not None
|
||||
), "token not found"
|
||||
|
||||
assert connection_banner._poll_for(lambda: not has_error_modal(driver))
|
||||
|
||||
delay_button = driver.find_element(By.ID, "delay")
|
||||
@ -188,36 +130,3 @@ async def test_connection_banner(connection_banner: AppHarness):
|
||||
|
||||
# Count should have incremented after coming back up
|
||||
assert connection_banner.poll_for_value(counter_element, exp_not_equal="1") == "2"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cloud_banner(
|
||||
connection_banner: AppHarness, simulate_compile_context: constants.CompileContext
|
||||
):
|
||||
"""Test that the connection banner is displayed when the websocket drops.
|
||||
|
||||
Args:
|
||||
connection_banner: AppHarness instance.
|
||||
simulate_compile_context: Which context to set for the app.
|
||||
"""
|
||||
assert connection_banner.app_instance is not None
|
||||
assert connection_banner.backend is not None
|
||||
driver = connection_banner.frontend()
|
||||
|
||||
driver.add_cookie({"name": "backend-enabled", "value": "truly"})
|
||||
driver.refresh()
|
||||
_assert_token(connection_banner, driver)
|
||||
assert connection_banner._poll_for(lambda: not has_cloud_banner(driver))
|
||||
|
||||
driver.add_cookie({"name": "backend-enabled", "value": "false"})
|
||||
driver.refresh()
|
||||
if simulate_compile_context == constants.CompileContext.DEPLOY:
|
||||
assert connection_banner._poll_for(lambda: has_cloud_banner(driver))
|
||||
else:
|
||||
_assert_token(connection_banner, driver)
|
||||
assert connection_banner._poll_for(lambda: not has_cloud_banner(driver))
|
||||
|
||||
driver.delete_cookie("backend-enabled")
|
||||
driver.refresh()
|
||||
_assert_token(connection_banner, driver)
|
||||
assert connection_banner._poll_for(lambda: not has_cloud_banner(driver))
|
@ -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
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Callable, Coroutine, Generator, Type
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
@ -18,23 +17,20 @@ def DynamicRoute():
|
||||
"""App for testing dynamic routes."""
|
||||
from typing import List
|
||||
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
|
||||
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 +42,15 @@ def DynamicRoute():
|
||||
|
||||
def index():
|
||||
return rx.fragment(
|
||||
rx.input(
|
||||
rc.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(
|
||||
rc.input(value=DynamicState.page_id, is_read_only=True, id="page_id"),
|
||||
rc.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 +58,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]
|
||||
rc.list(
|
||||
rx.foreach(
|
||||
DynamicState.order, # pyright: ignore [reportAttributeAccessIssue]
|
||||
lambda i: rx.list_item(rx.text(i)),
|
||||
DynamicState.order, # type: ignore
|
||||
lambda i: rc.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 +94,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 +113,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:
|
||||
@ -235,11 +166,8 @@ def poll_for_order(
|
||||
dynamic_state_name
|
||||
].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_name].order == exp_order
|
||||
|
||||
return _poll_for_order
|
||||
|
||||
@ -377,56 +305,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")
|
@ -16,6 +16,8 @@ def TestEventAction():
|
||||
"""App for testing event_actions."""
|
||||
from typing import List, Optional
|
||||
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
|
||||
class EventActionState(rx.State):
|
||||
@ -24,7 +26,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 +55,7 @@ def TestEventAction():
|
||||
|
||||
def index():
|
||||
return rx.vstack(
|
||||
rx.input(
|
||||
rc.input(
|
||||
value=EventActionState.router.session.client_token,
|
||||
is_read_only=True,
|
||||
id="token",
|
||||
@ -63,16 +64,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 +89,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 +103,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 +117,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]
|
||||
rc.list(
|
||||
rx.foreach(
|
||||
EventActionState.order,
|
||||
rx.list_item,
|
||||
EventActionState.order, # type: ignore
|
||||
rc.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 +172,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
|
||||
|
@ -18,6 +18,8 @@ def EventChain():
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
|
||||
# repeated here since the outer global isn't exported into the App module
|
||||
@ -27,126 +29,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 = rc.input(
|
||||
value=State.router.session.client_token, is_read_only=True, id="token"
|
||||
)
|
||||
|
||||
@ -154,7 +137,7 @@ def EventChain():
|
||||
def index():
|
||||
return rx.fragment(
|
||||
token_input,
|
||||
rx.input(value=State.interim_value, is_read_only=True, id="interim_value"),
|
||||
rc.input(value=State.interim_value, is_read_only=True, id="interim_value"),
|
||||
rx.button(
|
||||
"Return Event",
|
||||
id="return_event",
|
||||
@ -193,12 +176,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 +222,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 +234,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 +242,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 +260,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
|
||||
|
||||
@ -493,6 +476,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 +492,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",
|
@ -11,9 +11,7 @@ from selenium.webdriver.remote.webdriver import WebDriver
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
from reflex.testing import AppHarness, AppHarnessProd
|
||||
|
||||
pytestmark = [pytest.mark.ignore_console_error]
|
||||
from reflex.testing import AppHarness
|
||||
|
||||
|
||||
def TestApp():
|
||||
@ -28,8 +26,6 @@ def TestApp():
|
||||
class TestAppState(rx.State):
|
||||
"""State for the TestApp app."""
|
||||
|
||||
react_error: bool = False
|
||||
|
||||
def divide_by_number(self, number: int):
|
||||
"""Divide by number and print the result.
|
||||
|
||||
@ -39,7 +35,7 @@ def TestApp():
|
||||
"""
|
||||
print(1 / number)
|
||||
|
||||
app = rx.App(_state=rx.State)
|
||||
app = rx.App(state=rx.State)
|
||||
|
||||
@app.add_page
|
||||
def index():
|
||||
@ -51,21 +47,9 @@ def TestApp():
|
||||
),
|
||||
rx.button(
|
||||
"induce_backend_error",
|
||||
on_click=lambda: TestAppState.divide_by_number(0), # pyright: ignore [reportCallIssue]
|
||||
on_click=lambda: TestAppState.divide_by_number(0), # type: ignore
|
||||
id="induce-backend-error-btn",
|
||||
),
|
||||
rx.button(
|
||||
"induce_react_error",
|
||||
on_click=TestAppState.set_react_error(True), # pyright: ignore [reportAttributeAccessIssue]
|
||||
id="induce-react-error-btn",
|
||||
),
|
||||
rx.box(
|
||||
rx.cond(
|
||||
TestAppState.react_error,
|
||||
rx.Var.create({"invalid": "cannot have object as child"}),
|
||||
"",
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@ -86,7 +70,7 @@ def test_app(
|
||||
with app_harness_env.create(
|
||||
root=tmp_path_factory.mktemp("test_app"),
|
||||
app_name=f"testapp_{app_harness_env.__name__.lower()}",
|
||||
app_source=TestApp,
|
||||
app_source=TestApp, # type: ignore
|
||||
) as harness:
|
||||
yield harness
|
||||
|
||||
@ -168,37 +152,3 @@ def test_backend_exception_handler_during_runtime(
|
||||
"divide_by_number" in captured_default_handler_output.out
|
||||
and "ZeroDivisionError" in captured_default_handler_output.out
|
||||
)
|
||||
|
||||
|
||||
def test_frontend_exception_handler_with_react(
|
||||
test_app: AppHarness,
|
||||
driver: WebDriver,
|
||||
capsys,
|
||||
):
|
||||
"""Test calling frontend exception handler during runtime.
|
||||
|
||||
Render an object as a react child, which is invalid.
|
||||
|
||||
Args:
|
||||
test_app: harness for TestApp app
|
||||
driver: WebDriver instance.
|
||||
capsys: pytest fixture for capturing stdout and stderr.
|
||||
|
||||
"""
|
||||
reset_button = WebDriverWait(driver, 20).until(
|
||||
EC.element_to_be_clickable((By.ID, "induce-react-error-btn"))
|
||||
)
|
||||
|
||||
reset_button.click()
|
||||
|
||||
# Wait for the error to be logged
|
||||
time.sleep(2)
|
||||
|
||||
captured_default_handler_output = capsys.readouterr()
|
||||
if isinstance(test_app, AppHarnessProd):
|
||||
assert "Error: Minified React error #31" in captured_default_handler_output.out
|
||||
else:
|
||||
assert (
|
||||
"Error: Objects are not valid as a React child (found: object with keys \n{invalid})"
|
||||
in captured_default_handler_output.out
|
||||
)
|
@ -20,6 +20,8 @@ def FormSubmit(form_component):
|
||||
"""
|
||||
from typing import Dict, List
|
||||
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
|
||||
class FormState(rx.State):
|
||||
@ -30,34 +32,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(
|
||||
rc.input(
|
||||
value=FormState.router.session.client_token,
|
||||
is_read_only=True,
|
||||
id="token",
|
||||
),
|
||||
eval(form_component)(
|
||||
rx.vstack(
|
||||
rx.input(id="name_input"),
|
||||
rc.input(id="name_input"),
|
||||
rx.hstack(rc.pin_input(length=4, id="pin_input")),
|
||||
rc.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%"),
|
||||
rc.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"),
|
||||
rc.select(["option1", "option2"], id="select_input"),
|
||||
rc.select(FormState.var_options, id="select_input_var"),
|
||||
rx.text_area(id="text_area_input"),
|
||||
rx.input(
|
||||
rc.input(
|
||||
id="debounce_input",
|
||||
debounce_timeout=0,
|
||||
on_change=rx.console_log,
|
||||
@ -80,6 +81,8 @@ def FormSubmitName(form_component):
|
||||
"""
|
||||
from typing import Dict, List
|
||||
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
|
||||
class FormState(rx.State):
|
||||
@ -90,24 +93,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(
|
||||
rc.input(
|
||||
value=FormState.router.session.client_token,
|
||||
is_read_only=True,
|
||||
id="token",
|
||||
),
|
||||
eval(form_component)(
|
||||
rx.vstack(
|
||||
rx.input(name="name_input"),
|
||||
rc.input(name="name_input"),
|
||||
rx.hstack(rc.pin_input(length=4, name="pin_input")),
|
||||
rc.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%"),
|
||||
rc.range_slider(name="range_input"),
|
||||
rx.radio(FormState.options, name="radio_input"),
|
||||
rx.select(
|
||||
FormState.options,
|
||||
@ -115,13 +121,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,
|
||||
rc.input_group(
|
||||
rc.input_left_element(rx.icon(tag="chevron_right")),
|
||||
rc.input(
|
||||
name="debounce_input",
|
||||
debounce_timeout=0,
|
||||
on_change=rx.console_log,
|
||||
),
|
||||
rc.input_right_element(rx.icon(tag="chevron_left")),
|
||||
),
|
||||
rc.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 +152,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="rc.form"),
|
||||
functools.partial(FormSubmitName, form_component="rc.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 +177,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 +224,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()
|
||||
|
||||
@ -247,12 +275,15 @@ async def test_submit(driver, form_submit: AppHarness):
|
||||
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"
|
@ -16,7 +16,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 +26,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 +63,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
|
||||
|
||||
@ -183,6 +183,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") == ""
|
@ -58,7 +58,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:
|
@ -36,24 +36,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 +58,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 +79,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 +102,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
|
@ -21,18 +21,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 +43,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 +60,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
|
||||
|
@ -19,38 +19,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.var(cache=True)
|
||||
def img_default(self) -> Image.Image:
|
||||
return self._blue()
|
||||
|
||||
@rx.var
|
||||
@rx.var(cache=True)
|
||||
def img_bmp(self) -> Image.Image:
|
||||
return self._blue(format="BMP")
|
||||
|
||||
@rx.var
|
||||
@rx.var(cache=True)
|
||||
def img_jpg(self) -> Image.Image:
|
||||
return self._blue(format="JPEG")
|
||||
|
||||
@rx.var
|
||||
@rx.var(cache=True)
|
||||
def img_png(self) -> Image.Image:
|
||||
return self._blue(format="PNG")
|
||||
|
||||
@rx.var
|
||||
@rx.var(cache=True)
|
||||
def img_gif(self) -> Image.Image:
|
||||
return self._blue(format="GIF")
|
||||
|
||||
@rx.var
|
||||
@rx.var(cache=True)
|
||||
def img_webp(self) -> Image.Image:
|
||||
return self._blue(format="WEBP")
|
||||
|
||||
@rx.var
|
||||
@rx.var(cache=True)
|
||||
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 +84,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
|
||||
|
@ -52,7 +52,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 +74,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()
|
||||
|
@ -11,22 +11,21 @@ from reflex.testing import AppHarness
|
||||
|
||||
def ServerSideEvent():
|
||||
"""App with inputs set via event handlers and set_value."""
|
||||
import reflex_chakra as rc
|
||||
|
||||
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 +33,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(
|
||||
rc.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"),
|
||||
rc.input(default_value="a", id="a"),
|
||||
rc.input(default_value="b", id="b"),
|
||||
rc.input(default_value="c", id="c"),
|
||||
rx.button(
|
||||
"Clear Immediate",
|
||||
id="clear_immediate",
|
||||
@ -93,7 +91,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 +100,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
|
||||
|
@ -12,7 +12,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 +39,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
|
||||
|
@ -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
|
||||
|
173
integration/test_table.py
Normal file
173
integration/test_table.py
Normal file
@ -0,0 +1,173 @@
|
||||
"""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_chakra as rc
|
||||
|
||||
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(
|
||||
rc.input(
|
||||
id="token",
|
||||
value=TableState.router.session.client_token,
|
||||
is_read_only=True,
|
||||
),
|
||||
rc.table_container(
|
||||
rc.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(
|
||||
rc.table_container(
|
||||
rc.table( # type: ignore
|
||||
rc.thead( # type: ignore
|
||||
rc.tr( # type: ignore
|
||||
rc.th("Name"),
|
||||
rc.th("Age"),
|
||||
rc.th("Location"),
|
||||
)
|
||||
),
|
||||
rc.tbody( # type: ignore
|
||||
rc.tr( # type: ignore
|
||||
rc.td("John"),
|
||||
rc.td(30),
|
||||
rc.td("New York"),
|
||||
),
|
||||
rc.tr( # type: ignore
|
||||
rc.td("Jane"),
|
||||
rc.td(31),
|
||||
rc.td("San Francisco"),
|
||||
),
|
||||
rc.tr( # type: ignore
|
||||
rc.td("Joe"),
|
||||
rc.td(32),
|
||||
rc.td("Los Angeles"),
|
||||
),
|
||||
),
|
||||
rc.tfoot( # type: ignore
|
||||
rc.tr(
|
||||
rc.td("footer1"),
|
||||
rc.td("footer2"),
|
||||
rc.td("footer3"),
|
||||
) # type: ignore
|
||||
),
|
||||
rc.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"
|
@ -27,6 +27,8 @@ def TailwindApp(
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
import reflex_chakra as rc
|
||||
|
||||
import reflex as rx
|
||||
|
||||
class UnusedState(rx.State):
|
||||
@ -34,7 +36,7 @@ def TailwindApp(
|
||||
|
||||
def index():
|
||||
return rx.el.div(
|
||||
rx.text(paragraph_text, class_name=paragraph_class_name),
|
||||
rc.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"),
|
||||
@ -78,7 +80,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
|
||||
@ -107,7 +109,7 @@ def test_tailwind_app(tailwind_app: AppHarness, tailwind_disabled: bool):
|
||||
assert len(paragraphs) == 3
|
||||
for p in paragraphs:
|
||||
assert tailwind_app.poll_for_content(p, exp_not_equal="") == PARAGRAPH_TEXT
|
||||
assert p.value_of_css_property("font-family") == "monospace"
|
||||
assert p.value_of_css_property("font-family") == '"monospace"'
|
||||
if tailwind_disabled:
|
||||
# expect default color, not "text-red-500" from tailwind utility class
|
||||
assert p.value_of_css_property("color") not in TEXT_RED_500_COLOR
|
@ -4,33 +4,26 @@ 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."""
|
||||
from typing import Dict, List
|
||||
|
||||
import reflex as rx
|
||||
import reflex_chakra as rc
|
||||
|
||||
LARGE_DATA = "DUMMY" * 1024 * 512
|
||||
import reflex as rx
|
||||
|
||||
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 +34,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 +42,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(
|
||||
rc.input(
|
||||
value=UploadState.router.session.client_token,
|
||||
read_only=True,
|
||||
is_read_only=True,
|
||||
id="token",
|
||||
),
|
||||
rx.heading("Default Upload"),
|
||||
@ -78,16 +57,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 +85,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 +107,7 @@ def UploadFile():
|
||||
),
|
||||
rx.vstack(
|
||||
rx.foreach(
|
||||
UploadState.progress_dicts,
|
||||
UploadState.progress_dicts, # type: ignore
|
||||
lambda d: rx.text(d.to_string()),
|
||||
)
|
||||
),
|
||||
@ -138,37 +116,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 +134,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 +157,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,7 +171,11 @@ 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)
|
||||
token_input = driver.find_element(By.ID, "token")
|
||||
assert token_input
|
||||
# wait for the backend connection to send the token
|
||||
token = upload_file.poll_for_value(token_input)
|
||||
assert token is not None
|
||||
full_state_name = upload_file.get_full_state_name(["_upload_state"])
|
||||
state_name = upload_file.get_state_name("_upload_state")
|
||||
substate_token = f"{token}_{full_state_name}"
|
||||
@ -261,19 +197,6 @@ 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 (
|
||||
@ -284,8 +207,17 @@ async def test_upload_file(
|
||||
|
||||
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[state_name].event_order.count("upload_progress") == 1
|
||||
assert state.substates[state_name].event_order.count("chain_event") == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -298,7 +230,11 @@ 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)
|
||||
token_input = driver.find_element(By.ID, "token")
|
||||
assert token_input
|
||||
# wait for the backend connection to send the token
|
||||
token = upload_file.poll_for_value(token_input)
|
||||
assert token is not None
|
||||
full_state_name = upload_file.get_full_state_name(["_upload_state"])
|
||||
state_name = upload_file.get_state_name("_upload_state")
|
||||
substate_token = f"{token}_{full_state_name}"
|
||||
@ -322,9 +258,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()
|
||||
@ -339,9 +273,8 @@ async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver):
|
||||
|
||||
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 +290,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 +319,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 +344,18 @@ 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)
|
||||
token_input = driver.find_element(By.ID, "token")
|
||||
assert token_input
|
||||
# wait for the backend connection to send the token
|
||||
token = upload_file.poll_for_value(token_input)
|
||||
assert token is not None
|
||||
state_name = upload_file.get_state_name("_upload_state")
|
||||
state_full_name = upload_file.get_full_state_name(["_upload_state"])
|
||||
substate_token = f"{token}_{state_full_name}"
|
||||
|
||||
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 +368,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[state_name].progress_dicts
|
||||
assert exp_name not in state.substates[state_name]._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
|
68
integration/test_urls.py
Executable file
68
integration/test_urls.py
Executable file
@ -0,0 +1,68 @@
|
||||
"""Integration tests for all urls in Reflex."""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
|
||||
def check_urls(repo_dir):
|
||||
"""Check that all URLs in the repo are valid and secure.
|
||||
|
||||
Args:
|
||||
repo_dir: The directory of the repo.
|
||||
|
||||
Returns:
|
||||
A list of errors.
|
||||
"""
|
||||
url_pattern = re.compile(r'http[s]?://reflex\.dev[^\s")]*')
|
||||
errors = []
|
||||
|
||||
for root, _dirs, files in os.walk(repo_dir):
|
||||
if "__pycache__" in root:
|
||||
continue
|
||||
|
||||
for file_name in files:
|
||||
if not file_name.endswith(".py") and not file_name.endswith(".md"):
|
||||
continue
|
||||
|
||||
file_path = os.path.join(root, file_name)
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8", errors="ignore") as file:
|
||||
for line in file:
|
||||
urls = url_pattern.findall(line)
|
||||
for url in set(urls):
|
||||
if url.startswith("http://"):
|
||||
errors.append(
|
||||
f"Found insecure HTTP URL: {url} in {file_path}"
|
||||
)
|
||||
url = url.strip('"\n')
|
||||
try:
|
||||
response = requests.head(
|
||||
url, allow_redirects=True, timeout=5
|
||||
)
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
errors.append(
|
||||
f"Error accessing URL: {url} in {file_path} | Error: {e}, , Check your path ends with a /"
|
||||
)
|
||||
except Exception as e:
|
||||
errors.append(f"Error reading file: {file_path} | Error: {e}")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"repo_dir",
|
||||
[Path(__file__).resolve().parent.parent / "reflex"],
|
||||
)
|
||||
def test_find_and_check_urls(repo_dir):
|
||||
"""Test that all URLs in the repo are valid and secure.
|
||||
|
||||
Args:
|
||||
repo_dir: The directory of the repo.
|
||||
"""
|
||||
errors = check_urls(repo_dir)
|
||||
assert not errors, "\n".join(errors)
|
@ -7,47 +7,40 @@ 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_chakra as rc
|
||||
|
||||
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
|
||||
from reflex.ivars.base import LiteralVar
|
||||
from reflex.ivars.sequence import ArrayVar
|
||||
|
||||
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[int, int] = {1: 2}
|
||||
dict2: Dict[int, int] = {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 +376,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 +392,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 +404,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 +420,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 +472,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 +531,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,7 +547,10 @@ def VarOperations():
|
||||
VarOperationState.html_str,
|
||||
id="html_str",
|
||||
),
|
||||
rx.el.mark("second"),
|
||||
rc.highlight(
|
||||
"second",
|
||||
query=[VarOperationState.str_var2],
|
||||
),
|
||||
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"),
|
||||
@ -582,7 +571,7 @@ def VarOperations():
|
||||
),
|
||||
rx.box(
|
||||
rx.foreach(
|
||||
LiteralVar.create(list(range(0, 3))).to(ArrayVar, list[int]),
|
||||
LiteralVar.create(list(range(0, 3))).to(ArrayVar, List[int]),
|
||||
lambda x: rx.foreach(
|
||||
ArrayVar.range(x),
|
||||
lambda y: rx.text(VarOperationState.list1[y], as_="p"),
|
||||
@ -599,52 +588,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 +603,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 +749,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,19 +784,6 @@ 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:
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user