diff --git a/.github/actions/setup_build_env/action.yml b/.github/actions/setup_build_env/action.yml index a25f0ae44..d983a4daa 100644 --- a/.github/actions/setup_build_env/action.yml +++ b/.github/actions/setup_build_env/action.yml @@ -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 ot be invoked as `poetry`. +# - Poetry of version `poetry-version` is ready to 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' diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index aac67f7a6..6da40ef6f 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -5,7 +5,7 @@ on: types: - closed paths-ignore: - - '**/*.md' + - "**/*.md" permissions: contents: read @@ -15,21 +15,21 @@ defaults: shell: bash env: - PYTHONIOENCODING: 'utf8' + PYTHONIOENCODING: "utf8" TELEMETRY_ENABLED: false - NODE_OPTIONS: '--max_old_space_size=8192' + NODE_OPTIONS: "--max_old_space_size=8192" PR_TITLE: ${{ github.event.pull_request.title }} jobs: reflex-web: -# if: github.event.pull_request.merged == true + # if: github.event.pull_request.merged == true strategy: fail-fast: false matrix: # Show OS combos first in GUI os: [ubuntu-latest] - python-version: ['3.11.4'] - node-version: ['18.x'] + python-version: ["3.12.8"] + node-version: ["18.x"] runs-on: ${{ matrix.os }} steps: @@ -80,25 +80,23 @@ jobs: fail-fast: false matrix: # Show OS combos first in GUI - os: [ubuntu-latest, windows-latest, macos-12] - python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0'] + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.10.16", "3.11.11", "3.12.8"] exclude: - os: windows-latest - python-version: '3.10.13' + python-version: "3.10.16" - os: windows-latest - python-version: '3.9.18' + python-version: "3.11.11" # keep only one python version for MacOS - os: macos-latest - python-version: '3.9.18' + python-version: "3.10.16" - os: macos-latest - python-version: '3.10.13' - - os: macos-12 - python-version: '3.12.0' + python-version: "3.11.11" include: - os: windows-latest - python-version: '3.10.11' + python-version: "3.10.11" - os: windows-latest - python-version: '3.9.13' + python-version: "3.11.9" runs-on: ${{ matrix.os }} steps: @@ -123,7 +121,7 @@ jobs: --event-type "${{ github.event_name }}" --pr-id "${{ github.event.pull_request.id }}" reflex-dist-size: # This job is used to calculate the size of the Reflex distribution (wheel file) - if: github.event.pull_request.merged == true + if: github.event.pull_request.merged == true timeout-minutes: 30 strategy: # Prioritize getting more information out of the workflow (even if something fails) @@ -133,7 +131,7 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/setup_build_env with: - python-version: 3.11.5 + python-version: 3.12.8 run-poetry-install: true create-venv-at-path: .venv - name: Build reflex @@ -143,25 +141,29 @@ jobs: # Only run if the database creds are available in this context. run: poetry run python benchmarks/benchmark_package_size.py --os ubuntu-latest - --python-version 3.11.5 --commit-sha "${{ github.sha }}" --pr-id "${{ github.event.pull_request.id }}" + --python-version 3.12.8 --commit-sha "${{ github.sha }}" --pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}" --path ./dist reflex-venv-size: # This job calculates the total size of Reflex and its dependencies - if: github.event.pull_request.merged == true + if: github.event.pull_request.merged == true timeout-minutes: 30 strategy: # Prioritize getting more information out of the workflow (even if something fails) fail-fast: false matrix: # Show OS combos first in GUI - os: [ubuntu-latest, windows-latest, macos-12] - python-version: ['3.11.5'] + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ["3.12.8"] 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: @@ -186,6 +188,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 \ No newline at end of file + --path ./.venv diff --git a/.github/workflows/check_generated_pyi.yml b/.github/workflows/check_generated_pyi.yml index d9a0e8e71..760707d15 100644 --- a/.github/workflows/check_generated_pyi.yml +++ b/.github/workflows/check_generated_pyi.yml @@ -6,16 +6,16 @@ concurrency: on: push: - branches: ['main'] + branches: ["main"] # We don't just trigger on make_pyi.py and the components dir, because # there are other things that can change the generator output # e.g. black version, reflex.Component, reflex.Var. paths-ignore: - - '**/*.md' + - "**/*.md" pull_request: - branches: ['main'] + branches: ["main"] paths-ignore: - - '**/*.md' + - "**/*.md" jobs: check-generated-pyi-components: @@ -25,7 +25,7 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/setup_build_env with: - python-version: '3.11.5' + python-version: "3.12.8" run-poetry-install: true create-venv-at-path: .venv - run: | diff --git a/.github/workflows/check_node_latest.yml b/.github/workflows/check_node_latest.yml index 1cf9f6fdf..1957f64f8 100644 --- a/.github/workflows/check_node_latest.yml +++ b/.github/workflows/check_node_latest.yml @@ -1,43 +1,40 @@ name: integration-node-latest on: - push: - branches: - - main - pull_request: - branches: - - main + push: + branches: + - main + pull_request: + branches: + - main env: - TELEMETRY_ENABLED: false - REFLEX_USE_SYSTEM_NODE: true + TELEMETRY_ENABLED: false + REFLEX_USE_SYSTEM_NODE: true jobs: - check_latest_node: - runs-on: ubuntu-22.04 - strategy: - matrix: - python-version: ['3.12'] - split_index: [1, 2] - node-version: ['node'] - fail-fast: false - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup_build_env - with: - python-version: ${{ matrix.python-version }} - run-poetry-install: true - create-venv-at-path: .venv - - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - run: | - poetry run uv pip install pyvirtualdisplay pillow pytest-split - poetry run playwright install --with-deps - - run: | - poetry run pytest tests/test_node_version.py - poetry run pytest tests/integration --splits 2 --group ${{matrix.split_index}} - - + check_latest_node: + runs-on: ubuntu-22.04 + strategy: + matrix: + python-version: ["3.12.8"] + split_index: [1, 2] + node-version: ["node"] + fail-fast: false + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup_build_env + with: + python-version: ${{ matrix.python-version }} + run-poetry-install: true + create-venv-at-path: .venv + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: | + poetry run uv pip install pyvirtualdisplay pillow pytest-split + poetry run playwright install --with-deps + - run: | + poetry run pytest tests/test_node_version.py + poetry run pytest tests/integration --splits 2 --group ${{matrix.split_index}} diff --git a/.github/workflows/check_outdated_dependencies.yml b/.github/workflows/check_outdated_dependencies.yml index fe8c42608..34bfa23bf 100644 --- a/.github/workflows/check_outdated_dependencies.yml +++ b/.github/workflows/check_outdated_dependencies.yml @@ -1,88 +1,86 @@ name: check-outdated-dependencies on: - push: # This will trigger the action when a pull request is opened or updated. + push: # This will trigger the action when a pull request is opened or updated. branches: - - 'release/**' # This will trigger the action when any branch starting with "release/" is created. - workflow_dispatch: # Allow manual triggering if needed. + - "release/**" # This will trigger the action when any branch starting with "release/" is created. + workflow_dispatch: # Allow manual triggering if needed. jobs: backend: runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v3 + - name: Checkout code + uses: actions/checkout@v3 - - uses: ./.github/actions/setup_build_env - with: - python-version: '3.9' - run-poetry-install: true - create-venv-at-path: .venv + - uses: ./.github/actions/setup_build_env + with: + python-version: '3.10' + run-poetry-install: true + create-venv-at-path: .venv - - name: Check outdated backend dependencies - run: | - outdated=$(poetry show -oT) - echo "Outdated:" - echo "$outdated" + - name: Check outdated backend dependencies + run: | + outdated=$(poetry show -oT) + echo "Outdated:" + echo "$outdated" - filtered_outdated=$(echo "$outdated" | grep -vE 'pyright|ruff' || true) - - if [ ! -z "$filtered_outdated" ]; then - echo "Outdated dependencies found:" - echo "$filtered_outdated" - exit 1 - else - echo "All dependencies are up to date. (pyright and ruff are ignored)" - fi + filtered_outdated=$(echo "$outdated" | grep -vE 'pyright|ruff' || true) + if [ ! -z "$filtered_outdated" ]; then + echo "Outdated dependencies found:" + echo "$filtered_outdated" + exit 1 + else + echo "All dependencies are up to date. (pyright and ruff are ignored)" + fi frontend: runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 - - uses: ./.github/actions/setup_build_env - with: - python-version: '3.10.11' - run-poetry-install: true - create-venv-at-path: .venv - - name: Clone Reflex Website Repo - uses: actions/checkout@v4 - with: - repository: reflex-dev/reflex-web - ref: main - path: reflex-web - - name: Install Requirements for reflex-web - working-directory: ./reflex-web - run: poetry run uv pip install -r requirements.txt - - name: Install additional dependencies for DB access - run: poetry run uv pip install psycopg2-binary - - name: Init Website for reflex-web - working-directory: ./reflex-web - run: poetry run reflex init - - name: Run Website and Check for errors - run: | - poetry run bash scripts/integration.sh ./reflex-web dev - - name: Check outdated frontend dependencies - working-directory: ./reflex-web/.web - run: | - raw_outdated=$(/home/runner/.local/share/reflex/bun/bin/bun outdated) - outdated=$(echo "$raw_outdated" | grep -vE '\|\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\|' || true) - echo "Outdated:" - echo "$outdated" + - name: Checkout code + uses: actions/checkout@v4 + - uses: ./.github/actions/setup_build_env + with: + python-version: "3.10.16" + run-poetry-install: true + create-venv-at-path: .venv + - name: Clone Reflex Website Repo + uses: actions/checkout@v4 + with: + repository: reflex-dev/reflex-web + ref: main + path: reflex-web + - name: Install Requirements for reflex-web + working-directory: ./reflex-web + run: poetry run uv pip install $(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' || true) - no_extra=$(echo "$filtered_outdated" | grep -vE '\|\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-' || true) + # Ignore 3rd party dependencies that are not updated. + filtered_outdated=$(echo "$outdated" | grep -vE 'Package|@chakra-ui|lucide-react|@splinetool/runtime|ag-grid-react|framer-motion|react-markdown|remark-math|remark-gfm|rehype-katex|rehype-raw|remark-unwrap-images|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 - + 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 diff --git a/.github/workflows/integration_app_harness.yml b/.github/workflows/integration_app_harness.yml index 6ac5fe6ab..0bafd3601 100644 --- a/.github/workflows/integration_app_harness.yml +++ b/.github/workflows/integration_app_harness.yml @@ -23,8 +23,8 @@ jobs: strategy: matrix: state_manager: ["redis", "memory"] + python-version: ["3.11.11", "3.12.8", "3.13.1"] split_index: [1, 2] - python-version: ["3.11.5", "3.12.0"] fail-fast: false runs-on: ubuntu-22.04 services: @@ -47,17 +47,10 @@ 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 + - run: poetry run uv pip install pyvirtualdisplay pillow pytest-split pytest-retry - name: Run app harness tests env: - SCREENSHOT_DIR: /tmp/screenshots/${{ matrix.state_manager }}/${{ matrix.python-version }}/${{ matrix.split_index }} REDIS_URL: ${{ matrix.state_manager == 'redis' && 'redis://localhost:6379' || '' }} run: | - poetry run playwright install --with-deps - poetry run pytest tests/integration --splits 2 --group ${{matrix.split_index}} - - uses: actions/upload-artifact@v4 - name: Upload failed test screenshots - if: always() - with: - name: failed_test_screenshots - path: /tmp/screenshots + poetry run playwright install chromium + poetry run pytest tests/integration --retries 3 --maxfail=5 --splits 2 --group ${{matrix.split_index}} diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index 3e22234b8..b02604fd6 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -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: + example-counter-and-nba-proxy: env: OUTPUT_FILE: import_benchmark.json timeout-minutes: 30 @@ -43,17 +43,17 @@ jobs: matrix: # Show OS combos first in GUI os: [ubuntu-latest, windows-latest] - python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0'] + python-version: ['3.10.16', '3.11.11', '3.12.8', '3.13.1'] exclude: - os: windows-latest - python-version: '3.10.13' + python-version: "3.11.11" - os: windows-latest - python-version: '3.9.18' + python-version: '3.10.16' include: - os: windows-latest - python-version: '3.10.11' + python-version: "3.11.9" - os: windows-latest - python-version: '3.9.13' + python-version: '3.10.11' runs-on: ${{ matrix.os }} steps: @@ -73,7 +73,7 @@ jobs: run: | poetry run uv pip install -r requirements.txt - name: Install additional dependencies for DB access - run: poetry run uv pip install psycopg2-binary + run: poetry run uv pip install psycopg - name: Check export --backend-only before init for counter example working-directory: ./reflex-examples/counter run: | @@ -114,7 +114,25 @@ jobs: --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" - + - 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 reflex-web: @@ -123,10 +141,10 @@ jobs: matrix: # Show OS combos first in GUI os: [ubuntu-latest] - python-version: ['3.10.11', '3.11.4'] + python-version: ["3.11.11", "3.12.8"] env: - REFLEX_WEB_WINDOWS_OVERRIDE: '1' + REFLEX_WEB_WINDOWS_OVERRIDE: "1" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -147,7 +165,7 @@ jobs: 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 psycopg2-binary + run: poetry run uv pip install psycopg - name: Init Website for reflex-web working-directory: ./reflex-web run: poetry run reflex init @@ -171,7 +189,7 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/setup_build_env with: - python-version: '3.11.4' + python-version: "3.11.11" run-poetry-install: true create-venv-at-path: .venv - name: Create app directory @@ -190,15 +208,15 @@ jobs: # Check that npm is home npm -v poetry run bash scripts/integration.sh ./rx-shout-from-template prod - reflex-web-macos: if: github.event_name == 'push' && github.ref == 'refs/heads/main' strategy: fail-fast: false matrix: - python-version: ['3.11.5', '3.12.0'] - runs-on: macos-12 + # 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 @@ -216,7 +234,7 @@ jobs: 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 psycopg2-binary + run: poetry run uv pip install psycopg - name: Init Website for reflex-web working-directory: ./reflex-web run: poetry run reflex init @@ -231,4 +249,3 @@ jobs: --python-version "${{ matrix.python-version }}" --commit-sha "${{ github.sha }}" --pr-id "${{ github.event.pull_request.id }}" --branch-name "${{ github.head_ref || github.ref_name }}" --app-name "reflex-web" --path ./reflex-web/.web - \ No newline at end of file diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml new file mode 100644 index 000000000..c7bd1003a --- /dev/null +++ b/.github/workflows/performance.yml @@ -0,0 +1,34 @@ +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 benchmarks/test_evaluate.py --codspeed diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 9e6e42a38..4c71e3035 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -6,12 +6,12 @@ concurrency: on: pull_request: - branches: ['main'] + branches: ["main"] push: # Note even though this job is called "pre-commit" and runs "pre-commit", this job will run # also POST-commit on main also! In case there are mishandled merge conflicts / bad auto-resolves # when merging into main branch. - branches: ['main'] + branches: ["main"] jobs: pre-commit: @@ -23,7 +23,7 @@ jobs: with: # running vs. one version of Python is OK # i.e. ruff, black, etc. - python-version: 3.11.5 + python-version: 3.12.8 run-poetry-install: true create-venv-at-path: .venv # TODO pre-commit related stuff can be cached too (not a bottleneck yet) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index c76918583..1ef063ca7 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -6,13 +6,13 @@ concurrency: on: push: - branches: ['main'] + branches: ["main"] paths-ignore: - - '**/*.md' + - "**/*.md" pull_request: - branches: ['main'] + branches: ["main"] paths-ignore: - - '**/*.md' + - "**/*.md" permissions: contents: read @@ -28,18 +28,18 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0'] + python-version: ["3.10.16", "3.11.11", "3.12.8", "3.13.1"] # Windows is a bit behind on Python version availability in Github exclude: - os: windows-latest - python-version: '3.10.13' + python-version: "3.11.11" - os: windows-latest - python-version: '3.9.18' + python-version: "3.10.16" include: - os: windows-latest - python-version: '3.10.11' + python-version: "3.11.9" - os: windows-latest - python-version: '3.9.13' + python-version: "3.10.11" runs-on: ${{ matrix.os }} # Service containers to run with `runner-job` @@ -88,8 +88,9 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.9.18', '3.10.13', '3.11.5', '3.12.0'] - runs-on: macos-12 + # 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 @@ -105,4 +106,4 @@ jobs: run: | export PYTHONUNBUFFERED=1 poetry run uv pip install "pydantic~=1.10" - poetry run pytest tests/units --cov --no-cov-on-fail --cov-report= \ No newline at end of file + poetry run pytest tests/units --cov --no-cov-on-fail --cov-report= diff --git a/.gitignore b/.gitignore index 0f7d9e5ff..29a868796 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ assets/external/* dist/* examples/ .web +.states .idea .vscode .coverage @@ -14,3 +15,4 @@ requirements.txt .pyi_generator_last_run .pyi_generator_diff reflex.db +.codspeed \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1acf9ecb8..0bad7b996 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ fail_fast: true repos: - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: v0.8.2 + rev: v0.9.3 hooks: - id: ruff-format args: [reflex, tests] @@ -11,6 +11,12 @@ repos: 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: @@ -18,11 +24,12 @@ 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.313 + rev: v1.1.393 hooks: - id: pyright args: [reflex, tests] diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 253076695..d22d91973 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -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, socio-economic status, +identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fc8398013..aed576c42 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ Here is a quick guide on how to run Reflex repo locally so you can start contrib **Prerequisites:** -- Python >= 3.9 +- Python >= 3.10 - 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:** @@ -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.9. +Note that pre-commit will only be installed when you use a Python version >= 3.10. ``` bash pre-commit install diff --git a/README.md b/README.md index 527cca980..5174d563e 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ See our [architecture page](https://reflex.dev/blog/2024-03-21-reflex-architectu ## ⚙️ Installation -Open a terminal and run (Requires Python 3.9+): +Open a terminal and run (Requires Python 3.10+): ```bash pip install reflex @@ -249,7 +249,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 [CONTIBUTING.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 [CONTRIBUTING.md](https://github.com/reflex-dev/reflex/blob/main/CONTRIBUTING.md) ## All Thanks To Our Contributors: diff --git a/benchmarks/benchmark_compile_times.py b/benchmarks/benchmark_compile_times.py index 2273bd5c8..56cb4e4cc 100644 --- a/benchmarks/benchmark_compile_times.py +++ b/benchmarks/benchmark_compile_times.py @@ -5,6 +5,7 @@ from __future__ import annotations import argparse import json import os +from pathlib import Path from utils import send_data_to_posthog @@ -18,7 +19,7 @@ def extract_stats_from_json(json_file: str) -> list[dict]: Returns: list[dict]: The stats for each test. """ - with open(json_file, "r") as file: + with Path(json_file).open() as file: json_data = json.load(file) # Load the JSON data if it is a string, otherwise assume it's already a dictionary diff --git a/benchmarks/benchmark_imports.py b/benchmarks/benchmark_imports.py index 4706c0cf6..8c3f9f46c 100644 --- a/benchmarks/benchmark_imports.py +++ b/benchmarks/benchmark_imports.py @@ -5,6 +5,7 @@ from __future__ import annotations import argparse import json import os +from pathlib import Path from utils import send_data_to_posthog @@ -18,7 +19,7 @@ def extract_stats_from_json(json_file: str) -> dict: Returns: dict: The stats for each test. """ - with open(json_file, "r") as file: + with Path(json_file).open() as file: json_data = json.load(file) # Load the JSON data if it is a string, otherwise assume it's already a dictionary diff --git a/benchmarks/test_benchmark_compile_components.py b/benchmarks/test_benchmark_compile_components.py index 81d0c2e89..9bcfbf85b 100644 --- a/benchmarks/test_benchmark_compile_components.py +++ b/benchmarks/test_benchmark_compile_components.py @@ -34,13 +34,13 @@ def render_component(num: int): rx.box( rx.accordion.root( rx.accordion.item( - header="Full Ingredients", # type: ignore - content="Yes. It's built with accessibility in mind.", # type: ignore + header="Full Ingredients", + content="Yes. It's built with accessibility in mind.", 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 + header="Applications", + content="Yes. It's unstyled by default, giving you freedom over the look and feel.", ), collapsible=True, variant="ghost", @@ -122,7 +122,7 @@ def AppWithTenComponentsOnePage(): def index() -> rx.Component: return rx.center(rx.vstack(*render_component(1))) - app = rx.App(state=rx.State) + app = rx.App(_state=rx.State) app.add_page(index) @@ -133,7 +133,7 @@ def AppWithHundredComponentOnePage(): def index() -> rx.Component: return rx.center(rx.vstack(*render_component(100))) - app = rx.App(state=rx.State) + app = rx.App(_state=rx.State) app.add_page(index) @@ -144,7 +144,7 @@ def AppWithThousandComponentsOnePage(): def index() -> rx.Component: return rx.center(rx.vstack(*render_component(1000))) - app = rx.App(state=rx.State) + app = rx.App(_state=rx.State) app.add_page(index) @@ -166,9 +166,9 @@ def app_with_10_components( root=root, app_source=functools.partial( AppWithTenComponentsOnePage, - render_component=render_component, # type: ignore + render_component=render_component, # pyright: ignore [reportCallIssue] ), - ) # type: ignore + ) @pytest.fixture(scope="session") @@ -189,9 +189,9 @@ def app_with_100_components( root=root, app_source=functools.partial( AppWithHundredComponentOnePage, - render_component=render_component, # type: ignore + render_component=render_component, # pyright: ignore [reportCallIssue] ), - ) # type: ignore + ) @pytest.fixture(scope="session") @@ -212,9 +212,9 @@ def app_with_1000_components( root=root, app_source=functools.partial( AppWithThousandComponentsOnePage, - render_component=render_component, # type: ignore + render_component=render_component, # pyright: ignore [reportCallIssue] ), - ) # type: ignore + ) @pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) diff --git a/benchmarks/test_benchmark_compile_pages.py b/benchmarks/test_benchmark_compile_pages.py index 292882b74..149fc6130 100644 --- a/benchmarks/test_benchmark_compile_pages.py +++ b/benchmarks/test_benchmark_compile_pages.py @@ -28,7 +28,7 @@ def render_multiple_pages(app, num: int): """ from typing import Tuple - from rxconfig import config # type: ignore + from rxconfig import config # pyright: ignore [reportMissingImports] import reflex as rx @@ -74,13 +74,13 @@ def render_multiple_pages(app, num: int): rx.select( ["C", "PF", "SF", "PG", "SG"], placeholder="Select a position. (All)", - on_change=State.set_position, # type: ignore + on_change=State.set_position, # pyright: ignore [reportAttributeAccessIssue] size="3", ), rx.select( college, placeholder="Select a college. (All)", - on_change=State.set_college, # type: ignore + on_change=State.set_college, # pyright: ignore [reportAttributeAccessIssue] size="3", ), ), @@ -95,7 +95,7 @@ def render_multiple_pages(app, num: int): default_value=[18, 50], min=18, max=50, - on_value_commit=State.set_age, # type: ignore + on_value_commit=State.set_age, # pyright: ignore [reportAttributeAccessIssue] ), align_items="left", width="100%", @@ -110,7 +110,7 @@ def render_multiple_pages(app, num: int): default_value=[0, 25000000], min=0, max=25000000, - on_value_commit=State.set_salary, # type: ignore + on_value_commit=State.set_salary, # pyright: ignore [reportAttributeAccessIssue] ), align_items="left", width="100%", @@ -130,7 +130,7 @@ def render_multiple_pages(app, num: int): def AppWithOnePage(): """A reflex app with one page.""" - from rxconfig import config # type: ignore + from rxconfig import config # pyright: ignore [reportMissingImports] import reflex as rx @@ -162,7 +162,7 @@ def AppWithOnePage(): height="100vh", ) - app = rx.App(state=rx.State) + app = rx.App(_state=rx.State) app.add_page(index) @@ -170,7 +170,7 @@ def AppWithTenPages(): """A reflex app with 10 pages.""" import reflex as rx - app = rx.App(state=rx.State) + app = rx.App(_state=rx.State) render_multiple_pages(app, 10) @@ -178,7 +178,7 @@ def AppWithHundredPages(): """A reflex app with 100 pages.""" import reflex as rx - app = rx.App(state=rx.State) + app = rx.App(_state=rx.State) render_multiple_pages(app, 100) @@ -186,7 +186,7 @@ def AppWithThousandPages(): """A reflex app with Thousand pages.""" import reflex as rx - app = rx.App(state=rx.State) + app = rx.App(_state=rx.State) render_multiple_pages(app, 1000) @@ -194,7 +194,7 @@ def AppWithTenThousandPages(): """A reflex app with ten thousand pages.""" import reflex as rx - app = rx.App(state=rx.State) + app = rx.App(_state=rx.State) render_multiple_pages(app, 10000) @@ -232,7 +232,7 @@ def app_with_ten_pages( root=root, app_source=functools.partial( AppWithTenPages, - render_comp=render_multiple_pages, # type: ignore + render_comp=render_multiple_pages, # pyright: ignore [reportCallIssue] ), ) @@ -255,9 +255,9 @@ def app_with_hundred_pages( root=root, app_source=functools.partial( AppWithHundredPages, - render_comp=render_multiple_pages, # type: ignore + render_comp=render_multiple_pages, # pyright: ignore [reportCallIssue] ), - ) # type: ignore + ) @pytest.fixture(scope="session") @@ -278,9 +278,9 @@ def app_with_thousand_pages( root=root, app_source=functools.partial( AppWithThousandPages, - render_comp=render_multiple_pages, # type: ignore + render_comp=render_multiple_pages, # pyright: ignore [reportCallIssue] ), - ) # type: ignore + ) @pytest.fixture(scope="session") @@ -301,9 +301,9 @@ def app_with_ten_thousand_pages( root=root, app_source=functools.partial( AppWithTenThousandPages, - render_comp=render_multiple_pages, # type: ignore + render_comp=render_multiple_pages, # pyright: ignore [reportCallIssue] ), - ) # type: ignore + ) @pytest.mark.skipif(constants.IS_WINDOWS, reason=WINDOWS_SKIP_REASON) diff --git a/benchmarks/test_evaluate.py b/benchmarks/test_evaluate.py new file mode 100644 index 000000000..aa4c8237e --- /dev/null +++ b/benchmarks/test_evaluate.py @@ -0,0 +1,231 @@ +from dataclasses import dataclass +from typing import cast + +import pytest + +import reflex as rx + + +class SideBarState(rx.State): + """State for the side bar.""" + + current_page: rx.Field[str] = rx.field("/") + + +@dataclass(frozen=True) +class SideBarPage: + """A page in the side bar.""" + + title: str + href: str + + +@dataclass(frozen=True) +class SideBarSection: + """A section in the side bar.""" + + name: str + icon: str + pages: tuple[SideBarPage, ...] + + +@dataclass(frozen=True) +class Category: + """A category in the side bar.""" + + name: str + href: str + sections: tuple[SideBarSection, ...] + + +SIDE_BAR = ( + Category( + name="General", + href="/", + sections=( + SideBarSection( + name="Home", + icon="home", + pages=( + SideBarPage(title="Home", href="/"), + SideBarPage(title="Contact", href="/contact"), + ), + ), + SideBarSection( + name="About", + icon="info", + pages=( + SideBarPage(title="About", href="/about"), + SideBarPage(title="FAQ", href="/faq"), + ), + ), + ), + ), + Category( + name="Projects", + href="/projects", + sections=( + SideBarSection( + name="Python", + icon="worm", + pages=( + SideBarPage(title="Python", href="/projects/python"), + SideBarPage(title="Django", href="/projects/django"), + SideBarPage(title="Flask", href="/projects/flask"), + SideBarPage(title="FastAPI", href="/projects/fastapi"), + SideBarPage(title="Pyramid", href="/projects/pyramid"), + SideBarPage(title="Tornado", href="/projects/tornado"), + SideBarPage(title="TurboGears", href="/projects/turbogears"), + SideBarPage(title="Web2py", href="/projects/web2py"), + SideBarPage(title="Zope", href="/projects/zope"), + SideBarPage(title="Plone", href="/projects/plone"), + SideBarPage(title="Quixote", href="/projects/quixote"), + SideBarPage(title="Bottle", href="/projects/bottle"), + SideBarPage(title="CherryPy", href="/projects/cherrypy"), + SideBarPage(title="Falcon", href="/projects/falcon"), + SideBarPage(title="Sanic", href="/projects/sanic"), + SideBarPage(title="Starlette", href="/projects/starlette"), + ), + ), + SideBarSection( + name="JavaScript", + icon="banana", + pages=( + SideBarPage(title="JavaScript", href="/projects/javascript"), + SideBarPage(title="Angular", href="/projects/angular"), + SideBarPage(title="React", href="/projects/react"), + SideBarPage(title="Vue", href="/projects/vue"), + SideBarPage(title="Ember", href="/projects/ember"), + SideBarPage(title="Backbone", href="/projects/backbone"), + SideBarPage(title="Meteor", href="/projects/meteor"), + SideBarPage(title="Svelte", href="/projects/svelte"), + SideBarPage(title="Preact", href="/projects/preact"), + SideBarPage(title="Mithril", href="/projects/mithril"), + SideBarPage(title="Aurelia", href="/projects/aurelia"), + SideBarPage(title="Polymer", href="/projects/polymer"), + SideBarPage(title="Knockout", href="/projects/knockout"), + SideBarPage(title="Dojo", href="/projects/dojo"), + SideBarPage(title="Riot", href="/projects/riot"), + SideBarPage(title="Alpine", href="/projects/alpine"), + SideBarPage(title="Stimulus", href="/projects/stimulus"), + SideBarPage(title="Marko", href="/projects/marko"), + SideBarPage(title="Sapper", href="/projects/sapper"), + SideBarPage(title="Nuxt", href="/projects/nuxt"), + SideBarPage(title="Next", href="/projects/next"), + SideBarPage(title="Gatsby", href="/projects/gatsby"), + SideBarPage(title="Gridsome", href="/projects/gridsome"), + SideBarPage(title="Nest", href="/projects/nest"), + SideBarPage(title="Express", href="/projects/express"), + SideBarPage(title="Koa", href="/projects/koa"), + SideBarPage(title="Hapi", href="/projects/hapi"), + SideBarPage(title="LoopBack", href="/projects/loopback"), + SideBarPage(title="Feathers", href="/projects/feathers"), + SideBarPage(title="Sails", href="/projects/sails"), + SideBarPage(title="Adonis", href="/projects/adonis"), + SideBarPage(title="Meteor", href="/projects/meteor"), + SideBarPage(title="Derby", href="/projects/derby"), + SideBarPage(title="Socket.IO", href="/projects/socketio"), + ), + ), + ), + ), +) + + +def side_bar_page(page: SideBarPage): + return rx.box( + rx.link( + page.title, + href=page.href, + ) + ) + + +def side_bar_section(section: SideBarSection): + return rx.accordion.item( + rx.accordion.header( + rx.accordion.trigger( + rx.hstack( + rx.hstack( + rx.icon(section.icon), + section.name, + align="center", + ), + rx.accordion.icon(), + width="100%", + justify="between", + ) + ) + ), + rx.accordion.content( + rx.vstack( + *map(side_bar_page, section.pages), + ), + border_inline_start="1px solid", + padding_inline_start="1em", + margin_inline_start="1.5em", + ), + value=section.name, + width="100%", + variant="ghost", + ) + + +def side_bar_category(category: Category): + selected_section = cast( + rx.Var, + rx.match( + SideBarState.current_page, + *[ + ( + section.name, + section.name, + ) + for section in category.sections + ], + None, + ), + ) + return rx.vstack( + rx.heading( + rx.link( + category.name, + href=category.href, + ), + size="5", + ), + rx.accordion.root( + *map(side_bar_section, category.sections), + default_value=selected_section.to(str), + variant="ghost", + width="100%", + collapsible=True, + type="multiple", + ), + width="100%", + ) + + +def side_bar(): + return rx.vstack( + *map(side_bar_category, SIDE_BAR), + width="fit-content", + ) + + +LOREM_IPSUM = "Lorem ipsum dolor sit amet, dolor ut dolore pariatur aliqua enim tempor sed. Labore excepteur sed exercitation. Ullamco aliquip lorem sunt enim in incididunt. Magna anim officia sint cillum labore. Ut eu non dolore minim nostrud magna eu, aute ex in incididunt irure eu. Fugiat et magna magna est excepteur eiusmod minim. Quis eiusmod et non pariatur dolor veniam incididunt, eiusmod irure enim sed dolor lorem pariatur do. Occaecat duis irure excepteur dolore. Proident ut laborum pariatur sit sit, nisi nostrud voluptate magna commodo laborum esse velit. Voluptate non minim deserunt adipiscing irure deserunt cupidatat. Laboris veniam commodo incididunt veniam lorem occaecat, fugiat ipsum dolor cupidatat. Ea officia sed eu excepteur culpa adipiscing, tempor consectetur ullamco eu. Anim ex proident nulla sunt culpa, voluptate veniam proident est adipiscing sint elit velit. Laboris adipiscing est culpa cillum magna. Sit veniam nulla nulla, aliqua eiusmod commodo lorem cupidatat commodo occaecat. Fugiat cillum dolor incididunt mollit eiusmod sint. Non lorem dolore labore excepteur minim laborum sed. Irure nisi do lorem nulla sunt commodo, deserunt quis mollit consectetur minim et esse est, proident nostrud officia enim sed reprehenderit. Magna cillum consequat aute reprehenderit duis sunt ullamco. Labore qui mollit voluptate. Duis dolor sint aute amet aliquip officia, est non mollit tempor enim quis fugiat, eu do culpa consectetur magna. Do ullamco aliqua voluptate culpa excepteur reprehenderit reprehenderit. Occaecat nulla sit est magna. Deserunt ea voluptate veniam cillum. Amet cupidatat duis est tempor fugiat ex eu, officia est sunt consectetur labore esse exercitation. Nisi cupidatat irure est nisi. Officia amet eu veniam reprehenderit. In amet incididunt tempor commodo ea labore. Mollit dolor aliquip excepteur, voluptate aute occaecat id officia proident. Ullamco est amet tempor. Proident aliquip proident mollit do aliquip ipsum, culpa quis aute id irure. Velit excepteur cillum cillum ut cupidatat. Occaecat qui elit esse nulla minim. Consequat velit id ad pariatur tempor. Eiusmod deserunt aliqua ex sed quis non. Dolor sint commodo ex in deserunt nostrud excepteur, pariatur ex aliqua anim adipiscing amet proident. Laboris eu laborum magna lorem ipsum fugiat velit." + + +def complicated_page(): + return rx.hstack( + side_bar(), + rx.box( + rx.heading("Complicated Page", size="1"), + rx.text(LOREM_IPSUM), + ), + ) + + +@pytest.mark.benchmark +def test_component_init(): + complicated_page() diff --git a/docker-example/production-app-platform/Dockerfile b/docker-example/production-app-platform/Dockerfile index fec3b13f1..284c9eab8 100644 --- a/docker-example/production-app-platform/Dockerfile +++ b/docker-example/production-app-platform/Dockerfile @@ -27,7 +27,7 @@ FROM python:3.13 as init ARG uv=/root/.local/bin/uv -# Install `uv` for faster package boostrapping +# Install `uv` for faster package bootstrapping ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh RUN /install.sh && rm /install.sh @@ -52,7 +52,7 @@ 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 psycopg2 (skip if not using postgres). +# 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 diff --git a/docker-example/production-compose/Dockerfile b/docker-example/production-compose/Dockerfile index 757c03b8e..196e135a7 100644 --- a/docker-example/production-compose/Dockerfile +++ b/docker-example/production-compose/Dockerfile @@ -6,7 +6,7 @@ FROM python:3.13 as init ARG uv=/root/.local/bin/uv -# Install `uv` for faster package boostrapping +# Install `uv` for faster package bootstrapping ADD --chmod=755 https://astral.sh/uv/install.sh /install.sh RUN /install.sh && rm /install.sh @@ -39,7 +39,7 @@ 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 psycopg2 (skip if not using postgres). +# 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 diff --git a/docker-example/production-compose/compose.prod.yaml b/docker-example/production-compose/compose.prod.yaml index 225539515..7ed5b4eca 100644 --- a/docker-example/production-compose/compose.prod.yaml +++ b/docker-example/production-compose/compose.prod.yaml @@ -15,7 +15,7 @@ services: app: environment: - DB_URL: postgresql+psycopg2://postgres:secret@db/postgres + DB_URL: postgresql+psycopg://postgres:secret@db/postgres REDIS_URL: redis://redis:6379 depends_on: - db diff --git a/docs/de/README.md b/docs/de/README.md index 9931c24cc..6d2d69e94 100644 --- a/docs/de/README.md +++ b/docs/de/README.md @@ -34,7 +34,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.9+): +Öffne ein Terminal und führe den folgenden Befehl aus (benötigt Python 3.10+): ```bash pip install reflex diff --git a/docs/es/README.md b/docs/es/README.md index 15ce63335..538192e4b 100644 --- a/docs/es/README.md +++ b/docs/es/README.md @@ -35,7 +35,7 @@ Consulta nuestra [página de arquitectura](https://reflex.dev/blog/2024-03-21-re ## ⚙️ Instalación -Abra un terminal y ejecute (Requiere Python 3.9+): +Abra un terminal y ejecute (Requiere Python 3.10+): ```bash pip install reflex diff --git a/docs/in/README.md b/docs/in/README.md index ebc4155f4..81b1106ff 100644 --- a/docs/in/README.md +++ b/docs/in/README.md @@ -35,7 +35,7 @@ Reflex के अंदर के कामकाज को जानने क ## ⚙️ इंस्टॉलेशन (Installation) -एक टर्मिनल खोलें और चलाएं (Python 3.9+ की आवश्यकता है): +एक टर्मिनल खोलें और चलाएं (Python 3.10+ की आवश्यकता है): ```bash pip install reflex diff --git a/docs/it/README.md b/docs/it/README.md index 92438f696..cd6f24dd8 100644 --- a/docs/it/README.md +++ b/docs/it/README.md @@ -22,7 +22,7 @@ ## ⚙️ Installazione -Apri un terminale ed esegui (Richiede Python 3.9+): +Apri un terminale ed esegui (Richiede Python 3.10+): ```bash pip install reflex diff --git a/docs/ja/README.md b/docs/ja/README.md index 0a7ab0d53..941bef601 100644 --- a/docs/ja/README.md +++ b/docs/ja/README.md @@ -37,7 +37,7 @@ Reflex がどのように動作しているかを知るには、[アーキテク ## ⚙️ インストール -ターミナルを開いて以下のコマンドを実行してください。(Python 3.9 以上が必要です。): +ターミナルを開いて以下のコマンドを実行してください。(Python 3.10 以上が必要です。): ```bash pip install reflex diff --git a/docs/kr/README.md b/docs/kr/README.md index a92fcd0c5..57bb43794 100644 --- a/docs/kr/README.md +++ b/docs/kr/README.md @@ -20,7 +20,7 @@ --- ## ⚙️ 설치 -터미널을 열고 실행하세요. (Python 3.9+ 필요): +터미널을 열고 실행하세요. (Python 3.10+ 필요): ```bash pip install reflex diff --git a/docs/pe/README.md b/docs/pe/README.md index 3a0ba044b..867b543bc 100644 --- a/docs/pe/README.md +++ b/docs/pe/README.md @@ -34,7 +34,7 @@ ## ⚙️ Installation - نصب و راه اندازی -یک ترمینال را باز کنید و اجرا کنید (نیازمند Python 3.9+): +یک ترمینال را باز کنید و اجرا کنید (نیازمند Python 3.10+): ```bash pip install reflex diff --git a/docs/pt/pt_br/README.md b/docs/pt/pt_br/README.md index 184b668bc..8abfaebde 100644 --- a/docs/pt/pt_br/README.md +++ b/docs/pt/pt_br/README.md @@ -21,7 +21,7 @@ --- ## ⚙️ Instalação -Abra um terminal e execute (Requer Python 3.9+): +Abra um terminal e execute (Requer Python 3.10+): ```bash pip install reflex diff --git a/docs/tr/README.md b/docs/tr/README.md index 376547e01..afb8ae5b9 100644 --- a/docs/tr/README.md +++ b/docs/tr/README.md @@ -24,7 +24,7 @@ ## ⚙️ Kurulum -Bir terminal açın ve çalıştırın (Python 3.9+ gerekir): +Bir terminal açın ve çalıştırın (Python 3.10+ gerekir): ```bash pip install reflex diff --git a/docs/vi/README.md b/docs/vi/README.md index df7a31530..53fcad936 100644 --- a/docs/vi/README.md +++ b/docs/vi/README.md @@ -34,7 +34,7 @@ Các tính năng chính: ## ⚙️ Cài đặt -Mở cửa sổ lệnh và chạy (Yêu cầu Python phiên bản 3.9+): +Mở cửa sổ lệnh và chạy (Yêu cầu Python phiên bản 3.10+): ```bash pip install reflex diff --git a/docs/zh/zh_cn/README.md b/docs/zh/zh_cn/README.md index e114bc1e2..efaec0ca5 100644 --- a/docs/zh/zh_cn/README.md +++ b/docs/zh/zh_cn/README.md @@ -34,7 +34,7 @@ Reflex 是一个使用纯Python构建全栈web应用的库。 ## ⚙️ 安装 -打开一个终端并且运行(要求Python3.9+): +打开一个终端并且运行(要求Python3.10+): ```bash pip install reflex diff --git a/docs/zh/zh_tw/README.md b/docs/zh/zh_tw/README.md index 83f6b2ae2..6161e17d0 100644 --- a/docs/zh/zh_tw/README.md +++ b/docs/zh/zh_tw/README.md @@ -36,7 +36,7 @@ Reflex 是一個可以用純 Python 構建全端網頁應用程式的函式庫 ## ⚙️ 安裝 -開啟一個終端機並且執行 (需要 Python 3.9+): +開啟一個終端機並且執行 (需要 Python 3.10+): ```bash pip install reflex diff --git a/poetry.lock b/poetry.lock index aa826e4b0..f5007ee07 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,14 +1,16 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "alembic" -version = "1.14.0" +version = "1.14.1" description = "A database migration tool for SQLAlchemy." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "alembic-1.14.0-py3-none-any.whl", hash = "sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25"}, - {file = "alembic-1.14.0.tar.gz", hash = "sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b"}, + {file = "alembic-1.14.1-py3-none-any.whl", hash = "sha256:1acdd7a3a478e208b0503cd73614d5e4c6efafa4e73518bb60e4f2846a37b1c5"}, + {file = "alembic-1.14.1.tar.gz", hash = "sha256:496e888245a53adf1498fcab31713a469c65836f8de76e01399aa1c3e90dd213"}, ] [package.dependencies] @@ -17,7 +19,7 @@ SQLAlchemy = ">=1.3.0" typing-extensions = ">=4" [package.extras] -tz = ["backports.zoneinfo"] +tz = ["backports.zoneinfo", "tzdata"] [[package]] name = "annotated-types" @@ -25,6 +27,8 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, @@ -32,13 +36,15 @@ files = [ [[package]] name = "anyio" -version = "4.7.0" +version = "4.8.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, - {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, + {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, + {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, ] [package.dependencies] @@ -49,7 +55,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] @@ -58,6 +64,8 @@ version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_full_version < \"3.11.3\"" files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, @@ -69,6 +77,8 @@ version = "0.13.0" description = "Enhance the standard unittest package with features for testing asyncio libraries" optional = false python-versions = ">=3.5" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, @@ -76,19 +86,21 @@ files = [ [[package]] name = "attrs" -version = "24.2.0" +version = "25.1.0" description = "Classes Without Boilerplate" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, - {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, + {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, + {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, ] [package.extras] benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] @@ -99,6 +111,8 @@ version = "1.2.0" description = "Backport of CPython tarfile module" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and python_version <= \"3.11\"" files = [ {file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"}, {file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"}, @@ -114,6 +128,8 @@ version = "0.23.1" description = "The bidirectional mapping library for Python." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5"}, {file = "bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71"}, @@ -125,6 +141,8 @@ version = "1.2.2.post1" description = "A simple, correct Python build frontend" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5"}, {file = "build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7"}, @@ -146,13 +164,15 @@ virtualenv = ["virtualenv (>=20.0.35)"] [[package]] name = "certifi" -version = "2024.8.30" +version = "2025.1.31" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] [[package]] @@ -161,6 +181,7 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -230,6 +251,7 @@ files = [ {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] +markers = {main = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" and (python_version <= \"3.11\" or python_version >= \"3.12\")", dev = "python_version <= \"3.11\" or python_version >= \"3.12\""} [package.dependencies] pycparser = "*" @@ -240,6 +262,8 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -247,127 +271,118 @@ files = [ [[package]] name = "charset-normalizer" -version = "3.4.0" +version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, - {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, - {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] @@ -379,80 +394,84 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "(platform_system == \"Windows\" or os_name == \"nt\") and (python_version <= \"3.11\" or python_version >= \"3.12\")", dev = "(python_version <= \"3.11\" or python_version >= \"3.12\") and sys_platform == \"win32\""} [[package]] name = "coverage" -version = "7.6.9" +version = "7.6.10" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "coverage-7.6.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85d9636f72e8991a1706b2b55b06c27545448baf9f6dbf51c4004609aacd7dcb"}, - {file = "coverage-7.6.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:608a7fd78c67bee8936378299a6cb9f5149bb80238c7a566fc3e6717a4e68710"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96d636c77af18b5cb664ddf12dab9b15a0cfe9c0bde715da38698c8cea748bfa"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d75cded8a3cff93da9edc31446872d2997e327921d8eed86641efafd350e1df1"}, - {file = "coverage-7.6.9-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7b15f589593110ae767ce997775d645b47e5cbbf54fd322f8ebea6277466cec"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:44349150f6811b44b25574839b39ae35291f6496eb795b7366fef3bd3cf112d3"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d891c136b5b310d0e702e186d70cd16d1119ea8927347045124cb286b29297e5"}, - {file = "coverage-7.6.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:db1dab894cc139f67822a92910466531de5ea6034ddfd2b11c0d4c6257168073"}, - {file = "coverage-7.6.9-cp310-cp310-win32.whl", hash = "sha256:41ff7b0da5af71a51b53f501a3bac65fb0ec311ebed1632e58fc6107f03b9198"}, - {file = "coverage-7.6.9-cp310-cp310-win_amd64.whl", hash = "sha256:35371f8438028fdccfaf3570b31d98e8d9eda8bb1d6ab9473f5a390969e98717"}, - {file = "coverage-7.6.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:932fc826442132dde42ee52cf66d941f581c685a6313feebed358411238f60f9"}, - {file = "coverage-7.6.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:085161be5f3b30fd9b3e7b9a8c301f935c8313dcf928a07b116324abea2c1c2c"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ccc660a77e1c2bf24ddbce969af9447a9474790160cfb23de6be4fa88e3951c7"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c69e42c892c018cd3c8d90da61d845f50a8243062b19d228189b0224150018a9"}, - {file = "coverage-7.6.9-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0824a28ec542a0be22f60c6ac36d679e0e262e5353203bea81d44ee81fe9c6d4"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4401ae5fc52ad8d26d2a5d8a7428b0f0c72431683f8e63e42e70606374c311a1"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98caba4476a6c8d59ec1eb00c7dd862ba9beca34085642d46ed503cc2d440d4b"}, - {file = "coverage-7.6.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ee5defd1733fd6ec08b168bd4f5387d5b322f45ca9e0e6c817ea6c4cd36313e3"}, - {file = "coverage-7.6.9-cp311-cp311-win32.whl", hash = "sha256:f2d1ec60d6d256bdf298cb86b78dd715980828f50c46701abc3b0a2b3f8a0dc0"}, - {file = "coverage-7.6.9-cp311-cp311-win_amd64.whl", hash = "sha256:0d59fd927b1f04de57a2ba0137166d31c1a6dd9e764ad4af552912d70428c92b"}, - {file = "coverage-7.6.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e266ae0b5d15f1ca8d278a668df6f51cc4b854513daab5cae695ed7b721cf8"}, - {file = "coverage-7.6.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9901d36492009a0a9b94b20e52ebfc8453bf49bb2b27bca2c9706f8b4f5a554a"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abd3e72dd5b97e3af4246cdada7738ef0e608168de952b837b8dd7e90341f015"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff74026a461eb0660366fb01c650c1d00f833a086b336bdad7ab00cc952072b3"}, - {file = "coverage-7.6.9-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65dad5a248823a4996724a88eb51d4b31587aa7aa428562dbe459c684e5787ae"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:22be16571504c9ccea919fcedb459d5ab20d41172056206eb2994e2ff06118a4"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f957943bc718b87144ecaee70762bc2bc3f1a7a53c7b861103546d3a403f0a6"}, - {file = "coverage-7.6.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ae1387db4aecb1f485fb70a6c0148c6cdaebb6038f1d40089b1fc84a5db556f"}, - {file = "coverage-7.6.9-cp312-cp312-win32.whl", hash = "sha256:1a330812d9cc7ac2182586f6d41b4d0fadf9be9049f350e0efb275c8ee8eb692"}, - {file = "coverage-7.6.9-cp312-cp312-win_amd64.whl", hash = "sha256:b12c6b18269ca471eedd41c1b6a1065b2f7827508edb9a7ed5555e9a56dcfc97"}, - {file = "coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664"}, - {file = "coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00"}, - {file = "coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077"}, - {file = "coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb"}, - {file = "coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba"}, - {file = "coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1"}, - {file = "coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419"}, - {file = "coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae"}, - {file = "coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e"}, - {file = "coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9"}, - {file = "coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b"}, - {file = "coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611"}, - {file = "coverage-7.6.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:adb697c0bd35100dc690de83154627fbab1f4f3c0386df266dded865fc50a902"}, - {file = "coverage-7.6.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:be57b6d56e49c2739cdf776839a92330e933dd5e5d929966fbbd380c77f060be"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1592791f8204ae9166de22ba7e6705fa4ebd02936c09436a1bb85aabca3e599"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e12ae8cc979cf83d258acb5e1f1cf2f3f83524d1564a49d20b8bec14b637f08"}, - {file = "coverage-7.6.9-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb5555cff66c4d3d6213a296b360f9e1a8e323e74e0426b6c10ed7f4d021e464"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9389a429e0e5142e69d5bf4a435dd688c14478a19bb901735cdf75e57b13845"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:592ac539812e9b46046620341498caf09ca21023c41c893e1eb9dbda00a70cbf"}, - {file = "coverage-7.6.9-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a27801adef24cc30871da98a105f77995e13a25a505a0161911f6aafbd66e678"}, - {file = "coverage-7.6.9-cp39-cp39-win32.whl", hash = "sha256:8e3c3e38930cfb729cb8137d7f055e5a473ddaf1217966aa6238c88bd9fd50e6"}, - {file = "coverage-7.6.9-cp39-cp39-win_amd64.whl", hash = "sha256:e28bf44afa2b187cc9f41749138a64435bf340adfcacb5b2290c070ce99839d4"}, - {file = "coverage-7.6.9-pp39.pp310-none-any.whl", hash = "sha256:f3ca78518bc6bc92828cd11867b121891d75cae4ea9e908d72030609b996db1b"}, - {file = "coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"}, + {file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"}, + {file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"}, + {file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"}, + {file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"}, + {file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"}, + {file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"}, + {file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"}, + {file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"}, + {file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"}, + {file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"}, + {file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"}, + {file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"}, + {file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"}, + {file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"}, + {file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"}, + {file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"}, + {file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"}, + {file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"}, + {file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"}, + {file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"}, + {file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"}, + {file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"}, + {file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"}, + {file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"}, + {file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"}, + {file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"}, + {file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"}, + {file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"}, + {file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"}, + {file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"}, + {file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"}, + {file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"}, ] [package.dependencies] @@ -463,51 +482,53 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "43.0.3" +version = "44.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = ">=3.7" +python-versions = "!=3.9.0,!=3.9.1,>=3.7" +groups = ["main"] +markers = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" files = [ - {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, - {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, - {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, - {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, - {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, - {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, - {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, + {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, + {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, + {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"}, + {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, + {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, + {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"}, + {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"}, ] [package.dependencies] cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] -nox = ["nox"] -pep8test = ["check-sdist", "click", "mypy", "ruff"] -sdist = ["build"] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] @@ -516,6 +537,8 @@ version = "1.8.1" description = "A utility for ensuring Google-style docstrings stay up to date with the source code." optional = false python-versions = ">=3.6,<4.0" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "darglint-1.8.1-py3-none-any.whl", hash = "sha256:5ae11c259c17b0701618a20c3da343a3eb98b3bc4b5a83d31cdd94f5ebdced8d"}, {file = "darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da"}, @@ -527,6 +550,8 @@ version = "0.3.9" description = "serialize all of Python" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, @@ -542,6 +567,8 @@ version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, @@ -553,6 +580,8 @@ version = "1.9.0" description = "Distro - an OS platform information API" optional = false python-versions = ">=3.6" +groups = ["main"] +markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and sys_platform == \"linux\"" files = [ {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, @@ -564,6 +593,8 @@ version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, @@ -575,6 +606,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version < \"3.11\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -585,38 +618,42 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.115.6" +version = "0.115.8" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "fastapi-0.115.6-py3-none-any.whl", hash = "sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305"}, - {file = "fastapi-0.115.6.tar.gz", hash = "sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654"}, + {file = "fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf"}, + {file = "fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9"}, ] [package.dependencies] pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.40.0,<0.42.0" +starlette = ">=0.40.0,<0.46.0" typing-extensions = ">=4.8.0" [package.extras] -all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "filelock" -version = "3.16.1" +version = "3.17.0" description = "A platform independent file lock." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"}, - {file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"}, + {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, + {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, ] [package.extras] -docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] typing = ["typing-extensions (>=4.12.2)"] [[package]] @@ -625,6 +662,7 @@ version = "3.1.1" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, @@ -700,6 +738,7 @@ files = [ {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"}, {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, ] +markers = {main = "(python_version <= \"3.11\" or python_version >= \"3.12\") and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\") and python_version < \"3.14\"", dev = "python_version <= \"3.11\" or python_version >= \"3.12\""} [package.extras] docs = ["Sphinx", "furo"] @@ -711,6 +750,8 @@ version = "23.0.0" description = "WSGI HTTP Server for UNIX" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, @@ -732,6 +773,8 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -743,6 +786,8 @@ version = "1.0.7" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, @@ -764,6 +809,8 @@ version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, @@ -782,15 +829,38 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "id" +version = "1.5.0" +description = "A tool for generating OIDC identities" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658"}, + {file = "id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d"}, +] + +[package.dependencies] +requests = "*" + +[package.extras] +dev = ["build", "bump (>=1.3.2)", "id[lint,test]"] +lint = ["bandit", "interrogate", "mypy", "ruff (<0.8.2)", "types-requests"] +test = ["coverage[toml]", "pretend", "pytest", "pytest-cov"] + [[package]] name = "identify" -version = "2.6.3" +version = "2.6.6" description = "File identification library for Python" optional = false python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "identify-2.6.3-py2.py3-none-any.whl", hash = "sha256:9edba65473324c2ea9684b1f944fe3191db3345e50b6d04571d10ed164f8d7bd"}, - {file = "identify-2.6.3.tar.gz", hash = "sha256:62f5dae9b5fef52c84cc188514e9ea4f3f636b1d8799ab5ebc475471f9e47a02"}, + {file = "identify-2.6.6-py2.py3-none-any.whl", hash = "sha256:cbd1810bce79f8b671ecb20f53ee0ae8e86ae84b557de31d89709dc2a48ba881"}, + {file = "identify-2.6.6.tar.gz", hash = "sha256:7bec12768ed44ea4761efb47806f0a41f86e7c0a5fdf5950d4648c90eca7e251"}, ] [package.extras] @@ -802,6 +872,8 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -812,13 +884,15 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "importlib-metadata" -version = "8.5.0" +version = "8.6.1" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] +markers = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and python_version <= \"3.11\" or python_full_version < \"3.10.2\"" files = [ - {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, - {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, + {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, + {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, ] [package.dependencies] @@ -830,7 +904,7 @@ cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] @@ -839,6 +913,8 @@ version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -850,6 +926,8 @@ version = "3.4.0" description = "Utility functions for Python class constructs" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and (platform_machine != \"ppc64le\" and platform_machine != \"s390x\")" files = [ {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, @@ -868,6 +946,8 @@ version = "6.0.1" description = "Useful decorators and context managers" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and (platform_machine != \"ppc64le\" and platform_machine != \"s390x\")" files = [ {file = "jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4"}, {file = "jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3"}, @@ -886,6 +966,8 @@ version = "4.1.0" description = "Functools like those found in stdlib" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and (platform_machine != \"ppc64le\" and platform_machine != \"s390x\")" files = [ {file = "jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649"}, {file = "jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d"}, @@ -908,6 +990,8 @@ version = "0.8.0" description = "Low-level, pure Python DBus protocol wrapper." optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" files = [ {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, @@ -919,13 +1003,15 @@ trio = ["async_generator", "trio"] [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, ] [package.dependencies] @@ -936,17 +1022,19 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "keyring" -version = "25.5.0" +version = "25.6.0" description = "Store and access your passwords safely." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] +markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and (platform_machine != \"ppc64le\" and platform_machine != \"s390x\")" files = [ - {file = "keyring-25.5.0-py3-none-any.whl", hash = "sha256:e67f8ac32b04be4714b42fe84ce7dad9c40985b9ca827c592cc303e7c26d9741"}, - {file = "keyring-25.5.0.tar.gz", hash = "sha256:4c753b3ec91717fe713c4edd522d625889d8973a349b0e582622f49766de58e6"}, + {file = "keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd"}, + {file = "keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66"}, ] [package.dependencies] -importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +importlib_metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} "jaraco.classes" = "*" "jaraco.context" = "*" "jaraco.functools" = "*" @@ -969,6 +1057,8 @@ version = "0.4" description = "Makes it easy to load subpackages and functions on demand." optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc"}, {file = "lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1"}, @@ -988,6 +1078,8 @@ version = "1.3.8" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "Mako-1.3.8-py3-none-any.whl", hash = "sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627"}, {file = "mako-1.3.8.tar.gz", hash = "sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8"}, @@ -1007,6 +1099,8 @@ version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -1031,6 +1125,8 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -1101,6 +1197,8 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -1108,45 +1206,50 @@ files = [ [[package]] name = "more-itertools" -version = "10.5.0" +version = "10.6.0" description = "More routines for operating on iterables, beyond itertools" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] +markers = "(python_version <= \"3.11\" or python_version >= \"3.12\") and (platform_machine != \"ppc64le\" and platform_machine != \"s390x\")" files = [ - {file = "more-itertools-10.5.0.tar.gz", hash = "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6"}, - {file = "more_itertools-10.5.0-py3-none-any.whl", hash = "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef"}, + {file = "more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b"}, + {file = "more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89"}, ] [[package]] name = "nh3" -version = "0.2.19" -description = "Python bindings to the ammonia HTML sanitization library." +version = "0.2.20" +description = "Python binding to Ammonia HTML sanitizer Rust crate" optional = false -python-versions = "*" +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "nh3-0.2.19-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ec9c8bf86e397cb88c560361f60fdce478b5edb8b93f04ead419b72fbe937ea6"}, - {file = "nh3-0.2.19-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0adf00e2b2026fa10a42537b60d161e516f206781c7515e4e97e09f72a8c5d0"}, - {file = "nh3-0.2.19-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3805161c4e12088bd74752ba69630e915bc30fe666034f47217a2f16b16efc37"}, - {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3dedd7858a21312f7675841529941035a2ac91057db13402c8fe907aa19205a"}, - {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:0b6820fc64f2ff7ef3e7253a093c946a87865c877b3889149a6d21d322ed8dbd"}, - {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:833b3b5f1783ce95834a13030300cea00cbdfd64ea29260d01af9c4821da0aa9"}, - {file = "nh3-0.2.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5d4f5e2189861b352b73acb803b5f4bb409c2f36275d22717e27d4e0c217ae55"}, - {file = "nh3-0.2.19-cp313-cp313t-win32.whl", hash = "sha256:2b926f179eb4bce72b651bfdf76f8aa05d167b2b72bc2f3657fd319f40232adc"}, - {file = "nh3-0.2.19-cp313-cp313t-win_amd64.whl", hash = "sha256:ac536a4b5c073fdadd8f5f4889adabe1cbdae55305366fb870723c96ca7f49c3"}, - {file = "nh3-0.2.19-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c2e3f0d18cc101132fe10ab7ef5c4f41411297e639e23b64b5e888ccaad63f41"}, - {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11270b16c1b012677e3e2dd166c1aa273388776bf99a3e3677179db5097ee16a"}, - {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc483dd8d20f8f8c010783a25a84db3bebeadced92d24d34b40d687f8043ac69"}, - {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d53a4577b6123ca1d7e8483fad3e13cb7eda28913d516bd0a648c1a473aa21a9"}, - {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdb20740d24ab9f2a1341458a00a11205294e97e905de060eeab1ceca020c09c"}, - {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8325d51e47cb5b11f649d55e626d56c76041ba508cd59e0cb1cf687cc7612f1"}, - {file = "nh3-0.2.19-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8eb7affc590e542fa7981ef508cd1644f62176bcd10d4429890fc629b47f0bc"}, - {file = "nh3-0.2.19-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2eb021804e9df1761abeb844bb86648d77aa118a663c82f50ea04110d87ed707"}, - {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a7b928862daddb29805a1010a0282f77f4b8b238a37b5f76bc6c0d16d930fd22"}, - {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed06ed78f6b69d57463b46a04f68f270605301e69d80756a8adf7519002de57d"}, - {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:df8eac98fec80bd6f5fd0ae27a65de14f1e1a65a76d8e2237eb695f9cd1121d9"}, - {file = "nh3-0.2.19-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00810cd5275f5c3f44b9eb0e521d1a841ee2f8023622de39ffc7d88bd533d8e0"}, - {file = "nh3-0.2.19-cp38-abi3-win32.whl", hash = "sha256:7e98621856b0a911c21faa5eef8f8ea3e691526c2433f9afc2be713cb6fbdb48"}, - {file = "nh3-0.2.19-cp38-abi3-win_amd64.whl", hash = "sha256:75c7cafb840f24430b009f7368945cb5ca88b2b54bb384ebfba495f16bc9c121"}, + {file = "nh3-0.2.20-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e1061a4ab6681f6bdf72b110eea0c4e1379d57c9de937db3be4202f7ad6043db"}, + {file = "nh3-0.2.20-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb4254b1dac4a1ee49919a5b3f1caf9803ea8dada1816d9e8289e63d3cd0dd9a"}, + {file = "nh3-0.2.20-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ae9cbd713524cdb81e64663d0d6aae26f678db9f2cd9db0bf162606f1f9f20c"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1f7370b4e14cc03f5ae141ef30a1caf81fa5787711f80be9081418dd9eb79d2"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:ac4d27dc836a476efffc6eb661994426b8b805c951b29c9cf2ff36bc9ad58bc5"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4fd2e9248725ebcedac3997a8d3da0d90a12a28c9179c6ba51f1658938ac30d0"}, + {file = "nh3-0.2.20-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f7d564871833ddbe54df3aa59053b1110729d3a800cb7628ae8f42adb3d75208"}, + {file = "nh3-0.2.20-cp313-cp313t-win32.whl", hash = "sha256:d2a176fd4306b6f0f178a3f67fac91bd97a3a8d8fafb771c9b9ef675ba5c8886"}, + {file = "nh3-0.2.20-cp313-cp313t-win_amd64.whl", hash = "sha256:6ed834c68452a600f517dd3e1534dbfaff1f67f98899fecf139a055a25d99150"}, + {file = "nh3-0.2.20-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:76e2f603b30c02ff6456b233a83fc377dedab6a50947b04e960a6b905637b776"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:181063c581defe683bd4bb78188ac9936d208aebbc74c7f7c16b6a32ae2ebb38"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:231addb7643c952cd6d71f1c8702d703f8fe34afcb20becb3efb319a501a12d7"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1b9a8340a0aab991c68a5ca938d35ef4a8a3f4bf1b455da8855a40bee1fa0ace"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10317cd96fe4bbd4eb6b95f3920b71c902157ad44fed103fdcde43e3b8ee8be6"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8698db4c04b140800d1a1cd3067fda399e36e1e2b8fc1fe04292a907350a3e9b"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eb04b9c3deb13c3a375ea39fd4a3c00d1f92e8fb2349f25f1e3e4506751774b"}, + {file = "nh3-0.2.20-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92f3f1c4f47a2c6f3ca7317b1d5ced05bd29556a75d3a4e2715652ae9d15c05d"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ddefa9fd6794a87e37d05827d299d4b53a3ec6f23258101907b96029bfef138a"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ce3731c8f217685d33d9268362e5b4f770914e922bba94d368ab244a59a6c397"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:09f037c02fc2c43b211ff1523de32801dcfb0918648d8e651c36ef890f1731ec"}, + {file = "nh3-0.2.20-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:813f1c8012dd64c990514b795508abb90789334f76a561fa0fd4ca32d2275330"}, + {file = "nh3-0.2.20-cp38-abi3-win32.whl", hash = "sha256:47b2946c0e13057855209daeffb45dc910bd0c55daf10190bb0b4b60e2999784"}, + {file = "nh3-0.2.20-cp38-abi3-win_amd64.whl", hash = "sha256:da87573f03084edae8eb87cfe811ec338606288f81d333c07d2a9a0b9b976c0b"}, + {file = "nh3-0.2.20.tar.gz", hash = "sha256:9705c42d7ff88a0bea546c82d7fe5e59135e3d3f057e485394f491248a1f8ed5"}, ] [[package]] @@ -1155,6 +1258,8 @@ version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, @@ -1162,120 +1267,68 @@ files = [ [[package]] name = "numpy" -version = "2.0.2" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.9" -files = [ - {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}, - {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}, - {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}, - {file = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}, - {file = "numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}, - {file = "numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}, - {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}, - {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}, - {file = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}, - {file = "numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}, - {file = "numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c"}, - {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692"}, - {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c"}, - {file = "numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded"}, - {file = "numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5"}, - {file = "numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}, - {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1"}, - {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d"}, - {file = "numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d"}, - {file = "numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa"}, - {file = "numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}, - {file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}, - {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, -] - -[[package]] -name = "numpy" -version = "2.2.0" +version = "2.2.2" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "numpy-2.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1e25507d85da11ff5066269d0bd25d06e0a0f2e908415534f3e603d2a78e4ffa"}, - {file = "numpy-2.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a62eb442011776e4036af5c8b1a00b706c5bc02dc15eb5344b0c750428c94219"}, - {file = "numpy-2.2.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:b606b1aaf802e6468c2608c65ff7ece53eae1a6874b3765f69b8ceb20c5fa78e"}, - {file = "numpy-2.2.0-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:36b2b43146f646642b425dd2027730f99bac962618ec2052932157e213a040e9"}, - {file = "numpy-2.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fe8f3583e0607ad4e43a954e35c1748b553bfe9fdac8635c02058023277d1b3"}, - {file = "numpy-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:122fd2fcfafdefc889c64ad99c228d5a1f9692c3a83f56c292618a59aa60ae83"}, - {file = "numpy-2.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3f2f5cddeaa4424a0a118924b988746db6ffa8565e5829b1841a8a3bd73eb59a"}, - {file = "numpy-2.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7fe4bb0695fe986a9e4deec3b6857003b4cfe5c5e4aac0b95f6a658c14635e31"}, - {file = "numpy-2.2.0-cp310-cp310-win32.whl", hash = "sha256:b30042fe92dbd79f1ba7f6898fada10bdaad1847c44f2dff9a16147e00a93661"}, - {file = "numpy-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dc1d6d66f8d37843ed281773c7174f03bf7ad826523f73435deb88ba60d2d4"}, - {file = "numpy-2.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9874bc2ff574c40ab7a5cbb7464bf9b045d617e36754a7bc93f933d52bd9ffc6"}, - {file = "numpy-2.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0da8495970f6b101ddd0c38ace92edea30e7e12b9a926b57f5fabb1ecc25bb90"}, - {file = "numpy-2.2.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0557eebc699c1c34cccdd8c3778c9294e8196df27d713706895edc6f57d29608"}, - {file = "numpy-2.2.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:3579eaeb5e07f3ded59298ce22b65f877a86ba8e9fe701f5576c99bb17c283da"}, - {file = "numpy-2.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40deb10198bbaa531509aad0cd2f9fadb26c8b94070831e2208e7df543562b74"}, - {file = "numpy-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c2aed8fcf8abc3020d6a9ccb31dbc9e7d7819c56a348cc88fd44be269b37427e"}, - {file = "numpy-2.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a222d764352c773aa5ebde02dd84dba3279c81c6db2e482d62a3fa54e5ece69b"}, - {file = "numpy-2.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4e58666988605e251d42c2818c7d3d8991555381be26399303053b58a5bbf30d"}, - {file = "numpy-2.2.0-cp311-cp311-win32.whl", hash = "sha256:4723a50e1523e1de4fccd1b9a6dcea750c2102461e9a02b2ac55ffeae09a4410"}, - {file = "numpy-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:16757cf28621e43e252c560d25b15f18a2f11da94fea344bf26c599b9cf54b73"}, - {file = "numpy-2.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cff210198bb4cae3f3c100444c5eaa573a823f05c253e7188e1362a5555235b3"}, - {file = "numpy-2.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58b92a5828bd4d9aa0952492b7de803135038de47343b2aa3cc23f3b71a3dc4e"}, - {file = "numpy-2.2.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:ebe5e59545401fbb1b24da76f006ab19734ae71e703cdb4a8b347e84a0cece67"}, - {file = "numpy-2.2.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e2b8cd48a9942ed3f85b95ca4105c45758438c7ed28fff1e4ce3e57c3b589d8e"}, - {file = "numpy-2.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57fcc997ffc0bef234b8875a54d4058afa92b0b0c4223fc1f62f24b3b5e86038"}, - {file = "numpy-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ad7d11b309bd132d74397fcf2920933c9d1dc865487128f5c03d580f2c3d03"}, - {file = "numpy-2.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cb24cca1968b21355cc6f3da1a20cd1cebd8a023e3c5b09b432444617949085a"}, - {file = "numpy-2.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0798b138c291d792f8ea40fe3768610f3c7dd2574389e37c3f26573757c8f7ef"}, - {file = "numpy-2.2.0-cp312-cp312-win32.whl", hash = "sha256:afe8fb968743d40435c3827632fd36c5fbde633b0423da7692e426529b1759b1"}, - {file = "numpy-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:3a4199f519e57d517ebd48cb76b36c82da0360781c6a0353e64c0cac30ecaad3"}, - {file = "numpy-2.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f8c8b141ef9699ae777c6278b52c706b653bf15d135d302754f6b2e90eb30367"}, - {file = "numpy-2.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f0986e917aca18f7a567b812ef7ca9391288e2acb7a4308aa9d265bd724bdae"}, - {file = "numpy-2.2.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:1c92113619f7b272838b8d6702a7f8ebe5edea0df48166c47929611d0b4dea69"}, - {file = "numpy-2.2.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5a145e956b374e72ad1dff82779177d4a3c62bc8248f41b80cb5122e68f22d13"}, - {file = "numpy-2.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18142b497d70a34b01642b9feabb70156311b326fdddd875a9981f34a369b671"}, - {file = "numpy-2.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7d41d1612c1a82b64697e894b75db6758d4f21c3ec069d841e60ebe54b5b571"}, - {file = "numpy-2.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a98f6f20465e7618c83252c02041517bd2f7ea29be5378f09667a8f654a5918d"}, - {file = "numpy-2.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e09d40edfdb4e260cb1567d8ae770ccf3b8b7e9f0d9b5c2a9992696b30ce2742"}, - {file = "numpy-2.2.0-cp313-cp313-win32.whl", hash = "sha256:3905a5fffcc23e597ee4d9fb3fcd209bd658c352657548db7316e810ca80458e"}, - {file = "numpy-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a184288538e6ad699cbe6b24859206e38ce5fba28f3bcfa51c90d0502c1582b2"}, - {file = "numpy-2.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7832f9e8eb00be32f15fdfb9a981d6955ea9adc8574c521d48710171b6c55e95"}, - {file = "numpy-2.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f0dd071b95bbca244f4cb7f70b77d2ff3aaaba7fa16dc41f58d14854a6204e6c"}, - {file = "numpy-2.2.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b0b227dcff8cdc3efbce66d4e50891f04d0a387cce282fe1e66199146a6a8fca"}, - {file = "numpy-2.2.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6ab153263a7c5ccaf6dfe7e53447b74f77789f28ecb278c3b5d49db7ece10d6d"}, - {file = "numpy-2.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e500aba968a48e9019e42c0c199b7ec0696a97fa69037bea163b55398e390529"}, - {file = "numpy-2.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:440cfb3db4c5029775803794f8638fbdbf71ec702caf32735f53b008e1eaece3"}, - {file = "numpy-2.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a55dc7a7f0b6198b07ec0cd445fbb98b05234e8b00c5ac4874a63372ba98d4ab"}, - {file = "numpy-2.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4bddbaa30d78c86329b26bd6aaaea06b1e47444da99eddac7bf1e2fab717bd72"}, - {file = "numpy-2.2.0-cp313-cp313t-win32.whl", hash = "sha256:30bf971c12e4365153afb31fc73f441d4da157153f3400b82db32d04de1e4066"}, - {file = "numpy-2.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d35717333b39d1b6bb8433fa758a55f1081543de527171543a2b710551d40881"}, - {file = "numpy-2.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e12c6c1ce84628c52d6367863773f7c8c8241be554e8b79686e91a43f1733773"}, - {file = "numpy-2.2.0-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:b6207dc8fb3c8cb5668e885cef9ec7f70189bec4e276f0ff70d5aa078d32c88e"}, - {file = "numpy-2.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a50aeff71d0f97b6450d33940c7181b08be1441c6c193e678211bff11aa725e7"}, - {file = "numpy-2.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:df12a1f99b99f569a7c2ae59aa2d31724e8d835fc7f33e14f4792e3071d11221"}, - {file = "numpy-2.2.0.tar.gz", hash = "sha256:140dd80ff8981a583a60980be1a655068f8adebf7a45a06a6858c873fcdcd4a0"}, + {file = "numpy-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7079129b64cb78bdc8d611d1fd7e8002c0a2565da6a47c4df8062349fee90e3e"}, + {file = "numpy-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ec6c689c61df613b783aeb21f945c4cbe6c51c28cb70aae8430577ab39f163e"}, + {file = "numpy-2.2.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:40c7ff5da22cd391944a28c6a9c638a5eef77fcf71d6e3a79e1d9d9e82752715"}, + {file = "numpy-2.2.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:995f9e8181723852ca458e22de5d9b7d3ba4da3f11cc1cb113f093b271d7965a"}, + {file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b78ea78450fd96a498f50ee096f69c75379af5138f7881a51355ab0e11286c97"}, + {file = "numpy-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fbe72d347fbc59f94124125e73fc4976a06927ebc503ec5afbfb35f193cd957"}, + {file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8e6da5cffbbe571f93588f562ed130ea63ee206d12851b60819512dd3e1ba50d"}, + {file = "numpy-2.2.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09d6a2032faf25e8d0cadde7fd6145118ac55d2740132c1d845f98721b5ebcfd"}, + {file = "numpy-2.2.2-cp310-cp310-win32.whl", hash = "sha256:159ff6ee4c4a36a23fe01b7c3d07bd8c14cc433d9720f977fcd52c13c0098160"}, + {file = "numpy-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:64bd6e1762cd7f0986a740fee4dff927b9ec2c5e4d9a28d056eb17d332158014"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:642199e98af1bd2b6aeb8ecf726972d238c9877b0f6e8221ee5ab945ec8a2189"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d9fc9d812c81e6168b6d405bf00b8d6739a7f72ef22a9214c4241e0dc70b323"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:c7d1fd447e33ee20c1f33f2c8e6634211124a9aabde3c617687d8b739aa69eac"}, + {file = "numpy-2.2.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:451e854cfae0febe723077bd0cf0a4302a5d84ff25f0bfece8f29206c7bed02e"}, + {file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd249bc894af67cbd8bad2c22e7cbcd46cf87ddfca1f1289d1e7e54868cc785c"}, + {file = "numpy-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02935e2c3c0c6cbe9c7955a8efa8908dd4221d7755644c59d1bba28b94fd334f"}, + {file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a972cec723e0563aa0823ee2ab1df0cb196ed0778f173b381c871a03719d4826"}, + {file = "numpy-2.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d6d6a0910c3b4368d89dde073e630882cdb266755565155bc33520283b2d9df8"}, + {file = "numpy-2.2.2-cp311-cp311-win32.whl", hash = "sha256:860fd59990c37c3ef913c3ae390b3929d005243acca1a86facb0773e2d8d9e50"}, + {file = "numpy-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:da1eeb460ecce8d5b8608826595c777728cdf28ce7b5a5a8c8ac8d949beadcf2"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ac9bea18d6d58a995fac1b2cb4488e17eceeac413af014b1dd26170b766d8467"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23ae9f0c2d889b7b2d88a3791f6c09e2ef827c2446f1c4a3e3e76328ee4afd9a"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3074634ea4d6df66be04f6728ee1d173cfded75d002c75fac79503a880bf3825"}, + {file = "numpy-2.2.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ec0636d3f7d68520afc6ac2dc4b8341ddb725039de042faf0e311599f54eb37"}, + {file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ffbb1acd69fdf8e89dd60ef6182ca90a743620957afb7066385a7bbe88dc748"}, + {file = "numpy-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0349b025e15ea9d05c3d63f9657707a4e1d471128a3b1d876c095f328f8ff7f0"}, + {file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:463247edcee4a5537841d5350bc87fe8e92d7dd0e8c71c995d2c6eecb8208278"}, + {file = "numpy-2.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9dd47ff0cb2a656ad69c38da850df3454da88ee9a6fde0ba79acceee0e79daba"}, + {file = "numpy-2.2.2-cp312-cp312-win32.whl", hash = "sha256:4525b88c11906d5ab1b0ec1f290996c0020dd318af8b49acaa46f198b1ffc283"}, + {file = "numpy-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:5acea83b801e98541619af398cc0109ff48016955cc0818f478ee9ef1c5c3dcb"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b208cfd4f5fe34e1535c08983a1a6803fdbc7a1e86cf13dd0c61de0b51a0aadc"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d0bbe7dd86dca64854f4b6ce2ea5c60b51e36dfd597300057cf473d3615f2369"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:22ea3bb552ade325530e72a0c557cdf2dea8914d3a5e1fecf58fa5dbcc6f43cd"}, + {file = "numpy-2.2.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:128c41c085cab8a85dc29e66ed88c05613dccf6bc28b3866cd16050a2f5448be"}, + {file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:250c16b277e3b809ac20d1f590716597481061b514223c7badb7a0f9993c7f84"}, + {file = "numpy-2.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0c8854b09bc4de7b041148d8550d3bd712b5c21ff6a8ed308085f190235d7ff"}, + {file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b6fb9c32a91ec32a689ec6410def76443e3c750e7cfc3fb2206b985ffb2b85f0"}, + {file = "numpy-2.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:57b4012e04cc12b78590a334907e01b3a85efb2107df2b8733ff1ed05fce71de"}, + {file = "numpy-2.2.2-cp313-cp313-win32.whl", hash = "sha256:4dbd80e453bd34bd003b16bd802fac70ad76bd463f81f0c518d1245b1c55e3d9"}, + {file = "numpy-2.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:5a8c863ceacae696aff37d1fd636121f1a512117652e5dfb86031c8d84836369"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b3482cb7b3325faa5f6bc179649406058253d91ceda359c104dac0ad320e1391"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9491100aba630910489c1d0158034e1c9a6546f0b1340f716d522dc103788e39"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:41184c416143defa34cc8eb9d070b0a5ba4f13a0fa96a709e20584638254b317"}, + {file = "numpy-2.2.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7dca87ca328f5ea7dafc907c5ec100d187911f94825f8700caac0b3f4c384b49"}, + {file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bc61b307655d1a7f9f4b043628b9f2b721e80839914ede634e3d485913e1fb2"}, + {file = "numpy-2.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fad446ad0bc886855ddf5909cbf8cb5d0faa637aaa6277fb4b19ade134ab3c7"}, + {file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:149d1113ac15005652e8d0d3f6fd599360e1a708a4f98e43c9c77834a28238cb"}, + {file = "numpy-2.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:106397dbbb1896f99e044efc90360d098b3335060375c26aa89c0d8a97c5f648"}, + {file = "numpy-2.2.2-cp313-cp313t-win32.whl", hash = "sha256:0eec19f8af947a61e968d5429f0bd92fec46d92b0008d0a6685b40d6adf8a4f4"}, + {file = "numpy-2.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:97b974d3ba0fb4612b77ed35d7627490e8e3dff56ab41454d9e8b23448940576"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b0531f0b0e07643eb089df4c509d30d72c9ef40defa53e41363eca8a8cc61495"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:e9e82dcb3f2ebbc8cb5ce1102d5f1c5ed236bf8a11730fb45ba82e2841ec21df"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0d4142eb40ca6f94539e4db929410f2a46052a0fe7a2c1c59f6179c39938d2a"}, + {file = "numpy-2.2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:356ca982c188acbfa6af0d694284d8cf20e95b1c3d0aefa8929376fea9146f60"}, + {file = "numpy-2.2.2.tar.gz", hash = "sha256:ed6906f61834d687738d25988ae117683705636936cc605be0bb208b23df4d8f"}, ] [[package]] @@ -1284,6 +1337,8 @@ version = "1.3.0.post0" description = "Capture the outcome of Python function calls." optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, @@ -1298,6 +1353,8 @@ version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, @@ -1309,6 +1366,8 @@ version = "2.2.3" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, @@ -1391,145 +1450,102 @@ xml = ["lxml (>=4.9.2)"] [[package]] name = "pillow" -version = "11.0.0" +version = "11.1.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"}, - {file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488"}, - {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f"}, - {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb"}, - {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97"}, - {file = "pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50"}, - {file = "pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c"}, - {file = "pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1"}, - {file = "pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc"}, - {file = "pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b"}, - {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa"}, - {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306"}, - {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9"}, - {file = "pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5"}, - {file = "pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291"}, - {file = "pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9"}, - {file = "pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923"}, - {file = "pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"}, - {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7"}, - {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6"}, - {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc"}, - {file = "pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6"}, - {file = "pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47"}, - {file = "pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25"}, - {file = "pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699"}, - {file = "pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38"}, - {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2"}, - {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2"}, - {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527"}, - {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa"}, - {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f"}, - {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb"}, - {file = "pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798"}, - {file = "pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de"}, - {file = "pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84"}, - {file = "pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b"}, - {file = "pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003"}, - {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2"}, - {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a"}, - {file = "pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8"}, - {file = "pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8"}, - {file = "pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904"}, - {file = "pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3"}, - {file = "pillow-11.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba"}, - {file = "pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7"}, - {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e"}, - {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f"}, - {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae"}, - {file = "pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4"}, - {file = "pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd"}, - {file = "pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734"}, - {file = "pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790"}, - {file = "pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944"}, - {file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"}, + {file = "pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8"}, + {file = "pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482"}, + {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e"}, + {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269"}, + {file = "pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49"}, + {file = "pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a"}, + {file = "pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65"}, + {file = "pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457"}, + {file = "pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1"}, + {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2"}, + {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96"}, + {file = "pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f"}, + {file = "pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"}, + {file = "pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71"}, + {file = "pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a"}, + {file = "pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f"}, + {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91"}, + {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c"}, + {file = "pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6"}, + {file = "pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf"}, + {file = "pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5"}, + {file = "pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc"}, + {file = "pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114"}, + {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352"}, + {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3"}, + {file = "pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9"}, + {file = "pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c"}, + {file = "pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65"}, + {file = "pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861"}, + {file = "pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081"}, + {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c"}, + {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547"}, + {file = "pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab"}, + {file = "pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9"}, + {file = "pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe"}, + {file = "pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756"}, + {file = "pillow-11.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6"}, + {file = "pillow-11.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884"}, + {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196"}, + {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8"}, + {file = "pillow-11.1.0-cp39-cp39-win32.whl", hash = "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5"}, + {file = "pillow-11.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f"}, + {file = "pillow-11.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0"}, + {file = "pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20"}, ] [package.extras] docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] fpx = ["olefile"] mic = ["olefile"] -tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] typing = ["typing-extensions"] xmp = ["defusedxml"] -[[package]] -name = "pip" -version = "24.3.1" -description = "The PyPA recommended tool for installing Python packages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pip-24.3.1-py3-none-any.whl", hash = "sha256:3790624780082365f47549d032f3770eeb2b1e8bd1f7b2e02dace1afa361b4ed"}, - {file = "pip-24.3.1.tar.gz", hash = "sha256:ebcb60557f2aefabc2e0f918751cd24ea0d56d8ec5445fe1807f1d2109660b99"}, -] - -[[package]] -name = "pipdeptree" -version = "2.16.2" -description = "Command line utility to show dependency tree of packages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pipdeptree-2.16.2-py3-none-any.whl", hash = "sha256:4b60a20f632aa3449880141d1cd0bc99cb5f93ed46d54d689fd1c9b95f0e53d0"}, - {file = "pipdeptree-2.16.2.tar.gz", hash = "sha256:96ecde8e6f40c95998491a385e4af56d387f94ff7d3b8f209aa34982a721bc43"}, -] - -[package.dependencies] -pip = ">=23.1.2" - -[package.extras] -graphviz = ["graphviz (>=0.20.1)"] -test = ["covdefaults (>=2.3)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "virtualenv (>=20.25,<21)"] - -[[package]] -name = "pkginfo" -version = "1.12.0" -description = "Query metadata from sdists / bdists / installed packages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pkginfo-1.12.0-py3-none-any.whl", hash = "sha256:dcd589c9be4da8973eceffa247733c144812759aa67eaf4bbf97016a02f39088"}, - {file = "pkginfo-1.12.0.tar.gz", hash = "sha256:8ad91a0445a036782b9366ef8b8c2c50291f83a553478ba8580c73d3215700cf"}, -] - -[package.extras] -testing = ["pytest", "pytest-cov", "wheel"] - [[package]] name = "platformdirs" version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -1542,18 +1558,20 @@ type = ["mypy (>=1.11.2)"] [[package]] name = "playwright" -version = "1.49.0" +version = "1.49.1" description = "A high-level API to automate web browsers" optional = false python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "playwright-1.49.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:704532a2d8ba580ec9e1895bfeafddce2e3d52320d4eb8aa38e80376acc5cbb0"}, - {file = "playwright-1.49.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e453f02c4e5cc2db7e9759c47e7425f32e50ac76c76b7eb17c69eed72f01c4d8"}, - {file = "playwright-1.49.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:37ae985309184472946a6eb1a237e5d93c9e58a781fa73b75c8751325002a5d4"}, - {file = "playwright-1.49.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:68d94beffb3c9213e3ceaafa66171affd9a5d9162e0c8a3eed1b1132c2e57598"}, - {file = "playwright-1.49.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f12d2aecdb41fc25a624cb15f3e8391c252ebd81985e3d5c1c261fe93779345"}, - {file = "playwright-1.49.0-py3-none-win32.whl", hash = "sha256:91103de52d470594ad375b512d7143fa95d6039111ae11a93eb4fe2f2b4a4858"}, - {file = "playwright-1.49.0-py3-none-win_amd64.whl", hash = "sha256:34d28a2c2d46403368610be4339898dc9c34eb9f7c578207b4715c49743a072a"}, + {file = "playwright-1.49.1-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:1041ffb45a0d0bc44d698d3a5aa3ac4b67c9bd03540da43a0b70616ad52592b8"}, + {file = "playwright-1.49.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9f38ed3d0c1f4e0a6d1c92e73dd9a61f8855133249d6f0cec28648d38a7137be"}, + {file = "playwright-1.49.1-py3-none-macosx_11_0_universal2.whl", hash = "sha256:3be48c6d26dc819ca0a26567c1ae36a980a0303dcd4249feb6f59e115aaddfb8"}, + {file = "playwright-1.49.1-py3-none-manylinux1_x86_64.whl", hash = "sha256:753ca90ee31b4b03d165cfd36e477309ebf2b4381953f2a982ff612d85b147d2"}, + {file = "playwright-1.49.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd9bc8dab37aa25198a01f555f0a2e2c3813fe200fef018ac34dfe86b34994b9"}, + {file = "playwright-1.49.1-py3-none-win32.whl", hash = "sha256:43b304be67f096058e587dac453ece550eff87b8fbed28de30f4f022cc1745bb"}, + {file = "playwright-1.49.1-py3-none-win_amd64.whl", hash = "sha256:47b23cb346283278f5b4d1e1990bcb6d6302f80c0aa0ca93dd0601a1400191df"}, ] [package.dependencies] @@ -1566,6 +1584,8 @@ version = "5.24.1" description = "An open-source, interactive data visualization library for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "plotly-5.24.1-py3-none-any.whl", hash = "sha256:f67073a1e637eb0dc3e46324d9d51e2fe76e9727c892dde64ddf1e1b51f29089"}, {file = "plotly-5.24.1.tar.gz", hash = "sha256:dbc8ac8339d248a4bcc36e08a5659bacfe1b079390b8953533f4eb22169b4bae"}, @@ -1581,6 +1601,8 @@ version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -1592,13 +1614,15 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "4.0.1" +version = "4.1.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878"}, - {file = "pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2"}, + {file = "pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b"}, + {file = "pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4"}, ] [package.dependencies] @@ -1610,32 +1634,34 @@ virtualenv = ">=20.10.0" [[package]] name = "psutil" -version = "6.1.0" +version = "6.1.1" description = "Cross-platform lib for process and system monitoring in Python." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "psutil-6.1.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff34df86226c0227c52f38b919213157588a678d049688eded74c76c8ba4a5d0"}, - {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c0e0c00aa18ca2d3b2b991643b799a15fc8f0563d2ebb6040f64ce8dc027b942"}, - {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:000d1d1ebd634b4efb383f4034437384e44a6d455260aaee2eca1e9c1b55f047"}, - {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5cd2bcdc75b452ba2e10f0e8ecc0b57b827dd5d7aaffbc6821b2a9a242823a76"}, - {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:045f00a43c737f960d273a83973b2511430d61f283a44c96bf13a6e829ba8fdc"}, - {file = "psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e"}, - {file = "psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85"}, - {file = "psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688"}, - {file = "psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e"}, - {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38"}, - {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b"}, - {file = "psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a"}, - {file = "psutil-6.1.0-cp36-cp36m-win32.whl", hash = "sha256:6d3fbbc8d23fcdcb500d2c9f94e07b1342df8ed71b948a2649b5cb060a7c94ca"}, - {file = "psutil-6.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1209036fbd0421afde505a4879dee3b2fd7b1e14fee81c0069807adcbbcca747"}, - {file = "psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e"}, - {file = "psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be"}, - {file = "psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a"}, + {file = "psutil-6.1.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9ccc4316f24409159897799b83004cb1e24f9819b0dcf9c0b68bdcb6cefee6a8"}, + {file = "psutil-6.1.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ca9609c77ea3b8481ab005da74ed894035936223422dc591d6772b147421f777"}, + {file = "psutil-6.1.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8df0178ba8a9e5bc84fed9cfa61d54601b371fbec5c8eebad27575f1e105c0d4"}, + {file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:1924e659d6c19c647e763e78670a05dbb7feaf44a0e9c94bf9e14dfc6ba50468"}, + {file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:018aeae2af92d943fdf1da6b58665124897cfc94faa2ca92098838f83e1b1bca"}, + {file = "psutil-6.1.1-cp27-none-win32.whl", hash = "sha256:6d4281f5bbca041e2292be3380ec56a9413b790579b8e593b1784499d0005dac"}, + {file = "psutil-6.1.1-cp27-none-win_amd64.whl", hash = "sha256:c777eb75bb33c47377c9af68f30e9f11bc78e0f07fbf907be4a5d70b2fe5f030"}, + {file = "psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8"}, + {file = "psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377"}, + {file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003"}, + {file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160"}, + {file = "psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3"}, + {file = "psutil-6.1.1-cp36-cp36m-win32.whl", hash = "sha256:384636b1a64b47814437d1173be1427a7c83681b17a450bfc309a1953e329603"}, + {file = "psutil-6.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8be07491f6ebe1a693f17d4f11e69d0dc1811fa082736500f649f79df7735303"}, + {file = "psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53"}, + {file = "psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649"}, + {file = "psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5"}, ] [package.extras] -dev = ["black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "wheel"] +dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] test = ["pytest", "pytest-xdist", "setuptools"] [[package]] @@ -1644,6 +1670,8 @@ version = "9.0.0" description = "Get CPU info with pure Python" optional = false python-versions = "*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"}, {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, @@ -1655,25 +1683,29 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +markers = {main = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and platform_python_implementation != \"PyPy\" and (python_version <= \"3.11\" or python_version >= \"3.12\")", dev = "python_version <= \"3.11\" or python_version >= \"3.12\""} [[package]] name = "pydantic" -version = "2.10.3" +version = "2.10.6" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"}, - {file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"}, + {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, + {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.27.1" +pydantic-core = "2.27.2" typing-extensions = ">=4.12.2" [package.extras] @@ -1682,111 +1714,113 @@ timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.27.1" +version = "2.27.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, - {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"}, - {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"}, - {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"}, - {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"}, - {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"}, - {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"}, - {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, - {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, - {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, - {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, - {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, - {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, - {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, - {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, - {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"}, - {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"}, - {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"}, - {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"}, - {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"}, - {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"}, - {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"}, - {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"}, - {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"}, - {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"}, - {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"}, - {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"}, - {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"}, - {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"}, - {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"}, - {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"}, - {file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"}, - {file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"}, - {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"}, - {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"}, - {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"}, - {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"}, - {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"}, - {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"}, - {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"}, - {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"}, - {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"}, - {file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"}, - {file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"}, - {file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"}, - {file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"}, - {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"}, - {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"}, - {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"}, - {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"}, - {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"}, - {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"}, - {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"}, - {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"}, - {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"}, - {file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"}, - {file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"}, - {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, + {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, ] [package.dependencies] @@ -1798,6 +1832,8 @@ version = "12.0.0" description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pyee-12.0.0-py3-none-any.whl", hash = "sha256:7b14b74320600049ccc7d0e0b1becd3b4bd0a03c745758225e31a59f4095c990"}, {file = "pyee-12.0.0.tar.gz", hash = "sha256:c480603f4aa2927d4766eb41fa82793fe60a82cbfdb8d688e0d08c55a534e145"}, @@ -1811,13 +1847,15 @@ dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", " [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, ] [package.extras] @@ -1829,6 +1867,8 @@ version = "1.2.0" description = "Wrappers to call pyproject.toml-based build backend hooks." optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913"}, {file = "pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8"}, @@ -1836,21 +1876,25 @@ files = [ [[package]] name = "pyright" -version = "1.1.334" +version = "1.1.393" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "pyright-1.1.334-py3-none-any.whl", hash = "sha256:dcb13e8358e021189672c4d6ebcad192ab061e4c7225036973ec493183c6da68"}, - {file = "pyright-1.1.334.tar.gz", hash = "sha256:3adaf10f1f4209575dc022f9c897f7ef024639b7ea5b3cbe49302147e6949cd4"}, + {file = "pyright-1.1.393-py3-none-any.whl", hash = "sha256:8320629bb7a44ca90944ba599390162bf59307f3d9fb6e27da3b7011b8c17ae5"}, + {file = "pyright-1.1.393.tar.gz", hash = "sha256:aeeb7ff4e0364775ef416a80111613f91a05c8e01e58ecfefc370ca0db7aed9c"}, ] [package.dependencies] nodeenv = ">=1.6.0" +typing-extensions = ">=4.1" [package.extras] -all = ["twine (>=3.4.1)"] +all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"] dev = ["twine (>=3.4.1)"] +nodejs = ["nodejs-wheel-binaries"] [[package]] name = "pysocks" @@ -1858,6 +1902,8 @@ version = "1.7.1" description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, @@ -1870,6 +1916,8 @@ version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, @@ -1888,20 +1936,22 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments [[package]] name = "pytest-asyncio" -version = "0.24.0" +version = "0.25.3" description = "Pytest support for asyncio" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, - {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, + {file = "pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3"}, + {file = "pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a"}, ] [package.dependencies] pytest = ">=8.2,<9" [package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] @@ -1910,6 +1960,8 @@ version = "2.1.0" description = "pytest plugin for URL based testing" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest_base_url-2.1.0-py3-none-any.whl", hash = "sha256:3ad15611778764d451927b2a53240c1a7a591b521ea44cebfe45849d2d2812e6"}, {file = "pytest_base_url-2.1.0.tar.gz", hash = "sha256:02748589a54f9e63fcbe62301d6b0496da0d10231b753e950c63e03aee745d45"}, @@ -1928,6 +1980,8 @@ version = "5.1.0" description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." optional = false python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest-benchmark-5.1.0.tar.gz", hash = "sha256:9ea661cdc292e8231f7cd4c10b0319e56a2118e2c09d9f50e1b3d150d2aca105"}, {file = "pytest_benchmark-5.1.0-py3-none-any.whl", hash = "sha256:922de2dfa3033c227c96da942d1878191afa135a29485fb942e85dff1c592c89"}, @@ -1942,12 +1996,47 @@ aspect = ["aspectlib"] elasticsearch = ["elasticsearch"] histogram = ["pygal", "pygaljs", "setuptools"] +[[package]] +name = "pytest-codspeed" +version = "3.2.0" +description = "Pytest plugin to create CodSpeed benchmarks" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" +files = [ + {file = "pytest_codspeed-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5165774424c7ab8db7e7acdb539763a0e5657996effefdf0664d7fd95158d34"}, + {file = "pytest_codspeed-3.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bd55f92d772592c04a55209950c50880413ae46876e66bd349ef157075ca26c"}, + {file = "pytest_codspeed-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf6f56067538f4892baa8d7ab5ef4e45bb59033be1ef18759a2c7fc55b32035"}, + {file = "pytest_codspeed-3.2.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39a687b05c3d145642061b45ea78e47e12f13ce510104d1a2cda00eee0e36f58"}, + {file = "pytest_codspeed-3.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46a1afaaa1ac4c2ca5b0700d31ac46d80a27612961d031067d73c6ccbd8d3c2b"}, + {file = "pytest_codspeed-3.2.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c48ce3af3dfa78413ed3d69d1924043aa1519048dbff46edccf8f35a25dab3c2"}, + {file = "pytest_codspeed-3.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66692506d33453df48b36a84703448cb8b22953eea51f03fbb2eb758dc2bdc4f"}, + {file = "pytest_codspeed-3.2.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:479774f80d0bdfafa16112700df4dbd31bf2a6757fac74795fd79c0a7b3c389b"}, + {file = "pytest_codspeed-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:109f9f4dd1088019c3b3f887d003b7d65f98a7736ca1d457884f5aa293e8e81c"}, + {file = "pytest_codspeed-3.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2f69a03b52c9bb041aec1b8ee54b7b6c37a6d0a948786effa4c71157765b6da"}, + {file = "pytest_codspeed-3.2.0-py3-none-any.whl", hash = "sha256:54b5c2e986d6a28e7b0af11d610ea57bd5531cec8326abe486f1b55b09d91c39"}, + {file = "pytest_codspeed-3.2.0.tar.gz", hash = "sha256:f9d1b1a3b2c69cdc0490a1e8b1ced44bffbd0e8e21d81a7160cfdd923f6e8155"}, +] + +[package.dependencies] +cffi = ">=1.17.1" +pytest = ">=3.8" +rich = ">=13.8.1" + +[package.extras] +compat = ["pytest-benchmark (>=5.0.0,<5.1.0)", "pytest-xdist (>=3.6.1,<3.7.0)"] +lint = ["mypy (>=1.11.2,<1.12.0)", "ruff (>=0.6.5,<0.7.0)"] +test = ["pytest (>=7.0,<8.0)", "pytest-cov (>=4.0.0,<4.1.0)"] + [[package]] name = "pytest-cov" version = "6.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, @@ -1966,6 +2055,8 @@ version = "3.14.0" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, @@ -1979,13 +2070,15 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "pytest-playwright" -version = "0.6.2" +version = "0.7.0" description = "A pytest wrapper with fixtures for Playwright to automate web browsers" optional = false python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "pytest_playwright-0.6.2-py3-none-any.whl", hash = "sha256:0eff73bebe497b0158befed91e2f5fe94cfa17181f8b3acf575beed84e7e9043"}, - {file = "pytest_playwright-0.6.2.tar.gz", hash = "sha256:ff4054b19aa05df096ac6f74f0572591566aaf0f6d97f6cb9674db8a4d4ed06c"}, + {file = "pytest_playwright-0.7.0-py3-none-any.whl", hash = "sha256:2516d0871fa606634bfe32afbcc0342d68da2dbff97fe3459849e9c428486da2"}, + {file = "pytest_playwright-0.7.0.tar.gz", hash = "sha256:b3f2ea514bbead96d26376fac182f68dcd6571e7cb41680a89ff1673c05d60b6"}, ] [package.dependencies] @@ -2000,6 +2093,8 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -2010,13 +2105,15 @@ six = ">=1.5" [[package]] name = "python-engineio" -version = "4.10.1" +version = "4.11.2" description = "Engine.IO server and client for Python" optional = false python-versions = ">=3.6" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "python_engineio-4.10.1-py3-none-any.whl", hash = "sha256:445a94004ec8034960ab99e7ce4209ec619c6e6b6a12aedcb05abeab924025c0"}, - {file = "python_engineio-4.10.1.tar.gz", hash = "sha256:166cea8dd7429638c5c4e3a4895beae95196e860bc6f29ed0b9fe753d1ef2072"}, + {file = "python_engineio-4.11.2-py3-none-any.whl", hash = "sha256:f0971ac4c65accc489154fe12efd88f53ca8caf04754c46a66e85f5102ef22ad"}, + {file = "python_engineio-4.11.2.tar.gz", hash = "sha256:145bb0daceb904b4bb2d3eb2d93f7dbb7bb87a6a0c4f20a94cc8654dec977129"}, ] [package.dependencies] @@ -2029,13 +2126,15 @@ docs = ["sphinx"] [[package]] name = "python-multipart" -version = "0.0.19" +version = "0.0.20" description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "python_multipart-0.0.19-py3-none-any.whl", hash = "sha256:f8d5b0b9c618575bf9df01c684ded1d94a338839bdd8223838afacfb4bb2082d"}, - {file = "python_multipart-0.0.19.tar.gz", hash = "sha256:905502ef39050557b7a6af411f454bc19526529ca46ae6831508438890ce12cc"}, + {file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"}, + {file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"}, ] [[package]] @@ -2044,6 +2143,8 @@ version = "8.0.4" description = "A Python slugify application that also handles Unicode" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, @@ -2057,18 +2158,20 @@ unidecode = ["Unidecode (>=1.1.1)"] [[package]] name = "python-socketio" -version = "5.11.4" +version = "5.12.1" description = "Socket.IO server and client for Python" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "python_socketio-5.11.4-py3-none-any.whl", hash = "sha256:42efaa3e3e0b166fc72a527488a13caaac2cefc76174252486503bd496284945"}, - {file = "python_socketio-5.11.4.tar.gz", hash = "sha256:8b0b8ff2964b2957c865835e936310190639c00310a47d77321a594d1665355e"}, + {file = "python_socketio-5.12.1-py3-none-any.whl", hash = "sha256:24a0ea7cfff0e021eb28c68edbf7914ee4111bdf030b95e4d250c4dc9af7a386"}, + {file = "python_socketio-5.12.1.tar.gz", hash = "sha256:0299ff1f470b676c09c1bfab1dead25405077d227b2c13cf217a34dadc68ba9c"}, ] [package.dependencies] bidict = ">=0.21.0" -python-engineio = ">=4.8.0" +python-engineio = ">=4.11.0" [package.extras] asyncio-client = ["aiohttp (>=3.4)"] @@ -2077,13 +2180,15 @@ docs = ["sphinx"] [[package]] name = "pytz" -version = "2024.2" +version = "2025.1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, - {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, + {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, + {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, ] [[package]] @@ -2092,6 +2197,8 @@ version = "0.2.3" description = "A (partial) reimplementation of pywin32 using ctypes/cffi" optional = false python-versions = ">=3.6" +groups = ["main"] +markers = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"win32\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" files = [ {file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"}, {file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"}, @@ -2103,6 +2210,8 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -2165,6 +2274,8 @@ version = "44.0" description = "readme_renderer is a library for rendering readme descriptions for Warehouse" optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151"}, {file = "readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1"}, @@ -2184,6 +2295,8 @@ version = "5.2.1" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"}, {file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"}, @@ -2196,42 +2309,28 @@ async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\ hiredis = ["hiredis (>=3.0.0)"] ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"] -[[package]] -name = "reflex-chakra" -version = "0.6.2" -description = "reflex using chakra components" -optional = false -python-versions = "<4.0,>=3.9" -files = [ - {file = "reflex_chakra-0.6.2-py3-none-any.whl", hash = "sha256:b8aa19f39a02601c560b97f4b17f171c0b5980e13a58069e3a5dd0999e362e4f"}, - {file = "reflex_chakra-0.6.2.tar.gz", hash = "sha256:81ddb7f182cc454922cc817312755b799d4e1a49a46ef2e81305052dc76ef86d"}, -] - -[package.dependencies] -reflex = ">=0.6.0a" - [[package]] name = "reflex-hosting-cli" -version = "0.1.30" +version = "0.1.34" description = "Reflex Hosting CLI" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "reflex_hosting_cli-0.1.30-py3-none-any.whl", hash = "sha256:778c98d635003d8668158c22eaa0f7124d2bac92c8a1aabaed710960ca97796e"}, - {file = "reflex_hosting_cli-0.1.30.tar.gz", hash = "sha256:a0fdc73e595e6b9fd661e1307ae37267fb3815cc457b7f15938ba921c12fc0b6"}, + {file = "reflex_hosting_cli-0.1.34-py3-none-any.whl", hash = "sha256:eabc4dc7bf68e022a9388614c1a35b5ab36b01021df063d0c3356eda0e245264"}, + {file = "reflex_hosting_cli-0.1.34.tar.gz", hash = "sha256:07be37fda6dcede0a5d4bc1fd1786d9a3df5ad4e49dc1b6ba335418563cfecec"}, ] [package.dependencies] charset-normalizer = ">=3.3.2,<4.0.0" httpx = ">=0.25.1,<1.0" -pipdeptree = ">=2.13.1,<2.17.0" platformdirs = ">=3.10.0,<5.0" pydantic = ">=1.10.2,<3.0" -python-dateutil = ">=2.8.1" +pyyaml = ">=6.0.2,<7.0.0" rich = ">=13.0.0,<14.0" tabulate = ">=0.9.0,<0.10.0" typer = ">=0.15.0,<1" -websockets = ">=10.4" [[package]] name = "requests" @@ -2239,6 +2338,8 @@ version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, @@ -2260,6 +2361,8 @@ version = "1.0.0" description = "A utility belt for advanced users of python-requests" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, @@ -2274,6 +2377,8 @@ version = "2.0.0" description = "Validating URI References per RFC 3986" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, @@ -2288,6 +2393,8 @@ version = "13.9.4" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, @@ -2303,29 +2410,31 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.8.2" +version = "0.9.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d"}, - {file = "ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5"}, - {file = "ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93"}, - {file = "ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f"}, - {file = "ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22"}, - {file = "ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1"}, - {file = "ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea"}, - {file = "ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8"}, - {file = "ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5"}, + {file = "ruff-0.9.3-py3-none-linux_armv6l.whl", hash = "sha256:7f39b879064c7d9670197d91124a75d118d00b0990586549949aae80cdc16624"}, + {file = "ruff-0.9.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a187171e7c09efa4b4cc30ee5d0d55a8d6c5311b3e1b74ac5cb96cc89bafc43c"}, + {file = "ruff-0.9.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c59ab92f8e92d6725b7ded9d4a31be3ef42688a115c6d3da9457a5bda140e2b4"}, + {file = "ruff-0.9.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc153c25e715be41bb228bc651c1e9b1a88d5c6e5ed0194fa0dfea02b026439"}, + {file = "ruff-0.9.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:646909a1e25e0dc28fbc529eab8eb7bb583079628e8cbe738192853dbbe43af5"}, + {file = "ruff-0.9.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a5a46e09355695fbdbb30ed9889d6cf1c61b77b700a9fafc21b41f097bfbba4"}, + {file = "ruff-0.9.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c4bb09d2bbb394e3730d0918c00276e79b2de70ec2a5231cd4ebb51a57df9ba1"}, + {file = "ruff-0.9.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96a87ec31dc1044d8c2da2ebbed1c456d9b561e7d087734336518181b26b3aa5"}, + {file = "ruff-0.9.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb7554aca6f842645022fe2d301c264e6925baa708b392867b7a62645304df4"}, + {file = "ruff-0.9.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cabc332b7075a914ecea912cd1f3d4370489c8018f2c945a30bcc934e3bc06a6"}, + {file = "ruff-0.9.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:33866c3cc2a575cbd546f2cd02bdd466fed65118e4365ee538a3deffd6fcb730"}, + {file = "ruff-0.9.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:006e5de2621304c8810bcd2ee101587712fa93b4f955ed0985907a36c427e0c2"}, + {file = "ruff-0.9.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ba6eea4459dbd6b1be4e6bfc766079fb9b8dd2e5a35aff6baee4d9b1514ea519"}, + {file = "ruff-0.9.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:90230a6b8055ad47d3325e9ee8f8a9ae7e273078a66401ac66df68943ced029b"}, + {file = "ruff-0.9.3-py3-none-win32.whl", hash = "sha256:eabe5eb2c19a42f4808c03b82bd313fc84d4e395133fb3fc1b1516170a31213c"}, + {file = "ruff-0.9.3-py3-none-win_amd64.whl", hash = "sha256:040ceb7f20791dfa0e78b4230ee9dce23da3b64dd5848e40e3bf3ab76468dcf4"}, + {file = "ruff-0.9.3-py3-none-win_arm64.whl", hash = "sha256:800d773f6d4d33b0a3c60e2c6ae8f4c202ea2de056365acfa519aa48acf28e0b"}, + {file = "ruff-0.9.3.tar.gz", hash = "sha256:8293f89985a090ebc3ed1064df31f3b4b56320cdfcec8b60d3295bddb955c22a"}, ] [[package]] @@ -2334,6 +2443,8 @@ version = "3.3.3" description = "Python bindings to FreeDesktop.org Secret Service API" optional = false python-versions = ">=3.6" +groups = ["main"] +markers = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and sys_platform == \"linux\" and (python_version <= \"3.11\" or python_version >= \"3.12\")" files = [ {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, @@ -2345,13 +2456,15 @@ jeepney = ">=0.6" [[package]] name = "selenium" -version = "4.27.1" +version = "4.28.1" description = "Official Python bindings for Selenium WebDriver" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "selenium-4.27.1-py3-none-any.whl", hash = "sha256:b89b1f62b5cfe8025868556fe82360d6b649d464f75d2655cb966c8f8447ea18"}, - {file = "selenium-4.27.1.tar.gz", hash = "sha256:5296c425a75ff1b44d0d5199042b36a6d1ef76c04fb775b97b40be739a9caae2"}, + {file = "selenium-4.28.1-py3-none-any.whl", hash = "sha256:4238847e45e24e4472cfcf3554427512c7aab9443396435b1623ef406fff1cc1"}, + {file = "selenium-4.28.1.tar.gz", hash = "sha256:0072d08670d7ec32db901bd0107695a330cecac9f196e3afb3fa8163026e022a"}, ] [package.dependencies] @@ -2364,23 +2477,25 @@ websocket-client = ">=1.8,<2.0" [[package]] name = "setuptools" -version = "75.6.0" +version = "75.8.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d"}, - {file = "setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6"}, + {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, + {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "shellingham" @@ -2388,6 +2503,8 @@ version = "1.5.4" description = "Tool to Detect Surrounding Shell" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, @@ -2399,6 +2516,8 @@ version = "1.1.0" description = "Simple WebSocket server and client for Python" optional = false python-versions = ">=3.6" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c"}, {file = "simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4"}, @@ -2417,6 +2536,8 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -2428,6 +2549,8 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -2439,6 +2562,8 @@ version = "2.4.0" description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" optional = false python-versions = "*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, @@ -2446,72 +2571,74 @@ files = [ [[package]] name = "sqlalchemy" -version = "2.0.36" +version = "2.0.37" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8318f4776c85abc3f40ab185e388bee7a6ea99e7fa3a30686580b209eaa35c08"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c245b1fbade9c35e5bd3b64270ab49ce990369018289ecfde3f9c318411aaa07"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:69f93723edbca7342624d09f6704e7126b152eaed3cdbb634cb657a54332a3c5"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f9511d8dd4a6e9271d07d150fb2f81874a3c8c95e11ff9af3a2dfc35fe42ee44"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-win32.whl", hash = "sha256:c3f3631693003d8e585d4200730616b78fafd5a01ef8b698f6967da5c605b3fa"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-win_amd64.whl", hash = "sha256:a86bfab2ef46d63300c0f06936bd6e6c0105faa11d509083ba8f2f9d237fb5b5"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f5e9cd989b45b73bd359f693b935364f7e1f79486e29015813c338450aa5a71"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ddd9db6e59c44875211bc4c7953a9f6638b937b0a88ae6d09eb46cced54eff"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2519f3a5d0517fc159afab1015e54bb81b4406c278749779be57a569d8d1bb0d"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59b1ee96617135f6e1d6f275bbe988f419c5178016f3d41d3c0abb0c819f75bb"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:39769a115f730d683b0eb7b694db9789267bcd027326cccc3125e862eb03bfd8"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-win32.whl", hash = "sha256:66bffbad8d6271bb1cc2f9a4ea4f86f80fe5e2e3e501a5ae2a3dc6a76e604e6f"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-win_amd64.whl", hash = "sha256:23623166bfefe1487d81b698c423f8678e80df8b54614c2bf4b4cfcd7c711959"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-win32.whl", hash = "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-win_amd64.whl", hash = "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-win32.whl", hash = "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-win_amd64.whl", hash = "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:be9812b766cad94a25bc63bec11f88c4ad3629a0cec1cd5d4ba48dc23860486b"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50aae840ebbd6cdd41af1c14590e5741665e5272d2fee999306673a1bb1fdb4d"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4557e1f11c5f653ebfdd924f3f9d5ebfc718283b0b9beebaa5dd6b77ec290971"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07b441f7d03b9a66299ce7ccf3ef2900abc81c0db434f42a5694a37bd73870f2"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:28120ef39c92c2dd60f2721af9328479516844c6b550b077ca450c7d7dc68575"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-win32.whl", hash = "sha256:b81ee3d84803fd42d0b154cb6892ae57ea6b7c55d8359a02379965706c7efe6c"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-win_amd64.whl", hash = "sha256:f942a799516184c855e1a32fbc7b29d7e571b52612647866d4ec1c3242578fcb"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3d6718667da04294d7df1670d70eeddd414f313738d20a6f1d1f379e3139a545"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:72c28b84b174ce8af8504ca28ae9347d317f9dba3999e5981a3cd441f3712e24"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b11d0cfdd2b095e7b0686cf5fabeb9c67fae5b06d265d8180715b8cfa86522e3"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e32092c47011d113dc01ab3e1d3ce9f006a47223b18422c5c0d150af13a00687"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6a440293d802d3011028e14e4226da1434b373cbaf4a4bbb63f845761a708346"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c54a1e53a0c308a8e8a7dffb59097bff7facda27c70c286f005327f21b2bd6b1"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-win32.whl", hash = "sha256:1e0d612a17581b6616ff03c8e3d5eff7452f34655c901f75d62bd86449d9750e"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-win_amd64.whl", hash = "sha256:8958b10490125124463095bbdadda5aa22ec799f91958e410438ad6c97a7b793"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc022184d3e5cacc9579e41805a681187650e170eb2fd70e28b86192a479dcaa"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b817d41d692bf286abc181f8af476c4fbef3fd05e798777492618378448ee689"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e46a888b54be23d03a89be510f24a7652fe6ff660787b96cd0e57a4ebcb46d"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4ae3005ed83f5967f961fd091f2f8c5329161f69ce8480aa8168b2d7fe37f06"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03e08af7a5f9386a43919eda9de33ffda16b44eb11f3b313e6822243770e9763"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3dbb986bad3ed5ceaf090200eba750b5245150bd97d3e67343a3cfed06feecf7"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-win32.whl", hash = "sha256:9fe53b404f24789b5ea9003fc25b9a3988feddebd7e7b369c8fac27ad6f52f28"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-win_amd64.whl", hash = "sha256:af148a33ff0349f53512a049c6406923e4e02bf2f26c5fb285f143faf4f0e46a"}, - {file = "SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e"}, - {file = "sqlalchemy-2.0.36.tar.gz", hash = "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da36c3b0e891808a7542c5c89f224520b9a16c7f5e4d6a1156955605e54aef0e"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e7402ff96e2b073a98ef6d6142796426d705addd27b9d26c3b32dbaa06d7d069"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6f5d254a22394847245f411a2956976401e84da4288aa70cbcd5190744062c1"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41296bbcaa55ef5fdd32389a35c710133b097f7b2609d8218c0eabded43a1d84"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bedee60385c1c0411378cbd4dc486362f5ee88deceea50002772912d798bb00f"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6c67415258f9f3c69867ec02fea1bf6508153709ecbd731a982442a590f2b7e4"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-win32.whl", hash = "sha256:650dcb70739957a492ad8acff65d099a9586b9b8920e3507ca61ec3ce650bb72"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-win_amd64.whl", hash = "sha256:93d1543cd8359040c02b6614421c8e10cd7a788c40047dbc507ed46c29ae5636"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:78361be6dc9073ed17ab380985d1e45e48a642313ab68ab6afa2457354ff692c"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b661b49d0cb0ab311a189b31e25576b7ac3e20783beb1e1817d72d9d02508bf5"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d57bafbab289e147d064ffbd5cca2d7b1394b63417c0636cea1f2e93d16eb9e8"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa2c0913f02341d25fb858e4fb2031e6b0813494cca1ba07d417674128ce11b"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9df21b8d9e5c136ea6cde1c50d2b1c29a2b5ff2b1d610165c23ff250e0704087"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db18ff6b8c0f1917f8b20f8eca35c28bbccb9f83afa94743e03d40203ed83de9"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-win32.whl", hash = "sha256:46954173612617a99a64aee103bcd3f078901b9a8dcfc6ae80cbf34ba23df989"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-win_amd64.whl", hash = "sha256:7b7e772dc4bc507fdec4ee20182f15bd60d2a84f1e087a8accf5b5b7a0dcf2ba"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2952748ecd67ed3b56773c185e85fc084f6bdcdec10e5032a7c25a6bc7d682ef"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3151822aa1db0eb5afd65ccfafebe0ef5cda3a7701a279c8d0bf17781a793bb4"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaa8039b6d20137a4e02603aba37d12cd2dde7887500b8855356682fc33933f4"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cdba1f73b64530c47b27118b7053b8447e6d6f3c8104e3ac59f3d40c33aa9fd"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1b2690456528a87234a75d1a1644cdb330a6926f455403c8e4f6cad6921f9098"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf5ae8a9dcf657fd72144a7fd01f243236ea39e7344e579a121c4205aedf07bb"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-win32.whl", hash = "sha256:ea308cec940905ba008291d93619d92edaf83232ec85fbd514dcb329f3192761"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-win_amd64.whl", hash = "sha256:635d8a21577341dfe4f7fa59ec394b346da12420b86624a69e466d446de16aff"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c4096727193762e72ce9437e2a86a110cf081241919ce3fab8e89c02f6b6658"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4fb5ac86d8fe8151966814f6720996430462e633d225497566b3996966b9bdb"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e56a139bfe136a22c438478a86f8204c1eb5eed36f4e15c4224e4b9db01cb3e4"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f95fc8e3f34b5f6b3effb49d10ac97c569ec8e32f985612d9b25dd12d0d2e94"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c505edd429abdfe3643fa3b2e83efb3445a34a9dc49d5f692dd087be966020e0"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:12b0f1ec623cccf058cf21cb544f0e74656618165b083d78145cafde156ea7b6"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-win32.whl", hash = "sha256:293f9ade06b2e68dd03cfb14d49202fac47b7bb94bffcff174568c951fbc7af2"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-win_amd64.whl", hash = "sha256:d70f53a0646cc418ca4853da57cf3ddddbccb8c98406791f24426f2dd77fd0e2"}, + {file = "SQLAlchemy-2.0.37-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:44f569d0b1eb82301b92b72085583277316e7367e038d97c3a1a899d9a05e342"}, + {file = "SQLAlchemy-2.0.37-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2eae3423e538c10d93ae3e87788c6a84658c3ed6db62e6a61bb9495b0ad16bb"}, + {file = "SQLAlchemy-2.0.37-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfff7be361048244c3aa0f60b5e63221c5e0f0e509f4e47b8910e22b57d10ae7"}, + {file = "SQLAlchemy-2.0.37-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:5bc3339db84c5fb9130ac0e2f20347ee77b5dd2596ba327ce0d399752f4fce39"}, + {file = "SQLAlchemy-2.0.37-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:84b9f23b0fa98a6a4b99d73989350a94e4a4ec476b9a7dfe9b79ba5939f5e80b"}, + {file = "SQLAlchemy-2.0.37-cp37-cp37m-win32.whl", hash = "sha256:51bc9cfef83e0ac84f86bf2b10eaccb27c5a3e66a1212bef676f5bee6ef33ebb"}, + {file = "SQLAlchemy-2.0.37-cp37-cp37m-win_amd64.whl", hash = "sha256:8e47f1af09444f87c67b4f1bb6231e12ba6d4d9f03050d7fc88df6d075231a49"}, + {file = "SQLAlchemy-2.0.37-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6b788f14c5bb91db7f468dcf76f8b64423660a05e57fe277d3f4fad7b9dcb7ce"}, + {file = "SQLAlchemy-2.0.37-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521ef85c04c33009166777c77e76c8a676e2d8528dc83a57836b63ca9c69dcd1"}, + {file = "SQLAlchemy-2.0.37-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75311559f5c9881a9808eadbeb20ed8d8ba3f7225bef3afed2000c2a9f4d49b9"}, + {file = "SQLAlchemy-2.0.37-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cce918ada64c956b62ca2c2af59b125767097ec1dca89650a6221e887521bfd7"}, + {file = "SQLAlchemy-2.0.37-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9d087663b7e1feabea8c578d6887d59bb00388158e8bff3a76be11aa3f748ca2"}, + {file = "SQLAlchemy-2.0.37-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cf95a60b36997dad99692314c4713f141b61c5b0b4cc5c3426faad570b31ca01"}, + {file = "SQLAlchemy-2.0.37-cp38-cp38-win32.whl", hash = "sha256:d75ead7dd4d255068ea0f21492ee67937bd7c90964c8f3c2bea83c7b7f81b95f"}, + {file = "SQLAlchemy-2.0.37-cp38-cp38-win_amd64.whl", hash = "sha256:74bbd1d0a9bacf34266a7907d43260c8d65d31d691bb2356f41b17c2dca5b1d0"}, + {file = "SQLAlchemy-2.0.37-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:648ec5acf95ad59255452ef759054f2176849662af4521db6cb245263ae4aa33"}, + {file = "SQLAlchemy-2.0.37-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:35bd2df269de082065d4b23ae08502a47255832cc3f17619a5cea92ce478b02b"}, + {file = "SQLAlchemy-2.0.37-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f581d365af9373a738c49e0c51e8b18e08d8a6b1b15cc556773bcd8a192fa8b"}, + {file = "SQLAlchemy-2.0.37-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82df02816c14f8dc9f4d74aea4cb84a92f4b0620235daa76dde002409a3fbb5a"}, + {file = "SQLAlchemy-2.0.37-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94b564e38b344d3e67d2e224f0aec6ba09a77e4582ced41e7bfd0f757d926ec9"}, + {file = "SQLAlchemy-2.0.37-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:955a2a765aa1bd81aafa69ffda179d4fe3e2a3ad462a736ae5b6f387f78bfeb8"}, + {file = "SQLAlchemy-2.0.37-cp39-cp39-win32.whl", hash = "sha256:03f0528c53ca0b67094c4764523c1451ea15959bbf0a8a8a3096900014db0278"}, + {file = "SQLAlchemy-2.0.37-cp39-cp39-win_amd64.whl", hash = "sha256:4b12885dc85a2ab2b7d00995bac6d967bffa8594123b02ed21e8eb2205a7584b"}, + {file = "SQLAlchemy-2.0.37-py3-none-any.whl", hash = "sha256:a8998bf9f8658bd3839cbc44ddbe982955641863da0c1efe5b00c1ab4f5c16b1"}, + {file = "sqlalchemy-2.0.37.tar.gz", hash = "sha256:12b28d99a9c14eaf4055810df1001557176716de0167b91026e648e65229bffb"}, ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} +greenlet = {version = "!=0.4.17", markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} typing-extensions = ">=4.6.0" [package.extras] @@ -2545,6 +2672,8 @@ version = "0.0.22" description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness." optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "sqlmodel-0.0.22-py3-none-any.whl", hash = "sha256:a1ed13e28a1f4057cbf4ff6cdb4fc09e85702621d3259ba17b3c230bfb2f941b"}, {file = "sqlmodel-0.0.22.tar.gz", hash = "sha256:7d37c882a30c43464d143e35e9ecaf945d88035e20117bf5ec2834a23cbe505e"}, @@ -2556,21 +2685,22 @@ SQLAlchemy = ">=2.0.14,<2.1.0" [[package]] name = "starlette" -version = "0.41.3" +version = "0.45.3" description = "The little ASGI library that shines." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7"}, - {file = "starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835"}, + {file = "starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d"}, + {file = "starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f"}, ] [package.dependencies] -anyio = ">=3.4.0,<5" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} +anyio = ">=3.6.2,<5" [package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] [[package]] name = "starlette-admin" @@ -2578,6 +2708,8 @@ version = "0.14.1" description = "Fast, beautiful and extensible administrative interface framework for Starlette/FastApi applications" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "starlette_admin-0.14.1-py3-none-any.whl", hash = "sha256:5b6260d7ed3db455585852d669feb7ed9a8c5f9a1e3d48d21a52912ec37e18f9"}, {file = "starlette_admin-0.14.1.tar.gz", hash = "sha256:45e2baa3b9a8deec7a6e8ca9295123f648bb0d2070abe68f27193c6d5e32cc38"}, @@ -2601,6 +2733,8 @@ version = "0.9.0" description = "Pretty-print tabular data" optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, @@ -2615,6 +2749,8 @@ version = "9.0.0" description = "Retry code until it succeeds" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"}, {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"}, @@ -2630,6 +2766,8 @@ version = "1.3" description = "The most basic Text::Unidecode port" optional = false python-versions = "*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, @@ -2641,6 +2779,8 @@ version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -2652,6 +2792,8 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version < \"3.11\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -2693,6 +2835,8 @@ version = "0.13.2" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, @@ -2700,13 +2844,15 @@ files = [ [[package]] name = "trio" -version = "0.27.0" +version = "0.28.0" description = "A friendly Python library for async concurrency and I/O" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "trio-0.27.0-py3-none-any.whl", hash = "sha256:68eabbcf8f457d925df62da780eff15ff5dc68fd6b367e2dde59f7aaf2a0b884"}, - {file = "trio-0.27.0.tar.gz", hash = "sha256:1dcc95ab1726b2da054afea8fd761af74bad79bd52381b84eae408e983c76831"}, + {file = "trio-0.28.0-py3-none-any.whl", hash = "sha256:56d58977acc1635735a96581ec70513cc781b8b6decd299c487d3be2a721cd94"}, + {file = "trio-0.28.0.tar.gz", hash = "sha256:4e547896fe9e8a5658e54e4c7c5fa1db748cbbbaa7c965e7d40505b928c73c05"}, ] [package.dependencies] @@ -2724,6 +2870,8 @@ version = "0.11.1" description = "WebSocket library for Trio" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "trio-websocket-0.11.1.tar.gz", hash = "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f"}, {file = "trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638"}, @@ -2736,20 +2884,21 @@ wsproto = ">=0.14" [[package]] name = "twine" -version = "6.0.1" +version = "6.1.0" description = "Collection of utilities for publishing packages on PyPI" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "twine-6.0.1-py3-none-any.whl", hash = "sha256:9c6025b203b51521d53e200f4a08b116dee7500a38591668c6a6033117bdc218"}, - {file = "twine-6.0.1.tar.gz", hash = "sha256:36158b09df5406e1c9c1fb8edb24fc2be387709443e7376689b938531582ee27"}, + {file = "twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384"}, + {file = "twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd"}, ] [package.dependencies] -importlib-metadata = {version = ">=3.6", markers = "python_version < \"3.10\""} +id = "*" keyring = {version = ">=15.1", markers = "platform_machine != \"ppc64le\" and platform_machine != \"s390x\""} -packaging = "*" -pkginfo = ">=1.8.1" +packaging = ">=24.0" readme-renderer = ">=35.0" requests = ">=2.20" requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" @@ -2766,6 +2915,8 @@ version = "0.15.1" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847"}, {file = "typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a"}, @@ -2783,6 +2934,8 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, @@ -2790,24 +2943,28 @@ files = [ [[package]] name = "tzdata" -version = "2024.2" +version = "2025.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, - {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, + {file = "tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"}, + {file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"}, ] [[package]] name = "urllib3" -version = "2.2.3" +version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.dependencies] @@ -2821,13 +2978,15 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.32.1" +version = "0.34.0" description = "The lightning-fast ASGI server." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e"}, - {file = "uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175"}, + {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, + {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, ] [package.dependencies] @@ -2840,13 +2999,15 @@ standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", [[package]] name = "virtualenv" -version = "20.28.0" +version = "20.29.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "virtualenv-20.28.0-py3-none-any.whl", hash = "sha256:23eae1b4516ecd610481eda647f3a7c09aea295055337331bb4e6892ecce47b0"}, - {file = "virtualenv-20.28.0.tar.gz", hash = "sha256:2c9c3262bb8e7b87ea801d715fae4495e6032450c71d2309be9550e7364049aa"}, + {file = "virtualenv-20.29.1-py3-none-any.whl", hash = "sha256:4e4cb403c0b0da39e13b46b1b2476e505cb0046b25f242bee80f62bf990b2779"}, + {file = "virtualenv-20.29.1.tar.gz", hash = "sha256:b8b8970138d32fb606192cb97f6cd4bb644fa486be9308fb9b63f81091b5dc35"}, ] [package.dependencies] @@ -2864,6 +3025,8 @@ version = "1.8.0" description = "WebSocket client for Python with low level API options" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526"}, {file = "websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da"}, @@ -2874,90 +3037,14 @@ docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] optional = ["python-socks", "wsaccel"] test = ["websockets"] -[[package]] -name = "websockets" -version = "14.1" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -optional = false -python-versions = ">=3.9" -files = [ - {file = "websockets-14.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a0adf84bc2e7c86e8a202537b4fd50e6f7f0e4a6b6bf64d7ccb96c4cd3330b29"}, - {file = "websockets-14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90b5d9dfbb6d07a84ed3e696012610b6da074d97453bd01e0e30744b472c8179"}, - {file = "websockets-14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2177ee3901075167f01c5e335a6685e71b162a54a89a56001f1c3e9e3d2ad250"}, - {file = "websockets-14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f14a96a0034a27f9d47fd9788913924c89612225878f8078bb9d55f859272b0"}, - {file = "websockets-14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f874ba705deea77bcf64a9da42c1f5fc2466d8f14daf410bc7d4ceae0a9fcb0"}, - {file = "websockets-14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9607b9a442392e690a57909c362811184ea429585a71061cd5d3c2b98065c199"}, - {file = "websockets-14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bea45f19b7ca000380fbd4e02552be86343080120d074b87f25593ce1700ad58"}, - {file = "websockets-14.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:219c8187b3ceeadbf2afcf0f25a4918d02da7b944d703b97d12fb01510869078"}, - {file = "websockets-14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad2ab2547761d79926effe63de21479dfaf29834c50f98c4bf5b5480b5838434"}, - {file = "websockets-14.1-cp310-cp310-win32.whl", hash = "sha256:1288369a6a84e81b90da5dbed48610cd7e5d60af62df9851ed1d1d23a9069f10"}, - {file = "websockets-14.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0744623852f1497d825a49a99bfbec9bea4f3f946df6eb9d8a2f0c37a2fec2e"}, - {file = "websockets-14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:449d77d636f8d9c17952628cc7e3b8faf6e92a17ec581ec0c0256300717e1512"}, - {file = "websockets-14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a35f704be14768cea9790d921c2c1cc4fc52700410b1c10948511039be824aac"}, - {file = "websockets-14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b1f3628a0510bd58968c0f60447e7a692933589b791a6b572fcef374053ca280"}, - {file = "websockets-14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c3deac3748ec73ef24fc7be0b68220d14d47d6647d2f85b2771cb35ea847aa1"}, - {file = "websockets-14.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7048eb4415d46368ef29d32133134c513f507fff7d953c18c91104738a68c3b3"}, - {file = "websockets-14.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6cf0ad281c979306a6a34242b371e90e891bce504509fb6bb5246bbbf31e7b6"}, - {file = "websockets-14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cc1fc87428c1d18b643479caa7b15db7d544652e5bf610513d4a3478dbe823d0"}, - {file = "websockets-14.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f95ba34d71e2fa0c5d225bde3b3bdb152e957150100e75c86bc7f3964c450d89"}, - {file = "websockets-14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9481a6de29105d73cf4515f2bef8eb71e17ac184c19d0b9918a3701c6c9c4f23"}, - {file = "websockets-14.1-cp311-cp311-win32.whl", hash = "sha256:368a05465f49c5949e27afd6fbe0a77ce53082185bbb2ac096a3a8afaf4de52e"}, - {file = "websockets-14.1-cp311-cp311-win_amd64.whl", hash = "sha256:6d24fc337fc055c9e83414c94e1ee0dee902a486d19d2a7f0929e49d7d604b09"}, - {file = "websockets-14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed"}, - {file = "websockets-14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d"}, - {file = "websockets-14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707"}, - {file = "websockets-14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a"}, - {file = "websockets-14.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45"}, - {file = "websockets-14.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58"}, - {file = "websockets-14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058"}, - {file = "websockets-14.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4"}, - {file = "websockets-14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05"}, - {file = "websockets-14.1-cp312-cp312-win32.whl", hash = "sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0"}, - {file = "websockets-14.1-cp312-cp312-win_amd64.whl", hash = "sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f"}, - {file = "websockets-14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3630b670d5057cd9e08b9c4dab6493670e8e762a24c2c94ef312783870736ab9"}, - {file = "websockets-14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36ebd71db3b89e1f7b1a5deaa341a654852c3518ea7a8ddfdf69cc66acc2db1b"}, - {file = "websockets-14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5b918d288958dc3fa1c5a0b9aa3256cb2b2b84c54407f4813c45d52267600cd3"}, - {file = "websockets-14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:00fe5da3f037041da1ee0cf8e308374e236883f9842c7c465aa65098b1c9af59"}, - {file = "websockets-14.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8149a0f5a72ca36720981418eeffeb5c2729ea55fa179091c81a0910a114a5d2"}, - {file = "websockets-14.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77569d19a13015e840b81550922056acabc25e3f52782625bc6843cfa034e1da"}, - {file = "websockets-14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf5201a04550136ef870aa60ad3d29d2a59e452a7f96b94193bee6d73b8ad9a9"}, - {file = "websockets-14.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:88cf9163ef674b5be5736a584c999e98daf3aabac6e536e43286eb74c126b9c7"}, - {file = "websockets-14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:836bef7ae338a072e9d1863502026f01b14027250a4545672673057997d5c05a"}, - {file = "websockets-14.1-cp313-cp313-win32.whl", hash = "sha256:0d4290d559d68288da9f444089fd82490c8d2744309113fc26e2da6e48b65da6"}, - {file = "websockets-14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8621a07991add373c3c5c2cf89e1d277e49dc82ed72c75e3afc74bd0acc446f0"}, - {file = "websockets-14.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:01bb2d4f0a6d04538d3c5dfd27c0643269656c28045a53439cbf1c004f90897a"}, - {file = "websockets-14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:414ffe86f4d6f434a8c3b7913655a1a5383b617f9bf38720e7c0799fac3ab1c6"}, - {file = "websockets-14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fda642151d5affdee8a430bd85496f2e2517be3a2b9d2484d633d5712b15c56"}, - {file = "websockets-14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd7c11968bc3860d5c78577f0dbc535257ccec41750675d58d8dc66aa47fe52c"}, - {file = "websockets-14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a032855dc7db987dff813583d04f4950d14326665d7e714d584560b140ae6b8b"}, - {file = "websockets-14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7e7ea2f782408c32d86b87a0d2c1fd8871b0399dd762364c731d86c86069a78"}, - {file = "websockets-14.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:39450e6215f7d9f6f7bc2a6da21d79374729f5d052333da4d5825af8a97e6735"}, - {file = "websockets-14.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ceada5be22fa5a5a4cdeec74e761c2ee7db287208f54c718f2df4b7e200b8d4a"}, - {file = "websockets-14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3fc753451d471cff90b8f467a1fc0ae64031cf2d81b7b34e1811b7e2691bc4bc"}, - {file = "websockets-14.1-cp39-cp39-win32.whl", hash = "sha256:14839f54786987ccd9d03ed7f334baec0f02272e7ec4f6e9d427ff584aeea8b4"}, - {file = "websockets-14.1-cp39-cp39-win_amd64.whl", hash = "sha256:d9fd19ecc3a4d5ae82ddbfb30962cf6d874ff943e56e0c81f5169be2fda62979"}, - {file = "websockets-14.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e5dc25a9dbd1a7f61eca4b7cb04e74ae4b963d658f9e4f9aad9cd00b688692c8"}, - {file = "websockets-14.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:04a97aca96ca2acedf0d1f332c861c5a4486fdcba7bcef35873820f940c4231e"}, - {file = "websockets-14.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df174ece723b228d3e8734a6f2a6febbd413ddec39b3dc592f5a4aa0aff28098"}, - {file = "websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:034feb9f4286476f273b9a245fb15f02c34d9586a5bc936aff108c3ba1b21beb"}, - {file = "websockets-14.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c308dabd2b380807ab64b62985eaccf923a78ebc572bd485375b9ca2b7dc7"}, - {file = "websockets-14.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5a42d3ecbb2db5080fc578314439b1d79eef71d323dc661aa616fb492436af5d"}, - {file = "websockets-14.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddaa4a390af911da6f680be8be4ff5aaf31c4c834c1a9147bc21cbcbca2d4370"}, - {file = "websockets-14.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a4c805c6034206143fbabd2d259ec5e757f8b29d0a2f0bf3d2fe5d1f60147a4a"}, - {file = "websockets-14.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:205f672a6c2c671a86d33f6d47c9b35781a998728d2c7c2a3e1cf3333fcb62b7"}, - {file = "websockets-14.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef440054124728cc49b01c33469de06755e5a7a4e83ef61934ad95fc327fbb0"}, - {file = "websockets-14.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7591d6f440af7f73c4bd9404f3772bfee064e639d2b6cc8c94076e71b2471c1"}, - {file = "websockets-14.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:25225cc79cfebc95ba1d24cd3ab86aaa35bcd315d12fa4358939bd55e9bd74a5"}, - {file = "websockets-14.1-py3-none-any.whl", hash = "sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e"}, - {file = "websockets-14.1.tar.gz", hash = "sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8"}, -] - [[package]] name = "wheel" version = "0.45.1" description = "A built-package format for Python" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248"}, {file = "wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729"}, @@ -2968,76 +3055,92 @@ test = ["pytest (>=6.0.0)", "setuptools (>=65)"] [[package]] name = "wrapt" -version = "1.17.0" +version = "1.17.2" description = "Module for decorators, wrappers and monkey patching." optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ - {file = "wrapt-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8"}, - {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d"}, - {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df"}, - {file = "wrapt-1.17.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d"}, - {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea"}, - {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb"}, - {file = "wrapt-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301"}, - {file = "wrapt-1.17.0-cp310-cp310-win32.whl", hash = "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22"}, - {file = "wrapt-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575"}, - {file = "wrapt-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489"}, - {file = "wrapt-1.17.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d"}, - {file = "wrapt-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b"}, - {file = "wrapt-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346"}, - {file = "wrapt-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a"}, - {file = "wrapt-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569"}, - {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504"}, - {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451"}, - {file = "wrapt-1.17.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1"}, - {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106"}, - {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada"}, - {file = "wrapt-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4"}, - {file = "wrapt-1.17.0-cp312-cp312-win32.whl", hash = "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635"}, - {file = "wrapt-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7"}, - {file = "wrapt-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181"}, - {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393"}, - {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4"}, - {file = "wrapt-1.17.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b"}, - {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721"}, - {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90"}, - {file = "wrapt-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a"}, - {file = "wrapt-1.17.0-cp313-cp313-win32.whl", hash = "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045"}, - {file = "wrapt-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838"}, - {file = "wrapt-1.17.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b"}, - {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379"}, - {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d"}, - {file = "wrapt-1.17.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f"}, - {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c"}, - {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b"}, - {file = "wrapt-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab"}, - {file = "wrapt-1.17.0-cp313-cp313t-win32.whl", hash = "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf"}, - {file = "wrapt-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a"}, - {file = "wrapt-1.17.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13"}, - {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f"}, - {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c"}, - {file = "wrapt-1.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d"}, - {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce"}, - {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627"}, - {file = "wrapt-1.17.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f"}, - {file = "wrapt-1.17.0-cp38-cp38-win32.whl", hash = "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea"}, - {file = "wrapt-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed"}, - {file = "wrapt-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1"}, - {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c"}, - {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578"}, - {file = "wrapt-1.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33"}, - {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad"}, - {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9"}, - {file = "wrapt-1.17.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0"}, - {file = "wrapt-1.17.0-cp39-cp39-win32.whl", hash = "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88"}, - {file = "wrapt-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977"}, - {file = "wrapt-1.17.0-py3-none-any.whl", hash = "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371"}, - {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, + {file = "wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72"}, + {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c"}, + {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62"}, + {file = "wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563"}, + {file = "wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda"}, + {file = "wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000"}, + {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662"}, + {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72"}, + {file = "wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317"}, + {file = "wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392"}, + {file = "wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b"}, + {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae"}, + {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9"}, + {file = "wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9"}, + {file = "wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998"}, + {file = "wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6"}, + {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b"}, + {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504"}, + {file = "wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a"}, + {file = "wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b"}, + {file = "wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb"}, + {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6"}, + {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f"}, + {file = "wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555"}, + {file = "wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c803c401ea1c1c18de70a06a6f79fcc9c5acfc79133e9869e730ad7f8ad8ef9"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f917c1180fdb8623c2b75a99192f4025e412597c50b2ac870f156de8fb101119"}, + {file = "wrapt-1.17.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ecc840861360ba9d176d413a5489b9a0aff6d6303d7e733e2c4623cfa26904a6"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb87745b2e6dc56361bfde481d5a378dc314b252a98d7dd19a651a3fa58f24a9"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58455b79ec2661c3600e65c0a716955adc2410f7383755d537584b0de41b1d8a"}, + {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4e42a40a5e164cbfdb7b386c966a588b1047558a990981ace551ed7e12ca9c2"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:91bd7d1773e64019f9288b7a5101f3ae50d3d8e6b1de7edee9c2ccc1d32f0c0a"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bb90fb8bda722a1b9d48ac1e6c38f923ea757b3baf8ebd0c82e09c5c1a0e7a04"}, + {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:08e7ce672e35efa54c5024936e559469436f8b8096253404faeb54d2a878416f"}, + {file = "wrapt-1.17.2-cp38-cp38-win32.whl", hash = "sha256:410a92fefd2e0e10d26210e1dfb4a876ddaf8439ef60d6434f21ef8d87efc5b7"}, + {file = "wrapt-1.17.2-cp38-cp38-win_amd64.whl", hash = "sha256:95c658736ec15602da0ed73f312d410117723914a5c91a14ee4cdd72f1d790b3"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99039fa9e6306880572915728d7f6c24a86ec57b0a83f6b2491e1d8ab0235b9a"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2696993ee1eebd20b8e4ee4356483c4cb696066ddc24bd70bcbb80fa56ff9061"}, + {file = "wrapt-1.17.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:612dff5db80beef9e649c6d803a8d50c409082f1fedc9dbcdfde2983b2025b82"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c2caa1585c82b3f7a7ab56afef7b3602021d6da34fbc1cf234ff139fed3cd9"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c958bcfd59bacc2d0249dcfe575e71da54f9dcf4a8bdf89c4cb9a68a1170d73f"}, + {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc78a84e2dfbc27afe4b2bd7c80c8db9bca75cc5b85df52bfe634596a1da846b"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba0f0eb61ef00ea10e00eb53a9129501f52385c44853dbd6c4ad3f403603083f"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1e1fe0e6ab7775fd842bc39e86f6dcfc4507ab0ffe206093e76d61cde37225c8"}, + {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c86563182421896d73858e08e1db93afdd2b947a70064b813d515d66549e15f9"}, + {file = "wrapt-1.17.2-cp39-cp39-win32.whl", hash = "sha256:f393cda562f79828f38a819f4788641ac7c4085f30f1ce1a68672baa686482bb"}, + {file = "wrapt-1.17.2-cp39-cp39-win_amd64.whl", hash = "sha256:36ccae62f64235cf8ddb682073a60519426fdd4725524ae38874adf72b5f2aeb"}, + {file = "wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8"}, + {file = "wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3"}, ] [[package]] @@ -3046,6 +3149,8 @@ version = "1.2.0" description = "WebSockets state-machine based protocol implementation" optional = false python-versions = ">=3.7.0" +groups = ["main", "dev"] +markers = "python_version <= \"3.11\" or python_version >= \"3.12\"" files = [ {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, @@ -3060,6 +3165,8 @@ version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "(platform_machine != \"ppc64le\" and platform_machine != \"s390x\") and python_version <= \"3.11\" or python_full_version < \"3.10.2\"" files = [ {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, @@ -3074,6 +3181,6 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", type = ["pytest-mypy"] [metadata] -lock-version = "2.0" -python-versions = "^3.9" -content-hash = "d62cd1897d8f73e9aad9e907beb82be509dc5e33d8f37b36ebf26ad1f3075a9f" +lock-version = "2.1" +python-versions = ">=3.10, <4.0" +content-hash = "3b7e6e6e872c68f951f191d85a7d76fe1dd86caf32e2143a53a3152a3686fc7f" diff --git a/pyproject.toml b/pyproject.toml index 6e995a85d..2b5507a1d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,32 +1,24 @@ [tool.poetry] name = "reflex" -version = "0.6.7dev1" +version = "0.7.0dev1" description = "Web apps in pure Python." license = "Apache-2.0" authors = [ - "Nikhil Rao ", - "Alek Petuskey ", - "Masen Furer ", - "Elijah Ahianyo ", - "Thomas Brandého ", + "Nikhil Rao ", + "Alek Petuskey ", + "Masen Furer ", + "Elijah Ahianyo ", + "Thomas Brandého ", ] readme = "README.md" homepage = "https://reflex.dev" repository = "https://github.com/reflex-dev/reflex" documentation = "https://reflex.dev/docs/getting-started/introduction" -keywords = [ - "web", - "framework", -] -classifiers = [ - "Development Status :: 4 - Beta", -] -packages = [ - {include = "reflex"} -] +keywords = ["web", "framework"] +classifiers = ["Development Status :: 4 - Beta"] [tool.poetry.dependencies] -python = "^3.9" +python = ">=3.10, <4.0" fastapi = ">=0.96.0,!=0.111.0,!=0.111.1" gunicorn = ">=20.1.0,<24.0" jinja2 = ">=3.1.2,<4.0" @@ -42,14 +34,14 @@ uvicorn = ">=0.20.0" starlette-admin = ">=0.11.0,<1.0" alembic = ">=1.11.1,<2.0" platformdirs = ">=3.10.0,<5.0" -distro = {version = ">=1.8.0,<2.0", platform = "linux"} +distro = { version = ">=1.8.0,<2.0", platform = "linux" } python-engineio = "!=4.6.0" wrapt = [ - {version = ">=1.14.0,<2.0", python = ">=3.11"}, - {version = ">=1.11.0,<2.0", python = "<3.11"}, + { version = ">=1.14.0,<2.0", python = ">=3.11" }, + { version = ">=1.11.0,<2.0", python = "<3.11" }, ] packaging = ">=23.1,<25.0" -reflex-hosting-cli = ">=0.1.29,<2.0" +reflex-hosting-cli = ">=0.1.29" charset-normalizer = ">=3.3.2,<4.0" wheel = ">=0.42.0,<1.0" build = ">=1.0.3,<2.0" @@ -58,19 +50,18 @@ httpx = ">=0.25.1,<1.0" twine = ">=4.0.0,<7.0" tomlkit = ">=0.12.4,<1.0" lazy_loader = ">=0.4" -reflex-chakra = ">=0.6.0" typing_extensions = ">=4.6.0" [tool.poetry.group.dev.dependencies] pytest = ">=7.1.2,<9.0" pytest-mock = ">=3.10.0,<4.0" -pyright = ">=1.1.229,<1.1.335" +pyright = ">=1.1.392, <1.2" darglint = ">=1.8.1,<2.0" dill = ">=0.3.8" toml = ">=0.10.2,<1.0" pytest-asyncio = ">=0.24.0" pytest-cov = ">=4.0.0,<7.0" -ruff = "0.8.2" +ruff = "0.9.3" pandas = ">=2.1.1,<3.0" pillow = ">=10.0.0,<12.0" plotly = ">=5.13.0,<6.0" @@ -80,6 +71,7 @@ selenium = ">=4.11.0,<5.0" pytest-benchmark = ">=4.0.0,<6.0" playwright = ">=1.46.0" pytest-playwright = ">=0.5.1" +pytest-codspeed = "^3.1.2" [tool.poetry.scripts] reflex = "reflex.reflex:cli" @@ -89,21 +81,30 @@ requires = ["poetry-core>=1.5.1"] build-backend = "poetry.core.masonry.api" [tool.pyright] +reportIncompatibleMethodOverride = false [tool.ruff] -target-version = "py39" +target-version = "py310" +output-format = "concise" lint.isort.split-on-trailing-comma = false -lint.select = ["B", "D", "E", "F", "I", "SIM", "W", "RUF"] -lint.ignore = ["B008", "D205", "E501", "F403", "SIM115", "RUF006", "RUF012"] +lint.select = ["ANN001","B", "C4", "D", "E", "ERA", "F", "FURB", "I", "N", "PERF", "PGH", "PTH", "RUF", "SIM", "T", "TRY", "W"] +lint.ignore = ["B008", "D205", "E501", "F403", "SIM115", "RUF006", "RUF008", "RUF012", "TRY0"] lint.pydocstyle.convention = "google" [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] -"tests/*.py" = ["D100", "D103", "D104", "B018"] +"tests/*.py" = ["ANN001", "D100", "D103", "D104", "B018", "PERF", "T", "N"] +"benchmarks/*.py" = ["ANN001", "D100", "D103", "D104", "B018", "PERF", "T", "N"] "reflex/.templates/*.py" = ["D100", "D103", "D104"] -"*.pyi" = ["D301", "D415", "D417", "D418", "E742"] +"*.pyi" = ["D301", "D415", "D417", "D418", "E742", "N", "PGH"] +"pyi_generator.py" = ["N802"] +"reflex/constants/*.py" = ["N"] "*/blank.py" = ["I001"] [tool.pytest.ini_options] asyncio_default_fixture_loop_scope = "function" asyncio_mode = "auto" + +[tool.codespell] +skip = "docs/*,*.html,examples/*, *.pyi, poetry.lock" +ignore-words-list = "te, TreeE" diff --git a/reflex/.templates/jinja/custom_components/pyproject.toml.jinja2 b/reflex/.templates/jinja/custom_components/pyproject.toml.jinja2 index a5239f33b..abfd998fd 100644 --- a/reflex/.templates/jinja/custom_components/pyproject.toml.jinja2 +++ b/reflex/.templates/jinja/custom_components/pyproject.toml.jinja2 @@ -8,7 +8,7 @@ version = "0.0.1" description = "Reflex custom component {{ module_name }}" readme = "README.md" license = { text = "Apache-2.0" } -requires-python = ">=3.9" +requires-python = ">=3.10" authors = [{ name = "", email = "YOUREMAIL@domain.com" }] keywords = ["reflex","reflex-custom-components"] diff --git a/reflex/.templates/jinja/web/pages/_app.js.jinja2 b/reflex/.templates/jinja/web/pages/_app.js.jinja2 index 21cfd921a..ee3e24540 100644 --- a/reflex/.templates/jinja/web/pages/_app.js.jinja2 +++ b/reflex/.templates/jinja/web/pages/_app.js.jinja2 @@ -1,4 +1,5 @@ {% extends "web/pages/base_page.js.jinja2" %} +{% from "web/pages/macros.js.jinja2" import renderHooks %} {% block early_imports %} import '$/styles/styles.css' @@ -18,10 +19,7 @@ import * as {{library_alias}} from "{{library_path}}"; {% block export %} function AppWrap({children}) { - - {% for hook in hooks %} - {{ hook }} - {% endfor %} + {{ renderHooks(hooks) }} return ( {{utils.render(render, indent_width=0)}} @@ -40,13 +38,13 @@ export default function MyApp({ Component, pageProps }) { }, []); return ( - - - - - - - + + + + + + + ); } diff --git a/reflex/.templates/jinja/web/pages/custom_component.js.jinja2 b/reflex/.templates/jinja/web/pages/custom_component.js.jinja2 index 222524d2d..e729d7273 100644 --- a/reflex/.templates/jinja/web/pages/custom_component.js.jinja2 +++ b/reflex/.templates/jinja/web/pages/custom_component.js.jinja2 @@ -1,5 +1,5 @@ {% extends "web/pages/base_page.js.jinja2" %} - +{% from "web/pages/macros.js.jinja2" import renderHooks %} {% block export %} {% for component in components %} @@ -8,9 +8,8 @@ {% endfor %} export const {{component.name}} = memo(({ {{-component.props|join(", ")-}} }) => { - {% for hook in component.hooks %} - {{ hook }} - {% endfor %} + {{ renderHooks(component.hooks) }} + return( {{utils.render(component.render)}} ) diff --git a/reflex/.templates/jinja/web/pages/index.js.jinja2 b/reflex/.templates/jinja/web/pages/index.js.jinja2 index efb086ef5..5551ad5fc 100644 --- a/reflex/.templates/jinja/web/pages/index.js.jinja2 +++ b/reflex/.templates/jinja/web/pages/index.js.jinja2 @@ -1,4 +1,5 @@ {% extends "web/pages/base_page.js.jinja2" %} +{% from "web/pages/macros.js.jinja2" import renderHooks %} {% block declaration %} {% for custom_code in custom_codes %} @@ -8,9 +9,7 @@ {% block export %} export default function Component() { - {% for hook in hooks %} - {{ hook }} - {% endfor %} + {{ renderHooks(hooks)}} return ( {{utils.render(render, indent_width=0)}} diff --git a/reflex/.templates/jinja/web/pages/macros.js.jinja2 b/reflex/.templates/jinja/web/pages/macros.js.jinja2 new file mode 100644 index 000000000..68810d896 --- /dev/null +++ b/reflex/.templates/jinja/web/pages/macros.js.jinja2 @@ -0,0 +1,38 @@ +{% macro renderHooks(hooks) %} + {% set sorted_hooks = sort_hooks(hooks) %} + + {# Render the grouped hooks #} + {% for hook, _ in sorted_hooks[const.hook_position.INTERNAL] %} + {{ hook }} + {% endfor %} + + {% for hook, _ in sorted_hooks[const.hook_position.PRE_TRIGGER] %} + {{ hook }} + {% endfor %} + + {% for hook, _ in sorted_hooks[const.hook_position.POST_TRIGGER] %} + {{ hook }} + {% endfor %} +{% endmacro %} + +{% macro renderHooksWithMemo(hooks, memo)%} + {% set sorted_hooks = sort_hooks(hooks) %} + + {# Render the grouped hooks #} + {% for hook, _ in sorted_hooks[const.hook_position.INTERNAL] %} + {{ hook }} + {% endfor %} + + {% for hook, _ in sorted_hooks[const.hook_position.PRE_TRIGGER] %} + {{ hook }} + {% endfor %} + + {% for hook in memo %} + {{ hook }} + {% endfor %} + + {% for hook, _ in sorted_hooks[const.hook_position.POST_TRIGGER] %} + {{ hook }} + {% endfor %} + +{% endmacro %} \ No newline at end of file diff --git a/reflex/.templates/jinja/web/pages/stateful_component.js.jinja2 b/reflex/.templates/jinja/web/pages/stateful_component.js.jinja2 index 4a40ef545..208a5755f 100644 --- a/reflex/.templates/jinja/web/pages/stateful_component.js.jinja2 +++ b/reflex/.templates/jinja/web/pages/stateful_component.js.jinja2 @@ -1,18 +1,10 @@ {% import 'web/pages/utils.js.jinja2' as utils %} +{% from 'web/pages/macros.js.jinja2' import renderHooksWithMemo %} +{% set all_hooks = component._get_all_hooks() %} export function {{tag_name}} () { - {% for hook in component._get_all_hooks_internal() %} - {{ hook }} - {% endfor %} - - {% for hook in memo_trigger_hooks %} - {{ hook }} - {% endfor %} - - {% for hook in component._get_all_hooks() %} - {{ hook }} - {% endfor %} - + {{ renderHooksWithMemo(all_hooks, memo_trigger_hooks) }} + return ( {{utils.render(component.render(), indent_width=0)}} ) diff --git a/reflex/.templates/jinja/web/pages/utils.js.jinja2 b/reflex/.templates/jinja/web/pages/utils.js.jinja2 index 624e3bee8..08aeb0d38 100644 --- a/reflex/.templates/jinja/web/pages/utils.js.jinja2 +++ b/reflex/.templates/jinja/web/pages/utils.js.jinja2 @@ -86,11 +86,11 @@ {% for condition in case[:-1] %} case JSON.stringify({{ condition._js_expr }}): {% endfor %} - return {{ case[-1] }}; + return {{ render(case[-1]) }}; break; {% endfor %} default: - return {{ component.default }}; + return {{ render(component.default) }}; break; } })() diff --git a/reflex/.templates/jinja/web/utils/context.js.jinja2 b/reflex/.templates/jinja/web/utils/context.js.jinja2 index 2428cfa9d..32be61ebb 100644 --- a/reflex/.templates/jinja/web/utils/context.js.jinja2 +++ b/reflex/.templates/jinja/web/utils/context.js.jinja2 @@ -28,7 +28,7 @@ export const state_name = "{{state_name}}" export const exception_state_name = "{{const.frontend_exception_state}}" -// Theses events are triggered on initial load and each page navigation. +// These events are triggered on initial load and each page navigation. export const onLoadInternalEvent = () => { const internal_events = []; diff --git a/reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js b/reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js index dd7886c89..823eeea99 100644 --- a/reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js +++ b/reflex/.templates/web/components/reflex/radix_themes_color_mode_provider.js @@ -16,10 +16,7 @@ export default function RadixThemesColorModeProvider({ children }) { if (isDevMode) { const lastCompiledTimeInLocalStorage = localStorage.getItem("last_compiled_time"); - if ( - lastCompiledTimeInLocalStorage && - lastCompiledTimeInLocalStorage !== lastCompiledTimeStamp - ) { + if (lastCompiledTimeInLocalStorage !== lastCompiledTimeStamp) { // on app startup, make sure the application color mode is persisted correctly. setTheme(defaultColorMode); localStorage.setItem("last_compiled_time", lastCompiledTimeStamp); diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 622f171ad..2f09ac2de 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -3,6 +3,7 @@ import axios from "axios"; import io from "socket.io-client"; import JSON5 from "json5"; import env from "$/env.json"; +import reflexEnvironment from "$/reflex.json"; import Cookies from "universal-cookie"; import { useEffect, useRef, useState } from "react"; import Router, { useRouter } from "next/router"; @@ -40,9 +41,6 @@ let event_processing = false; // Array holding pending events to be processed. const event_queue = []; -// Pending upload promises, by id -const upload_controllers = {}; - /** * Generate a UUID (Used for session tokens). * Taken from: https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid @@ -108,6 +106,18 @@ export const getBackendURL = (url_str) => { return endpoint; }; +/** + * Check if the backend is disabled. + * + * @returns True if the backend is disabled, false otherwise. + */ +export const isBackendDisabled = () => { + const cookie = document.cookie + .split("; ") + .find((row) => row.startsWith("backend-enabled=")); + return cookie !== undefined && cookie.split("=")[1] == "false"; +}; + /** * Determine if any event in the event queue is stateful. * @@ -211,11 +221,16 @@ export const applyEvent = async (event, socket) => { if (event.name == "_download") { const a = document.createElement("a"); a.hidden = true; + a.href = event.payload.url; // Special case when linking to uploaded files - a.href = event.payload.url.replace( - "${getBackendURL(env.UPLOAD)}", - getBackendURL(env.UPLOAD) - ); + if (a.href.includes("getBackendURL(env.UPLOAD)")) { + a.href = eval?.( + event.payload.url.replace( + "getBackendURL(env.UPLOAD)", + `"${getBackendURL(env.UPLOAD)}"`, + ), + ); + } a.download = event.payload.filename; a.click(); a.remove(); @@ -298,10 +313,7 @@ export const applyEvent = async (event, socket) => { // Send the event to the server. if (socket) { - socket.emit( - "event", - JSON.stringify(event, (k, v) => (v === undefined ? null : v)) - ); + socket.emit("event", event); return true; } @@ -329,7 +341,7 @@ export const applyRestEvent = async (event, socket) => { event.payload.files, event.payload.upload_id, event.payload.on_upload_progress, - socket + socket, ); return false; } @@ -396,7 +408,7 @@ export const connect = async ( dispatch, transports, setConnectErrors, - client_storage = {} + client_storage = {}, ) => { // Get backend URL object from the endpoint. const endpoint = getBackendURL(EVENTURL); @@ -405,8 +417,18 @@ export const connect = async ( socket.current = io(endpoint.href, { path: endpoint["pathname"], transports: transports, + protocols: [reflexEnvironment.version], autoUnref: false, }); + // Ensure undefined fields in events are sent as null instead of removed + socket.current.io.encoder.replacer = (k, v) => (v === undefined ? null : v); + socket.current.io.decoder.tryParse = (str) => { + try { + return JSON5.parse(str); + } catch (e) { + return false; + } + }; function checkVisibility() { if (document.visibilityState === "visible") { @@ -443,8 +465,7 @@ export const connect = async ( }); // On each received message, queue the updates and events. - socket.current.on("event", async (message) => { - const update = JSON5.parse(message); + socket.current.on("event", async (update) => { for (const substate in update.delta) { dispatch[substate](update.delta[substate]); } @@ -456,7 +477,7 @@ export const connect = async ( }); socket.current.on("reload", async (event) => { event_processing = false; - queueEvents([...initialEvents(), JSON5.parse(event)], socket); + queueEvents([...initialEvents(), event], socket); }); document.addEventListener("visibilitychange", checkVisibility); @@ -478,14 +499,16 @@ export const uploadFiles = async ( files, upload_id, on_upload_progress, - socket + socket, ) => { // return if there's no file to upload if (files === undefined || files.length === 0) { return false; } - if (upload_controllers[upload_id]) { + const upload_ref_name = `__upload_controllers_${upload_id}`; + + if (refs[upload_ref_name]) { console.log("Upload already in progress for ", upload_id); return false; } @@ -497,23 +520,31 @@ export const uploadFiles = async ( // Whenever called, responseText will contain the entire response so far. const chunks = progressEvent.event.target.responseText.trim().split("\n"); // So only process _new_ chunks beyond resp_idx. - chunks.slice(resp_idx).map((chunk) => { - event_callbacks.map((f, ix) => { - f(chunk) - .then(() => { - if (ix === event_callbacks.length - 1) { - // Mark this chunk as processed. - resp_idx += 1; - } - }) - .catch((e) => { - if (progressEvent.progress === 1) { - // Chunk may be incomplete, so only report errors when full response is available. - console.log("Error parsing chunk", chunk, e); - } - return; - }); - }); + chunks.slice(resp_idx).map((chunk_json) => { + try { + const chunk = JSON5.parse(chunk_json); + event_callbacks.map((f, ix) => { + f(chunk) + .then(() => { + if (ix === event_callbacks.length - 1) { + // Mark this chunk as processed. + resp_idx += 1; + } + }) + .catch((e) => { + if (progressEvent.progress === 1) { + // Chunk may be incomplete, so only report errors when full response is available. + console.log("Error processing chunk", chunk, e); + } + return; + }); + }); + } catch (e) { + if (progressEvent.progress === 1) { + console.log("Error parsing chunk", chunk_json, e); + } + return; + } }); }; @@ -537,7 +568,7 @@ export const uploadFiles = async ( }); // Send the file to the server. - upload_controllers[upload_id] = controller; + refs[upload_ref_name] = controller; try { return await axios.post(getBackendURL(UPLOADURL), formdata, config); @@ -557,7 +588,7 @@ export const uploadFiles = async ( } return false; } finally { - delete upload_controllers[upload_id]; + delete refs[upload_ref_name]; } }; @@ -573,7 +604,7 @@ export const Event = ( name, payload = {}, event_actions = {}, - handler = null + handler = null, ) => { return { name, payload, handler, event_actions }; }; @@ -600,7 +631,7 @@ export const hydrateClientStorage = (client_storage) => { for (const state_key in client_storage.local_storage) { const options = client_storage.local_storage[state_key]; const local_storage_value = localStorage.getItem( - options.name || state_key + options.name || state_key, ); if (local_storage_value !== null) { client_storage_values[state_key] = local_storage_value; @@ -611,7 +642,7 @@ export const hydrateClientStorage = (client_storage) => { for (const state_key in client_storage.session_storage) { const session_options = client_storage.session_storage[state_key]; const session_storage_value = sessionStorage.getItem( - session_options.name || state_key + session_options.name || state_key, ); if (session_storage_value != null) { client_storage_values[state_key] = session_storage_value; @@ -636,7 +667,7 @@ export const hydrateClientStorage = (client_storage) => { const applyClientStorageDelta = (client_storage, delta) => { // find the main state and check for is_hydrated const unqualified_states = Object.keys(delta).filter( - (key) => key.split(".").length === 1 + (key) => key.split(".").length === 1, ); if (unqualified_states.length === 1) { const main_state = delta[unqualified_states[0]]; @@ -670,7 +701,7 @@ const applyClientStorageDelta = (client_storage, delta) => { const session_options = client_storage.session_storage[state_key]; sessionStorage.setItem( session_options.name || state_key, - delta[substate][key] + delta[substate][key], ); } } @@ -690,7 +721,7 @@ const applyClientStorageDelta = (client_storage, delta) => { export const useEventLoop = ( dispatch, initial_events = () => [], - client_storage = {} + client_storage = {}, ) => { const socket = useRef(null); const router = useRouter(); @@ -704,7 +735,7 @@ export const useEventLoop = ( event_actions = events.reduce( (acc, e) => ({ ...acc, ...e.event_actions }), - event_actions ?? {} + event_actions ?? {}, ); const _e = args.filter((o) => o?.preventDefault !== undefined)[0]; @@ -732,7 +763,7 @@ export const useEventLoop = ( debounce( combined_name, () => queueEvents(events, socket), - event_actions.debounce + event_actions.debounce, ); } else { queueEvents(events, socket); @@ -751,7 +782,7 @@ export const useEventLoop = ( query, asPath, }))(router), - })) + })), ); sentHydrate.current = true; } @@ -786,31 +817,42 @@ export const useEventLoop = ( }; }, []); - // Main event loop. + // Handle socket connect/disconnect. useEffect(() => { - // Skip if the router is not ready. - if (!router.isReady) { - return; - } - // only use websockets if state is present - if (Object.keys(initialState).length > 1) { + // only use websockets if state is present and backend is not disabled (reflex cloud). + if (Object.keys(initialState).length > 1 && !isBackendDisabled()) { // Initialize the websocket connection. if (!socket.current) { connect( socket, dispatch, - ["websocket", "polling"], + ["websocket"], setConnectErrors, - client_storage + client_storage, ); } - (async () => { - // Process all outstanding events. - while (event_queue.length > 0 && !event_processing) { - await processEvent(socket.current); - } - })(); } + + // Cleanup function. + return () => { + if (socket.current) { + socket.current.disconnect(); + } + }; + }, []); + + // Main event loop. + useEffect(() => { + // Skip if the router is not ready. + if (!router.isReady || isBackendDisabled()) { + return; + } + (async () => { + // Process all outstanding events. + while (event_queue.length > 0 && !event_processing) { + await processEvent(socket.current); + } + })(); }); // localStorage event handling @@ -834,7 +876,7 @@ export const useEventLoop = ( vars[storage_to_state_map[e.key]] = e.newValue; const event = Event( `${state_name}.reflex___state____update_vars_internal_state.update_vars_internal`, - { vars: vars } + { vars: vars }, ); addEvents([event], e); } @@ -927,7 +969,7 @@ export const getRefValues = (refs) => { return refs.map((ref) => ref.current ? ref.current.value || ref.current.getAttribute("aria-valuenow") - : null + : null, ); }; diff --git a/reflex/__init__.py b/reflex/__init__.py index 562524416..3209b505e 100644 --- a/reflex/__init__.py +++ b/reflex/__init__.py @@ -84,6 +84,9 @@ In the example above, you will be able to do `rx.list` from __future__ import annotations +from types import ModuleType +from typing import Any + from reflex.utils import ( compat, # for side-effects lazy_loader, @@ -303,7 +306,6 @@ _MAPPING: dict = { "event": [ "EventChain", "EventHandler", - "background", "call_script", "call_function", "run_script", @@ -331,7 +333,7 @@ _MAPPING: dict = { "SessionStorage", ], "middleware": ["middleware", "Middleware"], - "model": ["session", "Model"], + "model": ["asession", "session", "Model"], "state": [ "var", "ComponentState", @@ -366,20 +368,5 @@ getattr, __dir__, __all__ = lazy_loader.attach( ) -def __getattr__(name): - if name == "chakra": - from reflex.utils import console - - console.deprecate( - "rx.chakra", - reason="and moved to a separate package. " - "To continue using Chakra UI components, install the `reflex-chakra` package via `pip install " - "reflex-chakra`.", - deprecation_version="0.6.0", - removal_version="0.7.0", - dedupe=True, - ) - import reflex_chakra as rc - - return rc +def __getattr__(name: ModuleType | Any): return getattr(name) diff --git a/reflex/__init__.pyi b/reflex/__init__.pyi index 6f61435e6..5c80269ad 100644 --- a/reflex/__init__.pyi +++ b/reflex/__init__.pyi @@ -131,7 +131,7 @@ from .components.radix.themes.layout.container import container as container from .components.radix.themes.layout.flex import flex as flex from .components.radix.themes.layout.grid import grid as grid from .components.radix.themes.layout.list import list_item as list_item -from .components.radix.themes.layout.list import list_ns as list # noqa +from .components.radix.themes.layout.list import list_ns as list # noqa: F401 from .components.radix.themes.layout.list import ordered_list as ordered_list from .components.radix.themes.layout.list import unordered_list as unordered_list from .components.radix.themes.layout.section import section as section @@ -156,7 +156,6 @@ from .constants import Env as Env from .constants.colors import Color as Color from .event import EventChain as EventChain from .event import EventHandler as EventHandler -from .event import background as background from .event import call_function as call_function from .event import call_script as call_script from .event import clear_local_storage as clear_local_storage @@ -186,6 +185,7 @@ from .istate.wrappers import get_state as get_state from .middleware import Middleware as Middleware from .middleware import middleware as middleware from .model import Model as Model +from .model import asession as asession from .model import session as session from .page import page as page from .state import ComponentState as ComponentState diff --git a/reflex/app.py b/reflex/app.py index cdf21aa35..060f03469 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -17,6 +17,7 @@ import sys import traceback from datetime import datetime from pathlib import Path +from types import SimpleNamespace from typing import ( TYPE_CHECKING, Any, @@ -26,6 +27,7 @@ from typing import ( Dict, Generic, List, + MutableMapping, Optional, Set, Type, @@ -52,12 +54,17 @@ from reflex.compiler.compiler import ExecutorSafeFunctions, compile_theme from reflex.components.base.app_wrap import AppWrap from reflex.components.base.error_boundary import ErrorBoundary from reflex.components.base.fragment import Fragment +from reflex.components.base.strict_mode import StrictMode from reflex.components.component import ( Component, ComponentStyle, evaluate_style_namespaces, ) -from reflex.components.core.banner import connection_pulser, connection_toaster +from reflex.components.core.banner import ( + backend_disabled, + connection_pulser, + connection_toaster, +) from reflex.components.core.breakpoints import set_breakpoints from reflex.components.core.client_side_routing import ( Default404Page, @@ -67,6 +74,7 @@ from reflex.components.core.upload import Upload, get_upload_dir from reflex.components.radix import themes from reflex.config import environment, get_config from reflex.event import ( + _EVENT_FIELDS, BASE_STATE, Event, EventHandler, @@ -143,7 +151,7 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec: position="top-center", id="backend_error", style={"width": "500px"}, - ) # type: ignore + ) else: error_message.insert(0, "An error occurred.") return window_alert("\n".join(error_message)) @@ -155,9 +163,12 @@ def default_overlay_component() -> Component: Returns: The default overlay_component, which is a connection_modal. """ + config = get_config() + return Fragment.create( connection_pulser(), connection_toaster(), + *([backend_disabled()] if config.is_reflex_cloud else []), *codespaces.codespaces_auto_redirect(), ) @@ -249,36 +260,36 @@ class App(MiddlewareMixin, LifespanMixin): # Attributes to add to the html root tag of every page. html_custom_attrs: Optional[Dict[str, str]] = None - # A map from a route to an unevaluated page. PRIVATE. - unevaluated_pages: Dict[str, UnevaluatedPage] = dataclasses.field( + # A map from a route to an unevaluated page. + _unevaluated_pages: Dict[str, UnevaluatedPage] = dataclasses.field( default_factory=dict ) - # A map from a page route to the component to render. Users should use `add_page`. PRIVATE. - pages: Dict[str, Component] = dataclasses.field(default_factory=dict) + # A map from a page route to the component to render. Users should use `add_page`. + _pages: Dict[str, Component] = dataclasses.field(default_factory=dict) - # The backend API object. PRIVATE. - api: FastAPI = None # type: ignore + # The backend API object. + _api: FastAPI | None = None - # The state class to use for the app. PRIVATE. - state: Optional[Type[BaseState]] = None + # The state class to use for the app. + _state: Optional[Type[BaseState]] = None # Class to manage many client states. _state_manager: Optional[StateManager] = None - # Mapping from a route to event handlers to trigger when the page loads. PRIVATE. - load_events: Dict[str, List[IndividualEventType[[], Any]]] = dataclasses.field( + # Mapping from a route to event handlers to trigger when the page loads. + _load_events: Dict[str, List[IndividualEventType[[], Any]]] = dataclasses.field( default_factory=dict ) - # Admin dashboard to view and manage the database. PRIVATE. + # Admin dashboard to view and manage the database. admin_dash: Optional[AdminDash] = None - # The async server name space. PRIVATE. - event_namespace: Optional[EventNamespace] = None + # The async server name space. + _event_namespace: Optional[EventNamespace] = None - # Background tasks that are currently running. PRIVATE. - background_tasks: Set[asyncio.Task] = dataclasses.field(default_factory=set) + # Background tasks that are currently running. + _background_tasks: Set[asyncio.Task] = dataclasses.field(default_factory=set) # Frontend Error Handler Function frontend_exception_handler: Callable[[Exception], None] = ( @@ -290,6 +301,24 @@ class App(MiddlewareMixin, LifespanMixin): [Exception], Union[EventSpec, List[EventSpec], None] ] = default_backend_exception_handler + @property + def api(self) -> FastAPI | None: + """Get the backend api. + + Returns: + The backend api. + """ + return self._api + + @property + def event_namespace(self) -> EventNamespace | None: + """Get the event namespace. + + Returns: + The event namespace. + """ + return self._event_namespace + def __post_init__(self): """Initialize the app. @@ -309,7 +338,7 @@ class App(MiddlewareMixin, LifespanMixin): set_breakpoints(self.style.pop("breakpoints")) # Set up the API. - self.api = FastAPI(lifespan=self._run_lifespan_tasks) + self._api = FastAPI(lifespan=self._run_lifespan_tasks) self._add_cors() self._add_default_endpoints() @@ -332,8 +361,8 @@ class App(MiddlewareMixin, LifespanMixin): def _enable_state(self) -> None: """Enable state for the app.""" - if not self.state: - self.state = State + if not self._state: + self._state = State self._setup_state() def _setup_state(self) -> None: @@ -342,13 +371,13 @@ class App(MiddlewareMixin, LifespanMixin): Raises: RuntimeError: If the socket server is invalid. """ - if not self.state: + if not self._state: return config = get_config() # Set up the state manager. - self._state_manager = StateManager.create(state=self.state) + self._state_manager = StateManager.create(state=self._state) # Set up the Socket.IO AsyncServer. if not self.sio: @@ -363,6 +392,11 @@ class App(MiddlewareMixin, LifespanMixin): max_http_buffer_size=constants.POLLING_MAX_HTTP_BUFFER_SIZE, ping_interval=constants.Ping.INTERVAL, ping_timeout=constants.Ping.TIMEOUT, + json=SimpleNamespace( + dumps=staticmethod(format.json_dumps), + loads=staticmethod(json.loads), + ), + transports=["websocket"], ) elif getattr(self.sio, "async_mode", "") != "asgi": raise RuntimeError( @@ -374,12 +408,42 @@ class App(MiddlewareMixin, LifespanMixin): namespace = config.get_event_namespace() # Create the event namespace and attach the main app. Not related to any paths. - self.event_namespace = EventNamespace(namespace, self) + self._event_namespace = EventNamespace(namespace, self) # Register the event namespace with the socket. self.sio.register_namespace(self.event_namespace) # Mount the socket app with the API. - self.api.mount(str(constants.Endpoint.EVENT), socket_app) + if self.api: + + class HeaderMiddleware: + def __init__(self, app: ASGIApp): + self.app = app + + async def __call__( + self, scope: MutableMapping[str, Any], receive: Any, send: Callable + ): + original_send = send + + async def modified_send(message: dict): + if message["type"] == "websocket.accept": + if scope.get("subprotocols"): + # The following *does* say "subprotocol" instead of "subprotocols", intentionally. + message["subprotocol"] = scope["subprotocols"][0] + + headers = dict(message.get("headers", [])) + header_key = b"sec-websocket-protocol" + if subprotocol := headers.get(header_key): + message["headers"] = [ + *message.get("headers", []), + (header_key, subprotocol), + ] + + return await original_send(message) + + return await self.app(scope, receive, modified_send) + + socket_app_with_headers = HeaderMiddleware(socket_app) + self.api.mount(str(constants.Endpoint.EVENT), socket_app_with_headers) # Check the exception handlers self._validate_exception_handlers() @@ -390,24 +454,35 @@ class App(MiddlewareMixin, LifespanMixin): Returns: The string representation of the app. """ - return f"" + return f"" def __call__(self) -> FastAPI: """Run the backend api instance. + Raises: + ValueError: If the app has not been initialized. + Returns: The backend api. """ + if not self.api: + raise ValueError("The app has not been initialized.") return self.api def _add_default_endpoints(self): """Add default api endpoints (ping).""" # To test the server. + if not self.api: + return + self.api.get(str(constants.Endpoint.PING))(ping) self.api.get(str(constants.Endpoint.HEALTH))(health) def _add_optional_endpoints(self): """Add optional api endpoints (_upload).""" + if not self.api: + return + if Upload.is_used: # To upload files. self.api.post(str(constants.Endpoint.UPLOAD))(upload(self)) @@ -425,12 +500,14 @@ class App(MiddlewareMixin, LifespanMixin): def _add_cors(self): """Add CORS middleware to the app.""" + if not self.api: + return self.api.add_middleware( cors.CORSMiddleware, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], - allow_origins=["*"], + allow_origins=get_config().cors_allowed_origins, ) @property @@ -456,18 +533,12 @@ class App(MiddlewareMixin, LifespanMixin): Returns: The generated component. - - Raises: - exceptions.MatchTypeError: If the return types of match cases in rx.match are different. """ - try: - return component if isinstance(component, Component) else component() - except exceptions.MatchTypeError: - raise + return component if isinstance(component, Component) else component() def add_page( self, - component: Component | ComponentCallable, + component: Component | ComponentCallable | None = None, route: str | None = None, title: str | Var | None = None, description: str | Var | None = None, @@ -490,48 +561,64 @@ class App(MiddlewareMixin, LifespanMixin): meta: The metadata of the page. Raises: - ValueError: When the specified route name already exists. + PageValueError: When the component is not set for a non-404 page. + RouteValueError: When the specified route name already exists. """ # If the route is not set, get it from the callable. if route is None: if not isinstance(component, Callable): - raise ValueError("Route must be set if component is not a callable.") + raise exceptions.RouteValueError( + "Route must be set if component is not a callable." + ) # Format the route. route = format.format_route(component.__name__) else: route = format.format_route(route, format_case=False) + if route == constants.Page404.SLUG: + if component is None: + component = Default404Page.create() + component = wait_for_client_redirect(self._generate_component(component)) + title = title or constants.Page404.TITLE + description = description or constants.Page404.DESCRIPTION + image = image or constants.Page404.IMAGE + else: + if component is None: + raise exceptions.PageValueError( + "Component must be set for a non-404 page." + ) + # Check if the route given is valid verify_route_validity(route) - if route in self.unevaluated_pages and environment.RELOAD_CONFIG.is_set(): + if route in self._unevaluated_pages and environment.RELOAD_CONFIG.is_set(): # when the app is reloaded(typically for app harness tests), we should maintain # the latest render function of a route.This applies typically to decorated pages # since they are only added when app._compile is called. - self.unevaluated_pages.pop(route) + self._unevaluated_pages.pop(route) - if route in self.unevaluated_pages: + if route in self._unevaluated_pages: route_name = ( f"`{route}` or `/`" if route == constants.PageNames.INDEX_ROUTE else f"`{route}`" ) - raise ValueError( + raise exceptions.RouteValueError( f"Duplicate page route {route_name} already exists. Make sure you do not have two" f" pages with the same route" ) # Setup dynamic args for the route. # this state assignment is only required for tests using the deprecated state kwarg for App - state = self.state if self.state else State + state = self._state if self._state else State state.setup_dynamic_args(get_route_args(route)) if on_load: - self.load_events[route] = ( + self._load_events[route] = ( on_load if isinstance(on_load, list) else [on_load] ) - self.unevaluated_pages[route] = UnevaluatedPage( + self._unevaluated_pages[route] = UnevaluatedPage( component=component, route=route, title=title, @@ -541,14 +628,15 @@ class App(MiddlewareMixin, LifespanMixin): meta=meta, ) - def _compile_page(self, route: str): + def _compile_page(self, route: str, save_page: bool = True): """Compile a page. Args: route: The route of the page to compile. + save_page: If True, the compiled page is saved to self._pages. """ component, enable_state = compiler.compile_unevaluated_page( - route, self.unevaluated_pages[route], self.state, self.style, self.theme + route, self._unevaluated_pages[route], self._state, self.style, self.theme ) if enable_state: @@ -556,7 +644,8 @@ class App(MiddlewareMixin, LifespanMixin): # Add the page. self._check_routes_conflict(route) - self.pages[route] = component + if save_page: + self._pages[route] = component def get_load_events(self, route: str) -> list[IndividualEventType[[], Any]]: """Get the load events for a route. @@ -570,7 +659,7 @@ class App(MiddlewareMixin, LifespanMixin): route = route.lstrip("/") if route == "": route = constants.PageNames.INDEX_ROUTE - return self.load_events.get(route, []) + return self._load_events.get(route, []) def _check_routes_conflict(self, new_route: str): """Verify if there is any conflict between the new route and any existing route. @@ -594,10 +683,13 @@ class App(MiddlewareMixin, LifespanMixin): constants.RouteRegex.SINGLE_CATCHALL_SEGMENT, constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT, ) - for route in self.pages: + for route in self._pages: replaced_route = replace_brackets_with_keywords(route) for rw, r, nr in zip( - replaced_route.split("/"), route.split("/"), new_route.split("/") + replaced_route.split("/"), + route.split("/"), + new_route.split("/"), + strict=False, ): if rw in segments and r != nr: # If the slugs in the segments of both routes are not the same, then the route is invalid @@ -628,15 +720,19 @@ class App(MiddlewareMixin, LifespanMixin): Args: component: The component to display at the page. title: The title of the page. - description: The description of the page. image: The image to display on the page. + description: The description of the page. on_load: The event handler(s) that will be called each time the page load. meta: The metadata of the page. """ - if component is None: - component = Default404Page.create() + console.deprecate( + feature_name="App.add_custom_404_page", + reason=f"Use app.add_page(component, route='/{constants.Page404.SLUG}') instead.", + deprecation_version="0.6.7", + removal_version="0.8.0", + ) self.add_page( - component=wait_for_client_redirect(self._generate_component(component)), + component=component, route=constants.Page404.SLUG, title=title or constants.Page404.TITLE, image=image or constants.Page404.IMAGE, @@ -648,6 +744,9 @@ class App(MiddlewareMixin, LifespanMixin): def _setup_admin_dash(self): """Setup the admin dash.""" # Get the admin dash. + if not self.api: + return + admin_dash = self.admin_dash if admin_dash and admin_dash.models: @@ -689,7 +788,7 @@ class App(MiddlewareMixin, LifespanMixin): frontend_packages = get_config().frontend_packages _frontend_packages = [] for package in frontend_packages: - if package in (get_config().tailwind or {}).get("plugins", []): # type: ignore + if package in (get_config().tailwind or {}).get("plugins", []): console.warn( f"Tailwind packages are inferred from 'plugins', remove `{package}` from `frontend_packages`" ) @@ -752,10 +851,10 @@ class App(MiddlewareMixin, LifespanMixin): def _setup_overlay_component(self): """If a State is not used and no overlay_component is specified, do not render the connection modal.""" - if self.state is None and self.overlay_component is default_overlay_component: + if self._state is None and self.overlay_component is default_overlay_component: self.overlay_component = None - for k, component in self.pages.items(): - self.pages[k] = self._add_overlay_to_component(component) + for k, component in self._pages.items(): + self._pages[k] = self._add_overlay_to_component(component) def _add_error_boundary_to_component(self, component: Component) -> Component: if self.error_boundary is None: @@ -767,14 +866,14 @@ class App(MiddlewareMixin, LifespanMixin): def _setup_error_boundary(self): """If a State is not used and no error_boundary is specified, do not render the error boundary.""" - if self.state is None and self.error_boundary is default_error_boundary: + if self._state is None and self.error_boundary is default_error_boundary: self.error_boundary = None - for k, component in self.pages.items(): + for k, component in self._pages.items(): # Skip the 404 page if k == constants.Page404.SLUG: continue - self.pages[k] = self._add_error_boundary_to_component(component) + self._pages[k] = self._add_error_boundary_to_component(component) def _apply_decorated_pages(self): """Add @rx.page decorated pages to the app. @@ -800,21 +899,27 @@ class App(MiddlewareMixin, LifespanMixin): Raises: VarDependencyError: When a computed var has an invalid dependency. """ - if not self.state: + if not self._state: return if not state: - state = self.state + state = self._state for var in state.computed_vars.values(): if not var._cache: continue deps = var._deps(objclass=state) - for dep in deps: - if dep not in state.vars and dep not in state.backend_vars: - raise exceptions.VarDependencyError( - f"ComputedVar {var._js_expr} on state {state.__name__} has an invalid dependency {dep}" - ) + for state_name, dep_set in deps.items(): + state_cls = ( + state.get_root_state().get_class_substate(state_name) + if state_name != state.get_full_name() + else state + ) + for dep in dep_set: + if dep not in state_cls.vars and dep not in state_cls.backend_vars: + raise exceptions.VarDependencyError( + f"ComputedVar {var._js_expr} on state {state.__name__} has an invalid dependency {state_name}.{dep}" + ) for substate in state.class_subclasses: self._validate_var_dependencies(substate) @@ -830,14 +935,14 @@ class App(MiddlewareMixin, LifespanMixin): """ from reflex.utils.exceptions import ReflexRuntimeError - self.pages = {} + self._pages = {} def get_compilation_time() -> str: return str(datetime.now().time()).split(".")[0] # Render a default 404 page if the user didn't supply one - if constants.Page404.SLUG not in self.unevaluated_pages: - self.add_custom_404_page() + if constants.Page404.SLUG not in self._unevaluated_pages: + self.add_page(route=constants.Page404.SLUG) # Fix up the style. self.style = evaluate_style_namespaces(self.style) @@ -852,20 +957,24 @@ class App(MiddlewareMixin, LifespanMixin): # If a theme component was provided, wrap the app with it app_wrappers[(20, "Theme")] = self.theme - for route in self.unevaluated_pages: - console.debug(f"Evaluating page: {route}") - self._compile_page(route) + # Get the env mode. + config = get_config() - # Add the optional endpoints (_upload) - self._add_optional_endpoints() + if config.react_strict_mode: + app_wrappers[(200, "StrictMode")] = StrictMode.create() + + should_compile = self._should_compile() + + if not should_compile: + for route in self._unevaluated_pages: + console.debug(f"Evaluating page: {route}") + self._compile_page(route, save_page=should_compile) + + # Add the optional endpoints (_upload) + self._add_optional_endpoints() - if not self._should_compile(): return - self._validate_var_dependencies() - self._setup_overlay_component() - self._setup_error_boundary() - # Create a progress bar. progress = Progress( *Progress.get_default_columns()[:-1], @@ -874,18 +983,30 @@ class App(MiddlewareMixin, LifespanMixin): ) # try to be somewhat accurate - but still not 100% - adhoc_steps_without_executor = 6 + adhoc_steps_without_executor = 7 fixed_pages_within_executor = 5 progress.start() task = progress.add_task( f"[{get_compilation_time()}] Compiling:", - total=len(self.pages) + total=len(self._pages) + + (len(self._unevaluated_pages) * 2) + fixed_pages_within_executor + adhoc_steps_without_executor, ) - # Get the env mode. - config = get_config() + for route in self._unevaluated_pages: + console.debug(f"Evaluating page: {route}") + self._compile_page(route, save_page=should_compile) + progress.advance(task) + + # Add the optional endpoints (_upload) + self._add_optional_endpoints() + + self._validate_var_dependencies() + self._setup_overlay_component() + self._setup_error_boundary() + + progress.advance(task) # Store the compile results. compile_results = [] @@ -898,7 +1019,7 @@ class App(MiddlewareMixin, LifespanMixin): # This has to happen before compiling stateful components as that # prevents recursive functions from reaching all components. - for component in self.pages.values(): + for component in self._pages.values(): # Add component._get_all_imports() to all_imports. all_imports.update(component._get_all_imports()) @@ -913,12 +1034,12 @@ class App(MiddlewareMixin, LifespanMixin): stateful_components_path, stateful_components_code, page_components, - ) = compiler.compile_stateful_components(self.pages.values()) + ) = compiler.compile_stateful_components(self._pages.values()) progress.advance(task) # Catch "static" apps (that do not define a rx.State subclass) which are trying to access rx.State. - if code_uses_state_contexts(stateful_components_code) and self.state is None: + if code_uses_state_contexts(stateful_components_code) and self._state is None: raise ReflexRuntimeError( "To access rx.State in frontend components, at least one " "subclass of rx.State must be defined in the app." @@ -932,7 +1053,7 @@ class App(MiddlewareMixin, LifespanMixin): compiler.compile_document_root( self.head_components, html_lang=self.html_lang, - html_custom_attrs=self.html_custom_attrs, # type: ignore + html_custom_attrs=self.html_custom_attrs, # pyright: ignore [reportArgumentType] ) ) @@ -947,29 +1068,28 @@ class App(MiddlewareMixin, LifespanMixin): is not None ): executor = concurrent.futures.ProcessPoolExecutor( - max_workers=number_of_processes, + max_workers=number_of_processes or None, mp_context=multiprocessing.get_context("fork"), ) else: executor = concurrent.futures.ThreadPoolExecutor( - max_workers=environment.REFLEX_COMPILE_THREADS.get() + max_workers=environment.REFLEX_COMPILE_THREADS.get() or None ) - for route, component in zip(self.pages, page_components): + for route, component in zip(self._pages, page_components, strict=True): ExecutorSafeFunctions.COMPONENTS[route] = component - ExecutorSafeFunctions.STATE = self.state + ExecutorSafeFunctions.STATE = self._state with executor: result_futures = [] - def _submit_work(fn, *args, **kwargs): + def _submit_work(fn: Callable, *args, **kwargs): f = executor.submit(fn, *args, **kwargs) - # f = executor.apipe(fn, *args, **kwargs) result_futures.append(f) # Compile the pre-compiled pages. - for route in self.pages: + for route in self._pages: _submit_work( ExecutorSafeFunctions.compile_page, route, @@ -1004,7 +1124,7 @@ class App(MiddlewareMixin, LifespanMixin): # Compile the contexts. compile_results.append( - compiler.compile_contexts(self.state, self.theme), + compiler.compile_contexts(self._state, self.theme), ) if self.theme is not None: # Fix #2992 by removing the top-level appearance prop @@ -1126,9 +1246,9 @@ class App(MiddlewareMixin, LifespanMixin): ) task = asyncio.create_task(_coro()) - self.background_tasks.add(task) + self._background_tasks.add(task) # Clean up task from background_tasks set when complete. - task.add_done_callback(self.background_tasks.discard) + task.add_done_callback(self._background_tasks.discard) return task def _validate_exception_handlers(self): @@ -1138,11 +1258,11 @@ class App(MiddlewareMixin, LifespanMixin): ValueError: If the custom exception handlers are invalid. """ - FRONTEND_ARG_SPEC = { + frontend_arg_spec = { "exception": Exception, } - BACKEND_ARG_SPEC = { + backend_arg_spec = { "exception": Exception, } @@ -1150,14 +1270,15 @@ class App(MiddlewareMixin, LifespanMixin): ["frontend", "backend"], [self.frontend_exception_handler, self.backend_exception_handler], [ - FRONTEND_ARG_SPEC, - BACKEND_ARG_SPEC, + frontend_arg_spec, + backend_arg_spec, ], + strict=True, ): if hasattr(handler_fn, "__name__"): _fn_name = handler_fn.__name__ else: - _fn_name = handler_fn.__class__.__name__ + _fn_name = type(handler_fn).__name__ if isinstance(handler_fn, functools.partial): raise ValueError( @@ -1193,7 +1314,7 @@ class App(MiddlewareMixin, LifespanMixin): ): raise ValueError( f"Provided custom {handler_domain} exception handler `{_fn_name}` has the wrong argument order." - f"Expected `{required_arg}` as the {required_arg_index+1} argument but got `{list(arg_annotations.keys())[required_arg_index]}`" + f"Expected `{required_arg}` as the {required_arg_index + 1} argument but got `{list(arg_annotations.keys())[required_arg_index]}`" ) if not issubclass(arg_annotations[required_arg], Exception): @@ -1270,7 +1391,7 @@ async def process( await asyncio.create_task( app.event_namespace.emit( "reload", - data=format.json_dumps(event), + data=event, to=sid, ) ) @@ -1294,15 +1415,14 @@ async def process( if app._process_background(state, event) is not None: # `final=True` allows the frontend send more events immediately. yield StateUpdate(final=True) - return + else: + # Process the event synchronously. + async for update in state._process(event): + # Postprocess the event. + update = await app._postprocess(state, event, update) - # Process the event synchronously. - async for update in state._process(event): - # Postprocess the event. - update = await app._postprocess(state, event, update) - - # Yield the update. - yield update + # Yield the update. + yield update except Exception as ex: telemetry.send_error(ex, context="backend") @@ -1331,20 +1451,22 @@ async def health() -> JSONResponse: health_status = {"status": True} status_code = 200 - db_status, redis_status = await asyncio.gather( - get_db_status(), prerequisites.get_redis_status() - ) + tasks = [] - health_status["db"] = db_status + if prerequisites.check_db_used(): + tasks.append(get_db_status()) + if prerequisites.check_redis_used(): + tasks.append(prerequisites.get_redis_status()) - if redis_status is None: + results = await asyncio.gather(*tasks) + + for result in results: + health_status |= result + + if "redis" in health_status and health_status["redis"] is None: health_status["redis"] = False - else: - health_status["redis"] = redis_status - if not health_status["db"] or ( - not health_status["redis"] and redis_status is not None - ): + if not all(health_status.values()): health_status["status"] = False status_code = 503 @@ -1495,16 +1617,20 @@ class EventNamespace(AsyncNamespace): self.sid_to_token = {} self.app = app - def on_connect(self, sid, environ): + def on_connect(self, sid: str, environ: dict): """Event for when the websocket is connected. Args: sid: The Socket.IO session id. environ: The request information, including HTTP headers. """ - pass + subprotocol = environ.get("HTTP_SEC_WEBSOCKET_PROTOCOL") + if subprotocol and subprotocol != constants.Reflex.VERSION: + console.warn( + f"Frontend version {subprotocol} for session {sid} does not match the backend version {constants.Reflex.VERSION}." + ) - def on_disconnect(self, sid): + def on_disconnect(self, sid: str): """Event for when the websocket disconnects. Args: @@ -1523,10 +1649,10 @@ class EventNamespace(AsyncNamespace): """ # Creating a task prevents the update from being blocked behind other coroutines. await asyncio.create_task( - self.emit(str(constants.SocketEvent.EVENT), update.json(), to=sid) + self.emit(str(constants.SocketEvent.EVENT), update, to=sid) ) - async def on_event(self, sid, data): + async def on_event(self, sid: str, data: Any): """Event for receiving front-end websocket events. Raises: @@ -1535,12 +1661,36 @@ class EventNamespace(AsyncNamespace): Args: sid: The Socket.IO session id. data: The event data. + + Raises: + EventDeserializationError: If the event data is not a dictionary. """ - fields = json.loads(data) - # Get the event. - event = Event( - **{k: v for k, v in fields.items() if k not in ("handler", "event_actions")} - ) + fields = data + + if isinstance(fields, str): + console.warn( + "Received event data as a string. This generally should not happen and may indicate a bug." + f" Event data: {fields}" + ) + try: + fields = json.loads(fields) + except json.JSONDecodeError as ex: + raise exceptions.EventDeserializationError( + f"Failed to deserialize event data: {fields}." + ) from ex + + if not isinstance(fields, dict): + raise exceptions.EventDeserializationError( + f"Event data must be a dictionary, but received {fields} of type {type(fields)}." + ) + + try: + # Get the event. + event = Event(**{k: v for k, v in fields.items() if k in _EVENT_FIELDS}) + except (TypeError, ValueError) as ex: + raise exceptions.EventDeserializationError( + f"Failed to deserialize event data: {fields}." + ) from ex self.token_to_sid[event.token] = sid self.sid_to_token[sid] = event.token @@ -1569,7 +1719,7 @@ class EventNamespace(AsyncNamespace): # Emit the update from processing the event. await self.emit_update(update=update, sid=sid) - async def on_ping(self, sid): + async def on_ping(self, sid: str): """Event for testing the API endpoint. Args: diff --git a/reflex/app_mixins/lifespan.py b/reflex/app_mixins/lifespan.py index 52bf0be1d..50b90f25c 100644 --- a/reflex/app_mixins/lifespan.py +++ b/reflex/app_mixins/lifespan.py @@ -12,7 +12,7 @@ from typing import Callable, Coroutine, Set, Union from fastapi import FastAPI from reflex.utils import console -from reflex.utils.exceptions import InvalidLifespanTaskType +from reflex.utils.exceptions import InvalidLifespanTaskTypeError from .mixin import AppMixin @@ -32,7 +32,7 @@ class LifespanMixin(AppMixin): try: async with contextlib.AsyncExitStack() as stack: for task in self.lifespan_tasks: - run_msg = f"Started lifespan task: {task.__name__} as {{type}}" # type: ignore + run_msg = f"Started lifespan task: {task.__name__} as {{type}}" # pyright: ignore [reportAttributeAccessIssue] if isinstance(task, asyncio.Task): running_tasks.append(task) else: @@ -61,19 +61,19 @@ class LifespanMixin(AppMixin): Args: task: The task to register. - task_kwargs: The kwargs of the task. + **task_kwargs: The kwargs of the task. Raises: - InvalidLifespanTaskType: If the task is a generator function. + InvalidLifespanTaskTypeError: If the task is a generator function. """ if inspect.isgeneratorfunction(task) or inspect.isasyncgenfunction(task): - raise InvalidLifespanTaskType( + raise InvalidLifespanTaskTypeError( f"Task {task.__name__} of type generator must be decorated with contextlib.asynccontextmanager." ) if task_kwargs: original_task = task - task = functools.partial(task, **task_kwargs) # type: ignore - functools.update_wrapper(task, original_task) # type: ignore - self.lifespan_tasks.add(task) # type: ignore - console.debug(f"Registered lifespan task: {task.__name__}") # type: ignore + task = functools.partial(task, **task_kwargs) # pyright: ignore [reportArgumentType] + functools.update_wrapper(task, original_task) # pyright: ignore [reportArgumentType] + self.lifespan_tasks.add(task) + console.debug(f"Registered lifespan task: {task.__name__}") # pyright: ignore [reportAttributeAccessIssue] diff --git a/reflex/app_mixins/middleware.py b/reflex/app_mixins/middleware.py index 30593d9ae..c81fd7806 100644 --- a/reflex/app_mixins/middleware.py +++ b/reflex/app_mixins/middleware.py @@ -53,11 +53,11 @@ class MiddlewareMixin(AppMixin): """ for middleware in self.middleware: if asyncio.iscoroutinefunction(middleware.preprocess): - out = await middleware.preprocess(app=self, state=state, event=event) # type: ignore + out = await middleware.preprocess(app=self, state=state, event=event) # pyright: ignore [reportArgumentType] else: - out = middleware.preprocess(app=self, state=state, event=event) # type: ignore + out = middleware.preprocess(app=self, state=state, event=event) # pyright: ignore [reportArgumentType] if out is not None: - return out # type: ignore + return out # pyright: ignore [reportReturnType] async def _postprocess( self, state: BaseState, event: Event, update: StateUpdate @@ -78,18 +78,18 @@ class MiddlewareMixin(AppMixin): for middleware in self.middleware: if asyncio.iscoroutinefunction(middleware.postprocess): out = await middleware.postprocess( - app=self, # type: ignore + app=self, # pyright: ignore [reportArgumentType] state=state, event=event, update=update, ) else: out = middleware.postprocess( - app=self, # type: ignore + app=self, # pyright: ignore [reportArgumentType] state=state, event=event, update=update, ) if out is not None: - return out # type: ignore + return out # pyright: ignore [reportReturnType] return update diff --git a/reflex/app_module_for_backend.py b/reflex/app_module_for_backend.py index 8109fc3d6..28be30410 100644 --- a/reflex/app_module_for_backend.py +++ b/reflex/app_module_for_backend.py @@ -5,16 +5,13 @@ Only the app attribute is explicitly exposed. from concurrent.futures import ThreadPoolExecutor from reflex import constants -from reflex.utils import telemetry from reflex.utils.exec import is_prod_mode -from reflex.utils.prerequisites import get_app +from reflex.utils.prerequisites import get_and_validate_app if constants.CompileVars.APP != "app": raise AssertionError("unexpected variable name for 'app'") -telemetry.send("compile") -app_module = get_app(reload=False) -app = getattr(app_module, constants.CompileVars.APP) +app, app_module = get_and_validate_app(reload=False) # For py3.9 compatibility when redis is used, we MUST add any decorator pages # before compiling the app in a thread to avoid event loop error (REF-2172). app._apply_decorated_pages() @@ -30,8 +27,7 @@ if is_prod_mode(): # ensure only "app" is exposed. del app_module del compile_future -del get_app +del get_and_validate_app del is_prod_mode -del telemetry del constants del ThreadPoolExecutor diff --git a/reflex/assets.py b/reflex/assets.py index 8a50664b6..a9aa7a6a9 100644 --- a/reflex/assets.py +++ b/reflex/assets.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Optional from reflex import constants -from reflex.utils.exec import is_backend_only +from reflex.config import EnvironmentVariables def asset( @@ -52,7 +52,7 @@ def asset( The relative URL to the asset. """ assets = constants.Dirs.APP_ASSETS - backend_only = is_backend_only() + backend_only = EnvironmentVariables.REFLEX_BACKEND_ONLY.get() # Local asset handling if not shared: diff --git a/reflex/base.py b/reflex/base.py index 692f123a8..f6bbb8ce4 100644 --- a/reflex/base.py +++ b/reflex/base.py @@ -13,7 +13,7 @@ except ModuleNotFoundError: if not TYPE_CHECKING: import pydantic.main as pydantic_main from pydantic import BaseModel - from pydantic.fields import ModelField # type: ignore + from pydantic.fields import ModelField def validate_field_name(bases: List[Type["BaseModel"]], field_name: str) -> None: @@ -30,26 +30,27 @@ def validate_field_name(bases: List[Type["BaseModel"]], field_name: str) -> None # can't use reflex.config.environment here cause of circular import reload = os.getenv("__RELOAD_CONFIG", "").lower() == "true" - for base in bases: - try: + base = None + try: + for base in bases: if not reload and getattr(base, field_name, None): pass - except TypeError as te: - raise VarNameError( - f'State var "{field_name}" in {base} has been shadowed by a substate var; ' - f'use a different field name instead".' - ) from te + except TypeError as te: + raise VarNameError( + f'State var "{field_name}" in {base} has been shadowed by a substate var; ' + f'use a different field name instead".' + ) from te # monkeypatch pydantic validate_field_name method to skip validating # shadowed state vars when reloading app via utils.prerequisites.get_app(reload=True) -pydantic_main.validate_field_name = validate_field_name # type: ignore +pydantic_main.validate_field_name = validate_field_name # pyright: ignore [reportPossiblyUnboundVariable, reportPrivateImportUsage] if TYPE_CHECKING: from reflex.vars import Var -class Base(BaseModel): # pyright: ignore [reportUnboundVariable] +class Base(BaseModel): # pyright: ignore [reportPossiblyUnboundVariable] """The base class subclassed by all Reflex classes. This class wraps Pydantic and provides common methods such as @@ -74,12 +75,12 @@ class Base(BaseModel): # pyright: ignore [reportUnboundVariable] """ from reflex.utils.serializers import serialize - return self.__config__.json_dumps( # type: ignore + return self.__config__.json_dumps( self.dict(), default=serialize, ) - def set(self, **kwargs): + def set(self, **kwargs: Any): """Set multiple fields and return the object. Args: @@ -112,12 +113,12 @@ class Base(BaseModel): # pyright: ignore [reportUnboundVariable] default_value: The default value of the field """ var_name = var._var_field_name - new_field = ModelField.infer( + new_field = ModelField.infer( # pyright: ignore [reportPossiblyUnboundVariable] name=var_name, value=default_value, annotation=var._var_type, class_validators=None, - config=cls.__config__, # type: ignore + config=cls.__config__, ) cls.__fields__.update({var_name: new_field}) diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index 9f81f319d..c2a76aad3 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -75,7 +75,7 @@ def _compile_app(app_root: Component) -> str: return templates.APP_ROOT.render( imports=utils.compile_imports(app_root._get_all_imports()), custom_codes=app_root._get_all_custom_code(), - hooks={**app_root._get_all_hooks_internal(), **app_root._get_all_hooks()}, + hooks=app_root._get_all_hooks(), window_libraries=window_libraries, render=app_root.render(), ) @@ -149,7 +149,7 @@ def _compile_page( imports=imports, dynamic_imports=component._get_all_dynamic_imports(), custom_codes=component._get_all_custom_code(), - hooks={**component._get_all_hooks_internal(), **component._get_all_hooks()}, + hooks=component._get_all_hooks(), render=component.render(), **kwargs, ) @@ -239,11 +239,19 @@ def _compile_components( component_renders.append(component_render) imports = utils.merge_imports(imports, component_imports) + dynamic_imports = { + comp_import: None + for comp_render in component_renders + if "dynamic_imports" in comp_render + for comp_import in comp_render["dynamic_imports"] + } + # Compile the components page. return ( templates.COMPONENTS.render( imports=utils.compile_imports(imports), components=component_renders, + dynamic_imports=dynamic_imports, ), imports, ) diff --git a/reflex/compiler/templates.py b/reflex/compiler/templates.py index c868a0cbb..117b655a9 100644 --- a/reflex/compiler/templates.py +++ b/reflex/compiler/templates.py @@ -1,9 +1,46 @@ """Templates to use in the reflex compiler.""" +from __future__ import annotations + from jinja2 import Environment, FileSystemLoader, Template from reflex import constants +from reflex.constants import Hooks from reflex.utils.format import format_state_name, json_dumps +from reflex.vars.base import VarData + + +def _sort_hooks(hooks: dict[str, VarData | None]): + """Sort the hooks by their position. + + Args: + hooks: The hooks to sort. + + Returns: + The sorted hooks. + """ + sorted_hooks = { + Hooks.HookPosition.INTERNAL: [], + Hooks.HookPosition.PRE_TRIGGER: [], + Hooks.HookPosition.POST_TRIGGER: [], + } + + for hook, data in hooks.items(): + if data and data.position and data.position == Hooks.HookPosition.INTERNAL: + sorted_hooks[Hooks.HookPosition.INTERNAL].append((hook, data)) + elif not data or ( + not data.position + or data.position == constants.Hooks.HookPosition.PRE_TRIGGER + ): + sorted_hooks[Hooks.HookPosition.PRE_TRIGGER].append((hook, data)) + elif ( + data + and data.position + and data.position == constants.Hooks.HookPosition.POST_TRIGGER + ): + sorted_hooks[Hooks.HookPosition.POST_TRIGGER].append((hook, data)) + + return sorted_hooks class ReflexJinjaEnvironment(Environment): @@ -45,7 +82,9 @@ class ReflexJinjaEnvironment(Environment): "on_load_internal": constants.CompileVars.ON_LOAD_INTERNAL, "update_vars_internal": constants.CompileVars.UPDATE_VARS_INTERNAL, "frontend_exception_state": constants.CompileVars.FRONTEND_EXCEPTION_STATE_FULL, + "hook_position": constants.Hooks.HookPosition, } + self.globals["sort_hooks"] = _sort_hooks def get_template(name: str) -> Template: @@ -102,6 +141,9 @@ STYLE = get_template("web/styles/styles.css.jinja2") # Code that generate the package json file PACKAGE_JSON = get_template("web/package.json.jinja2") +# Template containing some macros used in the web pages. +MACROS = get_template("web/pages/macros.js.jinja2") + # Code that generate the pyproject.toml file for custom components. CUSTOM_COMPONENTS_PYPROJECT_TOML = get_template( "custom_components/pyproject.toml.jinja2" diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index 29398da87..57241fea9 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -2,17 +2,24 @@ from __future__ import annotations +import asyncio +import concurrent.futures +import traceback +from datetime import datetime from pathlib import Path from typing import Any, Callable, Dict, Optional, Type, Union from urllib.parse import urlparse +from reflex.utils.exec import is_in_app_harness from reflex.utils.prerequisites import get_web_dir from reflex.vars.base import Var try: from pydantic.v1.fields import ModelField except ModuleNotFoundError: - from pydantic.fields import ModelField # type: ignore + from pydantic.fields import ( + ModelField, # pyright: ignore [reportAttributeAccessIssue] + ) from reflex import constants from reflex.components.base import ( @@ -29,7 +36,7 @@ from reflex.components.base import ( ) from reflex.components.component import Component, ComponentStyle, CustomComponent from reflex.istate.storage import Cookie, LocalStorage, SessionStorage -from reflex.state import BaseState +from reflex.state import BaseState, _resolve_delta from reflex.style import Style from reflex.utils import console, format, imports, path_ops from reflex.utils.imports import ImportVar, ParsedImportDict @@ -115,7 +122,7 @@ def compile_imports(import_dict: ParsedImportDict) -> list[dict]: default, rest = compile_import_statement(fields) # prevent lib from being rendered on the page if all imports are non rendered kind - if not any({f.render for f in fields}): # type: ignore + if not any(f.render for f in fields): continue if not lib: @@ -123,8 +130,7 @@ def compile_imports(import_dict: ParsedImportDict) -> list[dict]: raise ValueError("No default field allowed for empty library.") if rest is None or len(rest) == 0: raise ValueError("No fields to import.") - for module in sorted(rest): - import_dicts.append(get_import_dict(module)) + import_dicts.extend(get_import_dict(module) for module in sorted(rest)) continue # remove the version before rendering the package imports @@ -164,13 +170,34 @@ def compile_state(state: Type[BaseState]) -> dict: try: initial_state = state(_reflex_internal_init=True).dict(initial=True) except Exception as e: + timestamp = datetime.now().strftime("%Y-%m-%d__%H-%M-%S") + constants.Reflex.LOGS_DIR.mkdir(parents=True, exist_ok=True) + log_path = constants.Reflex.LOGS_DIR / f"state_compile_error_{timestamp}.log" + traceback.TracebackException.from_exception(e).print(file=log_path.open("w+")) console.warn( - f"Failed to compile initial state with computed vars, excluding them: {e}" + f"Failed to compile initial state with computed vars. Error log saved to {log_path}" ) initial_state = state(_reflex_internal_init=True).dict( initial=True, include_computed=False ) - return initial_state + try: + _ = asyncio.get_running_loop() + except RuntimeError: + pass + else: + if is_in_app_harness(): + # Playwright tests already have an event loop running, so we can't use asyncio.run. + with concurrent.futures.ThreadPoolExecutor() as pool: + resolved_initial_state = pool.submit( + asyncio.run, _resolve_delta(initial_state) + ).result() + console.warn( + f"Had to get initial state in a thread 🤮 {resolved_initial_state}", + ) + return resolved_initial_state + + # Normally the compile runs before any event loop starts, we asyncio.run is available for calling. + return asyncio.run(_resolve_delta(initial_state)) def _compile_client_storage_field( @@ -291,8 +318,9 @@ def compile_custom_component( "name": component.tag, "props": props, "render": render.render(), - "hooks": {**render._get_all_hooks_internal(), **render._get_all_hooks()}, + "hooks": render._get_all_hooks(), "custom_code": render._get_all_custom_code(), + "dynamic_imports": render._get_all_dynamic_imports(), }, imports, ) @@ -495,7 +523,7 @@ def empty_dir(path: str | Path, keep_files: list[str] | None = None): path_ops.rm(element) -def is_valid_url(url) -> bool: +def is_valid_url(url: str) -> bool: """Check if a url is valid. Args: diff --git a/reflex/components/base/bare.py b/reflex/components/base/bare.py index e1b5d9237..0f0bef8b9 100644 --- a/reflex/components/base/bare.py +++ b/reflex/components/base/bare.py @@ -9,6 +9,7 @@ from reflex.components.tags import Tag from reflex.components.tags.tagless import Tagless from reflex.utils.imports import ParsedImportDict from reflex.vars import BooleanVar, ObjectVar, Var +from reflex.vars.base import VarData class Bare(Component): @@ -30,9 +31,9 @@ class Bare(Component): return cls(contents=contents) else: contents = str(contents) if contents is not None else "" - return cls(contents=contents) # type: ignore + return cls(contents=contents) - def _get_all_hooks_internal(self) -> dict[str, None]: + def _get_all_hooks_internal(self) -> dict[str, VarData | None]: """Include the hooks for the component. Returns: @@ -43,7 +44,7 @@ class Bare(Component): hooks |= self.contents._var_value._get_all_hooks_internal() return hooks - def _get_all_hooks(self) -> dict[str, None]: + def _get_all_hooks(self) -> dict[str, VarData | None]: """Include the hooks for the component. Returns: @@ -107,11 +108,14 @@ class Bare(Component): return Tagless(contents=f"{{{self.contents!s}}}") return Tagless(contents=str(self.contents)) - def _get_vars(self, include_children: bool = False) -> Iterator[Var]: + def _get_vars( + self, include_children: bool = False, ignore_ids: set[int] | None = None + ) -> Iterator[Var]: """Walk all Vars used in this component. Args: include_children: Whether to include Vars from children. + ignore_ids: The ids to ignore. Yields: The contents if it is a Var, otherwise nothing. diff --git a/reflex/components/base/error_boundary.py b/reflex/components/base/error_boundary.py index f328773c2..74867a757 100644 --- a/reflex/components/base/error_boundary.py +++ b/reflex/components/base/error_boundary.py @@ -11,10 +11,11 @@ from reflex.event import EventHandler, set_clipboard from reflex.state import FrontendEventExceptionState from reflex.vars.base import Var from reflex.vars.function import ArgsFunctionOperation +from reflex.vars.object import ObjectVar def on_error_spec( - error: Var[Dict[str, str]], info: Var[Dict[str, str]] + error: ObjectVar[Dict[str, str]], info: ObjectVar[Dict[str, str]] ) -> Tuple[Var[str], Var[str]]: """The spec for the on_error event handler. diff --git a/reflex/components/base/error_boundary.pyi b/reflex/components/base/error_boundary.pyi index 2e01c7da0..8d27af0f3 100644 --- a/reflex/components/base/error_boundary.pyi +++ b/reflex/components/base/error_boundary.pyi @@ -9,9 +9,10 @@ from reflex.components.component import Component from reflex.event import BASE_STATE, EventType from reflex.style import Style from reflex.vars.base import Var +from reflex.vars.object import ObjectVar def on_error_spec( - error: Var[Dict[str, str]], info: Var[Dict[str, str]] + error: ObjectVar[Dict[str, str]], info: ObjectVar[Dict[str, str]] ) -> Tuple[Var[str], Var[str]]: ... class ErrorBoundary(Component): diff --git a/reflex/components/base/meta.py b/reflex/components/base/meta.py index 526233c8b..10264009e 100644 --- a/reflex/components/base/meta.py +++ b/reflex/components/base/meta.py @@ -53,11 +53,11 @@ class Description(Meta): """A component that displays the title of the current page.""" # The type of the description. - name: str = "description" + name: str | None = "description" class Image(Meta): """A component that displays the title of the current page.""" # The type of the image. - property: str = "og:image" + property: str | None = "og:image" diff --git a/reflex/components/base/strict_mode.py b/reflex/components/base/strict_mode.py new file mode 100644 index 000000000..46b01ad87 --- /dev/null +++ b/reflex/components/base/strict_mode.py @@ -0,0 +1,10 @@ +"""Module for the StrictMode component.""" + +from reflex.components.component import Component + + +class StrictMode(Component): + """A React strict mode component to enable strict mode for its children.""" + + library = "react" + tag = "StrictMode" diff --git a/reflex/components/base/strict_mode.pyi b/reflex/components/base/strict_mode.pyi new file mode 100644 index 000000000..9005c0222 --- /dev/null +++ b/reflex/components/base/strict_mode.pyi @@ -0,0 +1,57 @@ +"""Stub file for reflex/components/base/strict_mode.py""" + +# ------------------- DO NOT EDIT ---------------------- +# This file was generated by `reflex/utils/pyi_generator.py`! +# ------------------------------------------------------ +from typing import Any, Dict, Optional, Union, overload + +from reflex.components.component import Component +from reflex.event import BASE_STATE, EventType +from reflex.style import Style +from reflex.vars.base import Var + +class StrictMode(Component): + @overload + @classmethod + def create( # type: ignore + cls, + *children, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_blur: Optional[EventType[[], BASE_STATE]] = None, + on_click: Optional[EventType[[], BASE_STATE]] = None, + on_context_menu: Optional[EventType[[], BASE_STATE]] = None, + on_double_click: Optional[EventType[[], BASE_STATE]] = None, + on_focus: Optional[EventType[[], BASE_STATE]] = None, + on_mount: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_down: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_enter: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_leave: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_move: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_out: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_over: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_up: Optional[EventType[[], BASE_STATE]] = None, + on_scroll: Optional[EventType[[], BASE_STATE]] = None, + on_unmount: Optional[EventType[[], BASE_STATE]] = None, + **props, + ) -> "StrictMode": + """Create the component. + + Args: + *children: The children of the component. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The props of the component. + + Returns: + The component. + """ + ... diff --git a/reflex/components/component.py b/reflex/components/component.py index 5c1c4941d..6d1264f4d 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -43,13 +43,8 @@ from reflex.constants.state import FRONTEND_EVENT_STATE from reflex.event import ( EventCallback, EventChain, - EventChainVar, EventHandler, EventSpec, - EventVar, - call_event_fn, - call_event_handler, - get_handler_args, no_args_event_spec, ) from reflex.style import Style, format_as_emotion @@ -104,7 +99,7 @@ class BaseComponent(Base, ABC): """ @abstractmethod - def _get_all_hooks_internal(self) -> dict[str, None]: + def _get_all_hooks_internal(self) -> dict[str, VarData | None]: """Get the reflex internal hooks for the component and its children. Returns: @@ -112,7 +107,7 @@ class BaseComponent(Base, ABC): """ @abstractmethod - def _get_all_hooks(self) -> dict[str, None]: + def _get_all_hooks(self) -> dict[str, VarData | None]: """Get the React hooks for this component. Returns: @@ -155,13 +150,13 @@ class BaseComponent(Base, ABC): class ComponentNamespace(SimpleNamespace): """A namespace to manage components with subcomponents.""" - def __hash__(self) -> int: + def __hash__(self) -> int: # pyright: ignore [reportIncompatibleVariableOverride] """Get the hash of the namespace. Returns: The hash of the namespace. """ - return hash(self.__class__.__name__) + return hash(type(self).__name__) def evaluate_style_namespaces(style: ComponentStyle) -> dict: @@ -431,20 +426,22 @@ class Component(BaseComponent, ABC): else: continue + def determine_key(value: Any): + # Try to create a var from the value + key = value if isinstance(value, Var) else LiteralVar.create(value) + + # Check that the var type is not None. + if key is None: + raise TypeError + + return key + # Check whether the key is a component prop. if types._issubclass(field_type, Var): # Used to store the passed types if var type is a union. passed_types = None try: - # Try to create a var from the value. - if isinstance(value, Var): - kwargs[key] = value - else: - kwargs[key] = LiteralVar.create(value) - - # Check that the var type is not None. - if kwargs[key] is None: - raise TypeError + kwargs[key] = determine_key(value) expected_type = fields[key].outer_type_.__args__[0] # validate literal fields. @@ -465,9 +462,7 @@ class Component(BaseComponent, ABC): if types.is_union(passed_type): # We need to check all possible types in the union. passed_types = ( - arg - for arg in passed_type.__args__ # type: ignore - if arg is not type(None) + arg for arg in passed_type.__args__ if arg is not type(None) ) if ( # If the passed var is a union, check if all possible types are valid. @@ -493,9 +488,8 @@ class Component(BaseComponent, ABC): ) # Check if the key is an event trigger. if key in component_specific_triggers: - # Temporarily disable full control for event triggers. - kwargs["event_triggers"][key] = self._create_event_chain( - value=value, # type: ignore + kwargs["event_triggers"][key] = EventChain.create( + value=value, args_spec=component_specific_triggers[key], key=key, ) @@ -548,104 +542,6 @@ class Component(BaseComponent, ABC): # Construct the component. super().__init__(*args, **kwargs) - def _create_event_chain( - self, - args_spec: types.ArgsSpec | Sequence[types.ArgsSpec], - value: Union[ - Var, - EventHandler, - EventSpec, - List[Union[EventHandler, EventSpec, EventVar]], - Callable, - ], - key: Optional[str] = None, - ) -> Union[EventChain, Var]: - """Create an event chain from a variety of input types. - - Args: - args_spec: The args_spec of the event trigger being bound. - value: The value to create the event chain from. - key: The key of the event trigger being bound. - - Returns: - The event chain. - - Raises: - ValueError: If the value is not a valid event chain. - """ - # If it's an event chain var, return it. - if isinstance(value, Var): - if isinstance(value, EventChainVar): - return value - elif isinstance(value, EventVar): - value = [value] - elif issubclass(value._var_type, (EventChain, EventSpec)): - return self._create_event_chain(args_spec, value.guess_type(), key=key) - else: - raise ValueError( - f"Invalid event chain: {value!s} of type {value._var_type}" - ) - elif isinstance(value, EventChain): - # Trust that the caller knows what they're doing passing an EventChain directly - return value - - # If the input is a single event handler, wrap it in a list. - if isinstance(value, (EventHandler, EventSpec)): - value = [value] - - # If the input is a list of event handlers, create an event chain. - if isinstance(value, List): - events: List[Union[EventSpec, EventVar]] = [] - for v in value: - if isinstance(v, (EventHandler, EventSpec)): - # Call the event handler to get the event. - events.append(call_event_handler(v, args_spec, key=key)) - elif isinstance(v, Callable): - # Call the lambda to get the event chain. - result = call_event_fn(v, args_spec, key=key) - if isinstance(result, Var): - raise ValueError( - f"Invalid event chain: {v}. Cannot use a Var-returning " - "lambda inside an EventChain list." - ) - events.extend(result) - elif isinstance(v, EventVar): - events.append(v) - else: - raise ValueError(f"Invalid event: {v}") - - # If the input is a callable, create an event chain. - elif isinstance(value, Callable): - result = call_event_fn(value, args_spec, key=key) - if isinstance(result, Var): - # Recursively call this function if the lambda returned an EventChain Var. - return self._create_event_chain(args_spec, result, key=key) - events = [*result] - - # Otherwise, raise an error. - else: - raise ValueError(f"Invalid event chain: {value}") - - # Add args to the event specs if necessary. - events = [ - (e.with_args(get_handler_args(e)) if isinstance(e, EventSpec) else e) - for e in events - ] - - # Return the event chain. - if isinstance(args_spec, Var): - return EventChain( - events=events, - args_spec=None, - event_actions={}, - ) - else: - return EventChain( - events=events, - args_spec=args_spec, - event_actions={}, - ) - def get_event_triggers( self, ) -> Dict[str, types.ArgsSpec | Sequence[types.ArgsSpec]]: @@ -653,7 +549,6 @@ class Component(BaseComponent, ABC): Returns: The event triggers. - """ default_triggers: Dict[str, types.ArgsSpec | Sequence[types.ArgsSpec]] = { EventTriggers.ON_FOCUS: no_args_event_spec, @@ -681,7 +576,7 @@ class Component(BaseComponent, ABC): annotation = field.annotation if (metadata := getattr(annotation, "__metadata__", None)) is not None: args_spec = metadata[0] - default_triggers[field.name] = args_spec or (no_args_event_spec) # type: ignore + default_triggers[field.name] = args_spec or (no_args_event_spec) return default_triggers def __repr__(self) -> str: @@ -728,8 +623,7 @@ class Component(BaseComponent, ABC): if props is None: # Add component props to the tag. props = { - attr[:-1] if attr.endswith("_") else attr: getattr(self, attr) - for attr in self.get_props() + attr.removesuffix("_"): getattr(self, attr) for attr in self.get_props() } # Add ref to element if `id` is not None. @@ -807,22 +701,21 @@ class Component(BaseComponent, ABC): # Import here to avoid circular imports. from reflex.components.base.bare import Bare from reflex.components.base.fragment import Fragment - from reflex.utils.exceptions import ComponentTypeError + from reflex.utils.exceptions import ChildrenTypeError # Filter out None props props = {key: value for key, value in props.items() if value is not None} - def validate_children(children): + def validate_children(children: tuple | list): for child in children: - if isinstance(child, tuple): + if isinstance(child, (tuple, list)): validate_children(child) + # Make sure the child is a valid type. - if not types._isinstance(child, ComponentChild): - raise ComponentTypeError( - "Children of Reflex components must be other components, " - "state vars, or primitive Python types. " - f"Got child {child} of type {type(child)}.", - ) + if isinstance(child, dict) or not types._isinstance( + child, ComponentChild + ): + raise ChildrenTypeError(component=cls.__name__, child=child) # Validate all the children. validate_children(children) @@ -865,7 +758,7 @@ class Component(BaseComponent, ABC): # Walk the MRO to call all `add_style` methods. for base in self._iter_parent_classes_with_method("add_style"): - s = base.add_style(self) # type: ignore + s = base.add_style(self) if s is not None: styles.append(s) @@ -957,7 +850,7 @@ class Component(BaseComponent, ABC): else {} ) - def render(self) -> Dict: + def render(self) -> dict: """Render the component. Returns: @@ -975,7 +868,7 @@ class Component(BaseComponent, ABC): self._replace_prop_names(rendered_dict) return rendered_dict - def _replace_prop_names(self, rendered_dict) -> None: + def _replace_prop_names(self, rendered_dict: dict) -> None: """Replace the prop names in the render dictionary. Args: @@ -1015,7 +908,7 @@ class Component(BaseComponent, ABC): comp.__name__ for comp in (Fragment, Foreach, Cond, Match) ] - def validate_child(child): + def validate_child(child: Any): child_name = type(child).__name__ # Iterate through the immediate children of fragment @@ -1087,18 +980,22 @@ class Component(BaseComponent, ABC): event_args.append(spec) yield event_trigger, event_args - def _get_vars(self, include_children: bool = False) -> list[Var]: + def _get_vars( + self, include_children: bool = False, ignore_ids: set[int] | None = None + ) -> Iterator[Var]: """Walk all Vars used in this component. Args: include_children: Whether to include Vars from children. + ignore_ids: The ids to ignore. - Returns: + Yields: Each var referenced by the component (props, styles, event handlers). """ - vars = getattr(self, "__vars", None) + ignore_ids = ignore_ids or set() + vars: List[Var] | None = getattr(self, "__vars", None) if vars is not None: - return vars + yield from vars vars = self.__vars = [] # Get Vars associated with event trigger arguments. for _, event_vars in self._get_vars_from_event_triggers(self.event_triggers): @@ -1142,12 +1039,15 @@ class Component(BaseComponent, ABC): # Get Vars associated with children. if include_children: for child in self.children: - if not isinstance(child, Component): + if not isinstance(child, Component) or id(child) in ignore_ids: continue - child_vars = child._get_vars(include_children=include_children) + ignore_ids.add(id(child)) + child_vars = child._get_vars( + include_children=include_children, ignore_ids=ignore_ids + ) vars.extend(child_vars) - return vars + yield from vars def _event_trigger_values_use_state(self) -> bool: """Check if the values of a component's event trigger use state. @@ -1209,7 +1109,7 @@ class Component(BaseComponent, ABC): Yields: The parent classes that define the method (differently than the base). """ - seen_methods = set([getattr(Component, method)]) + seen_methods = {getattr(Component, method)} for clz in cls.mro(): if clz is Component: break @@ -1339,7 +1239,7 @@ class Component(BaseComponent, ABC): """ _imports = {} - if self._get_ref_hook(): + if self._get_ref_hook() is not None: # Handle hooks needed for attaching react refs to DOM nodes. _imports.setdefault("react", set()).add(ImportVar(tag="useRef")) _imports.setdefault(f"$/{Dirs.STATE_PATH}", set()).add( @@ -1369,7 +1269,9 @@ class Component(BaseComponent, ABC): if user_hooks_data is not None: other_imports.append(user_hooks_data.imports) other_imports.extend( - hook_imports for hook_imports in self._get_added_hooks().values() + hook_vardata.imports + for hook_vardata in self._get_added_hooks().values() + if hook_vardata is not None ) return imports.merge_imports(_imports, *other_imports) @@ -1391,15 +1293,9 @@ class Component(BaseComponent, ABC): # Collect imports from Vars used directly by this component. var_datas = [var._get_all_var_data() for var in self._get_vars()] - var_imports: List[ImmutableParsedImportDict] = list( - map( - lambda var_data: var_data.imports, - filter( - None, - var_datas, - ), - ) - ) + var_imports: List[ImmutableParsedImportDict] = [ + var_data.imports for var_data in var_datas if var_data is not None + ] added_import_dicts: list[ParsedImportDict] = [] for clz in self._iter_parent_classes_with_method("add_imports"): @@ -1408,8 +1304,9 @@ class Component(BaseComponent, ABC): if not isinstance(list_of_import_dict, list): list_of_import_dict = [list_of_import_dict] - for import_dict in list_of_import_dict: - added_import_dicts.append(parse_imports(import_dict)) + added_import_dicts.extend( + [parse_imports(import_dict) for import_dict in list_of_import_dict] + ) return imports.merge_imports( *self._get_props_imports(), @@ -1458,7 +1355,7 @@ class Component(BaseComponent, ABC): }} }}, []);""" - def _get_ref_hook(self) -> str | None: + def _get_ref_hook(self) -> Var | None: """Generate the ref hook for the component. Returns: @@ -1466,11 +1363,12 @@ class Component(BaseComponent, ABC): """ ref = self.get_ref() if ref is not None: - return ( - f"const {ref} = useRef(null); {Var(_js_expr=ref)._as_ref()!s} = {ref};" + return Var( + f"const {ref} = useRef(null); {Var(_js_expr=ref)._as_ref()!s} = {ref};", + _var_data=VarData(position=Hooks.HookPosition.INTERNAL), ) - def _get_vars_hooks(self) -> dict[str, None]: + def _get_vars_hooks(self) -> dict[str, VarData | None]: """Get the hooks required by vars referenced in this component. Returns: @@ -1483,27 +1381,38 @@ class Component(BaseComponent, ABC): vars_hooks.update( var_data.hooks if isinstance(var_data.hooks, dict) - else {k: None for k in var_data.hooks} + else { + k: VarData(position=Hooks.HookPosition.INTERNAL) + for k in var_data.hooks + } ) return vars_hooks - def _get_events_hooks(self) -> dict[str, None]: + def _get_events_hooks(self) -> dict[str, VarData | None]: """Get the hooks required by events referenced in this component. Returns: The hooks for the events. """ - return {Hooks.EVENTS: None} if self.event_triggers else {} + return ( + {Hooks.EVENTS: VarData(position=Hooks.HookPosition.INTERNAL)} + if self.event_triggers + else {} + ) - def _get_special_hooks(self) -> dict[str, None]: + def _get_special_hooks(self) -> dict[str, VarData | None]: """Get the hooks required by special actions referenced in this component. Returns: The hooks for special actions. """ - return {Hooks.AUTOFOCUS: None} if self.autofocus else {} + return ( + {Hooks.AUTOFOCUS: VarData(position=Hooks.HookPosition.INTERNAL)} + if self.autofocus + else {} + ) - def _get_hooks_internal(self) -> dict[str, None]: + def _get_hooks_internal(self) -> dict[str, VarData | None]: """Get the React hooks for this component managed by the framework. Downstream components should NOT override this method to avoid breaking @@ -1514,7 +1423,7 @@ class Component(BaseComponent, ABC): """ return { **{ - hook: None + str(hook): VarData(position=Hooks.HookPosition.INTERNAL) for hook in [self._get_ref_hook(), self._get_mount_lifecycle_hook()] if hook is not None }, @@ -1523,7 +1432,7 @@ class Component(BaseComponent, ABC): **self._get_special_hooks(), } - def _get_added_hooks(self) -> dict[str, ImportDict]: + def _get_added_hooks(self) -> dict[str, VarData | None]: """Get the hooks added via `add_hooks` method. Returns: @@ -1532,17 +1441,15 @@ class Component(BaseComponent, ABC): code = {} def extract_var_hooks(hook: Var): - _imports = {} var_data = VarData.merge(hook._get_all_var_data()) if var_data is not None: for sub_hook in var_data.hooks: - code[sub_hook] = {} - if var_data.imports: - _imports = var_data.imports + code[sub_hook] = None + if str(hook) in code: - code[str(hook)] = imports.merge_imports(code[str(hook)], _imports) + code[str(hook)] = VarData.merge(var_data, code[str(hook)]) else: - code[str(hook)] = _imports + code[str(hook)] = var_data # Add the hook code from add_hooks for each parent class (this is reversed to preserve # the order of the hooks in the final output) @@ -1551,7 +1458,7 @@ class Component(BaseComponent, ABC): if isinstance(hook, Var): extract_var_hooks(hook) else: - code[hook] = {} + code[hook] = None return code @@ -1565,7 +1472,7 @@ class Component(BaseComponent, ABC): """ return - def _get_all_hooks_internal(self) -> dict[str, None]: + def _get_all_hooks_internal(self) -> dict[str, VarData | None]: """Get the reflex internal hooks for the component and its children. Returns: @@ -1580,7 +1487,7 @@ class Component(BaseComponent, ABC): return code - def _get_all_hooks(self) -> dict[str, None]: + def _get_all_hooks(self) -> dict[str, VarData | None]: """Get the React hooks for this component and its children. Returns: @@ -1588,13 +1495,15 @@ class Component(BaseComponent, ABC): """ code = {} + # Add the internal hooks for this component. + code.update(self._get_hooks_internal()) + # Add the hook code for this component. hooks = self._get_hooks() if hooks is not None: code[hooks] = None - for hook in self._get_added_hooks(): - code[hook] = None + code.update(self._get_added_hooks()) # Add the hook code for the children. for child in self.children: @@ -1744,7 +1653,7 @@ class CustomComponent(Component): # Handle event chains. if types._issubclass(type_, EventChain): - value = self._create_event_chain( + value = EventChain.create( value=value, args_spec=event_triggers_in_component_declaration.get( key, no_args_event_spec @@ -1762,7 +1671,7 @@ class CustomComponent(Component): if base_value is not None and isinstance(value, Component): self.component_props[key] = value value = base_value._replace( - merge_var_data=VarData( # type: ignore + merge_var_data=VarData( imports=value._get_all_imports(), hooks=value._get_all_hooks(), ) @@ -1795,7 +1704,7 @@ class CustomComponent(Component): return hash(self.tag) @classmethod - def get_props(cls) -> Set[str]: + def get_props(cls) -> Set[str]: # pyright: ignore [reportIncompatibleVariableOverride] """Get the props for the component. Returns: @@ -1869,22 +1778,28 @@ class CustomComponent(Component): for name, prop in self.props.items() ] - def _get_vars(self, include_children: bool = False) -> list[Var]: + def _get_vars( + self, include_children: bool = False, ignore_ids: set[int] | None = None + ) -> Iterator[Var]: """Walk all Vars used in this component. Args: include_children: Whether to include Vars from children. + ignore_ids: The ids to ignore. - Returns: + Yields: Each var referenced by the component (props, styles, event handlers). """ - return ( - super()._get_vars(include_children=include_children) - + [prop for prop in self.props.values() if isinstance(prop, Var)] - + self.get_component(self)._get_vars(include_children=include_children) + ignore_ids = ignore_ids or set() + yield from super()._get_vars( + include_children=include_children, ignore_ids=ignore_ids + ) + yield from filter(lambda prop: isinstance(prop, Var), self.props.values()) + yield from self.get_component(self)._get_vars( + include_children=include_children, ignore_ids=ignore_ids ) - @lru_cache(maxsize=None) # noqa + @lru_cache(maxsize=None) # noqa: B019 def get_component(self) -> Component: """Render the component. @@ -2028,7 +1943,7 @@ class StatefulComponent(BaseComponent): if not should_memoize: # Determine if any Vars have associated data. - for prop_var in component._get_vars(): + for prop_var in component._get_vars(include_children=True): if prop_var._get_all_var_data(): should_memoize = True break @@ -2196,6 +2111,31 @@ class StatefulComponent(BaseComponent): ] return [var_name] + @staticmethod + def _get_deps_from_event_trigger(event: EventChain | EventSpec | Var) -> set[str]: + """Get the dependencies accessed by event triggers. + + Args: + event: The event trigger to extract deps from. + + Returns: + The dependencies accessed by the event triggers. + """ + events: list = [event] + deps = set() + + if isinstance(event, EventChain): + events.extend(event.events) + + for ev in events: + if isinstance(ev, EventSpec): + for arg in ev.args: + for a in arg: + var_datas = VarData.merge(a._get_all_var_data()) + if var_datas and var_datas.deps is not None: + deps |= {str(dep) for dep in var_datas.deps} + return deps + @classmethod def _get_memoized_event_triggers( cls, @@ -2232,6 +2172,11 @@ class StatefulComponent(BaseComponent): # Calculate Var dependencies accessed by the handler for useCallback dep array. var_deps = ["addEvents", "Event"] + + # Get deps from event trigger var data. + var_deps.extend(cls._get_deps_from_event_trigger(event)) + + # Get deps from hooks. for arg in event_args: var_data = arg._get_all_var_data() if var_data is None: @@ -2254,7 +2199,7 @@ class StatefulComponent(BaseComponent): ) return trigger_memo - def _get_all_hooks_internal(self) -> dict[str, None]: + def _get_all_hooks_internal(self) -> dict[str, VarData | None]: """Get the reflex internal hooks for the component and its children. Returns: @@ -2262,7 +2207,7 @@ class StatefulComponent(BaseComponent): """ return {} - def _get_all_hooks(self) -> dict[str, None]: + def _get_all_hooks(self) -> dict[str, VarData | None]: """Get the React hooks for this component. Returns: @@ -2380,9 +2325,9 @@ class MemoizationLeaf(Component): The memoization leaf """ comp = super().create(*children, **props) - if comp._get_all_hooks() or comp._get_all_hooks_internal(): - comp._memoization_mode = cls._memoization_mode.copy( - update={"disposition": MemoizationDisposition.ALWAYS} + if comp._get_all_hooks(): + comp._memoization_mode = dataclasses.replace( + comp._memoization_mode, disposition=MemoizationDisposition.ALWAYS ) return comp @@ -2443,7 +2388,7 @@ def render_dict_to_var(tag: dict | Component | str, imported_names: set[str]) -> if tag["name"] == "match": element = tag["cond"] - conditionals = tag["default"] + conditionals = render_dict_to_var(tag["default"], imported_names) for case in tag["match_cases"][::-1]: condition = case[0].to_string() == element.to_string() @@ -2452,7 +2397,7 @@ def render_dict_to_var(tag: dict | Component | str, imported_names: set[str]) -> conditionals = ternary_operation( condition, - case[-1], + render_dict_to_var(case[-1], imported_names), conditionals, ) @@ -2511,6 +2456,7 @@ def render_dict_to_var(tag: dict | Component | str, imported_names: set[str]) -> @dataclasses.dataclass( eq=False, frozen=True, + slots=True, ) class LiteralComponentVar(CachedVarOperation, LiteralVar, ComponentVar): """A Var that represents a Component.""" @@ -2565,7 +2511,7 @@ class LiteralComponentVar(CachedVarOperation, LiteralVar, ComponentVar): Returns: The hash of the var. """ - return hash((self.__class__.__name__, self._js_expr)) + return hash((type(self).__name__, self._js_expr)) @classmethod def create( diff --git a/reflex/components/core/banner.py b/reflex/components/core/banner.py index a5553800d..882975f2f 100644 --- a/reflex/components/core/banner.py +++ b/reflex/components/core/banner.py @@ -4,8 +4,10 @@ from __future__ import annotations from typing import Optional +from reflex import constants from reflex.components.component import Component from reflex.components.core.cond import cond +from reflex.components.datadisplay.logo import svg_logo from reflex.components.el.elements.typography import Div from reflex.components.lucide.icon import Icon from reflex.components.radix.themes.components.dialog import ( @@ -25,7 +27,7 @@ from reflex.vars.function import FunctionStringVar from reflex.vars.number import BooleanVar from reflex.vars.sequence import LiteralArrayVar -connect_error_var_data: VarData = VarData( # type: ignore +connect_error_var_data: VarData = VarData( imports=Imports.EVENTS, hooks={Hooks.EVENTS: None}, ) @@ -99,14 +101,14 @@ class ConnectionToaster(Toaster): """ toast_id = "websocket-error" target_url = WebsocketTargetURL.create() - props = ToastProps( # type: ignore + props = ToastProps( description=LiteralVar.create( f"Check if server is reachable at {target_url}", ), close_button=True, duration=120000, id=toast_id, - ) + ) # pyright: ignore [reportCallIssue] individual_hooks = [ f"const toast_props = {LiteralVar.create(props)!s};", @@ -116,7 +118,7 @@ class ConnectionToaster(Toaster): _var_data=VarData( imports={ "react": ["useEffect", "useState"], - **dict(target_url._get_all_var_data().imports), # type: ignore + **dict(target_url._get_all_var_data().imports), # pyright: ignore [reportArgumentType, reportOptionalMemberAccess] } ), ).call( @@ -241,7 +243,7 @@ class WifiOffPulse(Icon): size=props.pop("size", 32), z_index=props.pop("z_index", 9999), position=props.pop("position", "fixed"), - bottom=props.pop("botton", "33px"), + bottom=props.pop("bottom", "33px"), right=props.pop("right", "33px"), animation=LiteralVar.create(f"{pulse_var} 1s infinite"), **props, @@ -293,7 +295,84 @@ class ConnectionPulser(Div): ) +class BackendDisabled(Div): + """A component that displays a message when the backend is disabled.""" + + @classmethod + def create(cls, **props) -> Component: + """Create a backend disabled component. + + Args: + **props: The properties of the component. + + Returns: + The backend disabled component. + """ + import reflex as rx + + is_backend_disabled = Var( + "backendDisabled", + _var_type=bool, + _var_data=VarData( + hooks={ + "const [backendDisabled, setBackendDisabled] = useState(false);": None, + "useEffect(() => { setBackendDisabled(isBackendDisabled()); }, []);": None, + }, + imports={ + f"$/{constants.Dirs.STATE_PATH}": [ + ImportVar(tag="isBackendDisabled") + ], + }, + ), + ) + + return super().create( + rx.cond( + is_backend_disabled, + rx.box( + rx.box( + rx.card( + rx.vstack( + svg_logo(), + rx.text( + "You ran out of compute credits.", + ), + rx.callout( + rx.fragment( + "Please upgrade your plan or raise your compute credits at ", + rx.link( + "Reflex Cloud.", + href="https://cloud.reflex.dev/", + ), + ), + width="100%", + icon="info", + variant="surface", + ), + ), + font_size="20px", + font_family='"Inter", "Helvetica", "Arial", sans-serif', + variant="classic", + ), + position="fixed", + top="50%", + left="50%", + transform="translate(-50%, -50%)", + width="40ch", + max_width="90vw", + ), + position="fixed", + z_index=9999, + backdrop_filter="grayscale(1) blur(5px)", + width="100dvw", + height="100dvh", + ), + ) + ) + + connection_banner = ConnectionBanner.create connection_modal = ConnectionModal.create connection_toaster = ConnectionToaster.create connection_pulser = ConnectionPulser.create +backend_disabled = BackendDisabled.create diff --git a/reflex/components/core/banner.pyi b/reflex/components/core/banner.pyi index 3296b84ee..2ea514965 100644 --- a/reflex/components/core/banner.pyi +++ b/reflex/components/core/banner.pyi @@ -321,7 +321,7 @@ class ConnectionPulser(Div): """Create a connection pulser component. Args: - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -350,7 +350,93 @@ class ConnectionPulser(Div): """ ... +class BackendDisabled(Div): + @overload + @classmethod + def create( # type: ignore + cls, + *children, + access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + auto_capitalize: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + content_editable: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + context_menu: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + dir: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + draggable: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + enter_key_hint: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + hidden: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + input_mode: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + item_prop: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + role: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + slot: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + spell_check: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + tab_index: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + title: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_blur: Optional[EventType[[], BASE_STATE]] = None, + on_click: Optional[EventType[[], BASE_STATE]] = None, + on_context_menu: Optional[EventType[[], BASE_STATE]] = None, + on_double_click: Optional[EventType[[], BASE_STATE]] = None, + on_focus: Optional[EventType[[], BASE_STATE]] = None, + on_mount: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_down: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_enter: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_leave: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_move: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_out: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_over: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_up: Optional[EventType[[], BASE_STATE]] = None, + on_scroll: Optional[EventType[[], BASE_STATE]] = None, + on_unmount: Optional[EventType[[], BASE_STATE]] = None, + **props, + ) -> "BackendDisabled": + """Create a backend disabled component. + + Args: + access_key: Provides a hint for generating a keyboard shortcut for the current element. + auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. + content_editable: Indicates whether the element's content is editable. + context_menu: Defines the ID of a element which will serve as the element's context menu. + dir: Defines the text direction. Allowed values are ltr (Left-To-Right) or rtl (Right-To-Left) + draggable: Defines whether the element can be dragged. + enter_key_hint: Hints what media types the media element is able to play. + hidden: Defines whether the element is hidden. + input_mode: Defines the type of the element. + item_prop: Defines the name of the element for metadata purposes. + lang: Defines the language used in the element. + role: Defines the role of the element. + slot: Assigns a slot in a shadow DOM shadow tree to an element. + spell_check: Defines whether the element may be checked for spelling errors. + tab_index: Defines the position of the current element in the tabbing order. + title: Defines a tooltip for the element. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The properties of the component. + + Returns: + The backend disabled component. + """ + ... + connection_banner = ConnectionBanner.create connection_modal = ConnectionModal.create connection_toaster = ConnectionToaster.create connection_pulser = ConnectionPulser.create +backend_disabled = BackendDisabled.create diff --git a/reflex/components/core/breakpoints.py b/reflex/components/core/breakpoints.py index 4b2372a70..9a8ef1556 100644 --- a/reflex/components/core/breakpoints.py +++ b/reflex/components/core/breakpoints.py @@ -58,7 +58,7 @@ class Breakpoints(Dict[K, V]): Args: custom: Custom mapping using CSS values or variables. - initial: Styling when in the inital width + initial: Styling when in the initial width xs: Styling when in the extra-small width sm: Styling when in the small width md: Styling when in the medium width @@ -82,7 +82,9 @@ class Breakpoints(Dict[K, V]): return Breakpoints( { k: v - for k, v in zip(["initial", *breakpoint_names], thresholds) + for k, v in zip( + ["initial", *breakpoint_names], thresholds, strict=True + ) if v is not None } ) diff --git a/reflex/components/core/client_side_routing.py b/reflex/components/core/client_side_routing.py index 342c69632..0fc40de5f 100644 --- a/reflex/components/core/client_side_routing.py +++ b/reflex/components/core/client_side_routing.py @@ -24,7 +24,7 @@ class ClientSideRouting(Component): library = "$/utils/client_side_routing" tag = "useClientSideRouting" - def add_hooks(self) -> list[str]: + def add_hooks(self) -> list[str | Var]: """Get the hooks to render. Returns: @@ -41,7 +41,7 @@ class ClientSideRouting(Component): return "" -def wait_for_client_redirect(component) -> Component: +def wait_for_client_redirect(component: Component) -> Component: """Wait for a redirect to occur before rendering a component. This prevents the 404 page from flashing while the redirect is happening. @@ -66,4 +66,4 @@ class Default404Page(Component): tag = "Error" is_default = True - status_code: Var[int] = 404 # type: ignore + status_code: Var[int] = Var.create(404) diff --git a/reflex/components/core/client_side_routing.pyi b/reflex/components/core/client_side_routing.pyi index bb853e2c7..078698198 100644 --- a/reflex/components/core/client_side_routing.pyi +++ b/reflex/components/core/client_side_routing.pyi @@ -13,7 +13,7 @@ from reflex.vars.base import Var route_not_found: Var class ClientSideRouting(Component): - def add_hooks(self) -> list[str]: ... + def add_hooks(self) -> list[str | Var]: ... def render(self) -> str: ... @overload @classmethod @@ -60,7 +60,7 @@ class ClientSideRouting(Component): """ ... -def wait_for_client_redirect(component) -> Component: ... +def wait_for_client_redirect(component: Component) -> Component: ... class Default404Page(Component): @overload diff --git a/reflex/components/core/clipboard.py b/reflex/components/core/clipboard.py index 938cd13c0..644de80d0 100644 --- a/reflex/components/core/clipboard.py +++ b/reflex/components/core/clipboard.py @@ -6,11 +6,12 @@ from typing import Dict, List, Tuple, Union from reflex.components.base.fragment import Fragment from reflex.components.tags.tag import Tag +from reflex.constants.compiler import Hooks from reflex.event import EventChain, EventHandler, passthrough_event_spec from reflex.utils.format import format_prop, wrap from reflex.utils.imports import ImportVar from reflex.vars import get_unique_variable_name -from reflex.vars.base import Var +from reflex.vars.base import Var, VarData class Clipboard(Fragment): @@ -72,7 +73,7 @@ class Clipboard(Fragment): ), } - def add_hooks(self) -> list[str]: + def add_hooks(self) -> list[str | Var[str]]: """Add hook to register paste event listener. Returns: @@ -83,13 +84,14 @@ class Clipboard(Fragment): return [] if isinstance(on_paste, EventChain): on_paste = wrap(str(format_prop(on_paste)).strip("{}"), "(") + hook_expr = f"usePasteHandler({self.targets!s}, {self.on_paste_event_actions!s}, {on_paste!s})" + return [ - "usePasteHandler(%s, %s, %s)" - % ( - str(self.targets), - str(self.on_paste_event_actions), - on_paste, - ) + Var( + hook_expr, + _var_type="str", + _var_data=VarData(position=Hooks.HookPosition.POST_TRIGGER), + ), ] diff --git a/reflex/components/core/clipboard.pyi b/reflex/components/core/clipboard.pyi index 69e0e866d..328554f2a 100644 --- a/reflex/components/core/clipboard.pyi +++ b/reflex/components/core/clipboard.pyi @@ -71,6 +71,6 @@ class Clipboard(Fragment): ... def add_imports(self) -> dict[str, ImportVar]: ... - def add_hooks(self) -> list[str]: ... + def add_hooks(self) -> list[str | Var[str]]: ... clipboard = Clipboard.create diff --git a/reflex/components/core/cond.py b/reflex/components/core/cond.py index b75667890..6f9110a16 100644 --- a/reflex/components/core/cond.py +++ b/reflex/components/core/cond.py @@ -26,10 +26,9 @@ class Cond(MemoizationLeaf): cond: Var[Any] # The component to render if the cond is true. - comp1: BaseComponent = None # type: ignore - + comp1: BaseComponent | None = None # The component to render if the cond is false. - comp2: BaseComponent = None # type: ignore + comp2: BaseComponent | None = None @classmethod def create( @@ -49,9 +48,9 @@ class Cond(MemoizationLeaf): The conditional component. """ # Wrap everything in fragments. - if comp1.__class__.__name__ != "Fragment": + if type(comp1).__name__ != "Fragment": comp1 = Fragment.create(comp1) - if comp2 is None or comp2.__class__.__name__ != "Fragment": + if comp2 is None or type(comp2).__name__ != "Fragment": comp2 = Fragment.create(comp2) if comp2 else Fragment.create() return Fragment.create( cls( @@ -73,8 +72,8 @@ class Cond(MemoizationLeaf): def _render(self) -> Tag: return CondTag( cond=self.cond, - true_value=self.comp1.render(), - false_value=self.comp2.render(), + true_value=self.comp1.render(), # pyright: ignore [reportOptionalMemberAccess] + false_value=self.comp2.render(), # pyright: ignore [reportOptionalMemberAccess] ) def render(self) -> Dict: @@ -111,7 +110,7 @@ class Cond(MemoizationLeaf): @overload -def cond(condition: Any, c1: Component, c2: Any) -> Component: ... +def cond(condition: Any, c1: Component, c2: Any) -> Component: ... # pyright: ignore [reportOverlappingOverload] @overload @@ -154,7 +153,7 @@ def cond(condition: Any, c1: Any, c2: Any = None) -> Component | Var: if c2 is None: raise ValueError("For conditional vars, the second argument must be set.") - def create_var(cond_part): + def create_var(cond_part: Any) -> Var[Any]: return LiteralVar.create(cond_part) # convert the truth and false cond parts into vars so the _var_data can be obtained. @@ -163,16 +162,16 @@ def cond(condition: Any, c1: Any, c2: Any = None) -> Component | Var: # Create the conditional var. return ternary_operation( - cond_var.bool()._replace( # type: ignore + cond_var.bool()._replace( merge_var_data=VarData(imports=_IS_TRUE_IMPORT), - ), # type: ignore + ), c1, c2, ) @overload -def color_mode_cond(light: Component, dark: Component | None = None) -> Component: ... # type: ignore +def color_mode_cond(light: Component, dark: Component | None = None) -> Component: ... # pyright: ignore [reportOverlappingOverload] @overload diff --git a/reflex/components/core/debounce.py b/reflex/components/core/debounce.py index 12cc94426..1d798994d 100644 --- a/reflex/components/core/debounce.py +++ b/reflex/components/core/debounce.py @@ -28,7 +28,7 @@ class DebounceInput(Component): min_length: Var[int] # Time to wait between end of input and triggering on_change - debounce_timeout: Var[int] = DEFAULT_DEBOUNCE_TIMEOUT # type: ignore + debounce_timeout: Var[int] = Var.create(DEFAULT_DEBOUNCE_TIMEOUT) # If true, notify when Enter key is pressed force_notify_by_enter: Var[bool] diff --git a/reflex/components/core/foreach.py b/reflex/components/core/foreach.py index c9fbe5bc5..927b01333 100644 --- a/reflex/components/core/foreach.py +++ b/reflex/components/core/foreach.py @@ -2,6 +2,7 @@ from __future__ import annotations +import functools import inspect from typing import Any, Callable, Iterable @@ -10,6 +11,7 @@ from reflex.components.component import Component from reflex.components.tags import IterTag from reflex.constants import MemoizationMode from reflex.state import ComponentState +from reflex.utils.exceptions import UntypedVarError from reflex.vars.base import LiteralVar, Var @@ -50,6 +52,7 @@ class Foreach(Component): Raises: ForeachVarError: If the iterable is of type Any. TypeError: If the render function is a ComponentState. + UntypedVarError: If the iterable is of type Any without a type annotation. """ iterable = LiteralVar.create(iterable) if iterable._var_type == Any: @@ -71,8 +74,14 @@ class Foreach(Component): iterable=iterable, render_fn=render_fn, ) - # Keep a ref to a rendered component to determine correct imports/hooks/styles. - component.children = [component._render().render_component()] + try: + # Keep a ref to a rendered component to determine correct imports/hooks/styles. + component.children = [component._render().render_component()] + except UntypedVarError as e: + raise UntypedVarError( + f"Could not foreach over var `{iterable!s}` without a type annotation. " + "See https://reflex.dev/docs/library/dynamic-rendering/foreach/" + ) from e return component def _render(self) -> IterTag: @@ -97,9 +106,20 @@ class Foreach(Component): # Determine the index var name based on the params accepted by render_fn. props["index_var_name"] = params[1].name else: + render_fn = self.render_fn # Otherwise, use a deterministic index, based on the render function bytecode. code_hash = ( - hash(self.render_fn.__code__) + hash( + getattr( + render_fn, + "__code__", + ( + repr(self.render_fn) + if not isinstance(render_fn, functools.partial) + else render_fn.func.__code__ + ), + ) + ) .to_bytes( length=8, byteorder="big", diff --git a/reflex/components/core/html.py b/reflex/components/core/html.py index cfe46e591..2cad4f331 100644 --- a/reflex/components/core/html.py +++ b/reflex/components/core/html.py @@ -14,7 +14,7 @@ class Html(Div): """ # The HTML to render. - dangerouslySetInnerHTML: Var[Dict[str, str]] + dangerouslySetInnerHTML: Var[Dict[str, str]] # noqa: N815 @classmethod def create(cls, *children, **props): diff --git a/reflex/components/core/html.pyi b/reflex/components/core/html.pyi index ffa7f88bb..e65549d0f 100644 --- a/reflex/components/core/html.pyi +++ b/reflex/components/core/html.pyi @@ -71,7 +71,7 @@ class Html(Div): Args: *children: The children of the component. dangerouslySetInnerHTML: The HTML to render. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/core/match.py b/reflex/components/core/match.py index 8b9382c89..5c31669a1 100644 --- a/reflex/components/core/match.py +++ b/reflex/components/core/match.py @@ -109,7 +109,7 @@ class Match(MemoizationLeaf): return cases, default @classmethod - def _create_case_var_with_var_data(cls, case_element): + def _create_case_var_with_var_data(cls, case_element: Any) -> Var: """Convert a case element into a Var.If the case is a Style type, we extract the var data and merge it with the newly created Var. @@ -222,7 +222,7 @@ class Match(MemoizationLeaf): cond=match_cond_var, match_cases=match_cases, default=default, - children=[case[-1] for case in match_cases] + [default], # type: ignore + children=[case[-1] for case in match_cases] + [default], # pyright: ignore [reportArgumentType] ) ) @@ -236,13 +236,13 @@ class Match(MemoizationLeaf): _js_expr=format.format_match( cond=str(match_cond_var), match_cases=match_cases, - default=default, # type: ignore + default=default, # pyright: ignore [reportArgumentType] ), - _var_type=default._var_type, # type: ignore + _var_type=default._var_type, # pyright: ignore [reportAttributeAccessIssue,reportOptionalMemberAccess] _var_data=VarData.merge( match_cond_var._get_all_var_data(), *[el._get_all_var_data() for case in match_cases for el in case], - default._get_all_var_data(), # type: ignore + default._get_all_var_data(), # pyright: ignore [reportAttributeAccessIssue, reportOptionalMemberAccess] ), ) diff --git a/reflex/components/core/upload.py b/reflex/components/core/upload.py index b5b701d6d..897b89608 100644 --- a/reflex/components/core/upload.py +++ b/reflex/components/core/upload.py @@ -29,7 +29,7 @@ from reflex.event import ( from reflex.utils import format from reflex.utils.imports import ImportVar from reflex.vars import VarData -from reflex.vars.base import CallableVar, LiteralVar, Var, get_unique_variable_name +from reflex.vars.base import CallableVar, Var, get_unique_variable_name from reflex.vars.sequence import LiteralStringVar DEFAULT_UPLOAD_ID: str = "default" @@ -108,7 +108,8 @@ def clear_selected_files(id_: str = DEFAULT_UPLOAD_ID) -> EventSpec: # UploadFilesProvider assigns a special function to clear selected files # into the shared global refs object to make it accessible outside a React # component via `run_script` (otherwise backend could never clear files). - return run_script(f"refs['__clear_selected_files']({id_!r})") + func = Var("__clear_selected_files")._as_ref() + return run_script(f"{func}({id_!r})") def cancel_upload(upload_id: str) -> EventSpec: @@ -120,7 +121,8 @@ def cancel_upload(upload_id: str) -> EventSpec: Returns: An event spec that cancels the upload when triggered. """ - return run_script(f"upload_controllers[{LiteralVar.create(upload_id)!s}]?.abort()") + controller = Var(f"__upload_controllers_{upload_id}")._as_ref() + return run_script(f"{controller}?.abort()") def get_upload_dir() -> Path: @@ -190,7 +192,7 @@ class GhostUpload(Fragment): class Upload(MemoizationLeaf): """A file upload component.""" - library = "react-dropzone@14.2.10" + library = "react-dropzone@14.3.5" tag = "" @@ -267,7 +269,7 @@ class Upload(MemoizationLeaf): on_drop = upload_props["on_drop"] if isinstance(on_drop, Callable): # Call the lambda to get the event chain. - on_drop = call_event_fn(on_drop, _on_drop_spec) # type: ignore + on_drop = call_event_fn(on_drop, _on_drop_spec) if isinstance(on_drop, EventSpec): # Update the provided args for direct use with on_drop. on_drop = on_drop.with_args( diff --git a/reflex/components/datadisplay/code.py b/reflex/components/datadisplay/code.py index 195aa6b66..4f1eb493e 100644 --- a/reflex/components/datadisplay/code.py +++ b/reflex/components/datadisplay/code.py @@ -14,7 +14,7 @@ from reflex.components.radix.themes.layout.box import Box from reflex.constants.colors import Color from reflex.event import set_clipboard from reflex.style import Style -from reflex.utils import console, format +from reflex.utils import format from reflex.utils.imports import ImportVar from reflex.vars.base import LiteralVar, Var, VarData @@ -382,7 +382,7 @@ for theme_name in dir(Theme): class CodeBlock(Component, MarkdownComponentMap): """A code block.""" - library = "react-syntax-highlighter@15.6.0" + library = "react-syntax-highlighter@15.6.1" tag = "PrismAsyncLight" @@ -438,6 +438,8 @@ class CodeBlock(Component, MarkdownComponentMap): can_copy = props.pop("can_copy", False) copy_button = props.pop("copy_button", None) + # react-syntax-highlighter doesn't have an explicit "light" or "dark" theme so we use one-light and one-dark + # themes respectively to ensure code compatibility. if "theme" not in props: # Default color scheme responds to global color mode. props["theme"] = color_mode_cond( @@ -445,20 +447,9 @@ class CodeBlock(Component, MarkdownComponentMap): dark=Theme.one_dark, ) - # react-syntax-highlighter doesnt have an explicit "light" or "dark" theme so we use one-light and one-dark - # themes respectively to ensure code compatibility. - if "theme" in props and not isinstance(props["theme"], Var): - props["theme"] = getattr(Theme, format.to_snake_case(props["theme"])) # type: ignore - console.deprecate( - feature_name="theme prop as string", - reason="Use code_block.themes instead.", - deprecation_version="0.6.0", - removal_version="0.7.0", - ) - if can_copy: code = children[0] - copy_button = ( # type: ignore + copy_button = ( copy_button if copy_button is not None else Button.create( @@ -502,8 +493,8 @@ class CodeBlock(Component, MarkdownComponentMap): theme = self.theme - out.add_props(style=theme).remove_props("theme", "code", "language").add_props( - children=self.code, language=_LANGUAGE + out.add_props(style=theme).remove_props("theme", "code").add_props( + children=self.code, ) return out @@ -512,20 +503,25 @@ class CodeBlock(Component, MarkdownComponentMap): return ["can_copy", "copy_button"] @classmethod - def _get_language_registration_hook(cls) -> str: + def _get_language_registration_hook(cls, language_var: Var = _LANGUAGE) -> str: """Get the hook to register the language. + Args: + language_var: The const/literal Var of the language module to import. + For markdown, uses the default placeholder _LANGUAGE. For direct use, + a LiteralStringVar should be passed via the language prop. + Returns: The hook to register the language. """ return f""" - if ({_LANGUAGE!s}) {{ + if ({language_var!s}) {{ (async () => {{ try {{ - const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${{{_LANGUAGE!s}}}`); - SyntaxHighlighter.registerLanguage({_LANGUAGE!s}, module.default); + const module = await import(`react-syntax-highlighter/dist/cjs/languages/prism/${{{language_var!s}}}`); + SyntaxHighlighter.registerLanguage({language_var!s}, module.default); }} catch (error) {{ - console.error(`Error importing language module for ${{{_LANGUAGE!s}}}:`, error); + console.error(`Error importing language module for ${{{language_var!s}}}:`, error); }} }})(); }} @@ -547,8 +543,7 @@ class CodeBlock(Component, MarkdownComponentMap): The hooks for the component. """ return [ - f"const {_LANGUAGE!s} = {self.language!s}", - self._get_language_registration_hook(), + self._get_language_registration_hook(language_var=self.language), ] diff --git a/reflex/components/datadisplay/dataeditor.py b/reflex/components/datadisplay/dataeditor.py index 93352c291..dfac0452a 100644 --- a/reflex/components/datadisplay/dataeditor.py +++ b/reflex/components/datadisplay/dataeditor.py @@ -51,27 +51,6 @@ class GridColumnIcons(Enum): VideoUri = "video_uri" -# @serializer -# def serialize_gridcolumn_icon(icon: GridColumnIcons) -> str: -# """Serialize grid column icon. - -# Args: -# icon: the Icon to serialize. - -# Returns: -# The serialized value. -# """ -# return "prefix" + str(icon) - - -# class DataEditorColumn(Base): -# """Column.""" - -# title: str -# id: Optional[str] = None -# type_: str = "str" - - class DataEditorTheme(Base): """The theme for the DataEditor component.""" @@ -186,7 +165,7 @@ class DataEditor(NoSSRComponent): tag = "DataEditor" is_default = True - library: str = "@glideapps/glide-data-grid@^6.0.3" + library: str | None = "@glideapps/glide-data-grid@^6.0.3" lib_dependencies: List[str] = [ "lodash@^4.17.21", "react-responsive-carousel@^3.2.7", @@ -229,7 +208,7 @@ class DataEditor(NoSSRComponent): header_height: Var[int] # Additional header icons: - # header_icons: Var[Any] # (TODO: must be a map of name: svg) + # header_icons: Var[Any] # (TODO: must be a map of name: svg) #noqa: ERA001 # The maximum width a column can be automatically sized to. max_column_auto_width: Var[int] @@ -240,7 +219,7 @@ class DataEditor(NoSSRComponent): # The minimum width a column can be resized to. min_column_width: Var[int] - # Determins the height of each row. + # Determines the height of each row. row_height: Var[int] # Kind of row markers. @@ -342,6 +321,8 @@ class DataEditor(NoSSRComponent): Returns: The import dict. """ + if self.library is None: + return {} return { "": f"{format.format_library_name(self.library)}/dist/index.css", self.library: "GridCellKind", @@ -360,10 +341,13 @@ class DataEditor(NoSSRComponent): editor_id = get_unique_variable_name() # Define the name of the getData callback associated with this component and assign to get_cell_content. - data_callback = f"getData_{editor_id}" - self.get_cell_content = Var(_js_expr=data_callback) # type: ignore + if self.get_cell_content is not None: + data_callback = self.get_cell_content._js_expr + else: + data_callback = f"getData_{editor_id}" + self.get_cell_content = Var(_js_expr=data_callback) - code = [f"function {data_callback}([col, row])" "{"] + code = [f"function {data_callback}([col, row]){{"] columns_path = str(self.columns) data_path = str(self.data) @@ -403,7 +387,8 @@ class DataEditor(NoSSRComponent): raise ValueError( "DataEditor data must be an ArrayVar if rows is not provided." ) - props["rows"] = data.length() if isinstance(data, Var) else len(data) + + props["rows"] = data.length() if isinstance(data, ArrayVar) else len(data) if not isinstance(columns, Var) and len(columns): if types.is_dataframe(type(data)) or ( diff --git a/reflex/components/datadisplay/dataeditor.pyi b/reflex/components/datadisplay/dataeditor.pyi index aa4b3b2e8..d930fe256 100644 --- a/reflex/components/datadisplay/dataeditor.pyi +++ b/reflex/components/datadisplay/dataeditor.pyi @@ -288,10 +288,10 @@ class DataEditor(NoSSRComponent): freeze_columns: The number of columns which should remain in place when scrolling horizontally. Doesn't include rowMarkers. group_header_height: Controls the header of the group header row. header_height: Controls the height of the header row. - max_column_auto_width: Additional header icons: header_icons: Var[Any] # (TODO: must be a map of name: svg) The maximum width a column can be automatically sized to. + max_column_auto_width: The maximum width a column can be automatically sized to. max_column_width: The maximum width a column can be resized to. min_column_width: The minimum width a column can be resized to. - row_height: Determins the height of each row. + row_height: Determines the height of each row. row_markers: Kind of row markers. row_marker_start_index: Changes the starting index for row markers. row_marker_width: Sets the width of row markers in pixels, if unset row markers will automatically size. diff --git a/reflex/components/datadisplay/logo.py b/reflex/components/datadisplay/logo.py index d960b8cee..1c4c02001 100644 --- a/reflex/components/datadisplay/logo.py +++ b/reflex/components/datadisplay/logo.py @@ -15,10 +15,8 @@ def svg_logo(color: Union[str, rx.Var[str]] = rx.color_mode_cond("#110F1F", "whi The Reflex logo SVG. """ - def logo_path(d): - return rx.el.svg.path( - d=d, - ) + def logo_path(d: str): + return rx.el.svg.path(d=d) paths = [ "M0 11.5999V0.399902H8.96V4.8799H6.72V2.6399H2.24V4.8799H6.72V7.1199H2.24V11.5999H0ZM6.72 11.5999V7.1199H8.96V11.5999H6.72Z", diff --git a/reflex/components/datadisplay/shiki_code_block.py b/reflex/components/datadisplay/shiki_code_block.py index 2b4e1f506..a4aaec1d4 100644 --- a/reflex/components/datadisplay/shiki_code_block.py +++ b/reflex/components/datadisplay/shiki_code_block.py @@ -490,17 +490,17 @@ class ShikiJsTransformer(ShikiBaseTransformers): }, # White Space # ".tab, .space": { - # "position": "relative", + # "position": "relative", # noqa: ERA001 # }, # ".tab::before": { - # "content": "'⇥'", - # "position": "absolute", - # "opacity": "0.3", + # "content": "'⇥'", # noqa: ERA001 + # "position": "absolute", # noqa: ERA001 + # "opacity": "0.3",# noqa: ERA001 # }, # ".space::before": { - # "content": "'·'", - # "position": "absolute", - # "opacity": "0.3", + # "content": "'·'", # noqa: ERA001 + # "position": "absolute", # noqa: ERA001 + # "opacity": "0.3", # noqa: ERA001 # }, } ) @@ -602,7 +602,7 @@ class ShikiCodeBlock(Component, MarkdownComponentMap): transformer_styles = {} # Collect styles from transformers and wrapper - for transformer in code_block.transformers._var_value: # type: ignore + for transformer in code_block.transformers._var_value: # pyright: ignore [reportAttributeAccessIssue] if isinstance(transformer, ShikiBaseTransformers) and transformer.style: transformer_styles.update(transformer.style) transformer_styles.update(code_wrapper_props.pop("style", {})) @@ -621,18 +621,22 @@ class ShikiCodeBlock(Component, MarkdownComponentMap): Returns: Imports for the component. + + Raises: + ValueError: If the transformers are not of type LiteralVar. """ imports = defaultdict(list) + if not isinstance(self.transformers, LiteralVar): + raise ValueError( + f"transformers should be a LiteralVar type. Got {type(self.transformers)} instead." + ) for transformer in self.transformers._var_value: if isinstance(transformer, ShikiBaseTransformers): imports[transformer.library].extend( [ImportVar(tag=str(fn)) for fn in transformer.fns] ) - ( + if transformer.library not in self.lib_dependencies: self.lib_dependencies.append(transformer.library) - if transformer.library not in self.lib_dependencies - else None - ) return imports @classmethod @@ -653,8 +657,9 @@ class ShikiCodeBlock(Component, MarkdownComponentMap): raise ValueError( f"the function names should be str names of functions in the specified transformer: {library!r}" ) - return ShikiBaseTransformers( # type: ignore - library=library, fns=[FunctionStringVar.create(fn) for fn in fns] + return ShikiBaseTransformers( + library=library, + fns=[FunctionStringVar.create(fn) for fn in fns], # pyright: ignore [reportCallIssue] ) def _render(self, props: dict[str, Any] | None = None): @@ -757,13 +762,13 @@ class ShikiHighLevelCodeBlock(ShikiCodeBlock): if can_copy: code = children[0] - copy_button = ( # type: ignore + copy_button = ( copy_button if copy_button is not None else Button.create( Icon.create(tag="copy", size=16, color=color("gray", 11)), on_click=[ - set_clipboard(cls._strip_transformer_triggers(code)), # type: ignore + set_clipboard(cls._strip_transformer_triggers(code)), copy_script(), ], style=Style( diff --git a/reflex/components/dynamic.py b/reflex/components/dynamic.py index fbfc55f97..d4efcd293 100644 --- a/reflex/components/dynamic.py +++ b/reflex/components/dynamic.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING, Union from reflex import constants from reflex.utils import imports -from reflex.utils.exceptions import DynamicComponentMissingLibrary +from reflex.utils.exceptions import DynamicComponentMissingLibraryError from reflex.utils.format import format_library_name from reflex.utils.serializers import serializer from reflex.vars import Var, get_unique_variable_name @@ -36,13 +36,15 @@ def bundle_library(component: Union["Component", str]): component: The component to bundle the library with. Raises: - DynamicComponentMissingLibrary: Raised when a dynamic component is missing a library. + DynamicComponentMissingLibraryError: Raised when a dynamic component is missing a library. """ if isinstance(component, str): bundled_libraries.add(component) return if component.library is None: - raise DynamicComponentMissingLibrary("Component must have a library to bundle.") + raise DynamicComponentMissingLibraryError( + "Component must have a library to bundle." + ) bundled_libraries.add(format_library_name(component.library)) @@ -136,6 +138,23 @@ def load_dynamic_serializer(): module_code_lines.insert(0, "const React = window.__reflex.react;") + function_line = next( + index + for index, line in enumerate(module_code_lines) + if line.startswith("export default function") + ) + + module_code_lines = [ + line + for _, line in sorted( + enumerate(module_code_lines), + key=lambda x: ( + not (x[1].startswith("import ") and x[0] < function_line), + x[0], + ), + ) + ] + return "\n".join( [ "//__reflex_evaluate", diff --git a/reflex/components/el/constants/reflex.py b/reflex/components/el/constants/reflex.py index 05c298325..199edf569 100644 --- a/reflex/components/el/constants/reflex.py +++ b/reflex/components/el/constants/reflex.py @@ -48,4 +48,4 @@ PROP_TO_ELEMENTS = { ELEMENT_TO_PROPS = defaultdict(list) for prop, elements in PROP_TO_ELEMENTS.items(): for el in elements: - ELEMENT_TO_PROPS[el].append(prop) # type: ignore + ELEMENT_TO_PROPS[el].append(prop) diff --git a/reflex/components/el/element.py b/reflex/components/el/element.py index 213cea65a..c9a58b1f6 100644 --- a/reflex/components/el/element.py +++ b/reflex/components/el/element.py @@ -6,7 +6,7 @@ from reflex.components.component import Component class Element(Component): """The base class for all raw HTML elements.""" - def __eq__(self, other): + def __eq__(self, other: object): """Two elements are equal if they have the same tag. Args: diff --git a/reflex/components/el/elements/__init__.py b/reflex/components/el/elements/__init__.py index 45a7e04b8..f0d4fd200 100644 --- a/reflex/components/el/elements/__init__.py +++ b/reflex/components/el/elements/__init__.py @@ -127,7 +127,7 @@ _MAPPING = { EXCLUDE = ["del_", "Del", "image"] -for _, v in _MAPPING.items(): +for v in _MAPPING.values(): v.extend([mod.capitalize() for mod in v if mod not in EXCLUDE]) _SUBMOD_ATTRS: dict[str, list[str]] = _MAPPING diff --git a/reflex/components/el/elements/__init__.pyi b/reflex/components/el/elements/__init__.pyi index c96a80987..defaa5848 100644 --- a/reflex/components/el/elements/__init__.pyi +++ b/reflex/components/el/elements/__init__.pyi @@ -339,5 +339,5 @@ _MAPPING = { ], } EXCLUDE = ["del_", "Del", "image"] -for _, v in _MAPPING.items(): +for v in _MAPPING.values(): v.extend([mod.capitalize() for mod in v if mod not in EXCLUDE]) diff --git a/reflex/components/el/elements/base.py b/reflex/components/el/elements/base.py index a9748ae25..f6e191f68 100644 --- a/reflex/components/el/elements/base.py +++ b/reflex/components/el/elements/base.py @@ -1,4 +1,4 @@ -"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" +"""Base classes.""" from typing import Union @@ -9,7 +9,7 @@ from reflex.vars.base import Var class BaseHTML(Element): """Base class for common attributes.""" - # Provides a hint for generating a keyboard shortcut for the current element. + # Provides a hint for generating a keyboard shortcut for the current element. access_key: Var[Union[str, int, bool]] # Controls whether and how text input is automatically capitalized as it is entered/edited by the user. diff --git a/reflex/components/el/elements/base.pyi b/reflex/components/el/elements/base.pyi index 4d1d2c5c4..b60dabe87 100644 --- a/reflex/components/el/elements/base.pyi +++ b/reflex/components/el/elements/base.pyi @@ -67,7 +67,7 @@ class BaseHTML(Element): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/el/elements/forms.py b/reflex/components/el/elements/forms.py index a82d6bcdd..51ad201b2 100644 --- a/reflex/components/el/elements/forms.py +++ b/reflex/components/el/elements/forms.py @@ -1,4 +1,4 @@ -"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" +"""Forms classes.""" from __future__ import annotations @@ -18,6 +18,7 @@ from reflex.event import ( prevent_default, ) from reflex.utils.imports import ImportDict +from reflex.utils.types import is_optional from reflex.vars import VarData from reflex.vars.base import LiteralVar, Var @@ -84,7 +85,6 @@ class Datalist(BaseHTML): """Display the datalist element.""" tag = "datalist" - # No unique attributes, only common ones are inherited class Fieldset(Element): @@ -102,7 +102,7 @@ class Fieldset(Element): name: Var[Union[str, int, bool]] -def on_submit_event_spec() -> Tuple[Var[Dict[str, Any]]]: +def on_submit_event_spec() -> Tuple[Var[dict[str, Any]]]: """Event handler spec for the on_submit event. Returns: @@ -111,7 +111,7 @@ def on_submit_event_spec() -> Tuple[Var[Dict[str, Any]]]: return (FORM_DATA,) -def on_submit_string_event_spec() -> Tuple[Var[Dict[str, str]]]: +def on_submit_string_event_spec() -> Tuple[Var[dict[str, str]]]: """Event handler spec for the on_submit event. Returns: @@ -153,7 +153,7 @@ class Form(BaseHTML): target: Var[Union[str, int, bool]] # If true, the form will be cleared after submit. - reset_on_submit: Var[bool] = False # type: ignore + reset_on_submit: Var[bool] = Var.create(False) # The name used to make this form's submit handler function unique. handle_submit_unique_name: Var[str] @@ -182,9 +182,7 @@ class Form(BaseHTML): props["handle_submit_unique_name"] = "" form = super().create(*children, **props) form.handle_submit_unique_name = md5( - str({**form._get_all_hooks_internal(), **form._get_all_hooks()}).encode( - "utf-8" - ) + str(form._get_all_hooks()).encode("utf-8") ).hexdigest() return form @@ -250,11 +248,14 @@ class Form(BaseHTML): _js_expr=f"getRefValue({ref_var!s})", _var_data=VarData.merge(ref_var._get_all_var_data()), ) - # print(repr(form_refs)) return form_refs - def _get_vars(self, include_children: bool = True) -> Iterator[Var]: - yield from super()._get_vars(include_children=include_children) + def _get_vars( + self, include_children: bool = True, ignore_ids: set[int] | None = None + ) -> Iterator[Var]: + yield from super()._get_vars( + include_children=include_children, ignore_ids=ignore_ids + ) yield from self._get_form_refs().values() def _exclude_props(self) -> list[str]: @@ -384,6 +385,33 @@ class Input(BaseHTML): # Fired when a key is released on_key_up: EventHandler[key_event] + @classmethod + def create(cls, *children, **props): + """Create an Input component. + + Args: + *children: The children of the component. + **props: The properties of the component. + + Returns: + The component. + """ + from reflex.vars.number import ternary_operation + + value = props.get("value") + + # React expects an empty string(instead of null) for controlled inputs. + if value is not None and is_optional( + (value_var := Var.create(value))._var_type + ): + props["value"] = ternary_operation( + (value_var != Var.create(None)) # pyright: ignore [reportArgumentType] + & (value_var != Var(_js_expr="undefined")), + value, + Var.create(""), + ) + return super().create(*children, **props) + class Label(BaseHTML): """Display the label element.""" @@ -401,7 +429,6 @@ class Legend(BaseHTML): """Display the legend element.""" tag = "legend" - # No unique attributes, only common ones are inherited class Meter(BaseHTML): diff --git a/reflex/components/el/elements/forms.pyi b/reflex/components/el/elements/forms.pyi index e2d659338..6dbff65b2 100644 --- a/reflex/components/el/elements/forms.pyi +++ b/reflex/components/el/elements/forms.pyi @@ -103,7 +103,7 @@ class Button(BaseHTML): name: Name of the button, used when sending form data type: Type of the button (submit, reset, or button) value: Value of the button, used when sending form data - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -189,7 +189,7 @@ class Datalist(BaseHTML): Args: *children: The children of the component. - access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -270,8 +270,8 @@ class Fieldset(Element): """ ... -def on_submit_event_spec() -> Tuple[Var[Dict[str, Any]]]: ... -def on_submit_string_event_spec() -> Tuple[Var[Dict[str, str]]]: ... +def on_submit_event_spec() -> Tuple[Var[dict[str, Any]]]: ... +def on_submit_string_event_spec() -> Tuple[Var[dict[str, str]]]: ... class Form(BaseHTML): @overload @@ -341,10 +341,10 @@ class Form(BaseHTML): on_submit: Optional[ Union[ Union[ - EventType[[], BASE_STATE], EventType[[Dict[str, Any]], BASE_STATE] + EventType[[], BASE_STATE], EventType[[dict[str, Any]], BASE_STATE] ], Union[ - EventType[[], BASE_STATE], EventType[[Dict[str, str]], BASE_STATE] + EventType[[], BASE_STATE], EventType[[dict[str, str]], BASE_STATE] ], ] ] = None, @@ -367,7 +367,7 @@ class Form(BaseHTML): reset_on_submit: If true, the form will be cleared after submit. handle_submit_unique_name: The name used to make this form's submit handler function unique. on_submit: Fired when the form is submitted - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -512,7 +512,7 @@ class Input(BaseHTML): on_unmount: Optional[EventType[[], BASE_STATE]] = None, **props, ) -> "Input": - """Create the component. + """Create an Input component. Args: *children: The children of the component. @@ -554,7 +554,7 @@ class Input(BaseHTML): on_blur: Fired when the input loses focus on_key_down: Fired when a key is pressed down on_key_up: Fired when a key is released - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -576,7 +576,7 @@ class Input(BaseHTML): class_name: The class name for the component. autofocus: Whether the component should take the focus once the page is loaded custom_attrs: custom attribute - **props: The props of the component. + **props: The properties of the component. Returns: The component. @@ -644,7 +644,7 @@ class Label(BaseHTML): *children: The children of the component. html_for: ID of a form control with which the label is associated form: Associates the label with a form (by id) - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -730,7 +730,7 @@ class Legend(BaseHTML): Args: *children: The children of the component. - access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -830,7 +830,7 @@ class Meter(BaseHTML): min: Minimum value of the range optimum: Optimum value in the range value: Current value of the meter - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -920,7 +920,7 @@ class Optgroup(BaseHTML): *children: The children of the component. disabled: Disables the optgroup label: Label for the optgroup - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1014,7 +1014,7 @@ class Option(BaseHTML): label: Label for the option, if the text is not the label selected: Indicates that the option is initially selected value: Value to be sent as form data - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1106,7 +1106,7 @@ class Output(BaseHTML): html_for: Associates the output with one or more elements (by their IDs) form: Associates the output with a form (by id) name: Name of the output element for form submission - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1198,7 +1198,7 @@ class Progress(BaseHTML): form: Associates the progress element with a form (by id) max: Maximum value of the progress indicator value: Current value of the progress indicator - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1306,7 +1306,7 @@ class Select(BaseHTML): required: Indicates that the select control must have a selected option size: Number of visible options in a drop-down list on_change: Fired when the select value changes - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1459,7 +1459,7 @@ class Textarea(BaseHTML): on_blur: Fired when the input loses focus on_key_down: Fired when a key is pressed down on_key_up: Fired when a key is released - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/el/elements/inline.py b/reflex/components/el/elements/inline.py index d1bdf6b87..270eca28e 100644 --- a/reflex/components/el/elements/inline.py +++ b/reflex/components/el/elements/inline.py @@ -1,4 +1,4 @@ -"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" +"""Inline classes.""" from typing import Union diff --git a/reflex/components/el/elements/inline.pyi b/reflex/components/el/elements/inline.pyi index 1b4af4fc8..06aeeca76 100644 --- a/reflex/components/el/elements/inline.pyi +++ b/reflex/components/el/elements/inline.pyi @@ -88,7 +88,7 @@ class A(BaseHTML): rel: Specifies the relationship between the linked document and the current document shape: Specifies the shape of the area target: Specifies where to open the linked document - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -174,7 +174,7 @@ class Abbr(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -260,7 +260,7 @@ class B(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -346,7 +346,7 @@ class Bdi(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -432,7 +432,7 @@ class Bdo(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -518,7 +518,7 @@ class Br(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -604,7 +604,7 @@ class Cite(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -690,7 +690,7 @@ class Code(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -778,7 +778,7 @@ class Data(BaseHTML): Args: *children: The children of the component. value: Specifies the machine-readable translation of the data element. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -864,7 +864,7 @@ class Dfn(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -950,7 +950,7 @@ class Em(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1036,7 +1036,7 @@ class I(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1122,7 +1122,7 @@ class Kbd(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1208,7 +1208,7 @@ class Mark(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1296,7 +1296,7 @@ class Q(BaseHTML): Args: *children: The children of the component. cite: Specifies the source URL of the quote. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1382,7 +1382,7 @@ class Rp(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1468,7 +1468,7 @@ class Rt(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1554,7 +1554,7 @@ class Ruby(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1640,7 +1640,7 @@ class S(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1726,7 +1726,7 @@ class Samp(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1812,7 +1812,7 @@ class Small(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1898,7 +1898,7 @@ class Span(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1984,7 +1984,7 @@ class Strong(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -2070,7 +2070,7 @@ class Sub(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -2156,7 +2156,7 @@ class Sup(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -2244,7 +2244,7 @@ class Time(BaseHTML): Args: *children: The children of the component. date_time: Specifies the date and/or time of the element. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -2330,7 +2330,7 @@ class U(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -2416,7 +2416,7 @@ class Wbr(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/el/elements/media.py b/reflex/components/el/elements/media.py index 9935902ad..7d2f0e3e9 100644 --- a/reflex/components/el/elements/media.py +++ b/reflex/components/el/elements/media.py @@ -1,4 +1,4 @@ -"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" +"""Media classes.""" from typing import Any, Union @@ -129,7 +129,6 @@ class Img(BaseHTML): Returns: The component. - """ return ( super().create(src=children[0], **props) @@ -274,14 +273,12 @@ class Picture(BaseHTML): """Display the picture element.""" tag = "picture" - # No unique attributes, only common ones are inherited class Portal(BaseHTML): """Display the portal element.""" tag = "portal" - # No unique attributes, only common ones are inherited class Source(BaseHTML): diff --git a/reflex/components/el/elements/media.pyi b/reflex/components/el/elements/media.pyi index edaf1228e..b172d0c07 100644 --- a/reflex/components/el/elements/media.pyi +++ b/reflex/components/el/elements/media.pyi @@ -94,7 +94,7 @@ class Area(BaseHTML): rel: Specifies the relationship of the target object to the link object shape: Defines the shape of the area (rectangle, circle, polygon) target: Specifies where to open the linked document - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -198,7 +198,7 @@ class Audio(BaseHTML): muted: Indicates whether the audio is muted by default preload: Specifies how the audio file should be preloaded src: URL of the audio to play - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -314,7 +314,7 @@ class Img(BaseHTML): src: URL of the image to display src_set: A set of source sizes and URLs for responsive images use_map: The name of the map to use with the image - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -340,7 +340,6 @@ class Img(BaseHTML): Returns: The component. - """ ... @@ -403,7 +402,7 @@ class Map(BaseHTML): Args: *children: The children of the component. name: Name of the map, referenced by the 'usemap' attribute in 'img' and 'object' elements - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -499,7 +498,7 @@ class Track(BaseHTML): label: Title of the text track, used by the browser when listing available text tracks src: URL of the track file src_lang: Language of the track text data - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -609,7 +608,7 @@ class Video(BaseHTML): poster: URL of an image to show while the video is downloading, or until the user hits the play button preload: Specifies how the video file should be preloaded src: URL of the video to play - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -699,7 +698,7 @@ class Embed(BaseHTML): *children: The children of the component. src: URL of the embedded content type: Media type of the embedded content - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -805,7 +804,7 @@ class Iframe(BaseHTML): sandbox: Security restrictions for the content in the iframe src: URL of the document to display in the iframe src_doc: HTML content to embed directly within the iframe - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -901,7 +900,7 @@ class Object(BaseHTML): name: Name of the object, used for scripting or as a target for forms and links type: Media type of the data specified in the data attribute use_map: Name of an image map to use with the object - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -987,7 +986,7 @@ class Picture(BaseHTML): Args: *children: The children of the component. - access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1073,7 +1072,7 @@ class Portal(BaseHTML): Args: *children: The children of the component. - access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1169,7 +1168,7 @@ class Source(BaseHTML): src: URL of the media file or an image for the element to use src_set: A set of source sizes and URLs for responsive images type: Media type of the source - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1261,7 +1260,7 @@ class Svg(BaseHTML): width: The width of the svg. height: The height of the svg. xmlns: The XML namespace declaration. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1361,7 +1360,7 @@ class Text(BaseHTML): rotate: Rotates orientation of each individual glyph. length_adjust: How the text is stretched or compressed to fit the width defined by the text_length attribute. text_length: A width that the text should be scaled to fit. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1457,7 +1456,7 @@ class Line(BaseHTML): y1: The y-axis coordinate of the line starting point. y2: The y-axis coordinate of the the line ending point. path_length: The total path length, in user units. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1551,7 +1550,7 @@ class Circle(BaseHTML): cy: The y-axis coordinate of the center of the circle. r: The radius of the circle. path_length: The total length for the circle's circumference, in user units. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1647,7 +1646,7 @@ class Ellipse(BaseHTML): rx: The radius of the ellipse on the x axis. ry: The radius of the ellipse on the y axis. path_length: The total length for the ellipse's circumference, in user units. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1747,7 +1746,7 @@ class Rect(BaseHTML): rx: The horizontal corner radius of the rect. Defaults to ry if it is specified. ry: The vertical corner radius of the rect. Defaults to rx if it is specified. path_length: The total length of the rectangle's perimeter, in user units. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1837,7 +1836,7 @@ class Polygon(BaseHTML): *children: The children of the component. points: defines the list of points (pairs of x,y absolute coordinates) required to draw the polygon. path_length: This prop lets specify the total length for the path, in user units. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1923,7 +1922,7 @@ class Defs(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -2023,7 +2022,7 @@ class LinearGradient(BaseHTML): x2: X coordinate of the ending point of the gradient. y1: Y coordinate of the starting point of the gradient. y2: Y coordinate of the ending point of the gradient. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -2127,7 +2126,7 @@ class RadialGradient(BaseHTML): gradient_transform: Transform applied to the gradient. r: The radius of the end circle of the radial gradient. spread_method: Method used to spread the gradient. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -2223,7 +2222,7 @@ class Stop(BaseHTML): offset: Offset of the gradient stop. stop_color: Color of the gradient stop. stop_opacity: Opacity of the gradient stop. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -2311,7 +2310,7 @@ class Path(BaseHTML): Args: *children: The children of the component. d: Defines the shape of the path. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -2413,7 +2412,7 @@ class SVG(ComponentNamespace): width: The width of the svg. height: The height of the svg. xmlns: The XML namespace declaration. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/el/elements/metadata.py b/reflex/components/el/elements/metadata.py index 94c1e8faa..8e0fbcd4d 100644 --- a/reflex/components/el/elements/metadata.py +++ b/reflex/components/el/elements/metadata.py @@ -1,4 +1,4 @@ -"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" +"""Metadata classes.""" from typing import List, Union @@ -81,7 +81,7 @@ class Title(Element): tag = "title" -# Had to be named with an underscore so it doesnt conflict with reflex.style Style in pyi +# Had to be named with an underscore so it doesn't conflict with reflex.style Style in pyi class StyleEl(Element): """Display the style element.""" diff --git a/reflex/components/el/elements/metadata.pyi b/reflex/components/el/elements/metadata.pyi index 5af92e2b2..08cd2fd76 100644 --- a/reflex/components/el/elements/metadata.pyi +++ b/reflex/components/el/elements/metadata.pyi @@ -71,7 +71,7 @@ class Base(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -157,7 +157,7 @@ class Head(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -265,7 +265,7 @@ class Link(BaseHTML): rel: Specifies the relationship between the current document and the linked one sizes: Specifies the sizes of icons for visual media type: Specifies the MIME type of the linked document - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -359,7 +359,7 @@ class Meta(BaseHTML): content: Defines the content of the metadata http_equiv: Provides an HTTP header for the information/value of the content attribute name: Specifies a name for the metadata - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/el/elements/other.py b/reflex/components/el/elements/other.py index fa7c6cdec..4e7f0f227 100644 --- a/reflex/components/el/elements/other.py +++ b/reflex/components/el/elements/other.py @@ -1,4 +1,4 @@ -"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" +"""Other classes.""" from typing import Union @@ -26,31 +26,39 @@ class Dialog(BaseHTML): class Summary(BaseHTML): - """Display the summary element.""" + """Display the summary element. + + Used as a summary or caption for a
element. + """ tag = "summary" - # No unique attributes, only common ones are inherited; used as a summary or caption for a
element class Slot(BaseHTML): - """Display the slot element.""" + """Display the slot element. + + Used as a placeholder inside a web component. + """ tag = "slot" - # No unique attributes, only common ones are inherited; used as a placeholder inside a web component class Template(BaseHTML): - """Display the template element.""" + """Display the template element. + + Used for declaring fragments of HTML that can be cloned and inserted in the document. + """ tag = "template" - # No unique attributes, only common ones are inherited; used for declaring fragments of HTML that can be cloned and inserted in the document class Math(BaseHTML): - """Display the math element.""" + """Display the math element. + + Represents a mathematical expression. + """ tag = "math" - # No unique attributes, only common ones are inherited; used for displaying mathematical expressions class Html(BaseHTML): diff --git a/reflex/components/el/elements/other.pyi b/reflex/components/el/elements/other.pyi index 3d65af647..57e4ab24b 100644 --- a/reflex/components/el/elements/other.pyi +++ b/reflex/components/el/elements/other.pyi @@ -70,7 +70,7 @@ class Details(BaseHTML): Args: *children: The children of the component. open: Indicates whether the details will be visible (expanded) to the user - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -158,7 +158,7 @@ class Dialog(BaseHTML): Args: *children: The children of the component. open: Indicates whether the dialog is active and can be interacted with - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -244,7 +244,7 @@ class Summary(BaseHTML): Args: *children: The children of the component. - access_key: No unique attributes, only common ones are inherited; used as a summary or caption for a
element Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -330,7 +330,7 @@ class Slot(BaseHTML): Args: *children: The children of the component. - access_key: No unique attributes, only common ones are inherited; used as a placeholder inside a web component Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -416,7 +416,7 @@ class Template(BaseHTML): Args: *children: The children of the component. - access_key: No unique attributes, only common ones are inherited; used for declaring fragments of HTML that can be cloned and inserted in the document Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -502,7 +502,7 @@ class Math(BaseHTML): Args: *children: The children of the component. - access_key: No unique attributes, only common ones are inherited; used for displaying mathematical expressions Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -590,7 +590,7 @@ class Html(BaseHTML): Args: *children: The children of the component. manifest: Specifies the URL of the document's cache manifest (obsolete in HTML5) - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/el/elements/scripts.py b/reflex/components/el/elements/scripts.py index b53306e02..c30931e99 100644 --- a/reflex/components/el/elements/scripts.py +++ b/reflex/components/el/elements/scripts.py @@ -1,4 +1,4 @@ -"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" +"""Scripts classes.""" from typing import Union @@ -17,7 +17,6 @@ class Noscript(BaseHTML): """Display the noscript element.""" tag = "noscript" - # No unique attributes, only common ones are inherited class Script(BaseHTML): diff --git a/reflex/components/el/elements/scripts.pyi b/reflex/components/el/elements/scripts.pyi index 6f03c20cb..c66e150af 100644 --- a/reflex/components/el/elements/scripts.pyi +++ b/reflex/components/el/elements/scripts.pyi @@ -68,7 +68,7 @@ class Canvas(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -154,7 +154,7 @@ class Noscript(BaseHTML): Args: *children: The children of the component. - access_key: No unique attributes, only common ones are inherited Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -262,7 +262,7 @@ class Script(BaseHTML): referrer_policy: Specifies which referrer information to send when fetching the script src: URL of an external script type: Specifies the MIME type of the script - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/el/elements/sectioning.py b/reflex/components/el/elements/sectioning.py index e74d7929c..cfe82b6d5 100644 --- a/reflex/components/el/elements/sectioning.py +++ b/reflex/components/el/elements/sectioning.py @@ -1,4 +1,4 @@ -"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" +"""Sectioning classes.""" from .base import BaseHTML diff --git a/reflex/components/el/elements/sectioning.pyi b/reflex/components/el/elements/sectioning.pyi index e34d53ee2..ecbabe516 100644 --- a/reflex/components/el/elements/sectioning.pyi +++ b/reflex/components/el/elements/sectioning.pyi @@ -68,7 +68,7 @@ class Body(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -154,7 +154,7 @@ class Address(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -240,7 +240,7 @@ class Article(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -326,7 +326,7 @@ class Aside(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -412,7 +412,7 @@ class Footer(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -498,7 +498,7 @@ class Header(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -584,7 +584,7 @@ class H1(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -670,7 +670,7 @@ class H2(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -756,7 +756,7 @@ class H3(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -842,7 +842,7 @@ class H4(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -928,7 +928,7 @@ class H5(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1014,7 +1014,7 @@ class H6(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1100,7 +1100,7 @@ class Main(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1186,7 +1186,7 @@ class Nav(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1272,7 +1272,7 @@ class Section(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/el/elements/tables.py b/reflex/components/el/elements/tables.py index 8f6cfcba4..a0c10d829 100644 --- a/reflex/components/el/elements/tables.py +++ b/reflex/components/el/elements/tables.py @@ -1,4 +1,4 @@ -"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" +"""Tables classes.""" from typing import Union diff --git a/reflex/components/el/elements/tables.pyi b/reflex/components/el/elements/tables.pyi index b0495009d..420bad585 100644 --- a/reflex/components/el/elements/tables.pyi +++ b/reflex/components/el/elements/tables.pyi @@ -70,7 +70,7 @@ class Caption(BaseHTML): Args: *children: The children of the component. align: Alignment of the caption - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -160,7 +160,7 @@ class Col(BaseHTML): *children: The children of the component. align: Alignment of the content within the column span: Number of columns the col element spans - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -250,7 +250,7 @@ class Colgroup(BaseHTML): *children: The children of the component. align: Alignment of the content within the column group span: Number of columns the colgroup element spans - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -340,7 +340,7 @@ class Table(BaseHTML): *children: The children of the component. align: Alignment of the table summary: Provides a summary of the table's purpose and structure - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -428,7 +428,7 @@ class Tbody(BaseHTML): Args: *children: The children of the component. align: Alignment of the content within the table body - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -522,7 +522,7 @@ class Td(BaseHTML): col_span: Number of columns a cell should span headers: IDs of the headers associated with this cell row_span: Number of rows a cell should span - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -610,7 +610,7 @@ class Tfoot(BaseHTML): Args: *children: The children of the component. align: Alignment of the content within the table footer - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -706,7 +706,7 @@ class Th(BaseHTML): headers: IDs of the headers associated with this header cell row_span: Number of rows a header cell should span scope: Scope of the header cell (row, col, rowgroup, colgroup) - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -794,7 +794,7 @@ class Thead(BaseHTML): Args: *children: The children of the component. align: Alignment of the content within the table header - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -882,7 +882,7 @@ class Tr(BaseHTML): Args: *children: The children of the component. align: Alignment of the content within the table row - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/el/elements/typography.py b/reflex/components/el/elements/typography.py index 7c55ecce7..9fa5c3a02 100644 --- a/reflex/components/el/elements/typography.py +++ b/reflex/components/el/elements/typography.py @@ -1,4 +1,4 @@ -"""Element classes. This is an auto-generated file. Do not edit. See ../generate.py.""" +"""Typography classes.""" from typing import Union diff --git a/reflex/components/el/elements/typography.pyi b/reflex/components/el/elements/typography.pyi index b28af1c40..8332b3306 100644 --- a/reflex/components/el/elements/typography.pyi +++ b/reflex/components/el/elements/typography.pyi @@ -70,7 +70,7 @@ class Blockquote(BaseHTML): Args: *children: The children of the component. cite: Define the title of a work. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -156,7 +156,7 @@ class Dd(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -242,7 +242,7 @@ class Div(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -328,7 +328,7 @@ class Dl(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -414,7 +414,7 @@ class Dt(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -500,7 +500,7 @@ class Figcaption(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -588,7 +588,7 @@ class Hr(BaseHTML): Args: *children: The children of the component. align: Used to specify the alignment of text content of The Element. this attribute is used in all elements. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -674,7 +674,7 @@ class Li(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -762,7 +762,7 @@ class Menu(BaseHTML): Args: *children: The children of the component. type: Specifies that the menu element is a context menu. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -854,7 +854,7 @@ class Ol(BaseHTML): reversed: Reverses the order of the list. start: Specifies the start value of the first list item in an ordered list. type: Specifies the kind of marker to use in the list (letters or numbers). - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -940,7 +940,7 @@ class P(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1026,7 +1026,7 @@ class Pre(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1112,7 +1112,7 @@ class Ul(BaseHTML): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1202,7 +1202,7 @@ class Ins(BaseHTML): *children: The children of the component. cite: Specifies the URL of the document that explains the reason why the text was inserted/changed. date_time: Specifies the date and time of when the text was inserted/changed. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1292,7 +1292,7 @@ class Del(BaseHTML): *children: The children of the component. cite: Specifies the URL of the document that explains the reason why the text was deleted. date_time: Specifies the date and time of when the text was deleted. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/lucide/icon.py b/reflex/components/lucide/icon.py index b32fb8de3..6c7cbede7 100644 --- a/reflex/components/lucide/icon.py +++ b/reflex/components/lucide/icon.py @@ -2,13 +2,15 @@ from reflex.components.component import Component from reflex.utils import format -from reflex.vars.base import Var +from reflex.utils.imports import ImportVar +from reflex.vars.base import LiteralVar, Var +from reflex.vars.sequence import LiteralStringVar class LucideIconComponent(Component): """Lucide Icon Component.""" - library = "lucide-react@0.359.0" + library = "lucide-react@0.471.1" class Icon(LucideIconComponent): @@ -32,6 +34,7 @@ class Icon(LucideIconComponent): Raises: AttributeError: The errors tied to bad usage of the Icon component. ValueError: If the icon tag is invalid. + TypeError: If the icon name is not a string. Returns: The created component. @@ -39,7 +42,6 @@ class Icon(LucideIconComponent): if children: if len(children) == 1 and isinstance(children[0], str): props["tag"] = children[0] - children = [] else: raise AttributeError( f"Passing multiple children to Icon component is not allowed: remove positional arguments {children[1:]} to fix" @@ -47,19 +49,46 @@ class Icon(LucideIconComponent): if "tag" not in props: raise AttributeError("Missing 'tag' keyword-argument for Icon") + tag: str | Var | LiteralVar = props.pop("tag") + if isinstance(tag, LiteralVar): + if isinstance(tag, LiteralStringVar): + tag = tag._var_value + else: + raise TypeError(f"Icon name must be a string, got {type(tag)}") + elif isinstance(tag, Var): + return DynamicIcon.create(name=tag, **props) + if ( - not isinstance(props["tag"], str) - or format.to_snake_case(props["tag"]) not in LUCIDE_ICON_LIST + not isinstance(tag, str) + or format.to_snake_case(tag) not in LUCIDE_ICON_LIST ): raise ValueError( - f"Invalid icon tag: {props['tag']}. Please use one of the following: {', '.join(LUCIDE_ICON_LIST[0:25])}, ..." + f"Invalid icon tag: {tag}. Please use one of the following: {', '.join(LUCIDE_ICON_LIST[0:25])}, ..." "\nSee full list at https://lucide.dev/icons." ) - props["tag"] = format.to_title_case(format.to_snake_case(props["tag"])) + "Icon" + if tag in LUCIDE_ICON_MAPPING_OVERRIDE: + props["tag"] = LUCIDE_ICON_MAPPING_OVERRIDE[tag] + else: + props["tag"] = format.to_title_case(format.to_snake_case(tag)) + "Icon" props["alias"] = f"Lucide{props['tag']}" props.setdefault("color", "var(--current-color)") - return super().create(*children, **props) + return super().create(**props) + + +class DynamicIcon(LucideIconComponent): + """A DynamicIcon component.""" + + tag = "DynamicIcon" + + name: Var[str] + + def _get_imports(self): + _imports = super()._get_imports() + if self.library: + _imports.pop(self.library) + _imports["lucide-react/dynamic"] = [ImportVar("DynamicIcon", install=False)] + return _imports LUCIDE_ICON_LIST = [ @@ -106,6 +135,7 @@ LUCIDE_ICON_LIST = [ "ambulance", "ampersand", "ampersands", + "amphora", "anchor", "angry", "annoyed", @@ -193,6 +223,7 @@ LUCIDE_ICON_LIST = [ "baggage_claim", "ban", "banana", + "bandage", "banknote", "bar_chart", "bar_chart_2", @@ -230,8 +261,10 @@ LUCIDE_ICON_LIST = [ "between_horizontal_start", "between_vertical_end", "between_vertical_start", + "biceps_flexed", "bike", "binary", + "binoculars", "biohazard", "bird", "bitcoin", @@ -278,6 +311,7 @@ LUCIDE_ICON_LIST = [ "boom_box", "bot", "bot_message_square", + "bot_off", "box", "box_select", "boxes", @@ -289,6 +323,7 @@ LUCIDE_ICON_LIST = [ "brick_wall", "briefcase", "briefcase_business", + "briefcase_conveyor_belt", "briefcase_medical", "bring_to_front", "brush", @@ -305,9 +340,13 @@ LUCIDE_ICON_LIST = [ "cake_slice", "calculator", "calendar", + "calendar_1", + "calendar_arrow_down", + "calendar_arrow_up", "calendar_check", "calendar_check_2", "calendar_clock", + "calendar_cog", "calendar_days", "calendar_fold", "calendar_heart", @@ -318,6 +357,7 @@ LUCIDE_ICON_LIST = [ "calendar_plus_2", "calendar_range", "calendar_search", + "calendar_sync", "calendar_x", "calendar_x_2", "camera", @@ -342,6 +382,29 @@ LUCIDE_ICON_LIST = [ "castle", "cat", "cctv", + "chart_area", + "chart_bar", + "chart_bar_big", + "chart_bar_decreasing", + "chart_bar_increasing", + "chart_bar_stacked", + "chart_candlestick", + "chart_column", + "chart_column_big", + "chart_column_decreasing", + "chart_column_increasing", + "chart_column_stacked", + "chart_gantt", + "chart_line", + "chart_network", + "chart_no_axes_column", + "chart_no_axes_column_decreasing", + "chart_no_axes_column_increasing", + "chart_no_axes_combined", + "chart_no_axes_gantt", + "chart_pie", + "chart_scatter", + "chart_spline", "check", "check_check", "chef_hat", @@ -356,6 +419,7 @@ LUCIDE_ICON_LIST = [ "chevrons_down_up", "chevrons_left", "chevrons_left_right", + "chevrons_left_right_ellipsis", "chevrons_right", "chevrons_right_left", "chevrons_up", @@ -374,8 +438,8 @@ LUCIDE_ICON_LIST = [ "circle_arrow_out_up_right", "circle_arrow_right", "circle_arrow_up", - "circle_check_big", "circle_check", + "circle_check_big", "circle_chevron_down", "circle_chevron_left", "circle_chevron_right", @@ -387,13 +451,14 @@ LUCIDE_ICON_LIST = [ "circle_dot_dashed", "circle_ellipsis", "circle_equal", + "circle_fading_arrow_up", "circle_fading_plus", "circle_gauge", "circle_help", "circle_minus", "circle_off", - "circle_parking_off", "circle_parking", + "circle_parking_off", "circle_pause", "circle_percent", "circle_play", @@ -432,7 +497,11 @@ LUCIDE_ICON_LIST = [ "clock_7", "clock_8", "clock_9", + "clock_alert", + "clock_arrow_down", + "clock_arrow_up", "cloud", + "cloud_alert", "cloud_cog", "cloud_download", "cloud_drizzle", @@ -503,6 +572,7 @@ LUCIDE_ICON_LIST = [ "cup_soda", "currency", "cylinder", + "dam", "database", "database_backup", "database_zap", @@ -510,7 +580,9 @@ LUCIDE_ICON_LIST = [ "dessert", "diameter", "diamond", + "diamond_minus", "diamond_percent", + "diamond_plus", "dice_1", "dice_2", "dice_3", @@ -539,6 +611,7 @@ LUCIDE_ICON_LIST = [ "dribbble", "drill", "droplet", + "droplet_off", "droplets", "drum", "drumstick", @@ -554,12 +627,15 @@ LUCIDE_ICON_LIST = [ "ellipsis", "ellipsis_vertical", "equal", + "equal_approximately", "equal_not", "eraser", + "ethernet_port", "euro", "expand", "external_link", "eye", + "eye_closed", "eye_off", "facebook", "factory", @@ -579,6 +655,10 @@ LUCIDE_ICON_LIST = [ "file_bar_chart", "file_bar_chart_2", "file_box", + "file_chart_column", + "file_chart_column_increasing", + "file_chart_line", + "file_chart_pie", "file_check", "file_check_2", "file_clock", @@ -620,6 +700,7 @@ LUCIDE_ICON_LIST = [ "file_type", "file_type_2", "file_up", + "file_user", "file_video", "file_video_2", "file_volume", @@ -661,6 +742,7 @@ LUCIDE_ICON_LIST = [ "folder_check", "folder_clock", "folder_closed", + "folder_code", "folder_cog", "folder_dot", "folder_down", @@ -733,7 +815,12 @@ LUCIDE_ICON_LIST = [ "graduation_cap", "grape", "grid_2x2", + "grid_2x_2", + "grid_2x_2_check", + "grid_2x_2_plus", + "grid_2x_2_x", "grid_3x3", + "grid_3x_3", "grip", "grip_horizontal", "grip_vertical", @@ -762,6 +849,7 @@ LUCIDE_ICON_LIST = [ "heading_4", "heading_5", "heading_6", + "headphone_off", "headphones", "headset", "heart", @@ -779,14 +867,21 @@ LUCIDE_ICON_LIST = [ "hospital", "hotel", "hourglass", + "house", + "house_plug", + "house_plus", + "house_wifi", "ice_cream_bowl", "ice_cream_cone", + "id_card", "image", "image_down", "image_minus", "image_off", + "image_play", "image_plus", "image_up", + "image_upscale", "images", "import", "inbox", @@ -808,6 +903,7 @@ LUCIDE_ICON_LIST = [ "key_square", "keyboard", "keyboard_music", + "keyboard_off", "lamp", "lamp_ceiling", "lamp_desk", @@ -817,8 +913,9 @@ LUCIDE_ICON_LIST = [ "land_plot", "landmark", "languages", - "laptop_minimal", "laptop", + "laptop_minimal", + "laptop_minimal_check", "lasso", "lasso_select", "laugh", @@ -833,6 +930,8 @@ LUCIDE_ICON_LIST = [ "layout_template", "leaf", "leafy_green", + "lectern", + "letter_text", "library", "library_big", "life_buoy", @@ -845,10 +944,12 @@ LUCIDE_ICON_LIST = [ "link_2_off", "linkedin", "list", + "list_check", "list_checks", "list_collapse", "list_end", "list_filter", + "list_filter_plus", "list_minus", "list_music", "list_ordered", @@ -861,15 +962,17 @@ LUCIDE_ICON_LIST = [ "list_x", "loader", "loader_circle", + "loader_pinwheel", "locate", "locate_fixed", "locate_off", "lock", - "lock_keyhole_open", "lock_keyhole", + "lock_keyhole_open", "lock_open", "log_in", "log_out", + "logs", "lollipop", "luggage", "magnet", @@ -886,7 +989,16 @@ LUCIDE_ICON_LIST = [ "mails", "map", "map_pin", + "map_pin_check", + "map_pin_check_inside", + "map_pin_house", + "map_pin_minus", + "map_pin_minus_inside", "map_pin_off", + "map_pin_plus", + "map_pin_plus_inside", + "map_pin_x", + "map_pin_x_inside", "map_pinned", "martini", "maximize", @@ -915,6 +1027,7 @@ LUCIDE_ICON_LIST = [ "message_square_diff", "message_square_dot", "message_square_heart", + "message_square_lock", "message_square_more", "message_square_off", "message_square_plus", @@ -926,8 +1039,9 @@ LUCIDE_ICON_LIST = [ "message_square_x", "messages_square", "mic", - "mic_vocal", "mic_off", + "mic_vocal", + "microchip", "microscope", "microwave", "milestone", @@ -938,6 +1052,7 @@ LUCIDE_ICON_LIST = [ "minus", "monitor", "monitor_check", + "monitor_cog", "monitor_dot", "monitor_down", "monitor_off", @@ -953,8 +1068,10 @@ LUCIDE_ICON_LIST = [ "mountain", "mountain_snow", "mouse", + "mouse_off", "mouse_pointer", "mouse_pointer_2", + "mouse_pointer_ban", "mouse_pointer_click", "move", "move_3d", @@ -991,10 +1108,13 @@ LUCIDE_ICON_LIST = [ "nut_off", "octagon", "octagon_alert", + "octagon_minus", "octagon_pause", "octagon_x", + "omega", "option", "orbit", + "origami", "package", "package_2", "package_check", @@ -1007,6 +1127,7 @@ LUCIDE_ICON_LIST = [ "paint_roller", "paintbrush", "paintbrush_2", + "paintbrush_vertical", "palette", "panel_bottom", "panel_bottom_close", @@ -1036,13 +1157,16 @@ LUCIDE_ICON_LIST = [ "pc_case", "pen", "pen_line", + "pen_off", "pen_tool", "pencil", "pencil_line", + "pencil_off", "pencil_ruler", "pentagon", "percent", "person_standing", + "philippine_peso", "phone", "phone_call", "phone_forwarded", @@ -1058,7 +1182,10 @@ LUCIDE_ICON_LIST = [ "pie_chart", "piggy_bank", "pilcrow", + "pilcrow_left", + "pilcrow_right", "pill", + "pill_bottle", "pin", "pin_off", "pipette", @@ -1084,6 +1211,7 @@ LUCIDE_ICON_LIST = [ "power_off", "presentation", "printer", + "printer_check", "projector", "proportions", "puzzle", @@ -1158,6 +1286,7 @@ LUCIDE_ICON_LIST = [ "satellite_dish", "save", "save_all", + "save_off", "scale", "scale_3d", "scaling", @@ -1165,7 +1294,9 @@ LUCIDE_ICON_LIST = [ "scan_barcode", "scan_eye", "scan_face", + "scan_heart", "scan_line", + "scan_qr_code", "scan_search", "scan_text", "scatter_chart", @@ -1181,6 +1312,7 @@ LUCIDE_ICON_LIST = [ "search_code", "search_slash", "search_x", + "section", "send", "send_horizontal", "send_to_back", @@ -1225,6 +1357,7 @@ LUCIDE_ICON_LIST = [ "signal_low", "signal_medium", "signal_zero", + "signature", "signpost", "signpost_big", "siren", @@ -1234,8 +1367,8 @@ LUCIDE_ICON_LIST = [ "slack", "slash", "slice", - "sliders_vertical", "sliders_horizontal", + "sliders_vertical", "smartphone", "smartphone_charging", "smartphone_nfc", @@ -1259,29 +1392,31 @@ LUCIDE_ICON_LIST = [ "sprout", "square", "square_activity", + "square_arrow_down", "square_arrow_down_left", "square_arrow_down_right", - "square_arrow_down", "square_arrow_left", "square_arrow_out_down_left", "square_arrow_out_down_right", "square_arrow_out_up_left", "square_arrow_out_up_right", "square_arrow_right", + "square_arrow_up", "square_arrow_up_left", "square_arrow_up_right", - "square_arrow_up", "square_asterisk", "square_bottom_dashed_scissors", - "square_check_big", + "square_chart_gantt", "square_check", + "square_check_big", "square_chevron_down", "square_chevron_left", "square_chevron_right", "square_chevron_up", "square_code", - "square_dashed_bottom_code", + "square_dashed", "square_dashed_bottom", + "square_dashed_bottom_code", "square_dashed_kanban", "square_dashed_mouse_pointer", "square_divide", @@ -1295,8 +1430,8 @@ LUCIDE_ICON_LIST = [ "square_menu", "square_minus", "square_mouse_pointer", - "square_parking_off", "square_parking", + "square_parking_off", "square_pen", "square_percent", "square_pi", @@ -1310,10 +1445,11 @@ LUCIDE_ICON_LIST = [ "square_slash", "square_split_horizontal", "square_split_vertical", + "square_square", "square_stack", "square_terminal", - "square_user_round", "square_user", + "square_user_round", "square_x", "squircle", "squirrel", @@ -1350,6 +1486,7 @@ LUCIDE_ICON_LIST = [ "table_cells_merge", "table_cells_split", "table_columns_split", + "table_of_contents", "table_properties", "table_rows_split", "tablet", @@ -1365,11 +1502,11 @@ LUCIDE_ICON_LIST = [ "tangent", "target", "telescope", + "tent", "tent_tree", "terminal", - "test_tube_diagonal", "test_tube", - "tent", + "test_tube_diagonal", "test_tubes", "text", "text_cursor", @@ -1390,11 +1527,14 @@ LUCIDE_ICON_LIST = [ "ticket_plus", "ticket_slash", "ticket_x", + "tickets", + "tickets_plane", "timer", "timer_off", "timer_reset", "toggle_left", "toggle_right", + "toilet", "tornado", "torus", "touchpad", @@ -1416,17 +1556,22 @@ LUCIDE_ICON_LIST = [ "trello", "trending_down", "trending_up", + "trending_up_down", "triangle", - "triangle_right", "triangle_alert", + "triangle_dashed", + "triangle_right", "trophy", "truck", "turtle", "tv", "tv_2", + "tv_minimal", + "tv_minimal_play", "twitch", "twitter", "type", + "type_outline", "umbrella", "umbrella_off", "underline", @@ -1437,8 +1582,8 @@ LUCIDE_ICON_LIST = [ "unfold_vertical", "ungroup", "university", - "unlink_2", "unlink", + "unlink_2", "unplug", "upload", "usb", @@ -1446,11 +1591,13 @@ LUCIDE_ICON_LIST = [ "user_check", "user_cog", "user_minus", + "user_pen", "user_plus", "user_round", "user_round_check", "user_round_cog", "user_round_minus", + "user_round_pen", "user_round_plus", "user_round_search", "user_round_x", @@ -1472,14 +1619,16 @@ LUCIDE_ICON_LIST = [ "videotape", "view", "voicemail", + "volleyball", "volume", "volume_1", "volume_2", + "volume_off", "volume_x", "vote", "wallet", - "wallet_minimal", "wallet_cards", + "wallet_minimal", "wallpaper", "wand", "wand_sparkles", @@ -1487,17 +1636,22 @@ LUCIDE_ICON_LIST = [ "washing_machine", "watch", "waves", + "waves_ladder", "waypoints", "webcam", - "webhook_off", "webhook", + "webhook_off", "weight", "wheat", "wheat_off", "whole_word", "wifi", + "wifi_high", + "wifi_low", "wifi_off", + "wifi_zero", "wind", + "wind_arrow_down", "wine", "wine_off", "workflow", @@ -1511,3 +1665,10 @@ LUCIDE_ICON_LIST = [ "zoom_in", "zoom_out", ] + +# The default transformation of some icon names doesn't match how the +# icons are exported from Lucide. Manual overrides can go here. +LUCIDE_ICON_MAPPING_OVERRIDE = { + "grid_2x_2_check": "Grid2x2Check", + "grid_2x_2_x": "Grid2x2X", +} diff --git a/reflex/components/lucide/icon.pyi b/reflex/components/lucide/icon.pyi index 7f59edec5..6094cfd87 100644 --- a/reflex/components/lucide/icon.pyi +++ b/reflex/components/lucide/icon.pyi @@ -104,12 +104,60 @@ class Icon(LucideIconComponent): Raises: AttributeError: The errors tied to bad usage of the Icon component. ValueError: If the icon tag is invalid. + TypeError: If the icon name is not a string. Returns: The created component. """ ... +class DynamicIcon(LucideIconComponent): + @overload + @classmethod + def create( # type: ignore + cls, + *children, + name: Optional[Union[Var[str], str]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_blur: Optional[EventType[[], BASE_STATE]] = None, + on_click: Optional[EventType[[], BASE_STATE]] = None, + on_context_menu: Optional[EventType[[], BASE_STATE]] = None, + on_double_click: Optional[EventType[[], BASE_STATE]] = None, + on_focus: Optional[EventType[[], BASE_STATE]] = None, + on_mount: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_down: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_enter: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_leave: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_move: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_out: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_over: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_up: Optional[EventType[[], BASE_STATE]] = None, + on_scroll: Optional[EventType[[], BASE_STATE]] = None, + on_unmount: Optional[EventType[[], BASE_STATE]] = None, + **props, + ) -> "DynamicIcon": + """Create the component. + + Args: + *children: The children of the component. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The props of the component. + + Returns: + The component. + """ + ... + LUCIDE_ICON_LIST = [ "a_arrow_down", "a_arrow_up", @@ -154,6 +202,7 @@ LUCIDE_ICON_LIST = [ "ambulance", "ampersand", "ampersands", + "amphora", "anchor", "angry", "annoyed", @@ -241,6 +290,7 @@ LUCIDE_ICON_LIST = [ "baggage_claim", "ban", "banana", + "bandage", "banknote", "bar_chart", "bar_chart_2", @@ -278,8 +328,10 @@ LUCIDE_ICON_LIST = [ "between_horizontal_start", "between_vertical_end", "between_vertical_start", + "biceps_flexed", "bike", "binary", + "binoculars", "biohazard", "bird", "bitcoin", @@ -326,6 +378,7 @@ LUCIDE_ICON_LIST = [ "boom_box", "bot", "bot_message_square", + "bot_off", "box", "box_select", "boxes", @@ -337,6 +390,7 @@ LUCIDE_ICON_LIST = [ "brick_wall", "briefcase", "briefcase_business", + "briefcase_conveyor_belt", "briefcase_medical", "bring_to_front", "brush", @@ -353,9 +407,13 @@ LUCIDE_ICON_LIST = [ "cake_slice", "calculator", "calendar", + "calendar_1", + "calendar_arrow_down", + "calendar_arrow_up", "calendar_check", "calendar_check_2", "calendar_clock", + "calendar_cog", "calendar_days", "calendar_fold", "calendar_heart", @@ -366,6 +424,7 @@ LUCIDE_ICON_LIST = [ "calendar_plus_2", "calendar_range", "calendar_search", + "calendar_sync", "calendar_x", "calendar_x_2", "camera", @@ -390,6 +449,29 @@ LUCIDE_ICON_LIST = [ "castle", "cat", "cctv", + "chart_area", + "chart_bar", + "chart_bar_big", + "chart_bar_decreasing", + "chart_bar_increasing", + "chart_bar_stacked", + "chart_candlestick", + "chart_column", + "chart_column_big", + "chart_column_decreasing", + "chart_column_increasing", + "chart_column_stacked", + "chart_gantt", + "chart_line", + "chart_network", + "chart_no_axes_column", + "chart_no_axes_column_decreasing", + "chart_no_axes_column_increasing", + "chart_no_axes_combined", + "chart_no_axes_gantt", + "chart_pie", + "chart_scatter", + "chart_spline", "check", "check_check", "chef_hat", @@ -404,6 +486,7 @@ LUCIDE_ICON_LIST = [ "chevrons_down_up", "chevrons_left", "chevrons_left_right", + "chevrons_left_right_ellipsis", "chevrons_right", "chevrons_right_left", "chevrons_up", @@ -422,8 +505,8 @@ LUCIDE_ICON_LIST = [ "circle_arrow_out_up_right", "circle_arrow_right", "circle_arrow_up", - "circle_check_big", "circle_check", + "circle_check_big", "circle_chevron_down", "circle_chevron_left", "circle_chevron_right", @@ -435,13 +518,14 @@ LUCIDE_ICON_LIST = [ "circle_dot_dashed", "circle_ellipsis", "circle_equal", + "circle_fading_arrow_up", "circle_fading_plus", "circle_gauge", "circle_help", "circle_minus", "circle_off", - "circle_parking_off", "circle_parking", + "circle_parking_off", "circle_pause", "circle_percent", "circle_play", @@ -480,7 +564,11 @@ LUCIDE_ICON_LIST = [ "clock_7", "clock_8", "clock_9", + "clock_alert", + "clock_arrow_down", + "clock_arrow_up", "cloud", + "cloud_alert", "cloud_cog", "cloud_download", "cloud_drizzle", @@ -551,6 +639,7 @@ LUCIDE_ICON_LIST = [ "cup_soda", "currency", "cylinder", + "dam", "database", "database_backup", "database_zap", @@ -558,7 +647,9 @@ LUCIDE_ICON_LIST = [ "dessert", "diameter", "diamond", + "diamond_minus", "diamond_percent", + "diamond_plus", "dice_1", "dice_2", "dice_3", @@ -587,6 +678,7 @@ LUCIDE_ICON_LIST = [ "dribbble", "drill", "droplet", + "droplet_off", "droplets", "drum", "drumstick", @@ -602,12 +694,15 @@ LUCIDE_ICON_LIST = [ "ellipsis", "ellipsis_vertical", "equal", + "equal_approximately", "equal_not", "eraser", + "ethernet_port", "euro", "expand", "external_link", "eye", + "eye_closed", "eye_off", "facebook", "factory", @@ -627,6 +722,10 @@ LUCIDE_ICON_LIST = [ "file_bar_chart", "file_bar_chart_2", "file_box", + "file_chart_column", + "file_chart_column_increasing", + "file_chart_line", + "file_chart_pie", "file_check", "file_check_2", "file_clock", @@ -668,6 +767,7 @@ LUCIDE_ICON_LIST = [ "file_type", "file_type_2", "file_up", + "file_user", "file_video", "file_video_2", "file_volume", @@ -709,6 +809,7 @@ LUCIDE_ICON_LIST = [ "folder_check", "folder_clock", "folder_closed", + "folder_code", "folder_cog", "folder_dot", "folder_down", @@ -781,7 +882,12 @@ LUCIDE_ICON_LIST = [ "graduation_cap", "grape", "grid_2x2", + "grid_2x_2", + "grid_2x_2_check", + "grid_2x_2_plus", + "grid_2x_2_x", "grid_3x3", + "grid_3x_3", "grip", "grip_horizontal", "grip_vertical", @@ -810,6 +916,7 @@ LUCIDE_ICON_LIST = [ "heading_4", "heading_5", "heading_6", + "headphone_off", "headphones", "headset", "heart", @@ -827,14 +934,21 @@ LUCIDE_ICON_LIST = [ "hospital", "hotel", "hourglass", + "house", + "house_plug", + "house_plus", + "house_wifi", "ice_cream_bowl", "ice_cream_cone", + "id_card", "image", "image_down", "image_minus", "image_off", + "image_play", "image_plus", "image_up", + "image_upscale", "images", "import", "inbox", @@ -856,6 +970,7 @@ LUCIDE_ICON_LIST = [ "key_square", "keyboard", "keyboard_music", + "keyboard_off", "lamp", "lamp_ceiling", "lamp_desk", @@ -865,8 +980,9 @@ LUCIDE_ICON_LIST = [ "land_plot", "landmark", "languages", - "laptop_minimal", "laptop", + "laptop_minimal", + "laptop_minimal_check", "lasso", "lasso_select", "laugh", @@ -881,6 +997,8 @@ LUCIDE_ICON_LIST = [ "layout_template", "leaf", "leafy_green", + "lectern", + "letter_text", "library", "library_big", "life_buoy", @@ -893,10 +1011,12 @@ LUCIDE_ICON_LIST = [ "link_2_off", "linkedin", "list", + "list_check", "list_checks", "list_collapse", "list_end", "list_filter", + "list_filter_plus", "list_minus", "list_music", "list_ordered", @@ -909,15 +1029,17 @@ LUCIDE_ICON_LIST = [ "list_x", "loader", "loader_circle", + "loader_pinwheel", "locate", "locate_fixed", "locate_off", "lock", - "lock_keyhole_open", "lock_keyhole", + "lock_keyhole_open", "lock_open", "log_in", "log_out", + "logs", "lollipop", "luggage", "magnet", @@ -934,7 +1056,16 @@ LUCIDE_ICON_LIST = [ "mails", "map", "map_pin", + "map_pin_check", + "map_pin_check_inside", + "map_pin_house", + "map_pin_minus", + "map_pin_minus_inside", "map_pin_off", + "map_pin_plus", + "map_pin_plus_inside", + "map_pin_x", + "map_pin_x_inside", "map_pinned", "martini", "maximize", @@ -963,6 +1094,7 @@ LUCIDE_ICON_LIST = [ "message_square_diff", "message_square_dot", "message_square_heart", + "message_square_lock", "message_square_more", "message_square_off", "message_square_plus", @@ -974,8 +1106,9 @@ LUCIDE_ICON_LIST = [ "message_square_x", "messages_square", "mic", - "mic_vocal", "mic_off", + "mic_vocal", + "microchip", "microscope", "microwave", "milestone", @@ -986,6 +1119,7 @@ LUCIDE_ICON_LIST = [ "minus", "monitor", "monitor_check", + "monitor_cog", "monitor_dot", "monitor_down", "monitor_off", @@ -1001,8 +1135,10 @@ LUCIDE_ICON_LIST = [ "mountain", "mountain_snow", "mouse", + "mouse_off", "mouse_pointer", "mouse_pointer_2", + "mouse_pointer_ban", "mouse_pointer_click", "move", "move_3d", @@ -1039,10 +1175,13 @@ LUCIDE_ICON_LIST = [ "nut_off", "octagon", "octagon_alert", + "octagon_minus", "octagon_pause", "octagon_x", + "omega", "option", "orbit", + "origami", "package", "package_2", "package_check", @@ -1055,6 +1194,7 @@ LUCIDE_ICON_LIST = [ "paint_roller", "paintbrush", "paintbrush_2", + "paintbrush_vertical", "palette", "panel_bottom", "panel_bottom_close", @@ -1084,13 +1224,16 @@ LUCIDE_ICON_LIST = [ "pc_case", "pen", "pen_line", + "pen_off", "pen_tool", "pencil", "pencil_line", + "pencil_off", "pencil_ruler", "pentagon", "percent", "person_standing", + "philippine_peso", "phone", "phone_call", "phone_forwarded", @@ -1106,7 +1249,10 @@ LUCIDE_ICON_LIST = [ "pie_chart", "piggy_bank", "pilcrow", + "pilcrow_left", + "pilcrow_right", "pill", + "pill_bottle", "pin", "pin_off", "pipette", @@ -1132,6 +1278,7 @@ LUCIDE_ICON_LIST = [ "power_off", "presentation", "printer", + "printer_check", "projector", "proportions", "puzzle", @@ -1206,6 +1353,7 @@ LUCIDE_ICON_LIST = [ "satellite_dish", "save", "save_all", + "save_off", "scale", "scale_3d", "scaling", @@ -1213,7 +1361,9 @@ LUCIDE_ICON_LIST = [ "scan_barcode", "scan_eye", "scan_face", + "scan_heart", "scan_line", + "scan_qr_code", "scan_search", "scan_text", "scatter_chart", @@ -1229,6 +1379,7 @@ LUCIDE_ICON_LIST = [ "search_code", "search_slash", "search_x", + "section", "send", "send_horizontal", "send_to_back", @@ -1273,6 +1424,7 @@ LUCIDE_ICON_LIST = [ "signal_low", "signal_medium", "signal_zero", + "signature", "signpost", "signpost_big", "siren", @@ -1282,8 +1434,8 @@ LUCIDE_ICON_LIST = [ "slack", "slash", "slice", - "sliders_vertical", "sliders_horizontal", + "sliders_vertical", "smartphone", "smartphone_charging", "smartphone_nfc", @@ -1307,29 +1459,31 @@ LUCIDE_ICON_LIST = [ "sprout", "square", "square_activity", + "square_arrow_down", "square_arrow_down_left", "square_arrow_down_right", - "square_arrow_down", "square_arrow_left", "square_arrow_out_down_left", "square_arrow_out_down_right", "square_arrow_out_up_left", "square_arrow_out_up_right", "square_arrow_right", + "square_arrow_up", "square_arrow_up_left", "square_arrow_up_right", - "square_arrow_up", "square_asterisk", "square_bottom_dashed_scissors", - "square_check_big", + "square_chart_gantt", "square_check", + "square_check_big", "square_chevron_down", "square_chevron_left", "square_chevron_right", "square_chevron_up", "square_code", - "square_dashed_bottom_code", + "square_dashed", "square_dashed_bottom", + "square_dashed_bottom_code", "square_dashed_kanban", "square_dashed_mouse_pointer", "square_divide", @@ -1343,8 +1497,8 @@ LUCIDE_ICON_LIST = [ "square_menu", "square_minus", "square_mouse_pointer", - "square_parking_off", "square_parking", + "square_parking_off", "square_pen", "square_percent", "square_pi", @@ -1358,10 +1512,11 @@ LUCIDE_ICON_LIST = [ "square_slash", "square_split_horizontal", "square_split_vertical", + "square_square", "square_stack", "square_terminal", - "square_user_round", "square_user", + "square_user_round", "square_x", "squircle", "squirrel", @@ -1398,6 +1553,7 @@ LUCIDE_ICON_LIST = [ "table_cells_merge", "table_cells_split", "table_columns_split", + "table_of_contents", "table_properties", "table_rows_split", "tablet", @@ -1413,11 +1569,11 @@ LUCIDE_ICON_LIST = [ "tangent", "target", "telescope", + "tent", "tent_tree", "terminal", - "test_tube_diagonal", "test_tube", - "tent", + "test_tube_diagonal", "test_tubes", "text", "text_cursor", @@ -1438,11 +1594,14 @@ LUCIDE_ICON_LIST = [ "ticket_plus", "ticket_slash", "ticket_x", + "tickets", + "tickets_plane", "timer", "timer_off", "timer_reset", "toggle_left", "toggle_right", + "toilet", "tornado", "torus", "touchpad", @@ -1464,17 +1623,22 @@ LUCIDE_ICON_LIST = [ "trello", "trending_down", "trending_up", + "trending_up_down", "triangle", - "triangle_right", "triangle_alert", + "triangle_dashed", + "triangle_right", "trophy", "truck", "turtle", "tv", "tv_2", + "tv_minimal", + "tv_minimal_play", "twitch", "twitter", "type", + "type_outline", "umbrella", "umbrella_off", "underline", @@ -1485,8 +1649,8 @@ LUCIDE_ICON_LIST = [ "unfold_vertical", "ungroup", "university", - "unlink_2", "unlink", + "unlink_2", "unplug", "upload", "usb", @@ -1494,11 +1658,13 @@ LUCIDE_ICON_LIST = [ "user_check", "user_cog", "user_minus", + "user_pen", "user_plus", "user_round", "user_round_check", "user_round_cog", "user_round_minus", + "user_round_pen", "user_round_plus", "user_round_search", "user_round_x", @@ -1520,14 +1686,16 @@ LUCIDE_ICON_LIST = [ "videotape", "view", "voicemail", + "volleyball", "volume", "volume_1", "volume_2", + "volume_off", "volume_x", "vote", "wallet", - "wallet_minimal", "wallet_cards", + "wallet_minimal", "wallpaper", "wand", "wand_sparkles", @@ -1535,17 +1703,22 @@ LUCIDE_ICON_LIST = [ "washing_machine", "watch", "waves", + "waves_ladder", "waypoints", "webcam", - "webhook_off", "webhook", + "webhook_off", "weight", "wheat", "wheat_off", "whole_word", "wifi", + "wifi_high", + "wifi_low", "wifi_off", + "wifi_zero", "wind", + "wind_arrow_down", "wine", "wine_off", "workflow", @@ -1559,3 +1732,7 @@ LUCIDE_ICON_LIST = [ "zoom_in", "zoom_out", ] +LUCIDE_ICON_MAPPING_OVERRIDE = { + "grid_2x_2_check": "Grid2x2Check", + "grid_2x_2_x": "Grid2x2X", +} diff --git a/reflex/components/markdown/markdown.py b/reflex/components/markdown/markdown.py index cd82d5903..27bd5bd62 100644 --- a/reflex/components/markdown/markdown.py +++ b/reflex/components/markdown/markdown.py @@ -8,7 +8,7 @@ from functools import lru_cache from hashlib import md5 from typing import Any, Callable, Dict, Sequence, Union -from reflex.components.component import Component, CustomComponent +from reflex.components.component import BaseComponent, Component, CustomComponent from reflex.components.tags.tag import Tag from reflex.utils import types from reflex.utils.imports import ImportDict, ImportVar @@ -65,8 +65,8 @@ def get_base_component_map() -> dict[str, Callable]: "h5": lambda value: Heading.create(value, as_="h5", size="2", margin_y="0.5em"), "h6": lambda value: Heading.create(value, as_="h6", size="1", margin_y="0.5em"), "p": lambda value: Text.create(value, margin_y="1em"), - "ul": lambda value: UnorderedList.create(value, margin_y="1em"), # type: ignore - "ol": lambda value: OrderedList.create(value, margin_y="1em"), # type: ignore + "ul": lambda value: UnorderedList.create(value, margin_y="1em"), + "ol": lambda value: OrderedList.create(value, margin_y="1em"), "li": lambda value: ListItem.create(value, margin_y="0.5em"), "a": lambda value: Link.create(value), "code": lambda value: Code.create(value), @@ -236,7 +236,7 @@ class Markdown(Component): ), }, *[ - component(_MOCK_ARG)._get_all_imports() # type: ignore + component(_MOCK_ARG)._get_all_imports() for component in self.component_map.values() ], ] @@ -327,7 +327,7 @@ const {_LANGUAGE!s} = match ? match[1] : ''; if tag != "codeblock" # For codeblock, the mapping for some cases returns an array of elements. Let's join them into a string. else ternary_operation( - ARRAY_ISARRAY.call(_CHILDREN), # type: ignore + ARRAY_ISARRAY.call(_CHILDREN), # pyright: ignore [reportArgumentType] _CHILDREN.to(list).join("\n"), _CHILDREN, ).to(str) @@ -379,7 +379,9 @@ const {_LANGUAGE!s} = match ? match[1] : ''; # fallback to the default fn Var creation if the component is not a MarkdownComponentMap. return MarkdownComponentMap.create_map_fn_var(fn_body=formatted_component) - def _get_map_fn_custom_code_from_children(self, component) -> list[str]: + def _get_map_fn_custom_code_from_children( + self, component: BaseComponent + ) -> list[str]: """Recursively get markdown custom code from children components. Args: @@ -409,7 +411,7 @@ const {_LANGUAGE!s} = match ? match[1] : ''; return custom_code_list @staticmethod - def _component_map_hash(component_map) -> str: + def _component_map_hash(component_map: dict) -> str: inp = str( {tag: component(_MOCK_ARG) for tag, component in component_map.items()} ).encode() @@ -420,11 +422,12 @@ const {_LANGUAGE!s} = match ? match[1] : ''; def _get_custom_code(self) -> str | None: hooks = {} + from reflex.compiler.templates import MACROS + for _component in self.component_map.values(): comp = _component(_MOCK_ARG) - hooks.update(comp._get_all_hooks_internal()) hooks.update(comp._get_all_hooks()) - formatted_hooks = "\n".join(hooks.keys()) + formatted_hooks = MACROS.module.renderHooks(hooks) # pyright: ignore [reportAttributeAccessIssue] return f""" function {self._get_component_map_name()} () {{ {formatted_hooks} diff --git a/reflex/components/moment/moment.py b/reflex/components/moment/moment.py index 80940d228..a5fe79f07 100644 --- a/reflex/components/moment/moment.py +++ b/reflex/components/moment/moment.py @@ -28,9 +28,9 @@ class MomentDelta: class Moment(NoSSRComponent): """The Moment component.""" - tag: str = "Moment" + tag: str | None = "Moment" is_default = True - library: str = "react-moment" + library: str | None = "react-moment" lib_dependencies: List[str] = ["moment"] # How often the date update (how often time update / 0 to disable). diff --git a/reflex/components/next/image.py b/reflex/components/next/image.py index 237c308ce..20ba5a304 100644 --- a/reflex/components/next/image.py +++ b/reflex/components/next/image.py @@ -1,13 +1,17 @@ """Image component from next/image.""" +from __future__ import annotations + from typing import Any, Literal, Optional, Union from reflex.event import EventHandler, no_args_event_spec -from reflex.utils import types +from reflex.utils import console, types from reflex.vars.base import Var from .base import NextComponent +DEFAULT_W_H = "100%" + class Image(NextComponent): """Display an image.""" @@ -47,13 +51,13 @@ class Image(NextComponent): placeholder: Var[str] # Allows passing CSS styles to the underlying image element. - # style: Var[Any] + # style: Var[Any] #noqa: ERA001 # The loading behavior of the image. Defaults to lazy. Can hurt performance, recommended to use `priority` instead. loading: Var[Literal["lazy", "eager"]] # A Data URL to be used as a placeholder image before the src image successfully loads. Only takes effect when combined with placeholder="blur". - blurDataURL: Var[str] + blur_data_url: Var[str] # Fires when the image has loaded. on_load: EventHandler[no_args_event_spec] @@ -80,10 +84,18 @@ class Image(NextComponent): Returns: _type_: _description_ """ - style = props.get("style", {}) - DEFAULT_W_H = "100%" + if "blurDataURL" in props: + console.deprecate( + feature_name="blurDataURL", + reason="Use blur_data_url instead", + deprecation_version="0.7.0", + removal_version="0.8.0", + ) + props["blur_data_url"] = props.pop("blurDataURL") - def check_prop_type(prop_name, prop_value): + style = props.get("style", {}) + + def check_prop_type(prop_name: str, prop_value: int | str | None): if types.check_prop_in_allowed_types(prop_value, allowed_types=[int]): props[prop_name] = prop_value diff --git a/reflex/components/next/image.pyi b/reflex/components/next/image.pyi index 0c1bf01f2..b8da4973d 100644 --- a/reflex/components/next/image.pyi +++ b/reflex/components/next/image.pyi @@ -11,6 +11,8 @@ from reflex.vars.base import Var from .base import NextComponent +DEFAULT_W_H = "100%" + class Image(NextComponent): @overload @classmethod @@ -30,7 +32,7 @@ class Image(NextComponent): loading: Optional[ Union[Literal["eager", "lazy"], Var[Literal["eager", "lazy"]]] ] = None, - blurDataURL: Optional[Union[Var[str], str]] = None, + blur_data_url: Optional[Union[Var[str], str]] = None, style: Optional[Style] = None, key: Optional[Any] = None, id: Optional[Any] = None, @@ -70,8 +72,8 @@ class Image(NextComponent): quality: The quality of the optimized image, an integer between 1 and 100, where 100 is the best quality and therefore largest file size. Defaults to 75. priority: When true, the image will be considered high priority and preload. Lazy loading is automatically disabled for images using priority. placeholder: A placeholder to use while the image is loading. Possible values are blur, empty, or data:image/.... Defaults to empty. - loading: Allows passing CSS styles to the underlying image element. style: Var[Any] The loading behavior of the image. Defaults to lazy. Can hurt performance, recommended to use `priority` instead. - blurDataURL: A Data URL to be used as a placeholder image before the src image successfully loads. Only takes effect when combined with placeholder="blur". + loading: The loading behavior of the image. Defaults to lazy. Can hurt performance, recommended to use `priority` instead. + blur_data_url: A Data URL to be used as a placeholder image before the src image successfully loads. Only takes effect when combined with placeholder="blur". on_load: Fires when the image has loaded. on_error: Fires when the image has an error. style: The style of the component. diff --git a/reflex/components/next/link.py b/reflex/components/next/link.py index 0f7c81296..187618a09 100644 --- a/reflex/components/next/link.py +++ b/reflex/components/next/link.py @@ -17,4 +17,4 @@ class NextLink(Component): href: Var[str] # Whether to pass the href prop to the child. - pass_href: Var[bool] = True # type: ignore + pass_href: Var[bool] = Var.create(True) diff --git a/reflex/components/plotly/plotly.py b/reflex/components/plotly/plotly.py index d69070ed3..c85423d35 100644 --- a/reflex/components/plotly/plotly.py +++ b/reflex/components/plotly/plotly.py @@ -18,8 +18,8 @@ try: Template = layout.Template except ImportError: console.warn("Plotly is not installed. Please run `pip install plotly`.") - Figure = Any # type: ignore - Template = Any # type: ignore + Figure = Any + Template = Any def _event_points_data_signature(e0: Var) -> Tuple[Var[List[Point]]]: @@ -95,20 +95,20 @@ class Plotly(NoSSRComponent): library = "react-plotly.js@2.6.0" - lib_dependencies: List[str] = ["plotly.js@2.35.2"] + lib_dependencies: List[str] = ["plotly.js@2.35.3"] tag = "Plot" is_default = True # The figure to display. This can be a plotly figure or a plotly data json. - data: Var[Figure] # type: ignore + data: Var[Figure] # pyright: ignore [reportInvalidTypeForm] # The layout of the graph. layout: Var[Dict] # The template for visual appearance of the graph. - template: Var[Template] # type: ignore + template: Var[Template] # pyright: ignore [reportInvalidTypeForm] # The config of the graph. config: Var[Dict] @@ -149,10 +149,10 @@ class Plotly(NoSSRComponent): # Fired when a plot element is hovered over. on_hover: EventHandler[_event_points_data_signature] - # Fired after the plot is layed out (zoom, pan, etc). + # Fired after the plot is laid out (zoom, pan, etc). on_relayout: EventHandler[no_args_event_spec] - # Fired while the plot is being layed out. + # Fired while the plot is being laid out. on_relayouting: EventHandler[no_args_event_spec] # Fired after the plot style is changed. @@ -167,7 +167,7 @@ class Plotly(NoSSRComponent): # Fired while dragging a selection. on_selecting: EventHandler[_event_points_data_signature] - # Fired while an animation is occuring. + # Fired while an animation is occurring. on_transitioning: EventHandler[no_args_event_spec] # Fired when a transition is stopped early. diff --git a/reflex/components/plotly/plotly.pyi b/reflex/components/plotly/plotly.pyi index 2d606b8a2..ca1ddef39 100644 --- a/reflex/components/plotly/plotly.pyi +++ b/reflex/components/plotly/plotly.pyi @@ -130,13 +130,13 @@ class Plotly(NoSSRComponent): on_deselect: Fired when a selection is cleared (via double click). on_double_click: Fired when the plot is double clicked. on_hover: Fired when a plot element is hovered over. - on_relayout: Fired after the plot is layed out (zoom, pan, etc). - on_relayouting: Fired while the plot is being layed out. + on_relayout: Fired after the plot is laid out (zoom, pan, etc). + on_relayouting: Fired while the plot is being laid out. on_restyle: Fired after the plot style is changed. on_redraw: Fired after the plot is redrawn. on_selected: Fired after selecting plot elements. on_selecting: Fired while dragging a selection. - on_transitioning: Fired while an animation is occuring. + on_transitioning: Fired while an animation is occurring. on_transition_interrupted: Fired when a transition is stopped early. on_unhover: Fired when a hovered element is no longer hovered. style: The style of the component. diff --git a/reflex/components/props.py b/reflex/components/props.py index adce134fc..779e714d9 100644 --- a/reflex/components/props.py +++ b/reflex/components/props.py @@ -48,7 +48,7 @@ class PropsBase(Base): class NoExtrasAllowedProps(Base): """A class that holds props to be passed or applied to a component with no extra props allowed.""" - def __init__(self, component_name=None, **kwargs): + def __init__(self, component_name: str | None = None, **kwargs): """Initialize the props. Args: @@ -62,13 +62,13 @@ class NoExtrasAllowedProps(Base): try: super().__init__(**kwargs) except ValidationError as e: - invalid_fields = ", ".join([error["loc"][0] for error in e.errors()]) # type: ignore + invalid_fields = ", ".join([error["loc"][0] for error in e.errors()]) # pyright: ignore [reportCallIssue, reportArgumentType] supported_props_str = ", ".join(f'"{field}"' for field in self.get_fields()) raise InvalidPropValueError( f"Invalid prop(s) {invalid_fields} for {component_name!r}. Supported props are {supported_props_str}" ) from None - class Config: + class Config: # pyright: ignore [reportIncompatibleVariableOverride] """Pydantic config.""" arbitrary_types_allowed = True diff --git a/reflex/components/radix/__init__.pyi b/reflex/components/radix/__init__.pyi index f4e81666a..9a16627b5 100644 --- a/reflex/components/radix/__init__.pyi +++ b/reflex/components/radix/__init__.pyi @@ -55,7 +55,7 @@ from .themes.layout.container import container as container from .themes.layout.flex import flex as flex from .themes.layout.grid import grid as grid from .themes.layout.list import list_item as list_item -from .themes.layout.list import list_ns as list # noqa +from .themes.layout.list import list_ns as list # noqa: F401 from .themes.layout.list import ordered_list as ordered_list from .themes.layout.list import unordered_list as unordered_list from .themes.layout.section import section as section diff --git a/reflex/components/radix/primitives/accordion.py b/reflex/components/radix/primitives/accordion.py index 0ba618e21..90a1c41f0 100644 --- a/reflex/components/radix/primitives/accordion.py +++ b/reflex/components/radix/primitives/accordion.py @@ -10,6 +10,7 @@ from reflex.components.core.cond import cond from reflex.components.lucide.icon import Icon from reflex.components.radix.primitives.base import RadixPrimitiveComponent from reflex.components.radix.themes.base import LiteralAccentColor, LiteralRadius +from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler from reflex.style import Style from reflex.vars import get_uuid_string_var @@ -196,8 +197,9 @@ class AccordionItem(AccordionComponent): # The header of the accordion item. header: Var[Union[Component, str]] + # The content of the accordion item. - content: Var[Union[Component, str]] = Var.create(None) + content: Var[Union[Component, str, None]] = Var.create(None) _valid_children: List[str] = [ "AccordionHeader", @@ -341,6 +343,8 @@ class AccordionTrigger(AccordionComponent): alias = "RadixAccordionTrigger" + _memoization_mode = MemoizationMode(recursive=False) + @classmethod def create(cls, *children, **props) -> Component: """Create the Accordion trigger component. @@ -485,11 +489,11 @@ to { Returns: The style of the component. """ - slideDown = LiteralVar.create( + slide_down = LiteralVar.create( "${slideDown} var(--animation-duration) var(--animation-easing)", ) - slideUp = LiteralVar.create( + slide_up = LiteralVar.create( "${slideUp} var(--animation-duration) var(--animation-easing)", ) @@ -503,8 +507,8 @@ to { "display": "block", "height": "var(--space-3)", }, - "&[data-state='open']": {"animation": slideDown}, - "&[data-state='closed']": {"animation": slideUp}, + "&[data-state='open']": {"animation": slide_down}, + "&[data-state='closed']": {"animation": slide_up}, _inherited_variant_selector("classic"): { "color": "var(--accent-contrast)", }, diff --git a/reflex/components/radix/primitives/accordion.pyi b/reflex/components/radix/primitives/accordion.pyi index 03208f496..447451d11 100644 --- a/reflex/components/radix/primitives/accordion.pyi +++ b/reflex/components/radix/primitives/accordion.pyi @@ -308,7 +308,9 @@ class AccordionItem(AccordionComponent): value: Optional[Union[Var[str], str]] = None, disabled: Optional[Union[Var[bool], bool]] = None, header: Optional[Union[Component, Var[Union[Component, str]], str]] = None, - content: Optional[Union[Component, Var[Union[Component, str]], str]] = None, + content: Optional[ + Union[Component, Var[Optional[Union[Component, str]]], str] + ] = None, color_scheme: Optional[ Union[ Literal[ diff --git a/reflex/components/radix/primitives/drawer.py b/reflex/components/radix/primitives/drawer.py index ed57dcbd8..30d1a6ae3 100644 --- a/reflex/components/radix/primitives/drawer.py +++ b/reflex/components/radix/primitives/drawer.py @@ -10,6 +10,7 @@ from reflex.components.component import Component, ComponentNamespace from reflex.components.radix.primitives.base import RadixPrimitiveComponent from reflex.components.radix.themes.base import Theme from reflex.components.radix.themes.layout.flex import Flex +from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var @@ -66,7 +67,7 @@ class DrawerRoot(DrawerComponent): scroll_lock_timeout: Var[int] # When `True`, it prevents scroll restoration. Defaults to `True`. - preventScrollRestoration: Var[bool] + prevent_scroll_restoration: Var[bool] # Enable background scaling, it requires container element with `vaul-drawer-wrapper` attribute to scale its background. should_scale_background: Var[bool] @@ -83,7 +84,9 @@ class DrawerTrigger(DrawerComponent): alias = "Vaul" + tag # Defaults to true, if the first child acts as the trigger. - as_child: Var[bool] = True # type: ignore + as_child: Var[bool] = Var.create(True) + + _memoization_mode = MemoizationMode(recursive=False) @classmethod def create(cls, *children: Any, **props: Any) -> Component: diff --git a/reflex/components/radix/primitives/drawer.pyi b/reflex/components/radix/primitives/drawer.pyi index 1ca1e4325..bb2890fea 100644 --- a/reflex/components/radix/primitives/drawer.pyi +++ b/reflex/components/radix/primitives/drawer.pyi @@ -81,7 +81,7 @@ class DrawerRoot(DrawerComponent): snap_points: Optional[List[Union[float, str]]] = None, fade_from_index: Optional[Union[Var[int], int]] = None, scroll_lock_timeout: Optional[Union[Var[int], int]] = None, - preventScrollRestoration: Optional[Union[Var[bool], bool]] = None, + prevent_scroll_restoration: Optional[Union[Var[bool], bool]] = None, should_scale_background: Optional[Union[Var[bool], bool]] = None, close_threshold: Optional[Union[Var[float], float]] = None, as_child: Optional[Union[Var[bool], bool]] = None, @@ -129,7 +129,7 @@ class DrawerRoot(DrawerComponent): snap_points: Array of numbers from 0 to 100 that corresponds to % of the screen a given snap point should take up. Should go from least visible. Also Accept px values, which doesn't take screen height into account. fade_from_index: Index of a snapPoint from which the overlay fade should be applied. Defaults to the last snap point. scroll_lock_timeout: Duration for which the drawer is not draggable after scrolling content inside of the drawer. Defaults to 500ms - preventScrollRestoration: When `True`, it prevents scroll restoration. Defaults to `True`. + prevent_scroll_restoration: When `True`, it prevents scroll restoration. Defaults to `True`. should_scale_background: Enable background scaling, it requires container element with `vaul-drawer-wrapper` attribute to scale its background. close_threshold: Number between 0 and 1 that determines when the drawer should be closed. as_child: Change the default rendered element for the one passed as a child. @@ -567,7 +567,7 @@ class Drawer(ComponentNamespace): snap_points: Optional[List[Union[float, str]]] = None, fade_from_index: Optional[Union[Var[int], int]] = None, scroll_lock_timeout: Optional[Union[Var[int], int]] = None, - preventScrollRestoration: Optional[Union[Var[bool], bool]] = None, + prevent_scroll_restoration: Optional[Union[Var[bool], bool]] = None, should_scale_background: Optional[Union[Var[bool], bool]] = None, close_threshold: Optional[Union[Var[float], float]] = None, as_child: Optional[Union[Var[bool], bool]] = None, @@ -615,7 +615,7 @@ class Drawer(ComponentNamespace): snap_points: Array of numbers from 0 to 100 that corresponds to % of the screen a given snap point should take up. Should go from least visible. Also Accept px values, which doesn't take screen height into account. fade_from_index: Index of a snapPoint from which the overlay fade should be applied. Defaults to the last snap point. scroll_lock_timeout: Duration for which the drawer is not draggable after scrolling content inside of the drawer. Defaults to 500ms - preventScrollRestoration: When `True`, it prevents scroll restoration. Defaults to `True`. + prevent_scroll_restoration: When `True`, it prevents scroll restoration. Defaults to `True`. should_scale_background: Enable background scaling, it requires container element with `vaul-drawer-wrapper` attribute to scale its background. close_threshold: Number between 0 and 1 that determines when the drawer should be closed. as_child: Change the default rendered element for the one passed as a child. diff --git a/reflex/components/radix/primitives/form.pyi b/reflex/components/radix/primitives/form.pyi index 77c8be77c..d06b57090 100644 --- a/reflex/components/radix/primitives/form.pyi +++ b/reflex/components/radix/primitives/form.pyi @@ -132,10 +132,10 @@ class FormRoot(FormComponent, HTMLForm): on_submit: Optional[ Union[ Union[ - EventType[[], BASE_STATE], EventType[[Dict[str, Any]], BASE_STATE] + EventType[[], BASE_STATE], EventType[[dict[str, Any]], BASE_STATE] ], Union[ - EventType[[], BASE_STATE], EventType[[Dict[str, str]], BASE_STATE] + EventType[[], BASE_STATE], EventType[[dict[str, str]], BASE_STATE] ], ] ] = None, @@ -160,7 +160,7 @@ class FormRoot(FormComponent, HTMLForm): reset_on_submit: If true, the form will be cleared after submit. handle_submit_unique_name: The name used to make this form's submit handler function unique. on_submit: Fired when the form is submitted - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -608,10 +608,10 @@ class Form(FormRoot): on_submit: Optional[ Union[ Union[ - EventType[[], BASE_STATE], EventType[[Dict[str, Any]], BASE_STATE] + EventType[[], BASE_STATE], EventType[[dict[str, Any]], BASE_STATE] ], Union[ - EventType[[], BASE_STATE], EventType[[Dict[str, str]], BASE_STATE] + EventType[[], BASE_STATE], EventType[[dict[str, str]], BASE_STATE] ], ] ] = None, @@ -636,7 +636,7 @@ class Form(FormRoot): reset_on_submit: If true, the form will be cleared after submit. handle_submit_unique_name: The name used to make this form's submit handler function unique. on_submit: Fired when the form is submitted - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -741,10 +741,10 @@ class FormNamespace(ComponentNamespace): on_submit: Optional[ Union[ Union[ - EventType[[], BASE_STATE], EventType[[Dict[str, Any]], BASE_STATE] + EventType[[], BASE_STATE], EventType[[dict[str, Any]], BASE_STATE] ], Union[ - EventType[[], BASE_STATE], EventType[[Dict[str, str]], BASE_STATE] + EventType[[], BASE_STATE], EventType[[dict[str, str]], BASE_STATE] ], ] ] = None, @@ -769,7 +769,7 @@ class FormNamespace(ComponentNamespace): reset_on_submit: If true, the form will be cleared after submit. handle_submit_unique_name: The name used to make this form's submit handler function unique. on_submit: Fired when the form is submitted - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/primitives/progress.py b/reflex/components/radix/primitives/progress.py index 72aee1038..5fcc52f1b 100644 --- a/reflex/components/radix/primitives/progress.py +++ b/reflex/components/radix/primitives/progress.py @@ -83,7 +83,7 @@ class ProgressIndicator(ProgressComponent): "&[data_state='loading']": { "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms linear", }, - "transform": f"translateX(calc(-100% + ({self.value} / {self.max} * 100%)))", # type: ignore + "transform": f"translateX(calc(-100% + ({self.value} / {self.max} * 100%)))", "boxShadow": "inset 0 0 0 1px var(--gray-a5)", } diff --git a/reflex/components/radix/primitives/slider.py b/reflex/components/radix/primitives/slider.py index 10a0079a4..6136e3171 100644 --- a/reflex/components/radix/primitives/slider.py +++ b/reflex/components/radix/primitives/slider.py @@ -30,11 +30,11 @@ def on_value_event_spec( Returns: The event handler spec. """ - return (value,) # type: ignore + return (value,) class SliderRoot(SliderComponent): - """The Slider component comtaining all slider parts.""" + """The Slider component containing all slider parts.""" tag = "Root" alias = "RadixSliderRoot" @@ -188,7 +188,7 @@ class Slider(ComponentNamespace): else: children = [ track, - # Foreach.create(props.get("value"), lambda e: SliderThumb.create()), # foreach doesn't render Thumbs properly + # Foreach.create(props.get("value"), lambda e: SliderThumb.create()), # foreach doesn't render Thumbs properly # noqa: ERA001 ] return SliderRoot.create(*children, **props) diff --git a/reflex/components/radix/themes/base.py b/reflex/components/radix/themes/base.py index 65a9ae835..19e805f7a 100644 --- a/reflex/components/radix/themes/base.py +++ b/reflex/components/radix/themes/base.py @@ -53,7 +53,7 @@ LiteralAccentColor = Literal[ class CommonMarginProps(Component): """Many radix-themes elements accept shorthand margin props.""" - # Margin: "0" - "9" + # Margin: "0" - "9" # noqa: ERA001 m: Var[LiteralSpacing] # Margin horizontal: "0" - "9" @@ -78,7 +78,7 @@ class CommonMarginProps(Component): class CommonPaddingProps(Component): """Many radix-themes elements accept shorthand padding props.""" - # Padding: "0" - "9" + # Padding: "0" - "9" # noqa: ERA001 p: Var[Responsive[LiteralSpacing]] # Padding horizontal: "0" - "9" @@ -139,14 +139,7 @@ class RadixThemesComponent(Component): component = super().create(*children, **props) if component.library is None: component.library = RadixThemesComponent.__fields__["library"].default - component.alias = "RadixThemes" + ( - component.tag or component.__class__.__name__ - ) - # value = props.get("value") - # if value is not None and component.alias == "RadixThemesSelect.Root": - # lv = LiteralVar.create(value) - # print(repr(lv)) - # print(f"Warning: Value {value} is not used in {component.alias}.") + component.alias = "RadixThemes" + (component.tag or type(component).__name__) return component @staticmethod @@ -268,6 +261,7 @@ class Theme(RadixThemesComponent): _js_expr="{...theme.styles.global[':root'], ...theme.styles.global.body}" ), ) + tag.remove_props("appearance") return tag diff --git a/reflex/components/radix/themes/color_mode.py b/reflex/components/radix/themes/color_mode.py index 2dd0f5e83..d9b7c0b02 100644 --- a/reflex/components/radix/themes/color_mode.py +++ b/reflex/components/radix/themes/color_mode.py @@ -17,7 +17,7 @@ rx.text( from __future__ import annotations -from typing import Dict, List, Literal, Optional, Union, get_args +from typing import Any, Dict, List, Literal, Optional, Union, get_args from reflex.components.component import BaseComponent from reflex.components.core.cond import Cond, color_mode_cond, cond @@ -78,17 +78,19 @@ position_map: Dict[str, List[str]] = { # needed to inverse contains for find -def _find(const: List[str], var): +def _find(const: List[str], var: Any): return LiteralArrayVar.create(const).contains(var) -def _set_var_default(props, position, prop, default1, default2=""): +def _set_var_default( + props: dict, position: Any, prop: str, default1: str, default2: str = "" +): props.setdefault( prop, cond(_find(position_map[prop], position), default1, default2) ) -def _set_static_default(props, position, prop, default): +def _set_static_default(props: dict, position: Any, prop: str, default: str): if prop in position: props.setdefault(prop, default) @@ -115,12 +117,12 @@ class ColorModeIconButton(IconButton): Returns: The button component. """ - position = props.pop("position", None) + position: str | Var = props.pop("position", None) allow_system = props.pop("allow_system", False) # position is used to set nice defaults for positioning the icon button if isinstance(position, Var): - _set_var_default(props, position, "position", "fixed", position) # type: ignore + _set_var_default(props, position, "position", "fixed", position) # pyright: ignore [reportArgumentType] _set_var_default(props, position, "bottom", "2rem") _set_var_default(props, position, "top", "2rem") _set_var_default(props, position, "left", "2rem") @@ -142,7 +144,7 @@ class ColorModeIconButton(IconButton): if allow_system: - def color_mode_item(_color_mode): + def color_mode_item(_color_mode: str): return dropdown_menu.item( _color_mode.title(), on_click=set_color_mode(_color_mode) ) @@ -151,8 +153,8 @@ class ColorModeIconButton(IconButton): dropdown_menu.trigger( super().create( ColorModeIcon.create(), - **props, - ) + ), + **props, ), dropdown_menu.content( color_mode_item("light"), diff --git a/reflex/components/radix/themes/color_mode.pyi b/reflex/components/radix/themes/color_mode.pyi index eb3c4234a..3a9347017 100644 --- a/reflex/components/radix/themes/color_mode.pyi +++ b/reflex/components/radix/themes/color_mode.pyi @@ -257,7 +257,7 @@ class ColorModeIconButton(IconButton): name: Name of the button, used when sending form data type: Type of the button (submit, reset, or button) value: Value of the button, used when sending form data - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -427,7 +427,7 @@ class ColorModeSwitch(Switch): color_scheme: Override theme color for switch high_contrast: Whether to render the switch with higher contrast color against background radius: Override theme radius for switch: "none" | "small" | "full" - on_change: Props to rename Fired when the value of the switch changes + on_change: Fired when the value of the switch changes style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/alert_dialog.py b/reflex/components/radix/themes/components/alert_dialog.py index 36d38532c..bc5e2dc7e 100644 --- a/reflex/components/radix/themes/components/alert_dialog.py +++ b/reflex/components/radix/themes/components/alert_dialog.py @@ -5,6 +5,7 @@ from typing import Literal from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive from reflex.components.el import elements +from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var @@ -33,6 +34,8 @@ class AlertDialogTrigger(RadixThemesTriggerComponent): tag = "AlertDialog.Trigger" + _memoization_mode = MemoizationMode(recursive=False) + class AlertDialogContent(elements.Div, RadixThemesComponent): """Contains the content of the dialog. This component is based on the div element.""" diff --git a/reflex/components/radix/themes/components/alert_dialog.pyi b/reflex/components/radix/themes/components/alert_dialog.pyi index ad243df34..6188fdd45 100644 --- a/reflex/components/radix/themes/components/alert_dialog.pyi +++ b/reflex/components/radix/themes/components/alert_dialog.pyi @@ -194,7 +194,7 @@ class AlertDialogContent(elements.Div, RadixThemesComponent): on_open_auto_focus: Fired when the dialog is opened. on_close_auto_focus: Fired when the dialog is closed. on_escape_key_down: Fired when the escape key is pressed. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/components/badge.pyi b/reflex/components/radix/themes/components/badge.pyi index 3e4f19f6e..38f20efeb 100644 --- a/reflex/components/radix/themes/components/badge.pyi +++ b/reflex/components/radix/themes/components/badge.pyi @@ -164,7 +164,7 @@ class Badge(elements.Span, RadixThemesComponent): color_scheme: Color theme of the badge high_contrast: Whether to render the badge with higher contrast color against background radius: Override theme radius for badge: "none" | "small" | "medium" | "large" | "full" - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/components/button.pyi b/reflex/components/radix/themes/components/button.pyi index ea2b63fbe..cee24abc4 100644 --- a/reflex/components/radix/themes/components/button.pyi +++ b/reflex/components/radix/themes/components/button.pyi @@ -196,7 +196,7 @@ class Button(elements.Button, RadixLoadingProp, RadixThemesComponent): name: Name of the button, used when sending form data type: Type of the button (submit, reset, or button) value: Value of the button, used when sending form data - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/components/callout.pyi b/reflex/components/radix/themes/components/callout.pyi index b2643b558..2c469956f 100644 --- a/reflex/components/radix/themes/components/callout.pyi +++ b/reflex/components/radix/themes/components/callout.pyi @@ -162,7 +162,7 @@ class CalloutRoot(elements.Div, RadixThemesComponent): variant: Variant of button: "soft" | "surface" | "outline" color_scheme: Override theme color for button high_contrast: Whether to render the button with higher contrast color against background - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -251,7 +251,7 @@ class CalloutIcon(elements.Div, RadixThemesComponent): Args: *children: Child components. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -340,7 +340,7 @@ class CalloutText(elements.P, RadixThemesComponent): Args: *children: Child components. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -516,7 +516,7 @@ class Callout(CalloutRoot): variant: Variant of button: "soft" | "surface" | "outline" color_scheme: Override theme color for button high_contrast: Whether to render the button with higher contrast color against background - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -694,7 +694,7 @@ class CalloutNamespace(ComponentNamespace): variant: Variant of button: "soft" | "surface" | "outline" color_scheme: Override theme color for button high_contrast: Whether to render the button with higher contrast color against background - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/components/card.py b/reflex/components/radix/themes/components/card.py index 30823de56..e99ea9cef 100644 --- a/reflex/components/radix/themes/components/card.py +++ b/reflex/components/radix/themes/components/card.py @@ -20,7 +20,7 @@ class Card(elements.Div, RadixThemesComponent): # Card size: "1" - "5" size: Var[Responsive[Literal["1", "2", "3", "4", "5"],]] - # Variant of Card: "solid" | "soft" | "outline" | "ghost" + # Variant of Card: "surface" | "classic" | "ghost" variant: Var[Literal["surface", "classic", "ghost"]] diff --git a/reflex/components/radix/themes/components/card.pyi b/reflex/components/radix/themes/components/card.pyi index 74b91f8ab..e515982e4 100644 --- a/reflex/components/radix/themes/components/card.pyi +++ b/reflex/components/radix/themes/components/card.pyi @@ -94,8 +94,8 @@ class Card(elements.Div, RadixThemesComponent): *children: Child components. as_child: Change the default rendered element for the one passed as a child, merging their props and behavior. size: Card size: "1" - "5" - variant: Variant of Card: "solid" | "soft" | "outline" | "ghost" - access_key: Provides a hint for generating a keyboard shortcut for the current element. + variant: Variant of Card: "surface" | "classic" | "ghost" + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/components/checkbox.pyi b/reflex/components/radix/themes/components/checkbox.pyi index a8ac3a0b6..fb6b51434 100644 --- a/reflex/components/radix/themes/components/checkbox.pyi +++ b/reflex/components/radix/themes/components/checkbox.pyi @@ -153,7 +153,7 @@ class Checkbox(RadixThemesComponent): required: Whether the checkbox is required name: The name of the checkbox control when submitting the form. value: The value of the checkbox control when submitting the form. - on_change: Props to rename Fired when the checkbox is checked or unchecked. + on_change: Fired when the checkbox is checked or unchecked. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -302,7 +302,7 @@ class HighLevelCheckbox(RadixThemesComponent): required: Whether the checkbox is required name: The name of the checkbox control when submitting the form. value: The value of the checkbox control when submitting the form. - on_change: Props to rename Fired when the checkbox is checked or unchecked. + on_change: Fired when the checkbox is checked or unchecked. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -449,7 +449,7 @@ class CheckboxNamespace(ComponentNamespace): required: Whether the checkbox is required name: The name of the checkbox control when submitting the form. value: The value of the checkbox control when submitting the form. - on_change: Props to rename Fired when the checkbox is checked or unchecked. + on_change: Fired when the checkbox is checked or unchecked. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/context_menu.py b/reflex/components/radix/themes/components/context_menu.py index ea4902233..60d23db1a 100644 --- a/reflex/components/radix/themes/components/context_menu.py +++ b/reflex/components/radix/themes/components/context_menu.py @@ -4,10 +4,12 @@ from typing import Dict, List, Literal, Union from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive +from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var from ..base import LiteralAccentColor, RadixThemesComponent +from .checkbox import Checkbox LiteralDirType = Literal["ltr", "rtl"] @@ -54,6 +56,8 @@ class ContextMenuTrigger(RadixThemesComponent): _invalid_children: List[str] = ["ContextMenuContent"] + _memoization_mode = MemoizationMode(recursive=False) + class ContextMenuContent(RadixThemesComponent): """The component that pops out when the context menu is open.""" @@ -152,6 +156,8 @@ class ContextMenuSubTrigger(RadixThemesComponent): _valid_parents: List[str] = ["ContextMenuContent", "ContextMenuSub"] + _memoization_mode = MemoizationMode(recursive=False) + class ContextMenuSubContent(RadixThemesComponent): """The component that pops out when a submenu is open.""" @@ -232,6 +238,15 @@ class ContextMenuSeparator(RadixThemesComponent): tag = "ContextMenu.Separator" +class ContextMenuCheckbox(Checkbox): + """The component that contains the checkbox.""" + + tag = "ContextMenu.CheckboxItem" + + # Text to render as shortcut. + shortcut: Var[str] + + class ContextMenu(ComponentNamespace): """Menu representing a set of actions, displayed at the origin of a pointer right-click or long-press.""" @@ -243,6 +258,7 @@ class ContextMenu(ComponentNamespace): sub_content = staticmethod(ContextMenuSubContent.create) item = staticmethod(ContextMenuItem.create) separator = staticmethod(ContextMenuSeparator.create) + checkbox = staticmethod(ContextMenuCheckbox.create) context_menu = ContextMenu() diff --git a/reflex/components/radix/themes/components/context_menu.pyi b/reflex/components/radix/themes/components/context_menu.pyi index c5ef757d1..2d3ffbebc 100644 --- a/reflex/components/radix/themes/components/context_menu.pyi +++ b/reflex/components/radix/themes/components/context_menu.pyi @@ -12,6 +12,7 @@ from reflex.style import Style from reflex.vars.base import Var from ..base import RadixThemesComponent +from .checkbox import Checkbox LiteralDirType = Literal["ltr", "rtl"] LiteralSizeType = Literal["1", "2"] @@ -672,6 +673,159 @@ class ContextMenuSeparator(RadixThemesComponent): """ ... +class ContextMenuCheckbox(Checkbox): + @overload + @classmethod + def create( # type: ignore + cls, + *children, + shortcut: Optional[Union[Var[str], str]] = None, + as_child: Optional[Union[Var[bool], bool]] = None, + size: Optional[ + Union[ + Breakpoints[str, Literal["1", "2", "3"]], + Literal["1", "2", "3"], + Var[ + Union[ + Breakpoints[str, Literal["1", "2", "3"]], Literal["1", "2", "3"] + ] + ], + ] + ] = None, + variant: Optional[ + Union[ + Literal["classic", "soft", "surface"], + Var[Literal["classic", "soft", "surface"]], + ] + ] = None, + color_scheme: Optional[ + Union[ + Literal[ + "amber", + "blue", + "bronze", + "brown", + "crimson", + "cyan", + "gold", + "grass", + "gray", + "green", + "indigo", + "iris", + "jade", + "lime", + "mint", + "orange", + "pink", + "plum", + "purple", + "red", + "ruby", + "sky", + "teal", + "tomato", + "violet", + "yellow", + ], + Var[ + Literal[ + "amber", + "blue", + "bronze", + "brown", + "crimson", + "cyan", + "gold", + "grass", + "gray", + "green", + "indigo", + "iris", + "jade", + "lime", + "mint", + "orange", + "pink", + "plum", + "purple", + "red", + "ruby", + "sky", + "teal", + "tomato", + "violet", + "yellow", + ] + ], + ] + ] = None, + high_contrast: Optional[Union[Var[bool], bool]] = None, + default_checked: Optional[Union[Var[bool], bool]] = None, + checked: Optional[Union[Var[bool], bool]] = None, + disabled: Optional[Union[Var[bool], bool]] = None, + required: Optional[Union[Var[bool], bool]] = None, + name: Optional[Union[Var[str], str]] = None, + value: Optional[Union[Var[str], str]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_blur: Optional[EventType[[], BASE_STATE]] = None, + on_change: Optional[ + Union[EventType[[], BASE_STATE], EventType[[bool], BASE_STATE]] + ] = None, + on_click: Optional[EventType[[], BASE_STATE]] = None, + on_context_menu: Optional[EventType[[], BASE_STATE]] = None, + on_double_click: Optional[EventType[[], BASE_STATE]] = None, + on_focus: Optional[EventType[[], BASE_STATE]] = None, + on_mount: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_down: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_enter: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_leave: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_move: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_out: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_over: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_up: Optional[EventType[[], BASE_STATE]] = None, + on_scroll: Optional[EventType[[], BASE_STATE]] = None, + on_unmount: Optional[EventType[[], BASE_STATE]] = None, + **props, + ) -> "ContextMenuCheckbox": + """Create a new component instance. + + Will prepend "RadixThemes" to the component tag to avoid conflicts with + other UI libraries for common names, like Text and Button. + + Args: + *children: Child components. + shortcut: Text to render as shortcut. + as_child: Change the default rendered element for the one passed as a child, merging their props and behavior. + size: Checkbox size "1" - "3" + variant: Variant of checkbox: "classic" | "surface" | "soft" + color_scheme: Override theme color for checkbox + high_contrast: Whether to render the checkbox with higher contrast color against background + default_checked: Whether the checkbox is checked by default + checked: Whether the checkbox is checked + disabled: Whether the checkbox is disabled + required: Whether the checkbox is required + name: The name of the checkbox control when submitting the form. + value: The value of the checkbox control when submitting the form. + on_change: Fired when the checkbox is checked or unchecked. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: Component properties. + + Returns: + A new component instance. + """ + ... + class ContextMenu(ComponentNamespace): root = staticmethod(ContextMenuRoot.create) trigger = staticmethod(ContextMenuTrigger.create) @@ -681,5 +835,6 @@ class ContextMenu(ComponentNamespace): sub_content = staticmethod(ContextMenuSubContent.create) item = staticmethod(ContextMenuItem.create) separator = staticmethod(ContextMenuSeparator.create) + checkbox = staticmethod(ContextMenuCheckbox.create) context_menu = ContextMenu() diff --git a/reflex/components/radix/themes/components/dialog.py b/reflex/components/radix/themes/components/dialog.py index 1b7c3b532..ce6e52cb5 100644 --- a/reflex/components/radix/themes/components/dialog.py +++ b/reflex/components/radix/themes/components/dialog.py @@ -5,6 +5,7 @@ from typing import Literal from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive from reflex.components.el import elements +from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var @@ -31,6 +32,8 @@ class DialogTrigger(RadixThemesTriggerComponent): tag = "Dialog.Trigger" + _memoization_mode = MemoizationMode(recursive=False) + class DialogTitle(RadixThemesComponent): """Title component to display inside a Dialog modal.""" diff --git a/reflex/components/radix/themes/components/dialog.pyi b/reflex/components/radix/themes/components/dialog.pyi index 4c3045741..b1dfc1b54 100644 --- a/reflex/components/radix/themes/components/dialog.pyi +++ b/reflex/components/radix/themes/components/dialog.pyi @@ -243,7 +243,7 @@ class DialogContent(elements.Div, RadixThemesComponent): on_escape_key_down: Fired when the escape key is pressed. on_pointer_down_outside: Fired when the pointer is down outside the dialog. on_interact_outside: Fired when the pointer interacts outside the dialog. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/components/dropdown_menu.py b/reflex/components/radix/themes/components/dropdown_menu.py index abce3e3bb..6d5709e11 100644 --- a/reflex/components/radix/themes/components/dropdown_menu.py +++ b/reflex/components/radix/themes/components/dropdown_menu.py @@ -4,6 +4,7 @@ from typing import Dict, List, Literal, Union from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive +from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var @@ -60,6 +61,8 @@ class DropdownMenuTrigger(RadixThemesTriggerComponent): _invalid_children: List[str] = ["DropdownMenuContent"] + _memoization_mode = MemoizationMode(recursive=False) + class DropdownMenuContent(RadixThemesComponent): """The Dropdown Menu Content component that pops out when the dropdown menu is open.""" @@ -143,6 +146,8 @@ class DropdownMenuSubTrigger(RadixThemesTriggerComponent): _valid_parents: List[str] = ["DropdownMenuContent", "DropdownMenuSub"] + _memoization_mode = MemoizationMode(recursive=False) + class DropdownMenuSub(RadixThemesComponent): """Contains all the parts of a submenu.""" diff --git a/reflex/components/radix/themes/components/hover_card.py b/reflex/components/radix/themes/components/hover_card.py index bd5489ce6..9e7aa4688 100644 --- a/reflex/components/radix/themes/components/hover_card.py +++ b/reflex/components/radix/themes/components/hover_card.py @@ -5,6 +5,7 @@ from typing import Dict, Literal, Union from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive from reflex.components.el import elements +from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import Var @@ -37,6 +38,8 @@ class HoverCardTrigger(RadixThemesTriggerComponent): tag = "HoverCard.Trigger" + _memoization_mode = MemoizationMode(recursive=False) + class HoverCardContent(elements.Div, RadixThemesComponent): """Contains the content of the open hover card.""" diff --git a/reflex/components/radix/themes/components/hover_card.pyi b/reflex/components/radix/themes/components/hover_card.pyi index 4b1de0d89..d43b583c2 100644 --- a/reflex/components/radix/themes/components/hover_card.pyi +++ b/reflex/components/radix/themes/components/hover_card.pyi @@ -228,7 +228,7 @@ class HoverCardContent(elements.Div, RadixThemesComponent): sticky: The sticky behavior on the align axis. "partial" will keep the content in the boundary as long as the trigger is at least partially in the boundary whilst "always" will keep the content in the boundary regardless hide_when_detached: Whether to hide the content when the trigger becomes fully occluded. size: Hovercard size "1" - "3" - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/components/icon_button.py b/reflex/components/radix/themes/components/icon_button.py index 2a32afe3a..aafb9e1eb 100644 --- a/reflex/components/radix/themes/components/icon_button.py +++ b/reflex/components/radix/themes/components/icon_button.py @@ -22,6 +22,8 @@ from ..base import ( LiteralButtonSize = Literal["1", "2", "3", "4"] +RADIX_TO_LUCIDE_SIZE = {"1": 12, "2": 24, "3": 36, "4": 48} + class IconButton(elements.Button, RadixLoadingProp, RadixThemesComponent): """A button designed specifically for usage with a single icon.""" @@ -72,14 +74,12 @@ class IconButton(elements.Button, RadixLoadingProp, RadixThemesComponent): "IconButton requires a child icon. Pass a string as the first child or a rx.icon." ) if "size" in props: - RADIX_TO_LUCIDE_SIZE = {"1": 12, "2": 24, "3": 36, "4": 48} - if isinstance(props["size"], str): children[0].size = RADIX_TO_LUCIDE_SIZE[props["size"]] else: size_map_var = Match.create( props["size"], - *[(size, px) for size, px in RADIX_TO_LUCIDE_SIZE.items()], + *list(RADIX_TO_LUCIDE_SIZE.items()), 12, ) if not isinstance(size_map_var, Var): diff --git a/reflex/components/radix/themes/components/icon_button.pyi b/reflex/components/radix/themes/components/icon_button.pyi index e600a9bee..bdb0fe845 100644 --- a/reflex/components/radix/themes/components/icon_button.pyi +++ b/reflex/components/radix/themes/components/icon_button.pyi @@ -14,6 +14,7 @@ from reflex.vars.base import Var from ..base import RadixLoadingProp, RadixThemesComponent LiteralButtonSize = Literal["1", "2", "3", "4"] +RADIX_TO_LUCIDE_SIZE = {"1": 12, "2": 24, "3": 36, "4": 48} class IconButton(elements.Button, RadixLoadingProp, RadixThemesComponent): @overload @@ -193,7 +194,7 @@ class IconButton(elements.Button, RadixLoadingProp, RadixThemesComponent): name: Name of the button, used when sending form data type: Type of the button (submit, reset, or button) value: Value of the button, used when sending form data - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/components/inset.pyi b/reflex/components/radix/themes/components/inset.pyi index c4f07320b..f03275ec0 100644 --- a/reflex/components/radix/themes/components/inset.pyi +++ b/reflex/components/radix/themes/components/inset.pyi @@ -166,7 +166,7 @@ class Inset(elements.Div, RadixThemesComponent): pr: Padding on the right pb: Padding on the bottom pl: Padding on the left - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/components/popover.py b/reflex/components/radix/themes/components/popover.py index bdf5f4af3..4c0542cb7 100644 --- a/reflex/components/radix/themes/components/popover.py +++ b/reflex/components/radix/themes/components/popover.py @@ -5,6 +5,7 @@ from typing import Dict, Literal, Union from reflex.components.component import ComponentNamespace from reflex.components.core.breakpoints import Responsive from reflex.components.el import elements +from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler, no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var @@ -34,6 +35,8 @@ class PopoverTrigger(RadixThemesTriggerComponent): tag = "Popover.Trigger" + _memoization_mode = MemoizationMode(recursive=False) + class PopoverContent(elements.Div, RadixThemesComponent): """Contains content to be rendered in the open popover.""" diff --git a/reflex/components/radix/themes/components/popover.pyi b/reflex/components/radix/themes/components/popover.pyi index d4b324817..51f114dd2 100644 --- a/reflex/components/radix/themes/components/popover.pyi +++ b/reflex/components/radix/themes/components/popover.pyi @@ -233,7 +233,7 @@ class PopoverContent(elements.Div, RadixThemesComponent): on_pointer_down_outside: Fired when the pointer is down outside the dialog. on_focus_outside: Fired when focus moves outside the dialog. on_interact_outside: Fired when the pointer interacts outside the dialog. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/components/radio_cards.py b/reflex/components/radix/themes/components/radio_cards.py index e075a1ba2..4a9aefc99 100644 --- a/reflex/components/radix/themes/components/radio_cards.py +++ b/reflex/components/radix/themes/components/radio_cards.py @@ -85,6 +85,8 @@ class RadioCardsItem(RadixThemesComponent): # When true, indicates that the user must check the radio item before the owning form can be submitted. required: Var[bool] + _valid_parents: list[str] = ["RadioCardsRoot"] + class RadioCards(SimpleNamespace): """RadioCards components namespace.""" diff --git a/reflex/components/radix/themes/components/radio_group.py b/reflex/components/radix/themes/components/radio_group.py index 80b3ee10c..f34c92159 100644 --- a/reflex/components/radix/themes/components/radio_group.py +++ b/reflex/components/radix/themes/components/radio_group.py @@ -155,7 +155,7 @@ class HighLevelRadioGroup(RadixThemesComponent): if isinstance(default_value, str) or ( isinstance(default_value, Var) and default_value._var_type is str ): - default_value = LiteralVar.create(default_value) # type: ignore + default_value = LiteralVar.create(default_value) else: default_value = LiteralVar.create(default_value).to_string() diff --git a/reflex/components/radix/themes/components/radio_group.pyi b/reflex/components/radix/themes/components/radio_group.pyi index f251f541f..e8e4e4254 100644 --- a/reflex/components/radix/themes/components/radio_group.pyi +++ b/reflex/components/radix/themes/components/radio_group.pyi @@ -148,7 +148,7 @@ class RadioGroupRoot(RadixThemesComponent): disabled: Whether the radio group is disabled name: The name of the group. Submitted with its owning form as part of a name/value pair. required: Whether the radio group is required - on_change: Props to rename Fired when the value of the radio group changes. + on_change: Fired when the value of the radio group changes. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/select.py b/reflex/components/radix/themes/components/select.py index 45e5712bc..6ac992380 100644 --- a/reflex/components/radix/themes/components/select.py +++ b/reflex/components/radix/themes/components/select.py @@ -5,6 +5,7 @@ from typing import List, Literal, Union import reflex as rx from reflex.components.component import Component, ComponentNamespace from reflex.components.core.breakpoints import Responsive +from reflex.constants.compiler import MemoizationMode from reflex.event import no_args_event_spec, passthrough_event_spec from reflex.vars.base import Var @@ -69,6 +70,8 @@ class SelectTrigger(RadixThemesComponent): _valid_parents: List[str] = ["SelectRoot"] + _memoization_mode = MemoizationMode(recursive=False) + class SelectContent(RadixThemesComponent): """The component that pops out when the select is open.""" diff --git a/reflex/components/radix/themes/components/select.pyi b/reflex/components/radix/themes/components/select.pyi index 39caeef9c..a6c1ff144 100644 --- a/reflex/components/radix/themes/components/select.pyi +++ b/reflex/components/radix/themes/components/select.pyi @@ -81,7 +81,7 @@ class SelectRoot(RadixThemesComponent): name: The name of the select control when submitting the form. disabled: When True, prevents the user from interacting with select. required: When True, indicates that the user must select a value before the owning form can be submitted. - on_change: Props to rename Fired when the value of the select changes. + on_change: Fired when the value of the select changes. on_open_change: Fired when the select is opened or closed. style: The style of the component. key: A unique key for the component. @@ -732,7 +732,7 @@ class HighLevelSelect(SelectRoot): name: The name of the select control when submitting the form. disabled: When True, prevents the user from interacting with select. required: When True, indicates that the user must select a value before the owning form can be submitted. - on_change: Props to rename Fired when the value of the select changes. + on_change: Fired when the value of the select changes. on_open_change: Fired when the select is opened or closed. style: The style of the component. key: A unique key for the component. @@ -912,7 +912,7 @@ class Select(ComponentNamespace): name: The name of the select control when submitting the form. disabled: When True, prevents the user from interacting with select. required: When True, indicates that the user must select a value before the owning form can be submitted. - on_change: Props to rename Fired when the value of the select changes. + on_change: Fired when the value of the select changes. on_open_change: Fired when the select is opened or closed. style: The style of the component. key: A unique key for the component. diff --git a/reflex/components/radix/themes/components/slider.pyi b/reflex/components/radix/themes/components/slider.pyi index 972385e52..f2552fbc6 100644 --- a/reflex/components/radix/themes/components/slider.pyi +++ b/reflex/components/radix/themes/components/slider.pyi @@ -195,7 +195,7 @@ class Slider(RadixThemesComponent): step: The step value of the slider. disabled: Whether the slider is disabled orientation: The orientation of the slider. - on_change: Props to rename Fired when the value of the slider changes. + on_change: Fired when the value of the slider changes. on_value_commit: Fired when a thumb is released after being dragged. style: The style of the component. key: A unique key for the component. diff --git a/reflex/components/radix/themes/components/switch.pyi b/reflex/components/radix/themes/components/switch.pyi index 8a858040a..4aabd7da2 100644 --- a/reflex/components/radix/themes/components/switch.pyi +++ b/reflex/components/radix/themes/components/switch.pyi @@ -157,7 +157,7 @@ class Switch(RadixThemesComponent): color_scheme: Override theme color for switch high_contrast: Whether to render the switch with higher contrast color against background radius: Override theme radius for switch: "none" | "small" | "full" - on_change: Props to rename Fired when the value of the switch changes + on_change: Fired when the value of the switch changes style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/table.pyi b/reflex/components/radix/themes/components/table.pyi index 9bc7bf730..99a0ba96d 100644 --- a/reflex/components/radix/themes/components/table.pyi +++ b/reflex/components/radix/themes/components/table.pyi @@ -94,7 +94,7 @@ class TableRoot(elements.Table, RadixThemesComponent): variant: The variant of the table align: Alignment of the table summary: Provides a summary of the table's purpose and structure - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -185,7 +185,7 @@ class TableHeader(elements.Thead, RadixThemesComponent): Args: *children: Child components. align: Alignment of the content within the table header - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -281,7 +281,7 @@ class TableRow(elements.Tr, RadixThemesComponent): Args: *children: Child components. align: Alignment of the content within the table row - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -395,7 +395,7 @@ class TableColumnHeaderCell(elements.Th, RadixThemesComponent): headers: IDs of the headers associated with this header cell row_span: Number of rows a header cell should span scope: Scope of the header cell (row, col, rowgroup, colgroup) - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -486,7 +486,7 @@ class TableBody(elements.Tbody, RadixThemesComponent): Args: *children: Child components. align: Alignment of the content within the table body - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -717,7 +717,7 @@ class TableCell(elements.Td, CommonPaddingProps, RadixThemesComponent): col_span: Number of columns a cell should span headers: IDs of the headers associated with this cell row_span: Number of rows a cell should span - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -957,7 +957,7 @@ class TableRowHeaderCell(elements.Th, CommonPaddingProps, RadixThemesComponent): headers: IDs of the headers associated with this header cell row_span: Number of rows a header cell should span scope: Scope of the header cell (row, col, rowgroup, colgroup) - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/components/tabs.py b/reflex/components/radix/themes/components/tabs.py index adfb32fab..7b5e5f475 100644 --- a/reflex/components/radix/themes/components/tabs.py +++ b/reflex/components/radix/themes/components/tabs.py @@ -7,6 +7,7 @@ from typing import Any, Dict, List, Literal from reflex.components.component import Component, ComponentNamespace from reflex.components.core.breakpoints import Responsive from reflex.components.core.colors import color +from reflex.constants.compiler import MemoizationMode from reflex.event import EventHandler, passthrough_event_spec from reflex.vars.base import Var @@ -95,6 +96,8 @@ class TabsTrigger(RadixThemesComponent): _valid_parents: List[str] = ["TabsList"] + _memoization_mode = MemoizationMode(recursive=False) + @classmethod def create(cls, *children, **props) -> Component: """Create a TabsTrigger component. diff --git a/reflex/components/radix/themes/components/tabs.pyi b/reflex/components/radix/themes/components/tabs.pyi index b5e7de5f4..8830c8e21 100644 --- a/reflex/components/radix/themes/components/tabs.pyi +++ b/reflex/components/radix/themes/components/tabs.pyi @@ -72,7 +72,7 @@ class TabsRoot(RadixThemesComponent): orientation: The orientation of the tabs. dir: Reading direction of the tabs. activation_mode: The mode of activation for the tabs. "automatic" will activate the tab when focused. "manual" will activate the tab when clicked. - on_change: Props to rename Fired when the value of the tabs changes. + on_change: Fired when the value of the tabs changes. style: The style of the component. key: A unique key for the component. id: The id for the component. @@ -374,7 +374,7 @@ class Tabs(ComponentNamespace): orientation: The orientation of the tabs. dir: Reading direction of the tabs. activation_mode: The mode of activation for the tabs. "automatic" will activate the tab when focused. "manual" will activate the tab when clicked. - on_change: Props to rename Fired when the value of the tabs changes. + on_change: Fired when the value of the tabs changes. style: The style of the component. key: A unique key for the component. id: The id for the component. diff --git a/reflex/components/radix/themes/components/text_area.py b/reflex/components/radix/themes/components/text_area.py index 83fa8a593..caf98eb2d 100644 --- a/reflex/components/radix/themes/components/text_area.py +++ b/reflex/components/radix/themes/components/text_area.py @@ -96,5 +96,17 @@ class TextArea(RadixThemesComponent, elements.Textarea): return DebounceInput.create(super().create(*children, **props)) return super().create(*children, **props) + def add_style(self): + """Add the style to the component. + + Returns: + The style of the component. + """ + added_style: dict[str, dict] = {} + added_style.setdefault("& textarea", {}) + if "padding" in self.style: + added_style["& textarea"]["padding"] = self.style.pop("padding") + return added_style + text_area = TextArea.create diff --git a/reflex/components/radix/themes/components/text_area.pyi b/reflex/components/radix/themes/components/text_area.pyi index 63d474842..e1e40c936 100644 --- a/reflex/components/radix/themes/components/text_area.pyi +++ b/reflex/components/radix/themes/components/text_area.pyi @@ -239,7 +239,7 @@ class TextArea(RadixThemesComponent, elements.Textarea): on_blur: Fired when the input loses focus on_key_down: Fired when a key is pressed down on_key_up: Fired when a key is released - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -268,4 +268,6 @@ class TextArea(RadixThemesComponent, elements.Textarea): """ ... + def add_style(self): ... + text_area = TextArea.create diff --git a/reflex/components/radix/themes/components/text_field.py b/reflex/components/radix/themes/components/text_field.py index 3dabe0936..130fb7aed 100644 --- a/reflex/components/radix/themes/components/text_field.py +++ b/reflex/components/radix/themes/components/text_field.py @@ -9,7 +9,9 @@ from reflex.components.core.breakpoints import Responsive from reflex.components.core.debounce import DebounceInput from reflex.components.el import elements from reflex.event import EventHandler, input_event, key_event +from reflex.utils.types import is_optional from reflex.vars.base import Var +from reflex.vars.number import ternary_operation from ..base import LiteralAccentColor, LiteralRadius, RadixThemesComponent @@ -17,7 +19,7 @@ LiteralTextFieldSize = Literal["1", "2", "3"] LiteralTextFieldVariant = Literal["classic", "surface", "soft"] -class TextFieldRoot(elements.Div, RadixThemesComponent): +class TextFieldRoot(elements.Input, RadixThemesComponent): """Captures user input with an optional slot for buttons and icons.""" tag = "TextField.Root" @@ -96,6 +98,19 @@ class TextFieldRoot(elements.Div, RadixThemesComponent): Returns: The component. """ + value = props.get("value") + + # React expects an empty string(instead of null) for controlled inputs. + if value is not None and is_optional( + (value_var := Var.create(value))._var_type + ): + props["value"] = ternary_operation( + (value_var != Var.create(None)) # pyright: ignore [reportArgumentType] + & (value_var != Var(_js_expr="undefined")), + value, + Var.create(""), + ) + component = super().create(*children, **props) if props.get("value") is not None and props.get("on_change") is not None: # create a debounced input if the user requests full control to avoid typing jank diff --git a/reflex/components/radix/themes/components/text_field.pyi b/reflex/components/radix/themes/components/text_field.pyi index edce803eb..81c991899 100644 --- a/reflex/components/radix/themes/components/text_field.pyi +++ b/reflex/components/radix/themes/components/text_field.pyi @@ -17,7 +17,7 @@ from ..base import RadixThemesComponent LiteralTextFieldSize = Literal["1", "2", "3"] LiteralTextFieldVariant = Literal["classic", "surface", "soft"] -class TextFieldRoot(elements.Div, RadixThemesComponent): +class TextFieldRoot(elements.Input, RadixThemesComponent): @overload @classmethod def create( # type: ignore @@ -120,6 +120,30 @@ class TextFieldRoot(elements.Div, RadixThemesComponent): type: Optional[Union[Var[str], str]] = None, value: Optional[Union[Var[Union[float, int, str]], float, int, str]] = None, list: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + accept: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + alt: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + auto_focus: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + capture: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + checked: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + default_checked: Optional[Union[Var[bool], bool]] = None, + dirname: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + form: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + form_action: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + form_enc_type: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + form_method: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + form_no_validate: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + form_target: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + max: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + min: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + multiple: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + pattern: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + src: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + step: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + use_map: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, auto_capitalize: Optional[ Union[Var[Union[bool, int, str]], bool, int, str] @@ -192,12 +216,12 @@ class TextFieldRoot(elements.Div, RadixThemesComponent): Args: *children: The children of the component. - size: Text field size "1" - "3" + size: Specifies the visible width of a text control variant: Variant of text field: "classic" | "surface" | "soft" color_scheme: Override theme color for text field radius: Override theme radius for text field: "none" | "small" | "medium" | "large" | "full" auto_complete: Whether the input should have autocomplete enabled - default_value: The value of the input when initially rendered. + default_value: The initial value for a text field disabled: Disables the input max_length: Specifies the maximum number of characters allowed in the input min_length: Specifies the minimum number of characters required in the input @@ -208,12 +232,32 @@ class TextFieldRoot(elements.Div, RadixThemesComponent): type: Specifies the type of input value: Value of the input list: References a datalist for suggested options - on_change: Fired when the value of the textarea changes. - on_focus: Fired when the textarea is focused. - on_blur: Fired when the textarea is blurred. - on_key_down: Fired when a key is pressed down. - on_key_up: Fired when a key is released. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + on_change: Fired when the input value changes + on_focus: Fired when the input gains focus + on_blur: Fired when the input loses focus + on_key_down: Fired when a key is pressed down + on_key_up: Fired when a key is released + accept: Accepted types of files when the input is file type + alt: Alternate text for input type="image" + auto_focus: Automatically focuses the input when the page loads + capture: Captures media from the user (camera or microphone) + checked: Indicates whether the input is checked (for checkboxes and radio buttons) + default_checked: The initial value (for checkboxes and radio buttons) + dirname: Name part of the input to submit in 'dir' and 'name' pair when form is submitted + form: Associates the input with a form (by id) + form_action: URL to send the form data to (for type="submit" buttons) + form_enc_type: How the form data should be encoded when submitting to the server (for type="submit" buttons) + form_method: HTTP method to use for sending form data (for type="submit" buttons) + form_no_validate: Bypasses form validation when submitting (for type="submit" buttons) + form_target: Specifies where to display the response after submitting the form (for type="submit" buttons) + max: Specifies the maximum value for the input + min: Specifies the minimum value for the input + multiple: Indicates whether multiple values can be entered in an input of the type email or file + pattern: Regex pattern the input's value must match to be valid + src: URL for image inputs + step: Specifies the legal number intervals for an input + use_map: Name of the image map used with the input + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -457,6 +501,30 @@ class TextField(ComponentNamespace): type: Optional[Union[Var[str], str]] = None, value: Optional[Union[Var[Union[float, int, str]], float, int, str]] = None, list: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + accept: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + alt: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + auto_focus: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + capture: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + checked: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + default_checked: Optional[Union[Var[bool], bool]] = None, + dirname: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + form: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + form_action: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + form_enc_type: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + form_method: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + form_no_validate: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + form_target: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + max: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + min: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + multiple: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + pattern: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + src: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + step: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + use_map: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, auto_capitalize: Optional[ Union[Var[Union[bool, int, str]], bool, int, str] @@ -529,12 +597,12 @@ class TextField(ComponentNamespace): Args: *children: The children of the component. - size: Text field size "1" - "3" + size: Specifies the visible width of a text control variant: Variant of text field: "classic" | "surface" | "soft" color_scheme: Override theme color for text field radius: Override theme radius for text field: "none" | "small" | "medium" | "large" | "full" auto_complete: Whether the input should have autocomplete enabled - default_value: The value of the input when initially rendered. + default_value: The initial value for a text field disabled: Disables the input max_length: Specifies the maximum number of characters allowed in the input min_length: Specifies the minimum number of characters required in the input @@ -545,12 +613,32 @@ class TextField(ComponentNamespace): type: Specifies the type of input value: Value of the input list: References a datalist for suggested options - on_change: Fired when the value of the textarea changes. - on_focus: Fired when the textarea is focused. - on_blur: Fired when the textarea is blurred. - on_key_down: Fired when a key is pressed down. - on_key_up: Fired when a key is released. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + on_change: Fired when the input value changes + on_focus: Fired when the input gains focus + on_blur: Fired when the input loses focus + on_key_down: Fired when a key is pressed down + on_key_up: Fired when a key is released + accept: Accepted types of files when the input is file type + alt: Alternate text for input type="image" + auto_focus: Automatically focuses the input when the page loads + capture: Captures media from the user (camera or microphone) + checked: Indicates whether the input is checked (for checkboxes and radio buttons) + default_checked: The initial value (for checkboxes and radio buttons) + dirname: Name part of the input to submit in 'dir' and 'name' pair when form is submitted + form: Associates the input with a form (by id) + form_action: URL to send the form data to (for type="submit" buttons) + form_enc_type: How the form data should be encoded when submitting to the server (for type="submit" buttons) + form_method: HTTP method to use for sending form data (for type="submit" buttons) + form_no_validate: Bypasses form validation when submitting (for type="submit" buttons) + form_target: Specifies where to display the response after submitting the form (for type="submit" buttons) + max: Specifies the maximum value for the input + min: Specifies the minimum value for the input + multiple: Indicates whether multiple values can be entered in an input of the type email or file + pattern: Regex pattern the input's value must match to be valid + src: URL for image inputs + step: Specifies the legal number intervals for an input + use_map: Name of the image map used with the input + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/components/tooltip.py b/reflex/components/radix/themes/components/tooltip.py index 07bab1a4a..53ec35264 100644 --- a/reflex/components/radix/themes/components/tooltip.py +++ b/reflex/components/radix/themes/components/tooltip.py @@ -28,6 +28,9 @@ LiteralStickyType = Literal[ ] +ARIA_LABEL_KEY = "aria_label" + + # The Tooltip inherits props from the Tooltip.Root, Tooltip.Portal, Tooltip.Content class Tooltip(RadixThemesComponent): """Floating element that provides a control with contextual information via pointer or focus.""" @@ -104,7 +107,6 @@ class Tooltip(RadixThemesComponent): Returns: The created component. """ - ARIA_LABEL_KEY = "aria_label" if props.get(ARIA_LABEL_KEY) is not None: props[format.to_kebab_case(ARIA_LABEL_KEY)] = props.pop(ARIA_LABEL_KEY) diff --git a/reflex/components/radix/themes/components/tooltip.pyi b/reflex/components/radix/themes/components/tooltip.pyi index fab1d0c12..0786bfada 100644 --- a/reflex/components/radix/themes/components/tooltip.pyi +++ b/reflex/components/radix/themes/components/tooltip.pyi @@ -14,6 +14,7 @@ from ..base import RadixThemesComponent LiteralSideType = Literal["top", "right", "bottom", "left"] LiteralAlignType = Literal["start", "center", "end"] LiteralStickyType = Literal["partial", "always"] +ARIA_LABEL_KEY = "aria_label" class Tooltip(RadixThemesComponent): @overload diff --git a/reflex/components/radix/themes/layout/__init__.pyi b/reflex/components/radix/themes/layout/__init__.pyi index 6712a3068..21fc8d921 100644 --- a/reflex/components/radix/themes/layout/__init__.pyi +++ b/reflex/components/radix/themes/layout/__init__.pyi @@ -9,7 +9,7 @@ from .container import container as container from .flex import flex as flex from .grid import grid as grid from .list import list_item as list_item -from .list import list_ns as list # noqa +from .list import list_ns as list # noqa: F401 from .list import ordered_list as ordered_list from .list import unordered_list as unordered_list from .section import section as section diff --git a/reflex/components/radix/themes/layout/box.pyi b/reflex/components/radix/themes/layout/box.pyi index 648995b01..416e45f3a 100644 --- a/reflex/components/radix/themes/layout/box.pyi +++ b/reflex/components/radix/themes/layout/box.pyi @@ -72,7 +72,7 @@ class Box(elements.Div, RadixThemesComponent): Args: *children: Child components. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/layout/center.pyi b/reflex/components/radix/themes/layout/center.pyi index e932be6c8..59062b293 100644 --- a/reflex/components/radix/themes/layout/center.pyi +++ b/reflex/components/radix/themes/layout/center.pyi @@ -150,12 +150,12 @@ class Center(Flex): Args: *children: Child components. as_child: Change the default rendered element for the one passed as a child, merging their props and behavior. - direction: How child items are layed out: "row" | "column" | "row-reverse" | "column-reverse" + direction: How child items are laid out: "row" | "column" | "row-reverse" | "column-reverse" align: Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch" justify: Alignment of children along the cross axis: "start" | "center" | "end" | "between" wrap: Whether children should wrap when they reach the end of their container: "nowrap" | "wrap" | "wrap-reverse" spacing: Gap between children: "0" - "9" - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/layout/flex.py b/reflex/components/radix/themes/layout/flex.py index 4403a9542..39982e4f4 100644 --- a/reflex/components/radix/themes/layout/flex.py +++ b/reflex/components/radix/themes/layout/flex.py @@ -22,7 +22,7 @@ class Flex(elements.Div, RadixThemesComponent): # Change the default rendered element for the one passed as a child, merging their props and behavior. as_child: Var[bool] - # How child items are layed out: "row" | "column" | "row-reverse" | "column-reverse" + # How child items are laid out: "row" | "column" | "row-reverse" | "column-reverse" direction: Var[Responsive[LiteralFlexDirection]] # Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch" diff --git a/reflex/components/radix/themes/layout/flex.pyi b/reflex/components/radix/themes/layout/flex.pyi index 8462720d0..dafa91c6c 100644 --- a/reflex/components/radix/themes/layout/flex.pyi +++ b/reflex/components/radix/themes/layout/flex.pyi @@ -153,12 +153,12 @@ class Flex(elements.Div, RadixThemesComponent): Args: *children: Child components. as_child: Change the default rendered element for the one passed as a child, merging their props and behavior. - direction: How child items are layed out: "row" | "column" | "row-reverse" | "column-reverse" + direction: How child items are laid out: "row" | "column" | "row-reverse" | "column-reverse" align: Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch" justify: Alignment of children along the cross axis: "start" | "center" | "end" | "between" wrap: Whether children should wrap when they reach the end of their container: "nowrap" | "wrap" | "wrap-reverse" spacing: Gap between children: "0" - "9" - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/layout/grid.py b/reflex/components/radix/themes/layout/grid.py index 3601e213a..133e19d10 100644 --- a/reflex/components/radix/themes/layout/grid.py +++ b/reflex/components/radix/themes/layout/grid.py @@ -27,7 +27,7 @@ class Grid(elements.Div, RadixThemesComponent): # Number of rows rows: Var[Responsive[str]] - # How the grid items are layed out: "row" | "column" | "dense" | "row-dense" | "column-dense" + # How the grid items are laid out: "row" | "column" | "dense" | "row-dense" | "column-dense" flow: Var[Responsive[LiteralGridFlow]] # Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch" diff --git a/reflex/components/radix/themes/layout/grid.pyi b/reflex/components/radix/themes/layout/grid.pyi index 0f4be760f..55153fca3 100644 --- a/reflex/components/radix/themes/layout/grid.pyi +++ b/reflex/components/radix/themes/layout/grid.pyi @@ -184,13 +184,13 @@ class Grid(elements.Div, RadixThemesComponent): as_child: Change the default rendered element for the one passed as a child, merging their props and behavior. columns: Number of columns rows: Number of rows - flow: How the grid items are layed out: "row" | "column" | "dense" | "row-dense" | "column-dense" + flow: How the grid items are laid out: "row" | "column" | "dense" | "row-dense" | "column-dense" align: Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch" justify: Alignment of children along the cross axis: "start" | "center" | "end" | "between" spacing: Gap between children: "0" - "9" spacing_x: Gap between children horizontal: "0" - "9" spacing_y: Gap between children vertical: "0" - "9" - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/layout/list.py b/reflex/components/radix/themes/layout/list.py index 96fa169a0..04fcb6ae5 100644 --- a/reflex/components/radix/themes/layout/list.py +++ b/reflex/components/radix/themes/layout/list.py @@ -64,7 +64,6 @@ class BaseList(Component, MarkdownComponentMap): Returns: The list component. - """ items = props.pop("items", None) list_style_type = props.pop("list_style_type", "none") @@ -73,7 +72,7 @@ class BaseList(Component, MarkdownComponentMap): if isinstance(items, Var): children = [Foreach.create(items, ListItem.create)] else: - children = [ListItem.create(item) for item in items] # type: ignore + children = [ListItem.create(item) for item in items] props["direction"] = "column" style = props.setdefault("style", {}) style["list_style_type"] = list_style_type @@ -114,7 +113,6 @@ class UnorderedList(BaseList, Ul): Returns: The list component. - """ items = props.pop("items", None) list_style_type = props.pop("list_style_type", "disc") @@ -144,7 +142,6 @@ class OrderedList(BaseList, Ol): Returns: The list component. - """ items = props.pop("items", None) list_style_type = props.pop("list_style_type", "decimal") @@ -168,7 +165,6 @@ class ListItem(Li, MarkdownComponentMap): Returns: The list item component. - """ for child in children: if isinstance(child, Text): @@ -193,7 +189,7 @@ ordered_list = list_ns.ordered unordered_list = list_ns.unordered -def __getattr__(name): +def __getattr__(name: Any): # special case for when accessing list to avoid shadowing # python's built in list object. if name == "list": diff --git a/reflex/components/radix/themes/layout/list.pyi b/reflex/components/radix/themes/layout/list.pyi index b42f689b9..8517a6897 100644 --- a/reflex/components/radix/themes/layout/list.pyi +++ b/reflex/components/radix/themes/layout/list.pyi @@ -118,7 +118,6 @@ class BaseList(Component, MarkdownComponentMap): Returns: The list component. - """ ... @@ -226,7 +225,7 @@ class UnorderedList(BaseList, Ul): *children: The children of the component. list_style_type: The style of the list. Default to "none". items: A list of items to add to the list. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -252,7 +251,6 @@ class UnorderedList(BaseList, Ul): Returns: The list component. - """ ... @@ -364,7 +362,7 @@ class OrderedList(BaseList, Ol): reversed: Reverses the order of the list. start: Specifies the start value of the first list item in an ordered list. type: Specifies the kind of marker to use in the list (letters or numbers). - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -390,7 +388,6 @@ class OrderedList(BaseList, Ol): Returns: The list component. - """ ... @@ -451,7 +448,7 @@ class ListItem(Li, MarkdownComponentMap): Args: *children: The children of the component. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -477,7 +474,6 @@ class ListItem(Li, MarkdownComponentMap): Returns: The list item component. - """ ... @@ -571,7 +567,6 @@ class List(ComponentNamespace): Returns: The list component. - """ ... diff --git a/reflex/components/radix/themes/layout/section.pyi b/reflex/components/radix/themes/layout/section.pyi index 7e909bf64..c005f273f 100644 --- a/reflex/components/radix/themes/layout/section.pyi +++ b/reflex/components/radix/themes/layout/section.pyi @@ -87,7 +87,7 @@ class Section(elements.Section, RadixThemesComponent): Args: *children: Child components. size: The size of the section: "1" - "3" (default "2") - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/layout/spacer.pyi b/reflex/components/radix/themes/layout/spacer.pyi index bd98ac656..9854aa1ba 100644 --- a/reflex/components/radix/themes/layout/spacer.pyi +++ b/reflex/components/radix/themes/layout/spacer.pyi @@ -150,12 +150,12 @@ class Spacer(Flex): Args: *children: Child components. as_child: Change the default rendered element for the one passed as a child, merging their props and behavior. - direction: How child items are layed out: "row" | "column" | "row-reverse" | "column-reverse" + direction: How child items are laid out: "row" | "column" | "row-reverse" | "column-reverse" align: Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch" justify: Alignment of children along the cross axis: "start" | "center" | "end" | "between" wrap: Whether children should wrap when they reach the end of their container: "nowrap" | "wrap" | "wrap-reverse" spacing: Gap between children: "0" - "9" - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/layout/stack.py b/reflex/components/radix/themes/layout/stack.py index d11c3488b..f017ff783 100644 --- a/reflex/components/radix/themes/layout/stack.py +++ b/reflex/components/radix/themes/layout/stack.py @@ -49,14 +49,14 @@ class VStack(Stack): """A vertical stack component.""" # The direction of the stack. - direction: Var[LiteralFlexDirection] = "column" # type: ignore + direction: Var[LiteralFlexDirection] = Var.create("column") class HStack(Stack): """A horizontal stack component.""" # The direction of the stack. - direction: Var[LiteralFlexDirection] = "row" # type: ignore + direction: Var[LiteralFlexDirection] = Var.create("row") stack = Stack.create diff --git a/reflex/components/radix/themes/layout/stack.pyi b/reflex/components/radix/themes/layout/stack.pyi index 712e050a8..d96c72504 100644 --- a/reflex/components/radix/themes/layout/stack.pyi +++ b/reflex/components/radix/themes/layout/stack.pyi @@ -126,10 +126,10 @@ class Stack(Flex): spacing: Gap between children: "0" - "9" align: Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch" as_child: Change the default rendered element for the one passed as a child, merging their props and behavior. - direction: How child items are layed out: "row" | "column" | "row-reverse" | "column-reverse" + direction: How child items are laid out: "row" | "column" | "row-reverse" | "column-reverse" justify: Alignment of children along the cross axis: "start" | "center" | "end" | "between" wrap: Whether children should wrap when they reach the end of their container: "nowrap" | "wrap" | "wrap-reverse" - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -258,13 +258,13 @@ class VStack(Stack): Args: *children: The children of the stack. - direction: How child items are layed out: "row" | "column" | "row-reverse" | "column-reverse" + direction: How child items are laid out: "row" | "column" | "row-reverse" | "column-reverse" spacing: Gap between children: "0" - "9" align: Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch" as_child: Change the default rendered element for the one passed as a child, merging their props and behavior. justify: Alignment of children along the cross axis: "start" | "center" | "end" | "between" wrap: Whether children should wrap when they reach the end of their container: "nowrap" | "wrap" | "wrap-reverse" - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -393,13 +393,13 @@ class HStack(Stack): Args: *children: The children of the stack. - direction: How child items are layed out: "row" | "column" | "row-reverse" | "column-reverse" + direction: How child items are laid out: "row" | "column" | "row-reverse" | "column-reverse" spacing: Gap between children: "0" - "9" align: Alignment of children along the main axis: "start" | "center" | "end" | "baseline" | "stretch" as_child: Change the default rendered element for the one passed as a child, merging their props and behavior. justify: Alignment of children along the cross axis: "start" | "center" | "end" | "between" wrap: Whether children should wrap when they reach the end of their container: "nowrap" | "wrap" | "wrap-reverse" - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/typography/blockquote.pyi b/reflex/components/radix/themes/typography/blockquote.pyi index efb18a5b2..747724763 100644 --- a/reflex/components/radix/themes/typography/blockquote.pyi +++ b/reflex/components/radix/themes/typography/blockquote.pyi @@ -168,7 +168,7 @@ class Blockquote(elements.Blockquote, RadixThemesComponent): color_scheme: Overrides the accent color inherited from the Theme. high_contrast: Whether to render the text with higher contrast color cite: Define the title of a work. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/typography/code.pyi b/reflex/components/radix/themes/typography/code.pyi index 0276eb982..847df267c 100644 --- a/reflex/components/radix/themes/typography/code.pyi +++ b/reflex/components/radix/themes/typography/code.pyi @@ -174,7 +174,7 @@ class Code(elements.Code, RadixThemesComponent, MarkdownComponentMap): weight: Thickness of text: "light" | "regular" | "medium" | "bold" color_scheme: Overrides the accent color inherited from the Theme. high_contrast: Whether to render the text with higher contrast color - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/typography/heading.pyi b/reflex/components/radix/themes/typography/heading.pyi index b5cb5c9d3..4a1e30dbf 100644 --- a/reflex/components/radix/themes/typography/heading.pyi +++ b/reflex/components/radix/themes/typography/heading.pyi @@ -197,7 +197,7 @@ class Heading(elements.H1, RadixThemesComponent, MarkdownComponentMap): trim: Removes the leading trim space: "normal" | "start" | "end" | "both" color_scheme: Overrides the accent color inherited from the Theme. high_contrast: Whether to render the text with higher contrast color - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/typography/link.py b/reflex/components/radix/themes/typography/link.py index 25a0902cc..09172b108 100644 --- a/reflex/components/radix/themes/typography/link.py +++ b/reflex/components/radix/themes/typography/link.py @@ -60,7 +60,7 @@ class Link(RadixThemesComponent, A, MemoizationLeaf, MarkdownComponentMap): Returns: The import dict. """ - return next_link._get_imports() # type: ignore + return next_link._get_imports() # pyright: ignore [reportReturnType] @classmethod def create(cls, *children, **props) -> Component: @@ -76,7 +76,7 @@ class Link(RadixThemesComponent, A, MemoizationLeaf, MarkdownComponentMap): Returns: Component: The link component """ - props.setdefault(":hover", {"color": color("accent", 8)}) + props.setdefault("_hover", {"color": color("accent", 8)}) href = props.get("href") is_external = props.pop("is_external", None) diff --git a/reflex/components/radix/themes/typography/link.pyi b/reflex/components/radix/themes/typography/link.pyi index db963c6df..807f8dda0 100644 --- a/reflex/components/radix/themes/typography/link.pyi +++ b/reflex/components/radix/themes/typography/link.pyi @@ -215,7 +215,7 @@ class Link(RadixThemesComponent, A, MemoizationLeaf, MarkdownComponentMap): rel: Specifies the relationship between the linked document and the current document shape: Specifies the shape of the area target: Specifies where to open the linked document - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/radix/themes/typography/text.py b/reflex/components/radix/themes/typography/text.py index 1663ddedf..cb6527915 100644 --- a/reflex/components/radix/themes/typography/text.py +++ b/reflex/components/radix/themes/typography/text.py @@ -47,7 +47,7 @@ class Text(elements.Span, RadixThemesComponent, MarkdownComponentMap): as_child: Var[bool] # Change the default rendered element into a semantically appropriate alternative (cannot be used with asChild) - as_: Var[LiteralType] = "p" # type: ignore + as_: Var[LiteralType] = Var.create("p") # Text size: "1" - "9" size: Var[Responsive[LiteralTextSize]] @@ -71,7 +71,7 @@ class Text(elements.Span, RadixThemesComponent, MarkdownComponentMap): class Span(Text): """A variant of text rendering as element.""" - as_: Var[LiteralType] = "span" # type: ignore + as_: Var[LiteralType] = Var.create("span") class Em(elements.Em, RadixThemesComponent): diff --git a/reflex/components/radix/themes/typography/text.pyi b/reflex/components/radix/themes/typography/text.pyi index 824348b42..d96b5799b 100644 --- a/reflex/components/radix/themes/typography/text.pyi +++ b/reflex/components/radix/themes/typography/text.pyi @@ -264,7 +264,7 @@ class Text(elements.Span, RadixThemesComponent, MarkdownComponentMap): trim: Removes the leading trim space: "normal" | "start" | "end" | "both" color_scheme: Overrides the accent color inherited from the Theme. high_contrast: Whether to render the text with higher contrast color - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -521,7 +521,7 @@ class Span(Text): trim: Removes the leading trim space: "normal" | "start" | "end" | "both" color_scheme: Overrides the accent color inherited from the Theme. high_contrast: Whether to render the text with higher contrast color - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -610,7 +610,7 @@ class Em(elements.Em, RadixThemesComponent): Args: *children: Child components. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -706,7 +706,7 @@ class Kbd(elements.Kbd, RadixThemesComponent): Args: *children: Child components. size: Text size: "1" - "9" - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -797,7 +797,7 @@ class Quote(elements.Q, RadixThemesComponent): Args: *children: Child components. cite: Specifies the source URL of the quote. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -886,7 +886,7 @@ class Strong(elements.Strong, RadixThemesComponent): Args: *children: Child components. - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. @@ -1147,7 +1147,7 @@ class TextNamespace(ComponentNamespace): trim: Removes the leading trim space: "normal" | "start" | "end" | "both" color_scheme: Overrides the accent color inherited from the Theme. high_contrast: Whether to render the text with higher contrast color - access_key: Provides a hint for generating a keyboard shortcut for the current element. + access_key: Provides a hint for generating a keyboard shortcut for the current element. auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. content_editable: Indicates whether the element's content is editable. context_menu: Defines the ID of a element which will serve as the element's context menu. diff --git a/reflex/components/react_player/react_player.py b/reflex/components/react_player/react_player.py index fb0319ceb..7b7bb34e3 100644 --- a/reflex/components/react_player/react_player.py +++ b/reflex/components/react_player/react_player.py @@ -39,7 +39,7 @@ class ReactPlayer(NoSSRComponent): loop: Var[bool] # Set to true or false to display native player controls. - controls: Var[bool] = True # type: ignore + controls: Var[bool] = Var.create(True) # Set to true to show just the video thumbnail, which loads the full player on click light: Var[bool] diff --git a/reflex/components/recharts/__init__.py b/reflex/components/recharts/__init__.py index 5e9e6fc14..6495c6583 100644 --- a/reflex/components/recharts/__init__.py +++ b/reflex/components/recharts/__init__.py @@ -70,6 +70,8 @@ _SUBMOD_ATTRS: dict = { "Label", "label_list", "LabelList", + "cell", + "Cell", ], "polar": [ "pie", diff --git a/reflex/components/recharts/__init__.pyi b/reflex/components/recharts/__init__.pyi index 8f6c4673f..61fc9b1e7 100644 --- a/reflex/components/recharts/__init__.pyi +++ b/reflex/components/recharts/__init__.pyi @@ -53,11 +53,13 @@ from .charts import radar_chart as radar_chart from .charts import radial_bar_chart as radial_bar_chart from .charts import scatter_chart as scatter_chart from .charts import treemap as treemap +from .general import Cell as Cell from .general import GraphingTooltip as GraphingTooltip from .general import Label as Label from .general import LabelList as LabelList from .general import Legend as Legend from .general import ResponsiveContainer as ResponsiveContainer +from .general import cell as cell from .general import graphing_tooltip as graphing_tooltip from .general import label as label from .general import label_list as label_list diff --git a/reflex/components/recharts/cartesian.py b/reflex/components/recharts/cartesian.py index 028bcb4e4..9f6bf672b 100644 --- a/reflex/components/recharts/cartesian.py +++ b/reflex/components/recharts/cartesian.py @@ -42,7 +42,7 @@ class Axis(Recharts): # The width of axis which is usually calculated internally. width: Var[Union[str, int]] - # The height of axis, which can be setted by user. + # The height of axis, which can be set by user. height: Var[Union[str, int]] # The type of axis 'number' | 'category' @@ -60,7 +60,7 @@ class Axis(Recharts): # Allow the axis has duplicated categorys or not when the type of axis is "category". Default: True allow_duplicated_category: Var[bool] - # The range of the axis. Work best in conjuction with allow_data_overflow. Default: [0, "auto"] + # The range of the axis. Work best in conjunction with allow_data_overflow. Default: [0, "auto"] domain: Var[List] # If set false, no axis line will be drawn. Default: True @@ -416,7 +416,7 @@ class Bar(Cartesian): radius: Var[Union[int, List[int]]] # The active bar is shown when a user enters a bar chart and this chart has tooltip. If set to false, no active bar will be drawn. If set to true, active bar will be drawn with the props calculated internally. If passed an object, active bar will be drawn, and the internally calculated props will be merged with the key value pairs of the passed object. - # active_bar: Var[Union[bool, Dict[str, Any]]] + # active_bar: Var[Union[bool, Dict[str, Any]]] #noqa: ERA001 # Valid children components _valid_children: List[str] = ["Cell", "LabelList", "ErrorBar"] diff --git a/reflex/components/recharts/cartesian.pyi b/reflex/components/recharts/cartesian.pyi index 84f16661d..64921ec55 100644 --- a/reflex/components/recharts/cartesian.pyi +++ b/reflex/components/recharts/cartesian.pyi @@ -144,13 +144,13 @@ class Axis(Recharts): data_key: The key of data displayed in the axis. hide: If set true, the axis do not display in the chart. Default: False width: The width of axis which is usually calculated internally. - height: The height of axis, which can be setted by user. + height: The height of axis, which can be set by user. type_: The type of axis 'number' | 'category' interval: If set 0, all the ticks will be shown. If set preserveStart", "preserveEnd" or "preserveStartEnd", the ticks which is to be shown or hidden will be calculated automatically. Default: "preserveEnd" allow_decimals: Allow the ticks of Axis to be decimals or not. Default: True allow_data_overflow: When domain of the axis is specified and the type of the axis is 'number', if allowDataOverflow is set to be false, the domain will be adjusted when the minimum value of data is smaller than domain[0] or the maximum value of data is greater than domain[1] so that the axis displays all data values. If set to true, graphic elements (line, area, bars) will be clipped to conform to the specified domain. Default: False allow_duplicated_category: Allow the axis has duplicated categorys or not when the type of axis is "category". Default: True - domain: The range of the axis. Work best in conjuction with allow_data_overflow. Default: [0, "auto"] + domain: The range of the axis. Work best in conjunction with allow_data_overflow. Default: [0, "auto"] axis_line: If set false, no axis line will be drawn. Default: True mirror: If set true, flips ticks around the axis line, displaying the labels inside the chart instead of outside. Default: False reversed: Reverse the ticks or not. Default: False @@ -330,13 +330,13 @@ class XAxis(Axis): data_key: The key of data displayed in the axis. hide: If set true, the axis do not display in the chart. Default: False width: The width of axis which is usually calculated internally. - height: The height of axis, which can be setted by user. + height: The height of axis, which can be set by user. type_: The type of axis 'number' | 'category' interval: If set 0, all the ticks will be shown. If set preserveStart", "preserveEnd" or "preserveStartEnd", the ticks which is to be shown or hidden will be calculated automatically. Default: "preserveEnd" allow_decimals: Allow the ticks of Axis to be decimals or not. Default: True allow_data_overflow: When domain of the axis is specified and the type of the axis is 'number', if allowDataOverflow is set to be false, the domain will be adjusted when the minimum value of data is smaller than domain[0] or the maximum value of data is greater than domain[1] so that the axis displays all data values. If set to true, graphic elements (line, area, bars) will be clipped to conform to the specified domain. Default: False allow_duplicated_category: Allow the axis has duplicated categorys or not when the type of axis is "category". Default: True - domain: The range of the axis. Work best in conjuction with allow_data_overflow. Default: [0, "auto"] + domain: The range of the axis. Work best in conjunction with allow_data_overflow. Default: [0, "auto"] axis_line: If set false, no axis line will be drawn. Default: True mirror: If set true, flips ticks around the axis line, displaying the labels inside the chart instead of outside. Default: False reversed: Reverse the ticks or not. Default: False @@ -512,13 +512,13 @@ class YAxis(Axis): data_key: The key of data displayed in the axis. hide: If set true, the axis do not display in the chart. Default: False width: The width of axis which is usually calculated internally. - height: The height of axis, which can be setted by user. + height: The height of axis, which can be set by user. type_: The type of axis 'number' | 'category' interval: If set 0, all the ticks will be shown. If set preserveStart", "preserveEnd" or "preserveStartEnd", the ticks which is to be shown or hidden will be calculated automatically. Default: "preserveEnd" allow_decimals: Allow the ticks of Axis to be decimals or not. Default: True allow_data_overflow: When domain of the axis is specified and the type of the axis is 'number', if allowDataOverflow is set to be false, the domain will be adjusted when the minimum value of data is smaller than domain[0] or the maximum value of data is greater than domain[1] so that the axis displays all data values. If set to true, graphic elements (line, area, bars) will be clipped to conform to the specified domain. Default: False allow_duplicated_category: Allow the axis has duplicated categorys or not when the type of axis is "category". Default: True - domain: The range of the axis. Work best in conjuction with allow_data_overflow. Default: [0, "auto"] + domain: The range of the axis. Work best in conjunction with allow_data_overflow. Default: [0, "auto"] axis_line: If set false, no axis line will be drawn. Default: True mirror: If set true, flips ticks around the axis line, displaying the labels inside the chart instead of outside. Default: False reversed: Reverse the ticks or not. Default: False diff --git a/reflex/components/recharts/charts.py b/reflex/components/recharts/charts.py index 13f125213..3e9df4143 100644 --- a/reflex/components/recharts/charts.py +++ b/reflex/components/recharts/charts.py @@ -25,10 +25,10 @@ class ChartBase(RechartsCharts): """A component that wraps a Recharts charts.""" # The width of chart container. String or Integer - width: Var[Union[str, int]] = "100%" # type: ignore + width: Var[Union[str, int]] = Var.create("100%") # The height of chart container. - height: Var[Union[str, int]] = "100%" # type: ignore + height: Var[Union[str, int]] = Var.create("100%") # The customized event handler of click on the component in this chart on_click: EventHandler[no_args_event_spec] @@ -69,7 +69,7 @@ class ChartBase(RechartsCharts): ) @classmethod - def create(cls, *children, **props) -> Component: + def create(cls, *children: Any, **props: Any) -> Component: """Create a chart component. Args: @@ -84,19 +84,19 @@ class ChartBase(RechartsCharts): cls._ensure_valid_dimension("width", width) cls._ensure_valid_dimension("height", height) - dim_props = dict( - width=width or "100%", - height=height or "100%", - ) - # Provide min dimensions so the graph always appears, even if the outer container is zero-size. - if width is None: - dim_props["min_width"] = 200 - if height is None: - dim_props["min_height"] = 100 + # Ensure that the min_height and min_width are set to prevent the chart from collapsing. + # We are using small values so that height and width can still be used over min_height and min_width. + # Without this, sometimes the chart will not be visible. Causing confusion to the user. + # With this, the user will see a small chart and can adjust the height and width and can figure out that the issue is with the size. + min_height = props.pop("min_height", 10) + min_width = props.pop("min_width", 10) return ResponsiveContainer.create( super().create(*children, **props), - **dim_props, # type: ignore + width=width if width is not None else "100%", + height=height if height is not None else "100%", + min_width=min_width, + min_height=min_height, ) @@ -458,10 +458,10 @@ class Treemap(RechartsCharts): alias = "RechartsTreemap" # The width of chart container. String or Integer. Default: "100%" - width: Var[Union[str, int]] = "100%" # type: ignore + width: Var[Union[str, int]] = Var.create("100%") # The height of chart container. String or Integer. Default: "100%" - height: Var[Union[str, int]] = "100%" # type: ignore + height: Var[Union[str, int]] = Var.create("100%") # data of treemap. Array data: Var[List[Dict[str, Any]]] diff --git a/reflex/components/recharts/general.py b/reflex/components/recharts/general.py index 1769ea125..4b8c527d3 100644 --- a/reflex/components/recharts/general.py +++ b/reflex/components/recharts/general.py @@ -36,11 +36,11 @@ class ResponsiveContainer(Recharts, MemoizationLeaf): # The height of chart container. Can be a number or string. Default: "100%" height: Var[Union[int, str]] - # The minimum width of chart container. Number - min_width: Var[int] + # The minimum width of chart container. Number or string. + min_width: Var[Union[int, str]] - # The minimum height of chart container. Number - min_height: Var[int] + # The minimum height of chart container. Number or string. + min_height: Var[Union[int, str]] # If specified a positive number, debounced function will be used to handle the resize event. Default: 0 debounce: Var[int] @@ -242,8 +242,23 @@ class LabelList(Recharts): stroke: Var[Union[str, Color]] = LiteralVar.create("none") +class Cell(Recharts): + """A Cell component in Recharts.""" + + tag = "Cell" + + alias = "RechartsCell" + + # The presentation attribute of a rectangle in bar or a sector in pie. + fill: Var[str | Color] + + # The presentation attribute of a rectangle in bar or a sector in pie. + stroke: Var[str | Color] + + responsive_container = ResponsiveContainer.create legend = Legend.create graphing_tooltip = GraphingTooltip.create label = Label.create label_list = LabelList.create +cell = Cell.create diff --git a/reflex/components/recharts/general.pyi b/reflex/components/recharts/general.pyi index 823a50fce..9c63d6de9 100644 --- a/reflex/components/recharts/general.pyi +++ b/reflex/components/recharts/general.pyi @@ -22,8 +22,8 @@ class ResponsiveContainer(Recharts, MemoizationLeaf): aspect: Optional[Union[Var[int], int]] = None, width: Optional[Union[Var[Union[int, str]], int, str]] = None, height: Optional[Union[Var[Union[int, str]], int, str]] = None, - min_width: Optional[Union[Var[int], int]] = None, - min_height: Optional[Union[Var[int], int]] = None, + min_width: Optional[Union[Var[Union[int, str]], int, str]] = None, + min_height: Optional[Union[Var[Union[int, str]], int, str]] = None, debounce: Optional[Union[Var[int], int]] = None, style: Optional[Style] = None, key: Optional[Any] = None, @@ -56,8 +56,8 @@ class ResponsiveContainer(Recharts, MemoizationLeaf): aspect: The aspect ratio of the container. The final aspect ratio of the SVG element will be (width / height) * aspect. Number width: The width of chart container. Can be a number or string. Default: "100%" height: The height of chart container. Can be a number or string. Default: "100%" - min_width: The minimum width of chart container. Number - min_height: The minimum height of chart container. Number + min_width: The minimum width of chart container. Number or string. + min_height: The minimum height of chart container. Number or string. debounce: If specified a positive number, debounced function will be used to handle the resize event. Default: 0 on_resize: If specified provides a callback providing the updated chart width and height values. style: The style of the component. @@ -482,8 +482,59 @@ class LabelList(Recharts): """ ... +class Cell(Recharts): + @overload + @classmethod + def create( # type: ignore + cls, + *children, + fill: Optional[Union[Color, Var[Union[Color, str]], str]] = None, + stroke: Optional[Union[Color, Var[Union[Color, str]], str]] = None, + style: Optional[Style] = None, + key: Optional[Any] = None, + id: Optional[Any] = None, + class_name: Optional[Any] = None, + autofocus: Optional[bool] = None, + custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None, + on_blur: Optional[EventType[[], BASE_STATE]] = None, + on_click: Optional[EventType[[], BASE_STATE]] = None, + on_context_menu: Optional[EventType[[], BASE_STATE]] = None, + on_double_click: Optional[EventType[[], BASE_STATE]] = None, + on_focus: Optional[EventType[[], BASE_STATE]] = None, + on_mount: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_down: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_enter: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_leave: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_move: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_out: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_over: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_up: Optional[EventType[[], BASE_STATE]] = None, + on_scroll: Optional[EventType[[], BASE_STATE]] = None, + on_unmount: Optional[EventType[[], BASE_STATE]] = None, + **props, + ) -> "Cell": + """Create the component. + + Args: + *children: The children of the component. + fill: The presentation attribute of a rectangle in bar or a sector in pie. + stroke: The presentation attribute of a rectangle in bar or a sector in pie. + style: The style of the component. + key: A unique key for the component. + id: The id for the component. + class_name: The class name for the component. + autofocus: Whether the component should take the focus once the page is loaded + custom_attrs: custom attribute + **props: The props of the component. + + Returns: + The component. + """ + ... + responsive_container = ResponsiveContainer.create legend = Legend.create graphing_tooltip = GraphingTooltip.create label = Label.create label_list = LabelList.create +cell = Cell.create diff --git a/reflex/components/recharts/polar.py b/reflex/components/recharts/polar.py index 0aedf4893..77aa1ef5e 100644 --- a/reflex/components/recharts/polar.py +++ b/reflex/components/recharts/polar.py @@ -64,7 +64,7 @@ class Pie(Recharts): legend_type: Var[LiteralLegendType] # If false set, labels will not be drawn. If true set, labels will be drawn which have the props calculated internally. Default: False - label: Var[bool] = False # type: ignore + label: Var[bool] = Var.create(False) # If false set, label lines will not be drawn. If true set, label lines will be drawn which have the props calculated internally. Default: False label_line: Var[bool] @@ -73,7 +73,7 @@ class Pie(Recharts): data: Var[List[Dict[str, Any]]] # Valid children components - _valid_children: List[str] = ["Cell", "LabelList"] + _valid_children: List[str] = ["Cell", "LabelList", "Bare"] # Stoke color. Default: rx.color("accent", 9) stroke: Var[Union[str, Color]] = LiteralVar.create(Color("accent", 9)) @@ -124,7 +124,7 @@ class Radar(Recharts): # The key of a group of data which should be unique in a radar chart. data_key: Var[Union[str, int]] - # The coordinates of all the vertexes of the radar shape, like [{ x, y }]. + # The coordinates of all the vertices of the radar shape, like [{ x, y }]. points: Var[List[Dict[str, Any]]] # If false set, dots will not be drawn. Default: True @@ -136,7 +136,7 @@ class Radar(Recharts): # Fill color. Default: rx.color("accent", 3) fill: Var[str] = LiteralVar.create(Color("accent", 3)) - # opacity. Default: 0.6 + # The opacity to fill the chart. Default: 0.6 fill_opacity: Var[float] = LiteralVar.create(0.6) # The type of icon in legend. If set to 'none', no legend item will be rendered. Default: "rect" @@ -373,7 +373,7 @@ class PolarRadiusAxis(Recharts): # The count of axis ticks. Not used if 'type' is 'category'. Default: 5 tick_count: Var[int] - # If 'auto' set, the scale funtion is linear scale. 'auto' | 'linear' | 'pow' | 'sqrt' | 'log' | 'identity' | 'time' | 'band' | 'point' | 'ordinal' | 'quantile' | 'quantize' | 'utc' | 'sequential' | 'threshold'. Default: "auto" + # If 'auto' set, the scale function is linear scale. 'auto' | 'linear' | 'pow' | 'sqrt' | 'log' | 'identity' | 'time' | 'band' | 'point' | 'ordinal' | 'quantile' | 'quantize' | 'utc' | 'sequential' | 'threshold'. Default: "auto" scale: Var[LiteralScale] # Valid children components diff --git a/reflex/components/recharts/polar.pyi b/reflex/components/recharts/polar.pyi index 5ca96acb6..5388fbcf2 100644 --- a/reflex/components/recharts/polar.pyi +++ b/reflex/components/recharts/polar.pyi @@ -200,11 +200,11 @@ class Radar(Recharts): Args: *children: The children of the component. data_key: The key of a group of data which should be unique in a radar chart. - points: The coordinates of all the vertexes of the radar shape, like [{ x, y }]. + points: The coordinates of all the vertices of the radar shape, like [{ x, y }]. dot: If false set, dots will not be drawn. Default: True stroke: Stoke color. Default: rx.color("accent", 9) fill: Fill color. Default: rx.color("accent", 3) - fill_opacity: opacity. Default: 0.6 + fill_opacity: The opacity to fill the chart. Default: 0.6 legend_type: The type of icon in legend. If set to 'none', no legend item will be rendered. Default: "rect" label: If false set, labels will not be drawn. Default: True is_animation_active: If set false, animation of polygon will be disabled. Default: True in CSR, and False in SSR @@ -574,7 +574,7 @@ class PolarRadiusAxis(Recharts): axis_line: If false set, axis line will not be drawn. If true set, axis line will be drawn which have the props calculated internally. If object set, axis line will be drawn which have the props mergered by the internal calculated props and the option. Default: True tick: If false set, ticks will not be drawn. If true set, ticks will be drawn which have the props calculated internally. If object set, ticks will be drawn which have the props mergered by the internal calculated props and the option. Default: True tick_count: The count of axis ticks. Not used if 'type' is 'category'. Default: 5 - scale: If 'auto' set, the scale funtion is linear scale. 'auto' | 'linear' | 'pow' | 'sqrt' | 'log' | 'identity' | 'time' | 'band' | 'point' | 'ordinal' | 'quantile' | 'quantize' | 'utc' | 'sequential' | 'threshold'. Default: "auto" + scale: If 'auto' set, the scale function is linear scale. 'auto' | 'linear' | 'pow' | 'sqrt' | 'log' | 'identity' | 'time' | 'band' | 'point' | 'ordinal' | 'quantile' | 'quantize' | 'utc' | 'sequential' | 'threshold'. Default: "auto" domain: The domain of the polar radius axis, specifying the minimum and maximum values. Default: [0, "auto"] stroke: The stroke color of axis. Default: rx.color("gray", 10) style: The style of the component. diff --git a/reflex/components/recharts/recharts.py b/reflex/components/recharts/recharts.py index b5a4ed113..0d1b692f1 100644 --- a/reflex/components/recharts/recharts.py +++ b/reflex/components/recharts/recharts.py @@ -1,6 +1,6 @@ """A component that wraps a recharts lib.""" -from typing import Dict, Literal +from typing import Literal from reflex.components.component import Component, MemoizationLeaf, NoSSRComponent @@ -8,16 +8,16 @@ from reflex.components.component import Component, MemoizationLeaf, NoSSRCompone class Recharts(Component): """A component that wraps a recharts lib.""" - library = "recharts@2.13.0" + library = "recharts@2.15.0" - def _get_style(self) -> Dict: + def _get_style(self) -> dict: return {"wrapperStyle": self.style} class RechartsCharts(NoSSRComponent, MemoizationLeaf): """A component that wraps a recharts lib.""" - library = "recharts@2.13.0" + library = "recharts@2.15.0" LiteralAnimationEasing = Literal["ease", "ease-in", "ease-out", "ease-in-out", "linear"] diff --git a/reflex/components/sonner/toast.py b/reflex/components/sonner/toast.py index 14694e6ad..dbac8e733 100644 --- a/reflex/components/sonner/toast.py +++ b/reflex/components/sonner/toast.py @@ -98,7 +98,7 @@ class ToastProps(PropsBase, NoExtrasAllowedProps): # TODO: fix serialization of icons for toast? (might not be possible yet) # Icon displayed in front of toast's text, aligned vertically. - # icon: Optional[Icon] = None + # icon: Optional[Icon] = None # noqa: ERA001 # TODO: fix implementation for action / cancel buttons # Renders a primary button, clicking it will close the toast. @@ -132,7 +132,7 @@ class ToastProps(PropsBase, NoExtrasAllowedProps): # Function that gets called when the toast disappears automatically after it's timeout (duration` prop). on_auto_close: Optional[Any] - def dict(self, *args, **kwargs) -> dict[str, Any]: + def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]: """Convert the object to a dictionary. Args: @@ -142,7 +142,7 @@ class ToastProps(PropsBase, NoExtrasAllowedProps): Returns: The object as a dictionary with ToastAction fields intact. """ - kwargs.setdefault("exclude_none", True) # type: ignore + kwargs.setdefault("exclude_none", True) d = super().dict(*args, **kwargs) # Keep these fields as ToastAction so they can be serialized specially if "action" in d: @@ -167,7 +167,7 @@ class ToastProps(PropsBase, NoExtrasAllowedProps): class Toaster(Component): """A Toaster Component for displaying toast notifications.""" - library: str = "sonner@1.5.0" + library: str | None = "sonner@1.7.2" tag = "Toaster" @@ -222,6 +222,8 @@ class Toaster(Component): Returns: The hooks for the toaster component. """ + if self.library is None: + return [] hook = Var( _js_expr=f"{toast_ref} = toast", _var_data=VarData( @@ -266,7 +268,7 @@ class Toaster(Component): raise ValueError("Toast message or title or description must be provided.") if props: - args = LiteralVar.create(ToastProps(component_name="rx.toast", **props)) # pyright: ignore [reportCallIssue, reportGeneralTypeIssues] + args = LiteralVar.create(ToastProps(component_name="rx.toast", **props)) # pyright: ignore [reportCallIssue] toast = toast_command.call(message, args) else: toast = toast_command.call(message) @@ -274,12 +276,12 @@ class Toaster(Component): return run_script(toast) @staticmethod - def toast_info(message: str | Var = "", **kwargs): + def toast_info(message: str | Var = "", **kwargs: Any): """Display an info toast message. Args: message: The message to display. - kwargs: Additional toast props. + **kwargs: Additional toast props. Returns: The toast event. @@ -287,12 +289,12 @@ class Toaster(Component): return Toaster.send_toast(message, level="info", **kwargs) @staticmethod - def toast_warning(message: str | Var = "", **kwargs): + def toast_warning(message: str | Var = "", **kwargs: Any): """Display a warning toast message. Args: message: The message to display. - kwargs: Additional toast props. + **kwargs: Additional toast props. Returns: The toast event. @@ -300,12 +302,12 @@ class Toaster(Component): return Toaster.send_toast(message, level="warning", **kwargs) @staticmethod - def toast_error(message: str | Var = "", **kwargs): + def toast_error(message: str | Var = "", **kwargs: Any): """Display an error toast message. Args: message: The message to display. - kwargs: Additional toast props. + **kwargs: Additional toast props. Returns: The toast event. @@ -313,12 +315,12 @@ class Toaster(Component): return Toaster.send_toast(message, level="error", **kwargs) @staticmethod - def toast_success(message: str | Var = "", **kwargs): + def toast_success(message: str | Var = "", **kwargs: Any): """Display a success toast message. Args: message: The message to display. - kwargs: Additional toast props. + **kwargs: Additional toast props. Returns: The toast event. @@ -350,7 +352,7 @@ class Toaster(Component): return run_script(dismiss_action) @classmethod - def create(cls, *children, **props) -> Component: + def create(cls, *children: Any, **props: Any) -> Component: """Create a toaster component. Args: @@ -364,9 +366,7 @@ class Toaster(Component): return super().create(*children, **props) -# TODO: figure out why loading toast stay open forever -# def toast_loading(message: str, **kwargs): -# return _toast(message, level="loading", **kwargs) +# TODO: figure out why loading toast stay open forever when using level="loading" in toast() class ToastNamespace(ComponentNamespace): @@ -379,7 +379,6 @@ class ToastNamespace(ComponentNamespace): error = staticmethod(Toaster.toast_error) success = staticmethod(Toaster.toast_success) dismiss = staticmethod(Toaster.toast_dismiss) - # loading = staticmethod(toast_loading) __call__ = staticmethod(Toaster.send_toast) diff --git a/reflex/components/sonner/toast.pyi b/reflex/components/sonner/toast.pyi index 7fd9fdf54..632fb0d87 100644 --- a/reflex/components/sonner/toast.pyi +++ b/reflex/components/sonner/toast.pyi @@ -51,7 +51,7 @@ class ToastProps(PropsBase, NoExtrasAllowedProps): on_dismiss: Optional[Any] on_auto_close: Optional[Any] - def dict(self, *args, **kwargs) -> dict[str, Any]: ... + def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]: ... class Toaster(Component): is_used: ClassVar[bool] = False @@ -62,13 +62,13 @@ class Toaster(Component): message: str | Var = "", level: str | None = None, **props ) -> EventSpec: ... @staticmethod - def toast_info(message: str | Var = "", **kwargs): ... + def toast_info(message: str | Var = "", **kwargs: Any): ... @staticmethod - def toast_warning(message: str | Var = "", **kwargs): ... + def toast_warning(message: str | Var = "", **kwargs: Any): ... @staticmethod - def toast_error(message: str | Var = "", **kwargs): ... + def toast_error(message: str | Var = "", **kwargs: Any): ... @staticmethod - def toast_success(message: str | Var = "", **kwargs): ... + def toast_success(message: str | Var = "", **kwargs: Any): ... @staticmethod def toast_dismiss(id: Var | str | None = None): ... @overload @@ -177,7 +177,7 @@ class ToastNamespace(ComponentNamespace): @staticmethod def __call__( message: Union[str, Var] = "", level: Optional[str] = None, **props - ) -> "Optional[EventSpec]": + ) -> "EventSpec": """Send a toast message. Args: diff --git a/reflex/components/suneditor/editor.py b/reflex/components/suneditor/editor.py index 16d5689e2..3edf27545 100644 --- a/reflex/components/suneditor/editor.py +++ b/reflex/components/suneditor/editor.py @@ -3,7 +3,7 @@ from __future__ import annotations import enum -from typing import Dict, List, Literal, Optional, Tuple, Union +from typing import Any, Dict, List, Literal, Optional, Tuple, Union from reflex.base import Base from reflex.components.component import Component, NoSSRComponent @@ -115,8 +115,8 @@ class Editor(NoSSRComponent): # Alternatively to a string, a dict of your language can be passed to this prop. # Please refer to the library docs for this. # options: "en" | "da" | "de" | "es" | "fr" | "ja" | "ko" | "pt_br" | - # "ru" | "zh_cn" | "ro" | "pl" | "ckb" | "lv" | "se" | "ua" | "he" | "it" - # default : "en" + # "ru" | "zh_cn" | "ro" | "pl" | "ckb" | "lv" | "se" | "ua" | "he" | "it" + # default: "en". lang: Var[ Union[ Literal[ @@ -172,7 +172,7 @@ class Editor(NoSSRComponent): set_options: Var[Dict] # Whether all SunEditor plugins should be loaded. - # default: True + # default: True. set_all_plugins: Var[bool] # Set the content of the editor. @@ -191,19 +191,19 @@ class Editor(NoSSRComponent): set_default_style: Var[str] # Disable the editor - # default: False + # default: False. disable: Var[bool] # Hide the editor - # default: False + # default: False. hide: Var[bool] # Hide the editor toolbar - # default: False + # default: False. hide_toolbar: Var[bool] # Disable the editor toolbar - # default: False + # default: False. disable_toolbar: Var[bool] # Fired when the editor content changes. @@ -244,11 +244,13 @@ class Editor(NoSSRComponent): } @classmethod - def create(cls, set_options: Optional[EditorOptions] = None, **props) -> Component: + def create( + cls, set_options: Optional[EditorOptions] = None, **props: Any + ) -> Component: """Create an instance of Editor. No children allowed. Args: - set_options(Optional[EditorOptions]): Configuration object to further configure the instance. + set_options: Configuration object to further configure the instance. **props: Any properties to be passed to the Editor Returns: diff --git a/reflex/components/suneditor/editor.pyi b/reflex/components/suneditor/editor.pyi index 66ab4863e..5577220cb 100644 --- a/reflex/components/suneditor/editor.pyi +++ b/reflex/components/suneditor/editor.pyi @@ -171,8 +171,8 @@ class Editor(NoSSRComponent): """Create an instance of Editor. No children allowed. Args: - set_options(Optional[EditorOptions]): Configuration object to further configure the instance. - lang: Language of the editor. Alternatively to a string, a dict of your language can be passed to this prop. Please refer to the library docs for this. options: "en" | "da" | "de" | "es" | "fr" | "ja" | "ko" | "pt_br" | "ru" | "zh_cn" | "ro" | "pl" | "ckb" | "lv" | "se" | "ua" | "he" | "it" default : "en" + set_options: Configuration object to further configure the instance. + lang: Language of the editor. Alternatively to a string, a dict of your language can be passed to this prop. Please refer to the library docs for this. options: "en" | "da" | "de" | "es" | "fr" | "ja" | "ko" | "pt_br" | "ru" | "zh_cn" | "ro" | "pl" | "ckb" | "lv" | "se" | "ua" | "he" | "it" default: "en". name: This is used to set the HTML form name of the editor. This means on HTML form submission, it will be submitted together with contents of the editor by the name provided. default_value: Sets the default value of the editor. This is useful if you don't want the on_change method to be called on render. If you want the on_change method to be called on render please use the set_contents prop width: Sets the width of the editor. px and percentage values are accepted, eg width="100%" or width="500px" default: 100% @@ -180,14 +180,14 @@ class Editor(NoSSRComponent): placeholder: Sets the placeholder of the editor. auto_focus: Should the editor receive focus when initialized? set_options: Pass an EditorOptions instance to modify the behaviour of Editor even more. - set_all_plugins: Whether all SunEditor plugins should be loaded. default: True + set_all_plugins: Whether all SunEditor plugins should be loaded. default: True. set_contents: Set the content of the editor. Note: To set the initial contents of the editor without calling the on_change event, please use the default_value prop. set_contents is used to set the contents of the editor programmatically. You must be aware that, when the set_contents's prop changes, the on_change event is triggered. append_contents: Append editor content set_default_style: Sets the default style of the editor's edit area - disable: Disable the editor default: False - hide: Hide the editor default: False - hide_toolbar: Hide the editor toolbar default: False - disable_toolbar: Disable the editor toolbar default: False + disable: Disable the editor default: False. + hide: Hide the editor default: False. + hide_toolbar: Hide the editor toolbar default: False. + disable_toolbar: Disable the editor toolbar default: False. on_change: Fired when the editor content changes. on_input: Fired when the something is inputted in the editor. on_blur: Fired when the editor loses focus. diff --git a/reflex/components/tags/iter_tag.py b/reflex/components/tags/iter_tag.py index 38ecaf81c..cb02ca000 100644 --- a/reflex/components/tags/iter_tag.py +++ b/reflex/components/tags/iter_tag.py @@ -41,14 +41,14 @@ class IterTag(Tag): try: if iterable._var_type.mro()[0] is dict: # Arg is a tuple of (key, value). - return Tuple[get_args(iterable._var_type)] # type: ignore + return Tuple[get_args(iterable._var_type)] # pyright: ignore [reportReturnType] elif iterable._var_type.mro()[0] is tuple: # Arg is a union of any possible values in the tuple. - return Union[get_args(iterable._var_type)] # type: ignore + return Union[get_args(iterable._var_type)] # pyright: ignore [reportReturnType] else: return get_args(iterable._var_type)[0] except Exception: - return Any + return Any # pyright: ignore [reportReturnType] def get_index_var(self) -> Var: """Get the index var for the tag (with curly braces). diff --git a/reflex/components/tags/tag.py b/reflex/components/tags/tag.py index 0587c61ed..983726e56 100644 --- a/reflex/components/tags/tag.py +++ b/reflex/components/tags/tag.py @@ -3,13 +3,33 @@ from __future__ import annotations import dataclasses -from typing import Any, Dict, List, Optional, Union +from typing import Any, Dict, List, Optional, Sequence, Union from reflex.event import EventChain from reflex.utils import format, types from reflex.vars.base import LiteralVar, Var +def render_prop(value: Any) -> Any: + """Render the prop. + + Args: + value: The value to render. + + Returns: + The rendered value. + """ + from reflex.components.component import BaseComponent + + if isinstance(value, BaseComponent): + return value.render() + if isinstance(value, Sequence) and not isinstance(value, str): + return [render_prop(v) for v in value] + if callable(value) and not isinstance(value, Var): + return None + return value + + @dataclasses.dataclass() class Tag: """A React tag.""" @@ -49,7 +69,7 @@ class Tag: """Set the tag's fields. Args: - kwargs: The fields to set. + **kwargs: The fields to set. Returns: The tag with the fields @@ -66,7 +86,9 @@ class Tag: Tuple[str, Any]: The field name and value. """ for field in dataclasses.fields(self): - yield field.name, getattr(self, field.name) + rendered_value = render_prop(getattr(self, field.name)) + if rendered_value is not None: + yield field.name, rendered_value def add_props(self, **kwargs: Optional[Any]) -> Tag: """Add props to the tag. diff --git a/reflex/config.py b/reflex/config.py index ba3ec8c81..b9126f4b3 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -12,6 +12,7 @@ import threading import urllib.parse from importlib.util import find_spec from pathlib import Path +from types import ModuleType from typing import ( TYPE_CHECKING, Any, @@ -82,7 +83,7 @@ class DBConfig(Base): ) @classmethod - def postgresql_psycopg2( + def postgresql_psycopg( cls, database: str, username: str, @@ -90,7 +91,7 @@ class DBConfig(Base): host: str | None = None, port: int | None = 5432, ) -> DBConfig: - """Create an instance with postgresql+psycopg2 engine. + """Create an instance with postgresql+psycopg engine. Args: database: Database name. @@ -103,7 +104,7 @@ class DBConfig(Base): DBConfig instance. """ return cls( - engine="postgresql+psycopg2", + engine="postgresql+psycopg", username=username, password=password, host=host, @@ -389,7 +390,7 @@ class EnvVar(Generic[T]): os.environ[self.name] = str(value) -class env_var: # type: ignore +class env_var: # noqa: N801 # pyright: ignore [reportRedeclaration] """Descriptor for environment variables.""" name: str @@ -406,7 +407,7 @@ class env_var: # type: ignore self.default = default self.internal = internal - def __set_name__(self, owner, name): + def __set_name__(self, owner: Any, name: str): """Set the name of the descriptor. Args: @@ -415,7 +416,7 @@ class env_var: # type: ignore """ self.name = name - def __get__(self, instance, owner): + def __get__(self, instance: Any, owner: Any): """Get the EnvVar instance. Args: @@ -434,7 +435,7 @@ class env_var: # type: ignore if TYPE_CHECKING: - def env_var(default, internal=False) -> EnvVar: + def env_var(default: Any, internal: bool = False) -> EnvVar: """Typing helper for the env_var descriptor. Args: @@ -489,6 +490,9 @@ class EnvironmentVariables: # The working directory for the next.js commands. REFLEX_WEB_WORKDIR: EnvVar[Path] = env_var(Path(constants.Dirs.WEB)) + # The working directory for the states directory. + REFLEX_STATES_WORKDIR: EnvVar[Path] = env_var(Path(constants.Dirs.STATES)) + # Path to the alembic config file ALEMBIC_CONFIG: EnvVar[ExistingPath] = env_var(Path(constants.ALEMBIC_CONFIG)) @@ -512,6 +516,9 @@ class EnvironmentVariables: # Whether to print the SQL queries if the log level is INFO or lower. SQLALCHEMY_ECHO: EnvVar[bool] = env_var(False) + # Whether to check db connections before using them. + SQLALCHEMY_POOL_PRE_PING: EnvVar[bool] = env_var(True) + # Whether to ignore the redis config error. Some redis servers only allow out-of-band configuration. REFLEX_IGNORE_REDIS_CONFIG_ERROR: EnvVar[bool] = env_var(False) @@ -552,9 +559,6 @@ class EnvironmentVariables: # Arguments to pass to the app harness driver. APP_HARNESS_DRIVER_ARGS: EnvVar[str] = env_var("") - # Where to save screenshots when tests fail. - SCREENSHOT_DIR: EnvVar[Optional[Path]] = env_var(None) - # Whether to check for outdated package versions. REFLEX_CHECK_LATEST_VERSION: EnvVar[bool] = env_var(True) @@ -564,10 +568,17 @@ class EnvironmentVariables: # The maximum size of the reflex state in kilobytes. REFLEX_STATE_SIZE_LIMIT: EnvVar[int] = env_var(1000) + # Whether to use the turbopack bundler. + REFLEX_USE_TURBOPACK: EnvVar[bool] = env_var(True) + environment = EnvironmentVariables() +# These vars are not logged because they may contain sensitive information. +_sensitive_env_vars = {"DB_URL", "ASYNC_DB_URL", "REDIS_URL"} + + class Config(Base): """The config defines runtime settings for the app. @@ -589,7 +600,7 @@ class Config(Base): See the [configuration](https://reflex.dev/docs/getting-started/configuration/) docs for more info. """ - class Config: + class Config: # pyright: ignore [reportIncompatibleVariableOverride] """Pydantic config for the config.""" validate_assignment = True @@ -597,6 +608,9 @@ class Config(Base): # The name of the app (should match the name of the app directory). app_name: str + # The path to the app module. + app_module_import: Optional[str] = None + # The log level to use. loglevel: constants.LogLevel = constants.LogLevel.DEFAULT @@ -621,6 +635,9 @@ class Config(Base): # The database url used by rx.Model. db_url: Optional[str] = "sqlite:///reflex.db" + # The async database url used by rx.Model. + async_db_url: Optional[str] = None + # The redis url redis_url: Optional[str] = None @@ -674,6 +691,9 @@ class Config(Base): # Maximum expiration lock time for redis state manager redis_lock_expiration: int = constants.Expiration.LOCK + # Maximum lock time before warning for redis state manager. + redis_lock_warning_threshold: int = constants.Expiration.LOCK_WARNING_THRESHOLD + # Token expiration time for redis state manager redis_token_expiration: int = constants.Expiration.TOKEN @@ -686,6 +706,9 @@ class Config(Base): # Whether to automatically create setters for state base vars state_auto_setters: bool = True + # Whether the app is running in the reflex cloud environment. + is_reflex_cloud: bool = False + def __init__(self, *args, **kwargs): """Initialize the config values. @@ -716,6 +739,19 @@ class Config(Base): "REDIS_URL is required when using the redis state manager." ) + @property + def app_module(self) -> ModuleType | None: + """Return the app module if `app_module_import` is set. + + Returns: + The app module. + """ + return ( + importlib.import_module(self.app_module_import) + if self.app_module_import + else None + ) + @property def module(self) -> str: """Get the module name of the app. @@ -723,6 +759,8 @@ class Config(Base): Returns: The module name. """ + if self.app_module is not None: + return self.app_module.__name__ return ".".join([self.app_name, self.app_name]) def update_from_env(self) -> dict[str, Any]: @@ -734,7 +772,7 @@ class Config(Base): """ if self.env_file: try: - from dotenv import load_dotenv # type: ignore + from dotenv import load_dotenv # pyright: ignore [reportMissingImports] # load env file if exists load_dotenv(self.env_file, override=True) @@ -751,18 +789,20 @@ class Config(Base): # If the env var is set, override the config value. if env_var is not None: - if key.upper() != "DB_URL": - console.info( - f"Overriding config value {key} with env var {key.upper()}={env_var}", - dedupe=True, - ) - # Interpret the value. value = interpret_env_var_value(env_var, field.outer_type_, field.name) # Set the value. updated_values[key] = value + if key.upper() in _sensitive_env_vars: + env_var = "***" + + console.info( + f"Overriding config value {key} with env var {key.upper()}={env_var}", + dedupe=True, + ) + return updated_values def get_event_namespace(self) -> str: @@ -792,16 +832,16 @@ class Config(Base): if "api_url" not in self._non_default_attributes: # If running in Github Codespaces, override API_URL codespace_name = os.getenv("CODESPACE_NAME") - GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = os.getenv( + github_codespaces_port_forwarding_domain = os.getenv( "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN" ) # If running on Replit.com interactively, override API_URL to ensure we maintain the backend_port replit_dev_domain = os.getenv("REPLIT_DEV_DOMAIN") backend_port = kwargs.get("backend_port", self.backend_port) - if codespace_name and GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN: + if codespace_name and github_codespaces_port_forwarding_domain: self.api_url = ( f"https://{codespace_name}-{kwargs.get('backend_port', self.backend_port)}" - f".{GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}" + f".{github_codespaces_port_forwarding_domain}" ) elif replit_dev_domain and backend_port: self.api_url = f"https://{replit_dev_domain}:{backend_port}" @@ -859,17 +899,22 @@ def get_config(reload: bool = False) -> Config: return cached_rxconfig.config with _config_lock: - sys_path = sys.path.copy() + orig_sys_path = sys.path.copy() sys.path.clear() - sys.path.append(os.getcwd()) + sys.path.append(str(Path.cwd())) try: # Try to import the module with only the current directory in the path. return _get_config() except Exception: # If the module import fails, try to import with the original sys.path. - sys.path.extend(sys_path) + sys.path.extend(orig_sys_path) return _get_config() finally: + # Find any entries added to sys.path by rxconfig.py itself. + extra_paths = [ + p for p in sys.path if p not in orig_sys_path and p != str(Path.cwd()) + ] # Restore the original sys.path. sys.path.clear() - sys.path.extend(sys_path) + sys.path.extend(extra_paths) + sys.path.extend(orig_sys_path) diff --git a/reflex/constants/__init__.py b/reflex/constants/__init__.py index e816da0f7..f5946bf5e 100644 --- a/reflex/constants/__init__.py +++ b/reflex/constants/__init__.py @@ -1,6 +1,7 @@ """The constants package.""" from .base import ( + APP_HARNESS_FLAG, COOKIES, IS_LINUX, IS_MACOS, diff --git a/reflex/constants/base.py b/reflex/constants/base.py index 3266043c5..7fbcdf18a 100644 --- a/reflex/constants/base.py +++ b/reflex/constants/base.py @@ -27,7 +27,7 @@ class Dirs(SimpleNamespace): UPLOADED_FILES = "uploaded_files" # The name of the assets directory. APP_ASSETS = "assets" - # The name of the assets directory for external ressource (a subfolder of APP_ASSETS). + # The name of the assets directory for external resources (a subfolder of APP_ASSETS). EXTERNAL_APP_ASSETS = "external" # The name of the utils file. UTILS = "utils" @@ -52,7 +52,7 @@ class Dirs(SimpleNamespace): # The name of the postcss config file. POSTCSS_JS = "postcss.config.js" # The name of the states directory. - STATES = "states" + STATES = ".states" class Reflex(SimpleNamespace): @@ -75,6 +75,8 @@ class Reflex(SimpleNamespace): # If user sets REFLEX_DIR envroment variable use that instead. DIR = PlatformDirs(MODULE_NAME, False).user_data_path + LOGS_DIR = DIR / "logs" + # The root directory of the reflex library. ROOT_DIR = Path(__file__).parents[2] @@ -257,6 +259,7 @@ SESSION_STORAGE = "session_storage" # Testing variables. # Testing os env set by pytest when running a test case. PYTEST_CURRENT_TEST = "PYTEST_CURRENT_TEST" +APP_HARNESS_FLAG = "APP_HARNESS_FLAG" REFLEX_VAR_OPENING_TAG = "" REFLEX_VAR_CLOSING_TAG = "" diff --git a/reflex/constants/compiler.py b/reflex/constants/compiler.py index b7ffef161..9bc9978dc 100644 --- a/reflex/constants/compiler.py +++ b/reflex/constants/compiler.py @@ -1,10 +1,10 @@ """Compiler variables.""" +import dataclasses import enum from enum import Enum from types import SimpleNamespace -from reflex.base import Base from reflex.constants import Dirs from reflex.utils.imports import ImportVar @@ -28,6 +28,8 @@ class Ext(SimpleNamespace): ZIP = ".zip" # The extension for executable files on Windows. EXE = ".exe" + # The extension for markdown files. + MD = ".md" class CompileVars(SimpleNamespace): @@ -132,6 +134,13 @@ class Hooks(SimpleNamespace): } })""" + class HookPosition(enum.Enum): + """The position of the hook in the component.""" + + INTERNAL = "internal" + PRE_TRIGGER = "pre_trigger" + POST_TRIGGER = "post_trigger" + class MemoizationDisposition(enum.Enum): """The conditions under which a component should be memoized.""" @@ -142,7 +151,8 @@ class MemoizationDisposition(enum.Enum): NEVER = "never" -class MemoizationMode(Base): +@dataclasses.dataclass(frozen=True) +class MemoizationMode: """The mode for memoizing a Component.""" # The conditions under which the component should be memoized. diff --git a/reflex/constants/config.py b/reflex/constants/config.py index 234ecbd84..a49216c00 100644 --- a/reflex/constants/config.py +++ b/reflex/constants/config.py @@ -26,9 +26,11 @@ class Expiration(SimpleNamespace): # Token expiration time in seconds TOKEN = 60 * 60 # Maximum time in milliseconds that a state can be locked for exclusive access. - LOCK = 1000 + LOCK = 10000 # The PING timeout PING = 120 + # The maximum time in milliseconds to hold a lock before throwing a warning. + LOCK_WARNING_THRESHOLD = 1000 class GitIgnore(SimpleNamespace): @@ -37,7 +39,14 @@ class GitIgnore(SimpleNamespace): # The gitignore file. FILE = Path(".gitignore") # Files to gitignore. - DEFAULTS = {Dirs.WEB, "*.db", "__pycache__/", "*.py[cod]", "assets/external/"} + DEFAULTS = { + Dirs.WEB, + Dirs.STATES, + "*.db", + "__pycache__/", + "*.py[cod]", + "assets/external/", + } class RequirementsTxt(SimpleNamespace): diff --git a/reflex/constants/custom_components.py b/reflex/constants/custom_components.py index d879a01f2..a499327b1 100644 --- a/reflex/constants/custom_components.py +++ b/reflex/constants/custom_components.py @@ -10,7 +10,7 @@ class CustomComponents(SimpleNamespace): """Constants for the custom components.""" # The name of the custom components source directory. - SRC_DIR = "custom_components" + SRC_DIR = Path("custom_components") # The name of the custom components pyproject.toml file. PYPROJECT_TOML = Path("pyproject.toml") # The name of the custom components package README file. diff --git a/reflex/constants/installer.py b/reflex/constants/installer.py index 0b45586dd..0a89240b3 100644 --- a/reflex/constants/installer.py +++ b/reflex/constants/installer.py @@ -37,10 +37,10 @@ class Bun(SimpleNamespace): """Bun constants.""" # The Bun version. - VERSION = "1.1.29" + VERSION = "1.2.0" # Min Bun Version - MIN_VERSION = "0.7.0" + MIN_VERSION = "1.1.0" # URL to bun install script. INSTALL_URL = "https://raw.githubusercontent.com/reflex-dev/reflex/main/scripts/bun_install.sh" @@ -178,21 +178,21 @@ class PackageJson(SimpleNamespace): PATH = "package.json" DEPENDENCIES = { - "@babel/standalone": "7.26.0", - "@emotion/react": "11.13.3", - "axios": "1.7.7", + "@babel/standalone": "7.26.6", + "@emotion/react": "11.14.0", + "axios": "1.7.9", "json5": "2.2.3", - "next": "14.2.16", + "next": "15.1.6", "next-sitemap": "4.2.3", - "next-themes": "0.4.3", + "next-themes": "0.4.4", "react": "18.3.1", "react-dom": "18.3.1", - "react-focus-lock": "2.13.2", + "react-focus-lock": "2.13.5", "socket.io-client": "4.8.1", "universal-cookie": "7.2.2", } DEV_DEPENDENCIES = { "autoprefixer": "10.4.20", - "postcss": "8.4.49", + "postcss": "8.5.1", "postcss-import": "16.1.0", } diff --git a/reflex/constants/route.py b/reflex/constants/route.py index 2af2f33c6..ab00fab15 100644 --- a/reflex/constants/route.py +++ b/reflex/constants/route.py @@ -31,7 +31,7 @@ class RouteVar(SimpleNamespace): # This subset of router_data is included in chained on_load events. -ROUTER_DATA_INCLUDE = set((RouteVar.PATH, RouteVar.ORIGIN, RouteVar.QUERY)) +ROUTER_DATA_INCLUDE = {RouteVar.PATH, RouteVar.ORIGIN, RouteVar.QUERY} class RouteRegex(SimpleNamespace): diff --git a/reflex/constants/style.py b/reflex/constants/style.py index a1d30bcca..5b31ce9b3 100644 --- a/reflex/constants/style.py +++ b/reflex/constants/style.py @@ -7,7 +7,7 @@ class Tailwind(SimpleNamespace): """Tailwind constants.""" # The Tailwindcss version - VERSION = "tailwindcss@3.4.15" + VERSION = "tailwindcss@3.4.17" # The Tailwind config. CONFIG = "tailwind.config.js" # Default Tailwind content paths diff --git a/reflex/custom_components/custom_components.py b/reflex/custom_components/custom_components.py index 41808d60a..a54004803 100644 --- a/reflex/custom_components/custom_components.py +++ b/reflex/custom_components/custom_components.py @@ -83,7 +83,7 @@ def _get_package_config(exit_on_fail: bool = True) -> dict: The package configuration. Raises: - Exit: If the pyproject.toml file is not found. + Exit: If the pyproject.toml file is not found and exit_on_fail is True. """ pyproject = Path(CustomComponents.PYPROJECT_TOML) try: @@ -150,27 +150,27 @@ def _populate_demo_app(name_variants: NameVariants): from reflex.compiler import templates from reflex.reflex import _init - demo_app_dir = name_variants.demo_app_dir + demo_app_dir = Path(name_variants.demo_app_dir) demo_app_name = name_variants.demo_app_name - console.info(f"Creating app for testing: {demo_app_dir}") + console.info(f"Creating app for testing: {demo_app_dir!s}") - os.makedirs(demo_app_dir) + demo_app_dir.mkdir(exist_ok=True) with set_directory(demo_app_dir): # We start with the blank template as basis. _init(name=demo_app_name, template=constants.Templates.DEFAULT) # Then overwrite the app source file with the one we want for testing custom components. # This source file is rendered using jinja template file. - with open(f"{demo_app_name}/{demo_app_name}.py", "w") as f: - f.write( - templates.CUSTOM_COMPONENTS_DEMO_APP.render( - custom_component_module_dir=name_variants.custom_component_module_dir, - module_name=name_variants.module_name, - ) + demo_file = Path(f"{demo_app_name}/{demo_app_name}.py") + demo_file.write_text( + templates.CUSTOM_COMPONENTS_DEMO_APP.render( + custom_component_module_dir=name_variants.custom_component_module_dir, + module_name=name_variants.module_name, ) + ) # Append the custom component package to the requirements.txt file. - with open(f"{constants.RequirementsTxt.FILE}", "a") as f: + with Path(f"{constants.RequirementsTxt.FILE}").open(mode="a") as f: f.write(f"{name_variants.package_name}\n") @@ -296,13 +296,14 @@ def _populate_custom_component_project(name_variants: NameVariants): ) console.info( - f"Initializing the component directory: {CustomComponents.SRC_DIR}/{name_variants.custom_component_module_dir}" + f"Initializing the component directory: {CustomComponents.SRC_DIR / name_variants.custom_component_module_dir}" ) - os.makedirs(CustomComponents.SRC_DIR) + CustomComponents.SRC_DIR.mkdir(exist_ok=True) with set_directory(CustomComponents.SRC_DIR): - os.makedirs(name_variants.custom_component_module_dir) + module_dir = Path(name_variants.custom_component_module_dir) + module_dir.mkdir(exist_ok=True, parents=True) _write_source_and_init_py( - custom_component_src_dir=name_variants.custom_component_module_dir, + custom_component_src_dir=module_dir, component_class_name=name_variants.component_class_name, module_name=name_variants.module_name, ) @@ -420,12 +421,13 @@ def _run_commands_in_subprocess(cmds: list[str]) -> bool: console.debug(f"Running command: {' '.join(cmds)}") try: result = subprocess.run(cmds, capture_output=True, text=True, check=True) - console.debug(result.stdout) - return True except subprocess.CalledProcessError as cpe: console.error(cpe.stdout) console.error(cpe.stderr) return False + else: + console.debug(result.stdout) + return True def _make_pyi_files(): @@ -770,7 +772,7 @@ def _validate_project_info(): pyproject_toml = _get_package_config() project = pyproject_toml["project"] console.print( - f'Double check the information before publishing: {project["name"]} version {project["version"]}' + f"Double check the information before publishing: {project['name']} version {project['version']}" ) console.print("Update or enter to keep the current information.") @@ -782,7 +784,7 @@ def _validate_project_info(): author["name"] = console.ask("Author Name", default=author.get("name", "")) author["email"] = console.ask("Author Email", default=author.get("email", "")) - console.print(f'Current keywords are: {project.get("keywords") or []}') + console.print(f"Current keywords are: {project.get('keywords') or []}") keyword_action = console.ask( "Keep, replace or append?", choices=["k", "r", "a"], default="k" ) @@ -814,7 +816,7 @@ def _validate_project_info(): ) pyproject_toml["project"] = project try: - with open(CustomComponents.PYPROJECT_TOML, "w") as f: + with CustomComponents.PYPROJECT_TOML.open("w") as f: tomlkit.dump(pyproject_toml, f) except (OSError, TOMLKitError) as ex: console.error(f"Unable to write to pyproject.toml due to {ex}") @@ -922,19 +924,19 @@ def _validate_url_with_protocol_prefix(url: str | None) -> bool: def _get_file_from_prompt_in_loop() -> Tuple[bytes, str] | None: image_file = file_extension = None while image_file is None: - image_filepath = console.ask( - "Upload a preview image of your demo app (enter to skip)" + image_filepath = Path( + console.ask("Upload a preview image of your demo app (enter to skip)") # pyright: ignore [reportArgumentType] ) if not image_filepath: break - file_extension = image_filepath.split(".")[-1] + file_extension = image_filepath.suffix try: - with open(image_filepath, "rb") as f: - image_file = f.read() - return image_file, file_extension + image_file = image_filepath.read_bytes() except OSError as ose: console.error(f"Unable to read the {file_extension} file due to {ose}") raise typer.Exit(code=1) from ose + else: + return image_file, file_extension console.debug(f"File extension detected: {file_extension}") return None diff --git a/reflex/event.py b/reflex/event.py index 3022de556..f35e88389 100644 --- a/reflex/event.py +++ b/reflex/event.py @@ -4,7 +4,6 @@ from __future__ import annotations import dataclasses import inspect -import sys import types import urllib.parse from base64 import b64encode @@ -37,9 +36,14 @@ from typing_extensions import ( ) from reflex import constants +from reflex.constants.compiler import CompileVars, Hooks, Imports from reflex.constants.state import FRONTEND_EVENT_STATE from reflex.utils import console, format -from reflex.utils.exceptions import EventFnArgMismatch, EventHandlerArgTypeMismatch +from reflex.utils.exceptions import ( + EventFnArgMismatchError, + EventHandlerArgTypeMismatchError, + MissingAnnotationError, +) from reflex.utils.types import ArgsSpec, GenericType, typehint_issubclass from reflex.vars import VarData from reflex.vars.base import LiteralVar, Var @@ -90,35 +94,11 @@ class Event: return f"{self.token}_{substate}" +_EVENT_FIELDS: set[str] = {f.name for f in dataclasses.fields(Event)} + BACKGROUND_TASK_MARKER = "_reflex_background_task" -def background(fn, *, __internal_reflex_call: bool = False): - """Decorator to mark event handler as running in the background. - - Args: - fn: The function to decorate. - - Returns: - The same function, but with a marker set. - - - Raises: - TypeError: If the function is not a coroutine function or async generator. - """ - if not __internal_reflex_call: - console.deprecate( - "background-decorator", - "Use `rx.event(background=True)` instead.", - "0.6.5", - "0.7.0", - ) - if not inspect.iscoroutinefunction(fn) and not inspect.isasyncgenfunction(fn): - raise TypeError("Background task must be async function or generator.") - setattr(fn, BACKGROUND_TASK_MARKER, True) - return fn - - @dataclasses.dataclass( init=True, frozen=True, @@ -263,7 +243,7 @@ class EventHandler(EventActionsMixin): raise EventHandlerTypeError( f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}." ) from e - payload = tuple(zip(fn_args, values)) + payload = tuple(zip(fn_args, values, strict=False)) # Return the event spec. return EventSpec( @@ -283,7 +263,7 @@ class EventSpec(EventActionsMixin): """ # The event handler. - handler: EventHandler = dataclasses.field(default=None) # type: ignore + handler: EventHandler = dataclasses.field(default=None) # pyright: ignore [reportAssignmentType] # The handler on the client to process event. client_handler_name: str = dataclasses.field(default="") @@ -296,7 +276,7 @@ class EventSpec(EventActionsMixin): handler: EventHandler, event_actions: Dict[str, Union[bool, int]] | None = None, client_handler_name: str = "", - args: Tuple[Tuple[Var, Var], ...] = tuple(), + args: Tuple[Tuple[Var, Var], ...] = (), ): """Initialize an EventSpec. @@ -311,7 +291,7 @@ class EventSpec(EventActionsMixin): object.__setattr__(self, "event_actions", event_actions) object.__setattr__(self, "handler", handler) object.__setattr__(self, "client_handler_name", client_handler_name) - object.__setattr__(self, "args", args or tuple()) + object.__setattr__(self, "args", args or ()) def with_args(self, args: Tuple[Tuple[Var, Var], ...]) -> EventSpec: """Copy the event spec, with updated args. @@ -349,14 +329,15 @@ class EventSpec(EventActionsMixin): # Construct the payload. values = [] - for arg in args: - try: - values.append(LiteralVar.create(arg)) - except TypeError as e: - raise EventHandlerTypeError( - f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}." - ) from e - new_payload = tuple(zip(fn_args, values)) + arg = None + try: + for arg in args: + values.append(LiteralVar.create(value=arg)) # noqa: PERF401, RUF100 + except TypeError as e: + raise EventHandlerTypeError( + f"Arguments to event handlers must be Vars or JSON-serializable. Got {arg} of type {type(arg)}." + ) from e + new_payload = tuple(zip(fn_args, values, strict=False)) return self.with_args(self.args + new_payload) @@ -429,6 +410,101 @@ class EventChain(EventActionsMixin): invocation: Optional[Var] = dataclasses.field(default=None) + @classmethod + def create( + cls, + value: EventType, + args_spec: ArgsSpec | Sequence[ArgsSpec], + key: Optional[str] = None, + **event_chain_kwargs, + ) -> Union[EventChain, Var]: + """Create an event chain from a variety of input types. + + Args: + value: The value to create the event chain from. + args_spec: The args_spec of the event trigger being bound. + key: The key of the event trigger being bound. + **event_chain_kwargs: Additional kwargs to pass to the EventChain constructor. + + Returns: + The event chain. + + Raises: + ValueError: If the value is not a valid event chain. + """ + # If it's an event chain var, return it. + if isinstance(value, Var): + if isinstance(value, EventChainVar): + return value + elif isinstance(value, EventVar): + value = [value] + elif issubclass(value._var_type, (EventChain, EventSpec)): + return cls.create( + value=value.guess_type(), + args_spec=args_spec, + key=key, + **event_chain_kwargs, + ) + else: + raise ValueError( + f"Invalid event chain: {value!s} of type {value._var_type}" + ) + elif isinstance(value, EventChain): + # Trust that the caller knows what they're doing passing an EventChain directly + return value + + # If the input is a single event handler, wrap it in a list. + if isinstance(value, (EventHandler, EventSpec)): + value = [value] + + # If the input is a list of event handlers, create an event chain. + if isinstance(value, List): + events: List[Union[EventSpec, EventVar]] = [] + for v in value: + if isinstance(v, (EventHandler, EventSpec)): + # Call the event handler to get the event. + events.append(call_event_handler(v, args_spec, key=key)) + elif isinstance(v, Callable): + # Call the lambda to get the event chain. + result = call_event_fn(v, args_spec, key=key) + if isinstance(result, Var): + raise ValueError( + f"Invalid event chain: {v}. Cannot use a Var-returning " + "lambda inside an EventChain list." + ) + events.extend(result) + elif isinstance(v, EventVar): + events.append(v) + else: + raise ValueError(f"Invalid event: {v}") + + # If the input is a callable, create an event chain. + elif isinstance(value, Callable): + result = call_event_fn(value, args_spec, key=key) + if isinstance(result, Var): + # Recursively call this function if the lambda returned an EventChain Var. + return cls.create( + value=result, args_spec=args_spec, key=key, **event_chain_kwargs + ) + events = [*result] + + # Otherwise, raise an error. + else: + raise ValueError(f"Invalid event chain: {value}") + + # Add args to the event specs if necessary. + events = [ + (e.with_args(get_handler_args(e)) if isinstance(e, EventSpec) else e) + for e in events + ] + + # Return the event chain. + return cls( + events=events, + args_spec=args_spec, + **event_chain_kwargs, + ) + @dataclasses.dataclass( init=True, @@ -458,13 +534,13 @@ class JavasciptKeyboardEvent: """Interface for a Javascript KeyboardEvent https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.""" key: str = "" - altKey: bool = False - ctrlKey: bool = False - metaKey: bool = False - shiftKey: bool = False + altKey: bool = False # noqa: N815 + ctrlKey: bool = False # noqa: N815 + metaKey: bool = False # noqa: N815 + shiftKey: bool = False # noqa: N815 -def input_event(e: Var[JavascriptInputEvent]) -> Tuple[Var[str]]: +def input_event(e: ObjectVar[JavascriptInputEvent]) -> Tuple[Var[str]]: """Get the value from an input event. Args: @@ -485,7 +561,9 @@ class KeyInputInfo(TypedDict): shift_key: bool -def key_event(e: Var[JavasciptKeyboardEvent]) -> Tuple[Var[str], Var[KeyInputInfo]]: +def key_event( + e: ObjectVar[JavasciptKeyboardEvent], +) -> Tuple[Var[str], Var[KeyInputInfo]]: """Get the key from a keyboard event. Args: @@ -495,7 +573,7 @@ def key_event(e: Var[JavasciptKeyboardEvent]) -> Tuple[Var[str], Var[KeyInputInf The key from the keyboard event. """ return ( - e.key, + e.key.to(str), Var.create( { "alt_key": e.altKey, @@ -503,7 +581,7 @@ def key_event(e: Var[JavasciptKeyboardEvent]) -> Tuple[Var[str], Var[KeyInputInf "meta_key": e.metaKey, "shift_key": e.shiftKey, }, - ), + ).to(KeyInputInfo), ) @@ -513,7 +591,7 @@ def no_args_event_spec() -> Tuple[()]: Returns: An empty tuple. """ - return tuple() # type: ignore + return () # These chains can be used for their side effects when no other events are desired. @@ -541,9 +619,9 @@ class IdentityEventReturn(Generic[T], Protocol): @overload -def passthrough_event_spec( +def passthrough_event_spec( # pyright: ignore [reportOverlappingOverload] event_type: Type[T], / -) -> Callable[[Var[T]], Tuple[Var[T]]]: ... # type: ignore +) -> Callable[[Var[T]], Tuple[Var[T]]]: ... @overload @@ -556,7 +634,7 @@ def passthrough_event_spec( def passthrough_event_spec(*event_types: Type[T]) -> IdentityEventReturn[T]: ... -def passthrough_event_spec(*event_types: Type[T]) -> IdentityEventReturn[T]: # type: ignore +def passthrough_event_spec(*event_types: Type[T]) -> IdentityEventReturn[T]: # pyright: ignore [reportInconsistentOverload] """A helper function that returns the input event as output. Args: @@ -570,9 +648,9 @@ def passthrough_event_spec(*event_types: Type[T]) -> IdentityEventReturn[T]: # return values inner_type = tuple(Var[event_type] for event_type in event_types) - return_annotation = Tuple[inner_type] # type: ignore + return_annotation = Tuple[inner_type] - inner.__signature__ = inspect.signature(inner).replace( # type: ignore + inner.__signature__ = inspect.signature(inner).replace( # pyright: ignore [reportFunctionMemberAccess] parameters=[ inspect.Parameter( f"ev_{i}", @@ -654,7 +732,7 @@ class FileUpload: # Call the lambda to get the event chain. events = call_event_fn( on_upload_progress, self.on_upload_progress_args_spec - ) # type: ignore + ) else: raise ValueError(f"{on_upload_progress} is not a valid event handler.") if isinstance(events, Var): @@ -701,7 +779,7 @@ def server_side(name: str, sig: inspect.Signature, **kwargs) -> EventSpec: return None fn.__qualname__ = name - fn.__signature__ = sig + fn.__signature__ = sig # pyright: ignore [reportFunctionMemberAccess] return EventSpec( handler=EventHandler(fn=fn, state_full_name=FRONTEND_EVENT_STATE), args=tuple( @@ -716,14 +794,14 @@ def server_side(name: str, sig: inspect.Signature, **kwargs) -> EventSpec: def redirect( path: str | Var[str], - external: Optional[bool] = False, - replace: Optional[bool] = False, + is_external: bool = False, + replace: bool = False, ) -> EventSpec: """Redirect to a new path. Args: path: The path to redirect to. - external: Whether to open in new tab or not. + is_external: Whether to open in new tab or not. replace: If True, the current page will not create a new history entry. Returns: @@ -733,7 +811,7 @@ def redirect( "_redirect", get_fn_signature(redirect), path=path, - external=external, + external=is_external, replace=replace, ) @@ -974,13 +1052,13 @@ def download( is_data_url = (data.js_type() == "string") & ( data.to(str).startswith("data:") - ) # type: ignore + ) # If it's a data: URI, use it as is, otherwise convert the Var to JSON in a data: URI. - url = cond( # type: ignore + url = cond( is_data_url, data.to(str), - "data:text/plain," + data.to_string(), # type: ignore + "data:text/plain," + data.to_string(), ) elif isinstance(data, bytes): # Caller provided bytes, so base64 encode it as a data: URI. @@ -999,7 +1077,8 @@ def download( ) -def _callback_arg_spec(eval_result): +# This function seems unused. Check if we still need it. If not, remove in 0.7.0 +def _callback_arg_spec(eval_result: Any): """ArgSpec for call_script callback function. Args: @@ -1063,7 +1142,7 @@ def call_function( Returns: EventSpec: An event that will execute the client side javascript. """ - callback_kwargs = {} + callback_kwargs = {"callback": None} if callback is not None: callback_kwargs = { "callback": format.format_queue_events( @@ -1101,12 +1180,10 @@ def run_script( Var(javascript_code) if isinstance(javascript_code, str) else javascript_code ) - return call_function( - ArgsFunctionOperation.create(tuple(), javascript_code), callback - ) + return call_function(ArgsFunctionOperation.create((), javascript_code), callback) -def get_event(state, event): +def get_event(state: BaseState, event: str): """Get the event from the given state. Args: @@ -1119,7 +1196,7 @@ def get_event(state, event): return f"{state.get_name()}.{event}" -def get_hydrate_event(state) -> str: +def get_hydrate_event(state: BaseState) -> str: """Get the name of the hydrate event for the state. Args: @@ -1147,13 +1224,16 @@ def call_event_handler( event_spec: The lambda that define the argument(s) to pass to the event handler. key: The key to pass to the event handler. + Raises: + EventHandlerArgTypeMismatchError: If the event handler arguments do not match the event spec. #noqa: DAR402 + TypeError: If the event handler arguments are invalid. + Returns: The event spec from calling the event handler. - # noqa: DAR401 failure - + #noqa: DAR401 """ - event_spec_args = parse_args_spec(event_spec) # type: ignore + event_spec_args = parse_args_spec(event_spec) if isinstance(event_callback, EventSpec): check_fn_match_arg_spec( @@ -1188,10 +1268,15 @@ def call_event_handler( ), ) ) + type_match_found: dict[str, bool] = {} + delayed_exceptions: list[EventHandlerArgTypeMismatchError] = [] + + try: + type_hints_of_provided_callback = get_type_hints(event_callback.fn) + except NameError: + type_hints_of_provided_callback = {} if event_spec_return_types: - failures = [] - event_callback_spec = inspect.getfullargspec(event_callback.fn) for event_spec_index, event_spec_return_type in enumerate( @@ -1203,43 +1288,35 @@ def call_event_handler( arg if get_origin(arg) is not Var else get_args(arg)[0] for arg in args ] - try: - type_hints_of_provided_callback = get_type_hints(event_callback.fn) - except NameError: - type_hints_of_provided_callback = {} - - failed_type_check = False - # check that args of event handler are matching the spec if type hints are provided for i, arg in enumerate(event_callback_spec.args[1:]): if arg not in type_hints_of_provided_callback: continue + type_match_found.setdefault(arg, False) + try: compare_result = typehint_issubclass( args_types_without_vars[i], type_hints_of_provided_callback[arg] ) - except TypeError: - # TODO: In 0.7.0, remove this block and raise the exception - # raise TypeError( - # f"Could not compare types {args_types_without_vars[i]} and {type_hints_of_provided_callback[arg]} for argument {arg} of {event_handler.fn.__qualname__} provided for {key}." - # ) from e - console.warn( + except TypeError as te: + raise TypeError( f"Could not compare types {args_types_without_vars[i]} and {type_hints_of_provided_callback[arg]} for argument {arg} of {event_callback.fn.__qualname__} provided for {key}." - ) - compare_result = False + ) from te if compare_result: + type_match_found[arg] = True continue else: - failure = EventHandlerArgTypeMismatch( - f"Event handler {key} expects {args_types_without_vars[i]} for argument {arg} but got {type_hints_of_provided_callback[arg]} as annotated in {event_callback.fn.__qualname__} instead." + type_match_found[arg] = False + delayed_exceptions.append( + EventHandlerArgTypeMismatchError( + f"Event handler {key} expects {args_types_without_vars[i]} for argument {arg} but got {type_hints_of_provided_callback[arg]} as annotated in {event_callback.fn.__qualname__} instead." + ) ) - failures.append(failure) - failed_type_check = True - break - if not failed_type_check: + if all(type_match_found.values()): + delayed_exceptions.clear() if event_spec_index: args = get_args(event_spec_return_types[0]) @@ -1261,17 +1338,12 @@ def call_event_handler( f"Event handler {key} expects ({expect_string}) -> () but got ({given_string}) -> () as annotated in {event_callback.fn.__qualname__} instead. " f"This may lead to unexpected behavior but is intentionally ignored for {key}." ) - return event_callback(*event_spec_args) + break - if failures: - console.deprecate( - "Mismatched event handler argument types", - "\n".join([str(f) for f in failures]), - "0.6.5", - "0.7.0", - ) + if delayed_exceptions: + raise delayed_exceptions[0] - return event_callback(*event_spec_args) # type: ignore + return event_callback(*event_spec_args) def unwrap_var_annotation(annotation: GenericType): @@ -1283,31 +1355,31 @@ def unwrap_var_annotation(annotation: GenericType): Returns: The unwrapped annotation. """ - if get_origin(annotation) is Var and (args := get_args(annotation)): + if get_origin(annotation) in (Var, ObjectVar) and (args := get_args(annotation)): return args[0] return annotation -def resolve_annotation(annotations: dict[str, Any], arg_name: str): +def resolve_annotation(annotations: dict[str, Any], arg_name: str, spec: ArgsSpec): """Resolve the annotation for the given argument name. Args: annotations: The annotations. arg_name: The argument name. + spec: The specs which the annotations come from. + + Raises: + MissingAnnotationError: If the annotation is missing for non-lambda methods. Returns: The resolved annotation. """ annotation = annotations.get(arg_name) if annotation is None: - console.deprecate( - feature_name="Unannotated event handler arguments", - reason="Provide type annotations for event handler arguments.", - deprecation_version="0.6.3", - removal_version="0.7.0", - ) - # Allow arbitrary attribute access two levels deep until removed. - return Dict[str, dict] + if not isinstance(spec, types.LambdaType): + raise MissingAnnotationError(var_name=arg_name) + else: + return dict[str, dict] return annotation @@ -1329,7 +1401,13 @@ def parse_args_spec(arg_spec: ArgsSpec | Sequence[ArgsSpec]): arg_spec( *[ Var(f"_{l_arg}").to( - unwrap_var_annotation(resolve_annotation(annotations, l_arg)) + unwrap_var_annotation( + resolve_annotation( + annotations, + l_arg, + spec=arg_spec, + ) + ) ) for l_arg in spec.args ] @@ -1345,7 +1423,7 @@ def check_fn_match_arg_spec( func_name: str | None = None, ): """Ensures that the function signature matches the passed argument specification - or raises an EventFnArgMismatch if they do not. + or raises an EventFnArgMismatchError if they do not. Args: user_func: The function to be validated. @@ -1355,7 +1433,7 @@ def check_fn_match_arg_spec( func_name: The name of the function to be validated. Raises: - EventFnArgMismatch: Raised if the number of mandatory arguments do not match + EventFnArgMismatchError: Raised if the number of mandatory arguments do not match """ user_args = inspect.getfullargspec(user_func).args # Drop the first argument if it's a bound method @@ -1371,7 +1449,7 @@ def check_fn_match_arg_spec( number_of_event_args = len(parsed_event_args) if number_of_user_args - number_of_user_default_args > number_of_event_args: - raise EventFnArgMismatch( + raise EventFnArgMismatchError( f"Event {key} only provides {number_of_event_args} arguments, but " f"{func_name or user_func} requires at least {number_of_user_args - number_of_user_default_args} " "arguments to be passed to the event handler.\n" @@ -1455,11 +1533,11 @@ def get_handler_args( """ args = inspect.getfullargspec(event_spec.handler.fn).args - return event_spec.args if len(args) > 1 else tuple() + return event_spec.args if len(args) > 1 else () def fix_events( - events: list[EventHandler | EventSpec] | None, + events: list[EventSpec | EventHandler] | None, token: str, router_data: dict[str, Any] | None = None, ) -> list[Event]: @@ -1499,7 +1577,7 @@ def fix_events( if not isinstance(e, EventSpec): raise ValueError(f"Unexpected event type, {type(e)}.") name = format.format_event_handler(e.handler) - payload = {k._js_expr: v._decode() for k, v in e.args} # type: ignore + payload = {k._js_expr: v._decode() for k, v in e.args} # Filter router_data to reduce payload size event_router_data = { @@ -1543,12 +1621,12 @@ class EventVar(ObjectVar, python_types=EventSpec): @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class LiteralEventVar(VarOperationCall, LiteralVar, EventVar): """A literal event var.""" - _var_value: EventSpec = dataclasses.field(default=None) # type: ignore + _var_value: EventSpec = dataclasses.field(default=None) # pyright: ignore [reportAssignmentType] def __hash__(self) -> int: """Get the hash of the var. @@ -1556,7 +1634,7 @@ class LiteralEventVar(VarOperationCall, LiteralVar, EventVar): Returns: The hash of the var. """ - return hash((self.__class__.__name__, self._js_expr)) + return hash((type(self).__name__, self._js_expr)) @classmethod def create( @@ -1604,7 +1682,7 @@ class EventChainVar(BuilderFunctionVar, python_types=EventChain): @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) # Note: LiteralVar is second in the inheritance list allowing it act like a # CachedVarOperation (ArgsFunctionOperation) and get the _js_expr from the @@ -1612,7 +1690,7 @@ class EventChainVar(BuilderFunctionVar, python_types=EventChain): class LiteralEventChainVar(ArgsFunctionOperationBuilder, LiteralVar, EventChainVar): """A literal event chain var.""" - _var_value: EventChain = dataclasses.field(default=None) # type: ignore + _var_value: EventChain = dataclasses.field(default=None) # pyright: ignore [reportAssignmentType] def __hash__(self) -> int: """Get the hash of the var. @@ -1620,7 +1698,7 @@ class LiteralEventChainVar(ArgsFunctionOperationBuilder, LiteralVar, EventChainV Returns: The hash of the var. """ - return hash((self.__class__.__name__, self._js_expr)) + return hash((type(self).__name__, self._js_expr)) @classmethod def create( @@ -1636,13 +1714,16 @@ class LiteralEventChainVar(ArgsFunctionOperationBuilder, LiteralVar, EventChainV Returns: The created LiteralEventChainVar instance. + + Raises: + ValueError: If the invocation is not a FunctionVar. """ arg_spec = ( value.args_spec[0] if isinstance(value.args_spec, Sequence) else value.args_spec ) - sig = inspect.signature(arg_spec) # type: ignore + sig = inspect.signature(arg_spec) # pyright: ignore [reportArgumentType] if sig.parameters: arg_def = tuple((f"_{p}" for p in sig.parameters)) arg_def_expr = LiteralVar.create([Var(_js_expr=arg) for arg in arg_def]) @@ -1653,10 +1734,21 @@ class LiteralEventChainVar(ArgsFunctionOperationBuilder, LiteralVar, EventChainV arg_def_expr = Var(_js_expr="args") if value.invocation is None: - invocation = FunctionStringVar.create("addEvents") + invocation = FunctionStringVar.create( + CompileVars.ADD_EVENTS, + _var_data=VarData( + imports=Imports.EVENTS, + hooks={Hooks.EVENTS: None}, + ), + ) else: invocation = value.invocation + if invocation is not None and not isinstance(invocation, FunctionVar): + raise ValueError( + f"EventChain invocation must be a FunctionVar, got {invocation!s} of type {invocation._var_type!s}." + ) + return cls( _js_expr="", _var_type=EventChain, @@ -1680,8 +1772,6 @@ V3 = TypeVar("V3") V4 = TypeVar("V4") V5 = TypeVar("V5") -background_event_decorator = background - class EventCallback(Generic[P, T]): """A descriptor that wraps a function to be used as an event.""" @@ -1746,7 +1836,7 @@ class EventCallback(Generic[P, T]): value4: V4 | Var[V4], ) -> EventCallback[Q, T]: ... - def __call__(self, *values) -> EventCallback: # type: ignore + def __call__(self, *values) -> EventCallback: # pyright: ignore [reportInconsistentOverload] """Call the function with the values. Args: @@ -1755,17 +1845,17 @@ class EventCallback(Generic[P, T]): Returns: The function with the values. """ - return self.func(*values) # type: ignore + return self.func(*values) # pyright: ignore [reportCallIssue, reportReturnType] @overload def __get__( - self: EventCallback[P, T], instance: None, owner + self: EventCallback[P, T], instance: None, owner: Any ) -> EventCallback[P, T]: ... @overload - def __get__(self, instance, owner) -> Callable[P, T]: ... + def __get__(self, instance: Any, owner: Any) -> Callable[P, T]: ... - def __get__(self, instance, owner) -> Callable: # type: ignore + def __get__(self, instance: Any, owner: Any) -> Callable: """Get the function with the instance bound to it. Args: @@ -1776,9 +1866,9 @@ class EventCallback(Generic[P, T]): The function with the instance bound to it """ if instance is None: - return self.func # type: ignore + return self.func - return partial(self.func, instance) # type: ignore + return partial(self.func, instance) G = ParamSpec("G") @@ -1829,7 +1919,7 @@ class EventNamespace(types.SimpleNamespace): @staticmethod def __call__( func: None = None, *, background: bool | None = None - ) -> Callable[[Callable[Concatenate[BASE_STATE, P], T]], EventCallback[P, T]]: ... + ) -> Callable[[Callable[Concatenate[BASE_STATE, P], T]], EventCallback[P, T]]: ... # pyright: ignore [reportInvalidTypeVarUse] @overload @staticmethod @@ -1854,6 +1944,9 @@ class EventNamespace(types.SimpleNamespace): func: The function to wrap. background: Whether the event should be run in the background. Defaults to False. + Raises: + TypeError: If background is True and the function is not a coroutine or async generator. # noqa: DAR402 + Returns: The wrapped function. """ @@ -1862,8 +1955,14 @@ class EventNamespace(types.SimpleNamespace): func: Callable[Concatenate[BASE_STATE, P], T], ) -> EventCallback[P, T]: if background is True: - return background_event_decorator(func, __internal_reflex_call=True) # type: ignore - return func # type: ignore + if not inspect.iscoroutinefunction( + func + ) and not inspect.isasyncgenfunction(func): + raise TypeError( + "Background task must be async function or generator." + ) + setattr(func, BACKGROUND_TASK_MARKER, True) + return func # pyright: ignore [reportReturnType] if func is not None: return wrapper(func) diff --git a/reflex/experimental/__init__.py b/reflex/experimental/__init__.py index 164790fe5..1a198f35a 100644 --- a/reflex/experimental/__init__.py +++ b/reflex/experimental/__init__.py @@ -9,7 +9,6 @@ from reflex.components.sonner.toast import toast as toast from ..utils.console import warn from . import hooks as hooks -from .assets import asset as asset from .client_state import ClientStateVar as ClientStateVar from .layout import layout as layout from .misc import run_in_thread as run_in_thread @@ -62,7 +61,6 @@ class ExperimentalNamespace(SimpleNamespace): _x = ExperimentalNamespace( - asset=asset, client_state=ClientStateVar.create, hooks=hooks, layout=layout, diff --git a/reflex/experimental/assets.py b/reflex/experimental/assets.py deleted file mode 100644 index e9be19aaf..000000000 --- a/reflex/experimental/assets.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Helper functions for adding assets to the app.""" - -from typing import Optional - -from reflex import assets -from reflex.utils import console - - -def asset(relative_filename: str, subfolder: Optional[str] = None) -> str: - """DEPRECATED: use `rx.asset` with `shared=True` instead. - - Add an asset to the app. - Place the file next to your including python file. - Copies the file to the app's external assets directory. - - Example: - ```python - rx.script(src=rx._x.asset("my_custom_javascript.js")) - rx.image(src=rx._x.asset("test_image.png","subfolder")) - ``` - - Args: - relative_filename: The relative filename of the asset. - subfolder: The directory to place the asset in. - - Returns: - The relative URL to the copied asset. - """ - console.deprecate( - feature_name="rx._x.asset", - reason="Use `rx.asset` with `shared=True` instead of `rx._x.asset`.", - deprecation_version="0.6.6", - removal_version="0.7.0", - ) - return assets.asset( - relative_filename, shared=True, subfolder=subfolder, _stack_level=2 - ) diff --git a/reflex/experimental/client_state.py b/reflex/experimental/client_state.py index 6e9f9958d..8138c2721 100644 --- a/reflex/experimental/client_state.py +++ b/reflex/experimental/client_state.py @@ -4,7 +4,6 @@ from __future__ import annotations import dataclasses import re -import sys from typing import Any, Callable, Union from reflex import constants @@ -12,7 +11,7 @@ from reflex.event import EventChain, EventHandler, EventSpec, run_script from reflex.utils.imports import ImportVar from reflex.vars import VarData, get_unique_variable_name from reflex.vars.base import LiteralVar, Var -from reflex.vars.function import FunctionVar +from reflex.vars.function import ArgsFunctionOperationBuilder, FunctionVar NoValue = object() @@ -34,10 +33,22 @@ def _client_state_ref(var_name: str) -> str: return f"refs['_client_state_{var_name}']" +def _client_state_ref_dict(var_name: str) -> str: + """Get the ref path for a ClientStateVar. + + Args: + var_name: The name of the variable. + + Returns: + An accessor for ClientStateVar ref as a string. + """ + return f"refs['_client_state_dict_{var_name}']" + + @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class ClientStateVar(Var): """A Var that exists on the client via useState.""" @@ -45,6 +56,7 @@ class ClientStateVar(Var): # Track the names of the getters and setters _setter_name: str = dataclasses.field(default="") _getter_name: str = dataclasses.field(default="") + _id_name: str = dataclasses.field(default="") # Whether to add the var and setter to the global `refs` object for use in any Component. _global_ref: bool = dataclasses.field(default=True) @@ -96,6 +108,7 @@ class ClientStateVar(Var): """ if var_name is None: var_name = get_unique_variable_name() + id_name = "id_" + get_unique_variable_name() if not isinstance(var_name, str): raise ValueError("var_name must be a string.") if default is NoValue: @@ -105,20 +118,55 @@ class ClientStateVar(Var): else: default_var = default setter_name = f"set{var_name.capitalize()}" - hooks = { + hooks: dict[str, VarData | None] = { + f"const {id_name} = useId()": None, f"const [{var_name}, {setter_name}] = useState({default_var!s})": None, } imports = { - "react": [ImportVar(tag="useState")], + "react": [ImportVar(tag="useState"), ImportVar(tag="useId")], } if global_ref: - hooks[f"{_client_state_ref(var_name)} = {var_name}"] = None - hooks[f"{_client_state_ref(setter_name)} = {setter_name}"] = None + arg_name = get_unique_variable_name() + func = ArgsFunctionOperationBuilder.create( + args_names=(arg_name,), + return_expr=Var("Array.prototype.forEach.call") + .to(FunctionVar) + .call( + ( + Var("Object.values") + .to(FunctionVar) + .call(Var(_client_state_ref_dict(setter_name))) + .to(list) + .to(list) + ) + + Var.create( + [ + Var( + f"(value) => {{ {_client_state_ref(var_name)} = value; }}" + ) + ] + ).to(list), + ArgsFunctionOperationBuilder.create( + args_names=("setter",), + return_expr=Var("setter").to(FunctionVar).call(Var(arg_name)), + ), + ), + ) + + hooks[f"{_client_state_ref(setter_name)} = {func!s}"] = None + hooks[f"{_client_state_ref(var_name)} ??= {var_name!s}"] = None + hooks[f"{_client_state_ref_dict(var_name)} ??= {{}}"] = None + hooks[f"{_client_state_ref_dict(setter_name)} ??= {{}}"] = None + hooks[f"{_client_state_ref_dict(var_name)}[{id_name}] = {var_name}"] = None + hooks[ + f"{_client_state_ref_dict(setter_name)}[{id_name}] = {setter_name}" + ] = None imports.update(_refs_import) return cls( _js_expr="", _setter_name=setter_name, _getter_name=var_name, + _id_name=id_name, _global_ref=global_ref, _var_type=default_var._var_type, _var_data=VarData.merge( @@ -144,16 +192,15 @@ class ClientStateVar(Var): return ( Var( _js_expr=( - _client_state_ref(self._getter_name) + _client_state_ref_dict(self._getter_name) + f"[{self._id_name}]" if self._global_ref else self._getter_name - ) + ), + _var_data=self._var_data, ) .to(self._var_type) ._replace( - merge_var_data=VarData( # type: ignore - imports=_refs_import if self._global_ref else {} - ) + merge_var_data=VarData(imports=_refs_import if self._global_ref else {}) ) ) @@ -170,28 +217,28 @@ class ClientStateVar(Var): Returns: A special EventChain Var which will set the value when triggered. """ - setter = ( - _client_state_ref(self._setter_name) - if self._global_ref - else self._setter_name - ) _var_data = VarData(imports=_refs_import if self._global_ref else {}) + + setter = ( + Var(_client_state_ref(self._setter_name)) + if self._global_ref + else Var(self._setter_name, _var_data=_var_data) + ).to(FunctionVar) + if value is not NoValue: # This is a hack to make it work like an EventSpec taking an arg value_var = LiteralVar.create(value) - _var_data = VarData.merge(_var_data, value_var._get_all_var_data()) value_str = str(value_var) - if value_str.startswith("_"): + setter = ArgsFunctionOperationBuilder.create( # remove patterns of ["*"] from the value_str using regex - arg = re.sub(r"\[\".*\"\]", "", value_str) - setter = f"(({arg}) => {setter}({value_str}))" - else: - setter = f"(() => {setter}({value_str}))" - return Var( - _js_expr=setter, - _var_data=_var_data, - ).to(FunctionVar, EventChain) + args_names=(re.sub(r"\[\".*\"\]", "", value_str),) + if value_str.startswith("_") + else (), + return_expr=setter.call(value_var), + ) + + return setter.to(FunctionVar, EventChain) @property def set(self) -> Var: @@ -242,4 +289,5 @@ class ClientStateVar(Var): """ if not self._global_ref: raise ValueError("ClientStateVar must be global to push the value.") + value = Var.create(value) return run_script(f"{_client_state_ref(self._setter_name)}({value})") diff --git a/reflex/experimental/hooks.py b/reflex/experimental/hooks.py index 7d648225a..c00dd3bf9 100644 --- a/reflex/experimental/hooks.py +++ b/reflex/experimental/hooks.py @@ -11,7 +11,7 @@ def _compose_react_imports(tags: list[str]) -> dict[str, list[ImportVar]]: return {"react": [ImportVar(tag=tag) for tag in tags]} -def const(name, value) -> Var: +def const(name: str | list[str], value: str | Var) -> Var: """Create a constant Var. Args: @@ -26,7 +26,7 @@ def const(name, value) -> Var: return Var(_js_expr=f"const {name} = {value}") -def useCallback(func, deps) -> Var: +def useCallback(func: str, deps: list) -> Var: # noqa: N802 """Create a useCallback hook with a function and dependencies. Args: @@ -42,7 +42,7 @@ def useCallback(func, deps) -> Var: ) -def useContext(context) -> Var: +def useContext(context: str) -> Var: # noqa: N802 """Create a useContext hook with a context. Args: @@ -57,7 +57,7 @@ def useContext(context) -> Var: ) -def useRef(default) -> Var: +def useRef(default: str) -> Var: # noqa: N802 """Create a useRef hook with a default value. Args: @@ -72,7 +72,7 @@ def useRef(default) -> Var: ) -def useState(var_name, default=None) -> Var: +def useState(var_name: str, default: str | None = None) -> Var: # noqa: N802 """Create a useState hook with a variable name and setter name. Args: diff --git a/reflex/experimental/layout.py b/reflex/experimental/layout.py index d203ce714..d54e87f8b 100644 --- a/reflex/experimental/layout.py +++ b/reflex/experimental/layout.py @@ -12,6 +12,7 @@ from reflex.components.radix.themes.components.icon_button import IconButton from reflex.components.radix.themes.layout.box import Box from reflex.components.radix.themes.layout.container import Container from reflex.components.radix.themes.layout.stack import HStack +from reflex.constants.compiler import MemoizationMode from reflex.event import run_script from reflex.experimental import hooks from reflex.state import ComponentState @@ -33,12 +34,6 @@ class Sidebar(Box, MemoizationLeaf): Returns: The sidebar component. """ - # props.setdefault("border_right", f"1px solid {color('accent', 12)}") - # props.setdefault("background_color", color("accent", 1)) - # props.setdefault("width", "20vw") - # props.setdefault("height", "100vh") - # props.setdefault("position", "fixed") - return super().create( Box.create(*children, **props), # sidebar for content Box.create(width=props.get("width")), # spacer for layout @@ -50,10 +45,10 @@ class Sidebar(Box, MemoizationLeaf): Returns: The style of the component. """ - sidebar: Component = self.children[-2] # type: ignore - spacer: Component = self.children[-1] # type: ignore + sidebar: Component = self.children[-2] # pyright: ignore [reportAssignmentType] + spacer: Component = self.children[-1] # pyright: ignore [reportAssignmentType] open = ( - self.State.open # type: ignore + self.State.open # pyright: ignore [reportAttributeAccessIssue] if self.State else Var(_js_expr="open") ) @@ -152,6 +147,8 @@ sidebar_trigger_style = { class SidebarTrigger(Fragment): """A component that renders the sidebar trigger.""" + _memoization_mode = MemoizationMode(recursive=False) + @classmethod def create(cls, sidebar: Component, **props): """Create the sidebar trigger component. @@ -165,11 +162,11 @@ class SidebarTrigger(Fragment): """ trigger_props = {**props, **sidebar_trigger_style} - inner_sidebar: Component = sidebar.children[0] # type: ignore + inner_sidebar: Component = sidebar.children[0] # pyright: ignore [reportAssignmentType] sidebar_width = inner_sidebar.style.get("width") if sidebar.State: - open, toggle = sidebar.State.open, sidebar.State.toggle # type: ignore + open, toggle = sidebar.State.open, sidebar.State.toggle # pyright: ignore [reportAttributeAccessIssue] else: open, toggle = ( Var(_js_expr="open"), diff --git a/reflex/experimental/layout.pyi b/reflex/experimental/layout.pyi index 4c7fc8d47..93c8c0137 100644 --- a/reflex/experimental/layout.pyi +++ b/reflex/experimental/layout.pyi @@ -109,7 +109,7 @@ class DrawerSidebar(DrawerRoot): snap_points: Optional[List[Union[float, str]]] = None, fade_from_index: Optional[Union[Var[int], int]] = None, scroll_lock_timeout: Optional[Union[Var[int], int]] = None, - preventScrollRestoration: Optional[Union[Var[bool], bool]] = None, + prevent_scroll_restoration: Optional[Union[Var[bool], bool]] = None, should_scale_background: Optional[Union[Var[bool], bool]] = None, close_threshold: Optional[Union[Var[float], float]] = None, as_child: Optional[Union[Var[bool], bool]] = None, diff --git a/reflex/experimental/misc.py b/reflex/experimental/misc.py index a2a5a0615..986729881 100644 --- a/reflex/experimental/misc.py +++ b/reflex/experimental/misc.py @@ -1,16 +1,16 @@ """Miscellaneous functions for the experimental package.""" import asyncio -from typing import Any +from typing import Any, Callable -async def run_in_thread(func) -> Any: +async def run_in_thread(func: Callable) -> Any: """Run a function in a separate thread. To not block the UI event queue, run_in_thread must be inside inside a rx.event(background=True) decorated method. Args: - func (callable): The non-async function to run. + func: The non-async function to run. Raises: ValueError: If the function is an async function. diff --git a/reflex/istate/data.py b/reflex/istate/data.py index 9f6e3b3f4..987921889 100644 --- a/reflex/istate/data.py +++ b/reflex/istate/data.py @@ -26,7 +26,7 @@ class HeaderData: accept_language: str = "" def __init__(self, router_data: Optional[dict] = None): - """Initalize the HeaderData object based on router_data. + """Initialize the HeaderData object based on router_data. Args: router_data: the router_data dict. @@ -51,7 +51,7 @@ class PageData: params: dict = dataclasses.field(default_factory=dict) def __init__(self, router_data: Optional[dict] = None): - """Initalize the PageData object based on router_data. + """Initialize the PageData object based on router_data. Args: router_data: the router_data dict. @@ -91,7 +91,7 @@ class SessionData: session_id: str = "" def __init__(self, router_data: Optional[dict] = None): - """Initalize the SessionData object based on router_data. + """Initialize the SessionData object based on router_data. Args: router_data: the router_data dict. diff --git a/reflex/istate/wrappers.py b/reflex/istate/wrappers.py index d4e74cf8a..865bd6c63 100644 --- a/reflex/istate/wrappers.py +++ b/reflex/istate/wrappers.py @@ -6,7 +6,7 @@ from reflex.istate.proxy import ReadOnlyStateProxy from reflex.state import _split_substate_key, _substate_key, get_state_manager -async def get_state(token, state_cls: Any | None = None) -> ReadOnlyStateProxy: +async def get_state(token: str, state_cls: Any | None = None) -> ReadOnlyStateProxy: """Get the instance of a state for a token. Args: diff --git a/reflex/middleware/hydrate_middleware.py b/reflex/middleware/hydrate_middleware.py index 2198b82c2..2dea54e17 100644 --- a/reflex/middleware/hydrate_middleware.py +++ b/reflex/middleware/hydrate_middleware.py @@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Optional from reflex import constants from reflex.event import Event, get_hydrate_event from reflex.middleware.middleware import Middleware -from reflex.state import BaseState, StateUpdate +from reflex.state import BaseState, StateUpdate, _resolve_delta if TYPE_CHECKING: from reflex.app import App @@ -42,7 +42,7 @@ class HydrateMiddleware(Middleware): setattr(state, constants.CompileVars.IS_HYDRATED, False) # Get the initial state. - delta = state.dict() + delta = await _resolve_delta(state.dict()) # since a full dict was captured, clean any dirtiness state._clean() diff --git a/reflex/model.py b/reflex/model.py index 4b070ec67..06bb87b02 100644 --- a/reflex/model.py +++ b/reflex/model.py @@ -2,7 +2,9 @@ from __future__ import annotations +import re from collections import defaultdict +from contextlib import suppress from typing import Any, ClassVar, Optional, Type, Union import alembic.autogenerate @@ -14,13 +16,57 @@ import alembic.script import alembic.util import sqlalchemy import sqlalchemy.exc +import sqlalchemy.ext.asyncio import sqlalchemy.orm +from alembic.runtime.migration import MigrationContext from reflex.base import Base from reflex.config import environment, get_config from reflex.utils import console from reflex.utils.compat import sqlmodel, sqlmodel_field_has_primary_key +_ENGINE: dict[str, sqlalchemy.engine.Engine] = {} +_ASYNC_ENGINE: dict[str, sqlalchemy.ext.asyncio.AsyncEngine] = {} +_AsyncSessionLocal: dict[str | None, sqlalchemy.ext.asyncio.async_sessionmaker] = {} + +# Import AsyncSession _after_ reflex.utils.compat +from sqlmodel.ext.asyncio.session import AsyncSession # noqa: E402 + + +def _safe_db_url_for_logging(url: str) -> str: + """Remove username and password from the database URL for logging. + + Args: + url: The database URL. + + Returns: + The database URL with the username and password removed. + """ + return re.sub(r"://[^@]+@", "://:@", url) + + +def get_engine_args(url: str | None = None) -> dict[str, Any]: + """Get the database engine arguments. + + Args: + url: The database url. + + Returns: + The database engine arguments as a dict. + """ + kwargs: dict[str, Any] = { + # Print the SQL queries if the log level is INFO or lower. + "echo": environment.SQLALCHEMY_ECHO.get(), + # Check connections before returning them. + "pool_pre_ping": environment.SQLALCHEMY_POOL_PRE_PING.get(), + } + conf = get_config() + url = url or conf.db_url + if url is not None and url.startswith("sqlite"): + # Needed for the admin dash on sqlite. + kwargs["connect_args"] = {"check_same_thread": False} + return kwargs + def get_engine(url: str | None = None) -> sqlalchemy.engine.Engine: """Get the database engine. @@ -38,26 +84,71 @@ def get_engine(url: str | None = None) -> sqlalchemy.engine.Engine: url = url or conf.db_url if url is None: raise ValueError("No database url configured") + + global _ENGINE + if url in _ENGINE: + return _ENGINE[url] + if not environment.ALEMBIC_CONFIG.get().exists(): console.warn( "Database is not initialized, run [bold]reflex db init[/bold] first." ) - # Print the SQL queries if the log level is INFO or lower. - echo_db_query = environment.SQLALCHEMY_ECHO.get() - # Needed for the admin dash on sqlite. - connect_args = {"check_same_thread": False} if url.startswith("sqlite") else {} - return sqlmodel.create_engine(url, echo=echo_db_query, connect_args=connect_args) + _ENGINE[url] = sqlmodel.create_engine( + url, + **get_engine_args(url), + ) + return _ENGINE[url] -async def get_db_status() -> bool: +def get_async_engine(url: str | None) -> sqlalchemy.ext.asyncio.AsyncEngine: + """Get the async database engine. + + Args: + url: The database url. + + Returns: + The async database engine. + + Raises: + ValueError: If the async database url is None. + """ + if url is None: + conf = get_config() + url = conf.async_db_url + if url is not None and conf.db_url is not None: + async_db_url_tail = url.partition("://")[2] + db_url_tail = conf.db_url.partition("://")[2] + if async_db_url_tail != db_url_tail: + console.warn( + f"async_db_url `{_safe_db_url_for_logging(url)}` " + "should reference the same database as " + f"db_url `{_safe_db_url_for_logging(conf.db_url)}`." + ) + if url is None: + raise ValueError("No async database url configured") + + global _ASYNC_ENGINE + if url in _ASYNC_ENGINE: + return _ASYNC_ENGINE[url] + + if not environment.ALEMBIC_CONFIG.get().exists(): + console.warn( + "Database is not initialized, run [bold]reflex db init[/bold] first." + ) + _ASYNC_ENGINE[url] = sqlalchemy.ext.asyncio.create_async_engine( + url, + **get_engine_args(url), + ) + return _ASYNC_ENGINE[url] + + +async def get_db_status() -> dict[str, bool]: """Checks the status of the database connection. Attempts to connect to the database and execute a simple query to verify connectivity. Returns: - bool: The status of the database connection: - - True: The database is accessible. - - False: The database is not accessible. + The status of the database connection. """ status = True try: @@ -67,7 +158,7 @@ async def get_db_status() -> bool: except sqlalchemy.exc.OperationalError: status = False - return status + return {"db": status} SQLModelOrSqlAlchemy = Union[ @@ -152,7 +243,7 @@ class ModelRegistry: return metadata -class Model(Base, sqlmodel.SQLModel): # pyright: ignore [reportGeneralTypeIssues] +class Model(Base, sqlmodel.SQLModel): # pyright: ignore [reportGeneralTypeIssues,reportIncompatibleVariableOverride] """Base class to define a table in the database.""" # The primary key for the table. @@ -171,7 +262,7 @@ class Model(Base, sqlmodel.SQLModel): # pyright: ignore [reportGeneralTypeIssue super().__init_subclass__() @classmethod - def _dict_recursive(cls, value): + def _dict_recursive(cls, value: Any): """Recursively serialize the relationship object(s). Args: @@ -199,11 +290,10 @@ class Model(Base, sqlmodel.SQLModel): # pyright: ignore [reportGeneralTypeIssue relationships = {} # SQLModel relationships do not appear in __fields__, but should be included if present. for name in self.__sqlmodel_relationships__: - try: + with suppress( + sqlalchemy.orm.exc.DetachedInstanceError # This happens when the relationship was never loaded and the session is closed. + ): relationships[name] = self._dict_recursive(getattr(self, name)) - except sqlalchemy.orm.exc.DetachedInstanceError: - # This happens when the relationship was never loaded and the session is closed. - continue return { **base_fields, **relationships, @@ -304,7 +394,11 @@ class Model(Base, sqlmodel.SQLModel): # pyright: ignore [reportGeneralTypeIssue writer = alembic.autogenerate.rewriter.Rewriter() @writer.rewrites(alembic.operations.ops.AddColumnOp) - def render_add_column_with_server_default(context, revision, op): + def render_add_column_with_server_default( + context: MigrationContext, + revision: str | None, + op: Any, + ): # Carry the sqlmodel default as server_default so that newly added # columns get the desired default value in existing rows. if op.column.default is not None and op.column.server_default is None: @@ -313,7 +407,7 @@ class Model(Base, sqlmodel.SQLModel): # pyright: ignore [reportGeneralTypeIssue ) return op - def run_autogenerate(rev, context): + def run_autogenerate(rev: str, context: MigrationContext): revision_context.run_autogenerate(rev, context) return [] @@ -326,7 +420,7 @@ class Model(Base, sqlmodel.SQLModel): # pyright: ignore [reportGeneralTypeIssue connection=connection, target_metadata=ModelRegistry.get_metadata(), render_item=cls._alembic_render_item, - process_revision_directives=writer, # type: ignore + process_revision_directives=writer, compare_type=False, render_as_batch=True, # for sqlite compatibility ) @@ -355,7 +449,7 @@ class Model(Base, sqlmodel.SQLModel): # pyright: ignore [reportGeneralTypeIssue """ config, script_directory = cls._alembic_config() - def run_upgrade(rev, context): + def run_upgrade(rev: str, context: MigrationContext): return script_directory._upgrade_revs(to_rev, rev) with alembic.runtime.environment.EnvironmentContext( @@ -425,6 +519,32 @@ def session(url: str | None = None) -> sqlmodel.Session: return sqlmodel.Session(get_engine(url)) +def asession(url: str | None = None) -> AsyncSession: + """Get an async sqlmodel session to interact with the database. + + async with rx.asession() as asession: + ... + + Most operations against the `asession` must be awaited. + + Args: + url: The database url. + + Returns: + An async database session. + """ + global _AsyncSessionLocal + if url not in _AsyncSessionLocal: + _AsyncSessionLocal[url] = sqlalchemy.ext.asyncio.async_sessionmaker( + bind=get_async_engine(url), + class_=AsyncSession, + expire_on_commit=False, + autocommit=False, + autoflush=False, + ) + return _AsyncSessionLocal[url]() + + def sqla_session(url: str | None = None) -> sqlalchemy.orm.Session: """Get a bare sqlalchemy session to interact with the database. diff --git a/reflex/page.py b/reflex/page.py index 8cc031757..5f118aad1 100644 --- a/reflex/page.py +++ b/reflex/page.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections import defaultdict -from typing import Any, Dict, List +from typing import Any, Callable, Dict, List from reflex.config import get_config from reflex.event import BASE_STATE, EventType @@ -42,7 +42,7 @@ def page( The decorated function. """ - def decorator(render_fn): + def decorator(render_fn: Callable): kwargs = {} if route: kwargs["route"] = route @@ -66,11 +66,11 @@ def page( return decorator -def get_decorated_pages(omit_implicit_routes=True) -> list[dict[str, Any]]: +def get_decorated_pages(omit_implicit_routes: bool = True) -> list[dict[str, Any]]: """Get the decorated pages. Args: - omit_implicit_routes: Whether to omit pages where the route will be implicitely guessed later. + omit_implicit_routes: Whether to omit pages where the route will be implicitly guessed later. Returns: The decorated pages. diff --git a/reflex/reflex.py b/reflex/reflex.py index 829c7c0d2..d1e565665 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -3,7 +3,6 @@ from __future__ import annotations import atexit -import os from pathlib import Path from typing import List, Optional @@ -18,7 +17,7 @@ from reflex.state import reset_disk_state_manager from reflex.utils import console, telemetry # Disable typer+rich integration for help panels -typer.core.rich = None # type: ignore +typer.core.rich = None # pyright: ignore [reportPrivateImportUsage] # Create the app. try: @@ -126,8 +125,8 @@ def _run( env: constants.Env = constants.Env.DEV, frontend: bool = True, backend: bool = True, - frontend_port: str = str(config.frontend_port), - backend_port: str = str(config.backend_port), + frontend_port: int = config.frontend_port, + backend_port: int = config.backend_port, backend_host: str = config.backend_host, loglevel: constants.LogLevel = config.loglevel, ): @@ -161,18 +160,22 @@ def _run( # Find the next available open port if applicable. if frontend: frontend_port = processes.handle_port( - "frontend", frontend_port, str(constants.DefaultPorts.FRONTEND_PORT) + "frontend", + frontend_port, + constants.DefaultPorts.FRONTEND_PORT, ) if backend: backend_port = processes.handle_port( - "backend", backend_port, str(constants.DefaultPorts.BACKEND_PORT) + "backend", + backend_port, + constants.DefaultPorts.BACKEND_PORT, ) # Apply the new ports to the config. - if frontend_port != str(config.frontend_port): + if frontend_port != config.frontend_port: config._set_persistent(frontend_port=frontend_port) - if backend_port != str(config.backend_port): + if backend_port != config.backend_port: config._set_persistent(backend_port=backend_port) # Reload the config to make sure the env vars are persistent. @@ -263,10 +266,10 @@ def run( help="Execute only backend.", envvar=environment.REFLEX_BACKEND_ONLY.name, ), - frontend_port: str = typer.Option( + frontend_port: int = typer.Option( config.frontend_port, help="Specify a different frontend port." ), - backend_port: str = typer.Option( + backend_port: int = typer.Option( config.backend_port, help="Specify a different backend port." ), backend_host: str = typer.Option( @@ -298,7 +301,7 @@ def export( True, "--frontend-only", help="Export only frontend.", show_default=False ), zip_dest_dir: str = typer.Option( - os.getcwd(), + str(Path.cwd()), help="The directory to export the zip files to.", show_default=False, ), @@ -307,6 +310,9 @@ def export( help="Whether to exclude sqlite db files when exporting backend.", hidden=True, ), + env: constants.Env = typer.Option( + constants.Env.PROD, help="The environment to export the app in." + ), loglevel: constants.LogLevel = typer.Option( config.loglevel, help="The log level to use." ), @@ -324,19 +330,21 @@ def export( backend=backend, zip_dest_dir=zip_dest_dir, upload_db_file=upload_db_file, + env=env, loglevel=loglevel.subprocess_level(), ) @cli.command() def login(loglevel: constants.LogLevel = typer.Option(config.loglevel)): - """Authenicate with experimental Reflex hosting service.""" + """Authenticate with experimental Reflex hosting service.""" from reflex_cli.v2 import cli as hosting_cli check_version() validated_info = hosting_cli.login() if validated_info is not None: + _skip_compile() # Allow running outside of an app dir telemetry.send("login", user_uuid=validated_info.get("user_id")) @@ -351,7 +359,7 @@ def logout( check_version() - logout(loglevel) # type: ignore + logout(loglevel) # pyright: ignore [reportArgumentType] db_cli = typer.Typer() @@ -440,16 +448,20 @@ def deploy( config.app_name, "--app-name", help="The name of the App to deploy under.", - hidden=True, + ), + app_id: str = typer.Option( + None, + "--app-id", + help="The ID of the App to deploy over.", ), regions: List[str] = typer.Option( - list(), + [], "-r", "--region", help="The regions to deploy to. `reflex cloud regions` For multiple envs, repeat this option, e.g. --region sjc --region iad", ), envs: List[str] = typer.Option( - list(), + [], "--env", help="The environment variables to set: =. For multiple envs, repeat this option, e.g. --env k1=v2 --env k2=v2.", ), @@ -480,13 +492,24 @@ def deploy( "--project", help="project id to deploy to", ), + project_name: Optional[str] = typer.Option( + None, + "--project-name", + help="The name of the project to deploy to.", + ), token: Optional[str] = typer.Option( None, "--token", help="token to use for auth", ), + config_path: Optional[str] = typer.Option( + None, + "--config", + help="path to the config file", + ), ): """Deploy the app to the Reflex hosting service.""" + from reflex_cli.constants.base import LogLevel as HostingLogLevel from reflex_cli.utils import dependency from reflex_cli.v2 import cli as hosting_cli @@ -498,12 +521,20 @@ def deploy( # Set the log level. console.set_log_level(loglevel) - if not token: - # make sure user is logged in. - if interactive: - hosting_cli.login() - else: - raise SystemExit("Token is required for non-interactive mode.") + def convert_reflex_loglevel_to_reflex_cli_loglevel( + loglevel: constants.LogLevel, + ) -> HostingLogLevel: + if loglevel == constants.LogLevel.DEBUG: + return HostingLogLevel.DEBUG + if loglevel == constants.LogLevel.INFO: + return HostingLogLevel.INFO + if loglevel == constants.LogLevel.WARNING: + return HostingLogLevel.WARNING + if loglevel == constants.LogLevel.ERROR: + return HostingLogLevel.ERROR + if loglevel == constants.LogLevel.CRITICAL: + return HostingLogLevel.CRITICAL + return HostingLogLevel.INFO # Only check requirements if interactive. # There is user interaction for requirements update. @@ -517,6 +548,7 @@ def deploy( hosting_cli.deploy( app_name=app_name, + app_id=app_id, export_fn=lambda zip_dest_dir, api_url, deploy_url, @@ -537,12 +569,28 @@ def deploy( envfile=envfile, hostname=hostname, interactive=interactive, - loglevel=type(loglevel).INFO, # type: ignore + loglevel=convert_reflex_loglevel_to_reflex_cli_loglevel(loglevel), token=token, project=project, + project_name=project_name, + **({"config_path": config_path} if config_path is not None else {}), ) +@cli.command() +def rename( + new_name: str = typer.Argument(..., help="The new name for the app."), + loglevel: constants.LogLevel = typer.Option( + config.loglevel, help="The log level to use." + ), +): + """Rename the app in the current directory.""" + from reflex.utils import prerequisites + + prerequisites.validate_app_name(new_name) + prerequisites.rename_app(new_name, loglevel) + + cli.add_typer(db_cli, name="db", help="Subcommands for managing the database schema.") cli.add_typer(script_cli, name="script", help="Subcommands running helper scripts.") cli.add_typer( diff --git a/reflex/route.py b/reflex/route.py index 0b7172824..3f49f66e9 100644 --- a/reflex/route.py +++ b/reflex/route.py @@ -103,7 +103,7 @@ def catchall_prefix(route: str) -> str: return route.replace(pattern, "") if pattern else "" -def replace_brackets_with_keywords(input_string): +def replace_brackets_with_keywords(input_string: str) -> str: """Replace brackets and everything inside it in a string with a keyword. Args: diff --git a/reflex/state.py b/reflex/state.py index b80091086..fdf207189 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -11,10 +11,10 @@ import inspect import json import pickle import sys +import time import typing import uuid from abc import ABC, abstractmethod -from collections import defaultdict from hashlib import md5 from pathlib import Path from types import FunctionType, MethodType @@ -30,6 +30,7 @@ from typing import ( Optional, Sequence, Set, + SupportsIndex, Tuple, Type, TypeVar, @@ -39,6 +40,7 @@ from typing import ( get_type_hints, ) +from redis.asyncio.client import PubSub from sqlalchemy.orm import DeclarativeBase from typing_extensions import Self @@ -69,6 +71,11 @@ try: except ModuleNotFoundError: BaseModelV1 = BaseModelV2 +try: + from pydantic.v1 import validator +except ModuleNotFoundError: + from pydantic import validator + import wrapt from redis.asyncio import Redis from redis.exceptions import ResponseError @@ -86,24 +93,29 @@ from reflex.event import ( ) from reflex.utils import console, format, path_ops, prerequisites, types from reflex.utils.exceptions import ( - ComputedVarShadowsBaseVars, - ComputedVarShadowsStateVar, - DynamicComponentInvalidSignature, - DynamicRouteArgShadowsStateVar, - EventHandlerShadowsBuiltInStateMethod, + ComputedVarShadowsBaseVarsError, + ComputedVarShadowsStateVarError, + DynamicComponentInvalidSignatureError, + DynamicRouteArgShadowsStateVarError, + EventHandlerShadowsBuiltInStateMethodError, ImmutableStateError, - InvalidStateManagerMode, + InvalidLockWarningThresholdError, + InvalidStateManagerModeError, LockExpiredError, ReflexRuntimeError, SetUndefinedStateVarError, + StateMismatchError, StateSchemaMismatchError, + StateSerializationError, StateTooLargeError, + UnretrievableVarValueError, ) from reflex.utils.exec import is_testing_env from reflex.utils.serializers import serializer from reflex.utils.types import ( _isinstance, get_origin, + is_optional, is_union, override, value_inside_optional, @@ -133,6 +145,9 @@ HANDLED_PICKLE_ERRORS = ( ValueError, ) +# For BaseState.get_var_value +VAR_TYPE = TypeVar("VAR_TYPE") + def _no_chain_background_task( state_cls: Type["BaseState"], name: str, fn: Callable @@ -278,6 +293,22 @@ if TYPE_CHECKING: from pydantic.v1.fields import ModelField +def _unwrap_field_type(type_: Type) -> Type: + """Unwrap rx.Field type annotations. + + Args: + type_: The type to unwrap. + + Returns: + The unwrapped type. + """ + from reflex.vars import Field + + if get_origin(type_) is Field: + return get_args(type_)[0] + return type_ + + def get_var_for_field(cls: Type[BaseState], f: ModelField): """Get a Var instance for a Pydantic field. @@ -288,19 +319,34 @@ def get_var_for_field(cls: Type[BaseState], f: ModelField): Returns: The Var instance. """ - from reflex.vars import Field - field_name = format.format_state_name(cls.get_full_name()) + "." + f.name return dispatch( field_name=field_name, var_data=VarData.from_state(cls, f.name), - result_var_type=f.outer_type_ - if get_origin(f.outer_type_) is not Field - else get_args(f.outer_type_)[0], + result_var_type=_unwrap_field_type(f.outer_type_), ) +async def _resolve_delta(delta: Delta) -> Delta: + """Await all coroutines in the delta. + + Args: + delta: The delta to process. + + Returns: + The same delta dict with all coroutines resolved to their return value. + """ + tasks = {} + for state_name, state_delta in delta.items(): + for var_name, value in state_delta.items(): + if asyncio.iscoroutine(value): + tasks[state_name, var_name] = asyncio.create_task(value) + for (state_name, var_name), task in tasks.items(): + delta[state_name][var_name] = await task + return delta + + class BaseState(Base, ABC, extra=pydantic.Extra.allow): """The state of the app.""" @@ -328,11 +374,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): # A set of subclassses of this class. class_subclasses: ClassVar[Set[Type[BaseState]]] = set() - # Mapping of var name to set of computed variables that depend on it - _computed_var_dependencies: ClassVar[Dict[str, Set[str]]] = {} - - # Mapping of var name to set of substates that depend on it - _substate_var_dependencies: ClassVar[Dict[str, Set[str]]] = {} + # Mapping of var name to set of (state_full_name, var_name) that depend on it. + _var_dependencies: ClassVar[Dict[str, Set[Tuple[str, str]]]] = {} # Set of vars which always need to be recomputed _always_dirty_computed_vars: ClassVar[Set[str]] = set() @@ -340,6 +383,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): # Set of substates which always need to be recomputed _always_dirty_substates: ClassVar[Set[str]] = set() + # Set of states which might need to be recomputed if vars in this state change. + _potentially_dirty_states: ClassVar[Set[str]] = set() + # The parent state. parent_state: Optional[BaseState] = None @@ -415,9 +461,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): ) # Create a fresh copy of the backend variables for this instance - self._backend_vars = copy.deepcopy( - {name: item for name, item in self.backend_vars.items()} - ) + self._backend_vars = copy.deepcopy(self.backend_vars) def __repr__(self) -> str: """Get the string representation of the state. @@ -425,7 +469,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): Returns: The string representation of the state. """ - return f"{self.__class__.__name__}({self.dict()})" + return f"{type(self).__name__}({self.dict()})" @classmethod def _get_computed_vars(cls) -> list[ComputedVar]: @@ -493,6 +537,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): # Reset dirty substate tracking for this class. cls._always_dirty_substates = set() + cls._potentially_dirty_states = set() # Get the parent vars. parent_state = cls.get_parent_state() @@ -501,9 +546,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): cls.inherited_backend_vars = parent_state.backend_vars # Check if another substate class with the same name has already been defined. - if cls.get_name() in set( - c.get_name() for c in parent_state.class_subclasses - ): + if cls.get_name() in {c.get_name() for c in parent_state.class_subclasses}: # This should not happen, since we have added module prefix to state names in #3214 raise StateValueError( f"The substate class '{cls.get_name()}' has been defined multiple times. " @@ -564,7 +607,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): if cls._item_is_event_handler(name, fn) } - for mixin in cls._mixins(): + for mixin in cls._mixins(): # pyright: ignore [reportAssignmentType] for name, value in mixin.__dict__.items(): if name in cls.inherited_vars: continue @@ -576,7 +619,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): cls.computed_vars[newcv._js_expr] = newcv cls.vars[newcv._js_expr] = newcv continue - if types.is_backend_base_variable(name, mixin): + if types.is_backend_base_variable(name, mixin): # pyright: ignore [reportArgumentType] cls.backend_vars[name] = copy.deepcopy(value) continue if events.get(name) is not None: @@ -598,8 +641,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): setattr(cls, name, handler) # Initialize per-class var dependency tracking. - cls._computed_var_dependencies = defaultdict(set) - cls._substate_var_dependencies = defaultdict(set) + cls._var_dependencies = {} cls._init_var_dependency_dicts() @staticmethod @@ -744,33 +786,34 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): Additional updates tracking dicts for vars and substates that always need to be recomputed. """ - inherited_vars = set(cls.inherited_vars).union( - set(cls.inherited_backend_vars), - ) for cvar_name, cvar in cls.computed_vars.items(): - # Add the dependencies. - for var in cvar._deps(objclass=cls): - cls._computed_var_dependencies[var].add(cvar_name) - if var in inherited_vars: - # track that this substate depends on its parent for this var - state_name = cls.get_name() - parent_state = cls.get_parent_state() - while parent_state is not None and var in { - **parent_state.vars, - **parent_state.backend_vars, + if not cvar._cache: + # Do not perform dep calculation when cache=False (these are always dirty). + continue + for state_name, dvar_set in cvar._deps(objclass=cls).items(): + state_cls = cls.get_root_state().get_class_substate(state_name) + for dvar in dvar_set: + defining_state_cls = state_cls + while dvar in { + *defining_state_cls.inherited_vars, + *defining_state_cls.inherited_backend_vars, }: - parent_state._substate_var_dependencies[var].add(state_name) - state_name, parent_state = ( - parent_state.get_name(), - parent_state.get_parent_state(), - ) + parent_state = defining_state_cls.get_parent_state() + if parent_state is not None: + defining_state_cls = parent_state + defining_state_cls._var_dependencies.setdefault(dvar, set()).add( + (cls.get_full_name(), cvar_name) + ) + defining_state_cls._potentially_dirty_states.add( + cls.get_full_name() + ) # ComputedVar with cache=False always need to be recomputed - cls._always_dirty_computed_vars = set( + cls._always_dirty_computed_vars = { cvar_name for cvar_name, cvar in cls.computed_vars.items() if not cvar._cache - ) + } # Any substate containing a ComputedVar with cache=False always needs to be recomputed if cls._always_dirty_computed_vars: @@ -792,7 +835,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): """Check for shadow methods and raise error if any. Raises: - EventHandlerShadowsBuiltInStateMethod: When an event handler shadows an inbuilt state method. + EventHandlerShadowsBuiltInStateMethodError: When an event handler shadows an inbuilt state method. """ overridden_methods = set() state_base_functions = cls._get_base_functions() @@ -806,7 +849,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): overridden_methods.add(method.__name__) for method_name in overridden_methods: - raise EventHandlerShadowsBuiltInStateMethod( + raise EventHandlerShadowsBuiltInStateMethodError( f"The event handler name `{method_name}` shadows a builtin State method; use a different name instead" ) @@ -815,11 +858,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): """Check for shadow base vars and raise error if any. Raises: - ComputedVarShadowsBaseVars: When a computed var shadows a base var. + ComputedVarShadowsBaseVarsError: When a computed var shadows a base var. """ for computed_var_ in cls._get_computed_vars(): if computed_var_._js_expr in cls.__annotations__: - raise ComputedVarShadowsBaseVars( + raise ComputedVarShadowsBaseVarsError( f"The computed var name `{computed_var_._js_expr}` shadows a base var in {cls.__module__}.{cls.__name__}; use a different name instead" ) @@ -828,14 +871,14 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): """Check for shadow computed vars and raise error if any. Raises: - ComputedVarShadowsStateVar: When a computed var shadows another. + ComputedVarShadowsStateVarError: When a computed var shadows another. """ for name, cv in cls.__dict__.items(): if not is_computed_var(cv): continue name = cv._js_expr if name in cls.inherited_vars or name in cls.inherited_backend_vars: - raise ComputedVarShadowsStateVar( + raise ComputedVarShadowsStateVarError( f"The computed var name `{cv._js_expr}` shadows a var in {cls.__module__}.{cls.__name__}; use a different name instead" ) @@ -876,7 +919,18 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): ] if len(parent_states) >= 2: raise ValueError(f"Only one parent state is allowed {parent_states}.") - return parent_states[0] if len(parent_states) == 1 else None # type: ignore + return parent_states[0] if len(parent_states) == 1 else None + + @classmethod + @functools.lru_cache() + def get_root_state(cls) -> Type[BaseState]: + """Get the root state. + + Returns: + The root state. + """ + parent_state = cls.get_parent_state() + return cls if parent_state is None else parent_state.get_root_state() @classmethod def get_substates(cls) -> set[Type[BaseState]]: @@ -1036,7 +1090,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): setattr(cls, prop._var_field_name, prop) @classmethod - def _create_event_handler(cls, fn): + def _create_event_handler(cls, fn: Any): """Create an event handler for the given function. Args: @@ -1082,6 +1136,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): if ( not field.required and field.default is None + and field.default_factory is None and not types.is_optional(prop._var_type) ): # Ensure frontend uses null coalescing when accessing. @@ -1153,14 +1208,14 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): cls._check_overwritten_dynamic_args(list(args.keys())) - def argsingle_factory(param): - def inner_func(self) -> str: + def argsingle_factory(param: str): + def inner_func(self: BaseState) -> str: return self.router.page.params.get(param, "") return inner_func - def arglist_factory(param): - def inner_func(self) -> List[str]: + def arglist_factory(param: str): + def inner_func(self: BaseState) -> List[str]: return self.router.page.params.get(param, []) return inner_func @@ -1175,7 +1230,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): continue dynamic_vars[param] = DynamicRouteVar( fget=func, - cache=True, + auto_deps=False, + deps=["router"], _js_expr=param, _var_data=VarData.from_state(cls), ) @@ -1194,14 +1250,14 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): args: a dict of args Raises: - DynamicRouteArgShadowsStateVar: If a dynamic arg is shadowing an existing var. + DynamicRouteArgShadowsStateVarError: If a dynamic arg is shadowing an existing var. """ for arg in args: if ( arg in cls.computed_vars and not isinstance(cls.computed_vars[arg], DynamicRouteVar) ) or arg in cls.base_vars: - raise DynamicRouteArgShadowsStateVar( + raise DynamicRouteArgShadowsStateVarError( f"Dynamic route arg '{arg}' is shadowing an existing var in {cls.__module__}.{cls.__name__}" ) for substate in cls.get_substates(): @@ -1222,13 +1278,16 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): if not super().__getattribute__("__dict__"): return super().__getattribute__(name) - inherited_vars = { - **super().__getattribute__("inherited_vars"), - **super().__getattribute__("inherited_backend_vars"), - } + # Fast path for dunder + if name.startswith("__"): + return super().__getattribute__(name) # For now, handle router_data updates as a special case. - if name in inherited_vars or name == constants.ROUTER_DATA: + if ( + name == constants.ROUTER_DATA + or name in super().__getattribute__("inherited_vars") + or name in super().__getattribute__("inherited_backend_vars") + ): parent_state = super().__getattribute__("parent_state") if parent_state is not None: return getattr(parent_state, name) @@ -1241,8 +1300,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): fn = _no_chain_background_task(type(self), name, handler.fn) else: fn = functools.partial(handler.fn, self) - fn.__module__ = handler.fn.__module__ # type: ignore - fn.__qualname__ = handler.fn.__qualname__ # type: ignore + fn.__module__ = handler.fn.__module__ + fn.__qualname__ = handler.fn.__qualname__ return fn backend_vars = super().__getattribute__("_backend_vars") @@ -1283,15 +1342,11 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): value = value.__wrapped__ # Set the var on the parent state. - inherited_vars = {**self.inherited_vars, **self.inherited_backend_vars} - if name in inherited_vars: + if name in self.inherited_vars or name in self.inherited_backend_vars: setattr(self.parent_state, name, value) return if name in self.backend_vars: - # abort if unchanged - if self._backend_vars.get(name) == value: - return self._backend_vars.__setitem__(name, value) self.dirty_vars.add(name) self._mark_dirty() @@ -1314,23 +1369,20 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): if name in fields: field = fields[name] - field_type = field.outer_type_ - if field.allow_none: + field_type = _unwrap_field_type(field.outer_type_) + if field.allow_none and not is_optional(field_type): field_type = Union[field_type, None] if not _isinstance(value, field_type): - console.deprecate( - "mismatched-type-assignment", - f"Tried to assign value {value} of type {type(value)} to field {type(self).__name__}.{name} of type {field_type}." - " This might lead to unexpected behavior.", - "0.6.5", - "0.7.0", + console.error( + f"Expected field '{type(self).__name__}.{name}' to receive type '{field_type}'," + f" but got '{value}' of type '{type(value)}'." ) # Set the attribute. super().__setattr__(name, value) # Add the var to the dirty list. - if name in self.vars or name in self._computed_var_dependencies: + if name in self.base_vars: self.dirty_vars.add(name) self._mark_dirty() @@ -1401,63 +1453,21 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): return self.substates[path[0]].get_substate(path[1:]) @classmethod - def _get_common_ancestor(cls, other: Type[BaseState]) -> str: - """Find the name of the nearest common ancestor shared by this and the other state. - - Args: - other: The other state. + def _get_potentially_dirty_states(cls) -> set[type[BaseState]]: + """Get substates which may have dirty vars due to dependencies. Returns: - Full name of the nearest common ancestor. + The set of potentially dirty substate classes. """ - common_ancestor_parts = [] - for part1, part2 in zip( - cls.get_full_name().split("."), - other.get_full_name().split("."), - ): - if part1 != part2: - break - common_ancestor_parts.append(part1) - return ".".join(common_ancestor_parts) - - @classmethod - def _determine_missing_parent_states( - cls, target_state_cls: Type[BaseState] - ) -> tuple[str, list[str]]: - """Determine the missing parent states between the target_state_cls and common ancestor of this state. - - Args: - target_state_cls: The class of the state to find missing parent states for. - - Returns: - The name of the common ancestor and the list of missing parent states. - """ - common_ancestor_name = cls._get_common_ancestor(target_state_cls) - common_ancestor_parts = common_ancestor_name.split(".") - target_state_parts = tuple(target_state_cls.get_full_name().split(".")) - relative_target_state_parts = target_state_parts[len(common_ancestor_parts) :] - - # Determine which parent states to fetch from the common ancestor down to the target_state_cls. - fetch_parent_states = [common_ancestor_name] - for relative_parent_state_name in relative_target_state_parts: - fetch_parent_states.append( - ".".join((fetch_parent_states[-1], relative_parent_state_name)) - ) - - return common_ancestor_name, fetch_parent_states[1:-1] - - def _get_parent_states(self) -> list[tuple[str, BaseState]]: - """Get all parent state instances up to the root of the state tree. - - Returns: - A list of tuples containing the name and the instance of each parent state. - """ - parent_states_with_name = [] - parent_state = self - while parent_state.parent_state is not None: - parent_state = parent_state.parent_state - parent_states_with_name.append((parent_state.get_full_name(), parent_state)) - return parent_states_with_name + return { + cls.get_class_substate(substate_name) + for substate_name in cls._always_dirty_substates + }.union( + { + cls.get_root_state().get_class_substate(substate_name) + for substate_name in cls._potentially_dirty_states + } + ) def _get_root_state(self) -> BaseState: """Get the root state of the state tree. @@ -1470,69 +1480,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): parent_state = parent_state.parent_state return parent_state - async def _populate_parent_states(self, target_state_cls: Type[BaseState]): - """Populate substates in the tree between the target_state_cls and common ancestor of this state. - - Args: - target_state_cls: The class of the state to populate parent states for. - - Returns: - The parent state instance of target_state_cls. - - Raises: - RuntimeError: If redis is not used in this backend process. - """ - state_manager = get_state_manager() - if not isinstance(state_manager, StateManagerRedis): - raise RuntimeError( - f"Cannot populate parent states of {target_state_cls.get_full_name()} without redis. " - "(All states should already be available -- this is likely a bug).", - ) - - # Find the missing parent states up to the common ancestor. - ( - common_ancestor_name, - missing_parent_states, - ) = self._determine_missing_parent_states(target_state_cls) - - # Fetch all missing parent states and link them up to the common ancestor. - parent_states_tuple = self._get_parent_states() - root_state = parent_states_tuple[-1][1] - parent_states_by_name = dict(parent_states_tuple) - parent_state = parent_states_by_name[common_ancestor_name] - for parent_state_name in missing_parent_states: - try: - parent_state = root_state.get_substate(parent_state_name.split(".")) - # The requested state is already cached, do NOT fetch it again. - continue - except ValueError: - # The requested state is missing, fetch from redis. - pass - parent_state = await state_manager.get_state( - token=_substate_key( - self.router.session.client_token, parent_state_name - ), - top_level=False, - get_substates=False, - parent_state=parent_state, - ) - - # Return the direct parent of target_state_cls for subsequent linking. - return parent_state - - def _get_state_from_cache(self, state_cls: Type[BaseState]) -> BaseState: - """Get a state instance from the cache. - - Args: - state_cls: The class of the state. - - Returns: - The instance of state_cls associated with this state's client_token. - """ - root_state = self._get_root_state() - return root_state.get_substate(state_cls.get_full_name().split(".")) - - async def _get_state_from_redis(self, state_cls: Type[BaseState]) -> BaseState: + async def _get_state_from_redis(self, state_cls: Type[T_STATE]) -> T_STATE: """Get a state instance from redis. Args: @@ -1543,10 +1491,8 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): Raises: RuntimeError: If redis is not used in this backend process. + StateMismatchError: If the state instance is not of the expected type. """ - # Fetch all missing parent states from redis. - parent_state_of_state_cls = await self._populate_parent_states(state_cls) - # Then get the target state and all its substates. state_manager = get_state_manager() if not isinstance(state_manager, StateManagerRedis): @@ -1554,14 +1500,40 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): f"Requested state {state_cls.get_full_name()} is not cached and cannot be accessed without redis. " "(All states should already be available -- this is likely a bug).", ) - return await state_manager.get_state( + state_in_redis = await state_manager.get_state( token=_substate_key(self.router.session.client_token, state_cls), top_level=False, - get_substates=True, - parent_state=parent_state_of_state_cls, + for_state_instance=self, ) - async def get_state(self, state_cls: Type[BaseState]) -> BaseState: + if not isinstance(state_in_redis, state_cls): + raise StateMismatchError( + f"Searched for state {state_cls.get_full_name()} but found {state_in_redis}." + ) + + return state_in_redis + + def _get_state_from_cache(self, state_cls: Type[T_STATE]) -> T_STATE: + """Get a state instance from the cache. + + Args: + state_cls: The class of the state. + + Returns: + The instance of state_cls associated with this state's client_token. + + Raises: + StateMismatchError: If the state instance is not of the expected type. + """ + root_state = self._get_root_state() + substate = root_state.get_substate(state_cls.get_full_name().split(".")) + if not isinstance(substate, state_cls): + raise StateMismatchError( + f"Searched for state {state_cls.get_full_name()} but found {substate}." + ) + return substate + + async def get_state(self, state_cls: Type[T_STATE]) -> T_STATE: """Get an instance of the state associated with this token. Allows for arbitrary access to sibling states from within an event handler. @@ -1581,6 +1553,44 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): # Slow case - fetch missing parent states from redis. return await self._get_state_from_redis(state_cls) + async def get_var_value(self, var: Var[VAR_TYPE]) -> VAR_TYPE: + """Get the value of an rx.Var from another state. + + Args: + var: The var to get the value for. + + Returns: + The value of the var. + + Raises: + UnretrievableVarValueError: If the var does not have a literal value + or associated state. + """ + # Oopsie case: you didn't give me a Var... so get what you give. + if not isinstance(var, Var): + return var + + unset = object() + + # Fast case: this is a literal var and the value is known. + if (var_value := getattr(var, "_var_value", unset)) is not unset: + return var_value # pyright: ignore [reportReturnType] + + var_data = var._get_all_var_data() + if var_data is None or not var_data.state: + raise UnretrievableVarValueError( + f"Unable to retrieve value for {var._js_expr}: not associated with any state." + ) + # Fastish case: this var belongs to this state + if var_data.state == self.get_full_name(): + return getattr(self, var_data.field_name) + + # Slow case: this var belongs to another state + other_state = await self.get_state( + self._get_root_state().get_class_substate(var_data.state) + ) + return getattr(other_state, var_data.field_name) + def _get_event_handler( self, event: Event ) -> tuple[BaseState | StateProxy, EventHandler]: @@ -1661,7 +1671,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): f"Your handler {handler.fn.__qualname__} must only return/yield: None, Events or other EventHandlers referenced by their class (not using `self`)" ) - def _as_state_update( + async def _as_state_update( self, handler: EventHandler, events: EventSpec | list[EventSpec] | None, @@ -1689,7 +1699,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): try: # Get the delta after processing the event. - delta = state.get_delta() + delta = await _resolve_delta(state.get_delta()) state._clean() return StateUpdate( @@ -1700,9 +1710,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): except Exception as ex: state._clean() - app_instance = getattr(prerequisites.get_app(), constants.CompileVars.APP) - - event_specs = app_instance.backend_exception_handler(ex) + event_specs = ( + prerequisites.get_and_validate_app().app.backend_exception_handler(ex) + ) if event_specs is None: return StateUpdate() @@ -1755,7 +1765,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): if ( isinstance(value, dict) and inspect.isclass(hinted_args) - and not types.is_generic_alias(hinted_args) # py3.9-py3.10 + and not types.is_generic_alias(hinted_args) # py3.10 ): if issubclass(hinted_args, Model): # Remove non-fields from the payload @@ -1789,34 +1799,38 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): # Handle async generators. if inspect.isasyncgen(events): async for event in events: - yield state._as_state_update(handler, event, final=False) - yield state._as_state_update(handler, events=None, final=True) + yield await state._as_state_update(handler, event, final=False) + yield await state._as_state_update(handler, events=None, final=True) # Handle regular generators. elif inspect.isgenerator(events): try: while True: - yield state._as_state_update(handler, next(events), final=False) + yield await state._as_state_update( + handler, next(events), final=False + ) except StopIteration as si: # the "return" value of the generator is not available # in the loop, we must catch StopIteration to access it if si.value is not None: - yield state._as_state_update(handler, si.value, final=False) - yield state._as_state_update(handler, events=None, final=True) + yield await state._as_state_update( + handler, si.value, final=False + ) + yield await state._as_state_update(handler, events=None, final=True) # Handle regular event chains. else: - yield state._as_state_update(handler, events, final=True) + yield await state._as_state_update(handler, events, final=True) # If an error occurs, throw a window alert. except Exception as ex: telemetry.send_error(ex, context="backend") - app_instance = getattr(prerequisites.get_app(), constants.CompileVars.APP) + event_specs = ( + prerequisites.get_and_validate_app().app.backend_exception_handler(ex) + ) - event_specs = app_instance.backend_exception_handler(ex) - - yield state._as_state_update( + yield await state._as_state_update( handler, event_specs, final=True, @@ -1824,15 +1838,28 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): def _mark_dirty_computed_vars(self) -> None: """Mark ComputedVars that need to be recalculated based on dirty_vars.""" + # Append expired computed vars to dirty_vars to trigger recalculation + self.dirty_vars.update(self._expired_computed_vars()) + # Append always dirty computed vars to dirty_vars to trigger recalculation + self.dirty_vars.update(self._always_dirty_computed_vars) + dirty_vars = self.dirty_vars while dirty_vars: calc_vars, dirty_vars = dirty_vars, set() - for cvar in self._dirty_computed_vars(from_vars=calc_vars): - self.dirty_vars.add(cvar) + for state_name, cvar in self._dirty_computed_vars(from_vars=calc_vars): + if state_name == self.get_full_name(): + defining_state = self + else: + defining_state = self._get_root_state().get_substate( + tuple(state_name.split(".")) + ) + defining_state.dirty_vars.add(cvar) dirty_vars.add(cvar) - actual_var = self.computed_vars.get(cvar) + actual_var = defining_state.computed_vars.get(cvar) if actual_var is not None: - actual_var.mark_dirty(instance=self) + actual_var.mark_dirty(instance=defining_state) + if defining_state is not self: + defining_state._mark_dirty() def _expired_computed_vars(self) -> set[str]: """Determine ComputedVars that need to be recalculated based on the expiration time. @@ -1840,15 +1867,15 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): Returns: Set of computed vars to include in the delta. """ - return set( + return { cvar for cvar in self.computed_vars if self.computed_vars[cvar].needs_update(instance=self) - ) + } def _dirty_computed_vars( self, from_vars: set[str] | None = None, include_backend: bool = True - ) -> set[str]: + ) -> set[tuple[str, str]]: """Determine ComputedVars that need to be recalculated based on the given vars. Args: @@ -1858,33 +1885,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): Returns: Set of computed vars to include in the delta. """ - return set( - cvar + return { + (state_name, cvar) for dirty_var in from_vars or self.dirty_vars - for cvar in self._computed_var_dependencies[dirty_var] + for state_name, cvar in self._var_dependencies.get(dirty_var, set()) if include_backend or not self.computed_vars[cvar]._backend - ) - - @classmethod - def _potentially_dirty_substates(cls) -> set[Type[BaseState]]: - """Determine substates which could be affected by dirty vars in this state. - - Returns: - Set of State classes that may need to be fetched to recalc computed vars. - """ - # _always_dirty_substates need to be fetched to recalc computed vars. - fetch_substates = set( - cls.get_class_substate((cls.get_name(), *substate_name.split("."))) - for substate_name in cls._always_dirty_substates - ) - for dependent_substates in cls._substate_var_dependencies.values(): - fetch_substates.update( - set( - cls.get_class_substate((cls.get_name(), *substate_name.split("."))) - for substate_name in dependent_substates - ) - ) - return fetch_substates + } def get_delta(self) -> Delta: """Get the delta for the state. @@ -1894,21 +1900,15 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): """ delta = {} - # Apply dirty variables down into substates - self.dirty_vars.update(self._always_dirty_computed_vars) - self._mark_dirty() - + self._mark_dirty_computed_vars() frontend_computed_vars: set[str] = { name for name, cv in self.computed_vars.items() if not cv._backend } # Return the dirty vars for this instance, any cached/dependent computed vars, # and always dirty computed vars (cache=False) - delta_vars = ( - self.dirty_vars.intersection(self.base_vars) - .union(self.dirty_vars.intersection(frontend_computed_vars)) - .union(self._dirty_computed_vars(include_backend=False)) - .union(self._always_dirty_computed_vars) + delta_vars = self.dirty_vars.intersection(self.base_vars).union( + self.dirty_vars.intersection(frontend_computed_vars) ) subdelta: Dict[str, Any] = { @@ -1938,23 +1938,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): self.parent_state.dirty_substates.add(self.get_name()) self.parent_state._mark_dirty() - # Append expired computed vars to dirty_vars to trigger recalculation - self.dirty_vars.update(self._expired_computed_vars()) - # have to mark computed vars dirty to allow access to newly computed # values within the same ComputedVar function self._mark_dirty_computed_vars() - self._mark_dirty_substates() - - def _mark_dirty_substates(self): - """Propagate dirty var / computed var status into substates.""" - substates = self.substates - for var in self.dirty_vars: - for substate_name in self._substate_var_dependencies[var]: - self.dirty_substates.add(substate_name) - substate = substates[substate_name] - substate.dirty_vars.add(var) - substate._mark_dirty() def _update_was_touched(self): """Update the _was_touched flag based on dirty_vars.""" @@ -2026,11 +2012,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): The object as a dictionary. """ if include_computed: - # Apply dirty variables down into substates to allow never-cached ComputedVar to - # trigger recalculation of dependent vars - self.dirty_vars.update(self._always_dirty_computed_vars) - self._mark_dirty() - + self._mark_dirty_computed_vars() base_vars = { prop_name: self.get_value(prop_name) for prop_name in self.base_vars } @@ -2109,14 +2091,26 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): state["__dict__"].pop("router", None) state["__dict__"].pop("router_data", None) # Never serialize parent_state or substates. - state["__dict__"]["parent_state"] = None - state["__dict__"]["substates"] = {} + state["__dict__"].pop("parent_state", None) + state["__dict__"].pop("substates", None) state["__dict__"].pop("_was_touched", None) # Remove all inherited vars. for inherited_var_name in self.inherited_vars: state["__dict__"].pop(inherited_var_name, None) return state + def __setstate__(self, state: dict[str, Any]): + """Set the state from redis deserialization. + + This method is called by pickle to deserialize the object. + + Args: + state: The state dict for deserialization. + """ + state["__dict__"]["parent_state"] = None + state["__dict__"]["substates"] = {} + super().__setstate__(state) + def _check_state_size( self, pickle_state_size: int, @@ -2172,7 +2166,7 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): return md5( pickle.dumps( - list(sorted(_field_tuple(field_name) for field_name in cls.base_vars)) + sorted(_field_tuple(field_name) for field_name in cls.base_vars) ) ).hexdigest() @@ -2181,8 +2175,12 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): Returns: The serialized state. + + Raises: + StateSerializationError: If the state cannot be serialized. """ payload = b"" + error = "" try: payload = pickle.dumps((self._to_schema(), self)) except HANDLED_PICKLE_ERRORS as og_pickle_error: @@ -2202,8 +2200,13 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): except HANDLED_PICKLE_ERRORS as ex: error += f"Dill was also unable to pickle the state: {ex}" console.warn(error) + if environment.REFLEX_PERF_MODE.get() != PerformanceMode.OFF: self._check_state_size(len(payload)) + + if not payload: + raise StateSerializationError(error) + return payload @classmethod @@ -2236,6 +2239,9 @@ class BaseState(Base, ABC, extra=pydantic.Extra.allow): return state +T_STATE = TypeVar("T_STATE", bound=BaseState) + + class State(BaseState): """The app Base State.""" @@ -2256,8 +2262,7 @@ def dynamic(func: Callable[[T], Component]): The dynamically generated component. Raises: - DynamicComponentInvalidSignature: If the function does not have exactly one parameter. - DynamicComponentInvalidSignature: If the function does not have a type hint for the state class. + DynamicComponentInvalidSignatureError: If the function does not have exactly one parameter or a type hint for the state class. """ number_of_parameters = len(inspect.signature(func).parameters) @@ -2269,12 +2274,12 @@ def dynamic(func: Callable[[T], Component]): values = list(func_signature.values()) if number_of_parameters != 1: - raise DynamicComponentInvalidSignature( + raise DynamicComponentInvalidSignatureError( "The function must have exactly one parameter, which is the state class." ) if len(values) != 1: - raise DynamicComponentInvalidSignature( + raise DynamicComponentInvalidSignatureError( "You must provide a type hint for the state class in the function." ) @@ -2303,8 +2308,9 @@ class FrontendEventExceptionState(State): component_stack: The stack trace of the component where the exception occurred. """ - app_instance = getattr(prerequisites.get_app(), constants.CompileVars.APP) - app_instance.frontend_exception_handler(Exception(stack)) + prerequisites.get_and_validate_app().app.frontend_exception_handler( + Exception(stack) + ) class UpdateVarsInternalState(State): @@ -2342,19 +2348,20 @@ class OnLoadInternalState(State): The list of events to queue for on load handling. """ # Do not app._compile()! It should be already compiled by now. - app = getattr(prerequisites.get_app(), constants.CompileVars.APP) - load_events = app.get_load_events(self.router.page.path) + load_events = prerequisites.get_and_validate_app().app.get_load_events( + self.router.page.path + ) if not load_events: self.is_hydrated = True return # Fast path for navigation with no on_load events defined. self.is_hydrated = False return [ *fix_events( - load_events, + cast(list[Union[EventSpec, EventHandler]], load_events), self.router.session.client_token, router_data=self.router_data, ), - State.set_is_hydrated(True), # type: ignore + State.set_is_hydrated(True), # pyright: ignore [reportAttributeAccessIssue] ] @@ -2495,7 +2502,9 @@ class StateProxy(wrapt.ObjectProxy): """ def __init__( - self, state_instance, parent_state_proxy: Optional["StateProxy"] = None + self, + state_instance: BaseState, + parent_state_proxy: Optional["StateProxy"] = None, ): """Create a proxy for a state instance. @@ -2509,7 +2518,7 @@ class StateProxy(wrapt.ObjectProxy): """ super().__init__(state_instance) # compile is not relevant to backend logic - self._self_app = getattr(prerequisites.get_app(), constants.CompileVars.APP) + self._self_app = prerequisites.get_and_validate_app().app self._self_substate_path = tuple(state_instance.get_full_name().split(".")) self._self_actx = None self._self_mutable = False @@ -2638,7 +2647,7 @@ class StateProxy(wrapt.ObjectProxy): # ensure mutations to these containers are blocked unless proxy is _mutable return ImmutableMutableProxy( wrapped=value.__wrapped__, - state=self, # type: ignore + state=self, field_name=value._self_field_name, ) if isinstance(value, functools.partial) and value.args[0] is self.__wrapped__: @@ -2651,7 +2660,7 @@ class StateProxy(wrapt.ObjectProxy): ) if isinstance(value, MethodType) and value.__self__ is self.__wrapped__: # Rebind methods to the proxy instance - value = type(value)(value.__func__, self) # type: ignore + value = type(value)(value.__func__, self) return value def __setattr__(self, name: str, value: Any) -> None: @@ -2720,7 +2729,7 @@ class StateProxy(wrapt.ObjectProxy): await self.__wrapped__.get_state(state_cls), parent_state_proxy=self ) - def _as_state_update(self, *args, **kwargs) -> StateUpdate: + async def _as_state_update(self, *args, **kwargs) -> StateUpdate: """Temporarily allow mutability to access parent_state. Args: @@ -2733,7 +2742,7 @@ class StateProxy(wrapt.ObjectProxy): original_mutable = self._self_mutable self._self_mutable = True try: - return self.__wrapped__._as_state_update(*args, **kwargs) + return await self.__wrapped__._as_state_update(*args, **kwargs) finally: self._self_mutable = original_mutable @@ -2776,7 +2785,7 @@ class StateManager(Base, ABC): state: The state class to use. Raises: - InvalidStateManagerMode: If the state manager mode is invalid. + InvalidStateManagerModeError: If the state manager mode is invalid. Returns: The state manager (either disk, memory or redis). @@ -2797,8 +2806,9 @@ class StateManager(Base, ABC): redis=redis, token_expiration=config.redis_token_expiration, lock_expiration=config.redis_lock_expiration, + lock_warning_threshold=config.redis_lock_warning_threshold, ) - raise InvalidStateManagerMode( + raise InvalidStateManagerModeError( f"Expected one of: DISK, MEMORY, REDIS, got {config.state_manager_mode}" ) @@ -2850,7 +2860,7 @@ class StateManagerMemory(StateManager): # The dict of mutexes for each client _states_locks: Dict[str, asyncio.Lock] = pydantic.PrivateAttr({}) - class Config: + class Config: # pyright: ignore [reportIncompatibleVariableOverride] """The Pydantic config.""" fields = { @@ -2947,7 +2957,7 @@ def is_serializable(value: Any) -> bool: def reset_disk_state_manager(): """Reset the disk state manager.""" - states_directory = prerequisites.get_web_dir() / constants.Dirs.STATES + states_directory = prerequisites.get_states_dir() if states_directory.exists(): for path in states_directory.iterdir(): path.unlink() @@ -2968,7 +2978,7 @@ class StateManagerDisk(StateManager): # The token expiration time (s). token_expiration: int = pydantic.Field(default_factory=_default_token_expiration) - class Config: + class Config: # pyright: ignore [reportIncompatibleVariableOverride] """The Pydantic config.""" fields = { @@ -2995,7 +3005,7 @@ class StateManagerDisk(StateManager): Returns: The states directory. """ - return prerequisites.get_web_dir() / constants.Dirs.STATES + return prerequisites.get_states_dir() def _purge_expired_states(self): """Purge expired states from the disk.""" @@ -3166,6 +3176,15 @@ def _default_lock_expiration() -> int: return get_config().redis_lock_expiration +def _default_lock_warning_threshold() -> int: + """Get the default lock warning threshold. + + Returns: + The default lock warning threshold. + """ + return get_config().redis_lock_warning_threshold + + class StateManagerRedis(StateManager): """A state manager that stores states in redis.""" @@ -3178,6 +3197,11 @@ class StateManagerRedis(StateManager): # The maximum time to hold a lock (ms). lock_expiration: int = pydantic.Field(default_factory=_default_lock_expiration) + # The maximum time to hold a lock (ms) before warning. + lock_warning_threshold: int = pydantic.Field( + default_factory=_default_lock_warning_threshold + ) + # The keyspace subscription string when redis is waiting for lock to be released _redis_notify_keyspace_events: str = ( "K" # Enable keyspace notifications (target a particular key) @@ -3194,148 +3218,167 @@ class StateManagerRedis(StateManager): b"evicted", } - async def _get_parent_state( - self, token: str, state: BaseState | None = None - ) -> BaseState | None: - """Get the parent state for the state requested in the token. + def _get_required_state_classes( + self, + target_state_cls: Type[BaseState], + subclasses: bool = False, + required_state_classes: set[Type[BaseState]] | None = None, + ) -> set[Type[BaseState]]: + """Recursively determine which states are required to fetch the target state. + + This will always include potentially dirty substates that depend on vars + in the target_state_cls. Args: - token: The token to get the state for (_substate_key). - state: The state instance to get parent state for. + target_state_cls: The target state class being fetched. + subclasses: Whether to include subclasses of the target state. + required_state_classes: Recursive argument tracking state classes that have already been seen. Returns: - The parent state for the state requested by the token or None if there is no such parent. + The set of state classes required to fetch the target state. """ - parent_state = None - client_token, state_path = _split_substate_key(token) - parent_state_name = state_path.rpartition(".")[0] - if parent_state_name: - cached_substates = None - if state is not None: - cached_substates = [state] - # Retrieve the parent state to populate event handlers onto this substate. - parent_state = await self.get_state( - token=_substate_key(client_token, parent_state_name), - top_level=False, - get_substates=False, - cached_substates=cached_substates, + if required_state_classes is None: + required_state_classes = set() + # Get the substates if requested. + if subclasses: + for substate in target_state_cls.get_substates(): + self._get_required_state_classes( + substate, + subclasses=True, + required_state_classes=required_state_classes, + ) + if target_state_cls in required_state_classes: + return required_state_classes + required_state_classes.add(target_state_cls) + + # Get dependent substates. + for pd_substates in target_state_cls._get_potentially_dirty_states(): + self._get_required_state_classes( + pd_substates, + subclasses=False, + required_state_classes=required_state_classes, ) - return parent_state - async def _populate_substates( + # Get the parent state if it exists. + if parent_state := target_state_cls.get_parent_state(): + self._get_required_state_classes( + parent_state, + subclasses=False, + required_state_classes=required_state_classes, + ) + return required_state_classes + + def _get_populated_states( self, - token: str, - state: BaseState, - all_substates: bool = False, - ): - """Fetch and link substates for the given state instance. - - There is no return value; the side-effect is that `state` will have `substates` populated, - and each substate will have its `parent_state` set to `state`. + target_state: BaseState, + populated_states: dict[str, BaseState] | None = None, + ) -> dict[str, BaseState]: + """Recursively determine which states from target_state are already fetched. Args: - token: The token to get the state for. - state: The state instance to populate substates for. - all_substates: Whether to fetch all substates or just required substates. + target_state: The state to check for populated states. + populated_states: Recursive argument tracking states seen in previous calls. + + Returns: + A dictionary of state full name to state instance. """ - client_token, _ = _split_substate_key(token) - - if all_substates: - # All substates are requested. - fetch_substates = state.get_substates() - else: - # Only _potentially_dirty_substates need to be fetched to recalc computed vars. - fetch_substates = state._potentially_dirty_substates() - - tasks = {} - # Retrieve the necessary substates from redis. - for substate_cls in fetch_substates: - if substate_cls.get_name() in state.substates: - continue - substate_name = substate_cls.get_name() - tasks[substate_name] = asyncio.create_task( - self.get_state( - token=_substate_key(client_token, substate_cls), - top_level=False, - get_substates=all_substates, - parent_state=state, - ) + if populated_states is None: + populated_states = {} + if target_state.get_full_name() in populated_states: + return populated_states + populated_states[target_state.get_full_name()] = target_state + for substate in target_state.substates.values(): + self._get_populated_states(substate, populated_states=populated_states) + if target_state.parent_state is not None: + self._get_populated_states( + target_state.parent_state, populated_states=populated_states ) - - for substate_name, substate_task in tasks.items(): - state.substates[substate_name] = await substate_task + return populated_states @override async def get_state( self, token: str, top_level: bool = True, - get_substates: bool = True, - parent_state: BaseState | None = None, - cached_substates: list[BaseState] | None = None, + for_state_instance: BaseState | None = None, ) -> BaseState: """Get the state for a token. Args: token: The token to get the state for. top_level: If true, return an instance of the top-level state (self.state). - get_substates: If true, also retrieve substates. - parent_state: If provided, use this parent_state instead of getting it from redis. - cached_substates: If provided, attach these substates to the state. + for_state_instance: If provided, attach the requested states to this existing state tree. Returns: The state for the token. Raises: - RuntimeError: when the state_cls is not specified in the token + RuntimeError: when the state_cls is not specified in the token, or when the parent state for a + requested state was not fetched. """ # Split the actual token from the fully qualified substate name. - _, state_path = _split_substate_key(token) + token, state_path = _split_substate_key(token) if state_path: # Get the State class associated with the given path. state_cls = self.state.get_class_substate(state_path) else: raise RuntimeError( - "StateManagerRedis requires token to be specified in the form of {token}_{state_full_name}" + f"StateManagerRedis requires token to be specified in the form of {{token}}_{{state_full_name}}, but got {token}" ) - # The deserialized or newly created (sub)state instance. - state = None + # Determine which states we already have. + flat_state_tree: dict[str, BaseState] = ( + self._get_populated_states(for_state_instance) if for_state_instance else {} + ) - # Fetch the serialized substate from redis. - redis_state = await self.redis.get(token) + # Determine which states from the tree need to be fetched. + required_state_classes = sorted( + self._get_required_state_classes(state_cls, subclasses=True) + - {type(s) for s in flat_state_tree.values()}, + key=lambda x: x.get_full_name(), + ) - if redis_state is not None: - # Deserialize the substate. - with contextlib.suppress(StateSchemaMismatchError): - state = BaseState._deserialize(data=redis_state) - if state is None: - # Key didn't exist or schema mismatch so create a new instance for this token. - state = state_cls( - init_substates=False, - _reflex_internal_init=True, - ) - # Populate parent state if missing and requested. - if parent_state is None: - parent_state = await self._get_parent_state(token, state) - # Set up Bidirectional linkage between this state and its parent. - if parent_state is not None: - parent_state.substates[state.get_name()] = state - state.parent_state = parent_state - # Avoid fetching substates multiple times. - if cached_substates: - for substate in cached_substates: - state.substates[substate.get_name()] = substate - if substate.parent_state is None: - substate.parent_state = state - # Populate substates if requested. - await self._populate_substates(token, state, all_substates=get_substates) + redis_pipeline = self.redis.pipeline() + for state_cls in required_state_classes: + redis_pipeline.get(_substate_key(token, state_cls)) + + for state_cls, redis_state in zip( + required_state_classes, + await redis_pipeline.execute(), + strict=False, + ): + state = None + + if redis_state is not None: + # Deserialize the substate. + with contextlib.suppress(StateSchemaMismatchError): + state = BaseState._deserialize(data=redis_state) + if state is None: + # Key didn't exist or schema mismatch so create a new instance for this token. + state = state_cls( + init_substates=False, + _reflex_internal_init=True, + ) + flat_state_tree[state.get_full_name()] = state + if state.get_parent_state() is not None: + parent_state_name, _dot, state_name = state.get_full_name().rpartition( + "." + ) + parent_state = flat_state_tree.get(parent_state_name) + if parent_state is None: + raise RuntimeError( + f"Parent state for {state.get_full_name()} was not found " + "in the state tree, but should have already been fetched. " + "This is a bug", + ) + parent_state.substates[state_name] = state + state.parent_state = parent_state # To retain compatibility with previous implementation, by default, we return - # the top-level state by chasing `parent_state` pointers up the tree. + # the top-level state which should always be fetched or already cached. if top_level: - return state._get_root_state() - return state + return flat_state_tree[self.state.get_full_name()] + return flat_state_tree[state_cls.get_full_name()] @override async def set_state( @@ -3365,6 +3408,17 @@ class StateManagerRedis(StateManager): f"`app.state_manager.lock_expiration` (currently {self.lock_expiration}) " "or use `@rx.event(background=True)` decorator for long-running tasks." ) + elif lock_id is not None: + time_taken = self.lock_expiration / 1000 - ( + await self.redis.ttl(self._lock_key(token)) + ) + if time_taken > self.lock_warning_threshold / 1000: + console.warn( + f"Lock for token {token} was held too long {time_taken=}s, " + f"use `@rx.event(background=True)` decorator for long-running tasks.", + dedupe=True, + ) + client_token, substate_name = _split_substate_key(token) # If the substate name on the token doesn't match the instance name, it cannot have a parent. if state.parent_state is not None and state.get_full_name() != substate_name: @@ -3373,17 +3427,16 @@ class StateManagerRedis(StateManager): ) # Recursively set_state on all known substates. - tasks = [] - for substate in state.substates.values(): - tasks.append( - asyncio.create_task( - self.set_state( - token=_substate_key(client_token, substate), - state=substate, - lock_id=lock_id, - ) + tasks = [ + asyncio.create_task( + self.set_state( + _substate_key(client_token, substate), + substate, + lock_id, ) ) + for substate in state.substates.values() + ] # Persist only the given state (parents or substates are excluded by BaseState.__getstate__). if state._get_was_touched(): pickle_state = state._serialize() @@ -3414,6 +3467,29 @@ class StateManagerRedis(StateManager): yield state await self.set_state(token, state, lock_id) + @validator("lock_warning_threshold") + @classmethod + def validate_lock_warning_threshold( + cls, lock_warning_threshold: int, values: dict[str, int] + ): + """Validate the lock warning threshold. + + Args: + lock_warning_threshold: The lock warning threshold. + values: The validated attributes. + + Returns: + The lock warning threshold. + + Raises: + InvalidLockWarningThresholdError: If the lock warning threshold is invalid. + """ + if lock_warning_threshold >= (lock_expiration := values["lock_expiration"]): + raise InvalidLockWarningThresholdError( + f"The lock warning threshold({lock_warning_threshold}) must be less than the lock expiration time({lock_expiration})." + ) + return lock_warning_threshold + @staticmethod def _lock_key(token: str) -> bytes: """Get the redis key for a token's lock. @@ -3445,6 +3521,35 @@ class StateManagerRedis(StateManager): nx=True, # only set if it doesn't exist ) + async def _get_pubsub_message( + self, pubsub: PubSub, timeout: float | None = None + ) -> None: + """Get lock release events from the pubsub. + + Args: + pubsub: The pubsub to get a message from. + timeout: Remaining time to wait for a message. + + Returns: + The message. + """ + if timeout is None: + timeout = self.lock_expiration / 1000.0 + + started = time.time() + message = await pubsub.get_message( + ignore_subscribe_messages=True, + timeout=timeout, + ) + if ( + message is None + or message["data"] not in self._redis_keyspace_lock_release_events + ): + remaining = timeout - (time.time() - started) + if remaining <= 0: + return + await self._get_pubsub_message(pubsub, timeout=remaining) + async def _wait_lock(self, lock_key: bytes, lock_id: bytes) -> None: """Wait for a redis lock to be released via pubsub. @@ -3457,7 +3562,6 @@ class StateManagerRedis(StateManager): Raises: ResponseError: when the keyspace config cannot be set. """ - state_is_locked = False lock_key_channel = f"__keyspace@0__:{lock_key.decode()}" # Enable keyspace notifications for the lock key, so we know when it is available. try: @@ -3471,20 +3575,13 @@ class StateManagerRedis(StateManager): raise async with self.redis.pubsub() as pubsub: await pubsub.psubscribe(lock_key_channel) - while not state_is_locked: - # wait for the lock to be released - while True: - if not await self.redis.exists(lock_key): - break # key was removed, try to get the lock again - message = await pubsub.get_message( - ignore_subscribe_messages=True, - timeout=self.lock_expiration / 1000.0, - ) - if message is None: - continue - if message["data"] in self._redis_keyspace_lock_release_events: - break - state_is_locked = await self._try_get_lock(lock_key, lock_id) + # wait for the lock to be released + while True: + # fast path + if await self._try_get_lock(lock_key, lock_id): + return + # wait for lock events + await self._get_pubsub_message(pubsub) @contextlib.asynccontextmanager async def _lock(self, token: str): @@ -3535,41 +3632,40 @@ def get_state_manager() -> StateManager: Returns: The state manager. """ - app = getattr(prerequisites.get_app(), constants.CompileVars.APP) - return app.state_manager + return prerequisites.get_and_validate_app().app.state_manager class MutableProxy(wrapt.ObjectProxy): """A proxy for a mutable object that tracks changes.""" + # Hint for finding the base class of the proxy. + __base_proxy__ = "MutableProxy" + # Methods on wrapped objects which should mark the state as dirty. - __mark_dirty_attrs__ = set( - [ - "add", - "append", - "clear", - "difference_update", - "discard", - "extend", - "insert", - "intersection_update", - "pop", - "popitem", - "remove", - "reverse", - "setdefault", - "sort", - "symmetric_difference_update", - "update", - ] - ) + __mark_dirty_attrs__ = { + "add", + "append", + "clear", + "difference_update", + "discard", + "extend", + "insert", + "intersection_update", + "pop", + "popitem", + "remove", + "reverse", + "setdefault", + "sort", + "symmetric_difference_update", + "update", + } + # Methods on wrapped objects might return mutable objects that should be tracked. - __wrap_mutable_attrs__ = set( - [ - "get", - "setdefault", - ] - ) + __wrap_mutable_attrs__ = { + "get", + "setdefault", + } # These internal attributes on rx.Base should NOT be wrapped in a MutableProxy. __never_wrap_base_attrs__ = set(Base.__dict__) - {"set"} | set( @@ -3587,6 +3683,39 @@ class MutableProxy(wrapt.ObjectProxy): BaseModelV1, ) + # Dynamically generated classes for tracking dataclass mutations. + __dataclass_proxies__: Dict[type, type] = {} + + def __new__(cls, wrapped: Any, *args, **kwargs) -> MutableProxy: + """Create a proxy instance for a mutable object that tracks changes. + + Args: + wrapped: The object to proxy. + *args: Other args passed to MutableProxy (ignored). + **kwargs: Other kwargs passed to MutableProxy (ignored). + + Returns: + The proxy instance. + """ + if dataclasses.is_dataclass(wrapped): + wrapped_cls = type(wrapped) + wrapper_cls_name = wrapped_cls.__name__ + cls.__name__ + # Find the associated class + if wrapper_cls_name not in cls.__dataclass_proxies__: + # Create a new class that has the __dataclass_fields__ defined + cls.__dataclass_proxies__[wrapper_cls_name] = type( + wrapper_cls_name, + (cls,), + { + dataclasses._FIELDS: getattr( # pyright: ignore [reportAttributeAccessIssue] + wrapped_cls, + dataclasses._FIELDS, # pyright: ignore [reportAttributeAccessIssue] + ), + }, + ) + cls = cls.__dataclass_proxies__[wrapper_cls_name] + return super().__new__(cls) + def __init__(self, wrapped: Any, state: BaseState, field_name: str): """Create a proxy for a mutable object that tracks changes. @@ -3606,14 +3735,14 @@ class MutableProxy(wrapt.ObjectProxy): Returns: The representation of the wrapped object. """ - return f"{self.__class__.__name__}({self.__wrapped__})" + return f"{type(self).__name__}({self.__wrapped__})" def _mark_dirty( self, - wrapped=None, - instance=None, - args=tuple(), - kwargs=None, + wrapped: Callable | None = None, + instance: BaseState | None = None, + args: tuple = (), + kwargs: dict | None = None, ) -> Any: """Mark the state as dirty, then call a wrapped function. @@ -3643,7 +3772,27 @@ class MutableProxy(wrapt.ObjectProxy): Returns: Whether the value is of a mutable type. """ - return isinstance(value, cls.__mutable_types__) + return isinstance(value, cls.__mutable_types__) or ( + dataclasses.is_dataclass(value) and not isinstance(value, Var) + ) + + @staticmethod + def _is_called_from_dataclasses_internal() -> bool: + """Check if the current function is called from dataclasses helper. + + Returns: + Whether the current function is called from dataclasses internal code. + """ + # Walk up the stack a bit to see if we are called from dataclasses + # internal code, for example `asdict` or `astuple`. + frame = inspect.currentframe() + for _ in range(5): + # Why not `inspect.stack()` -- this is much faster! + if not (frame := frame and frame.f_back): + break + if inspect.getfile(frame) == dataclasses.__file__: + return True + return False def _wrap_recursive(self, value: Any) -> Any: """Wrap a value recursively if it is mutable. @@ -3654,16 +3803,22 @@ class MutableProxy(wrapt.ObjectProxy): Returns: The wrapped value. """ + # When called from dataclasses internal code, return the unwrapped value + if self._is_called_from_dataclasses_internal(): + return value # Recursively wrap mutable types, but do not re-wrap MutableProxy instances. if self._is_mutable_type(value) and not isinstance(value, MutableProxy): - return type(self)( + base_cls = globals()[self.__base_proxy__] + return base_cls( wrapped=value, state=self._self_state, field_name=self._self_field_name, ) return value - def _wrap_recursive_decorator(self, wrapped, instance, args, kwargs) -> Any: + def _wrap_recursive_decorator( + self, wrapped: Callable, instance: BaseState, args: list, kwargs: dict + ) -> Any: """Wrap a function that returns a possibly mutable value. Intended for use with `FunctionWrapper` from the `wrapt` library. @@ -3709,7 +3864,7 @@ class MutableProxy(wrapt.ObjectProxy): ): # Wrap methods called on Base subclasses, which might do _anything_ return wrapt.FunctionWrapper( - functools.partial(value.__func__, self), + functools.partial(value.__func__, self), # pyright: ignore [reportFunctionMemberAccess] self._wrap_recursive_decorator, ) @@ -3722,7 +3877,7 @@ class MutableProxy(wrapt.ObjectProxy): return value - def __getitem__(self, key) -> Any: + def __getitem__(self, key: Any) -> Any: """Get the item on the proxied object and return a proxy if mutable. Args: @@ -3745,7 +3900,7 @@ class MutableProxy(wrapt.ObjectProxy): # Recursively wrap mutable items retrieved through this proxy. yield self._wrap_recursive(value) - def __delattr__(self, name): + def __delattr__(self, name: str): """Delete the attribute on the proxied object and mark state dirty. Args: @@ -3753,7 +3908,7 @@ class MutableProxy(wrapt.ObjectProxy): """ self._mark_dirty(super().__delattr__, args=(name,)) - def __delitem__(self, key): + def __delitem__(self, key: str): """Delete the item on the proxied object and mark state dirty. Args: @@ -3761,7 +3916,7 @@ class MutableProxy(wrapt.ObjectProxy): """ self._mark_dirty(super().__delitem__, args=(key,)) - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: Any): """Set the item on the proxied object and mark state dirty. Args: @@ -3770,7 +3925,7 @@ class MutableProxy(wrapt.ObjectProxy): """ self._mark_dirty(super().__setitem__, args=(key, value)) - def __setattr__(self, name, value): + def __setattr__(self, name: str, value: Any): """Set the attribute on the proxied object and mark state dirty. If the attribute starts with "_self_", then the state is NOT marked @@ -3794,7 +3949,7 @@ class MutableProxy(wrapt.ObjectProxy): """ return copy.copy(self.__wrapped__) - def __deepcopy__(self, memo=None) -> Any: + def __deepcopy__(self, memo: dict[int, Any] | None = None) -> Any: """Return a deepcopy of the proxy. Args: @@ -3805,7 +3960,7 @@ class MutableProxy(wrapt.ObjectProxy): """ return copy.deepcopy(self.__wrapped__, memo=memo) - def __reduce_ex__(self, protocol_version): + def __reduce_ex__(self, protocol_version: SupportsIndex): """Get the state for redis serialization. This method is called by cloudpickle to serialize the object. @@ -3834,10 +3989,10 @@ def serialize_mutable_proxy(mp: MutableProxy): return mp.__wrapped__ -_orig_json_JSONEncoder_default = json.JSONEncoder.default +_orig_json_encoder_default = json.JSONEncoder.default -def _json_JSONEncoder_default_wrapper(self: json.JSONEncoder, o: Any) -> Any: +def _json_encoder_default_wrapper(self: json.JSONEncoder, o: Any) -> Any: """Wrap JSONEncoder.default to handle MutableProxy objects. Args: @@ -3851,10 +4006,10 @@ def _json_JSONEncoder_default_wrapper(self: json.JSONEncoder, o: Any) -> Any: return o.__wrapped__ except AttributeError: pass - return _orig_json_JSONEncoder_default(self, o) + return _orig_json_encoder_default(self, o) -json.JSONEncoder.default = _json_JSONEncoder_default_wrapper +json.JSONEncoder.default = _json_encoder_default_wrapper class ImmutableMutableProxy(MutableProxy): @@ -3864,12 +4019,15 @@ class ImmutableMutableProxy(MutableProxy): to modify the wrapped object when the StateProxy is immutable. """ + # Ensure that recursively wrapped proxies use ImmutableMutableProxy as base. + __base_proxy__ = "ImmutableMutableProxy" + def _mark_dirty( self, - wrapped=None, - instance=None, - args=tuple(), - kwargs=None, + wrapped: Callable | None = None, + instance: BaseState | None = None, + args: tuple = (), + kwargs: dict | None = None, ) -> Any: """Raise an exception when an attempt is made to modify the object. @@ -3920,12 +4078,19 @@ def reload_state_module( state: Recursive argument for the state class to reload. """ + # Clean out all potentially dirty states of reloaded modules. + for pd_state in tuple(state._potentially_dirty_states): + with contextlib.suppress(ValueError): + if ( + state.get_root_state().get_class_substate(pd_state).__module__ == module + and module is not None + ): + state._potentially_dirty_states.remove(pd_state) for subclass in tuple(state.class_subclasses): reload_state_module(module=module, state=subclass) if subclass.__module__ == module and module is not None: state.class_subclasses.remove(subclass) state._always_dirty_substates.discard(subclass.get_name()) - state._computed_var_dependencies = defaultdict(set) - state._substate_var_dependencies = defaultdict(set) + state._var_dependencies = {} state._init_var_dependency_dicts() state.get_class_substate.cache_clear() diff --git a/reflex/style.py b/reflex/style.py index 642d126ca..192835ca3 100644 --- a/reflex/style.py +++ b/reflex/style.py @@ -78,7 +78,7 @@ def set_color_mode( _var_data=VarData.merge( base_setter._get_all_var_data(), new_color_mode._get_all_var_data() ), - ).to(FunctionVar, EventChain) # type: ignore + ).to(FunctionVar, EventChain) # Var resolves to the current color mode for the app ("light", "dark" or "system") @@ -138,9 +138,6 @@ def convert_item( if isinstance(style_item, Var): return style_item, style_item._get_all_var_data() - # if isinstance(style_item, str) and REFLEX_VAR_OPENING_TAG not in style_item: - # return style_item, None - # Otherwise, convert to Var to collapse VarData encoded in f-string. new_var = LiteralVar.create(style_item) var_data = new_var._get_all_var_data() if new_var is not None else None @@ -185,7 +182,9 @@ def convert( var_data = None # Track import/hook data from any Vars in the style dict. out = {} - def update_out_dict(return_value, keys_to_update): + def update_out_dict( + return_value: Var | dict | list | str, keys_to_update: tuple[str, ...] + ): for k in keys_to_update: out[k] = return_value @@ -290,9 +289,31 @@ class Style(dict): _var = LiteralVar.create(value) if _var is not None: # Carry the imports/hooks when setting a Var as a value. - self._var_data = VarData.merge(self._var_data, _var._get_all_var_data()) + self._var_data = VarData.merge( + getattr(self, "_var_data", None), _var._get_all_var_data() + ) super().__setitem__(key, value) + def __or__(self, other: Style | dict) -> Style: + """Combine two styles. + + Args: + other: The other style to combine. + + Returns: + The combined style. + """ + other_var_data = None + if not isinstance(other, Style): + other_dict, other_var_data = convert(other) + else: + other_dict, other_var_data = other, other._var_data + + new_style = Style(super().__or__(other_dict)) + if self._var_data or other_var_data: + new_style._var_data = VarData.merge(self._var_data, other_var_data) + return new_style + def _format_emotion_style_pseudo_selector(key: str) -> str: """Format a pseudo selector for emotion CSS-in-JS. diff --git a/reflex/testing.py b/reflex/testing.py index 319be7cba..25f9e7aac 100644 --- a/reflex/testing.py +++ b/reflex/testing.py @@ -8,7 +8,6 @@ import dataclasses import functools import inspect import os -import pathlib import platform import re import signal @@ -20,6 +19,7 @@ import threading import time import types from http.server import SimpleHTTPRequestHandler +from pathlib import Path from typing import ( TYPE_CHECKING, Any, @@ -52,6 +52,7 @@ from reflex.state import ( StateManagerRedis, reload_state_module, ) +from reflex.utils import console try: from selenium import webdriver # pyright: ignore [reportMissingImports] @@ -79,17 +80,17 @@ T = TypeVar("T") TimeoutType = Optional[Union[int, float]] if platform.system() == "Windows": - FRONTEND_POPEN_ARGS["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore + FRONTEND_POPEN_ARGS["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP # pyright: ignore [reportAttributeAccessIssue] FRONTEND_POPEN_ARGS["shell"] = True else: FRONTEND_POPEN_ARGS["start_new_session"] = True # borrowed from py3.11 -class chdir(contextlib.AbstractContextManager): +class chdir(contextlib.AbstractContextManager): # noqa: N801 """Non thread-safe context manager to change the current working directory.""" - def __init__(self, path): + def __init__(self, path: str | Path): """Prepare contextmanager. Args: @@ -100,7 +101,7 @@ class chdir(contextlib.AbstractContextManager): def __enter__(self): """Save current directory and perform chdir.""" - self._old_cwd.append(os.getcwd()) + self._old_cwd.append(Path.cwd()) os.chdir(self.path) def __exit__(self, *excinfo): @@ -120,8 +121,8 @@ class AppHarness: app_source: Optional[ Callable[[], None] | types.ModuleType | str | functools.partial[Any] ] - app_path: pathlib.Path - app_module_path: pathlib.Path + app_path: Path + app_module_path: Path app_module: Optional[types.ModuleType] = None app_instance: Optional[reflex.App] = None frontend_process: Optional[subprocess.Popen] = None @@ -136,7 +137,7 @@ class AppHarness: @classmethod def create( cls, - root: pathlib.Path, + root: Path, app_source: Optional[ Callable[[], None] | types.ModuleType | str | functools.partial[Any] ] = None, @@ -206,7 +207,7 @@ class AppHarness: The full state name """ # NOTE: using State.get_name() somehow causes trouble here - # path = [State.get_name()] + [self.get_state_name(p) for p in path] + # path = [State.get_name()] + [self.get_state_name(p) for p in path] # noqa: ERA001 path = ["reflex___state____state"] + [self.get_state_name(p) for p in path] return ".".join(path) @@ -257,7 +258,7 @@ class AppHarness: if self.app_source is not None: app_globals = self._get_globals_from_signature(self.app_source) if isinstance(self.app_source, functools.partial): - self.app_source = self.app_source.func # type: ignore + self.app_source = self.app_source.func # get the source from a function or module object source_code = "\n".join( [ @@ -281,6 +282,7 @@ class AppHarness: before_decorated_pages = reflex.app.DECORATED_PAGES[self.app_name].copy() # Ensure the AppHarness test does not skip State assignment due to running via pytest os.environ.pop(reflex.constants.PYTEST_CURRENT_TEST, None) + os.environ[reflex.constants.APP_HARNESS_FLAG] = "true" self.app_module = reflex.utils.prerequisites.get_compiled_app( # Do not reload the module for pre-existing apps (only apps generated from source) reload=self.app_source is not None @@ -292,11 +294,15 @@ class AppHarness: if p not in before_decorated_pages ] self.app_instance = self.app_module.app - if isinstance(self.app_instance._state_manager, StateManagerRedis): + if self.app_instance and isinstance( + self.app_instance._state_manager, StateManagerRedis + ): # Create our own redis connection for testing. - self.state_manager = StateManagerRedis.create(self.app_instance.state) + self.state_manager = StateManagerRedis.create(self.app_instance._state) # pyright: ignore [reportArgumentType] else: - self.state_manager = self.app_instance._state_manager + self.state_manager = ( + self.app_instance._state_manager if self.app_instance else None + ) def _reload_state_module(self): """Reload the rx.State module to avoid conflict when reloading.""" @@ -321,8 +327,8 @@ class AppHarness: return _shutdown_redis - def _start_backend(self, port=0): - if self.app_instance is None: + def _start_backend(self, port: int = 0): + if self.app_instance is None or self.app_instance.api is None: raise RuntimeError("App was not initialized.") self.backend = uvicorn.Server( uvicorn.Config( @@ -351,12 +357,12 @@ class AppHarness: self.app_instance.state_manager, StateManagerRedis, ) - and self.app_instance.state is not None + and self.app_instance._state is not None ): with contextlib.suppress(RuntimeError): await self.app_instance.state_manager.close() self.app_instance._state_manager = StateManagerRedis.create( - state=self.app_instance.state, + state=self.app_instance._state, ) if not isinstance(self.app_instance.state_manager, StateManagerRedis): raise RuntimeError("Failed to reset state manager.") @@ -385,7 +391,7 @@ class AppHarness: ) if not line: break - print(line) # for pytest diagnosis + print(line) # for pytest diagnosis #noqa: T201 m = re.search(reflex.constants.Next.FRONTEND_LISTENING_REGEX, line) if m is not None: self.frontend_url = m.group(1) @@ -403,11 +409,10 @@ class AppHarness: ) # catch I/O operation on closed file. except ValueError as e: - print(e) + console.error(str(e)) break if not line: break - print(line) self.frontend_output_thread = threading.Thread(target=consume_frontend_output) self.frontend_output_thread.start() @@ -425,7 +430,7 @@ class AppHarness: return self @staticmethod - def get_app_global_source(key, value): + def get_app_global_source(key: str, value: Any): """Get the source code of a global object. If value is a function or class we render the actual source of value otherwise we assign value to key. @@ -436,7 +441,6 @@ class AppHarness: Returns: The rendered app global code. - """ if not inspect.isclass(value) and not inspect.isfunction(value): return f"{key} = {value!r}" @@ -621,23 +625,23 @@ class AppHarness: want_headless = True if driver_clz is None: requested_driver = environment.APP_HARNESS_DRIVER.get() - driver_clz = getattr(webdriver, requested_driver) + driver_clz = getattr(webdriver, requested_driver) # pyright: ignore [reportPossiblyUnboundVariable] if driver_options is None: - driver_options = getattr(webdriver, f"{requested_driver}Options")() - if driver_clz is webdriver.Chrome: + driver_options = getattr(webdriver, f"{requested_driver}Options")() # pyright: ignore [reportPossiblyUnboundVariable] + if driver_clz is webdriver.Chrome: # pyright: ignore [reportPossiblyUnboundVariable] if driver_options is None: - driver_options = webdriver.ChromeOptions() + driver_options = webdriver.ChromeOptions() # pyright: ignore [reportPossiblyUnboundVariable] driver_options.add_argument("--class=AppHarness") if want_headless: driver_options.add_argument("--headless=new") - elif driver_clz is webdriver.Firefox: + elif driver_clz is webdriver.Firefox: # pyright: ignore [reportPossiblyUnboundVariable] if driver_options is None: - driver_options = webdriver.FirefoxOptions() + driver_options = webdriver.FirefoxOptions() # pyright: ignore [reportPossiblyUnboundVariable] if want_headless: driver_options.add_argument("-headless") - elif driver_clz is webdriver.Edge: + elif driver_clz is webdriver.Edge: # pyright: ignore [reportPossiblyUnboundVariable] if driver_options is None: - driver_options = webdriver.EdgeOptions() + driver_options = webdriver.EdgeOptions() # pyright: ignore [reportPossiblyUnboundVariable] if want_headless: driver_options.add_argument("headless") if driver_options is None: @@ -653,7 +657,7 @@ class AppHarness: driver_options.set_capability(key, value) if driver_kwargs is None: driver_kwargs = {} - driver = driver_clz(options=driver_options, **driver_kwargs) # type: ignore + driver = driver_clz(options=driver_options, **driver_kwargs) # pyright: ignore [reportOptionalCall, reportArgumentType] driver.get(self.frontend_url) self._frontends.append(driver) return driver @@ -815,7 +819,7 @@ class AppHarness: class SimpleHTTPRequestHandlerCustomErrors(SimpleHTTPRequestHandler): """SimpleHTTPRequestHandler with custom error page handling.""" - def __init__(self, *args, error_page_map: dict[int, pathlib.Path], **kwargs): + def __init__(self, *args, error_page_map: dict[int, Path], **kwargs): """Initialize the handler. Args: @@ -858,8 +862,8 @@ class Subdir404TCPServer(socketserver.TCPServer): def __init__( self, *args, - root: pathlib.Path, - error_page_map: dict[int, pathlib.Path] | None, + root: Path, + error_page_map: dict[int, Path] | None, **kwargs, ): """Initialize the server. @@ -885,8 +889,8 @@ class Subdir404TCPServer(socketserver.TCPServer): request, client_address, self, - directory=str(self.root), # type: ignore - error_page_map=self.error_page_map, # type: ignore + directory=str(self.root), # pyright: ignore [reportCallIssue] + error_page_map=self.error_page_map, # pyright: ignore [reportCallIssue] ) @@ -933,6 +937,7 @@ class AppHarnessProd(AppHarness): frontend=True, backend=False, loglevel=reflex.constants.LogLevel.INFO, + env=reflex.constants.Env.PROD, ) self.frontend_thread = threading.Thread(target=self._run_frontend) diff --git a/reflex/utils/build.py b/reflex/utils/build.py index 14709d99c..9e35ab984 100644 --- a/reflex/utils/build.py +++ b/reflex/utils/build.py @@ -13,17 +13,21 @@ from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn from reflex import constants from reflex.config import get_config from reflex.utils import console, path_ops, prerequisites, processes +from reflex.utils.exec import is_in_app_harness def set_env_json(): """Write the upload url to a REFLEX_JSON.""" path_ops.update_json_file( str(prerequisites.get_web_dir() / constants.Dirs.ENV_JSON), - {endpoint.name: endpoint.get_url() for endpoint in constants.Endpoint}, + { + **{endpoint.name: endpoint.get_url() for endpoint in constants.Endpoint}, + "TEST_MODE": is_in_app_harness(), + }, ) -def generate_sitemap_config(deploy_url: str, export=False): +def generate_sitemap_config(deploy_url: str, export: bool = False): """Generate the sitemap config file. Args: @@ -150,7 +154,7 @@ def zip_app( _zip( component_name=constants.ComponentName.BACKEND, target=zip_dest_dir / constants.ComponentName.BACKEND.zip(), - root_dir=Path("."), + root_dir=Path.cwd(), dirs_to_exclude={"__pycache__"}, files_to_exclude=files_to_exclude, top_level_dirs_to_exclude={"assets"}, diff --git a/reflex/utils/codespaces.py b/reflex/utils/codespaces.py index 7ff686129..bb5286e31 100644 --- a/reflex/utils/codespaces.py +++ b/reflex/utils/codespaces.py @@ -42,10 +42,7 @@ def codespaces_port_forwarding_domain() -> str | None: Returns: The domain for port forwarding in Github Codespaces, or None if not running in Codespaces. """ - GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = os.getenv( - "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN" - ) - return GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN + return os.getenv("GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN") def is_running_in_codespaces() -> bool: diff --git a/reflex/utils/compat.py b/reflex/utils/compat.py index e63492a6b..e4fb5eb35 100644 --- a/reflex/utils/compat.py +++ b/reflex/utils/compat.py @@ -2,6 +2,7 @@ import contextlib import sys +from typing import Any async def windows_hot_reload_lifespan_hack(): @@ -50,11 +51,11 @@ def pydantic_v1_patch(): ] originals = {module: sys.modules.get(module) for module in patched_modules} try: - import pydantic.v1 # type: ignore + import pydantic.v1 - sys.modules["pydantic.fields"] = pydantic.v1.fields # type: ignore - sys.modules["pydantic.main"] = pydantic.v1.main # type: ignore - sys.modules["pydantic.errors"] = pydantic.v1.errors # type: ignore + sys.modules["pydantic.fields"] = pydantic.v1.fields # pyright: ignore [reportAttributeAccessIssue] + sys.modules["pydantic.main"] = pydantic.v1.main # pyright: ignore [reportAttributeAccessIssue] + sys.modules["pydantic.errors"] = pydantic.v1.errors # pyright: ignore [reportAttributeAccessIssue] sys.modules["pydantic"] = pydantic.v1 yield except (ImportError, AttributeError): @@ -74,7 +75,7 @@ with pydantic_v1_patch(): import sqlmodel as sqlmodel -def sqlmodel_field_has_primary_key(field) -> bool: +def sqlmodel_field_has_primary_key(field: Any) -> bool: """Determines if a field is a priamary. Args: diff --git a/reflex/utils/console.py b/reflex/utils/console.py index b3ba7163d..d5b7a0d6e 100644 --- a/reflex/utils/console.py +++ b/reflex/utils/console.py @@ -2,6 +2,11 @@ from __future__ import annotations +import inspect +import shutil +from pathlib import Path +from types import FrameType + from rich.console import Console from rich.progress import MofNCompleteColumn, Progress, TimeElapsedColumn from rich.prompt import Prompt @@ -20,6 +25,24 @@ _EMITTED_DEPRECATION_WARNINGS = set() # Info messages which have been printed. _EMITTED_INFO = set() +# Warnings which have been printed. +_EMIITED_WARNINGS = set() + +# Errors which have been printed. +_EMITTED_ERRORS = set() + +# Success messages which have been printed. +_EMITTED_SUCCESS = set() + +# Debug messages which have been printed. +_EMITTED_DEBUG = set() + +# Logs which have been printed. +_EMITTED_LOGS = set() + +# Prints which have been printed. +_EMITTED_PRINTS = set() + def set_log_level(log_level: LogLevel): """Set the log level. @@ -28,20 +51,12 @@ def set_log_level(log_level: LogLevel): log_level: The log level to set. Raises: - ValueError: If the log level is invalid. + TypeError: If the log level is a string. """ if not isinstance(log_level, LogLevel): - deprecate( - feature_name="Passing a string to set_log_level", - reason="use reflex.constants.LogLevel enum instead", - deprecation_version="0.6.6", - removal_version="0.7.0", + raise TypeError( + f"log_level must be a LogLevel enum value, got {log_level} of type {type(log_level)} instead." ) - try: - log_level = getattr(LogLevel, log_level.upper()) - except AttributeError as ae: - raise ValueError(f"Invalid log level: {log_level}") from ae - global _LOG_LEVEL _LOG_LEVEL = log_level @@ -55,25 +70,37 @@ def is_debug() -> bool: return _LOG_LEVEL <= LogLevel.DEBUG -def print(msg: str, **kwargs): +def print(msg: str, dedupe: bool = False, **kwargs): """Print a message. Args: msg: The message to print. + dedupe: If True, suppress multiple console logs of print message. kwargs: Keyword arguments to pass to the print function. """ + if dedupe: + if msg in _EMITTED_PRINTS: + return + else: + _EMITTED_PRINTS.add(msg) _console.print(msg, **kwargs) -def debug(msg: str, **kwargs): +def debug(msg: str, dedupe: bool = False, **kwargs): """Print a debug message. Args: msg: The debug message. + dedupe: If True, suppress multiple console logs of debug message. kwargs: Keyword arguments to pass to the print function. """ if is_debug(): msg_ = f"[purple]Debug: {msg}[/purple]" + if dedupe: + if msg_ in _EMITTED_DEBUG: + return + else: + _EMITTED_DEBUG.add(msg_) if progress := kwargs.pop("progress", None): progress.console.print(msg_, **kwargs) else: @@ -97,25 +124,37 @@ def info(msg: str, dedupe: bool = False, **kwargs): print(f"[cyan]Info: {msg}[/cyan]", **kwargs) -def success(msg: str, **kwargs): +def success(msg: str, dedupe: bool = False, **kwargs): """Print a success message. Args: msg: The success message. + dedupe: If True, suppress multiple console logs of success message. kwargs: Keyword arguments to pass to the print function. """ if _LOG_LEVEL <= LogLevel.INFO: + if dedupe: + if msg in _EMITTED_SUCCESS: + return + else: + _EMITTED_SUCCESS.add(msg) print(f"[green]Success: {msg}[/green]", **kwargs) -def log(msg: str, **kwargs): +def log(msg: str, dedupe: bool = False, **kwargs): """Takes a string and logs it to the console. Args: msg: The message to log. + dedupe: If True, suppress multiple console logs of log message. kwargs: Keyword arguments to pass to the print function. """ if _LOG_LEVEL <= LogLevel.INFO: + if dedupe: + if msg in _EMITTED_LOGS: + return + else: + _EMITTED_LOGS.add(msg) _console.log(msg, **kwargs) @@ -129,17 +168,50 @@ def rule(title: str, **kwargs): _console.rule(title, **kwargs) -def warn(msg: str, **kwargs): +def warn(msg: str, dedupe: bool = False, **kwargs): """Print a warning message. Args: msg: The warning message. + dedupe: If True, suppress multiple console logs of warning message. kwargs: Keyword arguments to pass to the print function. """ if _LOG_LEVEL <= LogLevel.WARNING: + if dedupe: + if msg in _EMIITED_WARNINGS: + return + else: + _EMIITED_WARNINGS.add(msg) print(f"[orange1]Warning: {msg}[/orange1]", **kwargs) +def _get_first_non_framework_frame() -> FrameType | None: + import click + import typer + import typing_extensions + + import reflex as rx + + # Exclude utility modules that should never be the source of deprecated reflex usage. + exclude_modules = [click, rx, typer, typing_extensions] + exclude_roots = [ + p.parent.resolve() + if (p := Path(m.__file__)).name == "__init__.py" # pyright: ignore [reportArgumentType] + else p.resolve() + for m in exclude_modules + ] + # Specifically exclude the reflex cli module. + if reflex_bin := shutil.which(b"reflex"): + exclude_roots.append(Path(reflex_bin.decode())) + + frame = inspect.currentframe() + while frame := frame and frame.f_back: + frame_path = Path(inspect.getfile(frame)).resolve() + if not any(frame_path.is_relative_to(root) for root in exclude_roots): + break + return frame + + def deprecate( feature_name: str, reason: str, @@ -158,25 +230,43 @@ def deprecate( dedupe: If True, suppress multiple console logs of deprecation message. kwargs: Keyword arguments to pass to the print function. """ - if feature_name not in _EMITTED_DEPRECATION_WARNINGS: + dedupe_key = feature_name + loc = "" + + # See if we can find where the deprecation exists in "user code" + origin_frame = _get_first_non_framework_frame() + if origin_frame is not None: + filename = Path(origin_frame.f_code.co_filename) + if filename.is_relative_to(Path.cwd()): + filename = filename.relative_to(Path.cwd()) + loc = f"{filename}:{origin_frame.f_lineno}" + dedupe_key = f"{dedupe_key} {loc}" + + if dedupe_key not in _EMITTED_DEPRECATION_WARNINGS: msg = ( f"{feature_name} has been deprecated in version {deprecation_version} {reason.rstrip('.')}. It will be completely " - f"removed in {removal_version}" + f"removed in {removal_version}. ({loc})" ) if _LOG_LEVEL <= LogLevel.WARNING: print(f"[yellow]DeprecationWarning: {msg}[/yellow]", **kwargs) if dedupe: - _EMITTED_DEPRECATION_WARNINGS.add(feature_name) + _EMITTED_DEPRECATION_WARNINGS.add(dedupe_key) -def error(msg: str, **kwargs): +def error(msg: str, dedupe: bool = False, **kwargs): """Print an error message. Args: msg: The error message. + dedupe: If True, suppress multiple console logs of error message. kwargs: Keyword arguments to pass to the print function. """ if _LOG_LEVEL <= LogLevel.ERROR: + if dedupe: + if msg in _EMITTED_ERRORS: + return + else: + _EMITTED_ERRORS.add(msg) print(f"[red]{msg}[/red]", **kwargs) @@ -185,7 +275,7 @@ def ask( choices: list[str] | None = None, default: str | None = None, show_choices: bool = True, -) -> str: +) -> str | None: """Takes a prompt question and optionally a list of choices and returns the user input. @@ -200,7 +290,7 @@ def ask( """ return Prompt.ask( question, choices=choices, default=default, show_choices=show_choices - ) # type: ignore + ) def progress(): diff --git a/reflex/utils/exceptions.py b/reflex/utils/exceptions.py index 714dc912c..05fbb297c 100644 --- a/reflex/utils/exceptions.py +++ b/reflex/utils/exceptions.py @@ -1,6 +1,6 @@ """Custom Exceptions.""" -from typing import NoReturn +from typing import Any class ReflexError(Exception): @@ -11,7 +11,7 @@ class ConfigError(ReflexError): """Custom exception for config related errors.""" -class InvalidStateManagerMode(ReflexError, ValueError): +class InvalidStateManagerModeError(ReflexError, ValueError): """Raised when an invalid state manager mode is provided.""" @@ -31,6 +31,22 @@ class ComponentTypeError(ReflexError, TypeError): """Custom TypeError for component related errors.""" +class ChildrenTypeError(ComponentTypeError): + """Raised when the children prop of a component is not a valid type.""" + + def __init__(self, component: str, child: Any): + """Initialize the exception. + + Args: + component: The name of the component. + child: The child that caused the error. + """ + super().__init__( + f"Component {component} received child {child} of type {type(child)}. " + "Accepted types are other components, state vars, or primitive Python types (dict excluded)." + ) + + class EventHandlerTypeError(ReflexError, TypeError): """Custom TypeError for event handler related errors.""" @@ -59,10 +75,42 @@ class VarAttributeError(ReflexError, AttributeError): """Custom AttributeError for var related errors.""" +class UntypedVarError(ReflexError, TypeError): + """Custom TypeError for untyped var errors.""" + + +class UntypedComputedVarError(ReflexError, TypeError): + """Custom TypeError for untyped computed var errors.""" + + def __init__(self, var_name: str): + """Initialize the UntypedComputedVarError. + + Args: + var_name: The name of the computed var. + """ + super().__init__(f"Computed var '{var_name}' must have a type annotation.") + + +class MissingAnnotationError(ReflexError, TypeError): + """Custom TypeError for missing annotations.""" + + def __init__(self, var_name: str): + """Initialize the MissingAnnotationError. + + Args: + var_name: The name of the var. + """ + super().__init__(f"Var '{var_name}' must have a type annotation.") + + class UploadValueError(ReflexError, ValueError): """Custom ValueError for upload related errors.""" +class PageValueError(ReflexError, ValueError): + """Custom ValueError for page related errors.""" + + class RouteValueError(ReflexError, ValueError): """Custom ValueError for route related errors.""" @@ -91,43 +139,43 @@ class MatchTypeError(ReflexError, TypeError): """Raised when the return types of match cases are different.""" -class EventHandlerArgTypeMismatch(ReflexError, TypeError): +class EventHandlerArgTypeMismatchError(ReflexError, TypeError): """Raised when the annotations of args accepted by an EventHandler differs from the spec of the event trigger.""" -class EventFnArgMismatch(ReflexError, TypeError): +class EventFnArgMismatchError(ReflexError, TypeError): """Raised when the number of args required by an event handler is more than provided by the event trigger.""" -class DynamicRouteArgShadowsStateVar(ReflexError, NameError): +class DynamicRouteArgShadowsStateVarError(ReflexError, NameError): """Raised when a dynamic route arg shadows a state var.""" -class ComputedVarShadowsStateVar(ReflexError, NameError): +class ComputedVarShadowsStateVarError(ReflexError, NameError): """Raised when a computed var shadows a state var.""" -class ComputedVarShadowsBaseVars(ReflexError, NameError): +class ComputedVarShadowsBaseVarsError(ReflexError, NameError): """Raised when a computed var shadows a base var.""" -class EventHandlerShadowsBuiltInStateMethod(ReflexError, NameError): +class EventHandlerShadowsBuiltInStateMethodError(ReflexError, NameError): """Raised when an event handler shadows a built-in state method.""" -class GeneratedCodeHasNoFunctionDefs(ReflexError): +class GeneratedCodeHasNoFunctionDefsError(ReflexError): """Raised when refactored code generated with flexgen has no functions defined.""" -class PrimitiveUnserializableToJSON(ReflexError, ValueError): +class PrimitiveUnserializableToJSONError(ReflexError, ValueError): """Raised when a primitive type is unserializable to JSON. Usually with NaN and Infinity.""" -class InvalidLifespanTaskType(ReflexError, TypeError): +class InvalidLifespanTaskTypeError(ReflexError, TypeError): """Raised when an invalid task type is registered as a lifespan task.""" -class DynamicComponentMissingLibrary(ReflexError, ValueError): +class DynamicComponentMissingLibraryError(ReflexError, ValueError): """Raised when a dynamic component is missing a library.""" @@ -143,7 +191,7 @@ class EnvironmentVarValueError(ReflexError, ValueError): """Raised when an environment variable is set to an invalid value.""" -class DynamicComponentInvalidSignature(ReflexError, TypeError): +class DynamicComponentInvalidSignatureError(ReflexError, TypeError): """Raised when a dynamic component has an invalid signature.""" @@ -155,23 +203,41 @@ class StateTooLargeError(ReflexError): """Raised when the state is too large to be serialized.""" +class StateSerializationError(ReflexError): + """Raised when the state cannot be serialized.""" + + +class StateMismatchError(ReflexError, ValueError): + """Raised when the state retrieved does not match the expected state.""" + + class SystemPackageMissingError(ReflexError): """Raised when a system package is missing.""" + def __init__(self, package: str): + """Initialize the SystemPackageMissingError. -def raise_system_package_missing_error(package: str) -> NoReturn: - """Raise a SystemPackageMissingError. + Args: + package: The missing package. + """ + from reflex.constants import IS_MACOS - Args: - package: The name of the missing system package. + extra = ( + f" You can do so by running 'brew install {package}'." if IS_MACOS else "" + ) + super().__init__( + f"System package '{package}' is missing." + f" Please install it through your system package manager.{extra}" + ) - Raises: - SystemPackageMissingError: The raised exception. - """ - from reflex.constants import IS_MACOS - raise SystemPackageMissingError( - f"System package '{package}' is missing." - " Please install it through your system package manager." - + (f" You can do so by running 'brew install {package}'." if IS_MACOS else "") - ) +class EventDeserializationError(ReflexError, ValueError): + """Raised when an event cannot be deserialized.""" + + +class InvalidLockWarningThresholdError(ReflexError): + """Raised when an invalid lock warning threshold is provided.""" + + +class UnretrievableVarValueError(ReflexError): + """Raised when the value of a var is not retrievable.""" diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index 3e69ecd0b..67df7ea91 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -24,7 +24,7 @@ from reflex.utils.prerequisites import get_web_dir frontend_process = None -def detect_package_change(json_file_path: str) -> str: +def detect_package_change(json_file_path: Path) -> str: """Calculates the SHA-256 hash of a JSON file and returns it as a hexadecimal string. Args: @@ -37,7 +37,7 @@ def detect_package_change(json_file_path: str) -> str: >>> detect_package_change("package.json") 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2' """ - with open(json_file_path, "r") as file: + with json_file_path.open("r") as file: json_data = json.load(file) # Calculate the hash @@ -71,7 +71,9 @@ def notify_backend(): # run_process_and_launch_url is assumed to be used # only to launch the frontend # If this is not the case, might have to change the logic -def run_process_and_launch_url(run_command: list[str], backend_present=True): +def run_process_and_launch_url( + run_command: list[str | None], backend_present: bool = True +): """Run the process and launch the URL. Args: @@ -81,7 +83,7 @@ def run_process_and_launch_url(run_command: list[str], backend_present=True): from reflex.utils import processes json_file_path = get_web_dir() / constants.PackageJson.PATH - last_hash = detect_package_change(str(json_file_path)) + last_hash = detect_package_change(json_file_path) process = None first_run = True @@ -89,7 +91,7 @@ def run_process_and_launch_url(run_command: list[str], backend_present=True): if process is None: kwargs = {} if constants.IS_WINDOWS and backend_present: - kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore + kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP # pyright: ignore [reportAttributeAccessIssue] process = processes.new_process( run_command, cwd=get_web_dir(), @@ -117,14 +119,14 @@ def run_process_and_launch_url(run_command: list[str], backend_present=True): console.print("New packages detected: Updating app...") else: if any( - [x in line for x in ("bin executable does not exist on disk",)] + x in line for x in ("bin executable does not exist on disk",) ): console.error( "Try setting `REFLEX_USE_NPM=1` and re-running `reflex init` and `reflex run` to use npm instead of bun:\n" "`REFLEX_USE_NPM=1 reflex init`\n" "`REFLEX_USE_NPM=1 reflex run`" ) - new_hash = detect_package_change(str(json_file_path)) + new_hash = detect_package_change(json_file_path) if new_hash != last_hash: last_hash = new_hash kill(process.pid) @@ -134,7 +136,7 @@ def run_process_and_launch_url(run_command: list[str], backend_present=True): break # while True -def run_frontend(root: Path, port: str, backend_present=True): +def run_frontend(root: Path, port: str, backend_present: bool = True): """Run the frontend. Args: @@ -151,12 +153,12 @@ def run_frontend(root: Path, port: str, backend_present=True): console.rule("[bold green]App Running") os.environ["PORT"] = str(get_config().frontend_port if port is None else port) run_process_and_launch_url( - [prerequisites.get_package_manager(), "run", "dev"], # type: ignore + [prerequisites.get_package_manager(), "run", "dev"], backend_present, ) -def run_frontend_prod(root: Path, port: str, backend_present=True): +def run_frontend_prod(root: Path, port: str, backend_present: bool = True): """Run the frontend. Args: @@ -173,7 +175,7 @@ def run_frontend_prod(root: Path, port: str, backend_present=True): # Run the frontend in production mode. console.rule("[bold green]App Running") run_process_and_launch_url( - [prerequisites.get_package_manager(), "run", "prod"], # type: ignore + [prerequisites.get_package_manager(), "run", "prod"], backend_present, ) @@ -240,7 +242,32 @@ def run_backend( run_uvicorn_backend(host, port, loglevel) -def run_uvicorn_backend(host, port, loglevel: LogLevel): +def get_reload_dirs() -> list[Path]: + """Get the reload directories for the backend. + + Returns: + The reload directories for the backend. + """ + config = get_config() + reload_dirs = [Path(config.app_name)] + if config.app_module is not None and config.app_module.__file__: + module_path = Path(config.app_module.__file__).resolve().parent + + while module_path.parent.name: + if any( + sibling_file.name == "__init__.py" + for sibling_file in module_path.parent.iterdir() + ): + # go up a level to find dir without `__init__.py` + module_path = module_path.parent + else: + break + + reload_dirs = [module_path] + return reload_dirs + + +def run_uvicorn_backend(host: str, port: int, loglevel: LogLevel): """Run the backend in development mode using Uvicorn. Args: @@ -256,11 +283,11 @@ def run_uvicorn_backend(host, port, loglevel: LogLevel): port=port, log_level=loglevel.value, reload=True, - reload_dirs=[get_config().app_name], + reload_dirs=list(map(str, get_reload_dirs())), ) -def run_granian_backend(host, port, loglevel: LogLevel): +def run_granian_backend(host: str, port: int, loglevel: LogLevel): """Run the backend in development mode using Granian. Args: @@ -270,9 +297,11 @@ def run_granian_backend(host, port, loglevel: LogLevel): """ console.debug("Using Granian for backend") try: - from granian import Granian # type: ignore - from granian.constants import Interfaces # type: ignore - from granian.log import LogLevels # type: ignore + from granian import Granian # pyright: ignore [reportMissingImports] + from granian.constants import ( # pyright: ignore [reportMissingImports] + Interfaces, + ) + from granian.log import LogLevels # pyright: ignore [reportMissingImports] Granian( target=get_granian_target(), @@ -281,8 +310,8 @@ def run_granian_backend(host, port, loglevel: LogLevel): interface=Interfaces.ASGI, log_level=LogLevels(loglevel.value), reload=True, - reload_paths=[Path(get_config().app_name)], - reload_ignore_dirs=[".web"], + reload_paths=get_reload_dirs(), + reload_ignore_dirs=[".web", ".states"], ).serve() except ImportError: console.error( @@ -325,7 +354,7 @@ def run_backend_prod( run_uvicorn_backend_prod(host, port, loglevel) -def run_uvicorn_backend_prod(host, port, loglevel): +def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel): """Run the backend in production mode using Uvicorn. Args: @@ -339,11 +368,11 @@ def run_uvicorn_backend_prod(host, port, loglevel): app_module = get_app_module() - RUN_BACKEND_PROD = f"gunicorn --worker-class {config.gunicorn_worker_class} --max-requests {config.gunicorn_max_requests} --max-requests-jitter {config.gunicorn_max_requests_jitter} --preload --timeout {config.timeout} --log-level critical".split() - RUN_BACKEND_PROD_WINDOWS = f"uvicorn --limit-max-requests {config.gunicorn_max_requests} --timeout-keep-alive {config.timeout}".split() + run_backend_prod = f"gunicorn --worker-class {config.gunicorn_worker_class} --max-requests {config.gunicorn_max_requests} --max-requests-jitter {config.gunicorn_max_requests_jitter} --preload --timeout {config.timeout} --log-level critical".split() + run_backend_prod_windows = f"uvicorn --limit-max-requests {config.gunicorn_max_requests} --timeout-keep-alive {config.timeout}".split() command = ( [ - *RUN_BACKEND_PROD_WINDOWS, + *run_backend_prod_windows, "--host", host, "--port", @@ -352,7 +381,7 @@ def run_uvicorn_backend_prod(host, port, loglevel): ] if constants.IS_WINDOWS else [ - *RUN_BACKEND_PROD, + *run_backend_prod, "--bind", f"{host}:{port}", "--threads", @@ -377,7 +406,7 @@ def run_uvicorn_backend_prod(host, port, loglevel): ) -def run_granian_backend_prod(host, port, loglevel): +def run_granian_backend_prod(host: str, port: int, loglevel: LogLevel): """Run the backend in production mode using Granian. Args: @@ -388,7 +417,9 @@ def run_granian_backend_prod(host, port, loglevel): from reflex.utils import processes try: - from granian.constants import Interfaces # type: ignore + from granian.constants import ( # pyright: ignore [reportMissingImports] + Interfaces, + ) command = [ "granian", @@ -442,22 +473,22 @@ def output_system_info(): system = platform.system() + fnm_info = f"[FNM {prerequisites.get_fnm_version()} (Expected: {constants.Fnm.VERSION}) (PATH: {constants.Fnm.EXE})]" + if system != "Windows" or ( system == "Windows" and prerequisites.is_windows_bun_supported() ): dependencies.extend( [ - f"[FNM {prerequisites.get_fnm_version()} (Expected: {constants.Fnm.VERSION}) (PATH: {constants.Fnm.EXE})]", - f"[Bun {prerequisites.get_bun_version()} (Expected: {constants.Bun.VERSION}) (PATH: {config.bun_path})]", + fnm_info, + f"[Bun {prerequisites.get_bun_version()} (Expected: {constants.Bun.VERSION}) (PATH: {path_ops.get_bun_path()})]", ], ) else: - dependencies.append( - f"[FNM {prerequisites.get_fnm_version()} (Expected: {constants.Fnm.VERSION}) (PATH: {constants.Fnm.EXE})]", - ) + dependencies.append(fnm_info) if system == "Linux": - import distro # type: ignore + import distro # pyright: ignore[reportMissingImports] os_version = distro.name(pretty=True) else: @@ -469,11 +500,11 @@ def output_system_info(): console.debug(f"{dep}") console.debug( - f"Using package installer at: {prerequisites.get_install_package_manager(on_failure_return_none=True)}" # type: ignore + f"Using package installer at: {prerequisites.get_install_package_manager(on_failure_return_none=True)}" ) console.debug( f"Using package executer at: {prerequisites.get_package_manager(on_failure_return_none=True)}" - ) # type: ignore + ) if system != "Windows": console.debug(f"Unzip path: {path_ops.which('unzip')}") @@ -487,6 +518,15 @@ def is_testing_env() -> bool: return constants.PYTEST_CURRENT_TEST in os.environ +def is_in_app_harness() -> bool: + """Whether the app is running in the app harness. + + Returns: + True if the app is running in the app harness. + """ + return constants.APP_HARNESS_FLAG in os.environ + + def is_prod_mode() -> bool: """Check if the app is running in production mode. @@ -495,48 +535,3 @@ def is_prod_mode() -> bool: """ current_mode = environment.REFLEX_ENV_MODE.get() return current_mode == constants.Env.PROD - - -def is_frontend_only() -> bool: - """Check if the app is running in frontend-only mode. - - Returns: - True if the app is running in frontend-only mode. - """ - console.deprecate( - "is_frontend_only() is deprecated and will be removed in a future release.", - reason="Use `environment.REFLEX_FRONTEND_ONLY.get()` instead.", - deprecation_version="0.6.5", - removal_version="0.7.0", - ) - return environment.REFLEX_FRONTEND_ONLY.get() - - -def is_backend_only() -> bool: - """Check if the app is running in backend-only mode. - - Returns: - True if the app is running in backend-only mode. - """ - console.deprecate( - "is_backend_only() is deprecated and will be removed in a future release.", - reason="Use `environment.REFLEX_BACKEND_ONLY.get()` instead.", - deprecation_version="0.6.5", - removal_version="0.7.0", - ) - return environment.REFLEX_BACKEND_ONLY.get() - - -def should_skip_compile() -> bool: - """Whether the app should skip compile. - - Returns: - True if the app should skip compile. - """ - console.deprecate( - "should_skip_compile() is deprecated and will be removed in a future release.", - reason="Use `environment.REFLEX_SKIP_COMPILE.get()` instead.", - deprecation_version="0.6.5", - removal_version="0.7.0", - ) - return environment.REFLEX_SKIP_COMPILE.get() diff --git a/reflex/utils/export.py b/reflex/utils/export.py index 31ac0d0b5..edb4a6e1a 100644 --- a/reflex/utils/export.py +++ b/reflex/utils/export.py @@ -1,11 +1,10 @@ """Export utilities.""" -import os from pathlib import Path from typing import Optional from reflex import constants -from reflex.config import get_config +from reflex.config import environment, get_config from reflex.utils import build, console, exec, prerequisites, telemetry config = get_config() @@ -15,10 +14,11 @@ def export( zipping: bool = True, frontend: bool = True, backend: bool = True, - zip_dest_dir: str = os.getcwd(), + zip_dest_dir: str = str(Path.cwd()), upload_db_file: bool = False, api_url: Optional[str] = None, deploy_url: Optional[str] = None, + env: constants.Env = constants.Env.PROD, loglevel: constants.LogLevel = console._LOG_LEVEL, ): """Export the app to a zip file. @@ -31,11 +31,15 @@ def export( upload_db_file: Whether to upload the database file. Defaults to False. api_url: The API URL to use. Defaults to None. deploy_url: The deploy URL to use. Defaults to None. + env: The environment to use. Defaults to constants.Env.PROD. loglevel: The log level to use. Defaults to console._LOG_LEVEL. """ # Set the log level. console.set_log_level(loglevel) + # Set env mode in the environment + environment.REFLEX_ENV_MODE.set(env) + # Override the config url values if provided. if api_url is not None: config.api_url = str(api_url) diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 6236a883e..6f05e0982 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -11,7 +11,6 @@ from typing import TYPE_CHECKING, Any, List, Optional, Union from reflex import constants from reflex.constants.state import FRONTEND_EVENT_STATE from reflex.utils import exceptions -from reflex.utils.console import deprecate if TYPE_CHECKING: from reflex.components.component import ComponentStyle @@ -221,7 +220,7 @@ def _escape_js_string(string: str) -> str: """ # TODO: we may need to re-vist this logic after new Var API is implemented. - def escape_outside_segments(segment): + def escape_outside_segments(segment: str): """Escape backticks in segments outside of `${}`. Args: @@ -284,7 +283,7 @@ def format_var(var: Var) -> str: return str(var) -def format_route(route: str, format_case=True) -> str: +def format_route(route: str, format_case: bool = True) -> str: """Format the given route. Args: @@ -378,7 +377,7 @@ def format_prop( # For dictionaries, convert any properties to strings. elif isinstance(prop, dict): - prop = serializers.serialize_dict(prop) # type: ignore + prop = serializers.serialize_dict(prop) # pyright: ignore [reportAttributeAccessIssue] else: # Dump the prop as JSON. @@ -502,37 +501,6 @@ if TYPE_CHECKING: from reflex.vars import Var -def format_event_chain( - event_chain: EventChain | Var[EventChain], - event_arg: Var | None = None, -) -> str: - """DEPRECATED: format an event chain as a javascript invocation. - - Use str(rx.Var.create(event_chain)) instead. - - Args: - event_chain: The event chain to format. - event_arg: this argument is ignored. - - Returns: - Compiled javascript code to queue the given event chain on the frontend. - """ - deprecate( - feature_name="format_event_chain", - reason="Use str(rx.Var.create(event_chain)) instead", - deprecation_version="0.6.0", - removal_version="0.7.0", - ) - - from reflex.vars import Var - from reflex.vars.function import ArgsFunctionOperation - - result = Var.create(event_chain) - if isinstance(result, ArgsFunctionOperation): - result = result._return_expr - return str(result) - - def format_queue_events( events: EventType | None = None, args_spec: Optional[ArgsSpec] = None, @@ -565,14 +533,14 @@ def format_queue_events( from reflex.vars import FunctionVar, Var if not events: - return Var("(() => null)").to(FunctionVar, EventChain) # type: ignore + return Var("(() => null)").to(FunctionVar, EventChain) # If no spec is provided, the function will take no arguments. def _default_args_spec(): return [] # Construct the arguments that the function accepts. - sig = inspect.signature(args_spec or _default_args_spec) # type: ignore + sig = inspect.signature(args_spec or _default_args_spec) if sig.parameters: arg_def = ",".join(f"_{p}" for p in sig.parameters) arg_def = f"({arg_def})" @@ -589,7 +557,7 @@ def format_queue_events( if isinstance(spec, (EventHandler, EventSpec)): specs = [call_event_handler(spec, args_spec or _default_args_spec)] elif isinstance(spec, type(lambda: None)): - specs = call_event_fn(spec, args_spec or _default_args_spec) # type: ignore + specs = call_event_fn(spec, args_spec or _default_args_spec) # pyright: ignore [reportAssignmentType, reportArgumentType] if isinstance(specs, Var): raise ValueError( f"Invalid event spec: {specs}. Expected a list of EventSpecs." @@ -601,7 +569,7 @@ def format_queue_events( return Var( f"{arg_def} => {{queueEvents([{','.join(payloads)}], {constants.CompileVars.SOCKET}); " f"processEvent({constants.CompileVars.SOCKET})}}", - ).to(FunctionVar, EventChain) # type: ignore + ).to(FunctionVar, EventChain) def format_query_params(router_data: dict[str, Any]) -> dict[str, str]: @@ -664,18 +632,22 @@ def format_library_name(library_fullname: str): return lib -def json_dumps(obj: Any) -> str: +def json_dumps(obj: Any, **kwargs) -> str: """Takes an object and returns a jsonified string. Args: obj: The object to be serialized. + kwargs: Additional keyword arguments to pass to json.dumps. Returns: A string """ from reflex.utils import serializers - return json.dumps(obj, ensure_ascii=False, default=serializers.serialize) + kwargs.setdefault("ensure_ascii", False) + kwargs.setdefault("default", serializers.serialize) + + return json.dumps(obj, **kwargs) def collect_form_dict_names(form_dict: dict[str, Any]) -> dict[str, Any]: @@ -712,7 +684,6 @@ def format_array_ref(refs: str, idx: Var | None) -> str: """ clean_ref = re.sub(r"[^\w]+", "_", refs) if idx is not None: - # idx._var_is_local = True return f"refs_{clean_ref}[{idx!s}]" return f"refs_{clean_ref}" diff --git a/reflex/utils/imports.py b/reflex/utils/imports.py index bd422ecc0..46e8e7362 100644 --- a/reflex/utils/imports.py +++ b/reflex/utils/imports.py @@ -90,7 +90,7 @@ def collapse_imports( } -@dataclasses.dataclass(order=True, frozen=True) +@dataclasses.dataclass(frozen=True) class ImportVar: """An import var.""" @@ -122,7 +122,7 @@ class ImportVar: """ if self.alias: return ( - self.alias if self.is_default else " as ".join([self.tag, self.alias]) # type: ignore + self.alias if self.is_default else " as ".join([self.tag, self.alias]) # pyright: ignore [reportCallIssue,reportArgumentType] ) else: return self.tag or "" diff --git a/reflex/utils/lazy_loader.py b/reflex/utils/lazy_loader.py index 61e3967e5..eba89532d 100644 --- a/reflex/utils/lazy_loader.py +++ b/reflex/utils/lazy_loader.py @@ -1,11 +1,17 @@ """Module to implement lazy loading in reflex.""" +from __future__ import annotations + import copy import lazy_loader as lazy -def attach(package_name, submodules=None, submod_attrs=None): +def attach( + package_name: str, + submodules: set | None = None, + submod_attrs: dict | None = None, +): """Replaces a package's __getattr__, __dir__, and __all__ attributes using lazy.attach. The lazy loader __getattr__ doesn't support tuples as list values. We needed to add this functionality (tuples) in Reflex to support 'import as _' statements. This function diff --git a/reflex/utils/path_ops.py b/reflex/utils/path_ops.py index a2ba2b151..edab085ff 100644 --- a/reflex/utils/path_ops.py +++ b/reflex/utils/path_ops.py @@ -9,7 +9,7 @@ import shutil from pathlib import Path from reflex import constants -from reflex.config import environment +from reflex.config import environment, get_config # Shorthand for join. join = os.linesep.join @@ -118,7 +118,7 @@ def ln(src: str | Path, dest: str | Path, overwrite: bool = False) -> bool: return True -def which(program: str | Path) -> str | Path | None: +def which(program: str | Path) -> Path | None: """Find the path to an executable. Args: @@ -127,7 +127,8 @@ def which(program: str | Path) -> str | Path | None: Returns: The path to the executable. """ - return shutil.which(str(program)) + which_result = shutil.which(program) + return Path(which_result) if which_result else None def use_system_node() -> bool: @@ -156,12 +157,12 @@ def get_node_bin_path() -> Path | None: """ bin_path = Path(constants.Node.BIN_PATH) if not bin_path.exists(): - str_path = which("node") - return Path(str_path).parent.resolve() if str_path else None - return bin_path.resolve() + path = which("node") + return path.parent.absolute() if path else None + return bin_path.absolute() -def get_node_path() -> str | None: +def get_node_path() -> Path | None: """Get the node binary path. Returns: @@ -169,12 +170,11 @@ def get_node_path() -> str | None: """ node_path = Path(constants.Node.PATH) if use_system_node() or not node_path.exists(): - system_node_path = which("node") - return str(system_node_path) if system_node_path else None - return str(node_path) + node_path = which("node") + return node_path -def get_npm_path() -> str | None: +def get_npm_path() -> Path | None: """Get npm binary path. Returns: @@ -182,9 +182,20 @@ def get_npm_path() -> str | None: """ npm_path = Path(constants.Node.NPM_PATH) if use_system_node() or not npm_path.exists(): - system_npm_path = which("npm") - return str(system_npm_path) if system_npm_path else None - return str(npm_path) + npm_path = which("npm") + return npm_path.absolute() if npm_path else None + + +def get_bun_path() -> Path | None: + """Get bun binary path. + + Returns: + The path to the bun binary file. + """ + bun_path = get_config().bun_path + if use_system_bun() or not bun_path.exists(): + bun_path = which("bun") + return bun_path.absolute() if bun_path else None def update_json_file(file_path: str | Path, update_dict: dict[str, int | str]): @@ -196,6 +207,9 @@ def update_json_file(file_path: str | Path, update_dict: dict[str, int | str]): """ fp = Path(file_path) + # Create the parent directory if it doesn't exist. + fp.parent.mkdir(parents=True, exist_ok=True) + # Create the file if it doesn't exist. fp.touch(exist_ok=True) @@ -205,14 +219,14 @@ def update_json_file(file_path: str | Path, update_dict: dict[str, int | str]): # Read the existing json object from the file. json_object = {} if fp.stat().st_size: - with open(fp) as f: + with fp.open() as f: json_object = json.load(f) # Update the json object with the new data. json_object.update(update_dict) # Write the updated json object to the file - with open(fp, "w") as f: + with fp.open("w") as f: json.dump(json_object, f, ensure_ascii=False) diff --git a/reflex/utils/prerequisites.py b/reflex/utils/prerequisites.py index 88260fe45..4741400f8 100644 --- a/reflex/utils/prerequisites.py +++ b/reflex/utils/prerequisites.py @@ -7,6 +7,7 @@ import dataclasses import functools import importlib import importlib.metadata +import importlib.util import json import os import platform @@ -17,34 +18,45 @@ import stat import sys import tempfile import time +import typing import zipfile from datetime import datetime from pathlib import Path from types import ModuleType -from typing import Callable, List, Optional +from typing import Callable, List, NamedTuple, Optional import httpx import typer from alembic.util.exc import CommandError from packaging import version from redis import Redis as RedisSync -from redis import exceptions from redis.asyncio import Redis +from redis.exceptions import RedisError from reflex import constants, model from reflex.compiler import templates from reflex.config import Config, environment, get_config from reflex.utils import console, net, path_ops, processes, redir from reflex.utils.exceptions import ( - GeneratedCodeHasNoFunctionDefs, - raise_system_package_missing_error, + GeneratedCodeHasNoFunctionDefsError, + SystemPackageMissingError, ) from reflex.utils.format import format_library_name from reflex.utils.registry import _get_npm_registry +if typing.TYPE_CHECKING: + from reflex.app import App + CURRENTLY_INSTALLING_NODE = False +class AppInfo(NamedTuple): + """A tuple containing the app instance and module.""" + + app: App + module: ModuleType + + @dataclasses.dataclass(frozen=True) class Template: """A template for a Reflex app.""" @@ -75,16 +87,15 @@ def get_web_dir() -> Path: return environment.REFLEX_WEB_WORKDIR.get() -def _python_version_check(): - """Emit deprecation warning for deprecated python versions.""" - # Check for end-of-life python versions. - if sys.version_info < (3, 10): - console.deprecate( - feature_name="Support for Python 3.9 and older", - reason="please upgrade to Python 3.10 or newer", - deprecation_version="0.6.0", - removal_version="0.7.0", - ) +def get_states_dir() -> Path: + """Get the working directory for the states. + + Can be overridden with REFLEX_STATES_WORKDIR. + + Returns: + The working directory. + """ + return environment.REFLEX_STATES_WORKDIR.get() def check_latest_package_version(package_name: str): @@ -109,8 +120,6 @@ def check_latest_package_version(package_name: str): console.warn( f"Your version ({current_version}) of {package_name} is out of date. Upgrade to {latest_version} with 'pip install {package_name} --upgrade'" ) - # Check for depreacted python versions - _python_version_check() except Exception: pass @@ -167,7 +176,7 @@ def get_node_version() -> version.Version | None: try: result = processes.new_process([node_path, "-v"], run=True) # The output will be in the form "vX.Y.Z", but version.parse() can handle it - return version.parse(result.stdout) # type: ignore + return version.parse(result.stdout) # pyright: ignore [reportArgumentType] except (FileNotFoundError, TypeError): return None @@ -180,7 +189,7 @@ def get_fnm_version() -> version.Version | None: """ try: result = processes.new_process([constants.Fnm.EXE, "--version"], run=True) - return version.parse(result.stdout.split(" ")[1]) # type: ignore + return version.parse(result.stdout.split(" ")[1]) # pyright: ignore [reportOptionalMemberAccess, reportAttributeAccessIssue] except (FileNotFoundError, TypeError): return None except version.InvalidVersion as e: @@ -196,10 +205,13 @@ def get_bun_version() -> version.Version | None: Returns: The version of bun. """ + bun_path = path_ops.get_bun_path() + if bun_path is None: + return None try: # Run the bun -v command and capture the output - result = processes.new_process([str(get_config().bun_path), "-v"], run=True) - return version.parse(result.stdout) # type: ignore + result = processes.new_process([str(bun_path), "-v"], run=True) + return version.parse(str(result.stdout)) # pyright: ignore [reportArgumentType] except FileNotFoundError: return None except version.InvalidVersion as e: @@ -243,7 +255,7 @@ def get_package_manager(on_failure_return_none: bool = False) -> str | None: """ npm_path = path_ops.get_npm_path() if npm_path is not None: - return str(Path(npm_path).resolve()) + return str(npm_path) if on_failure_return_none: return None raise FileNotFoundError("NPM not found. You may need to run `reflex init`.") @@ -267,6 +279,22 @@ def windows_npm_escape_hatch() -> bool: return environment.REFLEX_USE_NPM.get() +def _check_app_name(config: Config): + """Check if the app name is set in the config. + + Args: + config: The config object. + + Raises: + RuntimeError: If the app name is not set in the config. + """ + if not config.app_name: + raise RuntimeError( + "Cannot get the app module because `app_name` is not set in rxconfig! " + "If this error occurs in a reflex test case, ensure that `get_app` is mocked." + ) + + def get_app(reload: bool = False) -> ModuleType: """Get the app module based on the default config. @@ -277,22 +305,23 @@ def get_app(reload: bool = False) -> ModuleType: The app based on the default config. Raises: - RuntimeError: If the app name is not set in the config. + Exception: If an error occurs while getting the app module. """ from reflex.utils import telemetry try: environment.RELOAD_CONFIG.set(reload) config = get_config() - if not config.app_name: - raise RuntimeError( - "Cannot get the app module because `app_name` is not set in rxconfig! " - "If this error occurs in a reflex test case, ensure that `get_app` is mocked." - ) - module = config.module - sys.path.insert(0, os.getcwd()) - app = __import__(module, fromlist=(constants.CompileVars.APP,)) + _check_app_name(config) + + module = config.module + sys.path.insert(0, str(Path.cwd())) + app = ( + __import__(module, fromlist=(constants.CompileVars.APP,)) + if not config.app_module + else config.app_module + ) if reload: from reflex.state import reload_state_module @@ -301,11 +330,34 @@ def get_app(reload: bool = False) -> ModuleType: # Reload the app module. importlib.reload(app) - - return app except Exception as ex: telemetry.send_error(ex, context="frontend") raise + else: + return app + + +def get_and_validate_app(reload: bool = False) -> AppInfo: + """Get the app instance based on the default config and validate it. + + Args: + reload: Re-import the app module from disk + + Returns: + The app instance and the app module. + + Raises: + RuntimeError: If the app instance is not an instance of rx.App. + """ + from reflex.app import App + + app_module = get_app(reload=reload) + app = getattr(app_module, constants.CompileVars.APP) + if not isinstance(app, App): + raise RuntimeError( + "The app instance in the specified app_module_import in rxconfig must be an instance of rx.App." + ) + return AppInfo(app=app, module=app_module) def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType: @@ -318,8 +370,7 @@ def get_compiled_app(reload: bool = False, export: bool = False) -> ModuleType: Returns: The compiled app based on the default config. """ - app_module = get_app(reload=reload) - app = getattr(app_module, constants.CompileVars.APP) + app, app_module = get_and_validate_app(reload=reload) # For py3.9 compatibility when redis is used, we MUST add any decorator pages # before compiling the app in a thread to avoid event loop error (REF-2172). app._apply_decorated_pages() @@ -333,10 +384,11 @@ def get_redis() -> Redis | None: Returns: The asynchronous redis client. """ - if isinstance((redis_url_or_options := parse_redis_url()), str): - return Redis.from_url(redis_url_or_options) - elif isinstance(redis_url_or_options, dict): - return Redis(**redis_url_or_options) + if (redis_url := parse_redis_url()) is not None: + return Redis.from_url( + redis_url, + retry_on_error=[RedisError], + ) return None @@ -346,14 +398,15 @@ def get_redis_sync() -> RedisSync | None: Returns: The synchronous redis client. """ - if isinstance((redis_url_or_options := parse_redis_url()), str): - return RedisSync.from_url(redis_url_or_options) - elif isinstance(redis_url_or_options, dict): - return RedisSync(**redis_url_or_options) + if (redis_url := parse_redis_url()) is not None: + return RedisSync.from_url( + redis_url, + retry_on_error=[RedisError], + ) return None -def parse_redis_url() -> str | dict | None: +def parse_redis_url() -> str | None: """Parse the REDIS_URL in config if applicable. Returns: @@ -372,16 +425,13 @@ def parse_redis_url() -> str | dict | None: return config.redis_url -async def get_redis_status() -> bool | None: +async def get_redis_status() -> dict[str, bool | None]: """Checks the status of the Redis connection. Attempts to connect to Redis and send a ping command to verify connectivity. Returns: - bool or None: The status of the Redis connection: - - True: Redis is accessible and responding. - - False: Redis is not accessible due to a connection error. - - None: Redis not used i.e redis_url is not set in rxconfig. + The status of the Redis connection. """ try: status = True @@ -390,10 +440,10 @@ async def get_redis_status() -> bool | None: redis_client.ping() else: status = None - except exceptions.RedisError: + except RedisError: status = False - return status + return {"redis": status} def validate_app_name(app_name: str | None = None) -> str: @@ -428,6 +478,167 @@ def validate_app_name(app_name: str | None = None) -> str: return app_name +def rename_path_up_tree(full_path: str | Path, old_name: str, new_name: str) -> Path: + """Rename all instances of `old_name` in the path (file and directories) to `new_name`. + The renaming stops when we reach the directory containing `rxconfig.py`. + + Args: + full_path: The full path to start renaming from. + old_name: The name to be replaced. + new_name: The replacement name. + + Returns: + The updated path after renaming. + """ + current_path = Path(full_path) + new_path = None + + while True: + directory, base = current_path.parent, current_path.name + # Stop renaming when we reach the root dir (which contains rxconfig.py) + if current_path.is_dir() and (current_path / "rxconfig.py").exists(): + new_path = current_path + break + + if old_name == base.removesuffix(constants.Ext.PY): + new_base = base.replace(old_name, new_name) + new_path = directory / new_base + current_path.rename(new_path) + console.debug(f"Renamed {current_path} -> {new_path}") + current_path = new_path + else: + new_path = current_path + + # Move up the directory tree + current_path = directory + + return new_path + + +def rename_app(new_app_name: str, loglevel: constants.LogLevel): + """Rename the app directory. + + Args: + new_app_name: The new name for the app. + loglevel: The log level to use. + + Raises: + Exit: If the command is not ran in the root dir or the app module cannot be imported. + """ + # Set the log level. + console.set_log_level(loglevel) + + if not constants.Config.FILE.exists(): + console.error( + "No rxconfig.py found. Make sure you are in the root directory of your app." + ) + raise typer.Exit(1) + + sys.path.insert(0, str(Path.cwd())) + + config = get_config() + module_path = importlib.util.find_spec(config.module) + if module_path is None: + console.error(f"Could not find module {config.module}.") + raise typer.Exit(1) + + if not module_path.origin: + console.error(f"Could not find origin for module {config.module}.") + raise typer.Exit(1) + console.info(f"Renaming app directory to {new_app_name}.") + process_directory( + Path.cwd(), + config.app_name, + new_app_name, + exclude_dirs=[constants.Dirs.WEB, constants.Dirs.APP_ASSETS], + ) + + rename_path_up_tree(Path(module_path.origin), config.app_name, new_app_name) + + console.success(f"App directory renamed to [bold]{new_app_name}[/bold].") + + +def rename_imports_and_app_name(file_path: str | Path, old_name: str, new_name: str): + """Rename imports the file using string replacement as well as app_name in rxconfig.py. + + Args: + file_path: The file to process. + old_name: The old name to replace. + new_name: The new name to use. + """ + file_path = Path(file_path) + content = file_path.read_text() + + # Replace `from old_name.` or `from old_name` with `from new_name` + content = re.sub( + rf"\bfrom {re.escape(old_name)}(\b|\.|\s)", + lambda match: f"from {new_name}{match.group(1)}", + content, + ) + + # Replace `import old_name` with `import new_name` + content = re.sub( + rf"\bimport {re.escape(old_name)}\b", + f"import {new_name}", + content, + ) + + # Replace `app_name="old_name"` in rx.Config + content = re.sub( + rf'\bapp_name\s*=\s*["\']{re.escape(old_name)}["\']', + f'app_name="{new_name}"', + content, + ) + + # Replace positional argument `"old_name"` in rx.Config + content = re.sub( + rf'\brx\.Config\(\s*["\']{re.escape(old_name)}["\']', + f'rx.Config("{new_name}"', + content, + ) + + file_path.write_text(content) + + +def process_directory( + directory: str | Path, + old_name: str, + new_name: str, + exclude_dirs: list | None = None, + extensions: list | None = None, +): + """Process files with specified extensions in a directory, excluding specified directories. + + Args: + directory: The root directory to process. + old_name: The old name to replace. + new_name: The new name to use. + exclude_dirs: List of directory names to exclude. Defaults to None. + extensions: List of file extensions to process. + """ + exclude_dirs = exclude_dirs or [] + extensions = extensions or [ + constants.Ext.PY, + constants.Ext.MD, + ] # include .md files, typically used in reflex-web. + extensions_set = {ext.lstrip(".") for ext in extensions} + directory = Path(directory) + + root_exclude_dirs = {directory / exclude_dir for exclude_dir in exclude_dirs} + + files = ( + p.resolve() + for p in directory.glob("**/*") + if p.is_file() and p.suffix.lstrip(".") in extensions_set + ) + + for file_path in files: + if not any( + file_path.is_relative_to(exclude_dir) for exclude_dir in root_exclude_dirs + ): + rename_imports_and_app_name(file_path, old_name, new_name) + + def create_config(app_name: str): """Create a new rxconfig file. @@ -438,9 +649,11 @@ def create_config(app_name: str): from reflex.compiler import templates config_name = f"{re.sub(r'[^a-zA-Z]', '', app_name).capitalize()}Config" - with open(constants.Config.FILE, "w") as f: - console.debug(f"Creating {constants.Config.FILE}") - f.write(templates.RXCONFIG.render(app_name=app_name, config_name=config_name)) + + console.debug(f"Creating {constants.Config.FILE}") + constants.Config.FILE.write_text( + templates.RXCONFIG.render(app_name=app_name, config_name=config_name) + ) def initialize_gitignore( @@ -494,14 +707,14 @@ def initialize_requirements_txt(): console.debug(f"Detected encoding for {fp} as {encoding}.") try: other_requirements_exist = False - with open(fp, "r", encoding=encoding) as f: - for req in f.readlines(): + with fp.open("r", encoding=encoding) as f: + for req in f: # Check if we have a package name that is reflex if re.match(r"^reflex[^a-zA-Z0-9]", req): console.debug(f"{fp} already has reflex as dependency.") return other_requirements_exist = True - with open(fp, "a", encoding=encoding) as f: + with fp.open("a", encoding=encoding) as f: preceding_newline = "\n" if other_requirements_exist else "" f.write( f"{preceding_newline}{constants.RequirementsTxt.DEFAULTS_STUB}{constants.Reflex.VERSION}\n" @@ -592,7 +805,7 @@ def initialize_web_directory(): """Initialize the web directory on reflex init.""" console.log("Initializing the web directory.") - # Re-use the hash if one is already created, so we don't over-write it when running reflex init + # Reuse the hash if one is already created, so we don't over-write it when running reflex init project_hash = get_project_hash() path_ops.cp(constants.Templates.Dirs.WEB_TEMPLATE, str(get_web_dir())) @@ -609,10 +822,14 @@ def initialize_web_directory(): init_reflex_json(project_hash=project_hash) +def _turbopack_flag() -> str: + return " --turbopack" if environment.REFLEX_USE_TURBOPACK.get() else "" + + def _compile_package_json(): return templates.PACKAGE_JSON.render( scripts={ - "dev": constants.PackageJson.Commands.DEV, + "dev": constants.PackageJson.Commands.DEV + _turbopack_flag(), "export": constants.PackageJson.Commands.EXPORT, "export_sitemap": constants.PackageJson.Commands.EXPORT_SITEMAP, "prod": constants.PackageJson.Commands.PROD, @@ -645,7 +862,7 @@ def initialize_bun_config(): def init_reflex_json(project_hash: int | None): """Write the hash of the Reflex project to a REFLEX_JSON. - Re-use the hash if one is already created, therefore do not + Reuse the hash if one is already created, therefore do not overwrite it every time we run the reflex init command . @@ -667,7 +884,9 @@ def init_reflex_json(project_hash: int | None): path_ops.update_json_file(get_web_dir() / constants.Reflex.JSON, reflex_json) -def update_next_config(export=False, transpile_packages: Optional[List[str]] = None): +def update_next_config( + export: bool = False, transpile_packages: Optional[List[str]] = None +): """Update Next.js config from Reflex config. Args: @@ -693,13 +912,12 @@ def _update_next_config( next_config = { "basePath": config.frontend_path or "", "compress": config.next_compression, - "reactStrictMode": config.react_strict_mode, "trailingSlash": True, "staticPageGenerationTimeout": config.static_page_generation_timeout, } if transpile_packages: next_config["transpilePackages"] = list( - set((format_library_name(p) for p in transpile_packages)) + {format_library_name(p) for p in transpile_packages} ) if export: next_config["output"] = "export" @@ -732,13 +950,13 @@ def download_and_run(url: str, *args, show_status: bool = False, **env): response.raise_for_status() # Save the script to a temporary file. - script = tempfile.NamedTemporaryFile() - with open(script.name, "w") as f: - f.write(response.text) + script = Path(tempfile.NamedTemporaryFile().name) + + script.write_text(response.text) # Run the script. env = {**os.environ, **env} - process = processes.new_process(["bash", f.name, *args], env=env) + process = processes.new_process(["bash", str(script), *args], env=env) show = processes.show_status if show_status else processes.show_logs show(f"Installing {url}", process) @@ -752,14 +970,14 @@ def download_and_extract_fnm_zip(): # Download the zip file url = constants.Fnm.INSTALL_URL console.debug(f"Downloading {url}") - fnm_zip_file = constants.Fnm.DIR / f"{constants.Fnm.FILENAME}.zip" + fnm_zip_file: Path = constants.Fnm.DIR / f"{constants.Fnm.FILENAME}.zip" # Function to download and extract the FNM zip release. try: # Download the FNM zip release. # TODO: show progress to improve UX response = net.get(url, follow_redirects=True) response.raise_for_status() - with open(fnm_zip_file, "wb") as output_file: + with fnm_zip_file.open("wb") as output_file: for chunk in response.iter_bytes(): output_file.write(chunk) @@ -807,7 +1025,7 @@ def install_node(): ) else: # All other platforms (Linux, MacOS). # Add execute permissions to fnm executable. - os.chmod(constants.Fnm.EXE, stat.S_IXUSR) + constants.Fnm.EXE.chmod(stat.S_IXUSR) # Install node. # Specify arm64 arch explicitly for M1s and M2s. architecture_arg = ( @@ -830,7 +1048,11 @@ def install_node(): def install_bun(): - """Install bun onto the user's system.""" + """Install bun onto the user's system. + + Raises: + SystemPackageMissingError: If "unzip" is missing. + """ win_supported = is_windows_bun_supported() one_drive_in_path = windows_check_onedrive_in_path() if constants.IS_WINDOWS and (not win_supported or one_drive_in_path): @@ -844,9 +1066,7 @@ def install_bun(): ) # Skip if bun is already installed. - if Path(get_config().bun_path).exists() and get_bun_version() == version.parse( - constants.Bun.VERSION - ): + if get_bun_version() == version.parse(constants.Bun.VERSION): console.debug("Skipping bun installation as it is already installed.") return @@ -867,15 +1087,15 @@ def install_bun(): show_logs=console.is_debug(), ) else: - unzip_path = path_ops.which("unzip") - if unzip_path is None: - raise_system_package_missing_error("unzip") + if path_ops.which("unzip") is None: + raise SystemPackageMissingError("unzip") # Run the bun install script. download_and_run( constants.Bun.INSTALL_URL, f"bun-v{constants.Bun.VERSION}", BUN_INSTALL=str(constants.Bun.ROOT_PATH), + BUN_VERSION=str(constants.Bun.VERSION), ) @@ -909,7 +1129,7 @@ def cached_procedure(cache_file: str, payload_fn: Callable[..., str]): The decorated function. """ - def _inner_decorator(func): + def _inner_decorator(func: Callable): def _inner(*args, **kwargs): payload = _read_cached_procedure_file(cache_file) new_payload = payload_fn(*args, **kwargs) @@ -925,7 +1145,7 @@ def cached_procedure(cache_file: str, payload_fn: Callable[..., str]): @cached_procedure( cache_file=str(get_web_dir() / "reflex.install_frontend_packages.cached"), - payload_fn=lambda p, c: f"{sorted(list(p))!r},{c.json()}", + payload_fn=lambda p, c: f"{sorted(p)!r},{c.json()}", ) def install_frontend_packages(packages: set[str], config: Config): """Installs the base and custom frontend packages. @@ -969,7 +1189,7 @@ def install_frontend_packages(packages: set[str], config: Config): ) processes.run_process_with_fallback( - [install_package_manager, "install"], # type: ignore + [install_package_manager, "install"], fallback=fallback_command, analytics_enabled=True, show_status_message="Installing base frontend packages", @@ -1071,12 +1291,9 @@ def validate_bun(): Raises: Exit: If custom specified bun does not exist or does not meet requirements. """ - # if a custom bun path is provided, make sure its valid - # This is specific to non-FHS OS - bun_path = get_config().bun_path - if path_ops.use_system_bun(): - bun_path = path_ops.which("bun") - if bun_path != constants.Bun.DEFAULT_PATH: + bun_path = path_ops.get_bun_path() + + if bun_path and bun_path.samefile(constants.Bun.DEFAULT_PATH): console.info(f"Using custom Bun path: {bun_path}") bun_version = get_bun_version() if not bun_version: @@ -1094,7 +1311,7 @@ def validate_bun(): raise typer.Exit(1) -def validate_frontend_dependencies(init=True): +def validate_frontend_dependencies(init: bool = True): """Validate frontend dependencies to ensure they meet requirements. Args: @@ -1148,11 +1365,12 @@ def ensure_reflex_installation_id() -> Optional[int]: if installation_id is None: installation_id = random.getrandbits(128) installation_id_file.write_text(str(installation_id)) - # If we get here, installation_id is definitely set - return installation_id except Exception as e: console.debug(f"Failed to ensure reflex installation id: {e}") return None + else: + # If we get here, installation_id is definitely set + return installation_id def initialize_reflex_user_directory(): @@ -1175,6 +1393,24 @@ def initialize_frontend_dependencies(): initialize_web_directory() +def check_db_used() -> bool: + """Check if the database is used. + + Returns: + True if the database is used. + """ + return bool(get_config().db_url) + + +def check_redis_used() -> bool: + """Check if Redis is used. + + Returns: + True if Redis is used. + """ + return bool(get_config().redis_url) + + def check_db_initialized() -> bool: """Check if the database migrations are initialized. @@ -1246,7 +1482,7 @@ def prompt_for_template_options(templates: list[Template]) -> str: ) # Return the template. - return templates[int(template)].name + return templates[int(template)].name # pyright: ignore [reportArgumentType] def fetch_app_templates(version: str) -> dict[str, Template]: @@ -1300,7 +1536,7 @@ def fetch_app_templates(version: str) -> dict[str, Template]: for tp in templates_data: if tp["hidden"] or tp["code_url"] is None: continue - known_fields = set(f.name for f in dataclasses.fields(Template)) + known_fields = {f.name for f in dataclasses.fields(Template)} filtered_templates[tp["name"]] = Template( **{k: v for k, v in tp.items() if k in known_fields} ) @@ -1326,7 +1562,7 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str raise typer.Exit(1) from ose # Use httpx GET with redirects to download the zip file. - zip_file_path = Path(temp_dir) / "template.zip" + zip_file_path: Path = Path(temp_dir) / "template.zip" try: # Note: following redirects can be risky. We only allow this for reflex built templates at the moment. response = net.get(template_url, follow_redirects=True) @@ -1336,9 +1572,8 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str console.error(f"Failed to download the template: {he}") raise typer.Exit(1) from he try: - with open(zip_file_path, "wb") as f: - f.write(response.content) - console.debug(f"Downloaded the zip to {zip_file_path}") + zip_file_path.write_bytes(response.content) + console.debug(f"Downloaded the zip to {zip_file_path}") except OSError as ose: console.error(f"Unable to write the downloaded zip to disk {ose}") raise typer.Exit(1) from ose @@ -1349,19 +1584,22 @@ def create_config_init_app_from_remote_template(app_name: str, template_url: str except OSError as ose: console.error(f"Failed to create temp directory for extracting zip: {ose}") raise typer.Exit(1) from ose + try: zipfile.ZipFile(zip_file_path).extractall(path=unzip_dir) # The zip file downloaded from github looks like: # repo-name-branch/**/*, so we need to remove the top level directory. - if len(subdirs := os.listdir(unzip_dir)) != 1: - console.error(f"Expected one directory in the zip, found {subdirs}") - raise typer.Exit(1) - template_dir = unzip_dir / subdirs[0] - console.debug(f"Template folder is located at {template_dir}") except Exception as uze: console.error(f"Failed to unzip the template: {uze}") raise typer.Exit(1) from uze + if len(subdirs := os.listdir(unzip_dir)) != 1: + console.error(f"Expected one directory in the zip, found {subdirs}") + raise typer.Exit(1) + + template_dir = unzip_dir / subdirs[0] + console.debug(f"Template folder is located at {template_dir}") + # Move the rxconfig file here first. path_ops.mv(str(template_dir / constants.Config.FILE), constants.Config.FILE) new_config = get_config(reload=True) @@ -1397,7 +1635,9 @@ def initialize_default_app(app_name: str): initialize_app_directory(app_name) -def validate_and_create_app_using_remote_template(app_name, template, templates): +def validate_and_create_app_using_remote_template( + app_name: str, template: str, templates: dict[str, Template] +): """Validate and create an app using a remote template. Args: @@ -1587,7 +1827,7 @@ def initialize_main_module_index_from_generation(app_name: str, generation_hash: generation_hash: The generation hash from reflex.build. Raises: - GeneratedCodeHasNoFunctionDefs: If the fetched code has no function definitions + GeneratedCodeHasNoFunctionDefsError: If the fetched code has no function definitions (the refactored reflex code is expected to have at least one root function defined). """ # Download the reflex code for the generation. @@ -1604,17 +1844,17 @@ def initialize_main_module_index_from_generation(app_name: str, generation_hash: # Determine the name of the last function, which renders the generated code. defined_funcs = re.findall(r"def ([a-zA-Z_]+)\(", resp.text) if not defined_funcs: - raise GeneratedCodeHasNoFunctionDefs( + raise GeneratedCodeHasNoFunctionDefsError( f"No function definitions found in generated code from {url!r}." ) render_func_name = defined_funcs[-1] - def replace_content(_match): + def replace_content(_match: re.Match) -> str: return "\n".join( [ resp.text, "", - "" "def index() -> rx.Component:", + "def index() -> rx.Component:", f" return {render_func_name}()", "", "", @@ -1639,7 +1879,7 @@ def initialize_main_module_index_from_generation(app_name: str, generation_hash: main_module_path.write_text(main_module_code) -def format_address_width(address_width) -> int | None: +def format_address_width(address_width: str | None) -> int | None: """Cast address width to an int. Args: diff --git a/reflex/utils/processes.py b/reflex/utils/processes.py index 4d0e64a96..c92fb7d1a 100644 --- a/reflex/utils/processes.py +++ b/reflex/utils/processes.py @@ -15,12 +15,14 @@ from typing import Callable, Generator, List, Optional, Tuple, Union import psutil import typer from redis.exceptions import RedisError +from rich.progress import Progress from reflex import constants +from reflex.config import environment from reflex.utils import console, path_ops, prerequisites -def kill(pid): +def kill(pid: int): """Kill a process. Args: @@ -48,7 +50,7 @@ def get_num_workers() -> int: return (os.cpu_count() or 1) * 2 + 1 -def get_process_on_port(port) -> Optional[psutil.Process]: +def get_process_on_port(port: int) -> Optional[psutil.Process]: """Get the process on the given port. Args: @@ -58,20 +60,20 @@ def get_process_on_port(port) -> Optional[psutil.Process]: The process on the given port. """ for proc in psutil.process_iter(["pid", "name", "cmdline"]): - try: + with contextlib.suppress( + psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess + ): if importlib.metadata.version("psutil") >= "6.0.0": - conns = proc.net_connections(kind="inet") # type: ignore + conns = proc.net_connections(kind="inet") else: conns = proc.connections(kind="inet") for conn in conns: if conn.laddr.port == int(port): return proc - except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): - pass return None -def is_process_on_port(port) -> bool: +def is_process_on_port(port: int) -> bool: """Check if a process is running on the given port. Args: @@ -83,7 +85,7 @@ def is_process_on_port(port) -> bool: return get_process_on_port(port) is not None -def kill_process_on_port(port): +def kill_process_on_port(port: int): """Kill the process on the given port. Args: @@ -91,10 +93,10 @@ def kill_process_on_port(port): """ if get_process_on_port(port) is not None: with contextlib.suppress(psutil.AccessDenied): - get_process_on_port(port).kill() # type: ignore + get_process_on_port(port).kill() # pyright: ignore [reportOptionalMemberAccess] -def change_port(port: str, _type: str) -> str: +def change_port(port: int, _type: str) -> int: """Change the port. Args: @@ -105,7 +107,7 @@ def change_port(port: str, _type: str) -> str: The new port. """ - new_port = str(int(port) + 1) + new_port = port + 1 if is_process_on_port(new_port): return change_port(new_port, _type) console.info( @@ -114,11 +116,11 @@ def change_port(port: str, _type: str) -> str: return new_port -def handle_port(service_name: str, port: str, default_port: str) -> str: +def handle_port(service_name: str, port: int, default_port: int) -> int: """Change port if the specified port is in use and is not explicitly specified as a CLI arg or config arg. otherwise tell the user the port is in use and exit the app. - We make an assumption that when port is the default port,then it hasnt been explicitly set since its not straightforward + We make an assumption that when port is the default port,then it hasn't been explicitly set since its not straightforward to know whether a port was explicitly provided by the user unless its any other than the default. Args: @@ -133,7 +135,7 @@ def handle_port(service_name: str, port: str, default_port: str) -> str: Exit:when the port is in use. """ if is_process_on_port(port): - if int(port) == int(default_port): + if port == int(default_port): return change_port(port, service_name) else: console.error(f"{service_name.capitalize()} port: {port} is already in use") @@ -141,7 +143,12 @@ def handle_port(service_name: str, port: str, default_port: str) -> str: return port -def new_process(args, run: bool = False, show_logs: bool = False, **kwargs): +def new_process( + args: str | list[str] | list[str | None] | list[str | Path | None], + run: bool = False, + show_logs: bool = False, + **kwargs, +): """Wrapper over subprocess.Popen to unify the launch of child processes. Args: @@ -156,24 +163,30 @@ def new_process(args, run: bool = False, show_logs: bool = False, **kwargs): Raises: Exit: When attempting to run a command with a None value. """ - node_bin_path = str(path_ops.get_node_bin_path()) - if not node_bin_path and not prerequisites.CURRENTLY_INSTALLING_NODE: - console.warn( - "The path to the Node binary could not be found. Please ensure that Node is properly " - "installed and added to your system's PATH environment variable or try running " - "`reflex init` again." - ) - if None in args: + # Check for invalid command first. + if isinstance(args, list) and None in args: console.error(f"Invalid command: {args}") raise typer.Exit(1) - # Add the node bin path to the PATH environment variable. + + path_env: str = os.environ.get("PATH", "") + + # Add node_bin_path to the PATH environment variable. + if not environment.REFLEX_BACKEND_ONLY.get(): + node_bin_path = str(path_ops.get_node_bin_path()) + if not node_bin_path and not prerequisites.CURRENTLY_INSTALLING_NODE: + console.warn( + "The path to the Node binary could not be found. Please ensure that Node is properly " + "installed and added to your system's PATH environment variable or try running " + "`reflex init` again." + ) + path_env = os.pathsep.join([node_bin_path, path_env]) + env: dict[str, str] = { **os.environ, - "PATH": os.pathsep.join( - [node_bin_path if node_bin_path else "", os.environ["PATH"]] - ), # type: ignore + "PATH": path_env, **kwargs.pop("env", {}), } + kwargs = { "env": env, "stderr": None if show_logs else subprocess.STDOUT, @@ -185,7 +198,7 @@ def new_process(args, run: bool = False, show_logs: bool = False, **kwargs): } console.debug(f"Running command: {args}") fn = subprocess.run if run else subprocess.Popen - return fn(args, **kwargs) + return fn(args, **kwargs) # pyright: ignore [reportCallIssue, reportArgumentType] @contextlib.contextmanager @@ -206,14 +219,14 @@ def run_concurrently_context( return # Convert the functions to tuples. - fns = [fn if isinstance(fn, tuple) else (fn,) for fn in fns] # type: ignore + fns = [fn if isinstance(fn, tuple) else (fn,) for fn in fns] # pyright: ignore [reportAssignmentType] # Run the functions concurrently. executor = None try: executor = futures.ThreadPoolExecutor(max_workers=len(fns)) # Submit the tasks. - tasks = [executor.submit(*fn) for fn in fns] # type: ignore + tasks = [executor.submit(*fn) for fn in fns] # pyright: ignore [reportArgumentType] # Yield control back to the main thread while tasks are running. yield tasks @@ -241,7 +254,7 @@ def run_concurrently(*fns: Union[Callable, Tuple]) -> None: def stream_logs( message: str, process: subprocess.Popen, - progress=None, + progress: Progress | None = None, suppress_errors: bool = False, analytics_enabled: bool = False, ): @@ -351,7 +364,7 @@ def atexit_handler(): def get_command_with_loglevel(command: list[str]) -> list[str]: """Add the right loglevel flag to the designated command. - npm uses --loglevel , Bun doesnt use the --loglevel flag and + npm uses --loglevel , Bun doesn't use the --loglevel flag and runs in debug mode by default. Args: @@ -361,7 +374,7 @@ def get_command_with_loglevel(command: list[str]) -> list[str]: The updated command list """ npm_path = path_ops.get_npm_path() - npm_path = str(Path(npm_path).resolve()) if npm_path else npm_path + npm_path = str(npm_path) if npm_path else None if command[0] == npm_path: return [*command, "--loglevel", "silly"] @@ -369,10 +382,10 @@ def get_command_with_loglevel(command: list[str]) -> list[str]: def run_process_with_fallback( - args, + args: list[str], *, - show_status_message, - fallback=None, + show_status_message: str, + fallback: str | list | None = None, analytics_enabled: bool = False, **kwargs, ): @@ -411,7 +424,7 @@ def run_process_with_fallback( ) -def execute_command_and_return_output(command) -> str | None: +def execute_command_and_return_output(command: str) -> str | None: """Execute a command and return the output. Args: diff --git a/reflex/utils/pyi_generator.py b/reflex/utils/pyi_generator.py index 9b2cbe722..beb355d31 100644 --- a/reflex/utils/pyi_generator.py +++ b/reflex/utils/pyi_generator.py @@ -24,7 +24,7 @@ from reflex.vars.base import Var logger = logging.getLogger("pyi_generator") -PWD = Path(".").resolve() +PWD = Path.cwd() EXCLUDED_FILES = [ "app.py", @@ -83,7 +83,7 @@ DEFAULT_IMPORTS = { } -def _walk_files(path): +def _walk_files(path: str | Path): """Walk all files in a path. This can be replaced with Path.walk() in python3.12. @@ -114,7 +114,9 @@ def _relative_to_pwd(path: Path) -> Path: return path -def _get_type_hint(value, type_hint_globals, is_optional=True) -> str: +def _get_type_hint( + value: Any, type_hint_globals: dict, is_optional: bool = True +) -> str: """Resolve the type hint for value. Args: @@ -196,12 +198,7 @@ def _get_type_hint(value, type_hint_globals, is_optional=True) -> str: elif isinstance(value, str): ev = eval(value, type_hint_globals) if rx_types.is_optional(ev): - # hints = { - # _get_type_hint(arg, type_hint_globals, is_optional=False) - # for arg in ev.__args__ - # } return _get_type_hint(ev, type_hint_globals, is_optional=False) - # return f"Optional[{', '.join(hints)}]" if rx_types.is_union(ev): res = [ @@ -234,7 +231,7 @@ def _generate_imports( """ return [ *[ - ast.ImportFrom(module=name, names=[ast.alias(name=val) for val in values]) + ast.ImportFrom(module=name, names=[ast.alias(name=val) for val in values]) # pyright: ignore [reportCallIssue] for name, values in DEFAULT_IMPORTS.items() ], ast.Import([ast.alias("reflex")]), @@ -260,8 +257,15 @@ def _generate_docstrings(clzs: list[Type[Component]], props: list[str]) -> str: # We've reached the functions, so stop. break + if line == "": + # We hit a blank line, so clear comments to avoid commented out prop appearing in next prop docs. + comments.clear() + continue + # Get comments for prop if line.strip().startswith("#"): + # Remove noqa from the comments. + line = line.partition(" # noqa")[0] comments.append(line) continue @@ -285,10 +289,9 @@ def _generate_docstrings(clzs: list[Type[Component]], props: list[str]) -> str: for line in (clz.create.__doc__ or "").splitlines(): if "**" in line: indent = line.split("**")[0] - for nline in [ - f"{indent}{n}:{' '.join(c)}" for n, c in props_comments.items() - ]: - new_docstring.append(nline) + new_docstring.extend( + [f"{indent}{n}:{' '.join(c)}" for n, c in props_comments.items()] + ) new_docstring.append(line) return "\n".join(new_docstring) @@ -366,7 +369,7 @@ def _extract_class_props_as_ast_nodes( # Try to get default from pydantic field definition. default = target_class.__fields__[name].default if isinstance(default, Var): - default = default._decode() # type: ignore + default = default._decode() kwargs.append( ( @@ -382,7 +385,7 @@ def _extract_class_props_as_ast_nodes( return kwargs -def type_to_ast(typ, cls: type) -> ast.AST: +def type_to_ast(typ: Any, cls: type) -> ast.AST: """Converts any type annotation into its AST representation. Handles nested generic types, unions, etc. @@ -433,14 +436,16 @@ def type_to_ast(typ, cls: type) -> ast.AST: if len(arg_nodes) == 1: slice_value = arg_nodes[0] else: - slice_value = ast.Tuple(elts=arg_nodes, ctx=ast.Load()) + slice_value = ast.Tuple(elts=arg_nodes, ctx=ast.Load()) # pyright: ignore [reportArgumentType] return ast.Subscript( - value=ast.Name(id=base_name), slice=ast.Index(value=slice_value), ctx=ast.Load() + value=ast.Name(id=base_name), + slice=ast.Index(value=slice_value), # pyright: ignore [reportArgumentType] + ctx=ast.Load(), ) -def _get_parent_imports(func): +def _get_parent_imports(func: Callable): _imports = {"reflex.vars": ["Var"]} for type_hint in inspect.get_annotations(func).values(): try: @@ -574,7 +579,7 @@ def _generate_component_create_functiondef( arg=trigger, annotation=ast.Subscript( ast.Name("Optional"), - ast.Index( # type: ignore + ast.Index( # pyright: ignore [reportArgumentType] value=ast.Name( id=ast.unparse( figure_out_return_type( @@ -617,10 +622,10 @@ def _generate_component_create_functiondef( defaults=[], ) - definition = ast.FunctionDef( + definition = ast.FunctionDef( # pyright: ignore [reportCallIssue] name="create", args=create_args, - body=[ + body=[ # pyright: ignore [reportArgumentType] ast.Expr( value=ast.Constant( value=_generate_docstrings( @@ -629,7 +634,7 @@ def _generate_component_create_functiondef( ), ), ast.Expr( - value=ast.Ellipsis(), + value=ast.Constant(value=Ellipsis), ), ], decorator_list=[ @@ -640,7 +645,7 @@ def _generate_component_create_functiondef( else [ast.Name(id="classmethod")] ), ], - lineno=node.lineno if node is not None else None, + lineno=node.lineno if node is not None else None, # pyright: ignore [reportArgumentType] returns=ast.Constant(value=clz.__name__), ) return definition @@ -679,7 +684,7 @@ def _generate_staticmethod_call_functiondef( else [] ), ) - definition = ast.FunctionDef( + definition = ast.FunctionDef( # pyright: ignore [reportCallIssue] name="__call__", args=call_args, body=[ @@ -689,11 +694,12 @@ def _generate_staticmethod_call_functiondef( ), ], decorator_list=[ast.Name(id="staticmethod")], - lineno=node.lineno if node is not None else None, + lineno=node.lineno if node is not None else None, # pyright: ignore [reportArgumentType] returns=ast.Constant( value=_get_type_hint( typing.get_type_hints(clz.__call__).get("return", None), type_hint_globals, + is_optional=False, ) ), ) @@ -725,17 +731,17 @@ def _generate_namespace_call_functiondef( clz = classes[clz_name] if not hasattr(clz.__call__, "__self__"): - return _generate_staticmethod_call_functiondef(node, clz, type_hint_globals) # type: ignore + return _generate_staticmethod_call_functiondef(node, clz, type_hint_globals) # pyright: ignore [reportArgumentType] # Determine which class is wrapped by the namespace __call__ method component_clz = clz.__call__.__self__ - if clz.__call__.__func__.__name__ != "create": + if clz.__call__.__func__.__name__ != "create": # pyright: ignore [reportFunctionMemberAccess] return None definition = _generate_component_create_functiondef( node=None, - clz=component_clz, # type: ignore + clz=component_clz, # pyright: ignore [reportArgumentType] type_hint_globals=type_hint_globals, ) definition.name = "__call__" @@ -815,7 +821,7 @@ class StubGenerator(ast.NodeTransformer): The modified Module node. """ self.generic_visit(node) - return self._remove_docstring(node) # type: ignore + return self._remove_docstring(node) # pyright: ignore [reportReturnType] def visit_Import( self, node: ast.Import | ast.ImportFrom @@ -913,7 +919,7 @@ class StubGenerator(ast.NodeTransformer): node.body.append(call_definition) if not node.body: # We should never return an empty body. - node.body.append(ast.Expr(value=ast.Ellipsis())) + node.body.append(ast.Expr(value=ast.Constant(value=Ellipsis))) self.current_class = None return node @@ -940,9 +946,9 @@ class StubGenerator(ast.NodeTransformer): if node.name.startswith("_") and node.name != "__call__": return None # remove private methods - if node.body[-1] != ast.Expr(value=ast.Ellipsis()): + if node.body[-1] != ast.Expr(value=ast.Constant(value=Ellipsis)): # Blank out the function body for public functions. - node.body = [ast.Expr(value=ast.Ellipsis())] + node.body = [ast.Expr(value=ast.Constant(value=Ellipsis))] return node def visit_Assign(self, node: ast.Assign) -> ast.Assign | None: @@ -1022,7 +1028,7 @@ class InitStubGenerator(StubGenerator): class PyiGenerator: """A .pyi file generator that will scan all defined Component in Reflex and - generate the approriate stub. + generate the appropriate stub. """ modules: list = [] @@ -1049,7 +1055,7 @@ class PyiGenerator: pyi_path.write_text(pyi_content) logger.info(f"Wrote {relpath}") - def _get_init_lazy_imports(self, mod, new_tree): + def _get_init_lazy_imports(self, mod: tuple | ModuleType, new_tree: ast.AST): # retrieve the _SUBMODULES and _SUBMOD_ATTRS from an init file if present. sub_mods = getattr(mod, "_SUBMODULES", None) sub_mod_attrs = getattr(mod, "_SUBMOD_ATTRS", None) @@ -1076,7 +1082,7 @@ class PyiGenerator: + ( " # type: ignore" if mod in pyright_ignore_imports - else " # noqa" # ignore ruff formatting here for cases like rx.list. + else " # noqa: F401" # ignore ruff formatting here for cases like rx.list. if isinstance(mod, tuple) else "" ) @@ -1135,7 +1141,7 @@ class PyiGenerator: if pyi_path: self.written_files.append(pyi_path) - def scan_all(self, targets, changed_files: list[Path] | None = None): + def scan_all(self, targets: list, changed_files: list[Path] | None = None): """Scan all targets for class inheriting Component and generate the .pyi files. Args: @@ -1201,4 +1207,4 @@ class PyiGenerator: or "Var[Template]" in line ): line = line.rstrip() + " # type: ignore\n" - print(line, end="") + print(line, end="") # noqa: T201 diff --git a/reflex/utils/registry.py b/reflex/utils/registry.py index d98178c61..47727d659 100644 --- a/reflex/utils/registry.py +++ b/reflex/utils/registry.py @@ -22,15 +22,15 @@ def latency(registry: str) -> int: return 10_000_000 -def average_latency(registry, attempts: int = 3) -> int: +def average_latency(registry: str, attempts: int = 3) -> int: """Get the average latency of a registry. Args: - registry (str): The URL of the registry. - attempts (int): The number of attempts to make. Defaults to 10. + registry: The URL of the registry. + attempts: The number of attempts to make. Defaults to 10. Returns: - int: The average latency of the registry in microseconds. + The average latency of the registry in microseconds. """ return sum(latency(registry) for _ in range(attempts)) // attempts diff --git a/reflex/utils/serializers.py b/reflex/utils/serializers.py index 4bb8dea92..f78438522 100644 --- a/reflex/utils/serializers.py +++ b/reflex/utils/serializers.py @@ -476,7 +476,7 @@ try: base64_image = base64.b64encode(image_bytes).decode("utf-8") try: # Newer method to get the mime type, but does not always work. - mime_type = image.get_format_mimetype() # type: ignore + mime_type = image.get_format_mimetype() # pyright: ignore [reportAttributeAccessIssue] except AttributeError: try: # Fallback method diff --git a/reflex/utils/telemetry.py b/reflex/utils/telemetry.py index b24b4d3bf..ecfd52428 100644 --- a/reflex/utils/telemetry.py +++ b/reflex/utils/telemetry.py @@ -7,6 +7,7 @@ import dataclasses import multiprocessing import platform import warnings +from contextlib import suppress from reflex.config import environment @@ -121,7 +122,7 @@ def _prepare_event(event: str, **kwargs) -> dict: return {} if UTC is None: - # for python 3.9 & 3.10 + # for python 3.10 stamp = datetime.utcnow().isoformat() else: # for python 3.11 & 3.12 @@ -155,12 +156,13 @@ def _prepare_event(event: str, **kwargs) -> dict: def _send_event(event_data: dict) -> bool: try: httpx.post(POSTHOG_API_URL, json=event_data) - return True except Exception: return False + else: + return True -def _send(event, telemetry_enabled, **kwargs): +def _send(event: str, telemetry_enabled: bool | None, **kwargs): from reflex.config import get_config # Get the telemetry_enabled from the config if it is not specified. @@ -171,10 +173,11 @@ def _send(event, telemetry_enabled, **kwargs): if not telemetry_enabled: return False - event_data = _prepare_event(event, **kwargs) - if not event_data: - return False - return _send_event(event_data) + with suppress(Exception): + event_data = _prepare_event(event, **kwargs) + if not event_data: + return False + return _send_event(event_data) def send(event: str, telemetry_enabled: bool | None = None, **kwargs): @@ -186,7 +189,7 @@ def send(event: str, telemetry_enabled: bool | None = None, **kwargs): kwargs: Additional data to send with the event. """ - async def async_send(event, telemetry_enabled, **kwargs): + async def async_send(event: str, telemetry_enabled: bool | None, **kwargs): return _send(event, telemetry_enabled, **kwargs) try: diff --git a/reflex/utils/types.py b/reflex/utils/types.py index 7138dafb1..58fec8f3b 100644 --- a/reflex/utils/types.py +++ b/reflex/utils/types.py @@ -24,7 +24,7 @@ from typing import ( Tuple, Type, Union, - _GenericAlias, # type: ignore + _GenericAlias, # pyright: ignore [reportAttributeAccessIssue] get_args, get_type_hints, ) @@ -39,7 +39,9 @@ from reflex.components.core.breakpoints import Breakpoints try: from pydantic.v1.fields import ModelField except ModuleNotFoundError: - from pydantic.fields import ModelField # type: ignore + from pydantic.fields import ( + ModelField, # pyright: ignore [reportAttributeAccessIssue] + ) from sqlalchemy.ext.associationproxy import AssociationProxyInstance from sqlalchemy.ext.hybrid import hybrid_property @@ -70,13 +72,15 @@ GenericAliasTypes = [_GenericAlias] with contextlib.suppress(ImportError): # For newer versions of Python. - from types import GenericAlias # type: ignore + from types import GenericAlias GenericAliasTypes.append(GenericAlias) with contextlib.suppress(ImportError): # For older versions of Python. - from typing import _SpecialGenericAlias # type: ignore + from typing import ( + _SpecialGenericAlias, # pyright: ignore [reportAttributeAccessIssue] + ) GenericAliasTypes.append(_SpecialGenericAlias) @@ -97,7 +101,6 @@ StateIterVar = Union[list, set, tuple] if TYPE_CHECKING: from reflex.vars.base import Var - # ArgsSpec = Callable[[Var], list[Var]] ArgsSpec = ( Callable[[], Sequence[Var]] | Callable[[Var], Sequence[Var]] @@ -154,7 +157,7 @@ class Unset: @lru_cache() -def get_origin(tp): +def get_origin(tp: Any): """Get the origin of a class. Args: @@ -176,7 +179,7 @@ def is_generic_alias(cls: GenericType) -> bool: Returns: Whether the class is a generic alias. """ - return isinstance(cls, GenericAliasTypes) + return isinstance(cls, GenericAliasTypes) # pyright: ignore [reportArgumentType] def unionize(*args: GenericType) -> Type: @@ -189,14 +192,14 @@ def unionize(*args: GenericType) -> Type: The unionized types. """ if not args: - return Any + return Any # pyright: ignore [reportReturnType] if len(args) == 1: return args[0] # We are bisecting the args list here to avoid hitting the recursion limit # In Python versions >= 3.11, we can simply do `return Union[*args]` midpoint = len(args) // 2 first_half, second_half = args[:midpoint], args[midpoint:] - return Union[unionize(*first_half), unionize(*second_half)] + return Union[unionize(*first_half), unionize(*second_half)] # pyright: ignore [reportReturnType] def is_none(cls: GenericType) -> bool: @@ -237,7 +240,7 @@ def is_literal(cls: GenericType) -> bool: return get_origin(cls) is Literal -def has_args(cls) -> bool: +def has_args(cls: Type) -> bool: """Check if the class has generic parameters. Args: @@ -331,7 +334,11 @@ def get_attribute_access_type(cls: GenericType, name: str) -> GenericType | None type_ = field.outer_type_ if isinstance(type_, ModelField): type_ = type_.type_ - if not field.required and field.default is None: + if ( + not field.required + and field.default is None + and field.default_factory is None + ): # Ensure frontend uses null coalescing when accessing. type_ = Optional[type_] return type_ @@ -348,13 +355,13 @@ def get_attribute_access_type(cls: GenericType, name: str) -> GenericType | None if type_ is not None: if hasattr(column_type, "item_type"): try: - item_type = column_type.item_type.python_type # type: ignore + item_type = column_type.item_type.python_type # pyright: ignore [reportAttributeAccessIssue] except NotImplementedError: item_type = None if item_type is not None: if type_ in PrimitiveToAnnotation: - type_ = PrimitiveToAnnotation[type_] # type: ignore - type_ = type_[item_type] # type: ignore + type_ = PrimitiveToAnnotation[type_] + type_ = type_[item_type] # pyright: ignore [reportIndexIssue] if column.nullable: type_ = Optional[type_] return type_ @@ -429,7 +436,7 @@ def get_base_class(cls: GenericType) -> Type: return type(get_args(cls)[0]) if is_union(cls): - return tuple(get_base_class(arg) for arg in get_args(cls)) + return tuple(get_base_class(arg) for arg in get_args(cls)) # pyright: ignore [reportReturnType] return get_base_class(cls.__origin__) if is_generic_alias(cls) else cls @@ -602,7 +609,9 @@ def _isinstance(obj: Any, cls: GenericType, nested: bool = False) -> bool: return ( isinstance(obj, tuple) and len(obj) == len(args) - and all(_isinstance(item, arg) for item, arg in zip(obj, args)) + and all( + _isinstance(item, arg) for item, arg in zip(obj, args, strict=True) + ) ) if origin in (dict, Breakpoints): return isinstance(obj, dict) and all( @@ -744,7 +753,7 @@ def check_prop_in_allowed_types(prop: Any, allowed_types: Iterable) -> bool: return type_ in allowed_types -def is_encoded_fstring(value) -> bool: +def is_encoded_fstring(value: Any) -> bool: """Check if a value is an encoded Var f-string. Args: @@ -787,7 +796,7 @@ def validate_literal(key: str, value: Any, expected_type: Type, comp_name: str): ) -def validate_parameter_literals(func): +def validate_parameter_literals(func: Callable): """Decorator to check that the arguments passed to a function correspond to the correct function parameter if it (the parameter) is a literal type. @@ -805,7 +814,7 @@ def validate_parameter_literals(func): annotations = {param[0]: param[1].annotation for param in func_params} # validate args - for param, arg in zip(annotations, args): + for param, arg in zip(annotations, args, strict=False): if annotations[param] is inspect.Parameter.empty: continue validate_literal(param, arg, annotations[param], func.__name__) @@ -826,6 +835,22 @@ StateBases = get_base_class(StateVar) StateIterBases = get_base_class(StateIterVar) +def safe_issubclass(cls: Type, cls_check: Type | Tuple[Type, ...]): + """Check if a class is a subclass of another class. Returns False if internal error occurs. + + Args: + cls: The class to check. + cls_check: The class to check against. + + Returns: + Whether the class is a subclass of the other class. + """ + try: + return issubclass(cls, cls_check) + except TypeError: + return False + + def typehint_issubclass(possible_subclass: Any, possible_superclass: Any) -> bool: """Check if a type hint is a subclass of another type hint. @@ -887,6 +912,8 @@ def typehint_issubclass(possible_subclass: Any, possible_superclass: Any) -> boo # It also ignores when the length of the arguments is different return all( typehint_issubclass(provided_arg, accepted_arg) - for provided_arg, accepted_arg in zip(provided_args, accepted_args) + for provided_arg, accepted_arg in zip( + provided_args, accepted_args, strict=False + ) if accepted_arg is not Any ) diff --git a/reflex/vars/__init__.py b/reflex/vars/__init__.py index 1a4cebe19..cb02319bc 100644 --- a/reflex/vars/__init__.py +++ b/reflex/vars/__init__.py @@ -9,6 +9,7 @@ from .base import get_unique_variable_name as get_unique_variable_name from .base import get_uuid_string_var as get_uuid_string_var from .base import var_operation as var_operation from .base import var_operation_return as var_operation_return +from .datetime import DateTimeVar as DateTimeVar from .function import FunctionStringVar as FunctionStringVar from .function import FunctionVar as FunctionVar from .function import VarOperationCall as VarOperationCall diff --git a/reflex/vars/base.py b/reflex/vars/base.py index ea78d1a89..be4f19955 100644 --- a/reflex/vars/base.py +++ b/reflex/vars/base.py @@ -5,14 +5,13 @@ from __future__ import annotations import contextlib import dataclasses import datetime -import dis import functools import inspect import json import random import re import string -import sys +import uuid import warnings from types import CodeType, FunctionType from typing import ( @@ -20,14 +19,17 @@ from typing import ( Any, Callable, ClassVar, + Coroutine, Dict, FrozenSet, Generic, Iterable, List, Literal, + Mapping, NoReturn, Optional, + Sequence, Set, Tuple, Type, @@ -38,16 +40,18 @@ from typing import ( overload, ) +from sqlalchemy.orm import DeclarativeBase from typing_extensions import ParamSpec, TypeGuard, deprecated, get_type_hints, override from reflex import constants from reflex.base import Base -from reflex.utils import console, imports, serializers, types +from reflex.constants.compiler import Hooks +from reflex.utils import console, exceptions, imports, serializers, types from reflex.utils.exceptions import ( + UntypedComputedVarError, VarAttributeError, VarDependencyError, VarTypeError, - VarValueError, ) from reflex.utils.format import format_state_name from reflex.utils.imports import ( @@ -63,6 +67,7 @@ from reflex.utils.types import ( _isinstance, get_origin, has_args, + safe_issubclass, unionize, ) @@ -76,6 +81,8 @@ if TYPE_CHECKING: VAR_TYPE = TypeVar("VAR_TYPE", covariant=True) OTHER_VAR_TYPE = TypeVar("OTHER_VAR_TYPE") +STRING_T = TypeVar("STRING_T", bound=str) +SEQUENCE_TYPE = TypeVar("SEQUENCE_TYPE", bound=Sequence) warnings.filterwarnings("ignore", message="fields may not start with an underscore") @@ -115,12 +122,20 @@ class VarData: # Hooks that need to be present in the component to render this var hooks: Tuple[str, ...] = dataclasses.field(default_factory=tuple) + # Dependencies of the var + deps: Tuple[Var, ...] = dataclasses.field(default_factory=tuple) + + # Position of the hook in the component + position: Hooks.HookPosition | None = None + def __init__( self, state: str = "", field_name: str = "", imports: ImportDict | ParsedImportDict | None = None, - hooks: dict[str, None] | None = None, + hooks: Mapping[str, VarData | None] | Sequence[str] | str | None = None, + deps: list[Var] | None = None, + position: Hooks.HookPosition | None = None, ): """Initialize the var data. @@ -129,16 +144,32 @@ class VarData: field_name: The name of the field in the state. imports: Imports needed to render this var. hooks: Hooks that need to be present in the component to render this var. + deps: Dependencies of the var for useCallback. + position: Position of the hook in the component. """ + if isinstance(hooks, str): + hooks = [hooks] + if not isinstance(hooks, dict): + hooks = {hook: None for hook in (hooks or [])} immutable_imports: ImmutableParsedImportDict = tuple( - sorted( - ((k, tuple(sorted(v))) for k, v in parse_imports(imports or {}).items()) - ) + (k, tuple(v)) for k, v in parse_imports(imports or {}).items() ) object.__setattr__(self, "state", state) object.__setattr__(self, "field_name", field_name) object.__setattr__(self, "imports", immutable_imports) object.__setattr__(self, "hooks", tuple(hooks or {})) + object.__setattr__(self, "deps", tuple(deps or [])) + object.__setattr__(self, "position", position or None) + + if hooks and any(hooks.values()): + merged_var_data = VarData.merge(self, *hooks.values()) + if merged_var_data is not None: + object.__setattr__(self, "state", merged_var_data.state) + object.__setattr__(self, "field_name", merged_var_data.field_name) + object.__setattr__(self, "imports", merged_var_data.imports) + object.__setattr__(self, "hooks", merged_var_data.hooks) + object.__setattr__(self, "deps", merged_var_data.deps) + object.__setattr__(self, "position", merged_var_data.position) def old_school_imports(self) -> ImportDict: """Return the imports as a mutable dict. @@ -146,7 +177,7 @@ class VarData: Returns: The imports as a mutable dict. """ - return dict((k, list(v)) for k, v in self.imports) + return {k: list(v) for k, v in self.imports} def merge(*all: VarData | None) -> VarData | None: """Merge multiple var data objects. @@ -154,6 +185,9 @@ class VarData: Args: *all: The var data objects to merge. + Raises: + ReflexError: If trying to merge VarData with different positions. + Returns: The merged var data object. @@ -178,18 +212,40 @@ class VarData: (var_data.state for var_data in all_var_datas if var_data.state), "" ) - hooks = {hook: None for var_data in all_var_datas for hook in var_data.hooks} + hooks: dict[str, VarData | None] = { + hook: None for var_data in all_var_datas for hook in var_data.hooks + } _imports = imports.merge_imports( *(var_data.imports for var_data in all_var_datas) ) - if state or _imports or hooks or field_name: + deps = [dep for var_data in all_var_datas for dep in var_data.deps] + + positions = list( + { + var_data.position + for var_data in all_var_datas + if var_data.position is not None + } + ) + if positions: + if len(positions) > 1: + raise exceptions.ReflexError( + f"Cannot merge var data with different positions: {positions}" + ) + position = positions[0] + else: + position = None + + if state or _imports or hooks or field_name or deps or position: return VarData( state=state, field_name=field_name, imports=_imports, hooks=hooks, + deps=deps, + position=position, ) return None @@ -200,7 +256,14 @@ class VarData: Returns: True if any field is set to a non-default value. """ - return bool(self.state or self.imports or self.hooks or self.field_name) + return bool( + self.state + or self.imports + or self.hooks + or self.field_name + or self.deps + or self.position + ) @classmethod def from_state(cls, state: Type[BaseState] | str, field_name: str = "") -> VarData: @@ -387,7 +450,7 @@ class Var(Generic[VAR_TYPE]): @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class ToVarOperation(ToOperation, cls): """Base class of converting a var to another var type.""" @@ -398,7 +461,12 @@ class Var(Generic[VAR_TYPE]): _default_var_type: ClassVar[GenericType] = default_type - ToVarOperation.__name__ = f'To{cls.__name__.removesuffix("Var")}Operation' + new_to_var_operation_name = f"To{cls.__name__.removesuffix('Var')}Operation" + ToVarOperation.__qualname__ = ( + ToVarOperation.__qualname__.removesuffix(ToVarOperation.__name__) + + new_to_var_operation_name + ) + ToVarOperation.__name__ = new_to_var_operation_name _var_subclasses.append(VarSubclassEntry(cls, ToVarOperation, python_types)) @@ -447,20 +515,30 @@ class Var(Generic[VAR_TYPE]): @overload def _replace( - self, _var_type: Type[OTHER_VAR_TYPE], merge_var_data=None, **kwargs: Any + self, + _var_type: Type[OTHER_VAR_TYPE], + merge_var_data: VarData | None = None, + **kwargs: Any, ) -> Var[OTHER_VAR_TYPE]: ... @overload def _replace( - self, _var_type: GenericType | None = None, merge_var_data=None, **kwargs: Any + self, + _var_type: GenericType | None = None, + merge_var_data: VarData | None = None, + **kwargs: Any, ) -> Self: ... def _replace( - self, _var_type: GenericType | None = None, merge_var_data=None, **kwargs: Any + self, + _var_type: GenericType | None = None, + merge_var_data: VarData | None = None, + **kwargs: Any, ) -> Self | Var: """Make a copy of this Var with updated fields. Args: + _var_type: The new type of the Var. merge_var_data: VarData to merge into the existing VarData. **kwargs: Var fields to update. @@ -480,7 +558,6 @@ class Var(Generic[VAR_TYPE]): raise TypeError( "The _var_full_name_needs_state_prefix argument is not supported for Var." ) - value_with_replaced = dataclasses.replace( self, _var_type=_var_type or self._var_type, @@ -495,56 +572,89 @@ class Var(Generic[VAR_TYPE]): return value_with_replaced + @overload + @classmethod + def create( # pyright: ignore[reportOverlappingOverload] + cls, + value: bool, + _var_data: VarData | None = None, + ) -> BooleanVar: ... + + @overload @classmethod def create( cls, - value: Any, - _var_is_local: bool | None = None, - _var_is_string: bool | None = None, + value: int, _var_data: VarData | None = None, - ) -> Var: + ) -> NumberVar[int]: ... + + @overload + @classmethod + def create( + cls, + value: float, + _var_data: VarData | None = None, + ) -> NumberVar[float]: ... + + @overload + @classmethod + def create( # pyright: ignore [reportOverlappingOverload] + cls, + value: STRING_T, + _var_data: VarData | None = None, + ) -> StringVar[STRING_T]: ... + + @overload + @classmethod + def create( # pyright: ignore[reportOverlappingOverload] + cls, + value: None, + _var_data: VarData | None = None, + ) -> NoneVar: ... + + @overload + @classmethod + def create( + cls, + value: MAPPING_TYPE, + _var_data: VarData | None = None, + ) -> ObjectVar[MAPPING_TYPE]: ... + + @overload + @classmethod + def create( + cls, + value: SEQUENCE_TYPE, + _var_data: VarData | None = None, + ) -> ArrayVar[SEQUENCE_TYPE]: ... + + @overload + @classmethod + def create( + cls, + value: OTHER_VAR_TYPE, + _var_data: VarData | None = None, + ) -> Var[OTHER_VAR_TYPE]: ... + + @classmethod + def create( + cls, + value: OTHER_VAR_TYPE, + _var_data: VarData | None = None, + ) -> Var[OTHER_VAR_TYPE]: """Create a var from a value. Args: value: The value to create the var from. - _var_is_local: Whether the var is local. Deprecated. - _var_is_string: Whether the var is a string literal. Deprecated. _var_data: Additional hooks and imports associated with the Var. Returns: The var. """ - if _var_is_local is not None: - console.deprecate( - feature_name="_var_is_local", - reason="The _var_is_local argument is not supported for Var." - "If you want to create a Var from a raw Javascript expression, use the constructor directly", - deprecation_version="0.6.0", - removal_version="0.7.0", - ) - if _var_is_string is not None: - console.deprecate( - feature_name="_var_is_string", - reason="The _var_is_string argument is not supported for Var." - "If you want to create a Var from a raw Javascript expression, use the constructor directly", - deprecation_version="0.6.0", - removal_version="0.7.0", - ) - # If the value is already a var, do nothing. if isinstance(value, Var): return value - # Try to pull the imports and hooks from contained values. - if not isinstance(value, str): - return LiteralVar.create(value) - - if _var_is_string is False or _var_is_local is True: - return cls( - _js_expr=value, - _var_data=_var_data, - ) - return LiteralVar.create(value, _var_data=_var_data) @classmethod @@ -599,8 +709,8 @@ class Var(Generic[VAR_TYPE]): @overload def to( self, - output: type[dict], - ) -> ObjectVar[dict]: ... + output: type[MAPPING_TYPE], + ) -> ObjectVar[MAPPING_TYPE]: ... @overload def to( @@ -642,14 +752,16 @@ class Var(Generic[VAR_TYPE]): # If the first argument is a python type, we map it to the corresponding Var type. for var_subclass in _var_subclasses[::-1]: - if fixed_output_type in var_subclass.python_types: + if fixed_output_type in var_subclass.python_types or safe_issubclass( + fixed_output_type, var_subclass.python_types + ): return self.to(var_subclass.var_subclass, output) if fixed_output_type is None: - return get_to_operation(NoneVar).create(self) # type: ignore + return get_to_operation(NoneVar).create(self) # pyright: ignore [reportReturnType] # Handle fixed_output_type being Base or a dataclass. - if can_use_in_object_var(fixed_output_type): + if can_use_in_object_var(output): return self.to(ObjectVar, output) if inspect.isclass(output): @@ -663,7 +775,7 @@ class Var(Generic[VAR_TYPE]): to_operation_return = var_subclass.to_var_subclass.create( value=self, _var_type=new_var_type ) - return to_operation_return # type: ignore + return to_operation_return # pyright: ignore [reportReturnType] # If we can't determine the first argument, we just replace the _var_type. if not issubclass(output, Var) or var_type is None: @@ -681,6 +793,9 @@ class Var(Generic[VAR_TYPE]): return self + @overload + def guess_type(self: Var[NoReturn]) -> Var[Any]: ... # pyright: ignore [reportOverlappingOverload] + @overload def guess_type(self: Var[str]) -> StringVar: ... @@ -690,6 +805,9 @@ class Var(Generic[VAR_TYPE]): @overload def guess_type(self: Var[int] | Var[float] | Var[int | float]) -> NumberVar: ... + @overload + def guess_type(self: Var[BASE_TYPE]) -> ObjectVar[BASE_TYPE]: ... + @overload def guess_type(self) -> Self: ... @@ -776,7 +894,7 @@ class Var(Generic[VAR_TYPE]): return False if issubclass(type_, list): return [] - if issubclass(type_, dict): + if issubclass(type_, Mapping): return {} if issubclass(type_, tuple): return () @@ -838,7 +956,7 @@ class Var(Generic[VAR_TYPE]): return setter - def _var_set_state(self, state: type[BaseState] | str): + def _var_set_state(self, state: type[BaseState] | str) -> Self: """Set the state of the var. Args: @@ -853,7 +971,7 @@ class Var(Generic[VAR_TYPE]): else format_state_name(state.get_full_name()) ) - return StateOperation.create( + return StateOperation.create( # pyright: ignore [reportReturnType] formatted_state_name, self, _var_data=VarData.merge( @@ -982,7 +1100,7 @@ class Var(Generic[VAR_TYPE]): f"$/{constants.Dirs.STATE_PATH}": [imports.ImportVar(tag="refs")] } ), - ).to(ObjectVar, Dict[str, str]) + ).to(ObjectVar, Mapping[str, str]) return refs[LiteralVar.create(str(self))] @deprecated("Use `.js_type()` instead.") @@ -1032,43 +1150,6 @@ class Var(Generic[VAR_TYPE]): """ return self - def __getattr__(self, name: str): - """Get an attribute of the var. - - Args: - name: The name of the attribute. - - Returns: - The attribute. - - Raises: - VarAttributeError: If the attribute does not exist. - TypeError: If the var type is Any. - """ - if name.startswith("_"): - return self.__getattribute__(name) - - if name == "contains": - raise TypeError( - f"Var of type {self._var_type} does not support contains check." - ) - if name == "reverse": - raise TypeError("Cannot reverse non-list var.") - - if self._var_type is Any: - raise TypeError( - f"You must provide an annotation for the state var `{self!s}`. Annotation cannot be `{self._var_type}`." - ) - - if name in REPLACED_NAMES: - raise VarAttributeError( - f"Field {name!r} was renamed to {REPLACED_NAMES[name]!r}" - ) - - raise VarAttributeError( - f"The State var has no attribute '{name}' or may have been annotated wrongly.", - ) - def _decode(self) -> Any: """Decode Var as a python value. @@ -1079,7 +1160,7 @@ class Var(Generic[VAR_TYPE]): The decoded value or the Var name. """ if isinstance(self, LiteralVar): - return self._var_value # type: ignore + return self._var_value try: return json.loads(str(self)) except ValueError: @@ -1130,36 +1211,76 @@ class Var(Generic[VAR_TYPE]): return ArrayVar.range(first_endpoint, second_endpoint, step) - def __bool__(self) -> bool: - """Raise exception if using Var in a boolean context. + if not TYPE_CHECKING: - Raises: - VarTypeError: when attempting to bool-ify the Var. - """ - raise VarTypeError( - f"Cannot convert Var {str(self)!r} to bool for use with `if`, `and`, `or`, and `not`. " - "Instead use `rx.cond` and bitwise operators `&` (and), `|` (or), `~` (invert)." - ) + def __getattr__(self, name: str): + """Get an attribute of the var. - def __iter__(self) -> Any: - """Raise exception if using Var in an iterable context. + Args: + name: The name of the attribute. - Raises: - VarTypeError: when attempting to iterate over the Var. - """ - raise VarTypeError( - f"Cannot iterate over Var {str(self)!r}. Instead use `rx.foreach`." - ) + Raises: + VarAttributeError: If the attribute does not exist. + UntypedVarError: If the var type is Any. + TypeError: If the var type is Any. - def __contains__(self, _: Any) -> Var: - """Override the 'in' operator to alert the user that it is not supported. + # noqa: DAR101 self + """ + if name.startswith("_"): + raise VarAttributeError(f"Attribute {name} not found.") - Raises: - VarTypeError: the operation is not supported - """ - raise VarTypeError( - "'in' operator not supported for Var types, use Var.contains() instead." - ) + if name == "contains": + raise TypeError( + f"Var of type {self._var_type} does not support contains check." + ) + if name == "reverse": + raise TypeError("Cannot reverse non-list var.") + + if self._var_type is Any: + raise exceptions.UntypedVarError( + f"You must provide an annotation for the state var `{self!s}`. Annotation cannot be `{self._var_type}`." + ) + + raise VarAttributeError( + f"The State var has no attribute '{name}' or may have been annotated wrongly.", + ) + + def __bool__(self) -> bool: + """Raise exception if using Var in a boolean context. + + Raises: + VarTypeError: when attempting to bool-ify the Var. + + # noqa: DAR101 self + """ + raise VarTypeError( + f"Cannot convert Var {str(self)!r} to bool for use with `if`, `and`, `or`, and `not`. " + "Instead use `rx.cond` and bitwise operators `&` (and), `|` (or), `~` (invert)." + ) + + def __iter__(self) -> Any: + """Raise exception if using Var in an iterable context. + + Raises: + VarTypeError: when attempting to iterate over the Var. + + # noqa: DAR101 self + """ + raise VarTypeError( + f"Cannot iterate over Var {str(self)!r}. Instead use `rx.foreach`." + ) + + def __contains__(self, _: Any) -> Var: + """Override the 'in' operator to alert the user that it is not supported. + + Raises: + VarTypeError: the operation is not supported + + # noqa: DAR101 self + """ + raise VarTypeError( + "'in' operator not supported for Var types, use Var.contains() instead." + ) OUTPUT = TypeVar("OUTPUT", bound=Var) @@ -1206,7 +1327,7 @@ class ToOperation: """ return VarData.merge( self._original._get_all_var_data(), - self._var_data, # type: ignore + self._var_data, ) @classmethod @@ -1227,10 +1348,10 @@ class ToOperation: The ToOperation. """ return cls( - _js_expr="", # type: ignore - _var_data=_var_data, # type: ignore - _var_type=_var_type or cls._default_var_type, # type: ignore - _original=value, # type: ignore + _js_expr="", # pyright: ignore [reportCallIssue] + _var_data=_var_data, # pyright: ignore [reportCallIssue] + _var_type=_var_type or cls._default_var_type, # pyright: ignore [reportCallIssue, reportAttributeAccessIssue] + _original=value, # pyright: ignore [reportCallIssue] ) @@ -1292,7 +1413,7 @@ class LiteralVar(Var): _var_literal_subclasses.append((cls, var_subclass)) @classmethod - def create( + def create( # pyright: ignore [reportArgumentType] cls, value: Any, _var_data: VarData | None = None, @@ -1310,7 +1431,7 @@ class LiteralVar(Var): TypeError: If the value is not a supported type for LiteralVar. """ from .object import LiteralObjectVar - from .sequence import LiteralStringVar + from .sequence import ArrayVar, LiteralStringVar if isinstance(value, Var): if _var_data is None: @@ -1329,7 +1450,7 @@ class LiteralVar(Var): serialized_value = serializers.serialize(value) if serialized_value is not None: - if isinstance(serialized_value, dict): + if isinstance(serialized_value, Mapping): return LiteralObjectVar.create( serialized_value, _var_type=type(value), @@ -1366,6 +1487,9 @@ class LiteralVar(Var): _var_data=_var_data, ) + if isinstance(value, range): + return ArrayVar.range(value.start, value.stop, value.step) + raise TypeError( f"Unsupported type {type(value)} for LiteralVar. Tried to create a LiteralVar from {value}." ) @@ -1373,6 +1497,12 @@ class LiteralVar(Var): def __post_init__(self): """Post-initialize the var.""" + @property + def _var_value(self) -> Any: + raise NotImplementedError( + "LiteralVar subclasses must implement the _var_value property." + ) + def json(self) -> str: """Serialize the var to a JSON string. @@ -1419,7 +1549,7 @@ T = TypeVar("T") # NoReturn is used to match CustomVarOperationReturn with no type hint. @overload -def var_operation( +def var_operation( # pyright: ignore [reportOverlappingOverload] func: Callable[P, CustomVarOperationReturn[NoReturn]], ) -> Callable[P, Var]: ... @@ -1445,7 +1575,7 @@ def var_operation( ) -> Callable[P, StringVar]: ... -LIST_T = TypeVar("LIST_T", bound=Union[List[Any], Tuple, Set]) +LIST_T = TypeVar("LIST_T", bound=Sequence) @overload @@ -1454,7 +1584,7 @@ def var_operation( ) -> Callable[P, ArrayVar[LIST_T]]: ... -OBJECT_TYPE = TypeVar("OBJECT_TYPE", bound=Dict) +OBJECT_TYPE = TypeVar("OBJECT_TYPE", bound=Mapping) @overload @@ -1469,7 +1599,7 @@ def var_operation( ) -> Callable[P, Var[T]]: ... -def var_operation( +def var_operation( # pyright: ignore [reportInconsistentOverload] func: Callable[P, CustomVarOperationReturn[T]], ) -> Callable[P, Var[T]]: """Decorator for creating a var operation. @@ -1503,7 +1633,7 @@ def var_operation( return CustomVarOperation.create( name=func.__name__, args=tuple(list(args_vars.items()) + list(kwargs_vars.items())), - return_var=func(*args_vars.values(), **kwargs_vars), # type: ignore + return_var=func(*args_vars.values(), **kwargs_vars), # pyright: ignore [reportCallIssue, reportReturnType] ).guess_type() return wrapper @@ -1529,25 +1659,100 @@ def figure_out_type(value: Any) -> types.GenericType: return Set[unionize(*(figure_out_type(v) for v in value))] if isinstance(value, tuple): return Tuple[unionize(*(figure_out_type(v) for v in value)), ...] - if isinstance(value, dict): - return Dict[ + if isinstance(value, Mapping): + return Mapping[ unionize(*(figure_out_type(k) for k in value)), unionize(*(figure_out_type(v) for v in value.values())), ] return type(value) -class cached_property_no_lock(functools.cached_property): - """A special version of functools.cached_property that does not use a lock.""" +GLOBAL_CACHE = {} - def __init__(self, func): - """Initialize the cached_property_no_lock. + +class cached_property: # noqa: N801 + """A cached property that caches the result of the function.""" + + def __init__(self, func: Callable): + """Initialize the cached_property. Args: func: The function to cache. """ - super().__init__(func) - self.lock = contextlib.nullcontext() + self._func = func + self._attrname = None + + def __set_name__(self, owner: Any, name: str): + """Set the name of the cached property. + + Args: + owner: The owner of the cached property. + name: The name of the cached property. + + Raises: + TypeError: If the cached property is assigned to two different names. + """ + if self._attrname is None: + self._attrname = name + + original_del = getattr(owner, "__del__", None) + + def delete_property(this: Any): + """Delete the cached property. + + Args: + this: The object to delete the cached property from. + """ + cached_field_name = "_reflex_cache_" + name + try: + unique_id = object.__getattribute__(this, cached_field_name) + except AttributeError: + if original_del is not None: + original_del(this) + return + if unique_id in GLOBAL_CACHE: + del GLOBAL_CACHE[unique_id] + + if original_del is not None: + original_del(this) + + owner.__del__ = delete_property + + elif name != self._attrname: + raise TypeError( + "Cannot assign the same cached_property to two different names " + f"({self._attrname!r} and {name!r})." + ) + + def __get__(self, instance: Any, owner: Type | None = None): + """Get the cached property. + + Args: + instance: The instance to get the cached property from. + owner: The owner of the cached property. + + Returns: + The cached property. + + Raises: + TypeError: If the class does not have __set_name__. + """ + if self._attrname is None: + raise TypeError( + "Cannot use cached_property on a class without __set_name__." + ) + cached_field_name = "_reflex_cache_" + self._attrname + try: + unique_id = object.__getattribute__(instance, cached_field_name) + except AttributeError: + unique_id = uuid.uuid4().int + object.__setattr__(instance, cached_field_name, unique_id) + if unique_id not in GLOBAL_CACHE: + GLOBAL_CACHE[unique_id] = self._func(instance) + return GLOBAL_CACHE[unique_id] + + +cached_property_no_lock = cached_property class CachedVarOperation: @@ -1569,11 +1774,11 @@ class CachedVarOperation: if name == "_js_expr": return self._cached_var_name - parent_classes = inspect.getmro(self.__class__) + parent_classes = inspect.getmro(type(self)) next_class = parent_classes[parent_classes.index(CachedVarOperation) + 1] - return next_class.__getattr__(self, name) # type: ignore + return next_class.__getattr__(self, name) def _get_all_var_data(self) -> VarData | None: """Get all VarData associated with the Var. @@ -1591,14 +1796,12 @@ class CachedVarOperation: The cached VarData. """ return VarData.merge( - *map( - lambda value: ( - value._get_all_var_data() if isinstance(value, Var) else None - ), - map( - lambda field: getattr(self, field.name), - dataclasses.fields(self), # type: ignore - ), + *( + value._get_all_var_data() if isinstance(value, Var) else None + for value in ( + getattr(self, field.name) + for field in dataclasses.fields(self) # pyright: ignore [reportArgumentType] + ) ), self._var_data, ) @@ -1611,10 +1814,10 @@ class CachedVarOperation: """ return hash( ( - self.__class__.__name__, + type(self).__name__, *[ getattr(self, field.name) - for field in dataclasses.fields(self) # type: ignore + for field in dataclasses.fields(self) # pyright: ignore [reportArgumentType] if field.name not in ["_js_expr", "_var_data", "_var_type"] ], ) @@ -1631,7 +1834,7 @@ def and_operation(a: Var | Any, b: Var | Any) -> Var: Returns: The result of the logical AND operation. """ - return _and_operation(a, b) # type: ignore + return _and_operation(a, b) @var_operation @@ -1661,7 +1864,7 @@ def or_operation(a: Var | Any, b: Var | Any) -> Var: Returns: The result of the logical OR operation. """ - return _or_operation(a, b) # type: ignore + return _or_operation(a, b) @var_operation @@ -1684,7 +1887,7 @@ def _or_operation(a: Var, b: Var): @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class CallableVar(Var): """Decorate a Var-returning function to act as both a Var and a function. @@ -1715,7 +1918,7 @@ class CallableVar(Var): object.__setattr__(self, "fn", fn) object.__setattr__(self, "original_var", original_var) - def __call__(self, *args, **kwargs) -> Var: + def __call__(self, *args: Any, **kwargs: Any) -> Var: """Call the decorated function. Args: @@ -1733,7 +1936,7 @@ class CallableVar(Var): Returns: The hash of the object. """ - return hash((self.__class__.__name__, self.original_var)) + return hash((type(self).__name__, self.original_var)) RETURN_TYPE = TypeVar("RETURN_TYPE") @@ -1765,7 +1968,7 @@ def is_computed_var(obj: Any) -> TypeGuard[ComputedVar]: @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class ComputedVar(Var[RETURN_TYPE]): """A field with computed getters.""" @@ -1780,7 +1983,7 @@ class ComputedVar(Var[RETURN_TYPE]): _initial_value: RETURN_TYPE | types.Unset = dataclasses.field(default=types.Unset()) # Explicit var dependencies to track - _static_deps: set[str] = dataclasses.field(default_factory=set) + _static_deps: dict[str | None, set[str]] = dataclasses.field(default_factory=dict) # Whether var dependencies should be auto-determined _auto_deps: bool = dataclasses.field(default=True) @@ -1790,13 +1993,13 @@ class ComputedVar(Var[RETURN_TYPE]): _fget: Callable[[BaseState], RETURN_TYPE] = dataclasses.field( default_factory=lambda: lambda _: None - ) # type: ignore + ) # pyright: ignore [reportAssignmentType] def __init__( self, fget: Callable[[BASE_STATE], RETURN_TYPE], initial_value: RETURN_TYPE | types.Unset = types.Unset(), - cache: bool = False, + cache: bool = True, deps: Optional[List[Union[str, Var]]] = None, auto_deps: bool = True, interval: Optional[Union[int, datetime.timedelta]] = None, @@ -1817,19 +2020,14 @@ class ComputedVar(Var[RETURN_TYPE]): Raises: TypeError: If the computed var dependencies are not Var instances or var names. + UntypedComputedVarError: If the computed var is untyped. """ hint = kwargs.pop("return_type", None) or get_type_hints(fget).get( "return", Any ) if hint is Any: - console.deprecate( - "untyped-computed-var", - "ComputedVar should have a return type annotation.", - "0.6.5", - "0.7.0", - ) - + raise UntypedComputedVarError(var_name=fget.__name__) kwargs.setdefault("_js_expr", fget.__name__) kwargs.setdefault("_var_type", hint) @@ -1855,28 +2053,78 @@ class ComputedVar(Var[RETURN_TYPE]): object.__setattr__(self, "_update_interval", interval) - if deps is None: - deps = [] - else: - for dep in deps: - if isinstance(dep, Var): - continue - if isinstance(dep, str) and dep != "": - continue - raise TypeError( - "ComputedVar dependencies must be Var instances or var names (non-empty strings)." - ) object.__setattr__( self, "_static_deps", - {dep._js_expr if isinstance(dep, Var) else dep for dep in deps}, + self._calculate_static_deps(deps), ) object.__setattr__(self, "_auto_deps", auto_deps) object.__setattr__(self, "_fget", fget) + def _calculate_static_deps( + self, + deps: Union[List[Union[str, Var]], dict[str | None, set[str]]] | None = None, + ) -> dict[str | None, set[str]]: + """Calculate the static dependencies of the computed var from user input or existing dependencies. + + Args: + deps: The user input dependencies or existing dependencies. + + Returns: + The static dependencies. + """ + if isinstance(deps, dict): + # Assume a dict is coming from _replace, so no special processing. + return deps + _static_deps = {} + if deps is not None: + for dep in deps: + _static_deps = self._add_static_dep(dep, _static_deps) + return _static_deps + + def _add_static_dep( + self, dep: Union[str, Var], deps: dict[str | None, set[str]] | None = None + ) -> dict[str | None, set[str]]: + """Add a static dependency to the computed var or existing dependency set. + + Args: + dep: The dependency to add. + deps: The existing dependency set. + + Returns: + The updated dependency set. + + Raises: + TypeError: If the computed var dependencies are not Var instances or var names. + """ + if deps is None: + deps = self._static_deps + if isinstance(dep, Var): + state_name = ( + all_var_data.state + if (all_var_data := dep._get_all_var_data()) and all_var_data.state + else None + ) + if all_var_data is not None: + var_name = all_var_data.field_name + else: + var_name = dep._js_expr + deps.setdefault(state_name, set()).add(var_name) + elif isinstance(dep, str) and dep != "": + deps.setdefault(None, set()).add(dep) + else: + raise TypeError( + "ComputedVar dependencies must be Var instances or var names (non-empty strings)." + ) + return deps + @override - def _replace(self, merge_var_data=None, **kwargs: Any) -> Self: + def _replace( + self, + merge_var_data: VarData | None = None, + **kwargs: Any, + ) -> Self: """Replace the attributes of the ComputedVar. Args: @@ -1889,20 +2137,23 @@ class ComputedVar(Var[RETURN_TYPE]): Raises: TypeError: If kwargs contains keys that are not allowed. """ - field_values = dict( - fget=kwargs.pop("fget", self._fget), - initial_value=kwargs.pop("initial_value", self._initial_value), - cache=kwargs.pop("cache", self._cache), - deps=kwargs.pop("deps", self._static_deps), - auto_deps=kwargs.pop("auto_deps", self._auto_deps), - interval=kwargs.pop("interval", self._update_interval), - backend=kwargs.pop("backend", self._backend), - _js_expr=kwargs.pop("_js_expr", self._js_expr), - _var_type=kwargs.pop("_var_type", self._var_type), - _var_data=kwargs.pop( + if "deps" in kwargs: + kwargs["deps"] = self._calculate_static_deps(kwargs["deps"]) + field_values = { + "fget": kwargs.pop("fget", self._fget), + "initial_value": kwargs.pop("initial_value", self._initial_value), + "cache": kwargs.pop("cache", self._cache), + "deps": kwargs.pop("deps", self._static_deps), + "auto_deps": kwargs.pop("auto_deps", self._auto_deps), + "interval": kwargs.pop("interval", self._update_interval), + "backend": kwargs.pop("backend", self._backend), + "_js_expr": kwargs.pop("_js_expr", self._js_expr), + "_var_type": kwargs.pop("_var_type", self._var_type), + "_var_data": kwargs.pop( "_var_data", VarData.merge(self._var_data, merge_var_data) ), - ) + "return_type": kwargs.pop("return_type", self._var_type), + } if kwargs: unexpected_kwargs = ", ".join(kwargs.keys()) @@ -1944,6 +2195,13 @@ class ComputedVar(Var[RETURN_TYPE]): return True return datetime.datetime.now() - last_updated > self._update_interval + @overload + def __get__( + self: ComputedVar[bool], + instance: None, + owner: Type, + ) -> BooleanVar: ... + @overload def __get__( self: ComputedVar[int] | ComputedVar[float], @@ -1960,10 +2218,10 @@ class ComputedVar(Var[RETURN_TYPE]): @overload def __get__( - self: ComputedVar[dict[DICT_KEY, DICT_VAL]], + self: ComputedVar[Mapping[DICT_KEY, DICT_VAL]], instance: None, owner: Type, - ) -> ObjectVar[dict[DICT_KEY, DICT_VAL]]: ... + ) -> ObjectVar[Mapping[DICT_KEY, DICT_VAL]]: ... @overload def __get__( @@ -1972,13 +2230,6 @@ class ComputedVar(Var[RETURN_TYPE]): owner: Type, ) -> ArrayVar[list[LIST_INSIDE]]: ... - @overload - def __get__( - self: ComputedVar[set[LIST_INSIDE]], - instance: None, - owner: Type, - ) -> ArrayVar[set[LIST_INSIDE]]: ... - @overload def __get__( self: ComputedVar[tuple[LIST_INSIDE, ...]], @@ -1992,7 +2243,7 @@ class ComputedVar(Var[RETURN_TYPE]): @overload def __get__(self, instance: BaseState, owner: Type) -> RETURN_TYPE: ... - def __get__(self, instance: BaseState | None, owner): + def __get__(self, instance: BaseState | None, owner: Type): """Get the ComputedVar value. If the value is already cached on the instance, return the cached value. @@ -2035,130 +2286,69 @@ class ComputedVar(Var[RETURN_TYPE]): setattr(instance, self._last_updated_attr, datetime.datetime.now()) value = getattr(instance, self._cache_attr) - if not _isinstance(value, self._var_type): - console.deprecate( - "mismatched-computed-var-return", - f"Computed var {type(instance).__name__}.{self._js_expr} returned value of type {type(value)}, " - f"expected {self._var_type}. This might cause unexpected behavior.", - "0.6.5", - "0.7.0", - ) + self._check_deprecated_return_type(instance, value) return value + def _check_deprecated_return_type(self, instance: BaseState, value: Any) -> None: + if not _isinstance(value, self._var_type): + console.error( + f"Computed var '{type(instance).__name__}.{self._js_expr}' must return" + f" type '{self._var_type}', got '{type(value)}'." + ) + def _deps( self, - objclass: Type, + objclass: Type[BaseState], obj: FunctionType | CodeType | None = None, - self_name: Optional[str] = None, - ) -> set[str]: + ) -> dict[str, set[str]]: """Determine var dependencies of this ComputedVar. - Save references to attributes accessed on "self". Recursively called - when the function makes a method call on "self" or define comprehensions - or nested functions that may reference "self". + Save references to attributes accessed on "self" or other fetched states. + + Recursively called when the function makes a method call on "self" or + define comprehensions or nested functions that may reference "self". Args: objclass: the class obj this ComputedVar is attached to. obj: the object to disassemble (defaults to the fget function). - self_name: if specified, look for this name in LOAD_FAST and LOAD_DEREF instructions. Returns: - A set of variable names accessed by the given obj. - - Raises: - VarValueError: if the function references the get_state, parent_state, or substates attributes - (cannot track deps in a related state, only implicitly via parent state). + A dictionary mapping state names to the set of variable names + accessed by the given obj. """ + from .dep_tracking import DependencyTracker + + d = {} + if self._static_deps: + d.update(self._static_deps) + # None is a placeholder for the current state class. + if None in d: + d[objclass.get_full_name()] = d.pop(None) + if not self._auto_deps: - return self._static_deps - d = self._static_deps.copy() + return d + if obj is None: fget = self._fget if fget is not None: obj = cast(FunctionType, fget) else: - return set() - with contextlib.suppress(AttributeError): - # unbox functools.partial - obj = cast(FunctionType, obj.func) # type: ignore - with contextlib.suppress(AttributeError): - # unbox EventHandler - obj = cast(FunctionType, obj.fn) # type: ignore + return d - if self_name is None and isinstance(obj, FunctionType): - try: - # the first argument to the function is the name of "self" arg - self_name = obj.__code__.co_varnames[0] - except (AttributeError, IndexError): - self_name = None - if self_name is None: - # cannot reference attributes on self if method takes no args - return set() + try: + return DependencyTracker( + func=obj, state_cls=objclass, dependencies=d + ).dependencies + except Exception as e: + console.warn( + "Failed to automatically determine dependencies for computed var " + f"{objclass.__name__}.{self._js_expr}: {e}. " + "Provide static_deps and set auto_deps=False to suppress this warning." + ) + return d - invalid_names = ["get_state", "parent_state", "substates", "get_substate"] - self_is_top_of_stack = False - for instruction in dis.get_instructions(obj): - if ( - instruction.opname in ("LOAD_FAST", "LOAD_DEREF") - and instruction.argval == self_name - ): - # bytecode loaded the class instance to the top of stack, next load instruction - # is referencing an attribute on self - self_is_top_of_stack = True - continue - if self_is_top_of_stack and instruction.opname in ( - "LOAD_ATTR", - "LOAD_METHOD", - ): - try: - ref_obj = getattr(objclass, instruction.argval) - except Exception: - ref_obj = None - if instruction.argval in invalid_names: - raise VarValueError( - f"Cached var {self!s} cannot access arbitrary state via `{instruction.argval}`." - ) - if callable(ref_obj): - # recurse into callable attributes - d.update( - self._deps( - objclass=objclass, - obj=ref_obj, - ) - ) - # recurse into property fget functions - elif isinstance(ref_obj, property) and not isinstance( - ref_obj, ComputedVar - ): - d.update( - self._deps( - objclass=objclass, - obj=ref_obj.fget, # type: ignore - ) - ) - elif ( - instruction.argval in objclass.backend_vars - or instruction.argval in objclass.vars - ): - # var access - d.add(instruction.argval) - elif instruction.opname == "LOAD_CONST" and isinstance( - instruction.argval, CodeType - ): - # recurse into nested functions / comprehensions, which can reference - # instance attributes from the outer scope - d.update( - self._deps( - objclass=objclass, - obj=instruction.argval, - self_name=self_name, - ) - ) - self_is_top_of_stack = False - return d - - def mark_dirty(self, instance) -> None: + def mark_dirty(self, instance: BaseState) -> None: """Mark this ComputedVar as dirty. Args: @@ -2167,6 +2357,37 @@ class ComputedVar(Var[RETURN_TYPE]): with contextlib.suppress(AttributeError): delattr(instance, self._cache_attr) + def add_dependency(self, objclass: Type[BaseState], dep: Var): + """Explicitly add a dependency to the ComputedVar. + + After adding the dependency, when the `dep` changes, this computed var + will be marked dirty. + + Args: + objclass: The class obj this ComputedVar is attached to. + dep: The dependency to add. + + Raises: + VarDependencyError: If the dependency is not a Var instance with a + state and field name + """ + if all_var_data := dep._get_all_var_data(): + state_name = all_var_data.state + if state_name: + var_name = all_var_data.field_name + if var_name: + self._static_deps.setdefault(state_name, set()).add(var_name) + objclass.get_root_state().get_class_substate( + state_name + )._var_dependencies.setdefault(var_name, set()).add( + (objclass.get_full_name(), self._js_expr) + ) + return + raise VarDependencyError( + "ComputedVar dependencies must be Var instances with a state and " + f"field name, got {dep!r}." + ) + def _determine_var_type(self) -> Type: """Get the type of the var. @@ -2176,7 +2397,7 @@ class ComputedVar(Var[RETURN_TYPE]): hints = get_type_hints(self._fget) if "return" in hints: return hints["return"] - return Any + return Any # pyright: ignore [reportReturnType] @property def __class__(self) -> Type: @@ -2203,6 +2424,126 @@ class DynamicRouteVar(ComputedVar[Union[str, List[str]]]): pass +async def _default_async_computed_var(_self: BaseState) -> Any: + return None + + +@dataclasses.dataclass( + eq=False, + frozen=True, + init=False, + slots=True, +) +class AsyncComputedVar(ComputedVar[RETURN_TYPE]): + """A computed var that wraps a coroutinefunction.""" + + _fget: Callable[[BaseState], Coroutine[None, None, RETURN_TYPE]] = ( + dataclasses.field(default=_default_async_computed_var) + ) + + @overload + def __get__( + self: AsyncComputedVar[bool], + instance: None, + owner: Type, + ) -> BooleanVar: ... + + @overload + def __get__( + self: AsyncComputedVar[int] | ComputedVar[float], + instance: None, + owner: Type, + ) -> NumberVar: ... + + @overload + def __get__( + self: AsyncComputedVar[str], + instance: None, + owner: Type, + ) -> StringVar: ... + + @overload + def __get__( + self: AsyncComputedVar[Mapping[DICT_KEY, DICT_VAL]], + instance: None, + owner: Type, + ) -> ObjectVar[Mapping[DICT_KEY, DICT_VAL]]: ... + + @overload + def __get__( + self: AsyncComputedVar[list[LIST_INSIDE]], + instance: None, + owner: Type, + ) -> ArrayVar[list[LIST_INSIDE]]: ... + + @overload + def __get__( + self: AsyncComputedVar[tuple[LIST_INSIDE, ...]], + instance: None, + owner: Type, + ) -> ArrayVar[tuple[LIST_INSIDE, ...]]: ... + + @overload + def __get__(self, instance: None, owner: Type) -> AsyncComputedVar[RETURN_TYPE]: ... + + @overload + def __get__( + self, instance: BaseState, owner: Type + ) -> Coroutine[None, None, RETURN_TYPE]: ... + + def __get__( + self, instance: BaseState | None, owner + ) -> Var | Coroutine[None, None, RETURN_TYPE]: + """Get the ComputedVar value. + + If the value is already cached on the instance, return the cached value. + + Args: + instance: the instance of the class accessing this computed var. + owner: the class that this descriptor is attached to. + + Returns: + The value of the var for the given instance. + """ + if instance is None: + return super(AsyncComputedVar, self).__get__(instance, owner) + + if not self._cache: + + async def _awaitable_result(instance: BaseState = instance) -> RETURN_TYPE: + value = await self.fget(instance) + self._check_deprecated_return_type(instance, value) + return value + + return _awaitable_result() + else: + # handle caching + async def _awaitable_result(instance: BaseState = instance) -> RETURN_TYPE: + if not hasattr(instance, self._cache_attr) or self.needs_update( + instance + ): + # Set cache attr on state instance. + setattr(instance, self._cache_attr, await self.fget(instance)) + # Ensure the computed var gets serialized to redis. + instance._was_touched = True + # Set the last updated timestamp on the state instance. + setattr(instance, self._last_updated_attr, datetime.datetime.now()) + value = getattr(instance, self._cache_attr) + self._check_deprecated_return_type(instance, value) + return value + + return _awaitable_result() + + @property + def fget(self) -> Callable[[BaseState], Coroutine[None, None, RETURN_TYPE]]: + """Get the getter function. + + Returns: + The getter function. + """ + return self._fget + + if TYPE_CHECKING: BASE_STATE = TypeVar("BASE_STATE", bound=BaseState) @@ -2211,20 +2552,20 @@ if TYPE_CHECKING: def computed_var( fget: None = None, initial_value: Any | types.Unset = types.Unset(), - cache: bool = False, + cache: bool = True, deps: Optional[List[Union[str, Var]]] = None, auto_deps: bool = True, interval: Optional[Union[datetime.timedelta, int]] = None, backend: bool | None = None, **kwargs, -) -> Callable[[Callable[[BASE_STATE], RETURN_TYPE]], ComputedVar[RETURN_TYPE]]: ... +) -> Callable[[Callable[[BASE_STATE], RETURN_TYPE]], ComputedVar[RETURN_TYPE]]: ... # pyright: ignore [reportInvalidTypeVarUse] @overload def computed_var( fget: Callable[[BASE_STATE], RETURN_TYPE], initial_value: RETURN_TYPE | types.Unset = types.Unset(), - cache: bool = False, + cache: bool = True, deps: Optional[List[Union[str, Var]]] = None, auto_deps: bool = True, interval: Optional[Union[datetime.timedelta, int]] = None, @@ -2236,7 +2577,7 @@ def computed_var( def computed_var( fget: Callable[[BASE_STATE], Any] | None = None, initial_value: Any | types.Unset = types.Unset(), - cache: bool = False, + cache: bool = True, deps: Optional[List[Union[str, Var]]] = None, auto_deps: bool = True, interval: Optional[Union[datetime.timedelta, int]] = None, @@ -2269,10 +2610,27 @@ def computed_var( raise VarDependencyError("Cannot track dependencies without caching.") if fget is not None: - return ComputedVar(fget, cache=cache) + if inspect.iscoroutinefunction(fget): + computed_var_cls = AsyncComputedVar + else: + computed_var_cls = ComputedVar + return computed_var_cls( + fget, + initial_value=initial_value, + cache=cache, + deps=deps, + auto_deps=auto_deps, + interval=interval, + backend=backend, + **kwargs, + ) def wrapper(fget: Callable[[BASE_STATE], Any]) -> ComputedVar: - return ComputedVar( + if inspect.iscoroutinefunction(fget): + computed_var_cls = AsyncComputedVar + else: + computed_var_cls = ComputedVar + return computed_var_cls( fget, initial_value=initial_value, cache=cache, @@ -2341,7 +2699,7 @@ def var_operation_return( @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class CustomVarOperation(CachedVarOperation, Var[T]): """Base class for custom var operations.""" @@ -2371,10 +2729,7 @@ class CustomVarOperation(CachedVarOperation, Var[T]): The cached VarData. """ return VarData.merge( - *map( - lambda arg: arg[1]._get_all_var_data(), - self._args, - ), + *(arg[1]._get_all_var_data() for arg in self._args), self._return._get_all_var_data(), self._var_data, ) @@ -2415,7 +2770,7 @@ class NoneVar(Var[None], python_types=type(None)): @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class LiteralNoneVar(LiteralVar, NoneVar): """A var representing None.""" @@ -2477,7 +2832,7 @@ def get_to_operation(var_subclass: Type[Var]) -> Type[ToOperation]: @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class StateOperation(CachedVarOperation, Var): """A var operation that accesses a field on an object.""" @@ -2549,7 +2904,7 @@ def get_uuid_string_var() -> Var: unique_uuid_var = get_unique_variable_name() unique_uuid_var_data = VarData( imports={ - f"$/{constants.Dirs.STATE_PATH}": {ImportVar(tag="generateUUID")}, # type: ignore + f"$/{constants.Dirs.STATE_PATH}": {ImportVar(tag="generateUUID")}, # pyright: ignore [reportArgumentType] "react": "useMemo", }, hooks={f"const {unique_uuid_var} = useMemo(generateUUID, [])": None}, @@ -2609,7 +2964,7 @@ def _extract_var_data(value: Iterable) -> list[VarData | None]: elif not isinstance(sub, str): # Recurse into dict values. if hasattr(sub, "values") and callable(sub.values): - var_datas.extend(_extract_var_data(sub.values())) + var_datas.extend(_extract_var_data(sub.values())) # pyright: ignore [reportArgumentType] # Recurse into iterable values (or dict keys). var_datas.extend(_extract_var_data(sub)) @@ -2620,23 +2975,10 @@ def _extract_var_data(value: Iterable) -> list[VarData | None]: # Recurse when value is a dict itself. values = getattr(value, "values", None) if callable(values): - var_datas.extend(_extract_var_data(values())) + var_datas.extend(_extract_var_data(values())) # pyright: ignore [reportArgumentType] return var_datas -# These names were changed in reflex 0.3.0 -REPLACED_NAMES = { - "full_name": "_var_full_name", - "name": "_js_expr", - "state": "_var_data.state", - "type_": "_var_type", - "is_local": "_var_is_local", - "is_string": "_var_is_string", - "set_state": "_var_set_state", - "deps": "_deps", -} - - dispatchers: Dict[GenericType, Callable[[Var], Var]] = {} @@ -2717,7 +3059,7 @@ def generic_type_to_actual_type_map( # call recursively for nested generic types and merge the results return { k: v - for generic_arg, actual_arg in zip(generic_args, actual_args) + for generic_arg, actual_arg in zip(generic_args, actual_args, strict=True) for k, v in generic_type_to_actual_type_map(generic_arg, actual_arg).items() } @@ -2874,13 +3216,22 @@ def dispatch( V = TypeVar("V") -BASE_TYPE = TypeVar("BASE_TYPE", bound=Base) +BASE_TYPE = TypeVar("BASE_TYPE", bound=Base | None) +SQLA_TYPE = TypeVar("SQLA_TYPE", bound=DeclarativeBase | None) + +if TYPE_CHECKING: + from _typeshed import DataclassInstance + + DATACLASS_TYPE = TypeVar("DATACLASS_TYPE", bound=DataclassInstance | None) + +FIELD_TYPE = TypeVar("FIELD_TYPE") +MAPPING_TYPE = TypeVar("MAPPING_TYPE", bound=Mapping | None) -class Field(Generic[T]): +class Field(Generic[FIELD_TYPE]): """Shadow class for Var to allow for type hinting in the IDE.""" - def __set__(self, instance, value: T): + def __set__(self, instance: Any, value: FIELD_TYPE): """Set the Var. Args: @@ -2889,41 +3240,55 @@ class Field(Generic[T]): """ @overload - def __get__(self: Field[bool], instance: None, owner) -> BooleanVar: ... + def __get__(self: Field[bool], instance: None, owner: Any) -> BooleanVar: ... @overload - def __get__(self: Field[int], instance: None, owner) -> NumberVar: ... + def __get__( + self: Field[int] | Field[float] | Field[int | float], instance: None, owner: Any + ) -> NumberVar: ... @overload - def __get__(self: Field[str], instance: None, owner) -> StringVar: ... + def __get__(self: Field[str], instance: None, owner: Any) -> StringVar: ... @overload - def __get__(self: Field[None], instance: None, owner) -> NoneVar: ... + def __get__(self: Field[None], instance: None, owner: Any) -> NoneVar: ... @overload def __get__( self: Field[List[V]] | Field[Set[V]] | Field[Tuple[V, ...]], instance: None, - owner, + owner: Any, ) -> ArrayVar[List[V]]: ... @overload def __get__( - self: Field[Dict[str, V]], instance: None, owner - ) -> ObjectVar[Dict[str, V]]: ... + self: Field[MAPPING_TYPE], instance: None, owner: Any + ) -> ObjectVar[MAPPING_TYPE]: ... @overload def __get__( - self: Field[BASE_TYPE], instance: None, owner + self: Field[BASE_TYPE], instance: None, owner: Any ) -> ObjectVar[BASE_TYPE]: ... @overload - def __get__(self, instance: None, owner) -> Var[T]: ... + def __get__( + self: Field[SQLA_TYPE], instance: None, owner: Any + ) -> ObjectVar[SQLA_TYPE]: ... + + if TYPE_CHECKING: + + @overload + def __get__( + self: Field[DATACLASS_TYPE], instance: None, owner: Any + ) -> ObjectVar[DATACLASS_TYPE]: ... @overload - def __get__(self, instance, owner) -> T: ... + def __get__(self, instance: None, owner: Any) -> Var[FIELD_TYPE]: ... - def __get__(self, instance, owner): # type: ignore + @overload + def __get__(self, instance: Any, owner: Any) -> FIELD_TYPE: ... + + def __get__(self, instance: Any, owner: Any): # pyright: ignore [reportInconsistentOverload] """Get the Var. Args: @@ -2932,7 +3297,7 @@ class Field(Generic[T]): """ -def field(value: T) -> Field[T]: +def field(value: FIELD_TYPE) -> Field[FIELD_TYPE]: """Create a Field with a value. Args: @@ -2941,4 +3306,4 @@ def field(value: T) -> Field[T]: Returns: The Field. """ - return value # type: ignore + return value # pyright: ignore [reportReturnType] diff --git a/reflex/vars/datetime.py b/reflex/vars/datetime.py new file mode 100644 index 000000000..a18df78d0 --- /dev/null +++ b/reflex/vars/datetime.py @@ -0,0 +1,221 @@ +"""Immutable datetime and date vars.""" + +from __future__ import annotations + +import dataclasses +from datetime import date, datetime +from typing import Any, NoReturn, TypeVar, Union, overload + +from reflex.utils.exceptions import VarTypeError +from reflex.vars.number import BooleanVar + +from .base import ( + CustomVarOperationReturn, + LiteralVar, + Var, + VarData, + var_operation, + var_operation_return, +) + +DATETIME_T = TypeVar("DATETIME_T", datetime, date) + +datetime_types = Union[datetime, date] + + +def raise_var_type_error(): + """Raise a VarTypeError. + + Raises: + VarTypeError: Cannot compare a datetime object with a non-datetime object. + """ + raise VarTypeError("Cannot compare a datetime object with a non-datetime object.") + + +class DateTimeVar(Var[DATETIME_T], python_types=(datetime, date)): + """A variable that holds a datetime or date object.""" + + @overload + def __lt__(self, other: datetime_types) -> BooleanVar: ... + + @overload + def __lt__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] + + def __lt__(self, other: Any): + """Less than comparison. + + Args: + other: The other datetime to compare. + + Returns: + The result of the comparison. + """ + if not isinstance(other, DATETIME_TYPES): + raise_var_type_error() + return date_lt_operation(self, other) + + @overload + def __le__(self, other: datetime_types) -> BooleanVar: ... + + @overload + def __le__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] + + def __le__(self, other: Any): + """Less than or equal comparison. + + Args: + other: The other datetime to compare. + + Returns: + The result of the comparison. + """ + if not isinstance(other, DATETIME_TYPES): + raise_var_type_error() + return date_le_operation(self, other) + + @overload + def __gt__(self, other: datetime_types) -> BooleanVar: ... + + @overload + def __gt__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] + + def __gt__(self, other: Any): + """Greater than comparison. + + Args: + other: The other datetime to compare. + + Returns: + The result of the comparison. + """ + if not isinstance(other, DATETIME_TYPES): + raise_var_type_error() + return date_gt_operation(self, other) + + @overload + def __ge__(self, other: datetime_types) -> BooleanVar: ... + + @overload + def __ge__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] + + def __ge__(self, other: Any): + """Greater than or equal comparison. + + Args: + other: The other datetime to compare. + + Returns: + The result of the comparison. + """ + if not isinstance(other, DATETIME_TYPES): + raise_var_type_error() + return date_ge_operation(self, other) + + +@var_operation +def date_gt_operation(lhs: Var | Any, rhs: Var | Any) -> CustomVarOperationReturn: + """Greater than comparison. + + Args: + lhs: The left-hand side of the operation. + rhs: The right-hand side of the operation. + + Returns: + The result of the operation. + """ + return date_compare_operation(rhs, lhs, strict=True) + + +@var_operation +def date_lt_operation(lhs: Var | Any, rhs: Var | Any) -> CustomVarOperationReturn: + """Less than comparison. + + Args: + lhs: The left-hand side of the operation. + rhs: The right-hand side of the operation. + + Returns: + The result of the operation. + """ + return date_compare_operation(lhs, rhs, strict=True) + + +@var_operation +def date_le_operation(lhs: Var | Any, rhs: Var | Any) -> CustomVarOperationReturn: + """Less than or equal comparison. + + Args: + lhs: The left-hand side of the operation. + rhs: The right-hand side of the operation. + + Returns: + The result of the operation. + """ + return date_compare_operation(lhs, rhs) + + +@var_operation +def date_ge_operation(lhs: Var | Any, rhs: Var | Any) -> CustomVarOperationReturn: + """Greater than or equal comparison. + + Args: + lhs: The left-hand side of the operation. + rhs: The right-hand side of the operation. + + Returns: + The result of the operation. + """ + return date_compare_operation(rhs, lhs) + + +def date_compare_operation( + lhs: DateTimeVar[DATETIME_T] | Any, + rhs: DateTimeVar[DATETIME_T] | Any, + strict: bool = False, +) -> CustomVarOperationReturn: + """Check if the value is less than the other value. + + Args: + lhs: The left-hand side of the operation. + rhs: The right-hand side of the operation. + strict: Whether to use strict comparison. + + Returns: + The result of the operation. + """ + return var_operation_return( + f"({lhs} {'<' if strict else '<='} {rhs})", + bool, + ) + + +@dataclasses.dataclass( + eq=False, + frozen=True, + slots=True, +) +class LiteralDatetimeVar(LiteralVar, DateTimeVar): + """Base class for immutable datetime and date vars.""" + + _var_value: datetime | date = dataclasses.field(default=datetime.now()) + + @classmethod + def create(cls, value: datetime | date, _var_data: VarData | None = None): + """Create a new instance of the class. + + Args: + value: The value to set. + + Returns: + LiteralDatetimeVar: The new instance of the class. + """ + js_expr = f'"{value!s}"' + return cls( + _js_expr=js_expr, + _var_type=type(value), + _var_value=value, + _var_data=_var_data, + ) + + +DATETIME_TYPES = (datetime, date, DateTimeVar) diff --git a/reflex/vars/dep_tracking.py b/reflex/vars/dep_tracking.py new file mode 100644 index 000000000..0b2367799 --- /dev/null +++ b/reflex/vars/dep_tracking.py @@ -0,0 +1,344 @@ +"""Collection of base classes.""" + +from __future__ import annotations + +import contextlib +import dataclasses +import dis +import enum +import inspect +from types import CellType, CodeType, FunctionType +from typing import TYPE_CHECKING, Any, ClassVar, Type, cast + +from reflex.utils.exceptions import VarValueError + +if TYPE_CHECKING: + from reflex.state import BaseState + + from .base import Var + + +CellEmpty = object() + + +def get_cell_value(cell: CellType) -> Any: + """Get the value of a cell object. + + Args: + cell: The cell object to get the value from. (func.__closure__ objects) + + Returns: + The value from the cell or CellEmpty if a ValueError is raised. + """ + try: + return cell.cell_contents + except ValueError: + return CellEmpty + + +class ScanStatus(enum.Enum): + """State of the dis instruction scanning loop.""" + + SCANNING = enum.auto() + GETTING_ATTR = enum.auto() + GETTING_STATE = enum.auto() + GETTING_VAR = enum.auto() + + +@dataclasses.dataclass +class DependencyTracker: + """State machine for identifying state attributes that are accessed by a function.""" + + func: FunctionType | CodeType = dataclasses.field() + state_cls: Type[BaseState] = dataclasses.field() + + dependencies: dict[str, set[str]] = dataclasses.field(default_factory=dict) + + scan_status: ScanStatus = dataclasses.field(default=ScanStatus.SCANNING) + top_of_stack: str | None = dataclasses.field(default=None) + + tracked_locals: dict[str, Type[BaseState]] = dataclasses.field(default_factory=dict) + + _getting_state_class: Type[BaseState] | None = dataclasses.field(default=None) + _getting_var_instructions: list[dis.Instruction] = dataclasses.field( + default_factory=list + ) + + INVALID_NAMES: ClassVar[list[str]] = ["parent_state", "substates", "get_substate"] + + def __post_init__(self): + """After initializing, populate the dependencies dict.""" + with contextlib.suppress(AttributeError): + # unbox functools.partial + self.func = cast(FunctionType, self.func.func) # pyright: ignore[reportAttributeAccessIssue] + with contextlib.suppress(AttributeError): + # unbox EventHandler + self.func = cast(FunctionType, self.func.fn) # pyright: ignore[reportAttributeAccessIssue] + + if isinstance(self.func, FunctionType): + with contextlib.suppress(AttributeError, IndexError): + # the first argument to the function is the name of "self" arg + self.tracked_locals[self.func.__code__.co_varnames[0]] = self.state_cls + + self._populate_dependencies() + + def _merge_deps(self, tracker: DependencyTracker) -> None: + """Merge dependencies from another DependencyTracker. + + Args: + tracker: The DependencyTracker to merge dependencies from. + """ + for state_name, dep_name in tracker.dependencies.items(): + self.dependencies.setdefault(state_name, set()).update(dep_name) + + def load_attr_or_method(self, instruction: dis.Instruction) -> None: + """Handle loading an attribute or method from the object on top of the stack. + + This method directly tracks attributes and recursively merges + dependencies from analyzing the dependencies of any methods called. + + Args: + instruction: The dis instruction to process. + + Raises: + VarValueError: if the attribute is an disallowed name. + """ + from .base import ComputedVar + + if instruction.argval in self.INVALID_NAMES: + raise VarValueError( + f"Cached var {self!s} cannot access arbitrary state via `{instruction.argval}`." + ) + if instruction.argval == "get_state": + # Special case: arbitrary state access requested. + self.scan_status = ScanStatus.GETTING_STATE + return + if instruction.argval == "get_var_value": + # Special case: arbitrary var access requested. + self.scan_status = ScanStatus.GETTING_VAR + return + + # Reset status back to SCANNING after attribute is accessed. + self.scan_status = ScanStatus.SCANNING + if not self.top_of_stack: + return + target_state = self.tracked_locals[self.top_of_stack] + try: + ref_obj = getattr(target_state, instruction.argval) + except AttributeError: + # Not found on this state class, maybe it is a dynamic attribute that will be picked up later. + ref_obj = None + + if isinstance(ref_obj, property) and not isinstance(ref_obj, ComputedVar): + # recurse into property fget functions + ref_obj = ref_obj.fget + if callable(ref_obj): + # recurse into callable attributes + self._merge_deps( + type(self)(func=cast(FunctionType, ref_obj), state_cls=target_state) + ) + elif ( + instruction.argval in target_state.backend_vars + or instruction.argval in target_state.vars + ): + # var access + self.dependencies.setdefault(target_state.get_full_name(), set()).add( + instruction.argval + ) + + def _get_globals(self) -> dict[str, Any]: + """Get the globals of the function. + + Returns: + The var names and values in the globals of the function. + """ + if isinstance(self.func, CodeType): + return {} + return self.func.__globals__ # pyright: ignore[reportAttributeAccessIssue] + + def _get_closure(self) -> dict[str, Any]: + """Get the closure of the function, with unbound values omitted. + + Returns: + The var names and values in the closure of the function. + """ + if isinstance(self.func, CodeType): + return {} + return { + var_name: get_cell_value(cell) + for var_name, cell in zip( + self.func.__code__.co_freevars, # pyright: ignore[reportAttributeAccessIssue] + self.func.__closure__ or (), + strict=False, + ) + if get_cell_value(cell) is not CellEmpty + } + + def handle_getting_state(self, instruction: dis.Instruction) -> None: + """Handle bytecode analysis when `get_state` was called in the function. + + If the wrapped function is getting an arbitrary state and saving it to a + local variable, this method associates the local variable name with the + state class in self.tracked_locals. + + When an attribute/method is accessed on a tracked local, it will be + associated with this state. + + Args: + instruction: The dis instruction to process. + + Raises: + VarValueError: if the state class cannot be determined from the instruction. + """ + from reflex.state import BaseState + + if instruction.opname == "LOAD_FAST": + raise VarValueError( + f"Dependency detection cannot identify get_state class from local var {instruction.argval}." + ) + if isinstance(self.func, CodeType): + raise VarValueError( + "Dependency detection cannot identify get_state class from a code object." + ) + if instruction.opname == "LOAD_GLOBAL": + # Special case: referencing state class from global scope. + try: + self._getting_state_class = self._get_globals()[instruction.argval] + except (ValueError, KeyError) as ve: + raise VarValueError( + f"Cached var {self!s} cannot access arbitrary state `{instruction.argval}`, not found in globals." + ) from ve + elif instruction.opname == "LOAD_DEREF": + # Special case: referencing state class from closure. + try: + self._getting_state_class = self._get_closure()[instruction.argval] + except (ValueError, KeyError) as ve: + raise VarValueError( + f"Cached var {self!s} cannot access arbitrary state `{instruction.argval}`, is it defined yet?" + ) from ve + elif instruction.opname == "STORE_FAST": + # Storing the result of get_state in a local variable. + if not isinstance(self._getting_state_class, type) or not issubclass( + self._getting_state_class, BaseState + ): + raise VarValueError( + f"Cached var {self!s} cannot determine dependencies in fetched state `{instruction.argval}`." + ) + self.tracked_locals[instruction.argval] = self._getting_state_class + self.scan_status = ScanStatus.SCANNING + self._getting_state_class = None + + def _eval_var(self) -> Var: + """Evaluate instructions from the wrapped function to get the Var object. + + Returns: + The Var object. + + Raises: + VarValueError: if the source code for the var cannot be determined. + """ + # Get the original source code and eval it to get the Var. + module = inspect.getmodule(self.func) + positions0 = self._getting_var_instructions[0].positions + positions1 = self._getting_var_instructions[-1].positions + if module is None or positions0 is None or positions1 is None: + raise VarValueError( + f"Cannot determine the source code for the var in {self.func!r}." + ) + start_line = positions0.lineno + start_column = positions0.col_offset + end_line = positions1.end_lineno + end_column = positions1.end_col_offset + if ( + start_line is None + or start_column is None + or end_line is None + or end_column is None + ): + raise VarValueError( + f"Cannot determine the source code for the var in {self.func!r}." + ) + source = inspect.getsource(module).splitlines(True)[start_line - 1 : end_line] + # Create a python source string snippet. + if len(source) > 1: + snipped_source = "".join( + [ + *source[0][start_column:], + *(source[1:-2] if len(source) > 2 else []), + *source[-1][: end_column - 1], + ] + ) + else: + snipped_source = source[0][start_column : end_column - 1] + # Evaluate the string in the context of the function's globals and closure. + return eval(f"({snipped_source})", self._get_globals(), self._get_closure()) + + def handle_getting_var(self, instruction: dis.Instruction) -> None: + """Handle bytecode analysis when `get_var_value` was called in the function. + + This only really works if the expression passed to `get_var_value` is + evaluable in the function's global scope or closure, so getting the var + value from a var saved in a local variable or in the class instance is + not possible. + + Args: + instruction: The dis instruction to process. + + Raises: + VarValueError: if the source code for the var cannot be determined. + """ + if instruction.opname == "CALL" and self._getting_var_instructions: + if self._getting_var_instructions: + the_var = self._eval_var() + the_var_data = the_var._get_all_var_data() + if the_var_data is None: + raise VarValueError( + f"Cannot determine the source code for the var in {self.func!r}." + ) + self.dependencies.setdefault(the_var_data.state, set()).add( + the_var_data.field_name + ) + self._getting_var_instructions.clear() + self.scan_status = ScanStatus.SCANNING + else: + self._getting_var_instructions.append(instruction) + + def _populate_dependencies(self) -> None: + """Update self.dependencies based on the disassembly of self.func. + + Save references to attributes accessed on "self" or other fetched states. + + Recursively called when the function makes a method call on "self" or + define comprehensions or nested functions that may reference "self". + """ + for instruction in dis.get_instructions(self.func): + if self.scan_status == ScanStatus.GETTING_STATE: + self.handle_getting_state(instruction) + elif self.scan_status == ScanStatus.GETTING_VAR: + self.handle_getting_var(instruction) + elif ( + instruction.opname in ("LOAD_FAST", "LOAD_DEREF") + and instruction.argval in self.tracked_locals + ): + # bytecode loaded the class instance to the top of stack, next load instruction + # is referencing an attribute on self + self.top_of_stack = instruction.argval + self.scan_status = ScanStatus.GETTING_ATTR + elif self.scan_status == ScanStatus.GETTING_ATTR and instruction.opname in ( + "LOAD_ATTR", + "LOAD_METHOD", + ): + self.load_attr_or_method(instruction) + self.top_of_stack = None + elif instruction.opname == "LOAD_CONST" and isinstance( + instruction.argval, CodeType + ): + # recurse into nested functions / comprehensions, which can reference + # instance attributes from the outer scope + self._merge_deps( + type(self)( + func=instruction.argval, + state_cls=self.state_cls, + tracked_locals=self.tracked_locals, + ) + ) diff --git a/reflex/vars/function.py b/reflex/vars/function.py index 9879fdb5d..505a69b4c 100644 --- a/reflex/vars/function.py +++ b/reflex/vars/function.py @@ -100,7 +100,7 @@ class FunctionVar(Var[CALLABLE_TYPE], default_type=ReflexCallable[Any, Any]): @overload def partial(self, *args: Var | Any) -> FunctionVar: ... - def partial(self, *args: Var | Any) -> FunctionVar: # type: ignore + def partial(self, *args: Var | Any) -> FunctionVar: # pyright: ignore [reportInconsistentOverload] """Partially apply the function with the given arguments. Args: @@ -174,7 +174,7 @@ class FunctionVar(Var[CALLABLE_TYPE], default_type=ReflexCallable[Any, Any]): @overload def call(self, *args: Var | Any) -> Var: ... - def call(self, *args: Var | Any) -> Var: # type: ignore + def call(self, *args: Var | Any) -> Var: # pyright: ignore [reportInconsistentOverload] """Call the function with the given arguments. Args: @@ -210,6 +210,7 @@ class FunctionStringVar(FunctionVar[CALLABLE_TYPE]): Args: func: The function to call. + _var_type: The type of the Var. _var_data: Additional hooks and imports associated with the Var. Returns: @@ -225,7 +226,7 @@ class FunctionStringVar(FunctionVar[CALLABLE_TYPE]): @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class VarOperationCall(Generic[P, R], CachedVarOperation, Var[R]): """Base class for immutable vars that are the result of a function call.""" @@ -268,6 +269,7 @@ class VarOperationCall(Generic[P, R], CachedVarOperation, Var[R]): Args: func: The function to call. *args: The arguments to call the function with. + _var_type: The type of the Var. _var_data: Additional hooks and imports associated with the Var. Returns: @@ -292,7 +294,7 @@ class VarOperationCall(Generic[P, R], CachedVarOperation, Var[R]): class DestructuredArg: """Class for destructured arguments.""" - fields: Tuple[str, ...] = tuple() + fields: Tuple[str, ...] = () rest: Optional[str] = None def to_javascript(self) -> str: @@ -314,7 +316,7 @@ class DestructuredArg: class FunctionArgs: """Class for function arguments.""" - args: Tuple[Union[str, DestructuredArg], ...] = tuple() + args: Tuple[Union[str, DestructuredArg], ...] = () rest: Optional[str] = None @@ -348,7 +350,7 @@ def format_args_function_operation( @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class ArgsFunctionOperation(CachedVarOperation, FunctionVar): """Base class for immutable function defined via arguments and return expression.""" @@ -385,11 +387,13 @@ class ArgsFunctionOperation(CachedVarOperation, FunctionVar): return_expr: The return expression of the function. rest: The name of the rest argument. explicit_return: Whether to use explicit return syntax. + _var_type: The type of the Var. _var_data: Additional hooks and imports associated with the Var. Returns: The function var. """ + return_expr = Var.create(return_expr) return cls( _js_expr="", _var_type=_var_type, @@ -403,7 +407,7 @@ class ArgsFunctionOperation(CachedVarOperation, FunctionVar): @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class ArgsFunctionOperationBuilder(CachedVarOperation, BuilderFunctionVar): """Base class for immutable function defined via arguments and return expression with the builder pattern.""" @@ -440,11 +444,13 @@ class ArgsFunctionOperationBuilder(CachedVarOperation, BuilderFunctionVar): return_expr: The return expression of the function. rest: The name of the rest argument. explicit_return: Whether to use explicit return syntax. + _var_type: The type of the Var. _var_data: Additional hooks and imports associated with the Var. Returns: The function var. """ + return_expr = Var.create(return_expr) return cls( _js_expr="", _var_type=_var_type, diff --git a/reflex/vars/number.py b/reflex/vars/number.py index a762796e2..35a55490a 100644 --- a/reflex/vars/number.py +++ b/reflex/vars/number.py @@ -5,7 +5,6 @@ from __future__ import annotations import dataclasses import json import math -import sys from typing import ( TYPE_CHECKING, Any, @@ -18,9 +17,8 @@ from typing import ( ) from reflex.constants.base import Dirs -from reflex.utils.exceptions import PrimitiveUnserializableToJSON, VarTypeError +from reflex.utils.exceptions import PrimitiveUnserializableToJSONError, VarTypeError from reflex.utils.imports import ImportDict, ImportVar -from reflex.utils.types import is_optional from .base import ( CustomVarOperationReturn, @@ -32,7 +30,7 @@ from .base import ( var_operation_return, ) -NUMBER_T = TypeVar("NUMBER_T", int, float, Union[int, float], bool) +NUMBER_T = TypeVar("NUMBER_T", int, float, bool) if TYPE_CHECKING: from .sequence import ArrayVar @@ -51,7 +49,7 @@ def raise_unsupported_operand_types( VarTypeError: The operand types are unsupported. """ raise VarTypeError( - f"Unsupported Operand type(s) for {operator}: {', '.join(map(lambda t: t.__name__, operands_types))}" + f"Unsupported Operand type(s) for {operator}: {', '.join(t.__name__ for t in operands_types)}" ) @@ -62,7 +60,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): def __add__(self, other: number_types) -> NumberVar: ... @overload - def __add__(self, other: NoReturn) -> NoReturn: ... + def __add__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __add__(self, other: Any): """Add two numbers. @@ -81,7 +79,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): def __radd__(self, other: number_types) -> NumberVar: ... @overload - def __radd__(self, other: NoReturn) -> NoReturn: ... + def __radd__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __radd__(self, other: Any): """Add two numbers. @@ -100,7 +98,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): def __sub__(self, other: number_types) -> NumberVar: ... @overload - def __sub__(self, other: NoReturn) -> NoReturn: ... + def __sub__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __sub__(self, other: Any): """Subtract two numbers. @@ -120,7 +118,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): def __rsub__(self, other: number_types) -> NumberVar: ... @overload - def __rsub__(self, other: NoReturn) -> NoReturn: ... + def __rsub__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __rsub__(self, other: Any): """Subtract two numbers. @@ -161,7 +159,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): """ from .sequence import ArrayVar, LiteralArrayVar - if isinstance(other, (list, tuple, set, ArrayVar)): + if isinstance(other, (list, tuple, ArrayVar)): if isinstance(other, ArrayVar): return other * self return LiteralArrayVar.create(other) * self @@ -188,7 +186,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): """ from .sequence import ArrayVar, LiteralArrayVar - if isinstance(other, (list, tuple, set, ArrayVar)): + if isinstance(other, (list, tuple, ArrayVar)): if isinstance(other, ArrayVar): return other * self return LiteralArrayVar.create(other) * self @@ -202,7 +200,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): def __truediv__(self, other: number_types) -> NumberVar: ... @overload - def __truediv__(self, other: NoReturn) -> NoReturn: ... + def __truediv__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __truediv__(self, other: Any): """Divide two numbers. @@ -222,7 +220,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): def __rtruediv__(self, other: number_types) -> NumberVar: ... @overload - def __rtruediv__(self, other: NoReturn) -> NoReturn: ... + def __rtruediv__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __rtruediv__(self, other: Any): """Divide two numbers. @@ -242,7 +240,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): def __floordiv__(self, other: number_types) -> NumberVar: ... @overload - def __floordiv__(self, other: NoReturn) -> NoReturn: ... + def __floordiv__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __floordiv__(self, other: Any): """Floor divide two numbers. @@ -262,7 +260,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): def __rfloordiv__(self, other: number_types) -> NumberVar: ... @overload - def __rfloordiv__(self, other: NoReturn) -> NoReturn: ... + def __rfloordiv__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __rfloordiv__(self, other: Any): """Floor divide two numbers. @@ -282,7 +280,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): def __mod__(self, other: number_types) -> NumberVar: ... @overload - def __mod__(self, other: NoReturn) -> NoReturn: ... + def __mod__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __mod__(self, other: Any): """Modulo two numbers. @@ -302,7 +300,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): def __rmod__(self, other: number_types) -> NumberVar: ... @overload - def __rmod__(self, other: NoReturn) -> NoReturn: ... + def __rmod__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __rmod__(self, other: Any): """Modulo two numbers. @@ -322,7 +320,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): def __pow__(self, other: number_types) -> NumberVar: ... @overload - def __pow__(self, other: NoReturn) -> NoReturn: ... + def __pow__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __pow__(self, other: Any): """Exponentiate two numbers. @@ -342,7 +340,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): def __rpow__(self, other: number_types) -> NumberVar: ... @overload - def __rpow__(self, other: NoReturn) -> NoReturn: ... + def __rpow__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __rpow__(self, other: Any): """Exponentiate two numbers. @@ -418,7 +416,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): def __lt__(self, other: number_types) -> BooleanVar: ... @overload - def __lt__(self, other: NoReturn) -> NoReturn: ... + def __lt__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __lt__(self, other: Any): """Less than comparison. @@ -431,13 +429,13 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): """ if not isinstance(other, NUMBER_TYPES): raise_unsupported_operand_types("<", (type(self), type(other))) - return less_than_operation(self, +other) + return less_than_operation(+self, +other) @overload def __le__(self, other: number_types) -> BooleanVar: ... @overload - def __le__(self, other: NoReturn) -> NoReturn: ... + def __le__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __le__(self, other: Any): """Less than or equal comparison. @@ -450,7 +448,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): """ if not isinstance(other, NUMBER_TYPES): raise_unsupported_operand_types("<=", (type(self), type(other))) - return less_than_or_equal_operation(self, +other) + return less_than_or_equal_operation(+self, +other) def __eq__(self, other: Any): """Equal comparison. @@ -462,7 +460,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): The result of the comparison. """ if isinstance(other, NUMBER_TYPES): - return equal_operation(self, +other) + return equal_operation(+self, +other) return equal_operation(self, other) def __ne__(self, other: Any): @@ -475,14 +473,14 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): The result of the comparison. """ if isinstance(other, NUMBER_TYPES): - return not_equal_operation(self, +other) + return not_equal_operation(+self, +other) return not_equal_operation(self, other) @overload def __gt__(self, other: number_types) -> BooleanVar: ... @overload - def __gt__(self, other: NoReturn) -> NoReturn: ... + def __gt__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __gt__(self, other: Any): """Greater than comparison. @@ -495,13 +493,13 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): """ if not isinstance(other, NUMBER_TYPES): raise_unsupported_operand_types(">", (type(self), type(other))) - return greater_than_operation(self, +other) + return greater_than_operation(+self, +other) @overload def __ge__(self, other: number_types) -> BooleanVar: ... @overload - def __ge__(self, other: NoReturn) -> NoReturn: ... + def __ge__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __ge__(self, other: Any): """Greater than or equal comparison. @@ -514,17 +512,7 @@ class NumberVar(Var[NUMBER_T], python_types=(int, float)): """ if not isinstance(other, NUMBER_TYPES): raise_unsupported_operand_types(">=", (type(self), type(other))) - return greater_than_or_equal_operation(self, +other) - - def bool(self): - """Boolean conversion. - - Returns: - The boolean value of the number. - """ - if is_optional(self._var_type): - return boolify((self != None) & (self != 0)) # noqa: E711 - return self != 0 + return greater_than_or_equal_operation(+self, +other) def _is_strict_float(self) -> bool: """Check if the number is a float. @@ -572,7 +560,7 @@ def binary_number_operation( Returns: The binary number operation. """ - return operation(lhs, rhs) # type: ignore + return operation(lhs, rhs) # pyright: ignore [reportReturnType, reportArgumentType] return wrapper @@ -984,7 +972,7 @@ def boolean_not_operation(value: BooleanVar): @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class LiteralNumberVar(LiteralVar, NumberVar): """Base class for immutable literal number vars.""" @@ -998,10 +986,10 @@ class LiteralNumberVar(LiteralVar, NumberVar): The JSON representation of the var. Raises: - PrimitiveUnserializableToJSON: If the var is unserializable to JSON. + PrimitiveUnserializableToJSONError: If the var is unserializable to JSON. """ if math.isinf(self._var_value) or math.isnan(self._var_value): - raise PrimitiveUnserializableToJSON( + raise PrimitiveUnserializableToJSONError( f"No valid JSON representation for {self}" ) return json.dumps(self._var_value) @@ -1012,7 +1000,7 @@ class LiteralNumberVar(LiteralVar, NumberVar): Returns: int: The hash value of the object. """ - return hash((self.__class__.__name__, self._var_value)) + return hash((type(self).__name__, self._var_value)) @classmethod def create(cls, value: float | int, _var_data: VarData | None = None): @@ -1043,7 +1031,7 @@ class LiteralNumberVar(LiteralVar, NumberVar): @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class LiteralBooleanVar(LiteralVar, BooleanVar): """Base class for immutable literal boolean vars.""" @@ -1064,7 +1052,7 @@ class LiteralBooleanVar(LiteralVar, BooleanVar): Returns: int: The hash value of the object. """ - return hash((self.__class__.__name__, self._var_value)) + return hash((type(self).__name__, self._var_value)) @classmethod def create(cls, value: bool, _var_data: VarData | None = None): diff --git a/reflex/vars/object.py b/reflex/vars/object.py index 032fc8058..cb29cabfb 100644 --- a/reflex/vars/object.py +++ b/reflex/vars/object.py @@ -3,13 +3,12 @@ from __future__ import annotations import dataclasses -import sys import typing from inspect import isclass from typing import ( Any, - Dict, List, + Mapping, NoReturn, Tuple, Type, @@ -19,6 +18,8 @@ from typing import ( overload, ) +from typing_extensions import is_typeddict + from reflex.utils import types from reflex.utils.exceptions import VarAttributeError from reflex.utils.types import GenericType, get_attribute_access_type, get_origin @@ -36,7 +37,7 @@ from .base import ( from .number import BooleanVar, NumberVar, raise_unsupported_operand_types from .sequence import ArrayVar, StringVar -OBJECT_TYPE = TypeVar("OBJECT_TYPE") +OBJECT_TYPE = TypeVar("OBJECT_TYPE", covariant=True) KEY_TYPE = TypeVar("KEY_TYPE") VALUE_TYPE = TypeVar("VALUE_TYPE") @@ -46,7 +47,7 @@ ARRAY_INNER_TYPE = TypeVar("ARRAY_INNER_TYPE") OTHER_KEY_TYPE = TypeVar("OTHER_KEY_TYPE") -class ObjectVar(Var[OBJECT_TYPE], python_types=dict): +class ObjectVar(Var[OBJECT_TYPE], python_types=Mapping): """Base class for immutable object vars.""" def _key_type(self) -> Type: @@ -59,7 +60,7 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=dict): @overload def _value_type( - self: ObjectVar[Dict[Any, VALUE_TYPE]], + self: ObjectVar[Mapping[Any, VALUE_TYPE]], ) -> Type[VALUE_TYPE]: ... @overload @@ -73,9 +74,9 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=dict): """ fixed_type = get_origin(self._var_type) or self._var_type if not isclass(fixed_type): - return Any - args = get_args(self._var_type) if issubclass(fixed_type, dict) else () - return args[1] if args else Any + return Any # pyright: ignore [reportReturnType] + args = get_args(self._var_type) if issubclass(fixed_type, Mapping) else () + return args[1] if args else Any # pyright: ignore [reportReturnType] def keys(self) -> ArrayVar[List[str]]: """Get the keys of the object. @@ -87,7 +88,7 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=dict): @overload def values( - self: ObjectVar[Dict[Any, VALUE_TYPE]], + self: ObjectVar[Mapping[Any, VALUE_TYPE]], ) -> ArrayVar[List[VALUE_TYPE]]: ... @overload @@ -103,7 +104,7 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=dict): @overload def entries( - self: ObjectVar[Dict[Any, VALUE_TYPE]], + self: ObjectVar[Mapping[Any, VALUE_TYPE]], ) -> ArrayVar[List[Tuple[str, VALUE_TYPE]]]: ... @overload @@ -132,50 +133,50 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=dict): # NoReturn is used here to catch when key value is Any @overload - def __getitem__( - self: ObjectVar[Dict[Any, NoReturn]], + def __getitem__( # pyright: ignore [reportOverlappingOverload] + self: ObjectVar[Mapping[Any, NoReturn]], key: Var | Any, ) -> Var: ... + @overload + def __getitem__( + self: (ObjectVar[Mapping[Any, bool]]), + key: Var | Any, + ) -> BooleanVar: ... + @overload def __getitem__( self: ( - ObjectVar[Dict[Any, int]] - | ObjectVar[Dict[Any, float]] - | ObjectVar[Dict[Any, int | float]] + ObjectVar[Mapping[Any, int]] + | ObjectVar[Mapping[Any, float]] + | ObjectVar[Mapping[Any, int | float]] ), key: Var | Any, ) -> NumberVar: ... @overload def __getitem__( - self: ObjectVar[Dict[Any, str]], + self: ObjectVar[Mapping[Any, str]], key: Var | Any, ) -> StringVar: ... @overload def __getitem__( - self: ObjectVar[Dict[Any, list[ARRAY_INNER_TYPE]]], + self: ObjectVar[Mapping[Any, list[ARRAY_INNER_TYPE]]], key: Var | Any, ) -> ArrayVar[list[ARRAY_INNER_TYPE]]: ... @overload def __getitem__( - self: ObjectVar[Dict[Any, set[ARRAY_INNER_TYPE]]], - key: Var | Any, - ) -> ArrayVar[set[ARRAY_INNER_TYPE]]: ... - - @overload - def __getitem__( - self: ObjectVar[Dict[Any, tuple[ARRAY_INNER_TYPE, ...]]], + self: ObjectVar[Mapping[Any, tuple[ARRAY_INNER_TYPE, ...]]], key: Var | Any, ) -> ArrayVar[tuple[ARRAY_INNER_TYPE, ...]]: ... @overload def __getitem__( - self: ObjectVar[Dict[Any, dict[OTHER_KEY_TYPE, VALUE_TYPE]]], + self: ObjectVar[Mapping[Any, Mapping[OTHER_KEY_TYPE, VALUE_TYPE]]], key: Var | Any, - ) -> ObjectVar[dict[OTHER_KEY_TYPE, VALUE_TYPE]]: ... + ) -> ObjectVar[Mapping[OTHER_KEY_TYPE, VALUE_TYPE]]: ... def __getitem__(self, key: Var | Any) -> Var: """Get an item from the object. @@ -194,50 +195,44 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=dict): # NoReturn is used here to catch when key value is Any @overload - def __getattr__( - self: ObjectVar[Dict[Any, NoReturn]], + def __getattr__( # pyright: ignore [reportOverlappingOverload] + self: ObjectVar[Mapping[Any, NoReturn]], name: str, ) -> Var: ... @overload def __getattr__( self: ( - ObjectVar[Dict[Any, int]] - | ObjectVar[Dict[Any, float]] - | ObjectVar[Dict[Any, int | float]] + ObjectVar[Mapping[Any, int]] + | ObjectVar[Mapping[Any, float]] + | ObjectVar[Mapping[Any, int | float]] ), name: str, ) -> NumberVar: ... @overload def __getattr__( - self: ObjectVar[Dict[Any, str]], + self: ObjectVar[Mapping[Any, str]], name: str, ) -> StringVar: ... @overload def __getattr__( - self: ObjectVar[Dict[Any, list[ARRAY_INNER_TYPE]]], + self: ObjectVar[Mapping[Any, list[ARRAY_INNER_TYPE]]], name: str, ) -> ArrayVar[list[ARRAY_INNER_TYPE]]: ... @overload def __getattr__( - self: ObjectVar[Dict[Any, set[ARRAY_INNER_TYPE]]], - name: str, - ) -> ArrayVar[set[ARRAY_INNER_TYPE]]: ... - - @overload - def __getattr__( - self: ObjectVar[Dict[Any, tuple[ARRAY_INNER_TYPE, ...]]], + self: ObjectVar[Mapping[Any, tuple[ARRAY_INNER_TYPE, ...]]], name: str, ) -> ArrayVar[tuple[ARRAY_INNER_TYPE, ...]]: ... @overload def __getattr__( - self: ObjectVar[Dict[Any, dict[OTHER_KEY_TYPE, VALUE_TYPE]]], + self: ObjectVar[Mapping[Any, Mapping[OTHER_KEY_TYPE, VALUE_TYPE]]], name: str, - ) -> ObjectVar[dict[OTHER_KEY_TYPE, VALUE_TYPE]]: ... + ) -> ObjectVar[Mapping[OTHER_KEY_TYPE, VALUE_TYPE]]: ... @overload def __getattr__( @@ -245,7 +240,7 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=dict): name: str, ) -> ObjectItemOperation: ... - def __getattr__(self, name) -> Var: + def __getattr__(self, name: str) -> Var: """Get an attribute of the var. Args: @@ -266,8 +261,11 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=dict): var_type = get_args(var_type)[0] fixed_type = var_type if isclass(var_type) else get_origin(var_type) - if (isclass(fixed_type) and not issubclass(fixed_type, dict)) or ( - fixed_type in types.UnionTypes + + if ( + (isclass(fixed_type) and not issubclass(fixed_type, Mapping)) + or (fixed_type in types.UnionTypes) + or is_typeddict(fixed_type) ): attribute_type = get_attribute_access_type(var_type, name) if attribute_type is None: @@ -294,12 +292,12 @@ class ObjectVar(Var[OBJECT_TYPE], python_types=dict): @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class LiteralObjectVar(CachedVarOperation, ObjectVar[OBJECT_TYPE], LiteralVar): """Base class for immutable literal object vars.""" - _var_value: Dict[Union[Var, Any], Union[Var, Any]] = dataclasses.field( + _var_value: Mapping[Union[Var, Any], Union[Var, Any]] = dataclasses.field( default_factory=dict ) @@ -310,7 +308,7 @@ class LiteralObjectVar(CachedVarOperation, ObjectVar[OBJECT_TYPE], LiteralVar): The type of the keys of the object. """ args_list = typing.get_args(self._var_type) - return args_list[0] if args_list else Any + return args_list[0] if args_list else Any # pyright: ignore [reportReturnType] def _value_type(self) -> Type: """Get the type of the values of the object. @@ -319,7 +317,7 @@ class LiteralObjectVar(CachedVarOperation, ObjectVar[OBJECT_TYPE], LiteralVar): The type of the values of the object. """ args_list = typing.get_args(self._var_type) - return args_list[1] if args_list else Any + return args_list[1] if args_list else Any # pyright: ignore [reportReturnType] @cached_property_no_lock def _cached_var_name(self) -> str: @@ -344,17 +342,20 @@ class LiteralObjectVar(CachedVarOperation, ObjectVar[OBJECT_TYPE], LiteralVar): Returns: The JSON representation of the object. + + Raises: + TypeError: The keys and values of the object must be literal vars to get the JSON representation """ - return ( - "{" - + ", ".join( - [ - f"{LiteralVar.create(key).json()}:{LiteralVar.create(value).json()}" - for key, value in self._var_value.items() - ] - ) - + "}" - ) + keys_and_values = [] + for key, value in self._var_value.items(): + key = LiteralVar.create(key) + value = LiteralVar.create(value) + if not isinstance(key, LiteralVar) or not isinstance(value, LiteralVar): + raise TypeError( + "The keys and values of the object must be literal vars to get the JSON representation." + ) + keys_and_values.append(f"{key.json()}:{value.json()}") + return "{" + ", ".join(keys_and_values) + "}" def __hash__(self) -> int: """Get the hash of the var. @@ -362,7 +363,7 @@ class LiteralObjectVar(CachedVarOperation, ObjectVar[OBJECT_TYPE], LiteralVar): Returns: The hash of the var. """ - return hash((self.__class__.__name__, self._js_expr)) + return hash((type(self).__name__, self._js_expr)) @cached_property_no_lock def _cached_get_all_var_data(self) -> VarData | None: @@ -383,7 +384,7 @@ class LiteralObjectVar(CachedVarOperation, ObjectVar[OBJECT_TYPE], LiteralVar): @classmethod def create( cls, - _var_value: dict, + _var_value: Mapping, _var_type: Type[OBJECT_TYPE] | None = None, _var_data: VarData | None = None, ) -> LiteralObjectVar[OBJECT_TYPE]: @@ -466,7 +467,7 @@ def object_merge_operation(lhs: ObjectVar, rhs: ObjectVar): """ return var_operation_return( js_expression=f"({{...{lhs}, ...{rhs}}})", - var_type=Dict[ + var_type=Mapping[ Union[lhs._key_type(), rhs._key_type()], Union[lhs._value_type(), rhs._value_type()], ], @@ -476,7 +477,7 @@ def object_merge_operation(lhs: ObjectVar, rhs: ObjectVar): @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class ObjectItemOperation(CachedVarOperation, Var): """Operation to get an item from an object.""" diff --git a/reflex/vars/sequence.py b/reflex/vars/sequence.py index f5639685f..fb797b4ec 100644 --- a/reflex/vars/sequence.py +++ b/reflex/vars/sequence.py @@ -6,7 +6,6 @@ import dataclasses import inspect import json import re -import sys import typing from typing import ( TYPE_CHECKING, @@ -15,7 +14,7 @@ from typing import ( List, Literal, NoReturn, - Set, + Sequence, Tuple, Type, Union, @@ -54,8 +53,11 @@ from .number import ( ) if TYPE_CHECKING: + from .base import BASE_TYPE, DATACLASS_TYPE, SQLA_TYPE + from .function import FunctionVar from .object import ObjectVar + STRING_TYPE = TypeVar("STRING_TYPE", default=str) @@ -66,7 +68,7 @@ class StringVar(Var[STRING_TYPE], python_types=str): def __add__(self, other: StringVar | str) -> ConcatVarOperation: ... @overload - def __add__(self, other: NoReturn) -> NoReturn: ... + def __add__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __add__(self, other: Any) -> ConcatVarOperation: """Concatenate two strings. @@ -86,7 +88,7 @@ class StringVar(Var[STRING_TYPE], python_types=str): def __radd__(self, other: StringVar | str) -> ConcatVarOperation: ... @overload - def __radd__(self, other: NoReturn) -> NoReturn: ... + def __radd__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __radd__(self, other: Any) -> ConcatVarOperation: """Concatenate two strings. @@ -106,7 +108,7 @@ class StringVar(Var[STRING_TYPE], python_types=str): def __mul__(self, other: NumberVar | int) -> StringVar: ... @overload - def __mul__(self, other: NoReturn) -> NoReturn: ... + def __mul__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __mul__(self, other: Any) -> StringVar: """Multiply the sequence by a number or an integer. @@ -126,7 +128,7 @@ class StringVar(Var[STRING_TYPE], python_types=str): def __rmul__(self, other: NumberVar | int) -> StringVar: ... @overload - def __rmul__(self, other: NoReturn) -> NoReturn: ... + def __rmul__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __rmul__(self, other: Any) -> StringVar: """Multiply the sequence by a number or an integer. @@ -211,7 +213,7 @@ class StringVar(Var[STRING_TYPE], python_types=str): ) -> BooleanVar: ... @overload - def contains( + def contains( # pyright: ignore [reportOverlappingOverload] self, other: NoReturn, field: StringVar | str | None = None ) -> NoReturn: ... @@ -237,7 +239,7 @@ class StringVar(Var[STRING_TYPE], python_types=str): def split(self, separator: StringVar | str = "") -> ArrayVar[List[str]]: ... @overload - def split(self, separator: NoReturn) -> NoReturn: ... + def split(self, separator: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def split(self, separator: Any = "") -> ArrayVar[List[str]]: """Split the string. @@ -256,7 +258,7 @@ class StringVar(Var[STRING_TYPE], python_types=str): def startswith(self, prefix: StringVar | str) -> BooleanVar: ... @overload - def startswith(self, prefix: NoReturn) -> NoReturn: ... + def startswith(self, prefix: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def startswith(self, prefix: Any) -> BooleanVar: """Check if the string starts with a prefix. @@ -271,11 +273,30 @@ class StringVar(Var[STRING_TYPE], python_types=str): raise_unsupported_operand_types("startswith", (type(self), type(prefix))) return string_starts_with_operation(self, prefix) + @overload + def endswith(self, suffix: StringVar | str) -> BooleanVar: ... + + @overload + def endswith(self, suffix: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] + + def endswith(self, suffix: Any) -> BooleanVar: + """Check if the string ends with a suffix. + + Args: + suffix: The suffix. + + Returns: + The string ends with operation. + """ + if not isinstance(suffix, (StringVar, str)): + raise_unsupported_operand_types("endswith", (type(self), type(suffix))) + return string_ends_with_operation(self, suffix) + @overload def __lt__(self, other: StringVar | str) -> BooleanVar: ... @overload - def __lt__(self, other: NoReturn) -> NoReturn: ... + def __lt__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __lt__(self, other: Any): """Check if the string is less than another string. @@ -295,7 +316,7 @@ class StringVar(Var[STRING_TYPE], python_types=str): def __gt__(self, other: StringVar | str) -> BooleanVar: ... @overload - def __gt__(self, other: NoReturn) -> NoReturn: ... + def __gt__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __gt__(self, other: Any): """Check if the string is greater than another string. @@ -315,7 +336,7 @@ class StringVar(Var[STRING_TYPE], python_types=str): def __le__(self, other: StringVar | str) -> BooleanVar: ... @overload - def __le__(self, other: NoReturn) -> NoReturn: ... + def __le__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __le__(self, other: Any): """Check if the string is less than or equal to another string. @@ -335,7 +356,7 @@ class StringVar(Var[STRING_TYPE], python_types=str): def __ge__(self, other: StringVar | str) -> BooleanVar: ... @overload - def __ge__(self, other: NoReturn) -> NoReturn: ... + def __ge__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __ge__(self, other: Any): """Check if the string is greater than or equal to another string. @@ -501,6 +522,24 @@ def string_starts_with_operation( ) +@var_operation +def string_ends_with_operation( + full_string: StringVar[Any], suffix: StringVar[Any] | str +): + """Check if a string ends with a suffix. + + Args: + full_string: The full string. + suffix: The suffix. + + Returns: + Whether the string ends with the suffix. + """ + return var_operation_return( + js_expression=f"{full_string}.endsWith({suffix})", var_type=bool + ) + + @var_operation def string_item_operation(string: StringVar[Any], index: NumberVar | int): """Get an item from a string. @@ -559,7 +598,7 @@ _decode_var_pattern = re.compile(_decode_var_pattern_re, flags=re.DOTALL) @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class LiteralStringVar(LiteralVar, StringVar[str]): """Base class for immutable literal string vars.""" @@ -667,7 +706,7 @@ class LiteralStringVar(LiteralVar, StringVar[str]): Returns: The hash of the var. """ - return hash((self.__class__.__name__, self._var_value)) + return hash((type(self).__name__, self._var_value)) def json(self) -> str: """Get the JSON representation of the var. @@ -681,7 +720,7 @@ class LiteralStringVar(LiteralVar, StringVar[str]): @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class ConcatVarOperation(CachedVarOperation, StringVar[str]): """Representing a concatenation of literal string vars.""" @@ -743,7 +782,7 @@ class ConcatVarOperation(CachedVarOperation, StringVar[str]): """Create a var from a string value. Args: - value: The values to concatenate. + *value: The values to concatenate. _var_data: Additional hooks and imports associated with the Var. Returns: @@ -757,7 +796,8 @@ class ConcatVarOperation(CachedVarOperation, StringVar[str]): ) -ARRAY_VAR_TYPE = TypeVar("ARRAY_VAR_TYPE", bound=Union[List, Tuple, Set]) +ARRAY_VAR_TYPE = TypeVar("ARRAY_VAR_TYPE", bound=Sequence, covariant=True) +OTHER_ARRAY_VAR_TYPE = TypeVar("OTHER_ARRAY_VAR_TYPE", bound=Sequence) OTHER_TUPLE = TypeVar("OTHER_TUPLE") @@ -774,7 +814,7 @@ class ArrayVar(Var[ARRAY_VAR_TYPE], python_types=(list, tuple, set)): def join(self, sep: StringVar | str = "") -> StringVar: ... @overload - def join(self, sep: NoReturn) -> NoReturn: ... + def join(self, sep: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def join(self, sep: Any = "") -> StringVar: """Join the elements of the array. @@ -821,7 +861,7 @@ class ArrayVar(Var[ARRAY_VAR_TYPE], python_types=(list, tuple, set)): def __add__(self, other: ArrayVar[ARRAY_VAR_TYPE]) -> ArrayVar[ARRAY_VAR_TYPE]: ... @overload - def __add__(self, other: NoReturn) -> NoReturn: ... + def __add__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __add__(self, other: Any) -> ArrayVar[ARRAY_VAR_TYPE]: """Concatenate two arrays. @@ -850,6 +890,11 @@ class ArrayVar(Var[ARRAY_VAR_TYPE], python_types=(list, tuple, set)): i: Literal[0, -2], ) -> NumberVar: ... + @overload + def __getitem__( + self: ArrayVar[Tuple[Any, bool]], i: Literal[1, -1] + ) -> BooleanVar: ... + @overload def __getitem__( self: ( @@ -877,7 +922,7 @@ class ArrayVar(Var[ARRAY_VAR_TYPE], python_types=(list, tuple, set)): @overload def __getitem__( - self: ArrayVar[Tuple[Any, bool]], i: Literal[1, -1] + self: ARRAY_VAR_OF_LIST_ELEMENT[bool], i: int | NumberVar ) -> BooleanVar: ... @overload @@ -895,23 +940,12 @@ class ArrayVar(Var[ARRAY_VAR_TYPE], python_types=(list, tuple, set)): self: ARRAY_VAR_OF_LIST_ELEMENT[str], i: int | NumberVar ) -> StringVar: ... - @overload - def __getitem__( - self: ARRAY_VAR_OF_LIST_ELEMENT[bool], i: int | NumberVar - ) -> BooleanVar: ... - @overload def __getitem__( self: ARRAY_VAR_OF_LIST_ELEMENT[List[INNER_ARRAY_VAR]], i: int | NumberVar, ) -> ArrayVar[List[INNER_ARRAY_VAR]]: ... - @overload - def __getitem__( - self: ARRAY_VAR_OF_LIST_ELEMENT[Set[INNER_ARRAY_VAR]], - i: int | NumberVar, - ) -> ArrayVar[Set[INNER_ARRAY_VAR]]: ... - @overload def __getitem__( self: ARRAY_VAR_OF_LIST_ELEMENT[Tuple[KEY_TYPE, VALUE_TYPE]], @@ -930,6 +964,24 @@ class ArrayVar(Var[ARRAY_VAR_TYPE], python_types=(list, tuple, set)): i: int | NumberVar, ) -> ObjectVar[Dict[KEY_TYPE, VALUE_TYPE]]: ... + @overload + def __getitem__( + self: ARRAY_VAR_OF_LIST_ELEMENT[BASE_TYPE], + i: int | NumberVar, + ) -> ObjectVar[BASE_TYPE]: ... + + @overload + def __getitem__( + self: ARRAY_VAR_OF_LIST_ELEMENT[SQLA_TYPE], + i: int | NumberVar, + ) -> ObjectVar[SQLA_TYPE]: ... + + @overload + def __getitem__( + self: ARRAY_VAR_OF_LIST_ELEMENT[DATACLASS_TYPE], + i: int | NumberVar, + ) -> ObjectVar[DATACLASS_TYPE]: ... + @overload def __getitem__(self, i: int | NumberVar) -> Var: ... @@ -950,7 +1002,7 @@ class ArrayVar(Var[ARRAY_VAR_TYPE], python_types=(list, tuple, set)): raise_unsupported_operand_types("[]", (type(self), type(i))) return array_item_operation(self, i) - def length(self) -> NumberVar: + def length(self) -> NumberVar[int]: """Get the length of the array. Returns: @@ -1052,7 +1104,7 @@ class ArrayVar(Var[ARRAY_VAR_TYPE], python_types=(list, tuple, set)): def __mul__(self, other: NumberVar | int) -> ArrayVar[ARRAY_VAR_TYPE]: ... @overload - def __mul__(self, other: NoReturn) -> NoReturn: ... + def __mul__(self, other: NoReturn) -> NoReturn: ... # pyright: ignore [reportOverlappingOverload] def __mul__(self, other: Any) -> ArrayVar[ARRAY_VAR_TYPE]: """Multiply the sequence by a number or integer. @@ -1070,7 +1122,7 @@ class ArrayVar(Var[ARRAY_VAR_TYPE], python_types=(list, tuple, set)): return repeat_array_operation(self, other) - __rmul__ = __mul__ # type: ignore + __rmul__ = __mul__ @overload def __lt__(self, other: ArrayVar[ARRAY_VAR_TYPE]) -> BooleanVar: ... @@ -1177,7 +1229,7 @@ class ArrayVar(Var[ARRAY_VAR_TYPE], python_types=(list, tuple, set)): if num_args == 0: return_value = fn() - function_var = ArgsFunctionOperation.create(tuple(), return_value) + function_var = ArgsFunctionOperation.create((), return_value) else: # generic number var number_var = Var("").to(NumberVar, int) @@ -1202,26 +1254,18 @@ class ArrayVar(Var[ARRAY_VAR_TYPE], python_types=(list, tuple, set)): LIST_ELEMENT = TypeVar("LIST_ELEMENT") -ARRAY_VAR_OF_LIST_ELEMENT = Union[ - ArrayVar[List[LIST_ELEMENT]], - ArrayVar[Set[LIST_ELEMENT]], - ArrayVar[Tuple[LIST_ELEMENT, ...]], -] +ARRAY_VAR_OF_LIST_ELEMENT = ArrayVar[Sequence[LIST_ELEMENT]] @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class LiteralArrayVar(CachedVarOperation, LiteralVar, ArrayVar[ARRAY_VAR_TYPE]): """Base class for immutable literal array vars.""" - _var_value: Union[ - List[Union[Var, Any]], - Set[Union[Var, Any]], - Tuple[Union[Var, Any], ...], - ] = dataclasses.field(default_factory=list) + _var_value: Sequence[Union[Var, Any]] = dataclasses.field(default=()) @cached_property_no_lock def _cached_var_name(self) -> str: @@ -1266,32 +1310,39 @@ class LiteralArrayVar(CachedVarOperation, LiteralVar, ArrayVar[ARRAY_VAR_TYPE]): Returns: The JSON representation of the var. + + Raises: + TypeError: If the array elements are not of type LiteralVar. """ - return ( - "[" - + ", ".join( - [LiteralVar.create(element).json() for element in self._var_value] - ) - + "]" - ) + elements = [] + for element in self._var_value: + element_var = LiteralVar.create(element) + if not isinstance(element_var, LiteralVar): + raise TypeError( + f"Array elements must be of type LiteralVar, not {type(element_var)}" + ) + elements.append(element_var.json()) + + return "[" + ", ".join(elements) + "]" @classmethod def create( cls, - value: ARRAY_VAR_TYPE, - _var_type: Type[ARRAY_VAR_TYPE] | None = None, + value: OTHER_ARRAY_VAR_TYPE, + _var_type: Type[OTHER_ARRAY_VAR_TYPE] | None = None, _var_data: VarData | None = None, - ) -> LiteralArrayVar[ARRAY_VAR_TYPE]: + ) -> LiteralArrayVar[OTHER_ARRAY_VAR_TYPE]: """Create a var from a string value. Args: value: The value to create the var from. + _var_type: The type of the var. _var_data: Additional hooks and imports associated with the Var. Returns: The var. """ - return cls( + return LiteralArrayVar( _js_expr="", _var_type=figure_out_type(value) if _var_type is None else _var_type, _var_data=_var_data, @@ -1318,7 +1369,7 @@ def string_split_operation(string: StringVar[Any], sep: StringVar | str = ""): @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class ArraySliceOperation(CachedVarOperation, ArrayVar): """Base class for immutable string vars that are the result of a string slice operation.""" @@ -1555,7 +1606,7 @@ def array_range_operation( The range of numbers. """ return var_operation_return( - js_expression=f"Array.from({{ length: ({stop!s} - {start!s}) / {step!s} }}, (_, i) => {start!s} + i * {step!s})", + js_expression=f"Array.from({{ length: Math.ceil(({stop!s} - {start!s}) / {step!s}) }}, (_, i) => {start!s} + i * {step!s})", var_type=List[int], ) @@ -1581,7 +1632,9 @@ def array_contains_field_operation( @var_operation -def array_contains_operation(haystack: ArrayVar, needle: Any | Var): +def array_contains_operation( + haystack: ArrayVar, needle: Any | Var +) -> CustomVarOperationReturn[bool]: """Check if an array contains an element. Args: @@ -1616,15 +1669,11 @@ def repeat_array_operation( ) -if TYPE_CHECKING: - from .function import FunctionVar - - @var_operation def map_array_operation( array: ArrayVar[ARRAY_VAR_TYPE], function: FunctionVar, -): +) -> CustomVarOperationReturn[List[Any]]: """Map a function over an array. Args: @@ -1654,7 +1703,7 @@ def array_concat_operation( """ return var_operation_return( js_expression=f"[...{lhs}, ...{rhs}]", - var_type=Union[lhs._var_type, rhs._var_type], + var_type=Union[lhs._var_type, rhs._var_type], # pyright: ignore [reportArgumentType] ) @@ -1665,7 +1714,7 @@ class ColorVar(StringVar[Color], python_types=Color): @dataclasses.dataclass( eq=False, frozen=True, - **{"slots": True} if sys.version_info >= (3, 10) else {}, + slots=True, ) class LiteralColorVar(CachedVarOperation, LiteralVar, ColorVar): """Base class for immutable literal color vars.""" diff --git a/scripts/bun_install.sh b/scripts/bun_install.sh index 08a0817f6..6961544ad 100644 --- a/scripts/bun_install.sh +++ b/scripts/bun_install.sh @@ -78,6 +78,14 @@ case $platform in ;; esac +case "$target" in +'linux'*) + if [ -f /etc/alpine-release ]; then + target="$target-musl" + fi + ;; +esac + if [[ $target = darwin-x64 ]]; then # Is this process running in Rosetta? # redirect stderr to devnull to avoid error message when not running in Rosetta @@ -91,19 +99,20 @@ GITHUB=${GITHUB-"https://github.com"} github_repo="$GITHUB/oven-sh/bun" -if [[ $target = darwin-x64 ]]; then - # If AVX2 isn't supported, use the -baseline build +# If AVX2 isn't supported, use the -baseline build +case "$target" in +'darwin-x64'*) if [[ $(sysctl -a | grep machdep.cpu | grep AVX2) == '' ]]; then - target=darwin-x64-baseline + target="$target-baseline" fi -fi - -if [[ $target = linux-x64 ]]; then + ;; +'linux-x64'*) # If AVX2 isn't supported, use the -baseline build if [[ $(cat /proc/cpuinfo | grep avx2) = '' ]]; then - target=linux-x64-baseline + target="$target-baseline" fi -fi + ;; +esac exe_name=bun @@ -113,8 +122,10 @@ if [[ $# = 2 && $2 = debug-info ]]; then info "You requested a debug build of bun. More information will be shown if a crash occurs." fi +bun_version=BUN_VERSION + if [[ $# = 0 ]]; then - bun_uri=$github_repo/releases/latest/download/bun-$target.zip + bun_uri=$github_repo/releases/download/bun-v$bun_version/bun-$target.zip else bun_uri=$github_repo/releases/download/$1/bun-$target.zip fi diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 3c432ac3b..52f1bcc14 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -214,8 +214,12 @@ function Install-Bun { # http://community.sqlbackupandftp.com/t/error-1073741515-solved/1305 if (($LASTEXITCODE -eq 3221225781) -or ($LASTEXITCODE -eq -1073741515)) # STATUS_DLL_NOT_FOUND { + # TODO: as of July 2024, Bun has no external dependencies. + # I want to keep this error message in for a few months to ensure that + # if someone somehow runs into this, it can be reported. Write-Output "Install Failed - You are missing a DLL required to run bun.exe" Write-Output "This can be solved by installing the Visual C++ Redistributable from Microsoft:`nSee https://learn.microsoft.com/cpp/windows/latest-supported-vc-redist`nDirect Download -> https://aka.ms/vs/17/release/vc_redist.x64.exe`n`n" + Write-Output "The error above should be unreachable as Bun does not depend on this library. Please comment in https://github.com/oven-sh/bun/issues/8598 or open a new issue.`n`n" Write-Output "The command '${BunBin}\bun.exe --revision' exited with code ${LASTEXITCODE}`n" return 1 } diff --git a/scripts/wait_for_listening_port.py b/scripts/wait_for_listening_port.py index 247ff4fba..4befa00bd 100644 --- a/scripts/wait_for_listening_port.py +++ b/scripts/wait_for_listening_port.py @@ -14,7 +14,7 @@ from typing import Tuple import psutil -def _pid_exists(pid): +def _pid_exists(pid: int): # os.kill(pid, 0) doesn't work on Windows (actually kills the PID) # psutil.pid_exists() doesn't work on Windows (does os.kill underneath) # psutil.pids() seems to return the right thing. Inefficient but doesn't matter - keeps things simple. @@ -23,9 +23,9 @@ def _pid_exists(pid): return pid in psutil.pids() -def _wait_for_port(port, server_pid, timeout) -> Tuple[bool, str]: +def _wait_for_port(port: int, server_pid: int, timeout: float) -> Tuple[bool, str]: start = time.time() - print(f"Waiting for up to {timeout} seconds for port {port} to start listening.") + print(f"Waiting for up to {timeout} seconds for port {port} to start listening.") # noqa: T201 while True: if not _pid_exists(server_pid): return False, f"Server PID {server_pid} is not running." @@ -49,17 +49,16 @@ def main(): parser.add_argument("--server-pid", type=int) args = parser.parse_args() executor = ThreadPoolExecutor(max_workers=len(args.port)) - futures = [] - for p in args.port: - futures.append( - executor.submit(_wait_for_port, p, args.server_pid, args.timeout) - ) + futures = [ + executor.submit(_wait_for_port, p, args.server_pid, args.timeout) + for p in args.port + ] for f in as_completed(futures): ok, msg = f.result() if ok: - print(f"OK: {msg}") + print(f"OK: {msg}") # noqa: T201 else: - print(f"FAIL: {msg}") + print(f"FAIL: {msg}") # noqa: T201 exit(1) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index f7b825f16..67bd26c49 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1,11 +1,10 @@ """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 @@ -35,34 +34,6 @@ 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 = environment.SCREENSHOT_DIR.get() - 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"] ) @@ -76,3 +47,25 @@ 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() diff --git a/tests/integration/init-test/Dockerfile b/tests/integration/init-test/Dockerfile index f30466e7f..e5d2a0820 100644 --- a/tests/integration/init-test/Dockerfile +++ b/tests/integration/init-test/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9 +FROM python:3.10 ARG USERNAME=kerrigan RUN useradd -m $USERNAME diff --git a/tests/integration/test_background_task.py b/tests/integration/test_background_task.py index d7fe20824..f312f8122 100644 --- a/tests/integration/test_background_task.py +++ b/tests/integration/test_background_task.py @@ -37,9 +37,9 @@ def BackgroundTask(): self._task_id += 1 for ix in range(int(self.iterations)): if ix % 2 == 0: - yield State.increment_arbitrary(1) # type: ignore + yield State.increment_arbitrary(1) else: - yield State.increment() # type: ignore + yield State.increment() await asyncio.sleep(0.005) @rx.event @@ -125,8 +125,8 @@ def BackgroundTask(): rx.input( id="iterations", placeholder="Iterations", - value=State.iterations.to_string(), # type: ignore - on_change=State.set_iterations, # type: ignore + value=State.iterations.to_string(), # pyright: ignore [reportAttributeAccessIssue] + on_change=State.set_iterations, # pyright: ignore [reportAttributeAccessIssue] ), rx.button( "Delayed Increment", @@ -172,7 +172,7 @@ def BackgroundTask(): 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) @@ -288,7 +288,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 # type: ignore + lambda: not background_task.app_instance._background_tasks # pyright: ignore [reportOptionalMemberAccess] ) diff --git a/tests/integration/test_call_script.py b/tests/integration/test_call_script.py index 8c4bab8ce..f57dd2850 100644 --- a/tests/integration/test_call_script.py +++ b/tests/integration/test_call_script.py @@ -15,7 +15,8 @@ from .utils import SessionStorage def CallScript(): """A test app for browser javascript integration.""" - from typing import Dict, List, Optional, Union + from pathlib import Path + from typing import Optional, Union import reflex as rx @@ -42,15 +43,17 @@ def CallScript(): external_scripts = inline_scripts.replace("inline", "external") class CallScriptState(rx.State): - results: List[Optional[Union[str, Dict, List]]] = [] - inline_counter: int = 0 - external_counter: int = 0 + results: rx.Field[list[Optional[Union[str, dict, list]]]] = rx.field([]) + inline_counter: rx.Field[int] = rx.field(0) + external_counter: rx.Field[int] = rx.field(0) value: str = "Initial" - last_result: str = "" + 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]) @@ -90,7 +93,7 @@ def CallScript(): def call_script_inline_return_lambda(self): return rx.call_script( "inline2()", - callback=lambda result: CallScriptState.call_script_callback_other_arg( # type: ignore + callback=lambda result: CallScriptState.call_script_callback_other_arg( result, "lambda" ), ) @@ -99,7 +102,7 @@ def CallScript(): def get_inline_counter(self): return rx.call_script( "inline_counter", - callback=CallScriptState.set_inline_counter, # type: ignore + callback=CallScriptState.setvar("inline_counter"), ) @rx.event @@ -138,7 +141,7 @@ def CallScript(): def call_script_external_return_lambda(self): return rx.call_script( "external2()", - callback=lambda result: CallScriptState.call_script_callback_other_arg( # type: ignore + callback=lambda result: CallScriptState.call_script_callback_other_arg( result, "lambda" ), ) @@ -147,28 +150,28 @@ def CallScript(): def get_external_counter(self): return rx.call_script( "external_counter", - callback=CallScriptState.set_external_counter, # type: ignore + callback=CallScriptState.setvar("external_counter"), ) @rx.event def call_with_var_f_string(self): return rx.call_script( f"{rx.Var('inline_counter')} + {rx.Var('external_counter')}", - callback=CallScriptState.set_last_result, # type: ignore + 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.set_last_result, # type: ignore + 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.set_last_result, # type: ignore + callback=CallScriptState.setvar("last_result"), ) @rx.event @@ -177,7 +180,7 @@ def CallScript(): rx.Var( f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}" ), - callback=CallScriptState.set_last_result, # type: ignore + callback=CallScriptState.setvar("last_result"), ) @rx.event @@ -185,25 +188,24 @@ def CallScript(): yield rx.call_script("inline_counter = 0; external_counter = 0") self.reset() - app = rx.App(state=rx.State) - with open("assets/external.js", "w") as f: - f.write(external_scripts) + app = rx.App(_state=rx.State) + Path("assets/external.js").write_text(external_scripts) @app.add_page def index(): return rx.vstack( rx.input( - value=CallScriptState.inline_counter.to(str), # type: ignore + value=CallScriptState.inline_counter.to(str), id="inline_counter", read_only=True, ), rx.input( - value=CallScriptState.external_counter.to(str), # type: ignore + value=CallScriptState.external_counter.to(str), id="external_counter", read_only=True, ), rx.text_area( - value=CallScriptState.results.to_string(), # type: ignore + value=CallScriptState.results.to_string(), id="results", read_only=True, ), @@ -273,7 +275,7 @@ def CallScript(): CallScriptState.value, on_click=rx.call_script( "'updated'", - callback=CallScriptState.set_value, # type: ignore + callback=CallScriptState.setvar("value"), ), id="update_value", ), @@ -282,7 +284,7 @@ def CallScript(): value=CallScriptState.last_result, id="last_result", read_only=True, - on_click=CallScriptState.set_last_result(""), # type: ignore + on_click=CallScriptState.setvar("last_result", 0), ), rx.button( "call_with_var_f_string", @@ -308,7 +310,7 @@ def CallScript(): "call_with_var_f_string_inline", on_click=rx.call_script( f"{rx.Var('inline_counter')} + {CallScriptState.last_result}", - callback=CallScriptState.set_last_result, # type: ignore + callback=CallScriptState.setvar("last_result"), ), id="call_with_var_f_string_inline", ), @@ -316,7 +318,7 @@ def CallScript(): "call_with_var_str_cast_inline", on_click=rx.call_script( f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}", - callback=CallScriptState.set_last_result, # type: ignore + callback=CallScriptState.setvar("last_result"), ), id="call_with_var_str_cast_inline", ), @@ -326,7 +328,7 @@ def CallScript(): rx.Var( f"{rx.Var('inline_counter')} + {CallScriptState.last_result}" ), - callback=CallScriptState.set_last_result, # type: ignore + callback=CallScriptState.setvar("last_result"), ), id="call_with_var_f_string_wrapped_inline", ), @@ -336,7 +338,7 @@ def CallScript(): rx.Var( f"{rx.Var('inline_counter')!s} + {rx.Var('external_counter')!s}" ), - callback=CallScriptState.set_last_result, # type: ignore + callback=CallScriptState.setvar("last_result"), ), id="call_with_var_str_cast_wrapped_inline", ), @@ -483,7 +485,7 @@ def test_call_script_w_var( """ assert_token(driver) last_result = driver.find_element(By.ID, "last_result") - assert last_result.get_attribute("value") == "" + assert last_result.get_attribute("value") == "0" inline_return_button = driver.find_element(By.ID, "inline_return") diff --git a/tests/integration/test_client_storage.py b/tests/integration/test_client_storage.py index 236d3e14e..3618c779d 100644 --- a/tests/integration/test_client_storage.py +++ b/tests/integration/test_client_storage.py @@ -33,18 +33,18 @@ def ClientSide(): class ClientSideSubState(ClientSideState): # cookies with default settings c1: str = rx.Cookie() - c2: rx.Cookie = "c2 default" # type: ignore + c2: str = rx.Cookie("c2 default") # cookies with custom settings c3: str = rx.Cookie(max_age=2) # expires after 2 second - c4: rx.Cookie = rx.Cookie(same_site="strict") + c4: str = 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: rx.LocalStorage = "l2 default" # type: ignore + l2: str = rx.LocalStorage("l2 default") # local storage with custom settings l3: str = rx.LocalStorage(name="l3") @@ -56,7 +56,7 @@ def ClientSide(): # Session storage s1: str = rx.SessionStorage() - s2: rx.SessionStorage = "s2 default" # type: ignore + s2: str = rx.SessionStorage("s2 default") s3: str = rx.SessionStorage(name="s3") def set_l6(self, my_param: str): @@ -87,13 +87,13 @@ def ClientSide(): rx.input( placeholder="state var", value=ClientSideState.state_var, - on_change=ClientSideState.set_state_var, # type: ignore + on_change=ClientSideState.setvar("state_var"), id="state_var", ), rx.input( placeholder="input value", value=ClientSideState.input_value, - on_change=ClientSideState.set_input_value, # type: ignore + on_change=ClientSideState.setvar("input_value"), id="input_value", ), rx.button( @@ -127,7 +127,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") @@ -321,6 +321,7 @@ async def test_client_side_state( assert not driver.get_cookies() local_storage_items = local_storage.items() local_storage_items.pop("last_compiled_time", None) + local_storage_items.pop("theme", None) assert not local_storage_items # set some cookies and local storage values @@ -436,6 +437,7 @@ async def test_client_side_state( local_storage_items = local_storage.items() local_storage_items.pop("last_compiled_time", None) + local_storage_items.pop("theme", None) assert local_storage_items.pop(f"{sub_state_name}.l1") == "l1 value" assert local_storage_items.pop(f"{sub_state_name}.l2") == "l2 value" assert local_storage_items.pop("l3") == "l3 value" @@ -637,8 +639,7 @@ async def test_client_side_state( 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. - state_var_input = driver.find_element(By.ID, "state_var") - state_var_input.send_keys("re-triggering") + set_sub("c1", "c1 post expire") # get new references to all cookie and local storage elements (again) c1 = driver.find_element(By.ID, "c1") @@ -659,7 +660,7 @@ async def test_client_side_state( l1s = driver.find_element(By.ID, "l1s") s1s = driver.find_element(By.ID, "s1s") - assert c1.text == "c1 value" + 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" @@ -690,11 +691,11 @@ async def test_client_side_state( async def poll_for_c1_set(): sub_state = await get_sub_state() - return sub_state.c1 == "c1 value" + 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 value" + assert sub_state.c1 == "c1 post expire" assert sub_state.c2 == "c2 value" assert sub_state.c3 == "" assert sub_state.c4 == "c4 value" diff --git a/tests/integration/test_component_state.py b/tests/integration/test_component_state.py index 97624e7c5..654dc7ce9 100644 --- a/tests/integration/test_component_state.py +++ b/tests/integration/test_component_state.py @@ -30,7 +30,7 @@ def ComponentStateApp(): @rx.event def increment(self): self.count += 1 - self._be = self.count # type: ignore + self._be = self.count # pyright: ignore [reportAttributeAccessIssue] @classmethod def get_component(cls, *children, **props): @@ -72,7 +72,7 @@ def ComponentStateApp(): State=_Counter, ) - app = rx.App(state=rx.State) # noqa + app = rx.App(_state=rx.State) # noqa: F841 @rx.page() def index(): @@ -89,7 +89,7 @@ def ComponentStateApp(): mc_d, rx.button( "Inc A", - on_click=mc_a.State.increment, # type: ignore + on_click=mc_a.State.increment, # pyright: ignore [reportAttributeAccessIssue, reportOptionalMemberAccess] id="inc-a", ), rx.text( diff --git a/tests/integration/test_computed_vars.py b/tests/integration/test_computed_vars.py index a41458173..f56001ea8 100644 --- a/tests/integration/test_computed_vars.py +++ b/tests/integration/test_computed_vars.py @@ -22,22 +22,22 @@ def ComputedVars(): count: int = 0 # cached var with dep on count - @rx.var(cache=True, interval=15) + @rx.var(interval=15) def count1(self) -> int: return self.count # cached backend var with dep on count - @rx.var(cache=True, interval=15, backend=True) + @rx.var(interval=15, backend=True) def count1_backend(self) -> int: return self.count # same as above but implicit backend with `_` prefix - @rx.var(cache=True, interval=15) + @rx.var(interval=15) def _count1_backend(self) -> int: return self.count # explicit disabled auto_deps - @rx.var(interval=15, cache=True, auto_deps=False) + @rx.var(interval=15, auto_deps=False) def count3(self) -> int: # this will not add deps, because auto_deps is False print(self.count1) @@ -45,19 +45,27 @@ def ComputedVars(): return self.count # explicit dependency on count var - @rx.var(cache=True, deps=["count"], auto_deps=False) + @rx.var(deps=["count"], auto_deps=False) def depends_on_count(self) -> int: return self.count # explicit dependency on count1 var - @rx.var(cache=True, deps=[count1], auto_deps=False) + @rx.var(deps=[count1], auto_deps=False) def depends_on_count1(self) -> int: return self.count - @rx.var(deps=[count3], auto_deps=False, cache=True) + @rx.var( + deps=[count3], + auto_deps=False, + ) 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 @@ -103,10 +111,14 @@ 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) @@ -225,6 +237,10 @@ 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() diff --git a/tests/integration/test_connection_banner.py b/tests/integration/test_connection_banner.py index 44187c8ba..e6a8caef6 100644 --- a/tests/integration/test_connection_banner.py +++ b/tests/integration/test_connection_banner.py @@ -1,5 +1,6 @@ """Test case for displaying the connection banner when the websocket drops.""" +import functools from typing import Generator import pytest @@ -11,12 +12,19 @@ from reflex.testing import AppHarness, WebDriver from .utils import SessionStorage -def ConnectionBanner(): - """App with a connection banner.""" +def ConnectionBanner(is_reflex_cloud: bool = False): + """App with a connection banner. + + Args: + is_reflex_cloud: The value for config.is_reflex_cloud. + """ import asyncio import reflex as rx + # Simulate reflex cloud deploy + rx.config.get_config().is_reflex_cloud = is_reflex_cloud + class State(rx.State): foo: int = 0 @@ -31,28 +39,52 @@ def ConnectionBanner(): rx.button( "Increment", id="increment", - on_click=State.set_foo(State.foo + 1), # type: ignore + on_click=State.set_foo(State.foo + 1), # pyright: ignore [reportAttributeAccessIssue] ), 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=[False, True], ids=["reflex_cloud_disabled", "reflex_cloud_enabled"] +) +def simulate_is_reflex_cloud(request) -> bool: + """Fixture to simulate reflex cloud deployment. + + Args: + request: pytest request fixture. + + Returns: + True if reflex cloud is enabled, False otherwise. + """ + return request.param + + @pytest.fixture() -def connection_banner(tmp_path) -> Generator[AppHarness, None, None]: +def connection_banner( + tmp_path, + simulate_is_reflex_cloud: bool, +) -> Generator[AppHarness, None, None]: """Start ConnectionBanner app at tmp_path via AppHarness. Args: tmp_path: pytest tmp_path fixture + simulate_is_reflex_cloud: Whether is_reflex_cloud is set for the app. Yields: running AppHarness instance """ with AppHarness.create( root=tmp_path, - app_source=ConnectionBanner, + app_source=functools.partial( + ConnectionBanner, is_reflex_cloud=simulate_is_reflex_cloud + ), + app_name="connection_banner_reflex_cloud" + if simulate_is_reflex_cloud + else "connection_banner", ) as harness: yield harness @@ -71,9 +103,42 @@ 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(), 'You ran out of compute credits.') ]" + ) + 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 @@ -87,11 +152,7 @@ async def test_connection_banner(connection_banner: AppHarness): assert connection_banner.backend is not None driver = connection_banner.frontend() - ss = SessionStorage(driver) - assert connection_banner._poll_for( - lambda: ss.get("token") is not None - ), "token not found" - + _assert_token(connection_banner, driver) assert connection_banner._poll_for(lambda: not has_error_modal(driver)) delay_button = driver.find_element(By.ID, "delay") @@ -131,3 +192,36 @@ 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_is_reflex_cloud: bool +): + """Test that the connection banner is displayed when the websocket drops. + + Args: + connection_banner: AppHarness instance. + simulate_is_reflex_cloud: Whether is_reflex_cloud is 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_is_reflex_cloud: + 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)) diff --git a/tests/integration/test_deploy_url.py b/tests/integration/test_deploy_url.py index 5c840d24d..207f37609 100644 --- a/tests/integration/test_deploy_url.py +++ b/tests/integration/test_deploy_url.py @@ -19,14 +19,14 @@ def DeployUrlSample() -> None: class State(rx.State): @rx.event def goto_self(self): - return rx.redirect(rx.config.get_config().deploy_url) # type: ignore + return rx.redirect(rx.config.get_config().deploy_url) # pyright: ignore [reportArgumentType] 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) diff --git a/tests/integration/test_dynamic_routes.py b/tests/integration/test_dynamic_routes.py index 8a3cde3a2..40886a601 100644 --- a/tests/integration/test_dynamic_routes.py +++ b/tests/integration/test_dynamic_routes.py @@ -49,7 +49,7 @@ def DynamicRoute(): read_only=True, id="token", ), - rx.input(value=rx.State.page_id, read_only=True, id="page_id"), # type: ignore + rx.input(value=rx.State.page_id, read_only=True, id="page_id"), # pyright: ignore [reportAttributeAccessIssue] rx.input( value=DynamicState.router.page.raw_path, read_only=True, @@ -60,12 +60,12 @@ def DynamicRoute(): rx.link( "next", href="/page/" + DynamicState.next_page, - id="link_page_next", # type: ignore + id="link_page_next", ), rx.link("missing", href="/missing", id="link_missing"), - rx.list( # type: ignore + rx.list( # pyright: ignore [reportAttributeAccessIssue] rx.foreach( - DynamicState.order, # type: ignore + DynamicState.order, # pyright: ignore [reportAttributeAccessIssue] lambda i: rx.list_item(rx.text(i)), ), ), @@ -74,16 +74,16 @@ def DynamicRoute(): class ArgState(rx.State): """The app state.""" - @rx.var + @rx.var(cache=False) def arg(self) -> int: return int(self.arg_str or 0) class ArgSubState(ArgState): - @rx.var(cache=True) + @rx.var def cached_arg(self) -> int: return self.arg - @rx.var(cache=True) + @rx.var def cached_arg_str(self) -> str: return self.arg_str @@ -98,11 +98,11 @@ def DynamicRoute(): 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"), # type: ignore + 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"), # type: ignore + rx.data_list.value(ArgState.arg_str, id="argstate-arg_str"), # pyright: ignore [reportAttributeAccessIssue] ), rx.data_list.item( rx.data_list.label("ArgState.arg"), @@ -110,7 +110,7 @@ def DynamicRoute(): ), rx.data_list.item( rx.data_list.label("ArgSubState.arg_str (dynamic) (inherited)"), - rx.data_list.value(ArgSubState.arg_str, id="argsubstate-arg_str"), # type: ignore + 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)"), @@ -134,15 +134,15 @@ def DynamicRoute(): height="100vh", ) - @rx.page(route="/redirect-page/[page_id]", on_load=DynamicState.on_load_redir) # type: ignore + @rx.page(route="/redirect-page/[page_id]", on_load=DynamicState.on_load_redir) 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) # type: ignore - app.add_page(index, route="/static/x", on_load=DynamicState.on_load) # type: ignore + 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.add_page(index) - app.add_custom_404_page(on_load=DynamicState.on_load) # type: ignore + app.add_custom_404_page(on_load=DynamicState.on_load) @pytest.fixture(scope="module") diff --git a/tests/integration/test_event_actions.py b/tests/integration/test_event_actions.py index 15f3c9877..707410075 100644 --- a/tests/integration/test_event_actions.py +++ b/tests/integration/test_event_actions.py @@ -63,16 +63,16 @@ def TestEventAction(): rx.button( "Stop Prop Only", id="btn-stop-prop-only", - on_click=rx.stop_propagation, # type: ignore + on_click=rx.stop_propagation, # pyright: ignore [reportArgumentType] ), rx.button( "Click event", - on_click=EventActionState.on_click("no_event_actions"), # type: ignore + on_click=EventActionState.on_click("no_event_actions"), # pyright: ignore [reportCallIssue] id="btn-click-event", ), rx.button( "Click stop propagation", - on_click=EventActionState.on_click("stop_propagation").stop_propagation, # type: ignore + on_click=EventActionState.on_click("stop_propagation").stop_propagation, # pyright: ignore [reportCallIssue] id="btn-click-stop-propagation", ), rx.button( @@ -88,13 +88,13 @@ def TestEventAction(): rx.link( "Link", href="#", - on_click=EventActionState.on_click("link_no_event_actions"), # type: ignore + on_click=EventActionState.on_click("link_no_event_actions"), # pyright: ignore [reportCallIssue] id="link", ), rx.link( "Link Stop Propagation", href="#", - on_click=EventActionState.on_click( # type: ignore + on_click=EventActionState.on_click( # pyright: ignore [reportCallIssue] "link_stop_propagation" ).stop_propagation, id="link-stop-propagation", @@ -102,13 +102,13 @@ def TestEventAction(): rx.link( "Link Prevent Default Only", href="/invalid", - on_click=rx.prevent_default, # type: ignore + on_click=rx.prevent_default, # pyright: ignore [reportArgumentType] id="link-prevent-default-only", ), rx.link( "Link Prevent Default", href="/invalid", - on_click=EventActionState.on_click( # type: ignore + on_click=EventActionState.on_click( # pyright: ignore [reportCallIssue] "link_prevent_default" ).prevent_default, id="link-prevent-default", @@ -116,47 +116,47 @@ def TestEventAction(): rx.link( "Link Both", href="/invalid", - on_click=EventActionState.on_click( # type: ignore + on_click=EventActionState.on_click( # pyright: ignore [reportCallIssue] "link_both" ).stop_propagation.prevent_default, id="link-stop-propagation-prevent-default", ), EventFiringComponent.create( id="custom-stop-propagation", - on_click=EventActionState.on_click( # type: ignore + on_click=EventActionState.on_click( # pyright: ignore [reportCallIssue] "custom-stop-propagation" ).stop_propagation, ), EventFiringComponent.create( id="custom-prevent-default", - on_click=EventActionState.on_click( # type: ignore + on_click=EventActionState.on_click( # pyright: ignore [reportCallIssue] "custom-prevent-default" ).prevent_default, ), rx.button( "Throttle", id="btn-throttle", - on_click=lambda: EventActionState.on_click_throttle.throttle( + on_click=lambda: EventActionState.on_click_throttle.throttle( # pyright: ignore [reportFunctionMemberAccess] 200 ).stop_propagation, ), rx.button( "Debounce", id="btn-debounce", - on_click=EventActionState.on_click_debounce.debounce( + on_click=EventActionState.on_click_debounce.debounce( # pyright: ignore [reportFunctionMemberAccess] 200 ).stop_propagation, ), - rx.list( # type: ignore + rx.list( # pyright: ignore [reportAttributeAccessIssue] rx.foreach( - EventActionState.order, # type: ignore + EventActionState.order, rx.list_item, ), ), - on_click=EventActionState.on_click("outer"), # type: ignore + on_click=EventActionState.on_click("outer"), # pyright: ignore [reportCallIssue] ) - app = rx.App(state=rx.State) + app = rx.App(_state=rx.State) app.add_page(index) diff --git a/tests/integration/test_event_chain.py b/tests/integration/test_event_chain.py index c4121ee94..df571e884 100644 --- a/tests/integration/test_event_chain.py +++ b/tests/integration/test_event_chain.py @@ -43,32 +43,32 @@ def EventChain(): def event_nested_1(self): self.event_order.append("event_nested_1") yield State.event_nested_2 - yield State.event_arg("nested_1") # type: ignore + yield State.event_arg("nested_1") @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") # type: ignore + yield State.event_arg("nested_2") @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") # type: ignore + yield State.event_arg("nested_3") @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)] # type: ignore + return [State.event_arg(1), State.event_arg(2), State.event_arg(3)] @rx.event def on_load_yield_chain(self): self.event_order.append("on_load_yield_chain") - yield State.event_arg(4) # type: ignore - yield State.event_arg(5) # type: ignore - yield State.event_arg(6) # type: ignore + yield State.event_arg(4) + yield State.event_arg(5) + yield State.event_arg(6) @rx.event def click_return_event(self): @@ -79,28 +79,28 @@ def EventChain(): def click_return_events(self): self.event_order.append("click_return_events") return [ - State.event_arg(7), # type: ignore + State.event_arg(7), rx.console_log("click_return_events"), - State.event_arg(8), # type: ignore - State.event_arg(9), # type: ignore + State.event_arg(8), + State.event_arg(9), ] @rx.event def click_yield_chain(self): self.event_order.append("click_yield_chain:0") - yield State.event_arg(10) # type: ignore + yield State.event_arg(10) self.event_order.append("click_yield_chain:1") yield rx.console_log("click_yield_chain") - yield State.event_arg(11) # type: ignore + yield State.event_arg(11) self.event_order.append("click_yield_chain:2") - yield State.event_arg(12) # type: ignore + yield State.event_arg(12) 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) # type: ignore + yield State.event_arg(ix) yield rx.console_log(f"many_events_{ix}") self.event_order.append("click_yield_many_events_done") @@ -108,7 +108,7 @@ def EventChain(): def click_yield_nested(self): self.event_order.append("click_yield_nested") yield State.event_nested_1 - yield State.event_arg("yield_nested") # type: ignore + yield State.event_arg("yield_nested") @rx.event def redirect_return_chain(self): @@ -123,12 +123,12 @@ def EventChain(): @rx.event def click_return_int_type(self): self.event_order.append("click_return_int_type") - return State.event_arg_repr_type(1) # type: ignore + return State.event_arg_repr_type(1) @rx.event def click_return_dict_type(self): self.event_order.append("click_return_dict_type") - return State.event_arg_repr_type({"a": 1}) # type: ignore + return State.event_arg_repr_type({"a": 1}) @rx.event async def click_yield_interim_value_async(self): @@ -144,7 +144,7 @@ def EventChain(): time.sleep(0.5) self.interim_value = "final" - app = rx.App(state=rx.State) + app = rx.App(_state=rx.State) token_input = rx.input( value=State.router.session.client_token, is_read_only=True, id="token" @@ -193,12 +193,12 @@ def EventChain(): rx.button( "Click Int Type", id="click_int_type", - on_click=lambda: State.event_arg_repr_type(1), # type: ignore + on_click=lambda: State.event_arg_repr_type(1), ), rx.button( "Click Dict Type", id="click_dict_type", - on_click=lambda: State.event_arg_repr_type({"a": 1}), # type: ignore + on_click=lambda: State.event_arg_repr_type({"a": 1}), ), rx.button( "Return Chain Int Type", @@ -239,7 +239,7 @@ def EventChain(): rx.text( "return", on_mount=State.on_load_return_chain, - on_unmount=lambda: State.event_arg("unmount"), # type: ignore + on_unmount=lambda: State.event_arg("unmount"), ), token_input, rx.button("Unmount", on_click=rx.redirect("/"), id="unmount"), @@ -251,7 +251,7 @@ def EventChain(): "yield", on_mount=[ State.on_load_yield_chain, - lambda: State.event_arg("mount"), # type: ignore + lambda: State.event_arg("mount"), ], on_unmount=State.event_no_args, ), @@ -259,8 +259,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) # type: ignore - app.add_page(on_load_yield_chain, on_load=State.on_load_yield_chain) # type: ignore + 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_mount_return_chain) app.add_page(on_mount_yield_chain) diff --git a/tests/integration/test_exception_handlers.py b/tests/integration/test_exception_handlers.py index 406c21e5d..71858b899 100644 --- a/tests/integration/test_exception_handlers.py +++ b/tests/integration/test_exception_handlers.py @@ -13,6 +13,8 @@ from selenium.webdriver.support.ui import WebDriverWait from reflex.testing import AppHarness, AppHarnessProd +pytestmark = [pytest.mark.ignore_console_error] + def TestApp(): """A test app for event exception handler integration.""" @@ -37,7 +39,7 @@ def TestApp(): """ print(1 / number) - app = rx.App(state=rx.State) + app = rx.App(_state=rx.State) @app.add_page def index(): @@ -49,12 +51,12 @@ def TestApp(): ), rx.button( "induce_backend_error", - on_click=lambda: TestAppState.divide_by_number(0), # type: ignore + on_click=lambda: TestAppState.divide_by_number(0), # pyright: ignore [reportCallIssue] id="induce-backend-error-btn", ), rx.button( "induce_react_error", - on_click=TestAppState.set_react_error(True), # type: ignore + on_click=TestAppState.set_react_error(True), # pyright: ignore [reportAttributeAccessIssue] id="induce-react-error-btn", ), rx.box( diff --git a/tests/integration/test_form_submit.py b/tests/integration/test_form_submit.py index ea8750595..bdf54173c 100644 --- a/tests/integration/test_form_submit.py +++ b/tests/integration/test_form_submit.py @@ -30,7 +30,7 @@ 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(): @@ -90,7 +90,7 @@ 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(): diff --git a/tests/integration/test_input.py b/tests/integration/test_input.py index c718749aa..5f2948feb 100644 --- a/tests/integration/test_input.py +++ b/tests/integration/test_input.py @@ -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, # type: ignore + on_change=State.set_text, # pyright: ignore [reportAttributeAccessIssue] 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"), # type: ignore + rx.input(on_change=State.set_text, id="on_change_input"), # pyright: ignore [reportAttributeAccessIssue] rx.el.input( value=State.text, id="plain_value_input", @@ -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 == "" - # assert debounce_input.get_attribute("value") == "" - # assert value_input.get_attribute("value") == "" + # assert backend_state.text == "" #noqa: ERA001 + # assert debounce_input.get_attribute("value") == "" #noqa: ERA001 + # assert value_input.get_attribute("value") == "" #noqa: ERA001 diff --git a/tests/integration/test_lifespan.py b/tests/integration/test_lifespan.py index cb6c640ab..24dd7df6a 100644 --- a/tests/integration/test_lifespan.py +++ b/tests/integration/test_lifespan.py @@ -36,18 +36,20 @@ def LifespanApp(): print("Lifespan global started.") try: while True: - lifespan_task_global += inc # pyright: ignore[reportUnboundVariable] + lifespan_task_global += inc # pyright: ignore[reportUnboundVariable, reportPossiblyUnboundVariable] await asyncio.sleep(0.1) except asyncio.CancelledError as ce: print(f"Lifespan global cancelled: {ce}.") lifespan_task_global = 0 class LifespanState(rx.State): - @rx.var + interval: int = 100 + + @rx.var(cache=False) def task_global(self) -> int: return lifespan_task_global - @rx.var + @rx.var(cache=False) def context_global(self) -> int: return lifespan_context_global @@ -59,7 +61,15 @@ def LifespanApp(): return rx.vstack( rx.text(LifespanState.task_global, id="task_global"), rx.text(LifespanState.context_global, id="context_global"), - rx.moment(interval=100, on_change=LifespanState.tick), + rx.button( + rx.moment( + interval=LifespanState.interval, on_change=LifespanState.tick + ), + on_click=LifespanState.set_interval( # pyright: ignore [reportAttributeAccessIssue] + rx.cond(LifespanState.interval, 0, 100) + ), + id="toggle-tick", + ), ) app = rx.App() @@ -103,12 +113,13 @@ 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 # type: ignore + assert lifespan_app.app_module.lifespan_context_global == 2 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) - assert lifespan_app.app_module.lifespan_task_global > original_task_global_value # type: ignore + driver.find_element(By.ID, "toggle-tick").click() # avoid teardown errors + assert lifespan_app.app_module.lifespan_task_global > original_task_global_value assert int(task_global.text) > original_task_global_value # Kill the backend diff --git a/tests/integration/test_login_flow.py b/tests/integration/test_login_flow.py index 1938672a3..a1df015bf 100644 --- a/tests/integration/test_login_flow.py +++ b/tests/integration/test_login_flow.py @@ -31,8 +31,8 @@ def LoginSample(): yield rx.redirect("/") def index(): - return rx.cond( - State.is_hydrated & State.auth_token, # type: ignore + return rx.cond( # pyright: ignore [reportCallIssue] + State.is_hydrated & State.auth_token, # pyright: ignore [reportOperatorIssue] rx.vstack( rx.heading(State.auth_token, id="auth-token"), rx.button("Logout", on_click=State.logout, id="logout"), @@ -45,7 +45,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) diff --git a/tests/integration/test_media.py b/tests/integration/test_media.py index 10af26591..f3ce65c87 100644 --- a/tests/integration/test_media.py +++ b/tests/integration/test_media.py @@ -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 # type: ignore + img.format = format return img - @rx.var(cache=True) + @rx.var def img_default(self) -> Image.Image: return self._blue() - @rx.var(cache=True) + @rx.var def img_bmp(self) -> Image.Image: return self._blue(format="BMP") - @rx.var(cache=True) + @rx.var def img_jpg(self) -> Image.Image: return self._blue(format="JPEG") - @rx.var(cache=True) + @rx.var def img_png(self) -> Image.Image: return self._blue(format="PNG") - @rx.var(cache=True) + @rx.var def img_gif(self) -> Image.Image: return self._blue(format="GIF") - @rx.var(cache=True) + @rx.var def img_webp(self) -> Image.Image: return self._blue(format="WEBP") - @rx.var(cache=True) + @rx.var def img_from_url(self) -> Image.Image: img_url = "https://picsum.photos/id/1/200/300" img_resp = httpx.get(img_url, follow_redirects=True) - return Image.open(img_resp) # type: ignore + return Image.open(img_resp) # pyright: ignore [reportArgumentType] app = rx.App() diff --git a/tests/integration/test_server_side_event.py b/tests/integration/test_server_side_event.py index f04cc3beb..3050a4e36 100644 --- a/tests/integration/test_server_side_event.py +++ b/tests/integration/test_server_side_event.py @@ -38,7 +38,7 @@ def ServerSideEvent(): 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(): diff --git a/tests/integration/test_state_inheritance.py b/tests/integration/test_state_inheritance.py index 81512a67a..f544fcc92 100644 --- a/tests/integration/test_state_inheritance.py +++ b/tests/integration/test_state_inheritance.py @@ -73,7 +73,7 @@ def StateInheritance(): def on_click_other_mixin(self): self.other_mixin_clicks += 1 self.other_mixin = ( - f"{self.__class__.__name__}.clicked.{self.other_mixin_clicks}" + f"{type(self).__name__}.clicked.{self.other_mixin_clicks}" ) class Base1(Mixin, rx.State): @@ -131,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, # type: ignore + on_click=Base1.on_click_mixin, id="base1-mixin-btn", ), rx.heading( @@ -153,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, # type: ignore + on_click=Child1.on_click_other_mixin, id="child1-other-mixin-btn", ), # Child 2 (Mixin, ChildMixin, OtherMixin) @@ -166,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, # type: ignore + on_click=Child2.on_click_mixin, id="child2-mixin-btn", ), rx.button( "Child2.on_click_other_mixin", - on_click=Child2.on_click_other_mixin, # type: ignore + on_click=Child2.on_click_other_mixin, id="child2-other-mixin-btn", ), # Child 3 (Mixin, ChildMixin, OtherMixin) @@ -186,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, # type: ignore + on_click=Child3.on_click_mixin, id="child3-mixin-btn", ), rx.button( "Child3.on_click_other_mixin", - on_click=Child3.on_click_other_mixin, # type: ignore + on_click=Child3.on_click_other_mixin, id="child3-other-mixin-btn", ), rx.heading( diff --git a/tests/integration/test_upload.py b/tests/integration/test_upload.py index b7f14b03d..a0df05f52 100644 --- a/tests/integration/test_upload.py +++ b/tests/integration/test_upload.py @@ -6,12 +6,16 @@ import asyncio import time from pathlib import Path from typing import Generator +from urllib.parse import urlsplit import pytest from selenium.webdriver.common.by import By +from reflex.constants.event import Endpoint from reflex.testing import AppHarness, WebDriver +from .utils import poll_for_navigation + def UploadFile(): """App for testing dynamic routes.""" @@ -23,7 +27,7 @@ def UploadFile(): class UploadState(rx.State): _file_data: Dict[str, str] = {} - event_order: List[str] = [] + event_order: rx.Field[List[str]] = rx.field([]) progress_dicts: List[dict] = [] disabled: bool = False large_data: str = "" @@ -50,6 +54,15 @@ def UploadFile(): self.large_data = "" self.event_order.append("chain_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() + ) + + def do_download(self): + return rx.download(rx.get_upload_url("test.txt")) + def index(): return rx.vstack( rx.input( @@ -67,7 +80,7 @@ def UploadFile(): ), rx.button( "Upload", - on_click=lambda: UploadState.handle_upload(rx.upload_files()), # type: ignore + on_click=lambda: UploadState.handle_upload(rx.upload_files()), # pyright: ignore [reportCallIssue] id="upload_button", ), rx.box( @@ -92,7 +105,7 @@ def UploadFile(): ), rx.button( "Upload", - on_click=UploadState.handle_upload_secondary( # type: ignore + on_click=UploadState.handle_upload_secondary( # pyright: ignore [reportCallIssue] rx.upload_files( upload_id="secondary", on_upload_progress=UploadState.upload_progress, @@ -114,7 +127,7 @@ def UploadFile(): ), rx.vstack( rx.foreach( - UploadState.progress_dicts, # type: ignore + UploadState.progress_dicts, lambda d: rx.text(d.to_string()), ) ), @@ -123,9 +136,37 @@ 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( # pyright: ignore [reportCallIssue] + rx.upload_files( + 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) @@ -164,6 +205,24 @@ 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( @@ -178,11 +237,7 @@ async def test_upload_file( secondary: whether to use the secondary upload form """ assert upload_file.app_instance is not None - token_input = driver.find_element(By.ID, "token") - assert token_input - # wait for the backend connection to send the token - token = upload_file.poll_for_value(token_input) - assert token is not None + token = poll_for_token(driver, upload_file) full_state_name = upload_file.get_full_state_name(["_upload_state"]) state_name = upload_file.get_state_name("_upload_state") substate_token = f"{token}_{full_state_name}" @@ -204,6 +259,19 @@ 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 ( @@ -217,16 +285,6 @@ async def test_upload_file( normalized_file_data = {Path(k).name: v for k, v in file_data.items()} assert normalized_file_data[Path(exp_name).name] == exp_contents - # check that the selected files are displayed - selected_files = driver.find_element(By.ID, f"selected_files{suffix}") - assert Path(selected_files.text).name == Path(exp_name).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 async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver): @@ -238,11 +296,7 @@ async def test_upload_file_multiple(tmp_path, upload_file: AppHarness, driver): driver: WebDriver instance. """ assert upload_file.app_instance is not None - token_input = driver.find_element(By.ID, "token") - assert token_input - # wait for the backend connection to send the token - token = upload_file.poll_for_value(token_input) - assert token is not None + token = poll_for_token(driver, upload_file) full_state_name = upload_file.get_full_state_name(["_upload_state"]) state_name = upload_file.get_state_name("_upload_state") substate_token = f"{token}_{full_state_name}" @@ -301,11 +355,7 @@ def test_clear_files( secondary: whether to use the secondary upload form. """ assert upload_file.app_instance is not None - token_input = driver.find_element(By.ID, "token") - assert token_input - # wait for the backend connection to send the token - token = upload_file.poll_for_value(token_input) - assert token is not None + poll_for_token(driver, upload_file) suffix = "_secondary" if secondary else "" @@ -357,11 +407,7 @@ async def test_cancel_upload(tmp_path, upload_file: AppHarness, driver: WebDrive driver: WebDriver instance. """ assert upload_file.app_instance is not None - token_input = driver.find_element(By.ID, "token") - assert token_input - # wait for the backend connection to send the token - token = upload_file.poll_for_value(token_input) - assert token is not None + token = poll_for_token(driver, upload_file) state_name = upload_file.get_state_name("_upload_state") state_full_name = upload_file.get_full_state_name(["_upload_state"]) substate_token = f"{token}_{state_full_name}" @@ -381,12 +427,77 @@ async def test_cancel_upload(tmp_path, upload_file: AppHarness, driver: WebDrive await asyncio.sleep(0.3) cancel_button.click() - # look up the backend state and assert on progress + # 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 + state = await upload_file.get_state(substate_token) - assert state.substates[state_name].progress_dicts 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 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 diff --git a/tests/integration/test_var_operations.py b/tests/integration/test_var_operations.py index 7a7c8328d..9b952c575 100644 --- a/tests/integration/test_var_operations.py +++ b/tests/integration/test_var_operations.py @@ -7,42 +7,38 @@ 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 Dict, List - import reflex as rx from reflex.vars.base import LiteralVar from reflex.vars.sequence import ArrayVar class Object(rx.Base): - str: str = "hello" + name: str = "hello" class VarOperationState(rx.State): - 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"] - list4: List = [Object(name="obj_1"), Object(name="obj_2")] - 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 = "
hello
" + 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("
hello
") - 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 @@ -378,7 +374,8 @@ 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, @@ -394,7 +391,8 @@ 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(), @@ -406,7 +404,8 @@ 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(), @@ -422,7 +421,8 @@ 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(), @@ -474,7 +474,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"), @@ -534,7 +535,8 @@ 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"), @@ -571,7 +573,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"), @@ -598,6 +600,11 @@ def VarOperations(): ), 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"), ) @@ -797,6 +804,11 @@ def test_var_operations(driver, var_operations: AppHarness): ("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"), ] for tag, expected in tests: diff --git a/tests/integration/tests_playwright/test_appearance.py b/tests/integration/tests_playwright/test_appearance.py new file mode 100644 index 000000000..d325b183f --- /dev/null +++ b/tests/integration/tests_playwright/test_appearance.py @@ -0,0 +1,218 @@ +from typing import Generator + +import pytest +from playwright.sync_api import Page, expect + +from reflex.testing import AppHarness + + +def DefaultLightModeApp(): + import reflex as rx + from reflex.style import color_mode + + app = rx.App(theme=rx.theme(appearance="light")) + + @app.add_page + def index(): + return rx.text(color_mode) + + +def DefaultDarkModeApp(): + import reflex as rx + from reflex.style import color_mode + + app = rx.App(theme=rx.theme(appearance="dark")) + + @app.add_page + def index(): + return rx.text(color_mode) + + +def DefaultSystemModeApp(): + import reflex as rx + from reflex.style import color_mode + + app = rx.App() + + @app.add_page + def index(): + return rx.text(color_mode) + + +def ColorToggleApp(): + import reflex as rx + from reflex.style import color_mode, resolved_color_mode, set_color_mode + + app = rx.App(theme=rx.theme(appearance="light")) + + @app.add_page + def index(): + return rx.box( + rx.segmented_control.root( + rx.segmented_control.item( + rx.icon(tag="monitor", size=20), + value="system", + ), + rx.segmented_control.item( + rx.icon(tag="sun", size=20), + value="light", + ), + rx.segmented_control.item( + rx.icon(tag="moon", size=20), + value="dark", + ), + on_change=set_color_mode, + variant="classic", + radius="large", + value=color_mode, + ), + rx.text(color_mode, id="current_color_mode"), + rx.text(resolved_color_mode, id="resolved_color_mode"), + rx.text(rx.color_mode_cond("LightMode", "DarkMode"), id="color_mode_cond"), + ) + + +@pytest.fixture() +def light_mode_app(tmp_path_factory) -> Generator[AppHarness, None, None]: + """Start DefaultLightMode 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("appearance_app"), + app_source=DefaultLightModeApp, + ) as harness: + assert harness.app_instance is not None, "app is not running" + yield harness + + +@pytest.fixture() +def dark_mode_app(tmp_path_factory) -> Generator[AppHarness, None, None]: + """Start DefaultDarkMode 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("appearance_app"), + app_source=DefaultDarkModeApp, + ) as harness: + assert harness.app_instance is not None, "app is not running" + yield harness + + +@pytest.fixture() +def system_mode_app(tmp_path_factory) -> Generator[AppHarness, None, None]: + """Start DefaultSystemMode 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("appearance_app"), + app_source=DefaultSystemModeApp, + ) as harness: + assert harness.app_instance is not None, "app is not running" + yield harness + + +@pytest.fixture() +def color_toggle_app(tmp_path_factory) -> Generator[AppHarness, None, None]: + """Start ColorToggle 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("appearance_app"), + app_source=ColorToggleApp, + ) as harness: + assert harness.app_instance is not None, "app is not running" + yield harness + + +def test_appearance_light_mode(light_mode_app: AppHarness, page: Page): + assert light_mode_app.frontend_url is not None + page.goto(light_mode_app.frontend_url) + + expect(page.get_by_text("light")).to_be_visible() + + +def test_appearance_dark_mode(dark_mode_app: AppHarness, page: Page): + assert dark_mode_app.frontend_url is not None + page.goto(dark_mode_app.frontend_url) + + expect(page.get_by_text("dark")).to_be_visible() + + +def test_appearance_system_mode(system_mode_app: AppHarness, page: Page): + assert system_mode_app.frontend_url is not None + page.goto(system_mode_app.frontend_url) + + expect(page.get_by_text("system")).to_be_visible() + + +def test_appearance_color_toggle(color_toggle_app: AppHarness, page: Page): + assert color_toggle_app.frontend_url is not None + page.goto(color_toggle_app.frontend_url) + + # Radio buttons locators. + radio_system = page.get_by_role("radio").nth(0) + radio_light = page.get_by_role("radio").nth(1) + radio_dark = page.get_by_role("radio").nth(2) + + # Text locators to check. + current_color_mode = page.locator("id=current_color_mode") + resolved_color_mode = page.locator("id=resolved_color_mode") + color_mode_cond = page.locator("id=color_mode_cond") + root_body = page.locator('div[data-is-root-theme="true"]') + + # Background colors. + dark_background = "rgb(17, 17, 19)" # value based on dark native appearance, can change depending on the browser + light_background = "rgb(255, 255, 255)" + + # check initial state + expect(current_color_mode).to_have_text("light") + expect(resolved_color_mode).to_have_text("light") + expect(color_mode_cond).to_have_text("LightMode") + expect(root_body).to_have_css("background-color", light_background) + + # click dark mode + radio_dark.click() + expect(current_color_mode).to_have_text("dark") + expect(resolved_color_mode).to_have_text("dark") + expect(color_mode_cond).to_have_text("DarkMode") + expect(root_body).to_have_css("background-color", dark_background) + + # click light mode + radio_light.click() + expect(current_color_mode).to_have_text("light") + expect(resolved_color_mode).to_have_text("light") + expect(color_mode_cond).to_have_text("LightMode") + expect(root_body).to_have_css("background-color", light_background) + page.reload() + expect(root_body).to_have_css("background-color", light_background) + + # click system mode + radio_system.click() + expect(current_color_mode).to_have_text("system") + expect(resolved_color_mode).to_have_text("light") + expect(color_mode_cond).to_have_text("LightMode") + expect(root_body).to_have_css("background-color", light_background) diff --git a/tests/integration/tests_playwright/test_datetime_operations.py b/tests/integration/tests_playwright/test_datetime_operations.py new file mode 100644 index 000000000..2ac516d4a --- /dev/null +++ b/tests/integration/tests_playwright/test_datetime_operations.py @@ -0,0 +1,87 @@ +from typing import Generator + +import pytest +from playwright.sync_api import Page, expect + +from reflex.testing import AppHarness + + +def DatetimeOperationsApp(): + from datetime import datetime + + import reflex as rx + + class DtOperationsState(rx.State): + date1: datetime = datetime(2021, 1, 1) + date2: datetime = datetime(2031, 1, 1) + date3: datetime = datetime(2021, 1, 1) + + app = rx.App(_state=DtOperationsState) + + @app.add_page + def index(): + return rx.vstack( + rx.text(DtOperationsState.date1, id="date1"), + rx.text(DtOperationsState.date2, id="date2"), + rx.text(DtOperationsState.date3, id="date3"), + rx.text("Operations between date1 and date2"), + rx.text(DtOperationsState.date1 == DtOperationsState.date2, id="1_eq_2"), + rx.text(DtOperationsState.date1 != DtOperationsState.date2, id="1_neq_2"), + rx.text(DtOperationsState.date1 < DtOperationsState.date2, id="1_lt_2"), + rx.text(DtOperationsState.date1 <= DtOperationsState.date2, id="1_le_2"), + rx.text(DtOperationsState.date1 > DtOperationsState.date2, id="1_gt_2"), + rx.text(DtOperationsState.date1 >= DtOperationsState.date2, id="1_ge_2"), + rx.text("Operations between date1 and date3"), + rx.text(DtOperationsState.date1 == DtOperationsState.date3, id="1_eq_3"), + rx.text(DtOperationsState.date1 != DtOperationsState.date3, id="1_neq_3"), + rx.text(DtOperationsState.date1 < DtOperationsState.date3, id="1_lt_3"), + rx.text(DtOperationsState.date1 <= DtOperationsState.date3, id="1_le_3"), + rx.text(DtOperationsState.date1 > DtOperationsState.date3, id="1_gt_3"), + rx.text(DtOperationsState.date1 >= DtOperationsState.date3, id="1_ge_3"), + ) + + +@pytest.fixture() +def datetime_operations_app(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("datetime_operations_app"), + app_source=DatetimeOperationsApp, + ) as harness: + assert harness.app_instance is not None, "app is not running" + yield harness + + +def test_datetime_operations(datetime_operations_app: AppHarness, page: Page): + assert datetime_operations_app.frontend_url is not None + + page.goto(datetime_operations_app.frontend_url) + expect(page).to_have_url(datetime_operations_app.frontend_url + "/") + # Check the actual values + expect(page.locator("id=date1")).to_have_text("2021-01-01 00:00:00") + expect(page.locator("id=date2")).to_have_text("2031-01-01 00:00:00") + expect(page.locator("id=date3")).to_have_text("2021-01-01 00:00:00") + + # Check the operations between date1 and date2 + expect(page.locator("id=1_eq_2")).to_have_text("false") + expect(page.locator("id=1_neq_2")).to_have_text("true") + expect(page.locator("id=1_lt_2")).to_have_text("true") + expect(page.locator("id=1_le_2")).to_have_text("true") + expect(page.locator("id=1_gt_2")).to_have_text("false") + expect(page.locator("id=1_ge_2")).to_have_text("false") + + # Check the operations between date1 and date3 + expect(page.locator("id=1_eq_3")).to_have_text("true") + expect(page.locator("id=1_neq_3")).to_have_text("false") + expect(page.locator("id=1_lt_3")).to_have_text("false") + expect(page.locator("id=1_le_3")).to_have_text("true") + expect(page.locator("id=1_gt_3")).to_have_text("false") + expect(page.locator("id=1_ge_3")).to_have_text("true") diff --git a/tests/integration/tests_playwright/test_link_hover.py b/tests/integration/tests_playwright/test_link_hover.py new file mode 100644 index 000000000..3c29d769a --- /dev/null +++ b/tests/integration/tests_playwright/test_link_hover.py @@ -0,0 +1,46 @@ +from typing import Generator + +import pytest +from playwright.sync_api import Page, expect + +from reflex.testing import AppHarness + + +def LinkApp(): + import reflex as rx + + app = rx.App() + + def index(): + return rx.vstack( + rx.box(height="10em"), # spacer, so the link isn't hovered initially + rx.link( + "Click me", + href="#", + color="blue", + _hover=rx.Style({"color": "red"}), + ), + ) + + app.add_page(index, "/") + + +@pytest.fixture() +def link_app(tmp_path_factory) -> Generator[AppHarness, None, None]: + with AppHarness.create( + root=tmp_path_factory.mktemp("link_app"), + app_source=LinkApp, + ) as harness: + assert harness.app_instance is not None, "app is not running" + yield harness + + +def test_link_hover(link_app: AppHarness, page: Page): + assert link_app.frontend_url is not None + page.goto(link_app.frontend_url) + + link = page.get_by_role("link") + expect(link).to_have_text("Click me") + expect(link).to_have_css("color", "rgb(0, 0, 255)") + link.hover() + expect(link).to_have_css("color", "rgb(255, 0, 0)") diff --git a/tests/integration/tests_playwright/test_table.py b/tests/integration/tests_playwright/test_table.py index db716aa5b..a88c4a621 100644 --- a/tests/integration/tests_playwright/test_table.py +++ b/tests/integration/tests_playwright/test_table.py @@ -3,7 +3,7 @@ from typing import Generator import pytest -from playwright.sync_api import Page +from playwright.sync_api import Page, expect from reflex.testing import AppHarness @@ -20,7 +20,7 @@ def Table(): """App using table component.""" import reflex as rx - app = rx.App(state=rx.State) + app = rx.App(_state=rx.State) @app.add_page def index(): @@ -87,12 +87,14 @@ def test_table(page: Page, table_app: AppHarness): table = page.get_by_role("table") # Check column headers - headers = table.get_by_role("columnheader").all_inner_texts() - assert headers == expected_col_headers + headers = table.get_by_role("columnheader") + for header, exp_value in zip(headers.all(), expected_col_headers, strict=True): + expect(header).to_have_text(exp_value) # Check rows headers - rows = table.get_by_role("rowheader").all_inner_texts() - assert rows == expected_row_headers + rows = table.get_by_role("rowheader") + for row, expected_row in zip(rows.all(), expected_row_headers, strict=True): + expect(row).to_have_text(expected_row) # Check cells rows = table.get_by_role("cell").all_inner_texts() diff --git a/tests/units/assets/test_assets.py b/tests/units/assets/test_assets.py index b957f1902..dc444cfad 100644 --- a/tests/units/assets/test_assets.py +++ b/tests/units/assets/test_assets.py @@ -37,19 +37,6 @@ def test_shared_asset() -> None: assert not Path(Path.cwd() / "assets/external").exists() -def test_deprecated_x_asset(capsys) -> None: - """Test that the deprecated asset function raises a warning. - - Args: - capsys: Pytest fixture that captures stdout and stderr. - """ - assert rx.asset("custom_script.js", shared=True) == rx._x.asset("custom_script.js") - assert ( - "DeprecationWarning: rx._x.asset has been deprecated in version 0.6.6" - in capsys.readouterr().out - ) - - @pytest.mark.parametrize( "path,shared", [ diff --git a/tests/units/compiler/test_compiler.py b/tests/units/compiler/test_compiler.py index 22f5c8483..50088e728 100644 --- a/tests/units/compiler/test_compiler.py +++ b/tests/units/compiler/test_compiler.py @@ -92,7 +92,7 @@ def test_compile_import_statement( ), ], ) -def test_compile_imports(import_dict: ParsedImportDict, test_dicts: List[dict]): +def test_compile_imports(import_dict: ParsedImportDict, test_dicts: list[dict]): """Test the compile_imports function. Args: @@ -100,10 +100,10 @@ def test_compile_imports(import_dict: ParsedImportDict, test_dicts: List[dict]): test_dicts: The expected output. """ imports = utils.compile_imports(import_dict) - for import_dict, test_dict in zip(imports, test_dicts): + for import_dict, test_dict in zip(imports, test_dicts, strict=True): assert import_dict["lib"] == test_dict["lib"] assert import_dict["default"] == test_dict["default"] - assert sorted(import_dict["rest"]) == test_dict["rest"] # type: ignore + assert sorted(import_dict["rest"]) == test_dict["rest"] # pyright: ignore [reportArgumentType] def test_compile_stylesheets(tmp_path, mocker): @@ -198,7 +198,7 @@ def test_create_document_root(): assert isinstance(root, utils.Html) assert isinstance(root.children[0], utils.DocumentHead) # Default language. - assert root.lang == "en" # type: ignore + assert root.lang == "en" # pyright: ignore [reportAttributeAccessIssue] # No children in head. assert len(root.children[0].children) == 0 @@ -208,13 +208,13 @@ def test_create_document_root(): utils.NextScript.create(src="bar.js"), ] root = utils.create_document_root( - head_components=comps, # type: ignore + head_components=comps, # pyright: ignore [reportArgumentType] html_lang="rx", html_custom_attrs={"project": "reflex"}, ) # Two children in head. assert isinstance(root, utils.Html) assert len(root.children[0].children) == 2 - assert root.lang == "rx" # type: ignore + assert root.lang == "rx" # pyright: ignore [reportAttributeAccessIssue] assert isinstance(root.custom_attrs, dict) assert root.custom_attrs == {"project": "reflex"} diff --git a/tests/units/components/base/test_bare.py b/tests/units/components/base/test_bare.py index c30ffaf15..178820cff 100644 --- a/tests/units/components/base/test_bare.py +++ b/tests/units/components/base/test_bare.py @@ -13,9 +13,6 @@ STATE_VAR = Var(_js_expr="default_state.name") ("{}", '{"{}"}'), (None, '{""}'), (STATE_VAR, "{default_state.name}"), - # This behavior is now unsupported. - # ("${default_state.name}", "${default_state.name}"), - # ("{state.name}", "{state.name}"), ], ) def test_fstrings(contents, expected): diff --git a/tests/units/components/core/test_banner.py b/tests/units/components/core/test_banner.py index fe6de5eae..e1498d12c 100644 --- a/tests/units/components/core/test_banner.py +++ b/tests/units/components/core/test_banner.py @@ -12,7 +12,7 @@ def test_websocket_target_url(): url = WebsocketTargetURL.create() var_data = url._get_all_var_data() assert var_data is not None - assert sorted(tuple((key for key, _ in var_data.imports))) == sorted( + assert sorted(key for key, _ in var_data.imports) == sorted( ("$/utils/state", "$/env.json") ) @@ -20,7 +20,7 @@ def test_websocket_target_url(): def test_connection_banner(): banner = ConnectionBanner.create() _imports = banner._get_all_imports(collapse=True) - assert sorted(tuple(_imports)) == sorted( + assert sorted(_imports) == sorted( ( "react", "$/utils/context", @@ -38,7 +38,7 @@ def test_connection_banner(): def test_connection_modal(): modal = ConnectionModal.create() _imports = modal._get_all_imports(collapse=True) - assert sorted(tuple(_imports)) == sorted( + assert sorted(_imports) == sorted( ( "react", "$/utils/context", diff --git a/tests/units/components/core/test_colors.py b/tests/units/components/core/test_colors.py index c1295fb41..31cd75b47 100644 --- a/tests/units/components/core/test_colors.py +++ b/tests/units/components/core/test_colors.py @@ -31,37 +31,37 @@ def create_color_var(color): (create_color_var(rx.color("mint", 3)), '"var(--mint-3)"', Color), (create_color_var(rx.color("mint", 3, True)), '"var(--mint-a3)"', Color), ( - create_color_var(rx.color(ColorState.color, ColorState.shade)), # type: ignore + create_color_var(rx.color(ColorState.color, ColorState.shade)), # pyright: ignore [reportArgumentType] f'("var(--"+{color_state_name!s}.color+"-"+(((__to_string) => __to_string.toString())({color_state_name!s}.shade))+")")', Color, ), ( create_color_var( - rx.color(ColorState.color, ColorState.shade, ColorState.alpha) # type: ignore + rx.color(ColorState.color, ColorState.shade, ColorState.alpha) # pyright: ignore [reportArgumentType] ), f'("var(--"+{color_state_name!s}.color+"-"+({color_state_name!s}.alpha ? "a" : "")+(((__to_string) => __to_string.toString())({color_state_name!s}.shade))+")")', Color, ), ( - create_color_var(rx.color(f"{ColorState.color}", f"{ColorState.shade}")), # type: ignore + create_color_var(rx.color(f"{ColorState.color}", f"{ColorState.shade}")), # pyright: ignore [reportArgumentType] f'("var(--"+{color_state_name!s}.color+"-"+{color_state_name!s}.shade+")")', Color, ), ( create_color_var( - rx.color(f"{ColorState.color_part}ato", f"{ColorState.shade}") # type: ignore + rx.color(f"{ColorState.color_part}ato", f"{ColorState.shade}") # pyright: ignore [reportArgumentType] ), f'("var(--"+({color_state_name!s}.color_part+"ato")+"-"+{color_state_name!s}.shade+")")', Color, ), ( - create_color_var(f'{rx.color(ColorState.color, f"{ColorState.shade}")}'), # type: ignore + create_color_var(f"{rx.color(ColorState.color, f'{ColorState.shade}')}"), # pyright: ignore [reportArgumentType] f'("var(--"+{color_state_name!s}.color+"-"+{color_state_name!s}.shade+")")', str, ), ( create_color_var( - f'{rx.color(f"{ColorState.color}", f"{ColorState.shade}")}' # type: ignore + f"{rx.color(f'{ColorState.color}', f'{ColorState.shade}')}" # pyright: ignore [reportArgumentType] ), f'("var(--"+{color_state_name!s}.color+"-"+{color_state_name!s}.shade+")")', str, @@ -81,7 +81,7 @@ def test_color(color, expected, expected_type: Union[Type[str], Type[Color]]): '(true ? "var(--mint-7)" : "var(--tomato-5)")', ), ( - rx.cond(True, rx.color(ColorState.color), rx.color(ColorState.color, 5)), # type: ignore + rx.cond(True, rx.color(ColorState.color), rx.color(ColorState.color, 5)), # pyright: ignore [reportArgumentType, reportCallIssue] f'(true ? ("var(--"+{color_state_name!s}.color+"-7)") : ("var(--"+{color_state_name!s}.color+"-5)"))', ), ( @@ -89,7 +89,7 @@ def test_color(color, expected, expected_type: Union[Type[str], Type[Color]]): "condition", ("first", rx.color("mint")), ("second", rx.color("tomato", 5)), - rx.color(ColorState.color, 2), # type: ignore + rx.color(ColorState.color, 2), # pyright: ignore [reportArgumentType] ), '(() => { switch (JSON.stringify("condition")) {case JSON.stringify("first"): return ("var(--mint-7)");' ' break;case JSON.stringify("second"): return ("var(--tomato-5)"); break;default: ' @@ -98,9 +98,9 @@ def test_color(color, expected, expected_type: Union[Type[str], Type[Color]]): ( rx.match( "condition", - ("first", rx.color(ColorState.color)), # type: ignore - ("second", rx.color(ColorState.color, 5)), # type: ignore - rx.color(ColorState.color, 2), # type: ignore + ("first", rx.color(ColorState.color)), # pyright: ignore [reportArgumentType] + ("second", rx.color(ColorState.color, 5)), # pyright: ignore [reportArgumentType] + rx.color(ColorState.color, 2), # pyright: ignore [reportArgumentType] ), '(() => { switch (JSON.stringify("condition")) {case JSON.stringify("first"): ' f'return (("var(--"+{color_state_name!s}.color+"-7)")); break;case JSON.stringify("second"): ' @@ -133,4 +133,4 @@ def test_radix_color(color, expected): expected (str): The expected custom_style string, radix or literal """ code_block = CodeBlock.create("Hello World", background_color=color) - assert str(code_block.custom_style["backgroundColor"]) == expected # type: ignore + assert str(code_block.custom_style["backgroundColor"]) == expected # pyright: ignore [reportAttributeAccessIssue] diff --git a/tests/units/components/core/test_cond.py b/tests/units/components/core/test_cond.py index 8ad51158e..ac073ed29 100644 --- a/tests/units/components/core/test_cond.py +++ b/tests/units/components/core/test_cond.py @@ -14,7 +14,7 @@ from reflex.vars.base import LiteralVar, Var, computed_var @pytest.fixture def cond_state(request): class CondState(BaseState): - value: request.param["value_type"] = request.param["value"] # noqa + value: request.param["value_type"] = request.param["value"] # pyright: ignore [reportInvalidTypeForm, reportUndefinedVariable] # noqa: F821 return CondState @@ -112,13 +112,13 @@ def test_cond_no_else(): assert isinstance(comp, Fragment) comp = comp.children[0] assert isinstance(comp, Cond) - assert comp.cond._decode() is True # type: ignore - assert comp.comp1.render() == Fragment.create(Text.create("hello")).render() + assert comp.cond._decode() is True + assert comp.comp1.render() == Fragment.create(Text.create("hello")).render() # pyright: ignore [reportOptionalMemberAccess] assert comp.comp2 == Fragment.create() # Props do not support the use of cond without else with pytest.raises(ValueError): - cond(True, "hello") # type: ignore + cond(True, "hello") # pyright: ignore [reportArgumentType] def test_cond_computed_var(): @@ -135,7 +135,7 @@ def test_cond_computed_var(): comp = cond(True, CondStateComputed.computed_int, CondStateComputed.computed_str) - # TODO: shouln't this be a ComputedVar? + # TODO: shouldn't this be a ComputedVar? assert isinstance(comp, Var) state_name = format_state_name(CondStateComputed.get_full_name()) diff --git a/tests/units/components/core/test_foreach.py b/tests/units/components/core/test_foreach.py index 228165d3e..094f6029d 100644 --- a/tests/units/components/core/test_foreach.py +++ b/tests/units/components/core/test_foreach.py @@ -1,8 +1,10 @@ from typing import Dict, List, Set, Tuple, Union +import pydantic.v1 import pytest from reflex import el +from reflex.base import Base from reflex.components.component import Component from reflex.components.core.foreach import ( Foreach, @@ -18,6 +20,12 @@ from reflex.vars.number import NumberVar from reflex.vars.sequence import ArrayVar +class ForEachTag(Base): + """A tag for testing the ForEach component.""" + + name: str = "" + + class ForEachState(BaseState): """A state for testing the ForEach component.""" @@ -46,6 +54,8 @@ class ForEachState(BaseState): bad_annotation_list: list = [["red", "orange"], ["yellow", "blue"]] color_index_tuple: Tuple[int, str] = (0, "red") + default_factory_list: list[ForEachTag] = pydantic.v1.Field(default_factory=list) + class ComponentStateTest(ComponentState): """A test component state.""" @@ -290,3 +300,11 @@ def test_foreach_component_state(): ForEachState.colors_list, ComponentStateTest.create, ) + + +def test_foreach_default_factory(): + """Test that the default factory is called.""" + _ = Foreach.create( + ForEachState.default_factory_list, + lambda tag: text(tag.name), + ) diff --git a/tests/units/components/core/test_html.py b/tests/units/components/core/test_html.py index 79c258dfb..bebb2587d 100644 --- a/tests/units/components/core/test_html.py +++ b/tests/units/components/core/test_html.py @@ -16,7 +16,7 @@ def test_html_many_children(): def test_html_create(): html = Html.create("

Hello !

") - assert str(html.dangerouslySetInnerHTML) == '({ ["__html"] : "

Hello !

" })' # type: ignore + assert str(html.dangerouslySetInnerHTML) == '({ ["__html"] : "

Hello !

" })' # pyright: ignore [reportAttributeAccessIssue] assert ( str(html) == '
Hello !

" })}/>' @@ -32,10 +32,10 @@ def test_html_fstring_create(): html = Html.create(f"

Hello {TestState.myvar}!

") assert ( - str(html.dangerouslySetInnerHTML) # type: ignore + str(html.dangerouslySetInnerHTML) # pyright: ignore [reportAttributeAccessIssue] == f'({{ ["__html"] : ("

Hello "+{TestState.myvar!s}+"!

") }})' ) assert ( str(html) - == f'
' # type: ignore + == f'
' # pyright: ignore [reportAttributeAccessIssue] ) diff --git a/tests/units/components/core/test_match.py b/tests/units/components/core/test_match.py index f09e800e5..11602b77a 100644 --- a/tests/units/components/core/test_match.py +++ b/tests/units/components/core/test_match.py @@ -1,8 +1,9 @@ -from typing import Dict, List, Tuple +from typing import List, Mapping, Tuple import pytest import reflex as rx +from reflex.components.component import Component from reflex.components.core.match import Match from reflex.state import BaseState from reflex.utils.exceptions import MatchTypeError @@ -29,7 +30,9 @@ def test_match_components(): rx.text("default value"), ) match_comp = Match.create(MatchState.value, *match_case_tuples) - match_dict = match_comp.render() # type: ignore + + assert isinstance(match_comp, Component) + match_dict = match_comp.render() assert match_dict["name"] == "Fragment" [match_child] = match_dict["children"] @@ -42,7 +45,7 @@ def test_match_components(): assert match_cases[0][0]._js_expr == "1" assert match_cases[0][0]._var_type is int - first_return_value_render = match_cases[0][1].render() + first_return_value_render = match_cases[0][1] assert first_return_value_render["name"] == "RadixThemesText" assert first_return_value_render["children"][0]["contents"] == '{"first value"}' @@ -50,35 +53,35 @@ def test_match_components(): assert match_cases[1][0]._var_type is int assert match_cases[1][1]._js_expr == "3" assert match_cases[1][1]._var_type is int - second_return_value_render = match_cases[1][2].render() + second_return_value_render = match_cases[1][2] assert second_return_value_render["name"] == "RadixThemesText" assert second_return_value_render["children"][0]["contents"] == '{"second value"}' assert match_cases[2][0]._js_expr == "[1, 2]" assert match_cases[2][0]._var_type == List[int] - third_return_value_render = match_cases[2][1].render() + third_return_value_render = match_cases[2][1] assert third_return_value_render["name"] == "RadixThemesText" assert third_return_value_render["children"][0]["contents"] == '{"third value"}' assert match_cases[3][0]._js_expr == '"random"' assert match_cases[3][0]._var_type is str - fourth_return_value_render = match_cases[3][1].render() + fourth_return_value_render = match_cases[3][1] assert fourth_return_value_render["name"] == "RadixThemesText" assert fourth_return_value_render["children"][0]["contents"] == '{"fourth value"}' assert match_cases[4][0]._js_expr == '({ ["foo"] : "bar" })' - assert match_cases[4][0]._var_type == Dict[str, str] - fifth_return_value_render = match_cases[4][1].render() + assert match_cases[4][0]._var_type == Mapping[str, str] + fifth_return_value_render = match_cases[4][1] assert fifth_return_value_render["name"] == "RadixThemesText" assert fifth_return_value_render["children"][0]["contents"] == '{"fifth value"}' assert match_cases[5][0]._js_expr == f"({MatchState.get_name()}.num + 1)" assert match_cases[5][0]._var_type is int - fifth_return_value_render = match_cases[5][1].render() + fifth_return_value_render = match_cases[5][1] assert fifth_return_value_render["name"] == "RadixThemesText" assert fifth_return_value_render["children"][0]["contents"] == '{"sixth value"}' - default = match_child["default"].render() + default = match_child["default"] assert default["name"] == "RadixThemesText" assert default["children"][0]["contents"] == '{"default value"}' @@ -151,9 +154,10 @@ def test_match_on_component_without_default(): ) match_comp = Match.create(MatchState.value, *match_case_tuples) - default = match_comp.render()["children"][0]["default"] # type: ignore + assert isinstance(match_comp, Component) + default = match_comp.render()["children"][0]["default"] - assert isinstance(default, Fragment) + assert isinstance(default, dict) and default["name"] == Fragment.__name__ def test_match_on_var_no_default(): diff --git a/tests/units/components/core/test_upload.py b/tests/units/components/core/test_upload.py index 710baa161..efade7b63 100644 --- a/tests/units/components/core/test_upload.py +++ b/tests/units/components/core/test_upload.py @@ -5,7 +5,7 @@ from reflex.components.core.upload import ( StyledUpload, Upload, UploadNamespace, - _on_drop_spec, # type: ignore + _on_drop_spec, # pyright: ignore [reportAttributeAccessIssue] cancel_upload, get_upload_url, ) @@ -60,7 +60,7 @@ def test_upload_create(): up_comp_2 = Upload.create( id="foo_id", - on_drop=UploadStateTest.drop_handler([]), # type: ignore + on_drop=UploadStateTest.drop_handler([]), ) assert isinstance(up_comp_2, Upload) assert up_comp_2.is_used @@ -80,7 +80,7 @@ def test_upload_create(): up_comp_4 = Upload.create( id="foo_id", - on_drop=UploadStateTest.not_drop_handler([]), # type: ignore + on_drop=UploadStateTest.not_drop_handler([]), ) assert isinstance(up_comp_4, Upload) assert up_comp_4.is_used @@ -96,7 +96,7 @@ def test_styled_upload_create(): styled_up_comp_2 = StyledUpload.create( id="foo_id", - on_drop=UploadStateTest.drop_handler([]), # type: ignore + on_drop=UploadStateTest.drop_handler([]), ) assert isinstance(styled_up_comp_2, StyledUpload) assert styled_up_comp_2.is_used @@ -116,7 +116,7 @@ def test_styled_upload_create(): styled_up_comp_4 = StyledUpload.create( id="foo_id", - on_drop=UploadStateTest.not_drop_handler([]), # type: ignore + on_drop=UploadStateTest.not_drop_handler([]), ) assert isinstance(styled_up_comp_4, StyledUpload) assert styled_up_comp_4.is_used diff --git a/tests/units/components/datadisplay/conftest.py b/tests/units/components/datadisplay/conftest.py index 13c571c8c..188e887c4 100644 --- a/tests/units/components/datadisplay/conftest.py +++ b/tests/units/components/datadisplay/conftest.py @@ -1,7 +1,5 @@ """Data display component tests fixtures.""" -from typing import List - import pandas as pd import pytest @@ -54,11 +52,11 @@ def data_table_state3(): """ class DataTableState(BaseState): - _data: List = [] - _columns: List = ["col1", "col2"] + _data: list = [] + _columns: list = ["col1", "col2"] @rx.var - def data(self) -> List: + def data(self) -> list: return self._data @rx.var @@ -77,15 +75,15 @@ def data_table_state4(): """ class DataTableState(BaseState): - _data: List = [] - _columns: List = ["col1", "col2"] + _data: list = [] + _columns: list[str] = ["col1", "col2"] @rx.var def data(self): return self._data @rx.var - def columns(self) -> List: + def columns(self) -> list: return self._columns return DataTableState diff --git a/tests/units/components/datadisplay/test_code.py b/tests/units/components/datadisplay/test_code.py index 6b7168756..db0120fe1 100644 --- a/tests/units/components/datadisplay/test_code.py +++ b/tests/units/components/datadisplay/test_code.py @@ -10,4 +10,4 @@ from reflex.components.datadisplay.code import CodeBlock, Theme def test_code_light_dark_theme(theme, expected): code_block = CodeBlock.create(theme=theme) - assert code_block.theme._js_expr == expected # type: ignore + assert code_block.theme._js_expr == expected # pyright: ignore [reportAttributeAccessIssue] diff --git a/tests/units/components/datadisplay/test_datatable.py b/tests/units/components/datadisplay/test_datatable.py index b3d31ea32..2dece464a 100644 --- a/tests/units/components/datadisplay/test_datatable.py +++ b/tests/units/components/datadisplay/test_datatable.py @@ -4,6 +4,7 @@ import pytest import reflex as rx from reflex.components.gridjs.datatable import DataTable from reflex.utils import types +from reflex.utils.exceptions import UntypedComputedVarError from reflex.utils.serializers import serialize, serialize_dataframe @@ -13,7 +14,8 @@ from reflex.utils.serializers import serialize, serialize_dataframe pytest.param( { "data": pd.DataFrame( - [["foo", "bar"], ["foo1", "bar1"]], columns=["column1", "column2"] + [["foo", "bar"], ["foo1", "bar1"]], + columns=["column1", "column2"], # pyright: ignore [reportArgumentType] ) }, "data", @@ -75,17 +77,17 @@ def test_invalid_props(props): [ ( "data_table_state2", - "Annotation of the computed var assigned to the data field should be provided.", + "Computed var 'data' must have a type annotation.", True, ), ( "data_table_state3", - "Annotation of the computed var assigned to the column field should be provided.", + "Computed var 'columns' must have a type annotation.", False, ), ( "data_table_state4", - "Annotation of the computed var assigned to the data field should be provided.", + "Computed var 'data' must have a type annotation.", False, ), ], @@ -99,7 +101,7 @@ def test_computed_var_without_annotation(fixture, request, err_msg, is_data_fram err_msg: expected error message. is_data_frame: whether data field is a pandas dataframe. """ - with pytest.raises(ValueError) as err: + with pytest.raises(UntypedComputedVarError) as err: if is_data_frame: DataTable.create(data=request.getfixturevalue(fixture).data) else: @@ -113,7 +115,8 @@ def test_computed_var_without_annotation(fixture, request, err_msg, is_data_fram def test_serialize_dataframe(): """Test if dataframe is serialized correctly.""" df = pd.DataFrame( - [["foo", "bar"], ["foo1", "bar1"]], columns=["column1", "column2"] + [["foo", "bar"], ["foo1", "bar1"]], + columns=["column1", "column2"], # pyright: ignore [reportArgumentType] ) value = serialize(df) assert value == serialize_dataframe(df) diff --git a/tests/units/components/datadisplay/test_shiki_code.py b/tests/units/components/datadisplay/test_shiki_code.py index eb473ba06..cc05c35b0 100644 --- a/tests/units/components/datadisplay/test_shiki_code.py +++ b/tests/units/components/datadisplay/test_shiki_code.py @@ -95,7 +95,7 @@ def test_create_shiki_code_block( # Test that the first child is the code code_block_component = component.children[0] - assert code_block_component.code._var_value == expected_first_child # type: ignore + assert code_block_component.code._var_value == expected_first_child # pyright: ignore [reportAttributeAccessIssue] applied_styles = component.style for key, value in expected_styles.items(): @@ -128,12 +128,12 @@ def test_create_shiki_high_level_code_block( # Test that the first child is the code block component code_block_component = component.children[0] - assert code_block_component.code._var_value == children[0] # type: ignore + assert code_block_component.code._var_value == children[0] # pyright: ignore [reportAttributeAccessIssue] # Check if the transformer is set correctly if expected if expected_transformers: exp_trans_names = [t.__name__ for t in expected_transformers] - for transformer in code_block_component.transformers._var_value: # type: ignore + for transformer in code_block_component.transformers._var_value: # pyright: ignore [reportAttributeAccessIssue] assert type(transformer).__name__ in exp_trans_names # Check if the second child is the copy button if can_copy is True @@ -161,12 +161,12 @@ def test_shiki_high_level_code_block_theme_language_mapping(children, props): if "theme" in props: assert component.children[ 0 - ].theme._var_value == ShikiHighLevelCodeBlock._map_themes(props["theme"]) # type: ignore + ].theme._var_value == ShikiHighLevelCodeBlock._map_themes(props["theme"]) # pyright: ignore [reportAttributeAccessIssue] # Test that the language is mapped correctly if "language" in props: assert component.children[ 0 - ].language._var_value == ShikiHighLevelCodeBlock._map_languages( # type: ignore + ].language._var_value == ShikiHighLevelCodeBlock._map_languages( # pyright: ignore [reportAttributeAccessIssue] props["language"] ) diff --git a/tests/units/components/forms/test_form.py b/tests/units/components/forms/test_form.py index 5f3ba2d37..69b5e7b63 100644 --- a/tests/units/components/forms/test_form.py +++ b/tests/units/components/forms/test_form.py @@ -10,7 +10,7 @@ def test_render_on_submit(): _var_type=EventChain, ) f = Form.create(on_submit=submit_it) - exp_submit_name = f"handleSubmit_{f.handle_submit_unique_name}" # type: ignore + exp_submit_name = f"handleSubmit_{f.handle_submit_unique_name}" # pyright: ignore [reportAttributeAccessIssue] assert f"onSubmit={{{exp_submit_name}}}" in f.render()["props"] diff --git a/tests/units/components/lucide/test_icon.py b/tests/units/components/lucide/test_icon.py index b0a3475dd..19bea7a7f 100644 --- a/tests/units/components/lucide/test_icon.py +++ b/tests/units/components/lucide/test_icon.py @@ -1,13 +1,19 @@ import pytest -from reflex.components.lucide.icon import LUCIDE_ICON_LIST, Icon +from reflex.components.lucide.icon import ( + LUCIDE_ICON_LIST, + LUCIDE_ICON_MAPPING_OVERRIDE, + Icon, +) from reflex.utils import format @pytest.mark.parametrize("tag", LUCIDE_ICON_LIST) def test_icon(tag): icon = Icon.create(tag) - assert icon.alias == f"Lucide{format.to_title_case(tag)}Icon" + assert icon.alias == "Lucide" + LUCIDE_ICON_MAPPING_OVERRIDE.get( + tag, f"{format.to_title_case(tag)}Icon" + ) def test_icon_missing_tag(): diff --git a/tests/units/components/media/test_image.py b/tests/units/components/media/test_image.py index f8618347c..519ca735e 100644 --- a/tests/units/components/media/test_image.py +++ b/tests/units/components/media/test_image.py @@ -4,7 +4,7 @@ import pytest from PIL.Image import Image as Img import reflex as rx -from reflex.components.next.image import Image # type: ignore +from reflex.components.next.image import Image from reflex.utils.serializers import serialize, serialize_image from reflex.vars.sequence import StringVar @@ -17,7 +17,7 @@ def pil_image() -> Img: A random PIL image. """ imarray = np.random.rand(100, 100, 3) * 255 - return PIL.Image.fromarray(imarray.astype("uint8")).convert("RGBA") # type: ignore + return PIL.Image.fromarray(imarray.astype("uint8")).convert("RGBA") # pyright: ignore [reportAttributeAccessIssue] def test_serialize_image(pil_image: Img): @@ -36,13 +36,13 @@ def test_set_src_str(): """Test that setting the src works.""" image = rx.image(src="pic2.jpeg") # when using next/image, we explicitly create a _var_is_str Var - assert str(image.src) in ( # type: ignore + assert str(image.src) in ( # pyright: ignore [reportAttributeAccessIssue] '"pic2.jpeg"', "'pic2.jpeg'", "`pic2.jpeg`", ) # For plain rx.el.img, an explicit var is not created, so the quoting happens later - # assert str(image.src) == "pic2.jpeg" # type: ignore + # assert str(image.src) == "pic2.jpeg" #noqa: ERA001 def test_set_src_img(pil_image: Img): @@ -52,7 +52,7 @@ def test_set_src_img(pil_image: Img): pil_image: The image to serialize. """ image = Image.create(src=pil_image) - assert str(image.src._js_expr) == '"' + serialize_image(pil_image) + '"' # type: ignore + assert str(image.src._js_expr) == '"' + serialize_image(pil_image) + '"' # pyright: ignore [reportAttributeAccessIssue] def test_render(pil_image: Img): @@ -62,4 +62,4 @@ def test_render(pil_image: Img): pil_image: The image to serialize. """ image = Image.create(src=pil_image) - assert isinstance(image.src, StringVar) # type: ignore + assert isinstance(image.src, StringVar) # pyright: ignore [reportAttributeAccessIssue] diff --git a/tests/units/components/test_component.py b/tests/units/components/test_component.py index e2b035a8f..8cffa6e0e 100644 --- a/tests/units/components/test_component.py +++ b/tests/units/components/test_component.py @@ -19,6 +19,7 @@ from reflex.constants import EventTriggers from reflex.event import ( EventChain, EventHandler, + JavascriptInputEvent, input_event, no_args_event_spec, parse_args_spec, @@ -27,10 +28,15 @@ from reflex.event import ( from reflex.state import BaseState from reflex.style import Style from reflex.utils import imports -from reflex.utils.exceptions import EventFnArgMismatch +from reflex.utils.exceptions import ( + ChildrenTypeError, + EventFnArgMismatchError, + EventHandlerArgTypeMismatchError, +) from reflex.utils.imports import ImportDict, ImportVar, ParsedImportDict, parse_imports from reflex.vars import VarData from reflex.vars.base import LiteralVar, Var +from reflex.vars.object import ObjectVar @pytest.fixture @@ -94,11 +100,14 @@ def component2() -> Type[Component]: A test component. """ + def on_prop_event_spec(e0: Any): + return [e0] + class TestComponent2(Component): # A test list prop. arr: Var[List[str]] - on_prop_event: EventHandler[lambda e0: [e0]] + on_prop_event: EventHandler[on_prop_event_spec] def get_event_triggers(self) -> Dict[str, Any]: """Test controlled triggers. @@ -444,8 +453,8 @@ def test_add_style(component1, component2): component1: Style({"color": "white"}), component2: Style({"color": "black"}), } - c1 = component1()._add_style_recursive(style) # type: ignore - c2 = component2()._add_style_recursive(style) # type: ignore + c1 = component1()._add_style_recursive(style) + c2 = component2()._add_style_recursive(style) assert str(c1.style["color"]) == '"white"' assert str(c2.style["color"]) == '"black"' @@ -461,8 +470,8 @@ def test_add_style_create(component1, component2): component1.create: Style({"color": "white"}), component2.create: Style({"color": "black"}), } - c1 = component1()._add_style_recursive(style) # type: ignore - c2 = component2()._add_style_recursive(style) # type: ignore + c1 = component1()._add_style_recursive(style) + c2 = component2()._add_style_recursive(style) assert str(c1.style["color"]) == '"white"' assert str(c2.style["color"]) == '"black"' @@ -645,14 +654,17 @@ def test_create_filters_none_props(test_component): assert str(component.style["text-align"]) == '"center"' -@pytest.mark.parametrize("children", [((None,),), ("foo", ("bar", (None,)))]) +@pytest.mark.parametrize( + "children", + [ + ((None,),), + ("foo", ("bar", (None,))), + ({"foo": "bar"},), + ], +) def test_component_create_unallowed_types(children, test_component): - with pytest.raises(TypeError) as err: + with pytest.raises(ChildrenTypeError): test_component.create(*children) - assert ( - err.value.args[0] - == "Children of Reflex components must be other components, state vars, or primitive Python types. Got child None of type ." - ) @pytest.mark.parametrize( @@ -815,10 +827,14 @@ def test_component_create_unpack_tuple_child(test_component, element, expected): assert fragment_wrapper.render() == expected +class _Obj(Base): + custom: int = 0 + + class C1State(BaseState): """State for testing C1 component.""" - def mock_handler(self, _e, _bravo, _charlie): + def mock_handler(self, _e: JavascriptInputEvent, _bravo: dict, _charlie: _Obj): """Mock handler.""" pass @@ -826,11 +842,13 @@ class C1State(BaseState): def test_component_event_trigger_arbitrary_args(): """Test that we can define arbitrary types for the args of an event trigger.""" - class Obj(Base): - custom: int = 0 - - def on_foo_spec(_e, alpha: str, bravo: Dict[str, Any], charlie: Obj): - return [_e.target.value, bravo["nested"], charlie.custom + 42] + def on_foo_spec( + _e: ObjectVar[JavascriptInputEvent], + alpha: Var[str], + bravo: dict[str, Any], + charlie: ObjectVar[_Obj], + ): + return [_e.target.value, bravo["nested"], charlie.custom.to(int) + 42] class C1(Component): library = "/local" @@ -842,13 +860,7 @@ def test_component_event_trigger_arbitrary_args(): "on_foo": on_foo_spec, } - comp = C1.create(on_foo=C1State.mock_handler) - - assert comp.render()["props"][0] == ( - "onFoo={((__e, _alpha, _bravo, _charlie) => (addEvents(" - f'[(Event("{C1State.get_full_name()}.mock_handler", ({{ ["_e"] : __e["target"]["value"], ["_bravo"] : _bravo["nested"], ["_charlie"] : (_charlie["custom"] + 42) }}), ({{ }})))], ' - "[__e, _alpha, _bravo, _charlie], ({ }))))}" - ) + C1.create(on_foo=C1State.mock_handler) def test_create_custom_component(my_component): @@ -905,30 +917,29 @@ def test_invalid_event_handler_args(component2, test_state): test_state: A test state. """ # EventHandler args must match - with pytest.raises(EventFnArgMismatch): + with pytest.raises(EventFnArgMismatchError): component2.create(on_click=test_state.do_something_arg) # Multiple EventHandler args: all must match - with pytest.raises(EventFnArgMismatch): + with pytest.raises(EventFnArgMismatchError): component2.create( on_click=[test_state.do_something_arg, test_state.do_something] ) - # Enable when 0.7.0 happens # # Event Handler types must match - # with pytest.raises(EventHandlerArgTypeMismatch): - # component2.create( - # on_user_visited_count_changed=test_state.do_something_with_bool - # ) - # with pytest.raises(EventHandlerArgTypeMismatch): - # component2.create(on_user_list_changed=test_state.do_something_with_int) - # with pytest.raises(EventHandlerArgTypeMismatch): - # component2.create(on_user_list_changed=test_state.do_something_with_list_int) + with pytest.raises(EventHandlerArgTypeMismatchError): + component2.create( + on_user_visited_count_changed=test_state.do_something_with_bool + ) + with pytest.raises(EventHandlerArgTypeMismatchError): + component2.create(on_user_list_changed=test_state.do_something_with_int) + with pytest.raises(EventHandlerArgTypeMismatchError): + component2.create(on_user_list_changed=test_state.do_something_with_list_int) - # component2.create(on_open=test_state.do_something_with_int) - # component2.create(on_open=test_state.do_something_with_bool) - # component2.create(on_user_visited_count_changed=test_state.do_something_with_int) - # component2.create(on_user_list_changed=test_state.do_something_with_list_str) + component2.create(on_open=test_state.do_something_with_int) + component2.create(on_open=test_state.do_something_with_bool) + component2.create(on_user_visited_count_changed=test_state.do_something_with_int) + component2.create(on_user_list_changed=test_state.do_something_with_list_str) # lambda cannot return weird values. with pytest.raises(ValueError): @@ -941,15 +952,15 @@ def test_invalid_event_handler_args(component2, test_state): ) # lambda signature must match event trigger. - with pytest.raises(EventFnArgMismatch): + with pytest.raises(EventFnArgMismatchError): component2.create(on_click=lambda _: test_state.do_something_arg(1)) # lambda returning EventHandler must match spec - with pytest.raises(EventFnArgMismatch): + with pytest.raises(EventFnArgMismatchError): component2.create(on_click=lambda: test_state.do_something_arg) # Mixed EventSpec and EventHandler must match spec. - with pytest.raises(EventFnArgMismatch): + with pytest.raises(EventFnArgMismatchError): component2.create( on_click=lambda: [ test_state.do_something_arg(1), @@ -1318,7 +1329,7 @@ class EventState(rx.State): ), pytest.param( rx.fragment(class_name=[TEST_VAR, "other-class"]), - [LiteralVar.create([TEST_VAR, "other-class"]).join(" ")], + [Var.create([TEST_VAR, "other-class"]).join(" ")], id="fstring-dual-class_name", ), pytest.param( @@ -1353,17 +1364,17 @@ class EventState(rx.State): id="fstring-background_color", ), pytest.param( - rx.fragment(style={"background_color": TEST_VAR}), # type: ignore + rx.fragment(style={"background_color": TEST_VAR}), # pyright: ignore [reportArgumentType] [STYLE_VAR], id="direct-style-background_color", ), pytest.param( - rx.fragment(style={"background_color": f"foo{TEST_VAR}bar"}), # type: ignore + rx.fragment(style={"background_color": f"foo{TEST_VAR}bar"}), # pyright: ignore [reportArgumentType] [STYLE_VAR], id="fstring-style-background_color", ), pytest.param( - rx.fragment(on_click=EVENT_CHAIN_VAR), # type: ignore + rx.fragment(on_click=EVENT_CHAIN_VAR), [EVENT_CHAIN_VAR], id="direct-event-chain", ), @@ -1373,17 +1384,17 @@ class EventState(rx.State): id="direct-event-handler", ), pytest.param( - rx.fragment(on_click=EventState.handler2(TEST_VAR)), # type: ignore + rx.fragment(on_click=EventState.handler2(TEST_VAR)), # pyright: ignore [reportCallIssue] [ARG_VAR, TEST_VAR], id="direct-event-handler-arg", ), pytest.param( - rx.fragment(on_click=EventState.handler2(EventState.v)), # type: ignore + rx.fragment(on_click=EventState.handler2(EventState.v)), # pyright: ignore [reportCallIssue] [ARG_VAR, EventState.v], id="direct-event-handler-arg2", ), pytest.param( - rx.fragment(on_click=lambda: EventState.handler2(TEST_VAR)), # type: ignore + rx.fragment(on_click=lambda: EventState.handler2(TEST_VAR)), # pyright: ignore [reportCallIssue] [ARG_VAR, TEST_VAR], id="direct-event-handler-lambda", ), @@ -1436,9 +1447,8 @@ def test_get_vars(component, exp_vars): for comp_var, exp_var in zip( comp_vars, sorted(exp_vars, key=lambda v: v._js_expr), + strict=True, ): - # print(str(comp_var), str(exp_var)) - # print(comp_var._get_all_var_data(), exp_var._get_all_var_data()) assert comp_var.equals(exp_var) @@ -1473,7 +1483,7 @@ def test_instantiate_all_components(): comp_name for submodule_list in component_nested_list for comp_name in submodule_list - ]: # type: ignore + ]: if component_name in untested_components: continue component = getattr( @@ -1546,11 +1556,11 @@ def test_validate_valid_children(): ) valid_component1( - rx.cond( # type: ignore + rx.cond( True, rx.fragment(valid_component2()), rx.fragment( - rx.foreach(LiteralVar.create([1, 2, 3]), lambda x: valid_component2(x)) # type: ignore + rx.foreach(LiteralVar.create([1, 2, 3]), lambda x: valid_component2(x)) ), ) ) @@ -1605,12 +1615,12 @@ def test_validate_valid_parents(): ) valid_component2( - rx.cond( # type: ignore + rx.cond( True, rx.fragment(valid_component3()), rx.fragment( rx.foreach( - LiteralVar.create([1, 2, 3]), # type: ignore + LiteralVar.create([1, 2, 3]), lambda x: valid_component2(valid_component3(x)), ) ), @@ -1673,13 +1683,13 @@ def test_validate_invalid_children(): with pytest.raises(ValueError): valid_component4( - rx.cond( # type: ignore + rx.cond( True, rx.fragment(invalid_component()), rx.fragment( rx.foreach( LiteralVar.create([1, 2, 3]), lambda x: invalid_component(x) - ) # type: ignore + ) ), ) ) @@ -1800,21 +1810,15 @@ def test_custom_component_declare_event_handlers_in_fields(): """ return { **super().get_event_triggers(), - "on_a": lambda e0: [e0], "on_b": input_event, - "on_c": lambda e0: [], "on_d": lambda: [], "on_e": lambda: [], - "on_f": lambda a, b, c: [c, b, a], } class TestComponent(Component): - on_a: EventHandler[lambda e0: [e0]] on_b: EventHandler[input_event] - on_c: EventHandler[no_args_event_spec] on_d: EventHandler[no_args_event_spec] on_e: EventHandler - on_f: EventHandler[lambda a, b, c: [c, b, a]] custom_component = ReferenceComponent.create() test_component = TestComponent.create() @@ -1825,6 +1829,7 @@ def test_custom_component_declare_event_handlers_in_fields(): for v1, v2 in zip( parse_args_spec(test_triggers[trigger_name]), parse_args_spec(custom_triggers[trigger_name]), + strict=True, ): assert v1.equals(v2) @@ -1866,7 +1871,7 @@ def test_invalid_event_trigger(): ) def test_component_add_imports(tags): class BaseComponent(Component): - def _get_imports(self) -> ImportDict: + def _get_imports(self) -> ImportDict: # pyright: ignore [reportIncompatibleMethodOverride] return {} class Reference(Component): @@ -1878,7 +1883,7 @@ def test_component_add_imports(tags): ) class TestBase(Component): - def add_imports( + def add_imports( # pyright: ignore [reportIncompatibleMethodOverride] self, ) -> Dict[str, Union[str, ImportVar, List[str], List[ImportVar]]]: return {"foo": "bar"} @@ -1910,7 +1915,7 @@ def test_component_add_hooks(): pass class GrandchildComponent1(ChildComponent1): - def add_hooks(self): + def add_hooks(self): # pyright: ignore [reportIncompatibleMethodOverride] return [ "const hook2 = 43", "const hook3 = 44", @@ -1923,11 +1928,11 @@ def test_component_add_hooks(): ] class GrandchildComponent2(ChildComponent1): - def _get_hooks(self): + def _get_hooks(self): # pyright: ignore [reportIncompatibleMethodOverride] return "const hook5 = 46" class GreatGrandchildComponent2(GrandchildComponent2): - def add_hooks(self): + def add_hooks(self): # pyright: ignore [reportIncompatibleMethodOverride] return [ "const hook2 = 43", "const hook6 = 47", @@ -2002,7 +2007,7 @@ def test_component_add_custom_code(): ] class GrandchildComponent2(ChildComponent1): - def _get_custom_code(self): + def _get_custom_code(self): # pyright: ignore [reportIncompatibleMethodOverride] return "const custom_code5 = 46" class GreatGrandchildComponent2(GrandchildComponent2): @@ -2098,11 +2103,11 @@ def test_add_style_embedded_vars(test_state: BaseState): test_state: A test state. """ v0 = LiteralVar.create("parent")._replace( - merge_var_data=VarData(hooks={"useParent": None}), # type: ignore + merge_var_data=VarData(hooks={"useParent": None}), ) v1 = rx.color("plum", 10) v2 = LiteralVar.create("text")._replace( - merge_var_data=VarData(hooks={"useText": None}), # type: ignore + merge_var_data=VarData(hooks={"useText": None}), ) class ParentComponent(Component): @@ -2116,7 +2121,7 @@ def test_add_style_embedded_vars(test_state: BaseState): class StyledComponent(ParentComponent): tag = "StyledComponent" - def add_style(self): + def add_style(self): # pyright: ignore [reportIncompatibleMethodOverride] return { "color": v1, "fake": v2, diff --git a/tests/units/components/typography/test_markdown.py b/tests/units/components/typography/test_markdown.py index 5e9abbb1f..12f3b0dbe 100644 --- a/tests/units/components/typography/test_markdown.py +++ b/tests/units/components/typography/test_markdown.py @@ -29,8 +29,8 @@ def test_get_component(tag, expected): expected: The expected component. """ md = Markdown.create("# Hello") - assert tag in md.component_map # type: ignore - assert md.get_component(tag).tag == expected # type: ignore + assert tag in md.component_map # pyright: ignore [reportAttributeAccessIssue] + assert md.get_component(tag).tag == expected def test_set_component_map(): @@ -42,8 +42,8 @@ def test_set_component_map(): md = Markdown.create("# Hello", component_map=component_map) # Check that the new tags have been added. - assert md.get_component("h1").tag == "Box" # type: ignore - assert md.get_component("p").tag == "Box" # type: ignore + assert md.get_component("h1").tag == "Box" + assert md.get_component("p").tag == "Box" # Make sure the old tags are still there. - assert md.get_component("h2").tag == "Heading" # type: ignore + assert md.get_component("h2").tag == "Heading" diff --git a/tests/units/conftest.py b/tests/units/conftest.py index 2f619a941..2ee290ea3 100644 --- a/tests/units/conftest.py +++ b/tests/units/conftest.py @@ -1,11 +1,8 @@ """Test fixtures.""" import asyncio -import contextlib -import os import platform import uuid -from pathlib import Path from typing import Dict, Generator, Type from unittest import mock @@ -14,6 +11,7 @@ import pytest from reflex.app import App from reflex.event import EventSpec from reflex.model import ModelRegistry +from reflex.testing import chdir from reflex.utils import prerequisites from .states import ( @@ -97,7 +95,7 @@ def upload_sub_state_event_spec(): Returns: Event Spec. """ - return EventSpec(handler=SubUploadState.handle_upload, upload=True) # type: ignore + return EventSpec(handler=SubUploadState.handle_upload, upload=True) # pyright: ignore [reportCallIssue] @pytest.fixture @@ -107,7 +105,7 @@ def upload_event_spec(): Returns: Event Spec. """ - return EventSpec(handler=UploadState.handle_upload1, upload=True) # type: ignore + return EventSpec(handler=UploadState.handle_upload1, upload=True) # pyright: ignore [reportCallIssue] @pytest.fixture @@ -145,7 +143,7 @@ def sqlite_db_config_values(base_db_config_values) -> Dict: @pytest.fixture -def router_data_headers() -> Dict[str, str]: +def router_data_headers() -> dict[str, str]: """Router data headers. Returns: @@ -172,7 +170,7 @@ def router_data_headers() -> Dict[str, str]: @pytest.fixture -def router_data(router_data_headers) -> Dict[str, str]: +def router_data(router_data_headers: dict[str, str]) -> dict[str, str | dict]: """Router data. Args: @@ -181,7 +179,7 @@ def router_data(router_data_headers) -> Dict[str, str]: Returns: Dict of router data. """ - return { # type: ignore + return { "pathname": "/", "query": {}, "token": "b181904c-3953-4a79-dc18-ae9518c22f05", @@ -191,33 +189,6 @@ def router_data(router_data_headers) -> Dict[str, str]: } -# borrowed from py3.11 -class chdir(contextlib.AbstractContextManager): - """Non thread-safe context manager to change the current working directory.""" - - def __init__(self, path): - """Prepare contextmanager. - - Args: - path: the path to change to - """ - self.path = path - self._old_cwd = [] - - def __enter__(self): - """Save current directory and perform chdir.""" - self._old_cwd.append(Path(".").resolve()) - os.chdir(self.path) - - def __exit__(self, *excinfo): - """Change back to previous directory on stack. - - Args: - excinfo: sys.exc_info captured in the context block - """ - os.chdir(self._old_cwd.pop()) - - @pytest.fixture def tmp_working_dir(tmp_path): """Create a temporary directory and chdir to it. diff --git a/tests/units/middleware/test_hydrate_middleware.py b/tests/units/middleware/test_hydrate_middleware.py index 9ee8d8d25..7b02f8515 100644 --- a/tests/units/middleware/test_hydrate_middleware.py +++ b/tests/units/middleware/test_hydrate_middleware.py @@ -41,7 +41,7 @@ async def test_preprocess_no_events(hydrate_middleware, event1, mocker): mocker.patch("reflex.state.State.class_subclasses", {TestState}) state = State() update = await hydrate_middleware.preprocess( - app=App(state=State), + app=App(_state=State), event=event1, state=state, ) diff --git a/tests/units/states/mutation.py b/tests/units/states/mutation.py index b05f558a1..ad658bbd0 100644 --- a/tests/units/states/mutation.py +++ b/tests/units/states/mutation.py @@ -18,7 +18,7 @@ class DictMutationTestState(BaseState): def add_age(self): """Add an age to the dict.""" - self.details.update({"age": 20}) # type: ignore + self.details.update({"age": 20}) # pyright: ignore [reportCallIssue, reportArgumentType] def change_name(self): """Change the name in the dict.""" diff --git a/tests/units/states/upload.py b/tests/units/states/upload.py index 338025bcd..66d9479b4 100644 --- a/tests/units/states/upload.py +++ b/tests/units/states/upload.py @@ -61,14 +61,13 @@ class FileUploadState(State): """ for file in files: upload_data = await file.read() - outfile = f"{self._tmp_path}/{file.filename}" + assert file.filename is not None + outfile = self._tmp_path / file.filename # Save the file. - with open(outfile, "wb") as file_object: - file_object.write(upload_data) + outfile.write_bytes(upload_data) # Update the img var. - assert file.filename is not None self.img_list.append(file.filename) @rx.event(background=True) @@ -109,14 +108,13 @@ class ChildFileUploadState(FileStateBase1): """ for file in files: upload_data = await file.read() - outfile = f"{self._tmp_path}/{file.filename}" + assert file.filename is not None + outfile = self._tmp_path / file.filename # Save the file. - with open(outfile, "wb") as file_object: - file_object.write(upload_data) + outfile.write_bytes(upload_data) # Update the img var. - assert file.filename is not None self.img_list.append(file.filename) @rx.event(background=True) @@ -157,14 +155,13 @@ class GrandChildFileUploadState(FileStateBase2): """ for file in files: upload_data = await file.read() - outfile = f"{self._tmp_path}/{file.filename}" + assert file.filename is not None + outfile = self._tmp_path / file.filename # Save the file. - with open(outfile, "wb") as file_object: - file_object.write(upload_data) + outfile.write_bytes(upload_data) # Update the img var. - assert file.filename is not None self.img_list.append(file.filename) @rx.event(background=True) diff --git a/tests/units/test_app.py b/tests/units/test_app.py index 7c7455cba..058174a1b 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -133,7 +133,7 @@ def test_model() -> Type[Model]: A default model. """ - class TestModel(Model, table=True): # type: ignore + class TestModel(Model, table=True): pass return TestModel @@ -147,7 +147,7 @@ def test_model_auth() -> Type[Model]: A default model. """ - class TestModelAuth(Model, table=True): # type: ignore + class TestModelAuth(Model, table=True): """A test model with auth.""" pass @@ -185,19 +185,19 @@ def test_custom_auth_admin() -> Type[AuthProvider]: login_path: str = "/login" logout_path: str = "/logout" - def login(self): + def login(self): # pyright: ignore [reportIncompatibleMethodOverride] """Login.""" pass - def is_authenticated(self): + def is_authenticated(self): # pyright: ignore [reportIncompatibleMethodOverride] """Is authenticated.""" pass - def get_admin_user(self): + def get_admin_user(self): # pyright: ignore [reportIncompatibleMethodOverride] """Get admin user.""" pass - def logout(self): + def logout(self): # pyright: ignore [reportIncompatibleMethodOverride] """Logout.""" pass @@ -236,14 +236,14 @@ def test_add_page_default_route(app: App, index_page, about_page): index_page: The index page. about_page: The about page. """ - assert app.pages == {} - assert app.unevaluated_pages == {} + assert app._pages == {} + assert app._unevaluated_pages == {} app.add_page(index_page) app._compile_page("index") - assert app.pages.keys() == {"index"} + assert app._pages.keys() == {"index"} app.add_page(about_page) app._compile_page("about") - assert app.pages.keys() == {"index", "about"} + assert app._pages.keys() == {"index", "about"} def test_add_page_set_route(app: App, index_page, windows_platform: bool): @@ -255,10 +255,10 @@ def test_add_page_set_route(app: App, index_page, windows_platform: bool): windows_platform: Whether the system is windows. """ route = "test" if windows_platform else "/test" - assert app.unevaluated_pages == {} + assert app._unevaluated_pages == {} app.add_page(index_page, route=route) app._compile_page("test") - assert app.pages.keys() == {"test"} + assert app._pages.keys() == {"test"} def test_add_page_set_route_dynamic(index_page, windows_platform: bool): @@ -268,18 +268,18 @@ def test_add_page_set_route_dynamic(index_page, windows_platform: bool): index_page: The index page. windows_platform: Whether the system is windows. """ - app = App(state=EmptyState) - assert app.state is not None + app = App(_state=EmptyState) + assert app._state is not None route = "/test/[dynamic]" - assert app.unevaluated_pages == {} + assert app._unevaluated_pages == {} app.add_page(index_page, route=route) app._compile_page("test/[dynamic]") - assert app.pages.keys() == {"test/[dynamic]"} - assert "dynamic" in app.state.computed_vars - assert app.state.computed_vars["dynamic"]._deps(objclass=EmptyState) == { - constants.ROUTER + assert app._pages.keys() == {"test/[dynamic]"} + assert "dynamic" in app._state.computed_vars + assert app._state.computed_vars["dynamic"]._deps(objclass=EmptyState) == { + EmptyState.get_full_name(): {constants.ROUTER}, } - assert constants.ROUTER in app.state()._computed_var_dependencies + assert constants.ROUTER in app._state()._var_dependencies def test_add_page_set_route_nested(app: App, index_page, windows_platform: bool): @@ -291,9 +291,9 @@ def test_add_page_set_route_nested(app: App, index_page, windows_platform: bool) windows_platform: Whether the system is windows. """ route = "test\\nested" if windows_platform else "/test/nested" - assert app.unevaluated_pages == {} + assert app._unevaluated_pages == {} app.add_page(index_page, route=route) - assert app.unevaluated_pages.keys() == {route.strip(os.path.sep)} + assert app._unevaluated_pages.keys() == {route.strip(os.path.sep)} def test_add_page_invalid_api_route(app: App, index_page): @@ -413,13 +413,13 @@ async def test_initialize_with_state(test_state: Type[ATestState], token: str): test_state: The default state. token: a Token. """ - app = App(state=test_state) - assert app.state == test_state + app = App(_state=test_state) + assert app._state == test_state # Get a state for a given token. state = await app.state_manager.get_state(_substate_key(token, test_state)) assert isinstance(state, test_state) - assert state.var == 0 # type: ignore + assert state.var == 0 if isinstance(app.state_manager, StateManagerRedis): await app.state_manager.close() @@ -432,7 +432,7 @@ async def test_set_and_get_state(test_state): Args: test_state: The default state. """ - app = App(state=test_state) + app = App(_state=test_state) # Create two tokens. token1 = str(uuid.uuid4()) + f"_{test_state.get_full_name()}" @@ -441,8 +441,8 @@ async def test_set_and_get_state(test_state): # Get the default state for each token. state1 = await app.state_manager.get_state(token1) state2 = await app.state_manager.get_state(token2) - assert state1.var == 0 # type: ignore - assert state2.var == 0 # type: ignore + assert state1.var == 0 + assert state2.var == 0 # Set the vars to different values. state1.var = 1 @@ -453,8 +453,8 @@ async def test_set_and_get_state(test_state): # Get the states again and check the values. state1 = await app.state_manager.get_state(token1) state2 = await app.state_manager.get_state(token2) - assert state1.var == 1 # type: ignore - assert state2.var == 2 # type: ignore + assert state1.var == 1 + assert state2.var == 2 if isinstance(app.state_manager, StateManagerRedis): await app.state_manager.close() @@ -469,17 +469,17 @@ async def test_dynamic_var_event(test_state: Type[ATestState], token: str): test_state: State Fixture. token: a Token. """ - state = test_state() # type: ignore + state = test_state() # pyright: ignore [reportCallIssue] state.add_var("int_val", int, 0) - result = await state._process( + async for result in state._process( Event( token=token, name=f"{test_state.get_name()}.set_int_val", router_data={"pathname": "/", "query": {}}, payload={"value": 50}, ) - ).__anext__() - assert result.delta == {test_state.get_name(): {"int_val": 50}} + ): + assert result.delta == {test_state.get_name(): {"int_val": 50}} @pytest.mark.asyncio @@ -583,18 +583,17 @@ async def test_list_mutation_detection__plain_list( token: a Token. """ for event_name, expected_delta in event_tuples: - result = await list_mutation_state._process( + async for result in list_mutation_state._process( Event( token=token, name=f"{list_mutation_state.get_name()}.{event_name}", router_data={"pathname": "/", "query": {}}, payload={}, ) - ).__anext__() - - # prefix keys in expected_delta with the state name - expected_delta = {list_mutation_state.get_name(): expected_delta} - assert result.delta == expected_delta + ): + # prefix keys in expected_delta with the state name + expected_delta = {list_mutation_state.get_name(): expected_delta} + assert result.delta == expected_delta @pytest.mark.asyncio @@ -709,19 +708,18 @@ async def test_dict_mutation_detection__plain_list( token: a Token. """ for event_name, expected_delta in event_tuples: - result = await dict_mutation_state._process( + async for result in dict_mutation_state._process( Event( token=token, name=f"{dict_mutation_state.get_name()}.{event_name}", router_data={"pathname": "/", "query": {}}, payload={}, ) - ).__anext__() + ): + # prefix keys in expected_delta with the state name + expected_delta = {dict_mutation_state.get_name(): expected_delta} - # prefix keys in expected_delta with the state name - expected_delta = {dict_mutation_state.get_name(): expected_delta} - - assert result.delta == expected_delta + assert result.delta == expected_delta @pytest.mark.asyncio @@ -772,7 +770,7 @@ async def test_upload_file(tmp_path, state, delta, token: str, mocker): # The App state must be the "root" of the state tree app = App() app._enable_state() - app.event_namespace.emit = AsyncMock() # type: ignore + app.event_namespace.emit = AsyncMock() # pyright: ignore [reportOptionalMemberAccess] current_state = await app.state_manager.get_state(_substate_key(token, state)) data = b"This is binary data" @@ -795,7 +793,7 @@ async def test_upload_file(tmp_path, state, delta, token: str, mocker): file=bio, ) upload_fn = upload(app) - streaming_response = await upload_fn(request_mock, [file1, file2]) + streaming_response = await upload_fn(request_mock, [file1, file2]) # pyright: ignore [reportFunctionMemberAccess] async for state_update in streaming_response.body_iterator: assert ( state_update @@ -827,7 +825,7 @@ async def test_upload_file_without_annotation(state, tmp_path, token): token: a Token. """ state._tmp_path = tmp_path - app = App(state=State) + app = App(_state=State) request_mock = unittest.mock.Mock() request_mock.headers = { @@ -861,7 +859,7 @@ async def test_upload_file_background(state, tmp_path, token): token: a Token. """ state._tmp_path = tmp_path - app = App(state=State) + app = App(_state=State) request_mock = unittest.mock.Mock() request_mock.headers = { @@ -899,8 +897,6 @@ class DynamicState(BaseState): loaded: int = 0 counter: int = 0 - # side_effect_counter: int = 0 - def on_load(self): """Event handler for page on_load, should trigger for all navigation events.""" self.loaded = self.loaded + 1 @@ -910,17 +906,16 @@ class DynamicState(BaseState): """Increment the counter var.""" self.counter = self.counter + 1 - @computed_var(cache=True) + @computed_var def comp_dynamic(self) -> str: """A computed var that depends on the dynamic var. Returns: same as self.dynamic """ - # self.side_effect_counter = self.side_effect_counter + 1 return self.dynamic - on_load_internal = OnLoadInternalState.on_load_internal.fn + on_load_internal = OnLoadInternalState.on_load_internal.fn # pyright: ignore [reportFunctionMemberAccess] def test_dynamic_arg_shadow( @@ -941,10 +936,10 @@ def test_dynamic_arg_shadow( """ arg_name = "counter" route = f"/test/[{arg_name}]" - app = app_module_mock.app = App(state=DynamicState) - assert app.state is not None + app = app_module_mock.app = App(_state=DynamicState) + assert app._state is not None with pytest.raises(NameError): - app.add_page(index_page, route=route, on_load=DynamicState.on_load) # type: ignore + app.add_page(index_page, route=route, on_load=DynamicState.on_load) def test_multiple_dynamic_args( @@ -966,7 +961,7 @@ def test_multiple_dynamic_args( arg_name = "my_arg" route = f"/test/[{arg_name}]" route2 = f"/test2/[{arg_name}]" - app = app_module_mock.app = App(state=EmptyState) + app = app_module_mock.app = App(_state=EmptyState) app.add_page(index_page, route=route) app.add_page(index_page, route=route2) @@ -993,16 +988,16 @@ async def test_dynamic_route_var_route_change_completed_on_load( """ arg_name = "dynamic" route = f"/test/[{arg_name}]" - app = app_module_mock.app = App(state=DynamicState) - assert app.state is not None - assert arg_name not in app.state.vars - app.add_page(index_page, route=route, on_load=DynamicState.on_load) # type: ignore - assert arg_name in app.state.vars - assert arg_name in app.state.computed_vars - assert app.state.computed_vars[arg_name]._deps(objclass=DynamicState) == { - constants.ROUTER + app = app_module_mock.app = App(_state=DynamicState) + assert app._state is not None + assert arg_name not in app._state.vars + app.add_page(index_page, route=route, on_load=DynamicState.on_load) + assert arg_name in app._state.vars + assert arg_name in app._state.computed_vars + assert app._state.computed_vars[arg_name]._deps(objclass=DynamicState) == { + DynamicState.get_full_name(): {constants.ROUTER}, } - assert constants.ROUTER in app.state()._computed_var_dependencies + assert constants.ROUTER in app._state()._var_dependencies substate_token = _substate_key(token, DynamicState) sid = "mock_sid" @@ -1025,7 +1020,7 @@ async def test_dynamic_route_var_route_change_completed_on_load( def _dynamic_state_event(name, val, **kwargs): return _event( - name=format.format_event_handler(getattr(DynamicState, name)), # type: ignore + name=format.format_event_handler(getattr(DynamicState, name)), val=val, **kwargs, ) @@ -1059,7 +1054,6 @@ async def test_dynamic_route_var_route_change_completed_on_load( arg_name: exp_val, f"comp_{arg_name}": exp_val, constants.CompileVars.IS_HYDRATED: False, - # "side_effect_counter": exp_index, "router": exp_router, } }, @@ -1155,8 +1149,6 @@ async def test_dynamic_route_var_route_change_completed_on_load( state = await app.state_manager.get_state(substate_token) assert state.loaded == len(exp_vals) assert state.counter == len(exp_vals) - # print(f"Expected {exp_vals} rendering side effects, got {state.side_effect_counter}") - # assert state.side_effect_counter == len(exp_vals) if isinstance(app.state_manager, StateManagerRedis): await app.state_manager.close() @@ -1180,7 +1172,7 @@ async def test_process_events(mocker, token: str): "headers": {}, "ip": "127.0.0.1", } - app = App(state=GenState) + app = App(_state=GenState) mocker.patch.object(app, "_postprocess", AsyncMock()) event = Event( @@ -1196,7 +1188,7 @@ async def test_process_events(mocker, token: str): pass assert (await app.state_manager.get_state(event.substate_token)).value == 5 - assert app._postprocess.call_count == 6 + assert app._postprocess.call_count == 6 # pyright: ignore [reportFunctionMemberAccess] if isinstance(app.state_manager, StateManagerRedis): await app.state_manager.close() @@ -1226,13 +1218,13 @@ def test_overlay_component( overlay_component: The overlay_component to pass to App. exp_page_child: The type of the expected child in the page fragment. """ - app = App(state=state, overlay_component=overlay_component) + app = App(_state=state, overlay_component=overlay_component) app._setup_overlay_component() if exp_page_child is None: assert app.overlay_component is None elif isinstance(exp_page_child, OverlayFragment): assert app.overlay_component is not None - generated_component = app._generate_component(app.overlay_component) # type: ignore + generated_component = app._generate_component(app.overlay_component) assert isinstance(generated_component, OverlayFragment) assert isinstance( generated_component.children[0], @@ -1241,7 +1233,7 @@ def test_overlay_component( else: assert app.overlay_component is not None assert isinstance( - app._generate_component(app.overlay_component), # type: ignore + app._generate_component(app.overlay_component), exp_page_child, ) @@ -1249,12 +1241,12 @@ def test_overlay_component( # overlay components are wrapped during compile only app._compile_page("test") app._setup_overlay_component() - page = app.pages["test"] + page = app._pages["test"] if exp_page_child is not None: assert len(page.children) == 3 children_types = (type(child) for child in page.children) - assert exp_page_child in children_types + assert exp_page_child in children_types # pyright: ignore [reportOperatorIssue] else: assert len(page.children) == 2 @@ -1282,12 +1274,23 @@ def compilable_app(tmp_path) -> Generator[tuple[App, Path], None, None]: yield app, web_dir -def test_app_wrap_compile_theme(compilable_app: tuple[App, Path]): +@pytest.mark.parametrize( + "react_strict_mode", + [True, False], +) +def test_app_wrap_compile_theme( + react_strict_mode: bool, compilable_app: tuple[App, Path], mocker +): """Test that the radix theme component wraps the app. Args: + react_strict_mode: Whether to use React Strict Mode. compilable_app: compilable_app fixture. + mocker: pytest mocker object. """ + conf = rx.Config(app_name="testing", react_strict_mode=react_strict_mode) + mocker.patch("reflex.config._get_config", return_value=conf) + app, web_dir = compilable_app app.theme = rx.theme(accent_color="plum") app._compile() @@ -1298,42 +1301,55 @@ def test_app_wrap_compile_theme(compilable_app: tuple[App, Path]): assert ( "function AppWrap({children}) {" "return (" - "" + + ("" if react_strict_mode else "") + + "" "" "" "{children}" "" "" "" - ")" + + ("" if react_strict_mode else "") + + ")" "}" ) in "".join(app_js_lines) -def test_app_wrap_priority(compilable_app: tuple[App, Path]): +@pytest.mark.parametrize( + "react_strict_mode", + [True, False], +) +def test_app_wrap_priority( + react_strict_mode: bool, compilable_app: tuple[App, Path], mocker +): """Test that the app wrap components are wrapped in the correct order. Args: + react_strict_mode: Whether to use React Strict Mode. compilable_app: compilable_app fixture. + mocker: pytest mocker object. """ + conf = rx.Config(app_name="testing", react_strict_mode=react_strict_mode) + mocker.patch("reflex.config._get_config", return_value=conf) + app, web_dir = compilable_app class Fragment1(Component): tag = "Fragment1" - def _get_app_wrap_components(self) -> dict[tuple[int, str], Component]: + def _get_app_wrap_components(self) -> dict[tuple[int, str], Component]: # pyright: ignore [reportIncompatibleMethodOverride] return {(99, "Box"): rx.box()} class Fragment2(Component): tag = "Fragment2" - def _get_app_wrap_components(self) -> dict[tuple[int, str], Component]: + def _get_app_wrap_components(self) -> dict[tuple[int, str], Component]: # pyright: ignore [reportIncompatibleMethodOverride] return {(50, "Text"): rx.text()} class Fragment3(Component): tag = "Fragment3" - def _get_app_wrap_components(self) -> dict[tuple[int, str], Component]: + def _get_app_wrap_components(self) -> dict[tuple[int, str], Component]: # pyright: ignore [reportIncompatibleMethodOverride] return {(10, "Fragment2"): Fragment2.create()} def page(): @@ -1347,8 +1363,7 @@ def test_app_wrap_priority(compilable_app: tuple[App, Path]): ] assert ( "function AppWrap({children}) {" - "return (" - "" + "return (" + ("" if react_strict_mode else "") + "" '' "" "" @@ -1358,8 +1373,7 @@ def test_app_wrap_priority(compilable_app: tuple[App, Path]): "" "" "" - "" - ")" + "" + ("" if react_strict_mode else "") + ")" "}" ) in "".join(app_js_lines) @@ -1367,52 +1381,52 @@ def test_app_wrap_priority(compilable_app: tuple[App, Path]): def test_app_state_determination(): """Test that the stateless status of an app is determined correctly.""" a1 = App() - assert a1.state is None + assert a1._state is None # No state, no router, no event handlers. a1.add_page(rx.box("Index"), route="/") - assert a1.state is None + assert a1._state is None # Add a page with `on_load` enables state. a1.add_page(rx.box("About"), route="/about", on_load=rx.console_log("")) a1._compile_page("about") - assert a1.state is not None + assert a1._state is not None a2 = App() - assert a2.state is None + assert a2._state is None # Referencing a state Var enables state. a2.add_page(rx.box(rx.text(GenState.value)), route="/") a2._compile_page("index") - assert a2.state is not None + assert a2._state is not None a3 = App() - assert a3.state is None + assert a3._state is None # Referencing router enables state. a3.add_page(rx.box(rx.text(State.router.page.full_path)), route="/") a3._compile_page("index") - assert a3.state is not None + assert a3._state is not None a4 = App() - assert a4.state is None + assert a4._state is None a4.add_page(rx.box(rx.button("Click", on_click=rx.console_log(""))), route="/") - assert a4.state is None + assert a4._state is None a4.add_page( rx.box(rx.button("Click", on_click=DynamicState.on_counter)), route="/page2" ) a4._compile_page("page2") - assert a4.state is not None + assert a4._state is not None def test_raise_on_state(): """Test that the state is set.""" # state kwargs is deprecated, we just make sure the app is created anyway. - _app = App(state=State) - assert _app.state is not None - assert issubclass(_app.state, State) + _app = App(_state=State) + assert _app._state is not None + assert issubclass(_app._state, State) def test_call_app(): @@ -1454,11 +1468,11 @@ def test_generate_component(): "Bar", ) - comp = App._generate_component(index) # type: ignore + comp = App._generate_component(index) assert isinstance(comp, Component) with pytest.raises(exceptions.MatchTypeError): - App._generate_component(index_mismatch) # type: ignore + App._generate_component(index_mismatch) # pyright: ignore [reportArgumentType] def test_add_page_component_returning_tuple(): @@ -1473,27 +1487,27 @@ def test_add_page_component_returning_tuple(): def page2(): return (rx.text("third"),) - app.add_page(index) # type: ignore - app.add_page(page2) # type: ignore + app.add_page(index) # pyright: ignore [reportArgumentType] + app.add_page(page2) # pyright: ignore [reportArgumentType] app._compile_page("index") app._compile_page("page2") - fragment_wrapper = app.pages["index"].children[0] + fragment_wrapper = app._pages["index"].children[0] assert isinstance(fragment_wrapper, Fragment) first_text = fragment_wrapper.children[0] assert isinstance(first_text, Text) - assert str(first_text.children[0].contents) == '"first"' # type: ignore + assert str(first_text.children[0].contents) == '"first"' # pyright: ignore [reportAttributeAccessIssue] second_text = fragment_wrapper.children[1] assert isinstance(second_text, Text) - assert str(second_text.children[0].contents) == '"second"' # type: ignore + assert str(second_text.children[0].contents) == '"second"' # pyright: ignore [reportAttributeAccessIssue] # Test page with trailing comma. - page2_fragment_wrapper = app.pages["page2"].children[0] + page2_fragment_wrapper = app._pages["page2"].children[0] assert isinstance(page2_fragment_wrapper, Fragment) third_text = page2_fragment_wrapper.children[0] assert isinstance(third_text, Text) - assert str(third_text.children[0].contents) == '"third"' # type: ignore + assert str(third_text.children[0].contents) == '"third"' # pyright: ignore [reportAttributeAccessIssue] @pytest.mark.parametrize("export", (True, False)) @@ -1531,7 +1545,7 @@ def test_app_with_transpile_packages(compilable_app: tuple[App, Path], export: b next_config = (web_dir / "next.config.js").read_text() transpile_packages_match = re.search(r"transpilePackages: (\[.*?\])", next_config) - transpile_packages_json = transpile_packages_match.group(1) # type: ignore + transpile_packages_json = transpile_packages_match.group(1) # pyright: ignore [reportOptionalMemberAccess] transpile_packages = sorted(json.loads(transpile_packages_json)) assert transpile_packages == [ @@ -1555,15 +1569,25 @@ def test_app_with_valid_var_dependencies(compilable_app: tuple[App, Path]): base: int = 0 _backend: int = 0 - @computed_var(cache=True) + @computed_var() def foo(self) -> str: return "foo" - @computed_var(deps=["_backend", "base", foo], cache=True) + @computed_var(deps=["_backend", "base", foo]) def bar(self) -> str: return "bar" - app.state = ValidDepState + class Child1(ValidDepState): + @computed_var(deps=["base", ValidDepState.bar]) + def other(self) -> str: + return "other" + + class Child2(ValidDepState): + @computed_var(deps=["base", Child1.other]) + def other(self) -> str: + return "other" + + app._state = ValidDepState app._compile() @@ -1571,11 +1595,11 @@ def test_app_with_invalid_var_dependencies(compilable_app: tuple[App, Path]): app, _ = compilable_app class InvalidDepState(BaseState): - @computed_var(deps=["foolksjdf"], cache=True) + @computed_var(deps=["foolksjdf"]) def bar(self) -> str: return "bar" - app.state = InvalidDepState + app._state = InvalidDepState with pytest.raises(exceptions.VarDependencyError): app._compile() diff --git a/tests/units/test_attribute_access_type.py b/tests/units/test_attribute_access_type.py index 0d490ec1e..d08c17c8c 100644 --- a/tests/units/test_attribute_access_type.py +++ b/tests/units/test_attribute_access_type.py @@ -3,11 +3,19 @@ from __future__ import annotations from typing import Dict, List, Optional, Type, Union import attrs +import pydantic.v1 import pytest import sqlalchemy +import sqlmodel from sqlalchemy import JSON, TypeDecorator from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship +from sqlalchemy.orm import ( + DeclarativeBase, + Mapped, + MappedAsDataclass, + mapped_column, + relationship, +) import reflex as rx from reflex.utils.types import GenericType, get_attribute_access_type @@ -53,6 +61,10 @@ class SQLALabel(SQLABase): id: Mapped[int] = mapped_column(primary_key=True) test_id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey("test.id")) test: Mapped[SQLAClass] = relationship(back_populates="labels") + test_dataclass_id: Mapped[int] = mapped_column( + sqlalchemy.ForeignKey("test_dataclass.id") + ) + test_dataclass: Mapped[SQLAClassDataclass] = relationship(back_populates="labels") class SQLAClass(SQLABase): @@ -104,9 +116,64 @@ class SQLAClass(SQLABase): return self.labels[0] if self.labels else None +class SQLAClassDataclass(MappedAsDataclass, SQLABase): + """Test sqlalchemy model.""" + + id: Mapped[int] = mapped_column(primary_key=True) + no_default: Mapped[int] = mapped_column(nullable=True) + count: Mapped[int] = mapped_column() + name: Mapped[str] = mapped_column() + int_list: Mapped[List[int]] = mapped_column( + sqlalchemy.types.ARRAY(item_type=sqlalchemy.INTEGER) + ) + str_list: Mapped[List[str]] = mapped_column( + sqlalchemy.types.ARRAY(item_type=sqlalchemy.String) + ) + optional_int: Mapped[Optional[int]] = mapped_column(nullable=True) + sqla_tag_id: Mapped[int] = mapped_column(sqlalchemy.ForeignKey(SQLATag.id)) + sqla_tag: Mapped[Optional[SQLATag]] = relationship() + labels: Mapped[List[SQLALabel]] = relationship(back_populates="test_dataclass") + # do not use lower case dict here! + # https://github.com/sqlalchemy/sqlalchemy/issues/9902 + dict_str_str: Mapped[Dict[str, str]] = mapped_column() + default_factory: Mapped[List[int]] = mapped_column( + sqlalchemy.types.ARRAY(item_type=sqlalchemy.INTEGER), + default_factory=list, + ) + __tablename__: str = "test_dataclass" + + @property + def str_property(self) -> str: + """String property. + + Returns: + Name attribute + """ + return self.name + + @hybrid_property + def str_or_int_property(self) -> Union[str, int]: + """String or int property. + + Returns: + Name attribute + """ + return self.name + + @hybrid_property + def first_label(self) -> Optional[SQLALabel]: + """First label property. + + Returns: + First label + """ + return self.labels[0] if self.labels else None + + class ModelClass(rx.Model): """Test reflex model.""" + no_default: Optional[int] = sqlmodel.Field(nullable=True) count: int = 0 name: str = "test" int_list: List[int] = [] @@ -115,6 +182,7 @@ class ModelClass(rx.Model): sqla_tag: Optional[SQLATag] = None labels: List[SQLALabel] = [] dict_str_str: Dict[str, str] = {} + default_factory: List[int] = sqlmodel.Field(default_factory=list) @property def str_property(self) -> str: @@ -147,6 +215,7 @@ class ModelClass(rx.Model): class BaseClass(rx.Base): """Test rx.Base class.""" + no_default: Optional[int] = pydantic.v1.Field(required=False) count: int = 0 name: str = "test" int_list: List[int] = [] @@ -155,6 +224,7 @@ class BaseClass(rx.Base): sqla_tag: Optional[SQLATag] = None labels: List[SQLALabel] = [] dict_str_str: Dict[str, str] = {} + default_factory: List[int] = pydantic.v1.Field(default_factory=list) @property def str_property(self) -> str: @@ -236,6 +306,7 @@ class AttrClass: sqla_tag: Optional[SQLATag] = None labels: List[SQLALabel] = [] dict_str_str: Dict[str, str] = {} + default_factory: List[int] = attrs.field(factory=list) @property def str_property(self) -> str: @@ -265,27 +336,17 @@ class AttrClass: return self.labels[0] if self.labels else None -@pytest.fixture( - params=[ +@pytest.mark.parametrize( + "cls", + [ SQLAClass, + SQLAClassDataclass, BaseClass, BareClass, ModelClass, AttrClass, - ] + ], ) -def cls(request: pytest.FixtureRequest) -> type: - """Fixture for the class to test. - - Args: - request: pytest request object. - - Returns: - Class to test. - """ - return request.param - - @pytest.mark.parametrize( "attr, expected", [ @@ -311,3 +372,38 @@ def test_get_attribute_access_type(cls: type, attr: str, expected: GenericType) expected: Expected type. """ assert get_attribute_access_type(cls, attr) == expected + + +@pytest.mark.parametrize( + "cls", + [ + SQLAClassDataclass, + BaseClass, + ModelClass, + AttrClass, + ], +) +def test_get_attribute_access_type_default_factory(cls: type) -> None: + """Test get_attribute_access_type returns the correct type for default factory fields. + + Args: + cls: Class to test. + """ + assert get_attribute_access_type(cls, "default_factory") == List[int] + + +@pytest.mark.parametrize( + "cls", + [ + SQLAClassDataclass, + BaseClass, + ModelClass, + ], +) +def test_get_attribute_access_type_no_default(cls: type) -> None: + """Test get_attribute_access_type returns the correct type for fields with no default which are not required. + + Args: + cls: Class to test. + """ + assert get_attribute_access_type(cls, "no_default") == Optional[int] diff --git a/tests/units/test_config.py b/tests/units/test_config.py index e5d4622bd..88d8b5f2f 100644 --- a/tests/units/test_config.py +++ b/tests/units/test_config.py @@ -21,7 +21,7 @@ from reflex.constants import Endpoint, Env def test_requires_app_name(): """Test that a config requires an app_name.""" with pytest.raises(ValueError): - rx.Config() # type: ignore + rx.Config() def test_set_app_name(base_config_values): @@ -207,7 +207,7 @@ def test_replace_defaults( exp_config_values: The expected config values. """ mock_os_env = os.environ.copy() - monkeypatch.setattr(reflex.config.os, "environ", mock_os_env) # type: ignore + monkeypatch.setattr(reflex.config.os, "environ", mock_os_env) mock_os_env.update({k: str(v) for k, v in env_vars.items()}) c = rx.Config(app_name="a", **config_kwargs) c._set_persistent(**set_persistent_vars) diff --git a/tests/units/test_db_config.py b/tests/units/test_db_config.py index b8d7c07cb..5b716e6bb 100644 --- a/tests/units/test_db_config.py +++ b/tests/units/test_db_config.py @@ -164,7 +164,7 @@ def test_constructor_postgresql(username, password, host, port, database, expect "localhost", 5432, "db", - "postgresql+psycopg2://user:pass@localhost:5432/db", + "postgresql+psycopg://user:pass@localhost:5432/db", ), ( "user", @@ -172,17 +172,17 @@ def test_constructor_postgresql(username, password, host, port, database, expect "localhost", None, "db", - "postgresql+psycopg2://user@localhost/db", + "postgresql+psycopg://user@localhost/db", ), - ("user", "", "", None, "db", "postgresql+psycopg2://user@/db"), - ("", "", "localhost", 5432, "db", "postgresql+psycopg2://localhost:5432/db"), - ("", "", "", None, "db", "postgresql+psycopg2:///db"), + ("user", "", "", None, "db", "postgresql+psycopg://user@/db"), + ("", "", "localhost", 5432, "db", "postgresql+psycopg://localhost:5432/db"), + ("", "", "", None, "db", "postgresql+psycopg:///db"), ], ) -def test_constructor_postgresql_psycopg2( +def test_constructor_postgresql_psycopg( username, password, host, port, database, expected_url ): - """Test DBConfig.postgresql_psycopg2 constructor creates the instance correctly. + """Test DBConfig.postgresql_psycopg constructor creates the instance correctly. Args: username: Database username. @@ -192,10 +192,10 @@ def test_constructor_postgresql_psycopg2( database: Database name. expected_url: Expected database URL generated. """ - db_config = DBConfig.postgresql_psycopg2( + db_config = DBConfig.postgresql_psycopg( username=username, password=password, host=host, port=port, database=database ) - assert db_config.engine == "postgresql+psycopg2" + assert db_config.engine == "postgresql+psycopg" assert db_config.username == username assert db_config.password == password assert db_config.host == host diff --git a/tests/units/test_event.py b/tests/units/test_event.py index 4f7adfeb3..afcfda504 100644 --- a/tests/units/test_event.py +++ b/tests/units/test_event.py @@ -3,6 +3,7 @@ from typing import Callable, List import pytest import reflex as rx +from reflex.constants.compiler import Hooks, Imports from reflex.event import ( Event, EventChain, @@ -14,7 +15,7 @@ from reflex.event import ( ) from reflex.state import BaseState from reflex.utils import format -from reflex.vars.base import Field, LiteralVar, Var, field +from reflex.vars.base import Field, LiteralVar, Var, VarData, field def make_var(value) -> Var: @@ -72,7 +73,7 @@ def test_call_event_handler(): ) # Passing args as strings should format differently. - event_spec = handler("first", "second") # type: ignore + event_spec = handler("first", "second") assert ( format.format_event(event_spec) == 'Event("test_fn_with_args", {arg1:"first",arg2:"second"})' @@ -80,7 +81,7 @@ def test_call_event_handler(): first, second = 123, "456" handler = EventHandler(fn=test_fn_with_args) - event_spec = handler(first, second) # type: ignore + event_spec = handler(first, second) assert ( format.format_event(event_spec) == 'Event("test_fn_with_args", {arg1:123,arg2:"456"})' @@ -94,7 +95,7 @@ def test_call_event_handler(): handler = EventHandler(fn=test_fn_with_args) with pytest.raises(TypeError): - handler(test_fn) # type: ignore + handler(test_fn) def test_call_event_handler_partial(): @@ -199,20 +200,15 @@ def test_event_redirect(input, output): input: The input for running the test. output: The expected output to validate the test. """ - path, external, replace = input + path, is_external, replace = input kwargs = {} - if external is not None: - kwargs["external"] = external + if is_external is not None: + kwargs["is_external"] = is_external if replace is not None: kwargs["replace"] = replace spec = event.redirect(path, **kwargs) assert isinstance(spec, EventSpec) assert spec.handler.fn.__qualname__ == "_redirect" - - # this asserts need comment about what it's testing (they fail with Var as input) - # assert spec.args[0][0].equals(Var(_js_expr="path")) - # assert spec.args[0][1].equals(Var(_js_expr="/path")) - assert format.format_event(spec) == output @@ -227,12 +223,17 @@ def test_event_console_log(): ) assert ( format.format_event(spec) - == 'Event("_call_function", {function:(() => (console["log"]("message")))})' + == 'Event("_call_function", {function:(() => (console["log"]("message"))),callback:null})' ) spec = event.console_log(Var(_js_expr="message")) assert ( format.format_event(spec) - == 'Event("_call_function", {function:(() => (console["log"](message)))})' + == 'Event("_call_function", {function:(() => (console["log"](message))),callback:null})' + ) + spec2 = event.console_log(Var(_js_expr="message2")).add_args(Var("throwaway")) + assert ( + format.format_event(spec2) + == 'Event("_call_function", {function:(() => (console["log"](message2))),callback:null})' ) @@ -247,12 +248,17 @@ def test_event_window_alert(): ) assert ( format.format_event(spec) - == 'Event("_call_function", {function:(() => (window["alert"]("message")))})' + == 'Event("_call_function", {function:(() => (window["alert"]("message"))),callback:null})' ) spec = event.window_alert(Var(_js_expr="message")) assert ( format.format_event(spec) - == 'Event("_call_function", {function:(() => (window["alert"](message)))})' + == 'Event("_call_function", {function:(() => (window["alert"](message))),callback:null})' + ) + spec2 = event.window_alert(Var(_js_expr="message2")).add_args(Var("throwaway")) + assert ( + format.format_event(spec2) + == 'Event("_call_function", {function:(() => (window["alert"](message2))),callback:null})' ) @@ -411,7 +417,7 @@ def test_event_actions_on_state(): assert isinstance(handler, EventHandler) assert not handler.event_actions - sp_handler = EventActionState.handler.stop_propagation + sp_handler = EventActionState.handler.stop_propagation # pyright: ignore [reportFunctionMemberAccess] assert sp_handler.event_actions == {"stopPropagation": True} # should NOT affect other references to the handler assert not handler.event_actions @@ -438,9 +444,28 @@ def test_event_var_data(): return (value,) # Ensure chain carries _var_data - chain_var = Var.create(EventChain(events=[S.s(S.x)], args_spec=_args_spec)) + chain_var = Var.create( + EventChain( + events=[S.s(S.x)], + args_spec=_args_spec, + invocation=rx.vars.FunctionStringVar.create(""), + ) + ) assert chain_var._get_all_var_data() == S.x._get_all_var_data() + chain_var_data = Var.create( + EventChain( + events=[], + args_spec=_args_spec, + ) + )._get_all_var_data() + assert chain_var_data is not None + + assert chain_var_data == VarData( + imports=Imports.EVENTS, + hooks={Hooks.EVENTS: None}, + ) + def test_event_bound_method() -> None: class S(BaseState): diff --git a/tests/units/test_health_endpoint.py b/tests/units/test_health_endpoint.py index fe350266f..5b3aedc00 100644 --- a/tests/units/test_health_endpoint.py +++ b/tests/units/test_health_endpoint.py @@ -15,11 +15,11 @@ from reflex.utils.prerequisites import get_redis_status "mock_redis_client, expected_status", [ # Case 1: Redis client is available and responds to ping - (Mock(ping=lambda: None), True), + (Mock(ping=lambda: None), {"redis": True}), # Case 2: Redis client raises RedisError - (Mock(ping=lambda: (_ for _ in ()).throw(RedisError)), False), + (Mock(ping=lambda: (_ for _ in ()).throw(RedisError)), {"redis": False}), # Case 3: Redis client is not used - (None, None), + (None, {"redis": None}), ], ) async def test_get_redis_status(mock_redis_client, expected_status, mocker): @@ -41,12 +41,12 @@ async def test_get_redis_status(mock_redis_client, expected_status, mocker): "mock_engine, execute_side_effect, expected_status", [ # Case 1: Database is accessible - (MagicMock(), None, True), + (MagicMock(), None, {"db": True}), # Case 2: Database connection error (OperationalError) ( MagicMock(), sqlalchemy.exc.OperationalError("error", "error", "error"), - False, + {"db": False}, ), ], ) @@ -74,33 +74,57 @@ async def test_get_db_status(mock_engine, execute_side_effect, expected_status, @pytest.mark.asyncio @pytest.mark.parametrize( - "db_status, redis_status, expected_status, expected_code", + "db_enabled, redis_enabled, db_status, redis_status, expected_status, expected_code", [ # Case 1: Both services are connected - (True, True, {"status": True, "db": True, "redis": True}, 200), + (True, True, True, True, {"status": True, "db": True, "redis": True}, 200), # Case 2: Database not connected, Redis connected - (False, True, {"status": False, "db": False, "redis": True}, 503), + (True, True, False, True, {"status": False, "db": False, "redis": True}, 503), # Case 3: Database connected, Redis not connected - (True, False, {"status": False, "db": True, "redis": False}, 503), + (True, True, True, False, {"status": False, "db": True, "redis": False}, 503), # Case 4: Both services not connected - (False, False, {"status": False, "db": False, "redis": False}, 503), + (True, True, False, False, {"status": False, "db": False, "redis": False}, 503), # Case 5: Database Connected, Redis not used - (True, None, {"status": True, "db": True, "redis": False}, 200), + (True, False, True, None, {"status": True, "db": True}, 200), + # Case 6: Database not used, Redis Connected + (False, True, None, True, {"status": True, "redis": True}, 200), + # Case 7: Both services not used + (False, False, None, None, {"status": True}, 200), ], ) -async def test_health(db_status, redis_status, expected_status, expected_code, mocker): +async def test_health( + db_enabled, + redis_enabled, + db_status, + redis_status, + expected_status, + expected_code, + mocker, +): # Mock get_db_status and get_redis_status - mocker.patch("reflex.app.get_db_status", return_value=db_status) mocker.patch( - "reflex.utils.prerequisites.get_redis_status", return_value=redis_status + "reflex.utils.prerequisites.check_db_used", + return_value=db_enabled, + ) + mocker.patch( + "reflex.utils.prerequisites.check_redis_used", + return_value=redis_enabled, + ) + mocker.patch( + "reflex.app.get_db_status", + return_value={"db": db_status}, + ) + mocker.patch( + "reflex.utils.prerequisites.get_redis_status", + return_value={"redis": redis_status}, ) # Call the async health function response = await health() - print(json.loads(response.body)) + print(json.loads(response.body)) # pyright: ignore [reportArgumentType] print(expected_status) # Verify the response content and status code assert response.status_code == expected_code - assert json.loads(response.body) == expected_status + assert json.loads(response.body) == expected_status # pyright: ignore [reportArgumentType] diff --git a/tests/units/test_model.py b/tests/units/test_model.py index ac8187e03..b17538248 100644 --- a/tests/units/test_model.py +++ b/tests/units/test_model.py @@ -46,7 +46,7 @@ def test_default_primary_key(model_default_primary: Model): Args: model_default_primary: Fixture. """ - assert "id" in model_default_primary.__class__.__fields__ + assert "id" in type(model_default_primary).__fields__ def test_custom_primary_key(model_custom_primary: Model): @@ -55,7 +55,7 @@ def test_custom_primary_key(model_custom_primary: Model): Args: model_custom_primary: Fixture. """ - assert "id" not in model_custom_primary.__class__.__fields__ + assert "id" not in type(model_custom_primary).__fields__ @pytest.mark.filterwarnings( @@ -86,7 +86,7 @@ def test_automigration( assert versions.exists() # initial table - class AlembicThing(Model, table=True): # type: ignore + class AlembicThing(Model, table=True): # pyright: ignore [reportRedeclaration] t1: str with Model.get_db_engine().connect() as connection: @@ -105,7 +105,7 @@ def test_automigration( model_registry.get_metadata().clear() # Create column t2, mark t1 as optional with default - class AlembicThing(Model, table=True): # type: ignore + class AlembicThing(Model, table=True): # pyright: ignore [reportRedeclaration] t1: Optional[str] = "default" t2: str = "bar" @@ -125,7 +125,7 @@ def test_automigration( model_registry.get_metadata().clear() # Drop column t1 - class AlembicThing(Model, table=True): # type: ignore + class AlembicThing(Model, table=True): # pyright: ignore [reportRedeclaration] t2: str = "bar" assert Model.migrate(autogenerate=True) @@ -138,7 +138,7 @@ def test_automigration( assert result[1].t2 == "baz" # Add table - class AlembicSecond(Model, table=True): # type: ignore + class AlembicSecond(Model, table=True): a: int = 42 b: float = 4.2 @@ -160,14 +160,14 @@ def test_automigration( # drop table (AlembicSecond) model_registry.get_metadata().clear() - class AlembicThing(Model, table=True): # type: ignore + class AlembicThing(Model, table=True): # pyright: ignore [reportRedeclaration] t2: str = "bar" assert Model.migrate(autogenerate=True) assert len(list(versions.glob("*.py"))) == 5 with reflex.model.session() as session: - with pytest.raises(sqlalchemy.exc.OperationalError) as errctx: # type: ignore + with pytest.raises(sqlalchemy.exc.OperationalError) as errctx: session.exec(sqlmodel.select(AlembicSecond)).all() assert errctx.match(r"no such table: alembicsecond") # first table should still exist @@ -178,7 +178,7 @@ def test_automigration( model_registry.get_metadata().clear() - class AlembicThing(Model, table=True): # type: ignore + class AlembicThing(Model, table=True): # changing column type not supported by default t2: int = 42 diff --git a/tests/units/test_prerequisites.py b/tests/units/test_prerequisites.py index 2497318e7..4723d8648 100644 --- a/tests/units/test_prerequisites.py +++ b/tests/units/test_prerequisites.py @@ -1,20 +1,28 @@ import json import re +import shutil import tempfile +from pathlib import Path from unittest.mock import Mock, mock_open import pytest +from typer.testing import CliRunner from reflex import constants from reflex.config import Config +from reflex.reflex import cli +from reflex.testing import chdir from reflex.utils.prerequisites import ( CpuInfo, _update_next_config, cached_procedure, get_cpu_info, initialize_requirements_txt, + rename_imports_and_app_name, ) +runner = CliRunner() + @pytest.mark.parametrize( "config, export, expected_output", @@ -24,7 +32,7 @@ from reflex.utils.prerequisites import ( app_name="test", ), False, - 'module.exports = {basePath: "", compress: true, reactStrictMode: true, trailingSlash: true, staticPageGenerationTimeout: 60};', + 'module.exports = {basePath: "", compress: true, trailingSlash: true, staticPageGenerationTimeout: 60};', ), ( Config( @@ -32,7 +40,7 @@ from reflex.utils.prerequisites import ( static_page_generation_timeout=30, ), False, - 'module.exports = {basePath: "", compress: true, reactStrictMode: true, trailingSlash: true, staticPageGenerationTimeout: 30};', + 'module.exports = {basePath: "", compress: true, trailingSlash: true, staticPageGenerationTimeout: 30};', ), ( Config( @@ -40,7 +48,7 @@ from reflex.utils.prerequisites import ( next_compression=False, ), False, - 'module.exports = {basePath: "", compress: false, reactStrictMode: true, trailingSlash: true, staticPageGenerationTimeout: 60};', + 'module.exports = {basePath: "", compress: false, trailingSlash: true, staticPageGenerationTimeout: 60};', ), ( Config( @@ -48,7 +56,7 @@ from reflex.utils.prerequisites import ( frontend_path="/test", ), False, - 'module.exports = {basePath: "/test", compress: true, reactStrictMode: true, trailingSlash: true, staticPageGenerationTimeout: 60};', + 'module.exports = {basePath: "/test", compress: true, trailingSlash: true, staticPageGenerationTimeout: 60};', ), ( Config( @@ -57,14 +65,14 @@ from reflex.utils.prerequisites import ( next_compression=False, ), False, - 'module.exports = {basePath: "/test", compress: false, reactStrictMode: true, trailingSlash: true, staticPageGenerationTimeout: 60};', + 'module.exports = {basePath: "/test", compress: false, trailingSlash: true, staticPageGenerationTimeout: 60};', ), ( Config( app_name="test", ), True, - 'module.exports = {basePath: "", compress: true, reactStrictMode: true, trailingSlash: true, staticPageGenerationTimeout: 60, output: "export", distDir: "_static"};', + 'module.exports = {basePath: "", compress: true, trailingSlash: true, staticPageGenerationTimeout: 60, output: "export", distDir: "_static"};', ), ], ) @@ -92,7 +100,7 @@ def test_transpile_packages(transpile_packages, expected_transpile_packages): transpile_packages=transpile_packages, ) transpile_packages_match = re.search(r"transpilePackages: (\[.*?\])", output) - transpile_packages_json = transpile_packages_match.group(1) # type: ignore + transpile_packages_json = transpile_packages_match.group(1) # pyright: ignore [reportOptionalMemberAccess] actual_transpile_packages = sorted(json.loads(transpile_packages_json)) assert actual_transpile_packages == expected_transpile_packages @@ -105,8 +113,8 @@ def test_initialize_requirements_txt_no_op(mocker): return_value=Mock(best=lambda: Mock(encoding="utf-8")), ) mock_fp_touch = mocker.patch("pathlib.Path.touch") - open_mock = mock_open(read_data="reflex==0.2.9") - mocker.patch("builtins.open", open_mock) + open_mock = mock_open(read_data="reflex==0.6.7") + mocker.patch("pathlib.Path.open", open_mock) initialize_requirements_txt() assert open_mock.call_count == 1 assert open_mock.call_args.kwargs["encoding"] == "utf-8" @@ -122,7 +130,7 @@ def test_initialize_requirements_txt_missing_reflex(mocker): return_value=Mock(best=lambda: Mock(encoding="utf-8")), ) open_mock = mock_open(read_data="random-package=1.2.3") - mocker.patch("builtins.open", open_mock) + mocker.patch("pathlib.Path.open", open_mock) initialize_requirements_txt() # Currently open for read, then open for append assert open_mock.call_count == 2 @@ -138,7 +146,7 @@ def test_initialize_requirements_txt_not_exist(mocker): # File does not exist, create file with reflex mocker.patch("pathlib.Path.exists", return_value=False) open_mock = mock_open() - mocker.patch("builtins.open", open_mock) + mocker.patch("pathlib.Path.open", open_mock) initialize_requirements_txt() assert open_mock.call_count == 2 # By default, use utf-8 encoding @@ -170,7 +178,7 @@ def test_requirements_txt_other_encoding(mocker): ) initialize_requirements_txt() open_mock = mock_open(read_data="random-package=1.2.3") - mocker.patch("builtins.open", open_mock) + mocker.patch("pathlib.Path.open", open_mock) initialize_requirements_txt() # Currently open for read, then open for append assert open_mock.call_count == 2 @@ -224,3 +232,156 @@ def test_get_cpu_info(): for attr in ("manufacturer_id", "model_name", "address_width"): value = getattr(cpu_info, attr) assert value.strip() if attr != "address_width" else value + + +@pytest.fixture +def temp_directory(): + temp_dir = tempfile.mkdtemp() + yield Path(temp_dir) + shutil.rmtree(temp_dir) + + +@pytest.mark.parametrize( + "config_code,expected", + [ + ("rx.Config(app_name='old_name')", 'rx.Config(app_name="new_name")'), + ('rx.Config(app_name="old_name")', 'rx.Config(app_name="new_name")'), + ("rx.Config('old_name')", 'rx.Config("new_name")'), + ('rx.Config("old_name")', 'rx.Config("new_name")'), + ], +) +def test_rename_imports_and_app_name(temp_directory, config_code, expected): + file_path = temp_directory / "rxconfig.py" + content = f""" +config = {config_code} +""" + file_path.write_text(content) + + rename_imports_and_app_name(file_path, "old_name", "new_name") + + updated_content = file_path.read_text() + expected_content = f""" +config = {expected} +""" + assert updated_content == expected_content + + +def test_regex_edge_cases(temp_directory): + file_path = temp_directory / "example.py" + content = """ +from old_name.module import something +import old_name +from old_name import something_else as alias +from old_name +""" + file_path.write_text(content) + + rename_imports_and_app_name(file_path, "old_name", "new_name") + + updated_content = file_path.read_text() + expected_content = """ +from new_name.module import something +import new_name +from new_name import something_else as alias +from new_name +""" + assert updated_content == expected_content + + +def test_cli_rename_command(temp_directory): + foo_dir = temp_directory / "foo" + foo_dir.mkdir() + (foo_dir / "__init__").touch() + (foo_dir / ".web").mkdir() + (foo_dir / "assets").mkdir() + (foo_dir / "foo").mkdir() + (foo_dir / "foo" / "__init__.py").touch() + (foo_dir / "rxconfig.py").touch() + (foo_dir / "rxconfig.py").write_text( + """ +import reflex as rx + +config = rx.Config( + app_name="foo", +) +""" + ) + (foo_dir / "foo" / "components").mkdir() + (foo_dir / "foo" / "components" / "__init__.py").touch() + (foo_dir / "foo" / "components" / "base.py").touch() + (foo_dir / "foo" / "components" / "views.py").touch() + (foo_dir / "foo" / "components" / "base.py").write_text( + """ +import reflex as rx +from foo.components import views +from foo.components.views import * +from .base import * + +def random_component(): + return rx.fragment() +""" + ) + (foo_dir / "foo" / "foo.py").touch() + (foo_dir / "foo" / "foo.py").write_text( + """ +import reflex as rx +import foo.components.base +from foo.components.base import random_component + +class State(rx.State): + pass + + +def index(): + return rx.text("Hello, World!") + +app = rx.App() +app.add_page(index) +""" + ) + + with chdir(temp_directory / "foo"): + result = runner.invoke(cli, ["rename", "bar"]) + + assert result.exit_code == 0 + assert (foo_dir / "rxconfig.py").read_text() == ( + """ +import reflex as rx + +config = rx.Config( + app_name="bar", +) +""" + ) + assert (foo_dir / "bar").exists() + assert not (foo_dir / "foo").exists() + assert (foo_dir / "bar" / "components" / "base.py").read_text() == ( + """ +import reflex as rx +from bar.components import views +from bar.components.views import * +from .base import * + +def random_component(): + return rx.fragment() +""" + ) + assert (foo_dir / "bar" / "bar.py").exists() + assert not (foo_dir / "bar" / "foo.py").exists() + assert (foo_dir / "bar" / "bar.py").read_text() == ( + """ +import reflex as rx +import bar.components.base +from bar.components.base import random_component + +class State(rx.State): + pass + + +def index(): + return rx.text("Hello, World!") + +app = rx.App() +app.add_page(index) +""" + ) diff --git a/tests/units/test_route.py b/tests/units/test_route.py index 851c9cf35..62f1788d3 100644 --- a/tests/units/test_route.py +++ b/tests/units/test_route.py @@ -89,7 +89,7 @@ def app(): ], ) def test_check_routes_conflict_invalid(mocker, app, route1, route2): - mocker.patch.object(app, "pages", {route1: []}) + mocker.patch.object(app, "_pages", {route1: []}) with pytest.raises(ValueError): app._check_routes_conflict(route2) @@ -117,6 +117,6 @@ def test_check_routes_conflict_invalid(mocker, app, route1, route2): ], ) def test_check_routes_conflict_valid(mocker, app, route1, route2): - mocker.patch.object(app, "pages", {route1: []}) + mocker.patch.object(app, "_pages", {route1: []}) # test that running this does not throw an error. app._check_routes_conflict(route2) diff --git a/tests/units/test_sqlalchemy.py b/tests/units/test_sqlalchemy.py index b18799e0c..4434f5ee1 100644 --- a/tests/units/test_sqlalchemy.py +++ b/tests/units/test_sqlalchemy.py @@ -59,7 +59,7 @@ def test_automigration( id: Mapped[Optional[int]] = mapped_column(primary_key=True, default=None) # initial table - class AlembicThing(ModelBase): # pyright: ignore[reportGeneralTypeIssues] + class AlembicThing(ModelBase): # pyright: ignore[reportRedeclaration] t1: Mapped[str] = mapped_column(default="") with Model.get_db_engine().connect() as connection: @@ -78,7 +78,7 @@ def test_automigration( model_registry.get_metadata().clear() # Create column t2, mark t1 as optional with default - class AlembicThing(ModelBase): # pyright: ignore[reportGeneralTypeIssues] + class AlembicThing(ModelBase): # pyright: ignore[reportRedeclaration] t1: Mapped[Optional[str]] = mapped_column(default="default") t2: Mapped[str] = mapped_column(default="bar") @@ -98,7 +98,7 @@ def test_automigration( model_registry.get_metadata().clear() # Drop column t1 - class AlembicThing(ModelBase): # pyright: ignore[reportGeneralTypeIssues] + class AlembicThing(ModelBase): # pyright: ignore[reportRedeclaration] t2: Mapped[str] = mapped_column(default="bar") assert Model.migrate(autogenerate=True) @@ -127,13 +127,13 @@ def test_automigration( assert result[0].b == 4.2 # No-op - # assert Model.migrate(autogenerate=True) - # assert len(list(versions.glob("*.py"))) == 4 + # assert Model.migrate(autogenerate=True) #noqa: ERA001 + # assert len(list(versions.glob("*.py"))) == 4 #noqa: ERA001 # drop table (AlembicSecond) model_registry.get_metadata().clear() - class AlembicThing(ModelBase): # pyright: ignore[reportGeneralTypeIssues] + class AlembicThing(ModelBase): # pyright: ignore[reportRedeclaration] t2: Mapped[str] = mapped_column(default="bar") assert Model.migrate(autogenerate=True) diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 9dc2bc4e2..e0390c5ac 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -14,6 +14,7 @@ from typing import ( Any, AsyncGenerator, Callable, + ClassVar, Dict, List, Optional, @@ -55,7 +56,13 @@ from reflex.state import ( ) from reflex.testing import chdir from reflex.utils import format, prerequisites, types -from reflex.utils.exceptions import ReflexRuntimeError, SetUndefinedStateVarError +from reflex.utils.exceptions import ( + InvalidLockWarningThresholdError, + ReflexRuntimeError, + SetUndefinedStateVarError, + StateSerializationError, + UnretrievableVarValueError, +) from reflex.utils.format import json_dumps from reflex.vars.base import Var, computed_var from tests.units.states.mutation import MutableSQLAModel, MutableTestState @@ -63,7 +70,9 @@ from tests.units.states.mutation import MutableSQLAModel, MutableTestState from .states import GenState CI = bool(os.environ.get("CI", False)) -LOCK_EXPIRATION = 2000 if CI else 300 +LOCK_EXPIRATION = 2500 if CI else 300 +LOCK_WARNING_THRESHOLD = 1000 if CI else 100 +LOCK_WARN_SLEEP = 1.5 if CI else 0.15 LOCK_EXPIRE_SLEEP = 2.5 if CI else 0.4 @@ -108,7 +117,7 @@ class TestState(BaseState): # Set this class as not test one __test__ = False - num1: int + num1: rx.Field[int] num2: float = 3.14 key: str map_key: str = "a" @@ -156,7 +165,7 @@ class ChildState(TestState): """A child state fixture.""" value: str - count: int = 23 + count: rx.Field[int] = rx.field(23) def change_both(self, value: str, count: int): """Change both the value and count. @@ -194,7 +203,7 @@ class GrandchildState(ChildState): class GrandchildState2(ChildState2): """A grandchild state fixture.""" - @rx.var(cache=True) + @rx.var def cached(self) -> str: """A cached var. @@ -207,7 +216,7 @@ class GrandchildState2(ChildState2): class GrandchildState3(ChildState3): """A great grandchild state fixture.""" - @rx.var + @rx.var(cache=False) def computed(self) -> str: """A computed var. @@ -233,7 +242,7 @@ def test_state() -> TestState: Returns: A test state. """ - return TestState() # type: ignore + return TestState() # pyright: ignore [reportCallIssue] @pytest.fixture @@ -423,10 +432,10 @@ def test_default_setters(test_state): def test_class_indexing_with_vars(): """Test that we can index into a state var with another var.""" - prop = TestState.array[TestState.num1] + prop = TestState.array[TestState.num1] # pyright: ignore [reportCallIssue, reportArgumentType] assert str(prop) == f"{TestState.get_name()}.array.at({TestState.get_name()}.num1)" - prop = TestState.mapping["a"][TestState.num1] + prop = TestState.mapping["a"][TestState.num1] # pyright: ignore [reportCallIssue, reportArgumentType] assert ( str(prop) == f'{TestState.get_name()}.mapping["a"].at({TestState.get_name()}.num1)' @@ -546,9 +555,9 @@ def test_get_class_var(): def test_set_class_var(): """Test setting the var of a class.""" with pytest.raises(AttributeError): - TestState.num3 # type: ignore + TestState.num3 # pyright: ignore [reportAttributeAccessIssue] TestState._set_var(Var(_js_expr="num3", _var_type=int)._var_set_state(TestState)) - var = TestState.num3 # type: ignore + var = TestState.num3 # pyright: ignore [reportAttributeAccessIssue] assert var._js_expr == TestState.get_full_name() + ".num3" assert var._var_type is int assert var._var_state == TestState.get_full_name() @@ -781,18 +790,16 @@ async def test_process_event_simple(test_state): assert test_state.num1 == 0 event = Event(token="t", name="set_num1", payload={"value": 69}) - update = await test_state._process(event).__anext__() + async for update in test_state._process(event): + # The event should update the value. + assert test_state.num1 == 69 - # The event should update the value. - assert test_state.num1 == 69 - - # The delta should contain the changes, including computed vars. - # assert update.delta == {"test_state": {"num1": 69, "sum": 72.14}} - assert update.delta == { - TestState.get_full_name(): {"num1": 69, "sum": 72.14, "upper": ""}, - GrandchildState3.get_full_name(): {"computed": ""}, - } - assert update.events == [] + # The delta should contain the changes, including computed vars. + assert update.delta == { + TestState.get_full_name(): {"num1": 69, "sum": 72.14}, + GrandchildState3.get_full_name(): {"computed": ""}, + } + assert update.events == [] @pytest.mark.asyncio @@ -812,15 +819,15 @@ async def test_process_event_substate(test_state, child_state, grandchild_state) name=f"{ChildState.get_name()}.change_both", payload={"value": "hi", "count": 12}, ) - update = await test_state._process(event).__anext__() - assert child_state.value == "HI" - assert child_state.count == 24 - assert update.delta == { - TestState.get_full_name(): {"sum": 3.14, "upper": ""}, - ChildState.get_full_name(): {"value": "HI", "count": 24}, - GrandchildState3.get_full_name(): {"computed": ""}, - } - test_state._clean() + async for update in test_state._process(event): + assert child_state.value == "HI" + assert child_state.count == 24 + assert update.delta == { + # TestState.get_full_name(): {"sum": 3.14, "upper": ""}, + ChildState.get_full_name(): {"value": "HI", "count": 24}, + GrandchildState3.get_full_name(): {"computed": ""}, + } + test_state._clean() # Test with the granchild state. assert grandchild_state.value2 == "" @@ -829,19 +836,19 @@ async def test_process_event_substate(test_state, child_state, grandchild_state) name=f"{GrandchildState.get_full_name()}.set_value2", payload={"value": "new"}, ) - update = await test_state._process(event).__anext__() - assert grandchild_state.value2 == "new" - assert update.delta == { - TestState.get_full_name(): {"sum": 3.14, "upper": ""}, - GrandchildState.get_full_name(): {"value2": "new"}, - GrandchildState3.get_full_name(): {"computed": ""}, - } + async for update in test_state._process(event): + assert grandchild_state.value2 == "new" + assert update.delta == { + # TestState.get_full_name(): {"sum": 3.14, "upper": ""}, + GrandchildState.get_full_name(): {"value2": "new"}, + GrandchildState3.get_full_name(): {"computed": ""}, + } @pytest.mark.asyncio async def test_process_event_generator(): """Test event handlers that generate multiple updates.""" - gen_state = GenState() # type: ignore + gen_state = GenState() # pyright: ignore [reportCallIssue] event = Event( token="t", name="go", @@ -941,12 +948,12 @@ def test_add_var(): assert not hasattr(ds1, "dynamic_int") ds1.add_var("dynamic_int", int, 42) # Existing instances get the BaseVar - assert ds1.dynamic_int.equals(DynamicState.dynamic_int) # type: ignore + assert ds1.dynamic_int.equals(DynamicState.dynamic_int) # pyright: ignore [reportAttributeAccessIssue] # New instances get an actual value with the default assert DynamicState().dynamic_int == 42 ds1.add_var("dynamic_list", List[int], [5, 10]) - assert ds1.dynamic_list.equals(DynamicState.dynamic_list) # type: ignore + assert ds1.dynamic_list.equals(DynamicState.dynamic_list) # pyright: ignore [reportAttributeAccessIssue] ds2 = DynamicState() assert ds2.dynamic_list == [5, 10] ds2.dynamic_list.append(15) @@ -954,8 +961,8 @@ def test_add_var(): assert DynamicState().dynamic_list == [5, 10] ds1.add_var("dynamic_dict", Dict[str, int], {"k1": 5, "k2": 10}) - assert ds1.dynamic_dict.equals(DynamicState.dynamic_dict) # type: ignore - assert ds2.dynamic_dict.equals(DynamicState.dynamic_dict) # type: ignore + assert ds1.dynamic_dict.equals(DynamicState.dynamic_dict) # pyright: ignore [reportAttributeAccessIssue] + assert ds2.dynamic_dict.equals(DynamicState.dynamic_dict) # pyright: ignore [reportAttributeAccessIssue] assert DynamicState().dynamic_dict == {"k1": 5, "k2": 10} assert DynamicState().dynamic_dict == {"k1": 5, "k2": 10} @@ -970,7 +977,7 @@ class InterdependentState(BaseState): """A state with 3 vars and 3 computed vars. x: a variable that no computed var depends on - v1: a varable that one computed var directly depeneds on + v1: a variable that one computed var directly depends on _v2: a backend variable that one computed var directly depends on v1x2: a computed var that depends on v1 @@ -982,7 +989,7 @@ class InterdependentState(BaseState): v1: int = 0 _v2: int = 1 - @rx.var(cache=True) + @rx.var def v1x2(self) -> int: """Depends on var v1. @@ -991,7 +998,7 @@ class InterdependentState(BaseState): """ return self.v1 * 2 - @rx.var(cache=True) + @rx.var def v2x2(self) -> int: """Depends on backend var _v2. @@ -1000,7 +1007,7 @@ class InterdependentState(BaseState): """ return self._v2 * 2 - @rx.var(cache=True, backend=True) + @rx.var(backend=True) def v2x2_backend(self) -> int: """Depends on backend var _v2. @@ -1009,16 +1016,16 @@ class InterdependentState(BaseState): """ return self._v2 * 2 - @rx.var(cache=True) + @rx.var def v1x2x2(self) -> int: """Depends on ComputedVar v1x2. Returns: ComputedVar v1x2 multiplied by 2 """ - return self.v1x2 * 2 # type: ignore + return self.v1x2 * 2 - @rx.var(cache=True) + @rx.var def _v3(self) -> int: """Depends on backend var _v2. @@ -1027,7 +1034,7 @@ class InterdependentState(BaseState): """ return self._v2 - @rx.var(cache=True) + @rx.var def v3x2(self) -> int: """Depends on ComputedVar _v3. @@ -1137,7 +1144,7 @@ def test_child_state(): class ChildState(MainState): @computed_var - def rendered_var(self): + def rendered_var(self) -> int: return self.v ms = MainState() @@ -1163,13 +1170,17 @@ def test_conditional_computed_vars(): ms = MainState() # Initially there are no dirty computed vars. - assert ms._dirty_computed_vars(from_vars={"flag"}) == {"rendered_var"} - assert ms._dirty_computed_vars(from_vars={"t2"}) == {"rendered_var"} - assert ms._dirty_computed_vars(from_vars={"t1"}) == {"rendered_var"} + assert ms._dirty_computed_vars(from_vars={"flag"}) == { + (MainState.get_full_name(), "rendered_var") + } + assert ms._dirty_computed_vars(from_vars={"t2"}) == { + (MainState.get_full_name(), "rendered_var") + } + assert ms._dirty_computed_vars(from_vars={"t1"}) == { + (MainState.get_full_name(), "rendered_var") + } assert ms.computed_vars["rendered_var"]._deps(objclass=MainState) == { - "flag", - "t1", - "t2", + MainState.get_full_name(): {"flag", "t1", "t2"} } @@ -1232,7 +1243,7 @@ def test_computed_var_cached(): class ComputedState(BaseState): v: int = 0 - @rx.var(cache=True) + @rx.var def comp_v(self) -> int: nonlocal comp_v_calls comp_v_calls += 1 @@ -1257,15 +1268,15 @@ def test_computed_var_cached_depends_on_non_cached(): class ComputedState(BaseState): v: int = 0 - @rx.var + @rx.var(cache=False) def no_cache_v(self) -> int: return self.v - @rx.var(cache=True) + @rx.var def dep_v(self) -> int: - return self.no_cache_v # type: ignore + return self.no_cache_v - @rx.var(cache=True) + @rx.var def comp_v(self) -> int: return self.v @@ -1297,16 +1308,16 @@ def test_computed_var_depends_on_parent_non_cached(): counter = 0 class ParentState(BaseState): - @rx.var + @rx.var(cache=False) def no_cache_v(self) -> int: nonlocal counter counter += 1 return counter class ChildState(ParentState): - @rx.var(cache=True) + @rx.var def dep_v(self) -> int: - return self.no_cache_v # type: ignore + return self.no_cache_v ps = ParentState() cs = ps.substates[ChildState.get_name()] @@ -1350,7 +1361,7 @@ def test_cached_var_depends_on_event_handler(use_partial: bool): def handler(self): self.x = self.x + 1 - @rx.var(cache=True) + @rx.var def cached_x_side_effect(self) -> int: self.handler() nonlocal counter @@ -1358,13 +1369,16 @@ def test_cached_var_depends_on_event_handler(use_partial: bool): return counter if use_partial: - HandlerState.handler = functools.partial(HandlerState.handler.fn) + HandlerState.handler = functools.partial(HandlerState.handler.fn) # pyright: ignore [reportFunctionMemberAccess] assert isinstance(HandlerState.handler, functools.partial) else: assert isinstance(HandlerState.handler, EventHandler) s = HandlerState() - assert "cached_x_side_effect" in s._computed_var_dependencies["x"] + assert ( + HandlerState.get_full_name(), + "cached_x_side_effect", + ) in s._var_dependencies["x"] assert s.cached_x_side_effect == 1 assert s.x == 43 s.handler() @@ -1386,7 +1400,7 @@ def test_computed_var_dependencies(): def testprop(self) -> int: return self.v - @rx.var(cache=True) + @rx.var def comp_v(self) -> int: """Direct access. @@ -1395,7 +1409,7 @@ def test_computed_var_dependencies(): """ return self.v - @rx.var(cache=True, backend=True) + @rx.var(backend=True) def comp_v_backend(self) -> int: """Direct access backend var. @@ -1404,7 +1418,7 @@ def test_computed_var_dependencies(): """ return self.v - @rx.var(cache=True) + @rx.var def comp_v_via_property(self) -> int: """Access v via property. @@ -1413,8 +1427,8 @@ def test_computed_var_dependencies(): """ return self.testprop - @rx.var(cache=True) - def comp_w(self): + @rx.var + def comp_w(self) -> Callable[[], int]: """Nested lambda. Returns: @@ -1422,8 +1436,8 @@ def test_computed_var_dependencies(): """ return lambda: self.w - @rx.var(cache=True) - def comp_x(self): + @rx.var + def comp_x(self) -> Callable[[], int]: """Nested function. Returns: @@ -1435,8 +1449,8 @@ def test_computed_var_dependencies(): return _ - @rx.var(cache=True) - def comp_y(self) -> List[int]: + @rx.var + def comp_y(self) -> list[int]: """Comprehension iterating over attribute. Returns: @@ -1444,7 +1458,7 @@ def test_computed_var_dependencies(): """ return [round(y) for y in self.y] - @rx.var(cache=True) + @rx.var def comp_z(self) -> List[bool]: """Comprehension accesses attribute. @@ -1454,15 +1468,15 @@ def test_computed_var_dependencies(): return [z in self._z for z in range(5)] cs = ComputedState() - assert cs._computed_var_dependencies["v"] == { - "comp_v", - "comp_v_backend", - "comp_v_via_property", + assert cs._var_dependencies["v"] == { + (ComputedState.get_full_name(), "comp_v"), + (ComputedState.get_full_name(), "comp_v_backend"), + (ComputedState.get_full_name(), "comp_v_via_property"), } - assert cs._computed_var_dependencies["w"] == {"comp_w"} - assert cs._computed_var_dependencies["x"] == {"comp_x"} - assert cs._computed_var_dependencies["y"] == {"comp_y"} - assert cs._computed_var_dependencies["_z"] == {"comp_z"} + assert cs._var_dependencies["w"] == {(ComputedState.get_full_name(), "comp_w")} + assert cs._var_dependencies["x"] == {(ComputedState.get_full_name(), "comp_x")} + assert cs._var_dependencies["y"] == {(ComputedState.get_full_name(), "comp_y")} + assert cs._var_dependencies["_z"] == {(ComputedState.get_full_name(), "comp_z")} def test_backend_method(): @@ -1609,7 +1623,7 @@ async def test_state_with_invalid_yield(capsys, mock_app): id="backend_error", position="top-center", style={"width": "500px"}, - ) # type: ignore + ) ], token="", ) @@ -1657,7 +1671,7 @@ async def state_manager(request) -> AsyncGenerator[StateManager, None]: @pytest.fixture() -def substate_token(state_manager, token): +def substate_token(state_manager, token) -> str: """A token + substate name for looking up in state manager. Args: @@ -1698,7 +1712,7 @@ async def test_state_manager_modify_state( assert not state_manager._states_locks[token].locked() # separate instances should NOT share locks - sm2 = state_manager.__class__(state=TestState) + sm2 = type(state_manager)(state=TestState) assert sm2._state_manager_lock is state_manager._state_manager_lock assert not sm2._states_locks if state_manager._states_locks: @@ -1784,6 +1798,7 @@ async def test_state_manager_lock_expire( substate_token_redis: A token + substate name for looking up in state manager. """ state_manager_redis.lock_expiration = LOCK_EXPIRATION + state_manager_redis.lock_warning_threshold = LOCK_WARNING_THRESHOLD async with state_manager_redis.modify_state(substate_token_redis): await asyncio.sleep(0.01) @@ -1808,6 +1823,7 @@ async def test_state_manager_lock_expire_contend( unexp_num1 = 666 state_manager_redis.lock_expiration = LOCK_EXPIRATION + state_manager_redis.lock_warning_threshold = LOCK_WARNING_THRESHOLD order = [] @@ -1837,6 +1853,57 @@ async def test_state_manager_lock_expire_contend( assert (await state_manager_redis.get_state(substate_token_redis)).num1 == exp_num1 +@pytest.mark.asyncio +async def test_state_manager_lock_warning_threshold_contend( + state_manager_redis: StateManager, token: str, substate_token_redis: str, mocker +): + """Test that the state manager triggers a warning when lock contention exceeds the warning threshold. + + Args: + state_manager_redis: A state manager instance. + token: A token. + substate_token_redis: A token + substate name for looking up in state manager. + mocker: Pytest mocker object. + """ + console_warn = mocker.patch("reflex.utils.console.warn") + + state_manager_redis.lock_expiration = LOCK_EXPIRATION + state_manager_redis.lock_warning_threshold = LOCK_WARNING_THRESHOLD + + order = [] + + async def _coro_blocker(): + async with state_manager_redis.modify_state(substate_token_redis): + order.append("blocker") + await asyncio.sleep(LOCK_WARN_SLEEP) + + tasks = [ + asyncio.create_task(_coro_blocker()), + ] + + await tasks[0] + console_warn.assert_called() + assert console_warn.call_count == 7 + + +class CopyingAsyncMock(AsyncMock): + """An AsyncMock, but deepcopy the args and kwargs first.""" + + def __call__(self, *args, **kwargs): + """Call the mock. + + Args: + args: the arguments passed to the mock + kwargs: the keyword arguments passed to the mock + + Returns: + The result of the mock call + """ + args = copy.deepcopy(args) + kwargs = copy.deepcopy(kwargs) + return super().__call__(*args, **kwargs) + + @pytest.fixture(scope="function") def mock_app_simple(monkeypatch) -> rx.App: """Simple Mock app fixture. @@ -1847,13 +1914,13 @@ def mock_app_simple(monkeypatch) -> rx.App: Returns: The app, after mocking out prerequisites.get_app() """ - app = App(state=TestState) + app = App(_state=TestState) app_module = Mock() setattr(app_module, CompileVars.APP, app) - app.state = TestState - app.event_namespace.emit = AsyncMock() # type: ignore + app._state = TestState + app.event_namespace.emit = CopyingAsyncMock() # pyright: ignore [reportOptionalMemberAccess] def _mock_get_app(*args, **kwargs): return app_module @@ -1877,6 +1944,14 @@ def mock_app(mock_app_simple: rx.App, state_manager: StateManager) -> rx.App: return mock_app_simple +@dataclasses.dataclass +class ModelDC: + """A dataclass.""" + + foo: str = "bar" + ls: list[dict] = dataclasses.field(default_factory=list) + + @pytest.mark.asyncio async def test_state_proxy(grandchild_state: GrandchildState, mock_app: rx.App): """Test that the state proxy works. @@ -1954,24 +2029,18 @@ async def test_state_proxy(grandchild_state: GrandchildState, mock_app: rx.App): # ensure state update was emitted assert mock_app.event_namespace is not None - mock_app.event_namespace.emit.assert_called_once() - mcall = mock_app.event_namespace.emit.mock_calls[0] + mock_app.event_namespace.emit.assert_called_once() # pyright: ignore [reportFunctionMemberAccess] + mcall = mock_app.event_namespace.emit.mock_calls[0] # pyright: ignore [reportFunctionMemberAccess] assert mcall.args[0] == str(SocketEvent.EVENT) - assert json.loads(mcall.args[1]) == dataclasses.asdict( - StateUpdate( - delta={ - parent_state.get_full_name(): { - "upper": "", - "sum": 3.14, - }, - grandchild_state.get_full_name(): { - "value2": "42", - }, - GrandchildState3.get_full_name(): { - "computed": "", - }, - } - ) + assert mcall.args[1] == StateUpdate( + delta={ + grandchild_state.get_full_name(): { + "value2": "42", + }, + GrandchildState3.get_full_name(): { + "computed": "", + }, + } ) assert mcall.kwargs["to"] == grandchild_state.router.session.session_id @@ -1981,12 +2050,13 @@ class BackgroundTaskState(BaseState): order: List[str] = [] dict_list: Dict[str, List[int]] = {"foo": [1, 2, 3]} + dc: ModelDC = ModelDC() def __init__(self, **kwargs): # noqa: D107 super().__init__(**kwargs) self.router_data = {"simulate": "hydrate"} - @rx.var + @rx.var(cache=False) def computed_order(self) -> List[str]: """Get the order as a computed var. @@ -2006,10 +2076,18 @@ class BackgroundTaskState(BaseState): with pytest.raises(ImmutableStateError): self.order.append("bad idea") + with pytest.raises(ImmutableStateError): + # Cannot manipulate dataclass attributes. + self.dc.foo = "baz" + with pytest.raises(ImmutableStateError): # Even nested access to mutables raises an exception. self.dict_list["foo"].append(42) + with pytest.raises(ImmutableStateError): + # Cannot modify dataclass list attribute. + self.dc.ls.append({"foo": "bar"}) + with pytest.raises(ImmutableStateError): # Direct calling another handler that modifies state raises an exception. self.other() @@ -2082,8 +2160,8 @@ async def test_background_task_no_block(mock_app: rx.App, token: str): token: A token. """ router_data = {"query": {}} - mock_app.state_manager.state = mock_app.state = BackgroundTaskState - async for update in rx.app.process( # type: ignore + mock_app.state_manager.state = mock_app._state = BackgroundTaskState + async for update in rx.app.process( mock_app, Event( token=token, @@ -2100,10 +2178,10 @@ async def test_background_task_no_block(mock_app: rx.App, token: str): # wait for the coroutine to start await asyncio.sleep(0.5 if CI else 0.1) - assert len(mock_app.background_tasks) == 1 + assert len(mock_app._background_tasks) == 1 # Process another normal event - async for update in rx.app.process( # type: ignore + async for update in rx.app.process( mock_app, Event( token=token, @@ -2132,9 +2210,9 @@ async def test_background_task_no_block(mock_app: rx.App, token: str): ) # Explicit wait for background tasks - for task in tuple(mock_app.background_tasks): + for task in tuple(mock_app._background_tasks): await task - assert not mock_app.background_tasks + assert not mock_app._background_tasks exp_order = [ "background_task:start", @@ -2153,51 +2231,51 @@ async def test_background_task_no_block(mock_app: rx.App, token: str): assert mock_app.event_namespace is not None emit_mock = mock_app.event_namespace.emit - first_ws_message = json.loads(emit_mock.mock_calls[0].args[1]) + first_ws_message = emit_mock.mock_calls[0].args[1] # pyright: ignore [reportFunctionMemberAccess] assert ( - first_ws_message["delta"][BackgroundTaskState.get_full_name()].pop("router") + first_ws_message.delta[BackgroundTaskState.get_full_name()].pop("router") is not None ) - assert first_ws_message == { - "delta": { + assert first_ws_message == StateUpdate( + delta={ BackgroundTaskState.get_full_name(): { "order": ["background_task:start"], "computed_order": ["background_task:start"], } }, - "events": [], - "final": True, - } - for call in emit_mock.mock_calls[1:5]: - assert json.loads(call.args[1]) == { - "delta": { + events=[], + final=True, + ) + for call in emit_mock.mock_calls[1:5]: # pyright: ignore [reportFunctionMemberAccess] + assert call.args[1] == StateUpdate( + delta={ BackgroundTaskState.get_full_name(): { "computed_order": ["background_task:start"], } }, - "events": [], - "final": True, - } - assert json.loads(emit_mock.mock_calls[-2].args[1]) == { - "delta": { + events=[], + final=True, + ) + assert emit_mock.mock_calls[-2].args[1] == StateUpdate( # pyright: ignore [reportFunctionMemberAccess] + delta={ BackgroundTaskState.get_full_name(): { "order": exp_order, "computed_order": exp_order, "dict_list": {}, } }, - "events": [], - "final": True, - } - assert json.loads(emit_mock.mock_calls[-1].args[1]) == { - "delta": { + events=[], + final=True, + ) + assert emit_mock.mock_calls[-1].args[1] == StateUpdate( # pyright: ignore [reportFunctionMemberAccess] + delta={ BackgroundTaskState.get_full_name(): { "computed_order": exp_order, }, }, - "events": [], - "final": True, - } + events=[], + final=True, + ) @pytest.mark.asyncio @@ -2209,8 +2287,8 @@ async def test_background_task_reset(mock_app: rx.App, token: str): token: A token. """ router_data = {"query": {}} - mock_app.state_manager.state = mock_app.state = BackgroundTaskState - async for update in rx.app.process( # type: ignore + mock_app.state_manager.state = mock_app._state = BackgroundTaskState + async for update in rx.app.process( mock_app, Event( token=token, @@ -2226,9 +2304,9 @@ async def test_background_task_reset(mock_app: rx.App, token: str): assert update == StateUpdate() # Explicit wait for background tasks - for task in tuple(mock_app.background_tasks): + for task in tuple(mock_app._background_tasks): await task - assert not mock_app.background_tasks + assert not mock_app._background_tasks assert ( await mock_app.state_manager.get_state( @@ -2552,10 +2630,10 @@ def test_duplicate_substate_class(mocker): class TestState(BaseState): pass - class ChildTestState(TestState): # type: ignore + class ChildTestState(TestState): # pyright: ignore [reportRedeclaration] pass - class ChildTestState(TestState): # type: ignore # noqa + class ChildTestState(TestState): # noqa: F811 pass return TestState @@ -2593,21 +2671,21 @@ def test_reset_with_mutables(): items: List[List[int]] = default instance = MutableResetState() - assert instance.items.__wrapped__ is not default # type: ignore + assert instance.items.__wrapped__ is not default # pyright: ignore [reportAttributeAccessIssue] assert instance.items == default == copied_default instance.items.append([3, 3]) assert instance.items != default assert instance.items != copied_default instance.reset() - assert instance.items.__wrapped__ is not default # type: ignore + assert instance.items.__wrapped__ is not default # pyright: ignore [reportAttributeAccessIssue] assert instance.items == default == copied_default instance.items.append([3, 3]) assert instance.items != default assert instance.items != copied_default instance.reset() - assert instance.items.__wrapped__ is not default # type: ignore + assert instance.items.__wrapped__ is not default # pyright: ignore [reportAttributeAccessIssue] assert instance.items == default == copied_default instance.items.append([3, 3]) assert instance.items != default @@ -2628,7 +2706,7 @@ class Custom1(Base): self.foo = val def double_foo(self) -> str: - """Concantenate foo with foo. + """Concatenate foo with foo. Returns: foo + foo @@ -2669,30 +2747,30 @@ def test_state_union_optional(): c3r: Custom3 = Custom3(c2r=Custom2(c1r=Custom1(foo=""))) custom_union: Union[Custom1, Custom2, Custom3] = Custom1(foo="") - assert str(UnionState.c3.c2) == f'{UnionState.c3!s}?.["c2"]' # type: ignore - assert str(UnionState.c3.c2.c1) == f'{UnionState.c3!s}?.["c2"]?.["c1"]' # type: ignore + assert str(UnionState.c3.c2) == f'{UnionState.c3!s}?.["c2"]' # pyright: ignore [reportOptionalMemberAccess] + assert str(UnionState.c3.c2.c1) == f'{UnionState.c3!s}?.["c2"]?.["c1"]' # pyright: ignore [reportOptionalMemberAccess] assert ( - str(UnionState.c3.c2.c1.foo) == f'{UnionState.c3!s}?.["c2"]?.["c1"]?.["foo"]' # type: ignore + str(UnionState.c3.c2.c1.foo) == f'{UnionState.c3!s}?.["c2"]?.["c1"]?.["foo"]' # pyright: ignore [reportOptionalMemberAccess] ) assert ( - str(UnionState.c3.c2.c1r.foo) == f'{UnionState.c3!s}?.["c2"]?.["c1r"]["foo"]' # type: ignore + str(UnionState.c3.c2.c1r.foo) == f'{UnionState.c3!s}?.["c2"]?.["c1r"]["foo"]' # pyright: ignore [reportOptionalMemberAccess] ) - assert str(UnionState.c3.c2r.c1) == f'{UnionState.c3!s}?.["c2r"]["c1"]' # type: ignore + assert str(UnionState.c3.c2r.c1) == f'{UnionState.c3!s}?.["c2r"]["c1"]' # pyright: ignore [reportOptionalMemberAccess] assert ( - str(UnionState.c3.c2r.c1.foo) == f'{UnionState.c3!s}?.["c2r"]["c1"]?.["foo"]' # type: ignore + str(UnionState.c3.c2r.c1.foo) == f'{UnionState.c3!s}?.["c2r"]["c1"]?.["foo"]' # pyright: ignore [reportOptionalMemberAccess] ) assert ( - str(UnionState.c3.c2r.c1r.foo) == f'{UnionState.c3!s}?.["c2r"]["c1r"]["foo"]' # type: ignore + str(UnionState.c3.c2r.c1r.foo) == f'{UnionState.c3!s}?.["c2r"]["c1r"]["foo"]' # pyright: ignore [reportOptionalMemberAccess] ) - assert str(UnionState.c3i.c2) == f'{UnionState.c3i!s}["c2"]' # type: ignore - assert str(UnionState.c3r.c2) == f'{UnionState.c3r!s}["c2"]' # type: ignore - assert UnionState.custom_union.foo is not None # type: ignore - assert UnionState.custom_union.c1 is not None # type: ignore - assert UnionState.custom_union.c1r is not None # type: ignore - assert UnionState.custom_union.c2 is not None # type: ignore - assert UnionState.custom_union.c2r is not None # type: ignore - assert types.is_optional(UnionState.opt_int._var_type) # type: ignore - assert types.is_union(UnionState.int_float._var_type) # type: ignore + assert str(UnionState.c3i.c2) == f'{UnionState.c3i!s}["c2"]' + assert str(UnionState.c3r.c2) == f'{UnionState.c3r!s}["c2"]' + assert UnionState.custom_union.foo is not None # pyright: ignore [reportAttributeAccessIssue] + assert UnionState.custom_union.c1 is not None # pyright: ignore [reportAttributeAccessIssue] + assert UnionState.custom_union.c1r is not None # pyright: ignore [reportAttributeAccessIssue] + assert UnionState.custom_union.c2 is not None # pyright: ignore [reportAttributeAccessIssue] + assert UnionState.custom_union.c2r is not None # pyright: ignore [reportAttributeAccessIssue] + assert types.is_optional(UnionState.opt_int._var_type) # pyright: ignore [reportAttributeAccessIssue, reportOptionalMemberAccess] + assert types.is_union(UnionState.int_float._var_type) # pyright: ignore [reportAttributeAccessIssue] def test_set_base_field_via_setter(): @@ -2813,7 +2891,7 @@ async def test_preprocess(app_module_mock, token, test_state, expected, mocker): "reflex.state.State.class_subclasses", {test_state, OnLoadInternalState} ) app = app_module_mock.app = App( - state=State, load_events={"index": [test_state.test_handler]} + _state=State, _load_events={"index": [test_state.test_handler]} ) async with app.state_manager.modify_state(_substate_key(token, State)) as state: state.router_data = {"simulate": "hydrate"} @@ -2838,10 +2916,10 @@ async def test_preprocess(app_module_mock, token, test_state, expected, mocker): events = updates[0].events assert len(events) == 2 - assert (await state._process(events[0]).__anext__()).delta == { - test_state.get_full_name(): {"num": 1} - } - assert (await state._process(events[1]).__anext__()).delta == exp_is_hydrated(state) + async for update in state._process(events[0]): + assert update.delta == {test_state.get_full_name(): {"num": 1}} + async for update in state._process(events[1]): + assert update.delta == exp_is_hydrated(state) if isinstance(app.state_manager, StateManagerRedis): await app.state_manager.close() @@ -2860,8 +2938,8 @@ async def test_preprocess_multiple_load_events(app_module_mock, token, mocker): "reflex.state.State.class_subclasses", {OnLoadState, OnLoadInternalState} ) app = app_module_mock.app = App( - state=State, - load_events={"index": [OnLoadState.test_handler, OnLoadState.test_handler]}, + _state=State, + _load_events={"index": [OnLoadState.test_handler, OnLoadState.test_handler]}, ) async with app.state_manager.modify_state(_substate_key(token, State)) as state: state.router_data = {"simulate": "hydrate"} @@ -2886,13 +2964,12 @@ async def test_preprocess_multiple_load_events(app_module_mock, token, mocker): events = updates[0].events assert len(events) == 3 - assert (await state._process(events[0]).__anext__()).delta == { - OnLoadState.get_full_name(): {"num": 1} - } - assert (await state._process(events[1]).__anext__()).delta == { - OnLoadState.get_full_name(): {"num": 2} - } - assert (await state._process(events[2]).__anext__()).delta == exp_is_hydrated(state) + async for update in state._process(events[0]): + assert update.delta == {OnLoadState.get_full_name(): {"num": 1}} + async for update in state._process(events[1]): + assert update.delta == {OnLoadState.get_full_name(): {"num": 2}} + async for update in state._process(events[2]): + assert update.delta == exp_is_hydrated(state) if isinstance(app.state_manager, StateManagerRedis): await app.state_manager.close() @@ -2906,7 +2983,7 @@ async def test_get_state(mock_app: rx.App, token: str): mock_app: An app that will be returned by `get_app()` token: A token. """ - mock_app.state_manager.state = mock_app.state = TestState + mock_app.state_manager.state = mock_app._state = TestState # Get instance of ChildState2. test_state = await mock_app.state_manager.get_state( @@ -2965,10 +3042,6 @@ async def test_get_state(mock_app: rx.App, token: str): grandchild_state.value2 = "set_value" assert test_state.get_delta() == { - TestState.get_full_name(): { - "sum": 3.14, - "upper": "", - }, GrandchildState.get_full_name(): { "value2": "set_value", }, @@ -3006,10 +3079,6 @@ async def test_get_state(mock_app: rx.App, token: str): child_state2.value = "set_c2_value" assert new_test_state.get_delta() == { - TestState.get_full_name(): { - "sum": 3.14, - "upper": "", - }, ChildState2.get_full_name(): { "value": "set_c2_value", }, @@ -3064,8 +3133,8 @@ async def test_get_state_from_sibling_not_cached(mock_app: rx.App, token: str): child3_var: int = 0 - @rx.var - def v(self): + @rx.var(cache=False) + def v(self) -> None: pass class Grandchild3(Child3): @@ -3084,7 +3153,7 @@ async def test_get_state_from_sibling_not_cached(mock_app: rx.App, token: str): pass - mock_app.state_manager.state = mock_app.state = Parent + mock_app.state_manager.state = mock_app._state = Parent # Get the top level state via unconnected sibling. root = await mock_app.state_manager.get_state(_substate_key(token, Child)) @@ -3119,7 +3188,7 @@ async def test_get_state_from_sibling_not_cached(mock_app: rx.App, token: str): RxState = State -def test_potentially_dirty_substates(): +def test_potentially_dirty_states(): """Test that potentially_dirty_substates returns the correct substates. Even if the name "State" is shadowed, it should still work correctly. @@ -3135,13 +3204,19 @@ def test_potentially_dirty_substates(): def bar(self) -> str: return "" - assert RxState._potentially_dirty_substates() == {State} - assert State._potentially_dirty_substates() == {C1} - assert C1._potentially_dirty_substates() == set() + assert RxState._get_potentially_dirty_states() == set() + assert State._get_potentially_dirty_states() == set() + assert C1._get_potentially_dirty_states() == set() -def test_router_var_dep() -> None: - """Test that router var dependencies are correctly tracked.""" +@pytest.mark.asyncio +async def test_router_var_dep(state_manager: StateManager, token: str) -> None: + """Test that router var dependencies are correctly tracked. + + Args: + state_manager: A state manager. + token: A token. + """ class RouterVarParentState(State): """A parent state for testing router var dependency.""" @@ -3151,37 +3226,34 @@ def test_router_var_dep() -> None: class RouterVarDepState(RouterVarParentState): """A state with a router var dependency.""" - @rx.var(cache=True) + @rx.var def foo(self) -> str: return self.router.page.params.get("foo", "") foo = RouterVarDepState.computed_vars["foo"] State._init_var_dependency_dicts() - assert foo._deps(objclass=RouterVarDepState) == {"router"} - assert RouterVarParentState._potentially_dirty_substates() == {RouterVarDepState} - assert RouterVarParentState._substate_var_dependencies == { - "router": {RouterVarDepState.get_name()} - } - assert RouterVarDepState._computed_var_dependencies == { - "router": {"foo"}, + assert foo._deps(objclass=RouterVarDepState) == { + RouterVarDepState.get_full_name(): {"router"} } + assert (RouterVarDepState.get_full_name(), "foo") in State._var_dependencies[ + "router" + ] - rx_state = State() - parent_state = RouterVarParentState() - state = RouterVarDepState() - - # link states - rx_state.substates = {RouterVarParentState.get_name(): parent_state} - parent_state.parent_state = rx_state - state.parent_state = parent_state - parent_state.substates = {RouterVarDepState.get_name(): state} + # Get state from state manager. + state_manager.state = State + rx_state = await state_manager.get_state(_substate_key(token, State)) + assert RouterVarParentState.get_name() in rx_state.substates + parent_state = rx_state.substates[RouterVarParentState.get_name()] + assert RouterVarDepState.get_name() in parent_state.substates + state = parent_state.substates[RouterVarDepState.get_name()] assert state.dirty_vars == set() # Reassign router var state.router = state.router - assert state.dirty_vars == {"foo", "router"} + assert rx_state.dirty_vars == {"router"} + assert state.dirty_vars == {"foo"} assert parent_state.dirty_substates == {RouterVarDepState.get_name()} @@ -3210,9 +3282,9 @@ async def test_setvar(mock_app: rx.App, token: str): print(update) assert state.array == [43] - # Cannot setvar for non-existant var + # Cannot setvar for non-existent var with pytest.raises(AttributeError): - TestState.setvar("non_existant_var") + TestState.setvar("non_existent_var") # Cannot setvar for computed vars with pytest.raises(AttributeError): @@ -3234,12 +3306,42 @@ async def test_setvar_async_setter(): @pytest.mark.parametrize( "expiration_kwargs, expected_values", [ - ({"redis_lock_expiration": 20000}, (20000, constants.Expiration.TOKEN)), + ( + {"redis_lock_expiration": 20000}, + ( + 20000, + constants.Expiration.TOKEN, + constants.Expiration.LOCK_WARNING_THRESHOLD, + ), + ), ( {"redis_lock_expiration": 50000, "redis_token_expiration": 5600}, - (50000, 5600), + (50000, 5600, constants.Expiration.LOCK_WARNING_THRESHOLD), + ), + ( + {"redis_token_expiration": 7600}, + ( + constants.Expiration.LOCK, + 7600, + constants.Expiration.LOCK_WARNING_THRESHOLD, + ), + ), + ( + {"redis_lock_expiration": 50000, "redis_lock_warning_threshold": 1500}, + (50000, constants.Expiration.TOKEN, 1500), + ), + ( + {"redis_token_expiration": 5600, "redis_lock_warning_threshold": 3000}, + (constants.Expiration.LOCK, 5600, 3000), + ), + ( + { + "redis_lock_expiration": 50000, + "redis_token_expiration": 5600, + "redis_lock_warning_threshold": 2000, + }, + (50000, 5600, 2000), ), - ({"redis_token_expiration": 7600}, (constants.Expiration.LOCK, 7600)), ], ) def test_redis_state_manager_config_knobs(tmp_path, expiration_kwargs, expected_values): @@ -3267,8 +3369,46 @@ config = rx.Config( from reflex.state import State, StateManager state_manager = StateManager.create(state=State) - assert state_manager.lock_expiration == expected_values[0] # type: ignore - assert state_manager.token_expiration == expected_values[1] # type: ignore + assert state_manager.lock_expiration == expected_values[0] # pyright: ignore [reportAttributeAccessIssue] + assert state_manager.token_expiration == expected_values[1] # pyright: ignore [reportAttributeAccessIssue] + assert state_manager.lock_warning_threshold == expected_values[2] # pyright: ignore [reportAttributeAccessIssue] + + +@pytest.mark.skipif("REDIS_URL" not in os.environ, reason="Test requires redis") +@pytest.mark.parametrize( + "redis_lock_expiration, redis_lock_warning_threshold", + [ + (10000, 10000), + (20000, 30000), + ], +) +def test_redis_state_manager_config_knobs_invalid_lock_warning_threshold( + tmp_path, redis_lock_expiration, redis_lock_warning_threshold +): + proj_root = tmp_path / "project1" + proj_root.mkdir() + + config_string = f""" +import reflex as rx +config = rx.Config( + app_name="project1", + redis_url="redis://localhost:6379", + state_manager_mode="redis", + redis_lock_expiration = {redis_lock_expiration}, + redis_lock_warning_threshold = {redis_lock_warning_threshold}, +) + """ + + (proj_root / "rxconfig.py").write_text(dedent(config_string)) + + with chdir(proj_root): + # reload config for each parameter to avoid stale values + reflex.config.get_config(reload=True) + from reflex.state import State, StateManager + + with pytest.raises(InvalidLockWarningThresholdError): + StateManager.create(state=State) + del sys.modules[constants.Config.MODULE] class MixinState(State, mixin=True): @@ -3278,7 +3418,7 @@ class MixinState(State, mixin=True): _backend: int = 0 _backend_no_default: dict - @rx.var(cache=True) + @rx.var def computed(self) -> str: """A computed var on mixin state. @@ -3310,7 +3450,7 @@ def test_mixin_state() -> None: assert "computed" in UsesMixinState.vars assert ( - UsesMixinState(_reflex_internal_init=True)._backend_no_default # type: ignore + UsesMixinState(_reflex_internal_init=True)._backend_no_default # pyright: ignore [reportCallIssue] is not UsesMixinState.backend_vars["_backend_no_default"] ) @@ -3330,7 +3470,7 @@ def test_assignment_to_undeclared_vars(): class State(BaseState): val: str _val: str - __val: str # type: ignore + __val: str # pyright: ignore [reportGeneralTypeIssues] def handle_supported_regular_vars(self): self.val = "no underscore" @@ -3350,8 +3490,8 @@ def test_assignment_to_undeclared_vars(): def handle_var(self): self.value = 20 - state = State() # type: ignore - sub_state = Substate() # type: ignore + state = State() # pyright: ignore [reportCallIssue] + sub_state = Substate() # pyright: ignore [reportCallIssue] with pytest.raises(SetUndefinedStateVarError): state.handle_regular_var() @@ -3413,7 +3553,7 @@ def test_fallback_pickle(): _f: Optional[Callable] = None _g: Any = None - state = DillState(_reflex_internal_init=True) # type: ignore + state = DillState(_reflex_internal_init=True) # pyright: ignore [reportCallIssue] state._o = Obj(_f=lambda: 42) state._f = lambda: 420 @@ -3424,17 +3564,18 @@ def test_fallback_pickle(): assert unpickled_state._o._f() == 42 # Threading locks are unpicklable normally, and raise TypeError instead of PicklingError. - state2 = DillState(_reflex_internal_init=True) # type: ignore + state2 = DillState(_reflex_internal_init=True) # pyright: ignore [reportCallIssue] state2._g = threading.Lock() pk2 = state2._serialize() unpickled_state2 = BaseState._deserialize(pk2) assert isinstance(unpickled_state2._g, type(threading.Lock())) # Some object, like generator, are still unpicklable with dill. - state3 = DillState(_reflex_internal_init=True) # type: ignore + state3 = DillState(_reflex_internal_init=True) # pyright: ignore [reportCallIssue] state3._g = (i for i in range(10)) - pk3 = state3._serialize() - assert len(pk3) == 0 + + with pytest.raises(StateSerializationError): + _ = state3._serialize() def test_typed_state() -> None: @@ -3456,13 +3597,6 @@ class ModelV2(BaseModelV2): foo: str = "bar" -@dataclasses.dataclass -class ModelDC: - """A dataclass.""" - - foo: str = "bar" - - class PydanticState(rx.State): """A state with pydantic BaseModel vars.""" @@ -3484,11 +3618,22 @@ def test_mutable_models(): assert state.dirty_vars == {"v2"} state.dirty_vars.clear() - # Not yet supported ENG-4083 - # assert isinstance(state.dc, MutableProxy) - # state.dc.foo = "baz" - # assert state.dirty_vars == {"dc"} - # state.dirty_vars.clear() + assert isinstance(state.dc, MutableProxy) + state.dc.foo = "baz" + assert state.dirty_vars == {"dc"} + state.dirty_vars.clear() + assert state.dirty_vars == set() + state.dc.ls.append({"hi": "reflex"}) + assert state.dirty_vars == {"dc"} + state.dirty_vars.clear() + assert state.dirty_vars == set() + assert dataclasses.asdict(state.dc) == {"foo": "baz", "ls": [{"hi": "reflex"}]} + assert dataclasses.astuple(state.dc) == ("baz", [{"hi": "reflex"}]) + # creating a new instance shouldn't mark the state dirty + assert dataclasses.replace(state.dc, foo="quuc") == ModelDC( + foo="quuc", ls=[{"hi": "reflex"}] + ) + assert state.dirty_vars == set() def test_get_value(): @@ -3601,7 +3746,7 @@ class UpcastState(rx.State): assert isinstance(a, list) self.passed = True - def py_unresolvable(self, u: "Unresolvable"): # noqa: D102, F821 # type: ignore + def py_unresolvable(self, u: "Unresolvable"): # noqa: D102, F821 # pyright: ignore [reportUndefinedVariable] assert isinstance(u, list) self.passed = True @@ -3638,3 +3783,157 @@ async def test_upcast_event_handler_arg(handler, payload): state = UpcastState() async for update in state._process_event(handler, state, payload): assert update.delta == {UpcastState.get_full_name(): {"passed": True}} + + +@pytest.mark.asyncio +async def test_get_var_value(state_manager: StateManager, substate_token: str): + """Test that get_var_value works correctly. + + Args: + state_manager: The state manager to use. + substate_token: Token for the substate used by state_manager. + """ + state = await state_manager.get_state(substate_token) + + # State Var from same state + assert await state.get_var_value(TestState.num1) == 0 + state.num1 = 42 + assert await state.get_var_value(TestState.num1) == 42 + + # State Var from another state + child_state = await state.get_state(ChildState) + assert await state.get_var_value(ChildState.count) == 23 + child_state.count = 66 + assert await state.get_var_value(ChildState.count) == 66 + + # LiteralVar with known value + assert await state.get_var_value(rx.Var.create([1, 2, 3])) == [1, 2, 3] + + # Generic Var with no state + with pytest.raises(UnretrievableVarValueError): + await state.get_var_value(rx.Var("undefined")) + + +@pytest.mark.asyncio +async def test_async_computed_var_get_state(mock_app: rx.App, token: str): + """A test where an async computed var depends on a var in another state. + + Args: + mock_app: An app that will be returned by `get_app()` + token: A token. + """ + + class Parent(BaseState): + """A root state like rx.State.""" + + parent_var: int = 0 + + class Child2(Parent): + """An unconnected child state.""" + + pass + + class Child3(Parent): + """A child state with a computed var causing it to be pre-fetched. + + If child3_var gets set to a value, and `get_state` erroneously + re-fetches it from redis, the value will be lost. + """ + + child3_var: int = 0 + + @rx.var(cache=True) + def v(self) -> int: + return self.child3_var + + class Child(Parent): + """A state simulating UpdateVarsInternalState.""" + + @rx.var(cache=True) + async def v(self) -> int: + p = await self.get_state(Parent) + child3 = await self.get_state(Child3) + return child3.child3_var + p.parent_var + + mock_app.state_manager.state = mock_app._state = Parent + + # Get the top level state via unconnected sibling. + root = await mock_app.state_manager.get_state(_substate_key(token, Child)) + # Set value in parent_var to assert it does not get refetched later. + root.parent_var = 1 + + if isinstance(mock_app.state_manager, StateManagerRedis): + # When redis is used, only states with uncached computed vars are pre-fetched. + assert Child2.get_name() not in root.substates + assert Child3.get_name() not in root.substates + + # Get the unconnected sibling state, which will be used to `get_state` other instances. + child = root.get_substate(Child.get_full_name().split(".")) + + # Get an uncached child state. + child2 = await child.get_state(Child2) + assert child2.parent_var == 1 + + # Set value on already-cached Child3 state (prefetched because it has a Computed Var). + child3 = await child.get_state(Child3) + child3.child3_var = 1 + + assert await child.v == 2 + assert await child.v == 2 + root.parent_var = 2 + assert await child.v == 3 + + +class Table(rx.ComponentState): + """A table state.""" + + data: ClassVar[Var] + + @rx.var(cache=True, auto_deps=False) + async def rows(self) -> List[Dict[str, Any]]: + """Computed var over the given rows. + + Returns: + The data rows. + """ + return await self.get_var_value(self.data) + + @classmethod + def get_component(cls, data: Var) -> rx.Component: + """Get the component for the table. + + Args: + data: The data var. + + Returns: + The component. + """ + cls.data = data + cls.computed_vars["rows"].add_dependency(cls, data) + return rx.foreach(data, lambda d: rx.text(d.to_string())) + + +@pytest.mark.asyncio +async def test_async_computed_var_get_var_value(mock_app: rx.App, token: str): + """A test where an async computed var depends on a var in another state. + + Args: + mock_app: An app that will be returned by `get_app()` + token: A token. + """ + + class OtherState(rx.State): + """A state with a var.""" + + data: List[Dict[str, Any]] = [{"foo": "bar"}] + + mock_app.state_manager.state = mock_app._state = rx.State + comp = Table.create(data=OtherState.data) + state = await mock_app.state_manager.get_state(_substate_key(token, OtherState)) + other_state = await state.get_state(OtherState) + assert comp.State is not None + comp_state = await state.get_state(comp.State) + assert comp_state.dirty_vars == set() + + other_state.data.append({"foo": "baz"}) + assert "rows" in comp_state.dirty_vars diff --git a/tests/units/test_state_tree.py b/tests/units/test_state_tree.py index ebdd877de..70ef71cb8 100644 --- a/tests/units/test_state_tree.py +++ b/tests/units/test_state_tree.py @@ -42,7 +42,7 @@ class SubA_A_A_A(SubA_A_A): class SubA_A_A_B(SubA_A_A): """SubA_A_A_B is a child of SubA_A_A.""" - @rx.var(cache=True) + @rx.var def sub_a_a_a_cached(self) -> int: """A cached var. @@ -117,7 +117,7 @@ class TreeD(Root): d: int - @rx.var + @rx.var(cache=False) def d_var(self) -> int: """A computed var. @@ -156,7 +156,7 @@ class SubE_A_A_A_A(SubE_A_A_A): sub_e_a_a_a_a: int - @rx.var + @rx.var(cache=False) def sub_e_a_a_a_a_var(self) -> int: """A computed var. @@ -183,7 +183,7 @@ class SubE_A_A_A_D(SubE_A_A_A): sub_e_a_a_a_d: int - @rx.var(cache=True) + @rx.var def sub_e_a_a_a_d_var(self) -> int: """A computed var. @@ -222,7 +222,7 @@ async def state_manager_redis( Yields: A state manager instance """ - app_module_mock.app = rx.App(state=Root) + app_module_mock.app = rx.App(_state=Root) state_manager = app_module_mock.app.state_manager if not isinstance(state_manager, StateManagerRedis): diff --git a/tests/units/test_style.py b/tests/units/test_style.py index e1d652798..e8ff5bd01 100644 --- a/tests/units/test_style.py +++ b/tests/units/test_style.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Dict +from typing import Any, Mapping import pytest @@ -356,7 +356,7 @@ def test_style_via_component( style_dict: The style_dict to pass to the component. expected_get_style: The expected style dict. """ - comp = rx.el.div(style=style_dict, **kwargs) # type: ignore + comp = rx.el.div(style=style_dict, **kwargs) # pyright: ignore [reportArgumentType] compare_dict_of_var(comp._get_style(), expected_get_style) @@ -379,7 +379,7 @@ class StyleState(rx.State): { "css": Var( _js_expr=f'({{ ["color"] : ("dark"+{StyleState.color}) }})' - ).to(Dict[str, str]) + ).to(Mapping[str, str]) }, ), ( @@ -515,17 +515,17 @@ def test_evaluate_style_namespaces(): """Test that namespaces get converted to component create functions.""" style_dict = {rx.text: {"color": "blue"}} assert rx.text.__call__ not in style_dict - style_dict = evaluate_style_namespaces(style_dict) # type: ignore + style_dict = evaluate_style_namespaces(style_dict) # pyright: ignore [reportArgumentType] assert rx.text.__call__ in style_dict def test_style_update_with_var_data(): """Test that .update with a Style containing VarData works.""" red_var = LiteralVar.create("red")._replace( - merge_var_data=VarData(hooks={"const red = true": None}), # type: ignore + merge_var_data=VarData(hooks={"const red = true": None}), ) blue_var = LiteralVar.create("blue")._replace( - merge_var_data=VarData(hooks={"const blue = true": None}), # type: ignore + merge_var_data=VarData(hooks={"const blue = true": None}), ) s1 = Style( @@ -541,3 +541,7 @@ def test_style_update_with_var_data(): assert s2._var_data is not None assert "const red = true" in s2._var_data.hooks assert "const blue = true" in s2._var_data.hooks + + s3 = s1 | s2 + assert s3._var_data is not None + assert "_varData" not in s3 diff --git a/tests/units/test_telemetry.py b/tests/units/test_telemetry.py index 25ad91323..d8a77dfd6 100644 --- a/tests/units/test_telemetry.py +++ b/tests/units/test_telemetry.py @@ -34,12 +34,6 @@ def test_disable(): @pytest.mark.parametrize("event", ["init", "reinit", "run-dev", "run-prod", "export"]) def test_send(mocker, event): httpx_post_mock = mocker.patch("httpx.post") - # mocker.patch( - # "builtins.open", - # mocker.mock_open( - # read_data='{"project_hash": "78285505863498957834586115958872998605"}' - # ), - # ) # Mock the read_text method of Path pathlib_path_read_text_mock = mocker.patch( diff --git a/tests/units/test_testing.py b/tests/units/test_testing.py index 83a03ad83..8c8f1461b 100644 --- a/tests/units/test_testing.py +++ b/tests/units/test_testing.py @@ -23,7 +23,7 @@ def test_app_harness(tmp_path): class State(rx.State): pass - app = rx.App(state=State) + app = rx.App(_state=State) app.add_page(lambda: rx.text("Basic App"), route="/", title="index") app._compile() diff --git a/tests/units/test_var.py b/tests/units/test_var.py index 3176443a6..a72242814 100644 --- a/tests/units/test_var.py +++ b/tests/units/test_var.py @@ -1,8 +1,7 @@ import json import math -import sys import typing -from typing import Dict, List, Optional, Set, Tuple, Union, cast +from typing import Dict, List, Mapping, Optional, Set, Tuple, Union, cast import pytest from pandas import DataFrame @@ -11,7 +10,10 @@ import reflex as rx from reflex.base import Base from reflex.constants.base import REFLEX_VAR_CLOSING_TAG, REFLEX_VAR_OPENING_TAG from reflex.state import BaseState -from reflex.utils.exceptions import PrimitiveUnserializableToJSON +from reflex.utils.exceptions import ( + PrimitiveUnserializableToJSONError, + UntypedComputedVarError, +) from reflex.utils.imports import ImportVar from reflex.vars import VarData from reflex.vars.base import ( @@ -185,6 +187,7 @@ def ChildWithRuntimeOnlyVar(StateWithRuntimeOnlyVar): "state.local", "local2", ], + strict=True, ), ) def test_full_name(prop, expected): @@ -202,6 +205,7 @@ def test_full_name(prop, expected): zip( test_vars, ["prop1", "key", "state.value", "state.local", "local2"], + strict=True, ), ) def test_str(prop, expected): @@ -248,6 +252,7 @@ def test_default_value(prop: Var, expected): "state.set_local", "set_local2", ], + strict=True, ), ) def test_get_setter(prop: Var, expected): @@ -270,7 +275,7 @@ def test_get_setter(prop: Var, expected): ([1, 2, 3], Var(_js_expr="[1, 2, 3]", _var_type=List[int])), ( {"a": 1, "b": 2}, - Var(_js_expr='({ ["a"] : 1, ["b"] : 2 })', _var_type=Dict[str, int]), + Var(_js_expr='({ ["a"] : 1, ["b"] : 2 })', _var_type=Mapping[str, int]), ), ], ) @@ -282,7 +287,7 @@ def test_create(value, expected): expected: The expected name of the setter function. """ prop = LiteralVar.create(value) - assert prop.equals(expected) # type: ignore + assert prop.equals(expected) def test_create_type_error(): @@ -372,7 +377,7 @@ def test_basic_operations(TestObj): "var, expected", [ (v([1, 2, 3]), "[1, 2, 3]"), - (v(set([1, 2, 3])), "[1, 2, 3]"), + (v({1, 2, 3}), "[1, 2, 3]"), (v(["1", "2", "3"]), '["1", "2", "3"]'), ( Var(_js_expr="foo")._var_set_state("state").to(list), @@ -416,19 +421,13 @@ class Bar(rx.Base): @pytest.mark.parametrize( ("var", "var_type"), - ( - [ - (Var(_js_expr="", _var_type=Foo | Bar).guess_type(), Foo | Bar), - (Var(_js_expr="", _var_type=Foo | Bar).guess_type().bar, Union[int, str]), - ] - if sys.version_info >= (3, 10) - else [] - ) - + [ - (Var(_js_expr="", _var_type=Union[Foo, Bar]).guess_type(), Union[Foo, Bar]), - (Var(_js_expr="", _var_type=Union[Foo, Bar]).guess_type().baz, str), + [ + (Var(_js_expr="").to(Foo | Bar), Foo | Bar), + (Var(_js_expr="").to(Foo | Bar).bar, Union[int, str]), + (Var(_js_expr="").to(Union[Foo, Bar]), Union[Foo, Bar]), + (Var(_js_expr="").to(Union[Foo, Bar]).baz, str), ( - Var(_js_expr="", _var_type=Union[Foo, Bar]).guess_type().foo, + Var(_js_expr="").to(Union[Foo, Bar]).foo, Union[int, None], ), ], @@ -515,7 +514,7 @@ def test_var_indexing_types(var, type_): """Test that indexing returns valid types. Args: - var : The list, typle base var. + var : The list, tuple base var. type_ : The type on indexed object. """ @@ -804,7 +803,7 @@ def test_shadow_computed_var_error(request: pytest.FixtureRequest, fixture: str) request: Fixture Request. fixture: The state fixture. """ - with pytest.raises(NameError): + with pytest.raises(UntypedComputedVarError): state = request.getfixturevalue(fixture) state.var_without_annotation.foo @@ -903,7 +902,7 @@ def test_literal_var(): True, False, None, - set([1, 2, 3]), + {1, 2, 3}, ] ) assert ( @@ -1004,7 +1003,7 @@ def test_all_number_operations(): assert ( str(even_more_complicated_number) - == "!(((Math.abs(Math.floor(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))) || (2 && Math.round(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2)))) !== 0))" + == "!(isTrue((Math.abs(Math.floor(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))) || (2 && Math.round(((Math.floor(((-((-5.4 + 1)) * 2) / 3) / 2) % 3) ** 2))))))" ) assert str(LiteralNumberVar.create(5) > False) == "(5 > 0)" @@ -1058,7 +1057,7 @@ def test_inf_and_nan(var, expected_js): assert str(var) == expected_js assert isinstance(var, NumberVar) assert isinstance(var, LiteralVar) - with pytest.raises(PrimitiveUnserializableToJSON): + with pytest.raises(PrimitiveUnserializableToJSONError): var.json() @@ -1070,19 +1069,19 @@ def test_array_operations(): assert str(array_var.reverse()) == "[1, 2, 3, 4, 5].slice().reverse()" assert ( str(ArrayVar.range(10)) - == "Array.from({ length: (10 - 0) / 1 }, (_, i) => 0 + i * 1)" + == "Array.from({ length: Math.ceil((10 - 0) / 1) }, (_, i) => 0 + i * 1)" ) assert ( str(ArrayVar.range(1, 10)) - == "Array.from({ length: (10 - 1) / 1 }, (_, i) => 1 + i * 1)" + == "Array.from({ length: Math.ceil((10 - 1) / 1) }, (_, i) => 1 + i * 1)" ) assert ( str(ArrayVar.range(1, 10, 2)) - == "Array.from({ length: (10 - 1) / 2 }, (_, i) => 1 + i * 2)" + == "Array.from({ length: Math.ceil((10 - 1) / 2) }, (_, i) => 1 + i * 2)" ) assert ( str(ArrayVar.range(1, 10, -1)) - == "Array.from({ length: (10 - 1) / -1 }, (_, i) => 1 + i * -1)" + == "Array.from({ length: Math.ceil((10 - 1) / -1) }, (_, i) => 1 + i * -1)" ) @@ -1127,7 +1126,7 @@ def test_var_component(): for _, imported_objects in var_data.imports ) - has_eval_react_component(ComponentVarState.field_var) # type: ignore + has_eval_react_component(ComponentVarState.field_var) # pyright: ignore [reportArgumentType] has_eval_react_component(ComponentVarState.computed_var) @@ -1139,15 +1138,15 @@ def test_type_chains(): List[int], ) assert ( - str(object_var.keys()[0].upper()) # type: ignore + str(object_var.keys()[0].upper()) == 'Object.keys(({ ["a"] : 1, ["b"] : 2, ["c"] : 3 })).at(0).toUpperCase()' ) assert ( - str(object_var.entries()[1][1] - 1) # type: ignore + str(object_var.entries()[1][1] - 1) == '(Object.entries(({ ["a"] : 1, ["b"] : 2, ["c"] : 3 })).at(1).at(1) - 1)' ) assert ( - str(object_var["c"] + object_var["b"]) # type: ignore + str(object_var["c"] + object_var["b"]) # pyright: ignore [reportCallIssue, reportOperatorIssue] == '(({ ["a"] : 1, ["b"] : 2, ["c"] : 3 })["c"] + ({ ["a"] : 1, ["b"] : 2, ["c"] : 3 })["b"])' ) @@ -1156,7 +1155,7 @@ def test_nested_dict(): arr = LiteralArrayVar.create([{"bar": ["foo", "bar"]}], List[Dict[str, List[str]]]) assert ( - str(arr[0]["bar"][0]) == '[({ ["bar"] : ["foo", "bar"] })].at(0)["bar"].at(0)' + str(arr[0]["bar"][0]) == '[({ ["bar"] : ["foo", "bar"] })].at(0)["bar"].at(0)' # pyright: ignore [reportIndexIssue] ) @@ -1352,7 +1351,7 @@ def test_unsupported_types_for_contains(var: Var): var: The base var. """ with pytest.raises(TypeError) as err: - assert var.contains(1) + assert var.contains(1) # pyright: ignore [reportAttributeAccessIssue] assert ( err.value.args[0] == f"Var of type {var._var_type} does not support contains check." @@ -1382,7 +1381,7 @@ def test_unsupported_types_for_string_contains(other): def test_unsupported_default_contains(): with pytest.raises(TypeError) as err: - assert 1 in Var(_js_expr="var", _var_type=str).guess_type() + assert 1 in Var(_js_expr="var", _var_type=str).guess_type() # pyright: ignore [reportOperatorIssue] assert ( err.value.args[0] == "'in' operator not supported for Var types, use Var.contains() instead." @@ -1495,8 +1494,6 @@ def test_valid_var_operations(operand1_var: Var, operand2_var, operators: List[s ) eval(f"operand1_var {operator} operand2_var") eval(f"operand2_var {operator} operand1_var") - # operand1_var.operation(op=operator, other=operand2_var) - # operand1_var.operation(op=operator, other=operand2_var, flip=True) @pytest.mark.parametrize( @@ -1773,11 +1770,9 @@ def test_invalid_var_operations(operand1_var: Var, operand2_var, operators: List print(f"testing {operator} on {operand1_var!s} and {operand2_var!s}") with pytest.raises(TypeError): print(eval(f"operand1_var {operator} operand2_var")) - # operand1_var.operation(op=operator, other=operand2_var) with pytest.raises(TypeError): print(eval(f"operand2_var {operator} operand1_var")) - # operand1_var.operation(op=operator, other=operand2_var, flip=True) @pytest.mark.parametrize( @@ -1812,16 +1807,13 @@ def cv_fget(state: BaseState) -> int: @pytest.mark.parametrize( "deps,expected", [ - (["a"], {"a"}), - (["b"], {"b"}), - ([ComputedVar(fget=cv_fget)], {"cv_fget"}), + (["a"], {None: {"a"}}), + (["b"], {None: {"b"}}), + ([ComputedVar(fget=cv_fget)], {None: {"cv_fget"}}), ], ) def test_computed_var_deps(deps: List[Union[str, Var]], expected: Set[str]): - @computed_var( - deps=deps, - cache=True, - ) + @computed_var(deps=deps) def test_var(state) -> int: return 1 @@ -1839,10 +1831,7 @@ def test_computed_var_deps(deps: List[Union[str, Var]], expected: Set[str]): def test_invalid_computed_var_deps(deps: List): with pytest.raises(TypeError): - @computed_var( - deps=deps, - cache=True, - ) + @computed_var(deps=deps) def test_var(state) -> int: return 1 @@ -1866,3 +1855,41 @@ def test_to_string_operation(): single_var = Var.create(Email()) assert single_var._var_type == Email + + +@pytest.mark.asyncio +async def test_async_computed_var(): + side_effect_counter = 0 + + class AsyncComputedVarState(BaseState): + v: int = 1 + + @computed_var(cache=True) + async def async_computed_var(self) -> int: + nonlocal side_effect_counter + side_effect_counter += 1 + return self.v + 1 + + my_state = AsyncComputedVarState() + assert await my_state.async_computed_var == 2 + assert await my_state.async_computed_var == 2 + my_state.v = 2 + assert await my_state.async_computed_var == 3 + assert await my_state.async_computed_var == 3 + assert side_effect_counter == 2 + + +def test_var_data_hooks(): + var_data_str = VarData(hooks="what") + var_data_list = VarData(hooks=["what"]) + var_data_dict = VarData(hooks={"what": None}) + assert var_data_str == var_data_list == var_data_dict + + var_data_list_multiple = VarData(hooks=["what", "whot"]) + var_data_dict_multiple = VarData(hooks={"what": None, "whot": None}) + assert var_data_list_multiple == var_data_dict_multiple + + +def test_var_data_with_hooks_value(): + var_data = VarData(hooks={"what": VarData(hooks={"whot": VarData(hooks="whott")})}) + assert var_data == VarData(hooks=["what", "whot", "whott"]) diff --git a/tests/units/utils/test_format.py b/tests/units/utils/test_format.py index cd1d0179d..89197a03e 100644 --- a/tests/units/utils/test_format.py +++ b/tests/units/utils/test_format.py @@ -262,7 +262,7 @@ def test_to_kebab_case(input: str, output: str): ], ) def test_format_string(input: str, output: str): - """Test formating the input as JS string literal. + """Test formatting the input as JS string literal. Args: input: the input string. @@ -523,7 +523,7 @@ def test_format_event_handler(input, output): input: The event handler input. output: The expected output. """ - assert format.format_event_handler(input) == output # type: ignore + assert format.format_event_handler(input) == output @pytest.mark.parametrize( @@ -582,7 +582,7 @@ formatted_router = { "input, output", [ ( - TestState(_reflex_internal_init=True).dict(), # type: ignore + TestState(_reflex_internal_init=True).dict(), # pyright: ignore [reportCallIssue] { TestState.get_full_name(): { "array": [1, 2, 3.14], @@ -615,7 +615,7 @@ formatted_router = { }, ), ( - DateTimeState(_reflex_internal_init=True).dict(), # type: ignore + DateTimeState(_reflex_internal_init=True).dict(), # pyright: ignore [reportCallIssue] { DateTimeState.get_full_name(): { "d": "1989-11-09", @@ -680,7 +680,7 @@ def test_format_array_ref(input, output): ], ) def test_format_library_name(input: str, output: str): - """Test formating a library name to remove the @version part. + """Test formatting a library name to remove the @version part. Args: input: the input string. diff --git a/tests/units/utils/test_serializers.py b/tests/units/utils/test_serializers.py index 355f40d3f..e5a47abaa 100644 --- a/tests/units/utils/test_serializers.py +++ b/tests/units/utils/test_serializers.py @@ -222,9 +222,10 @@ def test_serialize(value: Any, expected: str): '"2021-01-01 01:01:01.000001"', True, ), + (datetime.date(2021, 1, 1), '"2021-01-01"', True), (Color(color="slate", shade=1), '"var(--slate-1)"', True), (BaseSubclass, '"BaseSubclass"', True), - (Path("."), '"."', True), + (Path(), '"."', True), ], ) def test_serialize_var_to_str(value: Any, expected: str, exp_var_is_string: bool): diff --git a/tests/units/utils/test_utils.py b/tests/units/utils/test_utils.py index dd1a3b3ef..44356dac5 100644 --- a/tests/units/utils/test_utils.py +++ b/tests/units/utils/test_utils.py @@ -31,7 +31,7 @@ def get_above_max_version(): """ semantic_version_list = constants.Bun.VERSION.split(".") - semantic_version_list[-1] = str(int(semantic_version_list[-1]) + 1) # type: ignore + semantic_version_list[-1] = str(int(semantic_version_list[-1]) + 1) # pyright: ignore [reportArgumentType, reportCallIssue] return ".".join(semantic_version_list) @@ -122,13 +122,13 @@ def test_validate_invalid_bun_path(mocker): Args: mocker: Pytest mocker object. """ - mock = mocker.Mock() - mocker.patch.object(mock, "bun_path", return_value="/mock/path") - mocker.patch("reflex.utils.prerequisites.get_config", mock) + mock_path = mocker.Mock() + mocker.patch("reflex.utils.path_ops.get_bun_path", return_value=mock_path) mocker.patch("reflex.utils.prerequisites.get_bun_version", return_value=None) with pytest.raises(typer.Exit): prerequisites.validate_bun() + mock_path.samefile.assert_called_once() def test_validate_bun_path_incompatible_version(mocker): @@ -137,9 +137,8 @@ def test_validate_bun_path_incompatible_version(mocker): Args: mocker: Pytest mocker object. """ - mock = mocker.Mock() - mocker.patch.object(mock, "bun_path", return_value="/mock/path") - mocker.patch("reflex.utils.prerequisites.get_config", mock) + mock_path = mocker.Mock() + mocker.patch("reflex.utils.path_ops.get_bun_path", return_value=mock_path) mocker.patch( "reflex.utils.prerequisites.get_bun_version", return_value=version.parse("0.6.5"), @@ -270,7 +269,7 @@ def test_unsupported_literals(cls: type): ("appname2.io", "AppnameioConfig"), ], ) -def test_create_config(app_name, expected_config_name, mocker): +def test_create_config(app_name: str, expected_config_name: str, mocker): """Test templates.RXCONFIG is formatted with correct app name and config class name. Args: @@ -278,7 +277,7 @@ def test_create_config(app_name, expected_config_name, mocker): expected_config_name: Expected config name. mocker: Mocker object. """ - mocker.patch("builtins.open") + mocker.patch("pathlib.Path.write_text") tmpl_mock = mocker.patch("reflex.compiler.templates.RXCONFIG") prerequisites.create_config(app_name) tmpl_mock.render.assert_called_with( @@ -298,7 +297,7 @@ def tmp_working_dir(tmp_path): Yields: subdirectory of tmp_path which is now the current working directory. """ - old_pwd = Path(".").resolve() + old_pwd = Path.cwd() working_dir = tmp_path / "working_dir" working_dir.mkdir() os.chdir(working_dir) @@ -464,7 +463,7 @@ def test_node_install_unix(tmp_path, mocker, machine, system): mocker.patch("httpx.stream", return_value=Resp()) download = mocker.patch("reflex.utils.prerequisites.download_and_extract_fnm_zip") process = mocker.patch("reflex.utils.processes.new_process") - chmod = mocker.patch("reflex.utils.prerequisites.os.chmod") + chmod = mocker.patch("pathlib.Path.chmod") mocker.patch("reflex.utils.processes.stream_logs") prerequisites.install_node() @@ -587,9 +586,7 @@ def test_style_prop_with_event_handler_value(callable): } with pytest.raises(ReflexError): - rx.box( - style=style, # type: ignore - ) + rx.box(style=style) # pyright: ignore [reportArgumentType] def test_is_prod_mode() -> None: diff --git a/tests/units/vars/test_base.py b/tests/units/vars/test_base.py index 68bc0c38e..8f9e99fe4 100644 --- a/tests/units/vars/test_base.py +++ b/tests/units/vars/test_base.py @@ -1,8 +1,9 @@ -from typing import Dict, List, Union +from typing import List, Mapping, Union import pytest -from reflex.vars.base import figure_out_type +from reflex.state import State +from reflex.vars.base import computed_var, figure_out_type class CustomDict(dict[str, str]): @@ -37,13 +38,26 @@ class ChildGenericDict(GenericDict): ("a", str), ([1, 2, 3], List[int]), ([1, 2.0, "a"], List[Union[int, float, str]]), - ({"a": 1, "b": 2}, Dict[str, int]), - ({"a": 1, 2: "b"}, Dict[Union[int, str], Union[str, int]]), + ({"a": 1, "b": 2}, Mapping[str, int]), + ({"a": 1, 2: "b"}, Mapping[Union[int, str], Union[str, int]]), (CustomDict(), CustomDict), (ChildCustomDict(), ChildCustomDict), - (GenericDict({1: 1}), Dict[int, int]), - (ChildGenericDict({1: 1}), Dict[int, int]), + (GenericDict({1: 1}), Mapping[int, int]), + (ChildGenericDict({1: 1}), Mapping[int, int]), ], ) def test_figure_out_type(value, expected): assert figure_out_type(value) == expected + + +def test_computed_var_replace() -> None: + class StateTest(State): + @computed_var(cache=True) + def cv(self) -> int: + return 1 + + cv = StateTest.cv + assert cv._var_type is int + + replaced = cv._replace(_var_type=float) + assert replaced._var_type is float diff --git a/tests/units/vars/test_object.py b/tests/units/vars/test_object.py index efcb21166..90e34be96 100644 --- a/tests/units/vars/test_object.py +++ b/tests/units/vars/test_object.py @@ -1,10 +1,14 @@ +import dataclasses + import pytest +from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, mapped_column from typing_extensions import assert_type import reflex as rx from reflex.utils.types import GenericType from reflex.vars.base import Var from reflex.vars.object import LiteralObjectVar, ObjectVar +from reflex.vars.sequence import ArrayVar class Bare: @@ -32,14 +36,44 @@ class Base(rx.Base): quantity: int = 0 +class SqlaBase(DeclarativeBase, MappedAsDataclass): + """Sqlalchemy declarative mapping base class.""" + + pass + + +class SqlaModel(SqlaBase): + """A sqlalchemy model with a single attribute.""" + + __tablename__: str = "sqla_model" + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True, init=False) + quantity: Mapped[int] = mapped_column(default=0) + + +@dataclasses.dataclass +class Dataclass: + """A dataclass with a single attribute.""" + + quantity: int = 0 + + class ObjectState(rx.State): - """A reflex state with bare and base objects.""" + """A reflex state with bare, base and sqlalchemy base vars.""" bare: rx.Field[Bare] = rx.field(Bare()) + bare_optional: rx.Field[Bare | None] = rx.field(None) base: rx.Field[Base] = rx.field(Base()) + base_optional: rx.Field[Base | None] = rx.field(None) + sqlamodel: rx.Field[SqlaModel] = rx.field(SqlaModel()) + sqlamodel_optional: rx.Field[SqlaModel | None] = rx.field(None) + dataclass: rx.Field[Dataclass] = rx.field(Dataclass()) + dataclass_optional: rx.Field[Dataclass | None] = rx.field(None) + + base_list: rx.Field[list[Base]] = rx.field([Base()]) -@pytest.mark.parametrize("type_", [Base, Bare]) +@pytest.mark.parametrize("type_", [Base, Bare, SqlaModel, Dataclass]) def test_var_create(type_: GenericType) -> None: my_object = type_() var = Var.create(my_object) @@ -49,7 +83,7 @@ def test_var_create(type_: GenericType) -> None: assert quantity._var_type is int -@pytest.mark.parametrize("type_", [Base, Bare]) +@pytest.mark.parametrize("type_", [Base, Bare, SqlaModel, Dataclass]) def test_literal_create(type_: GenericType) -> None: my_object = type_() var = LiteralObjectVar.create(my_object) @@ -59,7 +93,7 @@ def test_literal_create(type_: GenericType) -> None: assert quantity._var_type is int -@pytest.mark.parametrize("type_", [Base, Bare]) +@pytest.mark.parametrize("type_", [Base, Bare, SqlaModel, Dataclass]) def test_guess(type_: GenericType) -> None: my_object = type_() var = Var.create(my_object) @@ -70,7 +104,7 @@ def test_guess(type_: GenericType) -> None: assert quantity._var_type is int -@pytest.mark.parametrize("type_", [Base, Bare]) +@pytest.mark.parametrize("type_", [Base, Bare, SqlaModel, Dataclass]) def test_state(type_: GenericType) -> None: attr_name = type_.__name__.lower() var = getattr(ObjectState, attr_name) @@ -80,7 +114,7 @@ def test_state(type_: GenericType) -> None: assert quantity._var_type is int -@pytest.mark.parametrize("type_", [Base, Bare]) +@pytest.mark.parametrize("type_", [Base, Bare, SqlaModel, Dataclass]) def test_state_to_operation(type_: GenericType) -> None: attr_name = type_.__name__.lower() original_var = getattr(ObjectState, attr_name) @@ -100,3 +134,29 @@ def test_typing() -> None: # Base var = ObjectState.base _ = assert_type(var, ObjectVar[Base]) + optional_var = ObjectState.base_optional + _ = assert_type(optional_var, ObjectVar[Base | None]) + list_var = ObjectState.base_list + _ = assert_type(list_var, ArrayVar[list[Base]]) + list_var_0 = list_var[0] + _ = assert_type(list_var_0, ObjectVar[Base]) + + # Sqla + var = ObjectState.sqlamodel + _ = assert_type(var, ObjectVar[SqlaModel]) + optional_var = ObjectState.sqlamodel_optional + _ = assert_type(optional_var, ObjectVar[SqlaModel | None]) + list_var = ObjectState.base_list + _ = assert_type(list_var, ArrayVar[list[Base]]) + list_var_0 = list_var[0] + _ = assert_type(list_var_0, ObjectVar[Base]) + + # Dataclass + var = ObjectState.dataclass + _ = assert_type(var, ObjectVar[Dataclass]) + optional_var = ObjectState.dataclass_optional + _ = assert_type(optional_var, ObjectVar[Dataclass | None]) + list_var = ObjectState.base_list + _ = assert_type(list_var, ArrayVar[list[Base]]) + list_var_0 = list_var[0] + _ = assert_type(list_var_0, ObjectVar[Base])