diff --git a/.fvmrc b/.fvmrc index 94d87f3405..01d72c9029 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.41.7" + "flutter": "3.44.2" } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ecda3f1cf6..6f3fa41a97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,10 +53,10 @@ jobs: python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ] steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.2.0 with: python-version: ${{ matrix.python-version }} @@ -85,7 +85,7 @@ jobs: - name: Upload docs-coverage logs if: matrix.python-version == '3.12' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: docs-coverage path: sdk/python/docstr_coverage.log @@ -98,18 +98,18 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: 20 + node-version: 24 - name: Enable Corepack run: corepack enable - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.2.0 - name: Build website run: | @@ -131,20 +131,20 @@ jobs: PYPI_VER: ${{ steps.versions.outputs.PYPI_VER }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 # fetch all history fetch-tags: true # ensure tags are available - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.2.0 - name: Compute versions id: versions run: source "${SCRIPTS}/update_build_version.sh" - name: Setup Dart (OIDC for pub.dev) - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + uses: dart-lang/setup-dart@v1.7.2 - name: Setup Flutter uses: kuhnroyal/flutter-fvm-config-action/setup@v3 @@ -179,10 +179,10 @@ jobs: PKG_VER: ${{ needs.build_flet_package.outputs.PKG_VER }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.2.0 - name: Patch build template flet version shell: bash @@ -207,7 +207,7 @@ jobs: zip -r "$GITHUB_WORKSPACE/flet-app-templates.zip" app/ - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: template-artifacts path: | @@ -219,7 +219,7 @@ jobs: # ============================= build_windows: name: Build Flet Client for Windows - runs-on: windows-latest + runs-on: windows-2025-vs2026 needs: - python_tests - build_flet_package @@ -229,10 +229,10 @@ jobs: PYPI_VER: ${{ needs.build_flet_package.outputs.PYPI_VER }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.2.0 - name: Setup Flutter uses: kuhnroyal/flutter-fvm-config-action/setup@v3 @@ -262,7 +262,7 @@ jobs: 7z a "${ROOT}/client/flet-windows.zip" "flet" - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: windows-artifacts if-no-files-found: error @@ -284,10 +284,10 @@ jobs: PYPI_VER: ${{ needs.build_flet_package.outputs.PYPI_VER }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.2.0 - name: Setup Flutter uses: kuhnroyal/flutter-fvm-config-action/setup@v3 @@ -303,7 +303,7 @@ jobs: tar -czvf flet-macos.tar.gz -C build/macos/Build/Products/Release Flet.app - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: macos-artifacts if-no-files-found: error @@ -442,10 +442,10 @@ jobs: apt-get install -y git curl unzip xz-utils zip ca-certificates jq tzdata - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.2.0 - name: Get Flutter version from ".fvmrc" uses: kuhnroyal/flutter-fvm-config-action/config@v3 @@ -475,7 +475,7 @@ jobs: sed -i.bak '/apt.postgresql.org/s/^/# /' /etc/apt/sources.list fi apt-get update --allow-releaseinfo-change - LINUX_DEPS="$(uv run --project sdk/python/packages/flet python -c 'from flet.utils.linux_deps import linux_dependencies; print(" ".join(linux_dependencies))')" + LINUX_DEPS="$(uv run --project sdk/python/packages/flet-cli flet --version --json | jq -r '.linux_dependencies | join(" ")')" apt-get install -y $LINUX_DEPS - name: Build Flutter Linux clients @@ -536,7 +536,7 @@ jobs: build_flutter "flet-desktop" - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: linux-${{ matrix.distro_id }}-${{ matrix.arch }}-artifacts if-no-files-found: error @@ -556,10 +556,10 @@ jobs: PYPI_VER: ${{ needs.build_flet_package.outputs.PYPI_VER }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.2.0 - name: Setup Flutter uses: kuhnroyal/flutter-fvm-config-action/setup@v3 @@ -578,7 +578,7 @@ jobs: shell: bash working-directory: ${{ env.SDK_PYTHON }} run: | - PYODIDE_VERSION="$( uv run python -c 'import flet.version; print(flet.version.pyodide_version)' )" + PYODIDE_VERSION="$( uv run python -c "from flet_cli.utils.python_versions import resolve_python_version; print(resolve_python_version(None).pyodide)" )" echo "PYODIDE_VERSION=$PYODIDE_VERSION" >> "$GITHUB_ENV" echo "Pyodide version: $PYODIDE_VERSION" @@ -586,10 +586,6 @@ jobs: shell: bash working-directory: client run: | - # Compute Pyodide URLs - PYODIDE_URL="https://github.com/pyodide/pyodide/releases/download/${PYODIDE_VERSION}/pyodide-core-${PYODIDE_VERSION}.tar.bz2" - PYODIDE_CDN_URL="https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full" - FLET_WEB="${SDK_PYTHON}/packages/flet-web/src/flet_web" flutter build web --wasm @@ -600,13 +596,9 @@ jobs: FLUTTER_JS_DIR="$(dirname "$(command -v flutter)")/cache/flutter_web_sdk/flutter_js" cp "$FLUTTER_JS_DIR/flutter.js.map" "${FLET_WEB}/web" - # Download the Pyodide tarball and extract its contents into the web build folder - curl -L "$PYODIDE_URL" | tar -xj -C "${FLET_WEB}/web" - - # Download the prebuilt pyodide wheels - for wheel in "packaging-24.2-py3-none-any.whl" "micropip-0.8.0-py3-none-any.whl"; do - curl -L "${PYODIDE_CDN_URL}/${wheel}" -o "${FLET_WEB}/web/pyodide/${wheel}" - done + # Download the Pyodide runtime (plus the micropip/packaging wheels + # resolved from pyodide-lock.json) into the web build folder + uv run --project "${SDK_PYTHON}" python -c "from pathlib import Path; from flet_cli.utils.pyodide import ensure_pyodide; ensure_pyodide('${PYODIDE_VERSION}', Path('${FLET_WEB}/web/pyodide'))" # Archive the web client into a gzipped tarball tar -czvf "flet-web.tar.gz" -C "build/web" . @@ -621,7 +613,7 @@ jobs: uv build --package flet-web --sdist - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: web-artifacts if-no-files-found: error @@ -644,13 +636,13 @@ jobs: PKG_VER: ${{ needs.build_flet_package.outputs.PKG_VER }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 fetch-tags: true - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.2.0 - name: Setup Flutter uses: kuhnroyal/flutter-fvm-config-action/setup@v3 @@ -702,7 +694,7 @@ jobs: done - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: flet-python-extensions if-no-files-found: error @@ -723,10 +715,10 @@ jobs: - build_flet_package steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.2.0 - name: Build Python packages shell: bash @@ -739,7 +731,7 @@ jobs: uv build --package flet-desktop - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: flet-cli-desktop-python-distribution path: | @@ -775,7 +767,7 @@ jobs: fi - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: dist merge-multiple: true @@ -808,13 +800,13 @@ jobs: - release steps: - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.2.0 with: ignore-empty-workdir: true cache-dependency-glob: "" - name: Download artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: dist merge-multiple: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index eb1b594afd..ccf87d36e5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -9,18 +9,18 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: 20 + node-version: 24 - name: Enable Corepack run: corepack enable - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.2.0 - name: Build website run: | diff --git a/.github/workflows/flet-build-image.yml b/.github/workflows/flet-build-image.yml index f64a3f13ba..14ce106dfa 100644 --- a/.github/workflows/flet-build-image.yml +++ b/.github/workflows/flet-build-image.yml @@ -26,7 +26,7 @@ jobs: flutter_version: ${{ steps.v.outputs.value }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Resolve version id: v @@ -55,7 +55,7 @@ jobs: FLUTTER_VERSION: ${{ needs.resolve_version.outputs.flutter_version }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Compute platform pair id: pair @@ -94,7 +94,7 @@ jobs: touch "/tmp/digests/${digest#sha256:}" - name: Upload digest - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: flet-build-digests-${{ steps.pair.outputs.value }} path: /tmp/digests/* @@ -111,7 +111,7 @@ jobs: FLUTTER_VERSION: ${{ needs.resolve_version.outputs.flutter_version }} steps: - name: Download digests - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: /tmp/digests pattern: flet-build-digests-* diff --git a/.github/workflows/flet-build-test-matrix.yml b/.github/workflows/flet-build-test-matrix.yml index 00a8318c36..29d3b187e8 100644 --- a/.github/workflows/flet-build-test-matrix.yml +++ b/.github/workflows/flet-build-test-matrix.yml @@ -52,7 +52,7 @@ jobs: needs_linux_deps: false - name: windows - runner: windows-latest + runner: windows-2025-vs2026 build_cmd: "flet build windows" artifact_name: windows-build-artifact artifact_path: build/windows @@ -74,7 +74,7 @@ jobs: needs_linux_deps: false - name: aab-windows - runner: windows-latest + runner: windows-2025-vs2026 build_cmd: "flet build aab" artifact_name: aab-build-windows-artifact artifact_path: build/aab @@ -95,7 +95,7 @@ jobs: needs_linux_deps: false - name: apk-windows - runner: windows-latest + runner: windows-2025-vs2026 build_cmd: "flet build apk" artifact_name: apk-build-windows-artifact artifact_path: build/apk @@ -132,7 +132,7 @@ jobs: needs_linux_deps: false - name: web-windows - runner: windows-latest + runner: windows-2025-vs2026 build_cmd: "flet build web" artifact_name: web-build-windows-artifact artifact_path: build/web @@ -140,13 +140,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 fetch-tags: true - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.2.0 - name: Patch versions shell: bash @@ -160,7 +160,7 @@ jobs: shell: bash run: | sudo apt update --allow-releaseinfo-change - LINUX_DEPS="$(uv run --project sdk/python/packages/flet python -c 'from flet.utils.linux_deps import linux_dependencies; print(" ".join(linux_dependencies))')" + LINUX_DEPS="$(uv run --project sdk/python/packages/flet-cli flet --version --json | jq -r '.linux_dependencies | join(" ")')" sudo apt-get install -y --no-install-recommends $LINUX_DEPS sudo apt-get clean @@ -178,7 +178,7 @@ jobs: uv run ${{ matrix.build_cmd }} --python-version ${{ inputs.python_version }} --yes --verbose --build-number ${{ github.run_number }} $FLET_BUILD_EXTRA_ARGS - name: Upload Artifact - uses: actions/upload-artifact@v5.0.0 + uses: actions/upload-artifact@v7 with: name: ${{ matrix.artifact_name }}-py${{ inputs.python_version }} path: sdk/python/examples/apps/flet_build_test/${{ matrix.artifact_path }} diff --git a/.github/workflows/flet-build-test.yml b/.github/workflows/flet-build-test.yml index df9312743d..2c459bc24d 100644 --- a/.github/workflows/flet-build-test.yml +++ b/.github/workflows/flet-build-test.yml @@ -107,17 +107,17 @@ jobs: runner: macos-26 - name: windows - runner: windows-latest + runner: windows-2025-vs2026 steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 fetch-tags: true - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.2.0 - name: Setup Flutter uses: kuhnroyal/flutter-fvm-config-action/setup@v3 @@ -137,7 +137,7 @@ jobs: shell: bash run: | sudo apt update --allow-releaseinfo-change - LINUX_DEPS="$(uv run --project sdk/python/packages/flet python -c 'from flet.utils.linux_deps import linux_dependencies; print(" ".join(linux_dependencies))')" + LINUX_DEPS="$(uv run --project sdk/python/packages/flet-cli flet --version --json | jq -r '.linux_dependencies | join(" ")')" sudo apt-get install -y --no-install-recommends $LINUX_DEPS sudo apt-get clean @@ -185,7 +185,7 @@ jobs: uv run --with pyinstaller flet pack src/main.py --yes --name flet-pack-test --distpath dist $FLET_PACK_EXTRA_ARGS - name: Upload Artifact - uses: actions/upload-artifact@v5.0.0 + uses: actions/upload-artifact@v7 with: name: ${{ matrix.name }}-pack-artifact path: sdk/python/examples/apps/flet_build_test/dist diff --git a/.github/workflows/macos-integration-tests.yml b/.github/workflows/macos-integration-tests.yml index 396e5f1be5..6dad23a0de 100644 --- a/.github/workflows/macos-integration-tests.yml +++ b/.github/workflows/macos-integration-tests.yml @@ -55,10 +55,10 @@ jobs: name: ${{ matrix.suite }} Integration Tests steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@v8.2.0 - name: Setup Flutter uses: kuhnroyal/flutter-fvm-config-action/setup@v3 @@ -91,7 +91,7 @@ jobs: - name: Upload artifact if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: integration-test-failures-macos-${{ env.SAFE_SUITE }} path: sdk/python/packages/flet/integration_tests/${{ matrix.suite }}/**/*_actual.png diff --git a/.github/workflows/release-pr-changelog.yml b/.github/workflows/release-pr-changelog.yml index f3b6d4aea8..e039e9c723 100644 --- a/.github/workflows/release-pr-changelog.yml +++ b/.github/workflows/release-pr-changelog.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/CHANGELOG.md b/CHANGELOG.md index e82cd42c50..35380e6d0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,20 +2,30 @@ ### New features -* Multi-version bundled CPython support in `flet build` and `flet publish`. Pick the runtime your app ships with via the new `--python-version` flag (3.12 / 3.13 / 3.14), or let it be derived from `[project].requires-python` in your `pyproject.toml`; defaults to the latest supported stable (currently 3.14). The matching CPython-standalone build, Pyodide release (0.27.7 / 0.29.4 / 314.0.0a2), and Emscripten wheel platform tag are all resolved from a central registry. Adding a future pre-release CPython line (e.g. 3.15 beta) is a one-row append with `prerelease=True` — opt-in only via an explicit `--python-version 3.15` or `requires-python = "==3.15.*"`, never the auto-resolved default. Requires `serious_python` >= 2.0.0, now pinned in the `flet build` template. See the new [Choosing a Python version](https://flet.dev/docs/publish#choosing-a-python-version) docs section ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. +* Multi-version bundled CPython support in `flet build` and `flet publish`. Pick the runtime your app ships with via the new `--python-version` flag (3.12 / 3.13 / 3.14), or let it be derived from `[project].requires-python` in your `pyproject.toml`; defaults to the latest supported stable (currently 3.14). The matching CPython-standalone build, Pyodide release (0.27.7 / 0.29.4 / 314.0.0), and Emscripten wheel platform tag are all resolved from `flet-dev/python-build`'s date-keyed manifest. Adding a future pre-release CPython line (e.g. 3.15 beta) is a one-row append with `prerelease=True` — opt-in only via an explicit `--python-version 3.15` or `requires-python = "==3.15.*"`, never the auto-resolved default. Requires `serious_python` >= 3.0.0, now pinned in the `flet build` template. See the new [Choosing a Python version](https://flet.dev/docs/publish#choosing-a-python-version) docs section ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. +* Add `ft.DataChannel`: dedicated byte channels for widgets that move bulk binary data (image frames, audio buffers, ML tensors) between Dart and Python, bypassing the MsgPack control protocol. The Dart side opens a channel via `FletBackend.of(context).openDataChannel()` and announces it to Python by firing a `data_channel_open` control event with `{channel_name, channel_id}`; the Python side declares `on_data_channel_open: Optional[ft.EventHandler[ft.DataChannelOpenEvent]]` and captures the channel via `self.get_data_channel(e.channel_id)`. Backed by a dedicated `PythonBridge` per channel in embedded native mode (4–7 GiB/s on M2 Pro) and by the default `ProtocolMuxedDataChannelFactory` in dev / web modes (raw-byte frames muxed over the active Flet protocol transport with a 1-byte type discriminator). Pyodide gets zero-copy outbound sends via `postMessage` Transferable ArrayBuffer. First consumer: `flet-charts` `MatplotlibChartCanvas`, migrated from `_invoke_method` PNG dispatch to a 1-byte-opcode data channel by @FeodorFitsner. +* **In-process Python transport (`dart_bridge` FFI).** `package:flet` gains a third protocol transport alongside the UDS / TCP socket servers: it can run Flet's MsgPack protocol over an in-process `dart_bridge` byte channel via a `FletApp(channelBuilder: …)` seam (the `flet` package stays Python-independent — it doesn't depend on `serious_python` or know about `PythonBridge`; the embedder wires the channel). `serious_python` >= 3.0.0 uses this seam to embed the Python interpreter **in-process** instead of talking to it over a localhost socket, and the `flet build` template migrates from sockets to the FFI transport. On Android, where the OS may keep the process alive across a back-button quit and restart only the Dart VM, the transport rebinds to the new VM's `dart_bridge` ports on a session-restart signal (`libdart_bridge` >= 1.3.0) — the Python process and its in-memory state are preserved and the Flet session is rebuilt from `REGISTER_CLIENT` by @FeodorFitsner. * Add `flet clean` command that deletes the `build` directory of a Flet app — the Flutter bootstrap project, cached artifacts, and generated output — in a single step ([#6233](https://github.com/flet-dev/flet/issues/6233)) by @ndonkoHenri. * Add `compression_quality` to `FilePicker.pick_files()` for selecting the image compression quality used by supported platforms ([#6573](https://github.com/flet-dev/flet/pull/6573)) by @ndonkoHenri. ### Improvements +* **Smaller Android apps with no native-packaging config.** `flet build apk`/`aab` consume serious_python's new Android packaging: Python extension modules load **memory-mapped directly from the APK** (no extraction to disk), and pure Python ships in stored asset zips read via `zipimport`, so the standard library is no longer duplicated per ABI. Apps no longer need `useLegacyPackaging` / `keepDebugSymbols` — the `flet build` Android template drops them; just use `minSdk 23+`. New `--android-extract-packages` flag and `[tool.flet.android].extract_packages` ship "path-hungry" packages — those that read bundled data via `__file__` / `pkg_resources` instead of `importlib.resources` — extracted to disk instead of inside the zip (most packages, including `certifi`, are zip-safe and need no entry). Requires `serious_python` with the native-mmap packaging (dart_bridge 1.4.0). * Pyodide is no longer pre-baked into the `flet build` template. Each `flet build web` / `flet publish` run downloads the matching `pyodide-core-.tar.bz2` (plus the runtime `micropip` and `packaging` wheels) into a per-version cache at `~/.flet/pyodide//` and copies the files into the build output. Subsequent builds reuse the cache; the older `0.27.5` bundle previously shipped in the cookiecutter template is gone ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. -* `flet --version` now lists the supported Python versions newest first, each with its matching Pyodide release and a `default` / `pre-release` annotation where applicable, instead of the single static `Pyodide: …` line. The global `flet.version.pyodide_version` export is removed (the only external consumer was the CLI version output, now updated) ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. +* The supported Python / Pyodide / dart_bridge versions are loaded on demand from `flet-dev/python-build`'s date-keyed `manifest.json` (fetched once and cached under `~/.flet/cache`), the single source of truth shared with `serious_python` — replacing flet's hand-mirrored version table. `flet build` forwards only `SERIOUS_PYTHON_VERSION` and lets `serious_python` derive the full version / build date / dart_bridge version from its own committed snapshot. The module exposes `get_supported_python_versions()` / `get_default_python_version()` (the previous `SUPPORTED_PYTHON_VERSIONS` / `DEFAULT_PYTHON_VERSION` constants are removed) ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. +* `flet --version` shows just the Flet and Flutter versions; the static `Pyodide: …` line and the global `flet.version.pyodide_version` export are removed (the supported Python / Pyodide set now lives in python-build's manifest, not the CLI output) ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. +* `flet --version --json` emits a machine-readable document — Flet/Flutter versions and the Linux build dependencies — for CI to read via `jq` instead of importing Flet internals with `python -c`. (The supported Python/Pyodide table is no longer included; it comes from python-build's manifest.) The canonical Linux apt dependency list moved from `flet.utils.linux_deps` (runtime package) to `flet_cli.utils.linux_deps` (build tooling) by @FeodorFitsner. * `client/web/python.js` and the build template's `python.js` no longer hardcode `defaultPyodideUrl`. `patch_index.py` now injects `flet.pyodideUrl` per build (CDN URL by default, or the local `pyodide/pyodide.js` path under `--no-cdn`) so the runtime URL always tracks the resolved Pyodide release ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. +* Stream-oriented Flet protocol transports (UDS / TCP used by `flet run` dev mode) now use length-prefixed framing instead of streaming `msgpack.Unpacker.feed`. Combined with a new 1-byte type discriminator at the head of every packet (`0x00` = MsgPack control frame, `0x01` = raw DataChannel frame), this unifies framing across all transports (sockets, WebSocket, `dart_bridge` FFI, Pyodide `postMessage`). `StreamingMsgpackDeserializer` is removed from `package:flet`; each inbound packet is one complete MsgPack value, decoded one-shot via `msgpack.deserialize(bytes)` by @FeodorFitsner. +* Bump the bundled Flutter to **3.44.2** (from 3.41.7). The Flet client and the `flet build` template migrate to Flutter 3.44's built-in Kotlin (the Android app no longer applies the Kotlin Gradle plugin itself) and Java 17; the client's Gradle wrapper moves to 8.14 by @FeodorFitsner. +* **Faster mobile cold start: `import flet` is now lazy.** The `flet` package previously executed its full ~270-module public API eagerly on `import flet`; it now resolves public names on first access via a module-level `__getattr__` (PEP 562), so an app loads only the modules it actually uses. On a mid-range Android device this cut `import flet` from ~2.0s to ~0.15s. The eager subsystem clusters that `Page` pulled in (auth, components/hooks, Cupertino controls) are deferred too. Type checkers, IDEs, and `from flet import *` are unaffected ([#6597](https://github.com/flet-dev/flet/pull/6597)) by @FeodorFitsner. ### Breaking changes * `flet build` and `flet publish` now bundle CPython 3.14 by default (previously 3.12, implicit via the old single-version `serious_python`). Existing apps that depend on native wheels without 3.14 binaries should pin explicitly with `--python-version 3.12` (CLI), `requires-python = ">=3.12,<3.13"` (pyproject), or `SERIOUS_PYTHON_VERSION=3.12` in the build environment ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. -* The `flet.version.pyodide_version` module attribute and the `PYODIDE_VERSION` constant are removed. Reach for `flet_cli.utils.python_versions.SUPPORTED_PYTHON_VERSIONS` if you need the per-version Pyodide mapping programmatically ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. +* Android builds now include only the ABIs the bundled Python supports: for Python 3.13+, `armeabi-v7a` is no longer supported nor packaged by default. An explicit `--arch armeabi-v7a` fails with a clear error unless combined with `--python-version 3.12`, which is the only Python version supporting it. The per-version ABI set is sourced from python-build's manifest (`pythons..android_abis`), not hardcoded in flet ([#6578](https://github.com/flet-dev/flet/pull/6578)) by @ndonkoHenri. +* `flet build` / `flet publish` now **compile your app and packages to `.pyc` by default** (previously off). This removes per-launch bytecode recompilation — a significant cold-start win, especially on mobile where pure Python is imported from a stored zip (`zipimport`) and can't cache bytecode back to disk, so every module would otherwise recompile from source on each launch. The CLI flags gain `--no-compile-app` / `--no-compile-packages` (via `argparse.BooleanOptionalAction`; the existing `--compile-app` / `--compile-packages` still work), and `[tool.flet.compile].app` / `.packages` now default to `true`. Pass `--no-compile-*` or set them to `false` to restore the old behavior (faster iterative builds, or keeping `.py` source in the bundle). Compiled web builds were verified to load in Pyodide (bundled CPython and Pyodide share the same minor version). See the [compile-on-by-default](/docs/updates/breaking-changes/v0-86-0-compile-on-by-default) guide ([#6598](https://github.com/flet-dev/flet/pull/6598)) by @FeodorFitsner. +* Flet protocol wire format on stream-oriented transports (UDS / TCP) is incompatible with pre-0.86 servers and clients. Every packet now starts with a 4-byte little-endian length prefix and a 1-byte type discriminator (`0x00` = MsgPack control frame, `0x01` = raw DataChannel frame). WebSocket / `postMessage` / `dart_bridge` transports keep native message boundaries and only gain the type byte. The Flet CLI dev server and the in-process Python runtime are upgraded in lockstep — running `flet run` with mismatched `flet` versions across CLI and runtime is no longer supported. See the [DataChannel protocol framing upgrade](/docs/updates/breaking-changes/v0-86-0-data-channel-protocol-upgrade) guide. The `MatplotlibChartCanvas` widget transports its full / diff / clear frames via a `DataChannel` rather than `_invoke_method` arguments — visually identical, but custom code that subclassed it and overrode the apply methods may need updating by @FeodorFitsner. ### Deprecations @@ -24,6 +34,11 @@ ### Bug fixes * Fix `flet build` failing on Windows when a dependency is pulled in via `[tool.flet.].dev_packages` (or any local-path install): the rewritten ` @ file://` URL now uses `Path.as_uri()`, producing the correct `file:///D:/...` three-slash form instead of `file://D:\...`, which pip on Windows parsed as a UNC path and aborted with `OSError: [Errno 2] No such file or directory: '\\\\D:\\a\\...'` ([#6577](https://github.com/flet-dev/flet/pull/6577)) by @FeodorFitsner. +* Fix `flet build web --python-version 3.13` failing to match any Pyodide-built native wheel. The 3.13 row in the Python version registry was set to Pyodide platform tag `pyodide-2025.0-wasm32`, but Pyodide actually publishes 0.29 wheels under `pyemscripten_2025_0_wasm32` (the `pyodide_` → `pyemscripten_` prefix transition happened at 0.28/0.29, not at 314.0). Corrected to `pyemscripten-2025.0-wasm32` so pip's wheel selection picks up the correct tags by @FeodorFitsner. +* `flet build` now cleans the build directory when the bundled Python version changes between builds, preventing stale compiled bytecode from the previous version crashing the app at runtime with `ImportError: bad magic number` by @FeodorFitsner. +* Fix locating Flet controls by their user-assigned `key` in tests. `ValueKey(control.key)` was constructed as `ValueKey`, and Flutter's runtimeType-strict `ValueKey.==` never matches that against the `ValueKey` the rendered widget carries — so `find.byKey(Key('foo'))` (flutter_test) and `find_by_key('foo')` (Flet tester) located 0 widgets. The `ValueKey` is now built with the value's concrete type (String → `ValueKey`, int → `ValueKey`, …) on both the Dart and Python sides by @FeodorFitsner. +* Fix `flet build apk` / `flet build aab` with `--arch` packaging native libraries for *all* Android ABIs instead of only the requested ones. The requested architectures are now forwarded to Flutter as `--target-platform` (so `--split-per-abi` builds only the requested splits), unrequested ABI directories are excluded from the artifact via `packaging.jniLibs.excludes`, Android `--arch` values are validated against the bundled Python's supported ABIs, multiple `--arch` values now correctly reach `serious_python` (comma-joined), and stale artifacts from previous builds are no longer copied into the output directory ([#6567](https://github.com/flet-dev/flet/issues/6567), [#6578](https://github.com/flet-dev/flet/pull/6578)) by @ndonkoHenri. +* Fix repeated `--arch`, `--source-packages` and `--permissions` flags in `flet build` keeping only the values of the last occurrence (`action="extend"` on each) ([#6578](https://github.com/flet-dev/flet/pull/6578)) by @ndonkoHenri. * Fix `flet build apk` failing at `mergeDebugNativeLibs` with `N files found with path 'lib//libc++_shared.so'` when an app combines `serious_python_android` with another Flutter plugin that also bundles the NDK C++ runtime ([#6570](https://github.com/flet-dev/flet/issues/6570), [#6571](https://github.com/flet-dev/flet/pull/6571)) by @ndonkoHenri. * Specify `handler` signatures in `subscribe` and `subscribe_topic` methods of `PubSubClient` for better type checking ([#6549](https://github.com/flet-dev/flet/pull/6564)) by @Iaw4tch * Fix `FilePicker.pick_files()` on web for slow network shares or slow machines: pass `cancel_upload_on_window_blur=False` to prevent valid file selections from being reported as cancelled when the browser window loses focus during file picking ([#771](https://github.com/flet-dev/flet/issues/771), [#6573](https://github.com/flet-dev/flet/pull/6573)) by @ndonkoHenri. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 60f68e536e..fd2ad45cd3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -259,6 +259,7 @@ For patches to the current stable release, branch directly from `main`, fix, ope * Ensure every merged PR on `release/v{version}` added a new record to the active root `CHANGELOG.md` section. * Open terminal in `client` directory and run `flutter pub get` to update Flet dependency versions in `client/pubspec.lock`. * Templates are in `sdk/python/templates/` and automatically packaged as zip artifacts with the GitHub Release. No manual branch creation in external repos is needed. +* The supported Python / Pyodide versions are loaded on demand from [python-build's](https://github.com/flet-dev/python-build) date-keyed `manifest.json`; flet pins one release via `PYTHON_BUILD_RELEASE_DATE` in `sdk/python/packages/flet-cli/src/flet_cli/utils/python_versions.py`. When bumping it, keep it aligned with serious_python's `pythonReleaseDate` (both should track the same python-build release). ## New macOS environment for Flet developer diff --git a/client/android/app/build.gradle.kts b/client/android/app/build.gradle.kts index 41100f0452..855a314b73 100644 --- a/client/android/app/build.gradle.kts +++ b/client/android/app/build.gradle.kts @@ -1,22 +1,19 @@ plugins { id("com.android.application") - id("kotlin-android") - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + // Kotlin is provided by the Flutter Gradle Plugin (Built-in Kotlin), so the + // app no longer applies the Kotlin Gradle Plugin itself. + // The Flutter Gradle Plugin must be applied after the Android Gradle plugin. id("dev.flutter.flutter-gradle-plugin") } android { namespace = "com.appveyor.flet_client" compileSdk = flutter.compileSdkVersion - ndkVersion = "27.0.12077973" + ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } defaultConfig { @@ -37,6 +34,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + flutter { source = "../.." } diff --git a/client/android/gradle/wrapper/gradle-wrapper.properties b/client/android/gradle/wrapper/gradle-wrapper.properties index 02767eb1ca..e4ef43fb98 100644 --- a/client/android/gradle/wrapper/gradle-wrapper.properties +++ b/client/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/client/android/settings.gradle.kts b/client/android/settings.gradle.kts index 38573a4d5b..fba4abce7b 100644 --- a/client/android/settings.gradle.kts +++ b/client/android/settings.gradle.kts @@ -18,8 +18,9 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.12.1" apply false - id("org.jetbrains.kotlin.android") version "2.1.0" apply false + id("com.android.application") version "8.11.1" apply false + // Plugin applies kotlin-android for us (Built-in Kotlin) — uses this KGP version. + id("org.jetbrains.kotlin.android") version "2.2.20" apply false } include(":app") diff --git a/client/pubspec.lock b/client/pubspec.lock index 8028a32cb3..79e83f043d 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -991,10 +991,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mgrs_dart: dependency: transitive description: @@ -1367,58 +1367,58 @@ packages: dependency: transitive description: name: screen_brightness - sha256: "5f70754028f169f059fdc61112a19dcbee152f8b293c42c848317854d650cba3" + sha256: e0edf92c08889e8f493cde291e7c687db2b4a1471f2371c074070b75d7c7d79b url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.1.11" screen_brightness_android: dependency: transitive description: name: screen_brightness_android - sha256: d34f5321abd03bc3474f4c381f53d189117eba0b039eac1916aa92cca5fd0a96 + sha256: "2008ad8e9527cc968f7a4de1ec58b476d495b3c612a149dbd6550c4f046da147" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.6" screen_brightness_ios: dependency: transitive description: name: screen_brightness_ios - sha256: "2493953340ecfe8f4f13f61db50ce72533a55b0bbd58ba1402893feecf3727f5" + sha256: "352d355e8523a186ba1e494b74218e5ca96e51975a0630b8ed89fbb7ff609762" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" screen_brightness_macos: dependency: transitive description: name: screen_brightness_macos - sha256: "4edf330ad21078686d8bfaf89413325fbaf571dcebe1e89254d675a3f288b5b9" + sha256: b0957237b842d846a84363b69f229b339a8f91faced55041e245b2ebff7b0142 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.4" screen_brightness_ohos: dependency: transitive description: name: screen_brightness_ohos - sha256: a93a263dcd39b5c56e589eb495bcd001ce65cdd96ff12ab1350683559d5c5bb7 + sha256: "569a2c97909a50894b81487619eb7654f5358443a5af05f840c19261f49c0940" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" screen_brightness_platform_interface: dependency: transitive description: name: screen_brightness_platform_interface - sha256: "737bd47b57746bc4291cab1b8a5843ee881af499514881b0247ec77447ee769c" + sha256: "2de60c0ba569b898950029cc1f7e9dd72bda44a22beb5054aac331cb6fce2ff2" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" screen_brightness_windows: dependency: transitive description: name: screen_brightness_windows - sha256: d3518bf0f5d7a884cee2c14449ae0b36803802866de09f7ef74077874b6b2448 + sha256: "368b9c822e1671de81616e48150006e39eff2a434957e47ee638b09a32b2297c" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" screen_retriever: dependency: transitive description: @@ -1652,10 +1652,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" torch_light: dependency: transitive description: diff --git a/client/web/index.html b/client/web/index.html index 2e622c51af..0e630c5821 100644 --- a/client/web/index.html +++ b/client/web/index.html @@ -30,7 +30,11 @@ assetBase: "/", routeUrlStrategy: "path", canvasKitBaseUrl: "/canvaskit/", - pyodideUrl: "/pyodide/pyodide.js", + // Default fallback only — `patch_index.py` overrides this with the + // resolved per-build URL (CDN or local) at deploy time. The `.mjs` + // suffix is required because python-worker.js is a module worker + // that loads the runtime via dynamic `import()`. See client/web/python.js. + pyodideUrl: "/pyodide/pyodide.mjs", webRenderer: "auto", fontFallbackBaseUrl: "assets/fonts/", // for Noto Emoji, use Google CDN appPackageUrl: "app.tar.gz" diff --git a/client/web/python-worker.js b/client/web/python-worker.js index 40c6083afc..5c743dc8e4 100644 --- a/client/web/python-worker.js +++ b/client/web/python-worker.js @@ -38,7 +38,14 @@ self.sendPythonOutput = function (text, isStderr) { self.initPyodide = async function () { try { - importScripts(self.pyodideUrl); + // Module-worker load path. `importScripts` only exists in classic + // workers — Pyodide >= 0.29 actively refuses to load there ("Classic + // web workers are not supported"). We're a module worker, so the + // runtime ships as `pyodide.mjs` and exposes `loadPyodide` via ESM + // exports. The CDN/jsdelivr fallback URL set by patch_index.py also + // points at the .mjs variant. + const pyodideModule = await import(self.pyodideUrl); + const loadPyodide = pyodideModule.loadPyodide || self.loadPyodide; self.pyodide = await loadPyodide({ stdout: (text) => self.sendPythonOutput(text, false), stderr: (text) => self.sendPythonOutput(text, true), @@ -206,7 +213,14 @@ self.initPyodide = async function () { }; self.receiveCallback = (message) => { - self.postMessage(message.toJs()); + // `message` is a Pyodide JsProxy wrapping a Python `bytes`. `toJs()` + // gives us a fresh Uint8Array; transferring its underlying ArrayBuffer + // to the main thread skips the structured-clone copy (~hundreds of KB + // per matplotlib frame). Safe because the Uint8Array is freshly + // materialized here, and the original Python `bytes` is untouched + // (Pyodide keeps its own reference). + const bytes = message.toJs(); + self.postMessage(bytes, [bytes.buffer]); } // Same channel as `receiveCallback`, exposed under `flet_js` so the // Python python_output shim can post pre-encoded msgpack frames. diff --git a/client/web/python.js b/client/web/python.js index d44d66d1d2..1e44e27b2e 100644 --- a/client/web/python.js +++ b/client/web/python.js @@ -13,8 +13,17 @@ globalThis.jsConnect = async function (appId, args, dartOnMessage) { }; console.log(`Starting up Python worker: ${appId}, args: ${args}`); _apps[appId] = app; - app.worker = new Worker((flet.entrypointBaseUrl.endsWith("/") ? - flet.entrypointBaseUrl.slice(0, -1) : flet.entrypointBaseUrl) + "/python-worker.js"); + // Module worker (type: "module") is required by Pyodide >= 0.29 — the + // runtime throws "Classic web workers are not supported" inside any + // worker where `importScripts` is callable. Module workers don't have + // `importScripts`, so the check passes. Older Pyodide lines (0.27.x) + // accept module workers too, so this is forward-compatible across all + // supported Python versions (3.12 → Pyodide 0.27.7, 3.13 → 0.29.4, + // 3.14 → 314.0.0). + app.worker = new Worker( + (flet.entrypointBaseUrl.endsWith("/") ? + flet.entrypointBaseUrl.slice(0, -1) : flet.entrypointBaseUrl) + "/python-worker.js", + { type: "module" }); var error; app.worker.onmessage = (event) => { @@ -32,7 +41,11 @@ globalThis.jsConnect = async function (appId, args, dartOnMessage) { // initialize worker app.worker.postMessage({ - pyodideUrl: flet.pyodideUrl || "pyodide/pyodide.js", + // `.mjs` is the ES-module variant. python-worker.js (now a module + // worker) loads it via dynamic `import()`. The legacy `.js` + // variant relied on `importScripts`, which doesn't exist in a + // module worker. + pyodideUrl: flet.pyodideUrl || "pyodide/pyodide.mjs", args: args, documentUrl: _documentUrl, appPackageUrl: flet.appPackageUrl, diff --git a/client/windows/flutter/generated_plugin_registrant.cc b/client/windows/flutter/generated_plugin_registrant.cc index 00afa0a858..c4d28c3ed8 100644 --- a/client/windows/flutter/generated_plugin_registrant.cc +++ b/client/windows/flutter/generated_plugin_registrant.cc @@ -17,7 +17,7 @@ #include #include #include -#include +#include #include #include #include @@ -47,8 +47,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); RiveNativePluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("RiveNativePlugin")); - ScreenBrightnessWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin")); + ScreenBrightnessWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPluginCApi")); ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); SharePlusWindowsPluginCApiRegisterWithRegistrar( diff --git a/packages/flet/lib/flet.dart b/packages/flet/lib/flet.dart index 6c0933a2e1..da46bb70ce 100644 --- a/packages/flet/lib/flet.dart +++ b/packages/flet/lib/flet.dart @@ -16,6 +16,11 @@ export 'src/flet_service.dart'; export 'src/models/asset_source.dart'; export 'src/models/control.dart'; export 'src/models/page_size_view_model.dart'; +export 'src/protocol/message.dart'; +export 'src/transport/data_channel.dart'; +export 'src/transport/flet_backend_channel.dart'; +export 'src/transport/flet_msgpack_decoder.dart'; +export 'src/transport/flet_msgpack_encoder.dart'; export 'src/routing/deep_linking_bootstrap.dart'; export 'src/testing/test_finder.dart'; export 'src/testing/tester.dart'; diff --git a/packages/flet/lib/src/controls/control_widget.dart b/packages/flet/lib/src/controls/control_widget.dart index ae4db35a96..ead9a54c0c 100644 --- a/packages/flet/lib/src/controls/control_widget.dart +++ b/packages/flet/lib/src/controls/control_widget.dart @@ -29,7 +29,19 @@ class ControlWidget extends StatelessWidget { FletBackend.of(context).globalKeys[controlKey.toString()] = key as GlobalKey; } else if (controlKey != null) { - key = ValueKey(controlKey.value); + // Preserve the concrete value type so the resulting ValueKey matches + // what callers construct in tests, e.g. `find.byKey(Key('foo'))` which + // resolves to `ValueKey('foo')`. `ValueKey(controlKey.value)` + // would produce `ValueKey(...)` because `controlKey.value` is + // statically typed `Object`, and ValueKey's `==` is runtimeType-strict + // — `ValueKey` is never equal to `ValueKey`. + key = switch (controlKey.value) { + String v => ValueKey(v), + int v => ValueKey(v), + double v => ValueKey(v), + bool v => ValueKey(v), + _ => ValueKey(controlKey.value), + }; } return control.buildInControlContext((context) { diff --git a/packages/flet/lib/src/flet_app.dart b/packages/flet/lib/src/flet_app.dart index ee0873a934..1eb088187f 100644 --- a/packages/flet/lib/src/flet_app.dart +++ b/packages/flet/lib/src/flet_app.dart @@ -7,6 +7,8 @@ import 'flet_backend.dart'; import 'flet_extension.dart'; import 'models/control.dart'; import 'testing/tester.dart'; +import 'transport/data_channel.dart'; +import 'transport/flet_backend_channel.dart'; /// FletApp - The top-level widget that initializes everything class FletApp extends StatefulWidget { @@ -26,6 +28,19 @@ class FletApp extends StatefulWidget { final Tester? tester; final bool multiView; + /// Optional escape hatch for embedders that bring their own transport + /// (e.g. `serious_python`'s in-process FFI bridge). When set, this builder + /// is invoked from [FletBackend.connect] in place of the URL-scheme + /// factory; [pageUrl] is then irrelevant for transport selection. + final FletBackendChannelBuilder? channelBuilder; + + /// Optional factory for high-throughput byte channels (see [DataChannel]). + /// Embedders that ship an in-process Python runtime can inject a + /// `PythonBridge`-backed factory here; when `null`, `FletBackend` falls + /// back to a built-in factory that muxes raw bytes over the regular Flet + /// protocol channel. + final DataChannelFactory? dataChannelFactory; + const FletApp( {super.key, required this.pageUrl, @@ -42,7 +57,9 @@ class FletApp extends StatefulWidget { this.args, this.forcePyodide, this.tester, - this.multiView = false}); + this.multiView = false, + this.channelBuilder, + this.dataChannelFactory}); @override State createState() => _FletAppState(); @@ -76,6 +93,8 @@ class _FletAppState extends State { forcePyodide: widget.forcePyodide, tester: widget.tester, multiView: widget.multiView, + channelBuilder: widget.channelBuilder, + dataChannelFactory: widget.dataChannelFactory, parentFletBackend: Provider.of(context, listen: false)); }, diff --git a/packages/flet/lib/src/flet_backend.dart b/packages/flet/lib/src/flet_backend.dart index f173ee8584..3a140314df 100644 --- a/packages/flet/lib/src/flet_backend.dart +++ b/packages/flet/lib/src/flet_backend.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:msgpack_dart/msgpack_dart.dart' as msgpack; import 'package:provider/provider.dart'; import 'flet_app_errors_handler.dart'; @@ -24,7 +25,11 @@ import 'protocol/register_client_response_body.dart'; import 'protocol/session_crashed_body.dart'; import 'protocol/update_control_body.dart'; import 'testing/tester.dart'; +import 'transport/data_channel.dart'; import 'transport/flet_backend_channel.dart'; +import 'transport/flet_msgpack_decoder.dart'; +import 'transport/flet_msgpack_encoder.dart'; +import 'transport/protocol_muxed_data_channel.dart'; import 'utils/desktop.dart'; import 'utils/images.dart'; import 'utils/numbers.dart'; @@ -62,6 +67,14 @@ class FletBackend extends ChangeNotifier { int _reconnectStarted = 0; int _reconnectDelayMs = 0; FletBackendChannel? _backendChannel; + final FletBackendChannelBuilder? _channelBuilder; + late final DataChannelFactory _dataChannelFactory; + final DataChannelFactory? _injectedDataChannelFactory; + // Inbound mux registry for ProtocolMuxedDataChannel — type-byte 0x01 + // frames are routed by channel_id to the matching channel's deliver hook. + // PythonBridge-backed DataChannels do NOT live in this registry (their + // bytes arrive on their own native port, never on the Flet transport). + final Map _dataChannels = {}; final List _sendQueue = []; String route = ""; bool isLoading = true; @@ -104,11 +117,17 @@ class FletBackend extends ChangeNotifier { this.forcePyodide, this.tester, required extensions, + FletBackendChannelBuilder? channelBuilder, + DataChannelFactory? dataChannelFactory, FletBackend? parentFletBackend}) : _parentFletBackend = parentFletBackend != null ? WeakReference(parentFletBackend) : null, _reconnectTimeoutMs = reconnectTimeoutMs, - _reconnectIntervalMs = reconnectIntervalMs { + _reconnectIntervalMs = reconnectIntervalMs, + _channelBuilder = channelBuilder, + _injectedDataChannelFactory = dataChannelFactory { + _dataChannelFactory = + _injectedDataChannelFactory ?? ProtocolMuxedDataChannelFactory(this); // add Flet extension with core controls and services this.extensions = [...extensions, FletCoreExtension()]; @@ -177,12 +196,21 @@ class FletBackend extends ChangeNotifier { Future connect() async { debugPrint("Connecting to Flet backend $pageUri..."); try { - _backendChannel = FletBackendChannel( - address: pageUri.toString(), - args: args ?? {}, - forcePyodide: forcePyodide == true, - onDisconnect: _onDisconnect, - onMessage: _onMessage); + final builder = _channelBuilder; + if (builder != null) { + // Embedder-supplied transport (e.g. serious_python's in-process FFI + // bridge). The builder is responsible for the entire transport + // lifecycle; we just wire its callbacks to ours. + _backendChannel = builder( + onDisconnect: _onDisconnect, onPacket: _onPacket); + } else { + _backendChannel = FletBackendChannel( + address: pageUri.toString(), + args: args ?? {}, + forcePyodide: forcePyodide == true, + onDisconnect: _onDisconnect, + onPacket: _onPacket); + } await _backendChannel!.connect(); _registerClient(); } catch (e) { @@ -192,6 +220,40 @@ class FletBackend extends ChangeNotifier { } } + /// Opens a dedicated [DataChannel] for high-throughput byte traffic from a + /// widget. In embedded mode this is backed by a fresh `PythonBridge`; in + /// dev / web modes it is a logical channel multiplexed over the active + /// [FletBackendChannel] (see [ProtocolMuxedDataChannelFactory]). + /// + /// Must be called from the main Isolate (it doesn't escape there, but + /// the returned channel is main-Isolate-bound either way). + DataChannel openDataChannel() => _dataChannelFactory.open(); + + // --------------------------------------------------------------------- + // Mux registry — used by ProtocolMuxedDataChannel only. + // --------------------------------------------------------------------- + + /// Registers a muxed data channel so inbound 0x01 frames for [id] are + /// routed to it. Called from [ProtocolMuxedDataChannel.]. + void registerDataChannel(int id, ProtocolMuxedDataChannel channel) { + assert(!_dataChannels.containsKey(id), "duplicate data channel id $id"); + _dataChannels[id] = channel; + } + + /// Removes [id] from the routing table. Called from + /// [ProtocolMuxedDataChannel.close]. Idempotent — frames for an + /// unregistered id are silently dropped. + void unregisterDataChannel(int id) { + _dataChannels.remove(id); + } + + /// Sends a fully-formed packet on the active transport. Used by + /// [ProtocolMuxedDataChannel] to ship `[0x01][channel_id:u32 LE][bytes]` + /// alongside regular protocol traffic. + void sendRawPacket(Uint8List packet) { + _backendChannel?.send(packet); + } + _registerClient() { debugPrint("Registering web client: $page"); _send( @@ -416,9 +478,42 @@ class FletBackend extends ChangeNotifier { return getAssetSrc(src, pageUri, assetsDir); } - _onMessage(Message message) { + /// Inbound transport dispatcher. Every packet starts with a 1-byte type + /// discriminator: + /// 0x00 → MsgPack-encoded Flet control frame (the existing protocol). + /// 0x01 → raw DataChannel frame `[channel_id:u32 LE][payload]`. + void _onPacket(Uint8List packet) { + if (packet.isEmpty) { + debugPrint("Dropping empty packet"); + return; + } + final type = packet[0]; + if (type == 0x00) { + // Decode the MsgPack body and dispatch as a Flet protocol message. + final body = msgpack.deserialize( + Uint8List.sublistView(packet, 1), + extDecoder: FletMsgpackDecoder()); + _onMessage(Message.fromList(body)); + } else if (type == 0x01) { + if (packet.length < 5) { + debugPrint("Dropping malformed data channel frame (len=${packet.length})"); + return; + } + final channelId = + ByteData.sublistView(packet, 1, 5).getUint32(0, Endian.little); + final channel = _dataChannels[channelId]; + if (channel == null) { + // Stale frame after channel.close() — silently drop. + return; + } + channel.deliver(Uint8List.sublistView(packet, 5)); + } else { + debugPrint("Dropping packet with unknown type byte 0x${type.toRadixString(16)}"); + } + } + + void _onMessage(Message message) { debugPrint("Received message: ${message.toList()}"); - //debugPrint("message.payload: ${message.payload}"); switch (message.action) { case MessageAction.registerClient: _onClientRegistered( @@ -568,7 +663,12 @@ class FletBackend extends ChangeNotifier { _send(Message message, {bool unbuffered = false}) { if (unbuffered || !isLoading) { debugPrint("_send: ${message.action} ${message.payload}"); - _backendChannel?.send(message); + final encoded = msgpack.serialize(message.toList(), + extEncoder: FletMsgpackEncoder()); + final packet = Uint8List(1 + encoded.length); + packet[0] = 0x00; + packet.setRange(1, packet.length, encoded); + _backendChannel?.send(packet); } else { _sendQueue.add(message); } diff --git a/packages/flet/lib/src/services/tester.dart b/packages/flet/lib/src/services/tester.dart index 18dc410319..cd747a1e55 100644 --- a/packages/flet/lib/src/services/tester.dart +++ b/packages/flet/lib/src/services/tester.dart @@ -50,9 +50,20 @@ class TesterService extends FletService { case "find_by_key": var controlKey = parseKey(args["key"])!; - var key = controlKey is ControlScrollKey + // Preserve the concrete value type so the constructed ValueKey + // matches the one ControlWidget assigned to the rendered widget. + // ValueKey's `==` is runtimeType-strict — `ValueKey('foo')` + // never equals `ValueKey('foo')`, which would make + // `find.byKey(...)` miss every Flet control. + Key? key = controlKey is ControlScrollKey ? control.backend.globalKeys[controlKey.toString()] - : ValueKey(controlKey.value); + : switch (controlKey.value) { + String v => ValueKey(v), + int v => ValueKey(v), + double v => ValueKey(v), + bool v => ValueKey(v), + _ => ValueKey(controlKey.value), + }; if (key == null) { throw Exception("Key not found: $key"); } diff --git a/packages/flet/lib/src/transport/data_channel.dart b/packages/flet/lib/src/transport/data_channel.dart new file mode 100644 index 0000000000..f0d39875a0 --- /dev/null +++ b/packages/flet/lib/src/transport/data_channel.dart @@ -0,0 +1,55 @@ +import 'dart:typed_data'; + +/// One bidirectional byte channel between Dart and Python, dedicated to a +/// single widget's bulk-data traffic. +/// +/// The Dart side of an extension widget opens one via +/// [FletBackend.openDataChannel] in `initState`, then announces it to Python +/// by firing a `data_channel_open` control event carrying +/// `{channel_name, channel_id: id}` — Python's handler retrieves the +/// matching `DataChannel` via `Control.get_data_channel(channel_id)`. +/// +/// Bytes flow over a transport chosen by the active [DataChannelFactory]: +/// a dedicated `PythonBridge` per channel in embedded native mode, or a +/// raw-byte frame muxed over the regular Flet protocol transport in dev / +/// web modes (see `ProtocolMuxedDataChannelFactory`). +/// +/// **Isolate scope.** [FletBackend.openDataChannel] runs on the main Isolate +/// (it goes through `FletBackend.of(BuildContext)`). The returned channel — +/// and, in embedded mode, the backing `PythonBridge` — therefore lives on +/// the main Isolate. For per-Isolate bridges in worker Isolates, construct +/// `PythonBridge` directly from `package:serious_python` and send the port +/// back to the main Isolate via `SendPort` for the `data_channel_open` +/// fire. +abstract class DataChannel { + /// Stable identifier carried in the `data_channel_open` event payload so + /// Python can attach to the same channel. Implementation-specific: for + /// the `PythonBridge`-backed factory this is the Dart native port number, + /// for the muxed fallback it is a monotonic u32 minted by the factory. + int get id; + + /// Bytes pushed from Python via this channel. Hot path — consumers should + /// avoid synchronous heavy work in the listener and instead enqueue to a + /// worker. + Stream get messages; + + /// Send bytes Dart → Python. Returns `false` only during the brief startup + /// window before the Python side has attached (embedded mode); widget + /// code should treat this as transient and retry / queue accordingly. + bool send(Uint8List bytes); + + /// Releases the channel. Must be called from the widget's `dispose()`. + /// Idempotent. + void close(); +} + +/// Factory injected by the embedder. The `flet build` template injects a +/// `PythonBridge`-backed factory for native mode; web / dev / Pyodide +/// deployments leave this `null` and `FletBackend` falls back to the +/// built-in `ProtocolMuxedDataChannelFactory` that rides the existing +/// Flet protocol transport. +abstract class DataChannelFactory { + /// Opens a fresh data channel. Each call mints a new id; a control may + /// open as many channels as it needs. + DataChannel open(); +} diff --git a/packages/flet/lib/src/transport/flet_backend_channel.dart b/packages/flet/lib/src/transport/flet_backend_channel.dart index b4f30c07d5..7b99e1ce02 100644 --- a/packages/flet/lib/src/transport/flet_backend_channel.dart +++ b/packages/flet/lib/src/transport/flet_backend_channel.dart @@ -1,4 +1,5 @@ -import '../protocol/message.dart'; +import 'dart:typed_data'; + import '../utils/platform_utils_web.dart' if (dart.library.io) "../utils/platform_utils_non_web.dart"; import 'flet_backend_channel_javascript_web.dart' @@ -8,7 +9,30 @@ import 'flet_backend_channel_socket.dart'; import 'flet_backend_channel_web_socket.dart'; typedef FletBackendChannelOnDisconnectCallback = void Function(); -typedef FletBackendChannelOnMessageCallback = void Function(Message message); + +/// Called when the transport receives one complete packet from the peer. +/// The packet is the **full** byte sequence including the 1-byte type +/// discriminator at offset 0: +/// +/// `[type:u8][payload]` +/// +/// where `type == 0x00` is a MsgPack-encoded Flet protocol frame and +/// `type == 0x01` is a raw DataChannel frame (`[channel_id:u32 LE][bytes]`). +/// Transports are responsible only for delivering packet boundaries; the +/// type byte is interpreted by [FletBackend]. +typedef FletBackendChannelOnPacketCallback = void Function(Uint8List packet); + +/// Builds a custom [FletBackendChannel] supplied by the embedder. +/// +/// When provided to [FletApp]/[FletBackend], the channel is used directly and +/// the URL-scheme factory below is skipped — letting embedded runtimes (e.g. +/// `serious_python`'s in-process Dart↔Python FFI bridge) plug in a transport +/// that needs more setup than a `String address` URL can express, without +/// forcing the `flet` package to take a Python-related dependency. +typedef FletBackendChannelBuilder = FletBackendChannel Function({ + required FletBackendChannelOnPacketCallback onPacket, + required FletBackendChannelOnDisconnectCallback onDisconnect, +}); abstract class FletBackendChannel { factory FletBackendChannel( @@ -16,33 +40,38 @@ abstract class FletBackendChannel { required Map args, required bool forcePyodide, required FletBackendChannelOnDisconnectCallback onDisconnect, - required FletBackendChannelOnMessageCallback onMessage}) { + required FletBackendChannelOnPacketCallback onPacket}) { if (isPyodideMode() || forcePyodide) { // Pyodide/JavaScript return FletJavaScriptBackendChannel( address: address, args: args, onDisconnect: onDisconnect, - onMessage: onMessage); + onPacket: onPacket); } else if (address.startsWith("http://") || address.startsWith("https://")) { // WebSocket return FletWebSocketBackendChannel( - address: address, onDisconnect: onDisconnect, onMessage: onMessage); + address: address, onDisconnect: onDisconnect, onPacket: onPacket); } else if (address == "mock") { // Mock return FletMockBackendChannel( - address: address, onDisconnect: onDisconnect, onMessage: onMessage); + address: address, onDisconnect: onDisconnect, onPacket: onPacket); } else { // TCP or UDS return FletSocketBackendChannel( - address: address, onDisconnect: onDisconnect, onMessage: onMessage); + address: address, onDisconnect: onDisconnect, onPacket: onPacket); } } Future connect(); bool get isLocalConnection; int get defaultReconnectIntervalMs; - void send(Message message); + + /// Sends one full packet — `[type:u8][payload]` — to the peer. The transport + /// is responsible for delimiting packet boundaries (length prefix on + /// stream-oriented transports; native message boundary on others). + void send(Uint8List packet); + void disconnect(); } diff --git a/packages/flet/lib/src/transport/flet_backend_channel_javascript_io.dart b/packages/flet/lib/src/transport/flet_backend_channel_javascript_io.dart index 65149e543f..1ed8c86b64 100644 --- a/packages/flet/lib/src/transport/flet_backend_channel_javascript_io.dart +++ b/packages/flet/lib/src/transport/flet_backend_channel_javascript_io.dart @@ -1,17 +1,18 @@ -import '../protocol/message.dart'; +import 'dart:typed_data'; + import 'flet_backend_channel.dart'; class FletJavaScriptBackendChannel implements FletBackendChannel { final String address; final Map args; - final FletBackendChannelOnMessageCallback onMessage; + final FletBackendChannelOnPacketCallback onPacket; final FletBackendChannelOnDisconnectCallback onDisconnect; FletJavaScriptBackendChannel( {required this.address, required this.args, required this.onDisconnect, - required this.onMessage}); + required this.onPacket}); @override connect() async {} @@ -23,7 +24,7 @@ class FletJavaScriptBackendChannel implements FletBackendChannel { int get defaultReconnectIntervalMs => 10; @override - void send(Message data) {} + void send(Uint8List packet) {} @override void disconnect() {} diff --git a/packages/flet/lib/src/transport/flet_backend_channel_javascript_web.dart b/packages/flet/lib/src/transport/flet_backend_channel_javascript_web.dart index 46a6933223..7b76dc8765 100644 --- a/packages/flet/lib/src/transport/flet_backend_channel_javascript_web.dart +++ b/packages/flet/lib/src/transport/flet_backend_channel_javascript_web.dart @@ -1,37 +1,35 @@ import 'dart:js_interop'; import 'package:flutter/foundation.dart'; -import 'package:msgpack_dart/msgpack_dart.dart' as msgpack; -import '../protocol/message.dart'; import 'flet_backend_channel.dart'; -import 'flet_msgpack_decoder.dart'; -import 'flet_msgpack_encoder.dart'; @JS() external JSPromise jsConnect( String appId, JSAny args, JSExportedDartFunction onMessage); +/// The optional `transferList` argument names ArrayBuffers (in `data`'s +/// underlying buffer) whose ownership should transfer to the receiver — +/// `postMessage` then avoids the structured-clone copy. We pass the +/// packet's `.buffer` here so bulk DataChannel frames are zero-copy across +/// the Worker boundary. @JS() -external void jsSend(String appId, JSUint8Array data); +external void jsSend(String appId, JSUint8Array data, JSArray? transferList); @JS() external void jsDisconnect(String appId); -typedef FletBackendJavascriptChannelOnMessageCallback = void Function( - List message); - class FletJavaScriptBackendChannel implements FletBackendChannel { final String address; final Map args; - final FletBackendChannelOnMessageCallback onMessage; + final FletBackendChannelOnPacketCallback onPacket; final FletBackendChannelOnDisconnectCallback onDisconnect; FletJavaScriptBackendChannel( {required this.address, required this.args, required this.onDisconnect, - required this.onMessage}); + required this.onPacket}); @override connect() async { @@ -40,8 +38,9 @@ class FletJavaScriptBackendChannel implements FletBackendChannel { } void _onMessage(JSUint8Array data) { - onMessage(Message.fromList( - msgpack.deserialize(data.toDart, extDecoder: FletMsgpackDecoder()))); + // Each postMessage event is one packet — message boundaries are + // preserved by the underlying MessageChannel. + onPacket(data.toDart); } @override @@ -51,12 +50,15 @@ class FletJavaScriptBackendChannel implements FletBackendChannel { int get defaultReconnectIntervalMs => 10000; @override - void send(Message message) { - jsSend( - address, - msgpack - .serialize(message.toList(), extEncoder: FletMsgpackEncoder()) - .toJS); + void send(Uint8List packet) { + final jsBytes = packet.toJS; + // Transfer the underlying ArrayBuffer to the receiver — zero copy + // across the Worker boundary. Safe because the sender does not access + // `packet` after this call (FletBackend always builds a fresh buffer + // per send). + final jsBuffer = packet.buffer.toJS; + final transferList = [jsBuffer as JSObject].toJS; + jsSend(address, jsBytes, transferList); } @override diff --git a/packages/flet/lib/src/transport/flet_backend_channel_mock.dart b/packages/flet/lib/src/transport/flet_backend_channel_mock.dart index f583f23819..a0b53bde40 100644 --- a/packages/flet/lib/src/transport/flet_backend_channel_mock.dart +++ b/packages/flet/lib/src/transport/flet_backend_channel_mock.dart @@ -1,18 +1,32 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; +import 'package:msgpack_dart/msgpack_dart.dart' as msgpack; import '../protocol/message.dart'; import 'flet_backend_channel.dart'; +import 'flet_msgpack_encoder.dart'; class FletMockBackendChannel implements FletBackendChannel { - FletBackendChannelOnMessageCallback onMessage; + FletBackendChannelOnPacketCallback onPacket; FletBackendChannelOnDisconnectCallback onDisconnect; FletMockBackendChannel( {required String address, required this.onDisconnect, - required this.onMessage}); + required this.onPacket}); + + /// Wrap test-scenario `Message` payloads in the on-wire packet shape + /// `[0x00][msgpack(message.toList())]` so the inbound dispatcher sees + /// them through the same code path as real transports. + void onMessage(Message message) { + final encoded = msgpack.serialize(message.toList(), + extEncoder: FletMsgpackEncoder()); + final packet = Uint8List(1 + encoded.length); + packet[0] = 0x00; + packet.setRange(1, packet.length, encoded); + onPacket(packet); + } @override bool get isLocalConnection => true; @@ -296,8 +310,9 @@ class FletMockBackendChannel implements FletBackendChannel { } @override - void send(Message message) { - debugPrint("Send message: ${message.toList()}"); + void send(Uint8List packet) { + debugPrint("Send packet: type=${packet.isNotEmpty ? packet[0] : -1} " + "len=${packet.length}"); } @override diff --git a/packages/flet/lib/src/transport/flet_backend_channel_socket.dart b/packages/flet/lib/src/transport/flet_backend_channel_socket.dart index a1fcb79a01..3bcd5af2ca 100644 --- a/packages/flet/lib/src/transport/flet_backend_channel_socket.dart +++ b/packages/flet/lib/src/transport/flet_backend_channel_socket.dart @@ -1,36 +1,36 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/foundation.dart'; -import 'package:msgpack_dart/msgpack_dart.dart' as msgpack; -import '../protocol/message.dart'; import '../utils/networking.dart'; import 'flet_backend_channel.dart'; -import 'flet_msgpack_decoder.dart'; -import 'flet_msgpack_encoder.dart'; -import 'streaming_msgpack_deserializer.dart'; const int defaultLocalReconnectInterval = 200; const int defaultPublicReconnectInterval = 500; +/// TCP / Unix-domain-socket transport. +/// +/// Wire format: every packet on the wire is prefixed with its length as +/// a 4-byte little-endian unsigned integer. The packet itself starts with +/// the 1-byte type discriminator interpreted by [FletBackend]; we just +/// deliver the payload bytes 1:1. class FletSocketBackendChannel implements FletBackendChannel { String address; - FletBackendChannelOnMessageCallback onMessage; + FletBackendChannelOnPacketCallback onPacket; FletBackendChannelOnDisconnectCallback onDisconnect; Socket? _socket; late final bool _isLocalConnection; late final int _defaultReconnectIntervalMs; - // Create an instance of the StreamingDeserializer. - // This object buffers incoming chunks and decodes complete MessagePack objects. - final StreamingMsgpackDeserializer _streamingDeserializer; + // Inbound framing state: accumulate bytes, parse length-prefixed packets. + final BytesBuilder _inboundBuffer = BytesBuilder(copy: false); FletSocketBackendChannel({ required this.address, required this.onDisconnect, - required this.onMessage, - }) : _streamingDeserializer = - StreamingMsgpackDeserializer(extDecoder: FletMsgpackDecoder()); + required this.onPacket, + }); @override connect() async { @@ -53,19 +53,8 @@ class FletSocketBackendChannel implements FletBackendChannel { debugPrint('Connected to: $udsPath'); } - // Listen for incoming data. _socket!.listen( - (Uint8List data) { - debugPrint("Received packet: ${data.length}"); - // Feed the incoming chunk into the streaming deserializer. - _streamingDeserializer.addChunk(data); - // Try to decode complete MessagePack messages from buffered data. - var messages = _streamingDeserializer.decodeMessages(); - for (var message in messages) { - //debugPrint('Decoded message: ${message.toString()}'); - _onMessage(message); - } - }, + _onBytes, onError: (error) { debugPrint("Error: $error"); _socket?.destroy(); @@ -79,22 +68,41 @@ class FletSocketBackendChannel implements FletBackendChannel { ); } + void _onBytes(Uint8List chunk) { + _inboundBuffer.add(chunk); + // Parse as many complete packets as the buffer currently holds. + while (true) { + final buffered = _inboundBuffer.length; + if (buffered < 4) return; + final bytes = _inboundBuffer.toBytes(); + final len = ByteData.sublistView(bytes, 0, 4).getUint32(0, Endian.little); + if (bytes.length < 4 + len) { + // Reset builder to the partial-packet remainder so we accumulate + // the rest on the next read. + _inboundBuffer.clear(); + _inboundBuffer.add(bytes); + return; + } + final packet = Uint8List.sublistView(bytes, 4, 4 + len); + onPacket(packet); + _inboundBuffer.clear(); + if (bytes.length > 4 + len) { + _inboundBuffer.add(Uint8List.sublistView(bytes, 4 + len)); + } + } + } + @override bool get isLocalConnection => _isLocalConnection; @override int get defaultReconnectIntervalMs => _defaultReconnectIntervalMs; - // Note: At this point, the incoming message is already a decoded MessagePack object. - _onMessage(dynamic message) { - onMessage(Message.fromList(message)); - } - @override - void send(Message message) { - // Serialize the message using MessagePack and send it. - _socket!.add( - msgpack.serialize(message.toList(), extEncoder: FletMsgpackEncoder())); + void send(Uint8List packet) { + final header = ByteData(4)..setUint32(0, packet.length, Endian.little); + _socket!.add(header.buffer.asUint8List()); + _socket!.add(packet); } @override diff --git a/packages/flet/lib/src/transport/flet_backend_channel_web_socket.dart b/packages/flet/lib/src/transport/flet_backend_channel_web_socket.dart index 34cb16580c..4529e6532b 100644 --- a/packages/flet/lib/src/transport/flet_backend_channel_web_socket.dart +++ b/packages/flet/lib/src/transport/flet_backend_channel_web_socket.dart @@ -1,27 +1,23 @@ import 'package:flutter/foundation.dart'; -import 'package:msgpack_dart/msgpack_dart.dart' as msgpack; import 'package:web_socket_channel/web_socket_channel.dart'; -import '../protocol/message.dart'; import '../utils/networking.dart'; import '../utils/platform_utils_web.dart' if (dart.library.io) "../utils/platform_utils_non_web.dart"; import '../utils/uri.dart'; import 'flet_backend_channel.dart'; -import 'flet_msgpack_decoder.dart'; -import 'flet_msgpack_encoder.dart'; class FletWebSocketBackendChannel implements FletBackendChannel { late final String _wsUrl; late final bool _isLocalConnection; - FletBackendChannelOnMessageCallback onMessage; + FletBackendChannelOnPacketCallback onPacket; FletBackendChannelOnDisconnectCallback onDisconnect; WebSocketChannel? _channel; FletWebSocketBackendChannel( {required String address, required this.onDisconnect, - required this.onMessage}) { + required this.onPacket}) { _wsUrl = getWebSocketEndpoint(Uri.parse(address)); } @@ -35,7 +31,6 @@ class FletWebSocketBackendChannel implements FletBackendChannel { Future connect() async { debugPrint("Connecting to WebSocket $_wsUrl..."); try { - // todo var uri = Uri.parse(_wsUrl); if (kIsWeb) { _isLocalConnection = isLocalhost(uri); @@ -56,15 +51,21 @@ class FletWebSocketBackendChannel implements FletBackendChannel { }); } - _onMessage(message) { - onMessage(Message.fromList( - msgpack.deserialize(message, extDecoder: FletMsgpackDecoder()))); + void _onMessage(dynamic message) { + // Each WebSocket binary message is one complete packet — message + // boundaries are preserved by the transport, no framing needed here. + if (message is Uint8List) { + onPacket(message); + } else if (message is List) { + onPacket(Uint8List.fromList(message)); + } else { + debugPrint("Unexpected WebSocket message type: ${message.runtimeType}"); + } } @override - void send(Message message) { - _channel?.sink.add( - msgpack.serialize(message.toList(), extEncoder: FletMsgpackEncoder())); + void send(Uint8List packet) { + _channel?.sink.add(packet); } @override diff --git a/packages/flet/lib/src/transport/protocol_muxed_data_channel.dart b/packages/flet/lib/src/transport/protocol_muxed_data_channel.dart new file mode 100644 index 0000000000..6dc9569d04 --- /dev/null +++ b/packages/flet/lib/src/transport/protocol_muxed_data_channel.dart @@ -0,0 +1,77 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import '../flet_backend.dart'; +import 'data_channel.dart'; + +/// Default [DataChannelFactory] used when the embedder does not inject a +/// faster transport (i.e. dev mode `flet run` over UDS/TCP, web with +/// Python server, web with Pyodide). Allocates a monotonic 32-bit id per +/// channel and ships frames as `[0x01][channel_id:u32 LE][payload]` over +/// the active [FletBackendChannel] alongside regular Flet protocol traffic. +/// +/// Ids start at 1; 0 is reserved as the "unallocated" sentinel for the +/// `channel_id` field on a control before Dart has minted one. Counter is +/// session-scoped (one [FletBackend] = one session); at one allocation +/// per microsecond the space lasts ~1 hour, well past any realistic +/// channel-churn pattern. +class ProtocolMuxedDataChannelFactory implements DataChannelFactory { + ProtocolMuxedDataChannelFactory(this._backend); + final FletBackend _backend; + int _nextId = 1; + + @override + DataChannel open() { + final id = _nextId++; + return ProtocolMuxedDataChannel(_backend, id); + } +} + +/// Concrete [DataChannel] that rides the Flet protocol transport. Frames +/// are wrapped as `[0x01][channel_id:u32 LE][payload]`; inbound frames are +/// dispatched to [deliver] via [FletBackend]'s mux registry. +class ProtocolMuxedDataChannel implements DataChannel { + ProtocolMuxedDataChannel(this._backend, this._id) { + _backend.registerDataChannel(_id, this); + } + + final FletBackend _backend; + final int _id; + final StreamController _controller = + StreamController.broadcast(); + bool _closed = false; + + @override + int get id => _id; + + @override + Stream get messages => _controller.stream; + + @override + bool send(Uint8List bytes) { + if (_closed) return false; + // Header: [0x01][channel_id:u32 LE]. Single allocation for the whole + // packet (no BytesBuilder copies) — keeps the hot path tight. + final packet = Uint8List(5 + bytes.length); + packet[0] = 0x01; + final view = ByteData.sublistView(packet, 1, 5); + view.setUint32(0, _id, Endian.little); + packet.setRange(5, packet.length, bytes); + _backend.sendRawPacket(packet); + return true; + } + + /// Called by [FletBackend._onPacket] when a 0x01 frame arrives for this id. + void deliver(Uint8List bytes) { + if (_closed) return; + _controller.add(bytes); + } + + @override + void close() { + if (_closed) return; + _closed = true; + _backend.unregisterDataChannel(_id); + _controller.close(); + } +} diff --git a/packages/flet/lib/src/transport/streaming_msgpack_deserializer.dart b/packages/flet/lib/src/transport/streaming_msgpack_deserializer.dart deleted file mode 100644 index 41c8dff14b..0000000000 --- a/packages/flet/lib/src/transport/streaming_msgpack_deserializer.dart +++ /dev/null @@ -1,291 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:msgpack_dart/msgpack_dart.dart'; - -/// Thrown when a complete MessagePack object cannot be decoded -/// because the data is incomplete. -class IncompleteDataError extends FormatError { - IncompleteDataError(super.message); -} - -class _Deserializer { - final ExtDecoder? _extDecoder; - final codec = const Utf8Codec(); - final Uint8List _list; - final ByteData _data; - int _offset = 0; - - /// If false, decoded binary data buffers will reference underlying input - /// buffer and thus may change when the content of input buffer changes. - /// If true, decoded buffers are copies and the underlying input buffer is - /// free to change after decoding. - final bool copyBinaryData; - - /// The current offset after decoding - int get offset => _offset; - - _Deserializer( - Uint8List list, { - ExtDecoder? extDecoder, - this.copyBinaryData = false, - int initialOffset = 0, - }) : _list = list, - _data = - ByteData.view(list.buffer, list.offsetInBytes, list.lengthInBytes), - _extDecoder = extDecoder { - _offset = initialOffset; - } - - // Checks that at least [required] bytes are available, - // or throws an IncompleteDataError. - void _ensureAvailable(int required) { - if (_offset + required > _list.length) { - throw IncompleteDataError( - "Not enough data: require $required more bytes, available ${_list.length - _offset}"); - } - } - - dynamic decode() { - _ensureAvailable(1); - final u = _list[_offset++]; - if (u <= 127) { - return u; - } else if ((u & 0xE0) == 0xE0) { - // negative small integer - return u - 256; - } else if ((u & 0xE0) == 0xA0) { - return _readString(u & 0x1F); - } else if ((u & 0xF0) == 0x90) { - return _readArray(u & 0xF); - } else if ((u & 0xF0) == 0x80) { - return _readMap(u & 0xF); - } - switch (u) { - case 0xc0: - return null; - case 0xc2: - return false; - case 0xc3: - return true; - case 0xcc: - return _readUInt8(); - case 0xcd: - return _readUInt16(); - case 0xce: - return _readUInt32(); - case 0xcf: - return _readUInt64(); - case 0xd0: - return _readInt8(); - case 0xd1: - return _readInt16(); - case 0xd2: - return _readInt32(); - case 0xd3: - return _readInt64(); - case 0xca: - return _readFloat(); - case 0xcb: - return _readDouble(); - case 0xd9: - return _readString(_readUInt8()); - case 0xda: - return _readString(_readUInt16()); - case 0xdb: - return _readString(_readUInt32()); - case 0xc4: - return _readBuffer(_readUInt8()); - case 0xc5: - return _readBuffer(_readUInt16()); - case 0xc6: - return _readBuffer(_readUInt32()); - case 0xdc: - return _readArray(_readUInt16()); - case 0xdd: - return _readArray(_readUInt32()); - case 0xde: - return _readMap(_readUInt16()); - case 0xdf: - return _readMap(_readUInt32()); - case 0xd4: - return _readExt(1); - case 0xd5: - return _readExt(2); - case 0xd6: - return _readExt(4); - case 0xd7: - return _readExt(8); - case 0xd8: - return _readExt(16); - case 0xc7: - return _readExt(_readUInt8()); - case 0xc8: - return _readExt(_readUInt16()); - case 0xc9: - return _readExt(_readUInt32()); - default: - throw FormatError("Invalid MessagePack format"); - } - } - - int _readInt8() { - _ensureAvailable(1); - return _data.getInt8(_offset++); - } - - int _readUInt8() { - _ensureAvailable(1); - return _data.getUint8(_offset++); - } - - int _readUInt16() { - _ensureAvailable(2); - final res = _data.getUint16(_offset); - _offset += 2; - return res; - } - - int _readInt16() { - _ensureAvailable(2); - final res = _data.getInt16(_offset); - _offset += 2; - return res; - } - - int _readUInt32() { - _ensureAvailable(4); - final res = _data.getUint32(_offset); - _offset += 4; - return res; - } - - int _readInt32() { - _ensureAvailable(4); - final res = _data.getInt32(_offset); - _offset += 4; - return res; - } - - int _readUInt64() { - _ensureAvailable(8); - final res = _data.getUint64(_offset); - _offset += 8; - return res; - } - - int _readInt64() { - _ensureAvailable(8); - final res = _data.getInt64(_offset); - _offset += 8; - return res; - } - - double _readFloat() { - _ensureAvailable(4); - final res = _data.getFloat32(_offset); - _offset += 4; - return res; - } - - double _readDouble() { - _ensureAvailable(8); - final res = _data.getFloat64(_offset); - _offset += 8; - return res; - } - - Uint8List _readBuffer(int length) { - _ensureAvailable(length); - final res = - Uint8List.view(_list.buffer, _list.offsetInBytes + _offset, length); - _offset += length; - return copyBinaryData ? Uint8List.fromList(res) : res; - } - - String _readString(int length) { - final list = _readBuffer(length); - final len = list.length; - for (int i = 0; i < len; ++i) { - if (list[i] > 127) { - return codec.decode(list); - } - } - return String.fromCharCodes(list); - } - - List _readArray(int length) { - final res = List.filled(length, null, growable: false); - for (int i = 0; i < length; ++i) { - res[i] = decode(); - } - return res; - } - - Map _readMap(int length) { - final res = {}; - while (length > 0) { - res[decode()] = decode(); - --length; - } - return res; - } - - dynamic _readExt(int length) { - final extType = _readUInt8(); - final data = _readBuffer(length); - return _extDecoder?.decodeObject(extType, data); - } -} - -/// A helper to decode MessagePack data as it arrives in chunks. -/// Call [addChunk] for every incoming piece of data, -/// then [decodeMessages] to retrieve complete messages. -class StreamingMsgpackDeserializer { - final ExtDecoder? _extDecoder; - final bool copyBinaryData; - final BytesBuilder _buffer = BytesBuilder(); - - StreamingMsgpackDeserializer( - {ExtDecoder? extDecoder, this.copyBinaryData = false}) - : _extDecoder = extDecoder; - - /// Adds a new chunk of MessagePack data. - void addChunk(Uint8List chunk) { - _buffer.add(chunk); - } - - /// Attempts to decode as many complete messages as possible - /// from the buffered data. Incomplete trailing data remains in the buffer. - List decodeMessages() { - List messages = []; - Uint8List data = _buffer.takeBytes(); - int offset = 0; - while (offset < data.length) { - try { - // Create a Deserializer using the current offset - _Deserializer d = _Deserializer( - data, - extDecoder: _extDecoder, - copyBinaryData: copyBinaryData, - initialOffset: offset, - ); - dynamic message = d.decode(); - messages.add(message); - offset = d.offset; - } on IncompleteDataError { - // Not enough data to decode a full message; break out of the loop. - break; - } on FormatError { - // For actual format errors (not just incomplete data), - // rethrow or handle as needed. - rethrow; - } - } - // If there is any leftover (incomplete) data, put it back into the buffer. - if (offset < data.length) { - _buffer.add(data.sublist(offset)); - } - return messages; - } -} diff --git a/packages/flet/pubspec.yaml b/packages/flet/pubspec.yaml index 10dd971e8e..257f6f9c97 100644 --- a/packages/flet/pubspec.yaml +++ b/packages/flet/pubspec.yaml @@ -42,7 +42,7 @@ dependencies: pasteboard: ^0.4.0 provider: ^6.1.2 screenshot: ^3.0.0 - screen_brightness: ^2.1.7 + screen_brightness: ^2.1.11 sensors_plus: ^7.0.0 shared_preferences: ^2.5.3 share_plus: ^12.0.1 diff --git a/packages/flet/test/transport/data_channel_test.dart b/packages/flet/test/transport/data_channel_test.dart new file mode 100644 index 0000000000..750caf9171 --- /dev/null +++ b/packages/flet/test/transport/data_channel_test.dart @@ -0,0 +1,128 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flet/flet.dart'; +import 'package:flet/src/transport/protocol_muxed_data_channel.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Minimal in-memory transport: captures outbound packets and lets the test +/// inject inbound ones, exercising FletBackend's type-byte dispatch + the +/// muxed DataChannel routing without spinning up a real transport. +class _FakeChannel implements FletBackendChannel { + final List sent = []; + late FletBackendChannelOnPacketCallback _onPacket; + + FletBackendChannelBuilder get builder => ({ + required FletBackendChannelOnPacketCallback onPacket, + required FletBackendChannelOnDisconnectCallback onDisconnect, + }) { + _onPacket = onPacket; + return this; + }; + + void deliver(Uint8List packet) => _onPacket(packet); + + @override + Future connect() async {} + + @override + bool get isLocalConnection => true; + + @override + int get defaultReconnectIntervalMs => 0; + + @override + void send(Uint8List packet) => sent.add(Uint8List.fromList(packet)); + + @override + void disconnect() {} +} + +void main() { + group('ProtocolMuxedDataChannel wire format', () { + test('send emits [0x01][channel_id:u32 LE][payload]', () { + final backend = FletBackend( + pageUri: Uri.parse("mock"), + assetsDir: "", + extensions: [], + multiView: false); + final ch1 = backend.openDataChannel(); + final ch2 = backend.openDataChannel(); + expect(ch1.id, 1); + expect(ch2.id, 2); + }); + + test('inbound 0x01 frame routes to the right channel', () async { + final fake = _FakeChannel(); + final backend = FletBackend( + pageUri: Uri.parse("mock"), + assetsDir: "", + extensions: [], + multiView: false, + channelBuilder: fake.builder); + // Trigger the connect path so FletBackend wires the fake's onPacket. + // We don't await — connect() also tries _registerClient which goes + // through the (now unused) channel send path; both are fine for this + // smoke test. + // ignore: unawaited_futures + backend.connect(); + + final chA = backend.openDataChannel() as ProtocolMuxedDataChannel; + final chB = backend.openDataChannel() as ProtocolMuxedDataChannel; + + final aFrames = []; + final bFrames = []; + chA.messages.listen(aFrames.add); + chB.messages.listen(bFrames.add); + + // Inbound for channel B (id=2) → 0x01, id LE, payload. + final inbound = Uint8List(5 + 3); + inbound[0] = 0x01; + ByteData.sublistView(inbound, 1, 5).setUint32(0, chB.id, Endian.little); + inbound.setRange(5, 8, [10, 20, 30]); + fake.deliver(inbound); + + // Let the stream-controller microtask flush. + await Future.delayed(Duration.zero); + + expect(aFrames, isEmpty); + expect(bFrames, hasLength(1)); + expect(bFrames.single, equals(Uint8List.fromList([10, 20, 30]))); + + // Stale frame for closed channel is silently dropped. + chA.close(); + final staleA = Uint8List(5 + 1); + staleA[0] = 0x01; + ByteData.sublistView(staleA, 1, 5).setUint32(0, 1, Endian.little); + staleA[5] = 99; + fake.deliver(staleA); // no throw, no delivery + }); + + test('outbound send emits the muxed packet shape', () async { + final fake = _FakeChannel(); + final backend = FletBackend( + pageUri: Uri.parse("mock"), + assetsDir: "", + extensions: [], + multiView: false, + channelBuilder: fake.builder); + // ignore: unawaited_futures + backend.connect(); + + // Drain the register-client packet that FletBackend.connect emits. + final preCount = fake.sent.length; + + final ch = backend.openDataChannel(); + ch.send(Uint8List.fromList([0xAA, 0xBB, 0xCC])); + + // Filter to the 0x01 packets we care about. + final dataPackets = + fake.sent.skip(preCount).where((p) => p.isNotEmpty && p[0] == 0x01).toList(); + expect(dataPackets, hasLength(1)); + final p = dataPackets.single; + expect(p[0], 0x01); + expect(ByteData.sublistView(p, 1, 5).getUint32(0, Endian.little), ch.id); + expect(p.sublist(5), equals(Uint8List.fromList([0xAA, 0xBB, 0xCC]))); + }); + }); +} diff --git a/sdk/python/_typos.toml b/sdk/python/_typos.toml index e68027a320..b503150fa2 100644 --- a/sdk/python/_typos.toml +++ b/sdk/python/_typos.toml @@ -4,3 +4,5 @@ AACHE = "AACHE" ROUTEROS = "ROUTEROS" # OpenType variable font axis tag for width wdth = "wdth" +# Python package name (Mozilla CA bundle) +certifi = "certifi" diff --git a/sdk/python/examples/extensions/charts/matplotlib_chart/three_d/main.py b/sdk/python/examples/extensions/charts/matplotlib_chart/three_d/main.py index 9cc1c2084d..98f369dcf5 100644 --- a/sdk/python/examples/extensions/charts/matplotlib_chart/three_d/main.py +++ b/sdk/python/examples/extensions/charts/matplotlib_chart/three_d/main.py @@ -1,4 +1,8 @@ +import asyncio import logging +import time +from collections import deque +from dataclasses import dataclass import matplotlib.pyplot as plt @@ -8,7 +12,134 @@ logging.basicConfig(level=logging.INFO) -def main(page: ft.Page): +@dataclass +class _Frame: + t: float + size: int + + +class FrameStats: + """Rolling counters for the matplotlib WebAgg-style frame stream. + + Cumulative averages for per-frame size + total bytes (lifetime of the + run); a short sliding window for transfer speed and FPS so the bar + reflects current activity rather than being dragged down by idle time. + + Latency split (also sliding-window): + + * `dart_ms` — time from a frame leaving Python (`apply_full/diff` call) + to Dart's `[0xFF]` ack arriving. This is Dart-side decode + paint + + ack transit. Transit is microseconds, so essentially decode + paint. + * `mpl_ms` — time from one ack arriving to the next frame leaving + Python. Combines matplotlib's render of the next frame and any + idle time waiting for matplotlib to react to the next "draw" + request. Under sustained interactive load (continuous dragging) + idle ≈ 0 and this is dominated by matplotlib's render cost. + """ + + WINDOW_SECONDS = 2.0 + + def __init__(self) -> None: + self.full_count = 0 + self.full_total = 0 + self.diff_count = 0 + self.diff_total = 0 + self.bytes_total = 0 + self._recent: deque[_Frame] = deque() + # Latency tracking: each entry is (timestamp_when_observed, latency_seconds). + self._dart_latencies: deque[tuple[float, float]] = deque() + self._mpl_gaps: deque[tuple[float, float]] = deque() + # In-flight bookkeeping for pairing send with ack. + self._inflight_send_ts: float | None = None + self._last_ack_ts: float | None = None + + def record_send(self, size: int, is_full: bool) -> None: + """Frame about to leave Python — record size and mark in-flight.""" + now = time.monotonic() + if is_full: + self.full_count += 1 + self.full_total += size + else: + self.diff_count += 1 + self.diff_total += size + self.bytes_total += size + self._recent.append(_Frame(now, size)) + self._evict(now) + # mpl-side gap: how long since the previous ack. + if self._last_ack_ts is not None: + self._mpl_gaps.append((now, now - self._last_ack_ts)) + self._evict_latency_window(self._mpl_gaps, now) + self._inflight_send_ts = now + + def record_ack(self) -> None: + """Dart confirms the frame painted — close the dart-side timing.""" + now = time.monotonic() + if self._inflight_send_ts is not None: + self._dart_latencies.append((now, now - self._inflight_send_ts)) + self._evict_latency_window(self._dart_latencies, now) + self._inflight_send_ts = None + self._last_ack_ts = now + + def _evict(self, now: float) -> None: + cutoff = now - self.WINDOW_SECONDS + while self._recent and self._recent[0].t < cutoff: + self._recent.popleft() + + def _evict_latency_window( + self, window: deque[tuple[float, float]], now: float + ) -> None: + cutoff = now - self.WINDOW_SECONDS + while window and window[0][0] < cutoff: + window.popleft() + + @property + def avg_full(self) -> float: + return self.full_total / self.full_count if self.full_count else 0.0 + + @property + def avg_diff(self) -> float: + return self.diff_total / self.diff_count if self.diff_count else 0.0 + + def speed_and_fps(self) -> tuple[float, float]: + now = time.monotonic() + self._evict(now) + if not self._recent: + return 0.0, 0.0 + span = max(self.WINDOW_SECONDS, now - self._recent[0].t) + speed = sum(f.size for f in self._recent) / span + fps = len(self._recent) / span + return speed, fps + + def dart_avg_ms(self) -> float: + now = time.monotonic() + self._evict_latency_window(self._dart_latencies, now) + if not self._dart_latencies: + return 0.0 + return ( + 1000.0 + * sum(lat for _, lat in self._dart_latencies) + / len(self._dart_latencies) + ) + + def mpl_avg_ms(self) -> float: + now = time.monotonic() + self._evict_latency_window(self._mpl_gaps, now) + if not self._mpl_gaps: + return 0.0 + return 1000.0 * sum(g for _, g in self._mpl_gaps) / len(self._mpl_gaps) + + +def _human_bytes(n: float) -> str: + if n < 1024: + return f"{n:.0f} B" + if n < 1024 * 1024: + return f"{n / 1024:.1f} KB" + if n < 1024**3: + return f"{n / (1024 * 1024):.1f} MB" + return f"{n / (1024**3):.2f} GB" + + +async def main(page: ft.Page): from mpl_toolkits.mplot3d import axes3d fig = plt.figure() @@ -36,13 +167,93 @@ def main(page: ft.Page): zlabel="Z", ) + chart = flet_charts.MatplotlibChartWithToolbar(figure=fig, expand=True) + + # Status bar: regular Flet Text controls in a Row at the bottom. + avg_full_text = ft.Text("avg full: —", size=12) + avg_diff_text = ft.Text("avg diff: —", size=12) + total_text = ft.Text("total: —", size=12) + speed_text = ft.Text("speed: —", size=12) + fps_text = ft.Text("fps: —", size=12) + dart_text = ft.Text("dart: —", size=12) + mpl_text = ft.Text("mpl: —", size=12) + + status_bar = ft.Container( + content=ft.Row( + [ + avg_full_text, + avg_diff_text, + total_text, + speed_text, + fps_text, + dart_text, + mpl_text, + ], + spacing=20, + ), + padding=ft.Padding.symmetric(horizontal=12, vertical=6), + bgcolor=ft.Colors.SURFACE_CONTAINER_HIGH, + ) + page.add( ft.SafeArea( - content=flet_charts.MatplotlibChartWithToolbar(figure=fig, expand=True), + content=ft.Column( + [chart, status_bar], + expand=True, + spacing=0, + ), expand=True, ) ) + stats = FrameStats() + + # Instrument the canvas to capture per-frame sizes + latency. Both + # `chart.mpl` and `chart.mpl.mpl_canvas` are populated by their + # respective `build()` calls, which run only after the control is + # mounted — hence this wrapping has to happen after `page.add(...)`. + canvas = chart.mpl.mpl_canvas + orig_full = canvas.apply_full + orig_diff = canvas.apply_diff + + async def apply_full(image_bytes: bytes) -> None: + stats.record_send(len(image_bytes), is_full=True) + await orig_full(image_bytes) + + async def apply_diff(image_bytes: bytes) -> None: + stats.record_send(len(image_bytes), is_full=False) + await orig_diff(image_bytes) + + canvas.apply_full = apply_full + canvas.apply_diff = apply_diff + + # Register an observer for frame-applied acks so we can record the + # Dart-side timing. Pure observation — backpressure is handled by + # the apply_*/await pattern in `MatplotlibChart._receive_loop`. + canvas.set_on_frame_applied(stats.record_ack) + + # Background task: refresh the labels at ~4 Hz so speed/fps decay + # visibly when traffic stops and stay readable during fast drags + # (vs. updating once per frame, which thrashes the layout). + async def refresh_loop() -> None: + while True: + speed, fps = stats.speed_and_fps() + avg_full_text.value = ( + f"avg full: {_human_bytes(stats.avg_full)} (n={stats.full_count})" + ) + avg_diff_text.value = ( + f"avg diff: {_human_bytes(stats.avg_diff)} (n={stats.diff_count})" + ) + total_text.value = f"total: {_human_bytes(stats.bytes_total)}" + speed_text.value = f"speed: {_human_bytes(speed)}/s" + fps_text.value = f"fps: {fps:.1f}" + dart_text.value = f"dart: {stats.dart_avg_ms():.1f} ms" + mpl_text.value = f"mpl: {stats.mpl_avg_ms():.1f} ms" + page.update() + await asyncio.sleep(0.25) + + asyncio.create_task(refresh_loop()) + if __name__ == "__main__": ft.run(main) diff --git a/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_chart.py b/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_chart.py index ed31e64403..edefff3963 100644 --- a/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_chart.py +++ b/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_chart.py @@ -434,10 +434,15 @@ async def _receive_loop(self): if is_binary: assert isinstance(content, (bytes, bytearray)) logger.debug(f"receive_binary({len(content)})") - # Hand the frame to the client widget — full PNG replaces the - # backbuffer, diff PNG composites onto it. Awaiting naturally - # rate-limits this loop to the client's processing speed and - # yields the asyncio loop for incoming events. + # Hand the frame to the client widget — full PNG replaces + # the backbuffer, diff PNG composites onto it. `await` + # here serialises this receive loop on the Dart-side + # frame-applied ack: matplotlib "draw" notifications that + # arrive during the round-trip stay queued in + # `_receive_queue` and are processed after the ack returns, + # instead of being eagerly dropped against a stale + # `_waiting=True` gate. This is the same backpressure shape + # the 0.85 `_invoke_method` round-trip used to provide. if self.__image_mode == "full": await self.mpl_canvas.apply_full(bytes(content)) else: diff --git a/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_chart_canvas.py b/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_chart_canvas.py index 174167a404..76f25b064e 100644 --- a/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_chart_canvas.py +++ b/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_chart_canvas.py @@ -1,5 +1,8 @@ +import asyncio +import contextlib +from collections import deque from dataclasses import dataclass, field -from typing import Optional +from typing import Callable, Optional import flet as ft @@ -26,10 +29,24 @@ class MatplotlibChartCanvas(ft.LayoutControl): Receives full and incremental "diff" PNG frames and composites them in CPU memory, holding at most one decoded image for display at a time. - Avoids the per-frame `Picture.toImage` allocations that the generic - `flet.canvas.Canvas` capture path uses, which on Flutter web - (CanvasKit/WASM) accumulate and aren't promptly reclaimed by the JS GC - during animation, causing browser memory growth. + Frames flow over a dedicated [ft.DataChannel] rather than the regular + Flet protocol, so the PNG bytes skip MsgPack encode/decode and travel + at memory-bandwidth-class speed in embedded native mode (per-channel + PythonBridge) and at near-bandwidth speed in web/dev modes (raw-byte + frames muxed over the protocol transport). + + Wire format on the data channel (one byte of opcode followed by the + PNG payload): + + | opcode | payload | meaning | + |--------|------------|--------------------------------------------| + | 0x01 | PNG bytes | apply_full — replace backdrop | + | 0x02 | PNG bytes | apply_diff — composite onto backbuffer | + | 0x03 | (empty) | clear — drop backdrop + backbuffer | + + The reverse-direction `resize` event (Dart → Python, small JSON-shaped + payload) stays on the existing Flet protocol channel — no reason to + move tiny control events off it. """ resize_interval: ft.Number = 10 @@ -42,6 +59,83 @@ class MatplotlibChartCanvas(ft.LayoutControl): Called when the size of this canvas has changed. """ + on_data_channel_open: Optional[ft.EventHandler[ft.DataChannelOpenEvent]] = None + """ + Framework hook — Dart fires this when it opens the data channel during + `initState`. The default handler captures the channel for use by + apply_full / apply_diff / clear. Override only if you need to do + something extra at attach-time. + """ + + def init(self) -> None: + # `init` is the @ft.control post-construct lifecycle hook (runs + # before `did_mount`). Wire up the default channel-capture handler. + self._channel: Optional[ft.DataChannel] = None + # FIFO of per-frame ack futures. Each `apply_*` enqueues a future + # and `awaits` it; `_on_dart_message` pops the head and resolves + # it when Dart's `[0xFF]` ack arrives. The await is what makes + # producer-side callers (e.g. MatplotlibChart._receive_loop) + # block — events queued during the wait are processed *after* + # the ack instead of being dropped by stale gate checks. + self._pending_acks: deque[asyncio.Future] = deque() + # Optional plain callback for observers that want to be notified + # on every frame ack (e.g. perf instrumentation). Fires alongside + # the future resolution; not load-bearing for backpressure. + self._on_frame_applied: Optional[Callable[[], None]] = None + if self.on_data_channel_open is None: + self.on_data_channel_open = self._capture_channel + + def _capture_channel(self, e: ft.DataChannelOpenEvent) -> None: + # Single-channel widget; no need to dispatch on e.channel_name. + self._channel = self.get_data_channel(e.channel_id) + self._channel.on_bytes(self._on_dart_message) + + def _on_dart_message(self, payload: bytes) -> None: + # Wire format on the reverse direction (Dart → Python): + # [0xFF] — frame_applied ack. Sent by Dart after each apply_full / + # apply_diff / clear completes on its end. Restores the + # round-trip backpressure that `_invoke_method` used to + # provide implicitly in 0.85. + if not payload or payload[0] != 0xFF: + return + # Resolve the head future so the matching `apply_*` await returns. + if self._pending_acks: + fut = self._pending_acks.popleft() + if not fut.done(): + fut.set_result(None) + # Then fire the observer callback (if any). + cb = self._on_frame_applied + if cb is not None: + with contextlib.suppress(Exception): + cb() + + def set_on_frame_applied(self, cb: Optional[Callable[[], None]]) -> None: + """Register a side-channel callback invoked on every frame ack. + + Useful for instrumentation. Backpressure is handled by awaiting + the result of `apply_full` / `apply_diff` / `clear` directly — + this callback is a fire-and-forget observer, not part of the + gating path. + """ + self._on_frame_applied = cb + + async def _send_and_wait(self, packet: bytes) -> None: + """Send a channel packet and await Dart's ack. + + Awaiting blocks the caller until `[0xFF]` arrives on the channel, + re-creating the 0.85 `_invoke_method` round-trip semantics: + events that arrive in the producer's queue during the wait stay + queued (instead of being processed eagerly against a stale + `_waiting` flag). + """ + if self._channel is None: + return + loop = asyncio.get_running_loop() + fut: asyncio.Future = loop.create_future() + self._pending_acks.append(fut) + self._channel.send(packet) + await fut + async def apply_full(self, image_bytes: bytes) -> None: """ Replace the current displayed image with a full PNG frame. @@ -49,7 +143,7 @@ async def apply_full(self, image_bytes: bytes) -> None: Args: image_bytes: PNG bytes of the complete frame. """ - await self._invoke_method("apply_full", arguments={"bytes": image_bytes}) + await self._send_and_wait(b"\x01" + image_bytes) async def apply_diff(self, image_bytes: bytes) -> None: """ @@ -63,10 +157,10 @@ async def apply_diff(self, image_bytes: bytes) -> None: Args: image_bytes: PNG bytes of the diff frame. """ - await self._invoke_method("apply_diff", arguments={"bytes": image_bytes}) + await self._send_and_wait(b"\x02" + image_bytes) async def clear(self) -> None: """ Clear the displayed image and discard the backbuffer. """ - await self._invoke_method("clear") + await self._send_and_wait(b"\x03") diff --git a/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/matplotlib_chart_canvas.dart b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/matplotlib_chart_canvas.dart index 2970c752f0..a44ef3e3ef 100644 --- a/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/matplotlib_chart_canvas.dart +++ b/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/matplotlib_chart_canvas.dart @@ -35,22 +35,9 @@ class MatplotlibChartCanvasControl extends StatefulWidget { } // --------------------------------------------------------------------------- -// Shared helpers +// Shared base // --------------------------------------------------------------------------- -Uint8List _extractBytes(dynamic args) { - final v = args is Map ? args["bytes"] : args; - if (v is Uint8List) return v; - if (v is ByteData) { - return v.buffer.asUint8List(v.offsetInBytes, v.lengthInBytes); - } - if (v is List) return Uint8List.fromList(v); - if (v is List && v.every((e) => e is int)) { - return Uint8List.fromList(v.cast()); - } - throw ArgumentError("Expected bytes for image data, got ${v.runtimeType}"); -} - abstract class _MatplotlibChartCanvasStateBase extends State { // Serialize concurrent apply_full / apply_diff calls so backdrop mutations @@ -60,15 +47,30 @@ abstract class _MatplotlibChartCanvasStateBase Size _lastSize = Size.zero; int _lastResize = DateTime.now().millisecondsSinceEpoch; + DataChannel? _channel; + StreamSubscription? _channelSub; + @override - void initState() { - super.initState(); - widget.control.addInvokeMethodListener(_invokeMethod); + void didChangeDependencies() { + super.didChangeDependencies(); + // Open the data channel lazily on first dependency lookup — we need + // BuildContext to reach FletBackend, which isn't available in initState. + if (_channel != null) return; + _channel = FletBackend.of(context).openDataChannel(); + _channelSub = _channel!.messages.listen(_onChannelFrame); + // Announce the channel to Python via the standard convention event. + widget.control.triggerEvent("data_channel_open", { + "channel_name": "frames", + "channel_id": _channel!.id, + }); } @override void dispose() { - widget.control.removeInvokeMethodListener(_invokeMethod); + _channelSub?.cancel(); + _channelSub = null; + _channel?.close(); + _channel = null; disposeResources(); super.dispose(); } @@ -81,22 +83,42 @@ abstract class _MatplotlibChartCanvasStateBase Future clearAll(); CustomPainter buildPainter(); - Future _invokeMethod(String name, dynamic args) async { - switch (name) { - case "apply_full": - await _enqueue(() => applyFull(_extractBytes(args))); - return; - case "apply_diff": - await _enqueue(() => applyDiff(_extractBytes(args))); - return; - case "clear": - await _enqueue(clearAll); - return; + // 1-byte ack sent back to Python after each apply completes. Restores + // round-trip backpressure: matplotlib's producer side keeps `_waiting` + // set until this ack arrives, so frames don't pile up in the Dart-side + // queue during interactive drags. + static final Uint8List _frameAppliedAck = Uint8List.fromList([0xFF]); + + /// Inbound DataChannel frame. Wire format: + /// [0x01][PNG bytes] → apply_full + /// [0x02][PNG bytes] → apply_diff + /// [0x03] → clear + void _onChannelFrame(Uint8List bytes) { + if (bytes.isEmpty) return; + // Zero-copy slice of the same underlying buffer. + final payload = Uint8List.sublistView(bytes, 1); + switch (bytes[0]) { + case 0x01: + _enqueueAndAck(() => applyFull(payload)); + break; + case 0x02: + _enqueueAndAck(() => applyDiff(payload)); + break; + case 0x03: + _enqueueAndAck(clearAll); + break; default: - throw Exception("Unknown MatplotlibChartCanvas method: $name"); + debugPrint( + "MatplotlibChartCanvas: unknown data-channel opcode 0x${bytes[0].toRadixString(16)}"); } } + void _enqueueAndAck(Future Function() task) { + _enqueue(task).whenComplete(() { + _channel?.send(_frameAppliedAck); + }); + } + Future _enqueue(Future Function() task) { final prev = _applyChain ?? Future.value(); final next = prev.then((_) => task()); diff --git a/sdk/python/packages/flet-cli/src/flet_cli/cli.py b/sdk/python/packages/flet-cli/src/flet_cli/cli.py index 06815bef16..08a7e511b3 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/cli.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/cli.py @@ -1,8 +1,7 @@ import argparse +import json import sys -from packaging.version import Version - import flet.version import flet_cli.commands.build import flet_cli.commands.clean @@ -15,29 +14,28 @@ import flet_cli.commands.publish import flet_cli.commands.run import flet_cli.commands.serve -from flet_cli.utils.python_versions import ( - DEFAULT_PYTHON_VERSION, - SUPPORTED_PYTHON_VERSIONS, -) - - -def _supported_python_versions_block() -> str: - """Render the multi-line `flet --version` listing of supported Python releases.""" - lines = ["Supported Python versions:"] - sorted_versions = sorted( - SUPPORTED_PYTHON_VERSIONS, - key=lambda r: Version(r.short), - reverse=True, - ) - for r in sorted_versions: - suffix = [] - if r.prerelease: - suffix.append("pre-release") - if r.short == DEFAULT_PYTHON_VERSION: - suffix.append("default") - tail = f", {', '.join(suffix)}" if suffix else "" - lines.append(f" {r.short} (Pyodide {r.pyodide}{tail})") - return "\n".join(lines) +from flet_cli.utils.linux_deps import linux_dependencies + + +def _version_info() -> dict: + """Build the machine-readable `flet --version --json` document. + + Exposes Flet/Flutter versions and the Linux build dependencies. The + supported Python/Pyodide set is no longer surfaced here — it now comes from + python-build's manifest (see `flet_cli.utils.python_versions`). + """ + return { + "flet": flet.version.flet_version, + "flutter": flet.version.flutter_version, + "linux_dependencies": list(linux_dependencies), + } + + +def _render_version(as_json: bool) -> str: + """Render `flet --version` output as JSON or the human-readable text block.""" + if as_json: + return json.dumps(_version_info(), indent=2) + return f"Flet: {flet.version.flet_version}\nFlutter: {flet.version.flutter_version}" # Source https://stackoverflow.com/a/26379693 @@ -94,16 +92,17 @@ def get_parser() -> argparse.ArgumentParser: formatter_class=argparse.RawDescriptionHelpFormatter ) - # add version flag + # add version flags parser.add_argument( "--version", "-V", - action="version", - version=( - f"Flet: {flet.version.flet_version}\n" - f"Flutter: {flet.version.flutter_version}\n" - f"{_supported_python_versions_block()}" - ), + action="store_true", + help="show version information and exit", + ) + parser.add_argument( + "--json", + action="store_true", + help="with --version, output version information as JSON", ) sp = parser.add_subparsers(dest="command") @@ -138,6 +137,11 @@ def main(): # parse arguments args = parser.parse_args() + # handle `flet --version [--json]` (no subcommand/handler is set) + if getattr(args, "version", False): + print(_render_version(args.json)) + sys.exit(0) + # execute command args.handler(args) diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/build.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/build.py index fb45b8e5a4..033ce5e5e4 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/build.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/build.py @@ -1,9 +1,13 @@ import argparse +import os +import shutil +from pathlib import Path from rich.console import Group from rich.live import Live from flet_cli.commands.build_base import BaseBuildCommand, console +from flet_cli.utils.android import flutter_target_platforms class Command(BaseBuildCommand): @@ -116,9 +120,24 @@ def add_flutter_command_args(self, args: list[str]): ["build", self.platforms[self.target_platform]["flutter_build_command"]] ) - if self.target_platform in "apk" and self.template_data["split_per_abi"]: + if self.target_platform == "apk" and self.template_data["split_per_abi"]: args.append("--split-per-abi") + if ( + self.target_platform in ("apk", "aab") + and self.template_data["options"]["target_arch"] + ): + args.extend( + [ + "--target-platform", + ",".join( + flutter_target_platforms( + self.template_data["options"]["target_arch"] + ) + ), + ] + ) + if self.target_platform in ["ipa"]: if self.template_data["ios_provisioning_profile"]: args.extend( @@ -166,6 +185,22 @@ def run_flutter(self): f"{self.platforms[self.target_platform]['status_text']}[/cyan]..." ) + # Clear the build output directories of artifacts from previous runs. Flutter + # only ever adds files to them, and copy_build_output harvests them wholesale — + # so without this, a previous build with different options (e.g. --arch, + # --split-per-abi, or a renamed product) would leak its artifacts into the + # user's output directory. + assert self.flutter_dir + flutter_dir = self.flutter_dir.resolve() + for output in self.platforms[self.target_platform]["outputs"]: + output_dir = Path( + os.path.dirname(self.resolve_output_path(output)) + ).resolve() + # only delete directories that are strictly inside the generated Flutter + # project (and never the project directory itself). + if output_dir != flutter_dir and output_dir.is_relative_to(flutter_dir): + shutil.rmtree(output_dir, ignore_errors=True) + self._run_flutter_command() console.log( diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py index c7c989638b..529dd2e804 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py @@ -24,6 +24,10 @@ verbose2_style, warning_style, ) +from flet_cli.utils.android import ( + ANDROID_ARCH_TO_FLUTTER_TARGET_PLATFORM, + excluded_android_abis, +) from flet_cli.utils.cli import parse_cli_bool_value from flet_cli.utils.hash_stamp import HashStamp from flet_cli.utils.merge import merge_dict @@ -43,6 +47,18 @@ "v{version}/flet-build-template.zip" ) +# Android (serious_python native-mmap packaging): pure Python ships in stored zips +# read via zipimport, which breaks packages that read bundled data through a real +# filesystem path (__file__ / pkg_resources) instead of importlib.resources. Such +# packages are shipped extracted to disk via --android-extract-packages or +# [tool.flet.android].extract_packages. +# +# The default set is empty: the common offenders read their data via +# importlib.resources, which is zip-safe (e.g. certifi.where() works from the zip — +# importlib.resources.as_file() extracts cacert.pem to a temp file on demand). Add +# real offenders here as they are found. +ANDROID_DEFAULT_EXTRACT_PACKAGES: list[str] = [] + class BaseBuildCommand(BaseFlutterCommand): """ @@ -249,10 +265,13 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: parser.add_argument( "--arch", dest="target_arch", + action="extend", nargs="+", default=[], help="Build for specific CPU architectures " - "(used in macOS and Android builds only). Example: `--arch arm64 x64`", + "(used in macOS and Android builds only). " + "Android: arm64-v8a, armeabi-v7a, x86_64; macOS: arm64, x64. " + "Example: `--arch arm64-v8a`", ) parser.add_argument( "--exclude", @@ -454,16 +473,18 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: parser.add_argument( "--compile-app", dest="compile_app", - action="store_true", + action=argparse.BooleanOptionalAction, default=None, - help="Pre-compile app's `.py` files to `.pyc`", + help="Pre-compile app's `.py` files to `.pyc` (on by default; " + "use --no-compile-app to disable)", ) parser.add_argument( "--compile-packages", dest="compile_packages", - action="store_true", + action=argparse.BooleanOptionalAction, default=None, - help="Pre-compile site packages' `.py` files to `.pyc`", + help="Pre-compile site packages' `.py` files to `.pyc` (on by default; " + "use --no-compile-packages to disable)", ) parser.add_argument( "--cleanup-app", @@ -503,10 +524,20 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: parser.add_argument( "--source-packages", dest="source_packages", + action="extend", nargs="+", default=[], help="The list of Python packages to install from source distributions", ) + parser.add_argument( + "--android-extract-packages", + dest="android_extract_packages", + nargs="+", + default=[], + help="Android only: Python packages (relative paths) to ship extracted " + "to disk instead of inside the app zip — for packages that read bundled " + "data via __file__ / pkg_resources rather than importlib.resources", + ) parser.add_argument( "--python-version", dest="python_version", @@ -566,6 +597,7 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: "--permissions", dest="permissions", type=str.lower, + action="extend", nargs="+", default=[], choices=["location", "camera", "microphone", "photo_library"], @@ -730,6 +762,22 @@ def initialize_command(self): except UnsupportedPythonVersionError as e: self.cleanup(1, str(e)) + # Changing the bundled Python version invalidates the compiled bytecode + # baked into the previous build's native bundles (stdlib/site-packages + # .pyc). Reusing the build directory would mix versions and crash at + # runtime with "bad magic number". Force a clean rebuild on a switch. + version_marker = self.build_dir / ".python-version" + if self.build_dir.exists() and version_marker.exists(): + previous = version_marker.read_text(encoding="utf-8").strip() + if previous and previous != self.python_release.short: + console.log( + f"Bundled Python version changed ({previous} -> " + f"{self.python_release.short}); cleaning the build directory." + ) + shutil.rmtree(self.build_dir, ignore_errors=True) + self.build_dir.mkdir(parents=True, exist_ok=True) + version_marker.write_text(self.python_release.short, encoding="utf-8") + def validate_target_platform(self): """ Validate whether current host OS can build the selected target platform. @@ -1123,6 +1171,40 @@ def _xml_attr_value(v): or self.get_pyproject(f"tool.flet.{self.config_platform}.target_arch") or self.get_pyproject("tool.flet.target_arch") ) + target_arch = ( + target_arch + if isinstance(target_arch, list) + else [target_arch] + if isinstance(target_arch, str) + else [] + ) + if self.package_platform == "Android": + invalid_archs = [ + arch + for arch in target_arch + if arch not in ANDROID_ARCH_TO_FLUTTER_TARGET_PLATFORM + ] + if invalid_archs: + self.cleanup( + 1, + f"Invalid Android architecture(s): {', '.join(invalid_archs)}.\n" + f"Supported: " + f"{', '.join(ANDROID_ARCH_TO_FLUTTER_TARGET_PLATFORM)}.\n" + f"Docs: https://flet.dev/docs/publish/android#supported-target-architectures", + ) + python_abis = list(self.python_release.android_abis) + unsupported_archs = [a for a in target_arch if a not in python_abis] + if unsupported_archs: + self.cleanup( + 1, + f"Architecture(s) not supported by Python " + f"{self.python_release.short}: {', '.join(unsupported_archs)}.\n" + f"Supported: {', '.join(python_abis)}.\n" + f"Docs: https://flet.dev/docs/publish/android#supported-target-architectures", + ) + if not target_arch: + # Build only for the ABIs the bundled Python supports. + target_arch = python_abis ios_export_method = ( self.options.ios_export_method @@ -1204,6 +1286,10 @@ def _xml_attr_value(v): "no_cdn": ( self.options.no_cdn or self.get_pyproject("tool.flet.web.cdn") == False # noqa: E712 ), + # Surface the resolved Pyodide release to the cookiecutter + # context so the web template's index.html can wire the + # correct jsdelivr URL when CDN mode is on. + "pyodide_version": self.python_release.pyodide, "base_url": f"/{base_url}/" if base_url else "/", "split_per_abi": split_per_abi, "project_name": project_name, @@ -1235,11 +1321,10 @@ def _xml_attr_value(v): "package_platform": self.package_platform, "config_platform": self.config_platform, "python_version": self.python_release.short, - "target_arch": ( - target_arch - if isinstance(target_arch, list) - else [target_arch] - if isinstance(target_arch, str) + "target_arch": target_arch, + "android_excluded_abis": ( + excluded_android_abis(target_arch) + if self.package_platform == "Android" else [] ), "info_plist": info_plist, @@ -1922,10 +2007,16 @@ def package_python_app(self): ] if self.template_data["options"]["target_arch"]: + # serious_python's --arch is a Dart multi-option: values must be + # comma-separated or the flag repeated. Space-separated values + # after the first are silently treated as positional arguments. package_args.extend( - ["--arch"] + self.template_data["options"]["target_arch"] + ["--arch", ",".join(self.template_data["options"]["target_arch"])] ) + # Only the short version is passed; serious_python derives the full + # version, python-build date, and dart_bridge version from its own + # committed snapshot of the manifest. package_env = { "SERIOUS_PYTHON_VERSION": self.python_release.short, } @@ -2032,11 +2123,31 @@ def package_python_app(self): source_packages ) - if self.get_bool_setting(self.options.compile_app, "compile.app", False): + # android-extract-packages: path-hungry packages shipped extracted to disk + # instead of inside the zip (serious_python Android native-mmap packaging). + # A built-in default set covers commonly-broken packages; the user list + # (CLI / pyproject) is merged on top. Consumed by the serious_python_android + # Gradle split during `flutter build`, so the env var is set on build_env + # (see _run_flutter_command), not on the package step. + self.android_extract_packages: list[str] = [] + if self.package_platform == "Android": + user_extract_packages = ( + self.options.android_extract_packages + or self.get_pyproject( + f"tool.flet.{self.config_platform}.extract_packages" + ) + or self.get_pyproject("tool.flet.extract_packages") + or [] + ) + self.android_extract_packages = list( + dict.fromkeys(ANDROID_DEFAULT_EXTRACT_PACKAGES + user_extract_packages) + ) + + if self.get_bool_setting(self.options.compile_app, "compile.app", True): package_args.append("--compile-app") if self.get_bool_setting( - self.options.compile_packages, "compile.packages", False + self.options.compile_packages, "compile.packages", True ): package_args.append("--compile-packages") @@ -2215,6 +2326,8 @@ def _run_flutter_command(self): ] ) + # Only the short version is passed; serious_python derives the rest + # from its committed manifest snapshot. build_env = { "SERIOUS_PYTHON_VERSION": self.python_release.short, } @@ -2225,6 +2338,13 @@ def _run_flutter_command(self): self.build_dir / "site-packages" ) + # Path-hungry packages to ship extracted to disk: consumed by the + # serious_python_android Gradle split during `flutter build`. + if self.package_platform == "Android" and self.android_extract_packages: + build_env["SERIOUS_PYTHON_ANDROID_EXTRACT_PACKAGES"] = ",".join( + self.android_extract_packages + ) + if self.package_platform == "Emscripten" and not self.template_data["no_wasm"]: build_args.append("--wasm") @@ -2297,6 +2417,32 @@ def _run_flutter_command(self): console.log(build_result.stderr, style=error_style) self.cleanup(build_result.returncode if build_result.returncode else 1) + def resolve_output_path(self, build_output: str) -> str: + """ + Resolve a platform `outputs` glob to an absolute path inside the + Flutter project, substituting the `{arch}` and name placeholders. + + Args: + build_output: An entry of `self.platforms[...]["outputs"]`. + """ + + assert self.flutter_dir + assert self.template_data + + arch = platform.machine().lower() + if arch in {"x86_64", "amd64"}: + arch = "x64" + elif arch in {"arm64", "aarch64"}: + arch = "arm64" + + return ( + str(self.flutter_dir.joinpath(build_output)) + .replace("{arch}", arch) + .replace("{artifact_name}", self.template_data["artifact_name"]) + .replace("{project_name}", self.template_data["project_name"]) + .replace("{product_name}", self.template_data["product_name"]) + ) + def copy_build_output(self): """ Copy generated platform artifacts into the requested output directory. @@ -2312,11 +2458,6 @@ def copy_build_output(self): self.update_status( f"[bold blue]Copying build to [cyan]{self.rel_out_dir}[/cyan] directory...", ) - arch = platform.machine().lower() - if arch in {"x86_64", "amd64"}: - arch = "x64" - elif arch in {"arm64", "aarch64"}: - arch = "arm64" def make_ignore_fn(out_dir, out_glob): """ @@ -2335,13 +2476,7 @@ def ignore(path, names): return ignore for build_output in self.platforms[self.target_platform]["outputs"]: - build_output_dir = ( - str(self.flutter_dir.joinpath(build_output)) - .replace("{arch}", arch) - .replace("{artifact_name}", self.template_data["artifact_name"]) - .replace("{project_name}", self.template_data["project_name"]) - .replace("{product_name}", self.template_data["product_name"]) - ) + build_output_dir = self.resolve_output_path(build_output) if self.verbose > 0: console.log( diff --git a/sdk/python/packages/flet-cli/src/flet_cli/utils/android.py b/sdk/python/packages/flet-cli/src/flet_cli/utils/android.py new file mode 100644 index 0000000000..256a97d796 --- /dev/null +++ b/sdk/python/packages/flet-cli/src/flet_cli/utils/android.py @@ -0,0 +1,53 @@ +"""Helpers for Android-specific build configuration.""" + +ANDROID_ARCH_TO_FLUTTER_TARGET_PLATFORM = { + "armeabi-v7a": "android-arm", + "arm64-v8a": "android-arm64", + "x86_64": "android-x64", +} +"""Android ABIs supported by Flutter, mapped to `flutter build --target-platform` +values. Order is meaningful: it defines the order in which +generated artifacts (e.g. Gradle packaging excludes) list ABIs.""" + + +def flutter_target_platforms(archs: list[str]) -> list[str]: + """ + Map Android ABI names to `flutter build --target-platform` values. + + Args: + archs: Android ABI names, e.g. `["arm64-v8a"]`. + + Returns: + The corresponding Flutter target platforms, e.g. `["android-arm64"]`. + + Raises: + ValueError: If any ABI is not supported by Flutter. + """ + + platforms = [] + for arch in archs: + platform = ANDROID_ARCH_TO_FLUTTER_TARGET_PLATFORM.get(arch) + if platform is None: + raise ValueError( + f"Unsupported Android architecture: {arch!r}. " + f"Supported: {', '.join(ANDROID_ARCH_TO_FLUTTER_TARGET_PLATFORM)}." + ) + platforms.append(platform) + return platforms + + +def excluded_android_abis(archs: list[str]) -> list[str]: + """ + Return the supported Android ABIs not requested in `archs`. + + Args: + archs: Requested Android ABI names; an empty list means "all ABIs" + and yields no exclusions. + + Returns: + ABIs to exclude from the generated artifact. + """ + + if not archs: + return [] + return [abi for abi in ANDROID_ARCH_TO_FLUTTER_TARGET_PLATFORM if abi not in archs] diff --git a/sdk/python/packages/flet/src/flet/utils/linux_deps.py b/sdk/python/packages/flet-cli/src/flet_cli/utils/linux_deps.py similarity index 100% rename from sdk/python/packages/flet/src/flet/utils/linux_deps.py rename to sdk/python/packages/flet-cli/src/flet_cli/utils/linux_deps.py diff --git a/sdk/python/packages/flet-cli/src/flet_cli/utils/python_versions.py b/sdk/python/packages/flet-cli/src/flet_cli/utils/python_versions.py index 729903a466..fef64433cf 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/utils/python_versions.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/utils/python_versions.py @@ -1,25 +1,51 @@ """Supported Python versions for `flet build` / `flet publish`. -This module is the single source of truth on the Python side for which -Python releases the Flet toolchain can bundle, and the matching -CPython-standalone + Pyodide artifacts. Mirror any change here in -serious_python's `_pythonReleases` map (bin/package_command.dart). +The set of bundlable Python releases — and the matching CPython-standalone + +Pyodide artifacts — is defined by python-build's date-keyed ``manifest.json``, +the single source of truth shared with serious_python. This module pins one +python-build release date and fetches that release's manifest (cached under +``~/.flet/cache``, immutable per date), so nothing here is hand-mirrored. + +Pin (``PYTHON_BUILD_RELEASE_DATE``) overrides, for dev/CI: +* ``FLET_PYTHON_BUILD_RELEASE_DATE`` — use a different published release date. +* ``FLET_PYTHON_BUILD_MANIFEST`` — read a local ``manifest.json`` instead of + fetching (mirrors serious_python's ``gen_version_tables --manifest``). """ +import json +import os +import shutil +import urllib.request from dataclasses import dataclass +from functools import lru_cache from typing import Callable, Optional from packaging.specifiers import SpecifierSet from packaging.version import Version +from flet_cli.utils.template_cache import get_cache_root + +# python-build release this flet pins. Keep in sync with serious_python's +# `pythonReleaseDate` (lib/src/python_versions.dart) — both should track the +# same python-build release. +PYTHON_BUILD_RELEASE_DATE = "20260618" + +RELEASE_DATE_ENV = "FLET_PYTHON_BUILD_RELEASE_DATE" +MANIFEST_PATH_ENV = "FLET_PYTHON_BUILD_MANIFEST" + +_MANIFEST_URL = ( + "https://github.com/flet-dev/python-build/releases/download/{date}/manifest.json" +) + @dataclass(frozen=True) class PythonRelease: short: str standalone: str - standalone_date: str pyodide: str - pyodide_platform_tag: str + # Android ABIs python-build publishes distributions for this minor. 32-bit + # Android was dropped in 3.13 (PEP 738), so only 3.12 carries armeabi-v7a. + android_abis: tuple[str, ...] # When True, this release is supported via `--python-version` (and an # explicit `requires-python = "==X.Y.*"` specifier) but is not picked # automatically by the default or by open-ended `requires-python` @@ -27,57 +53,83 @@ class PythonRelease: prerelease: bool -SUPPORTED_PYTHON_VERSIONS: list[PythonRelease] = [ - PythonRelease( - short="3.12", - standalone="3.12.13", - standalone_date="20260602", - pyodide="0.27.7", - pyodide_platform_tag="pyodide-2024.0-wasm32", - prerelease=False, - ), - PythonRelease( - short="3.13", - standalone="3.13.13", - standalone_date="20260602", - pyodide="0.29.4", - pyodide_platform_tag="pyodide-2025.0-wasm32", - prerelease=False, - ), - PythonRelease( - short="3.14", - standalone="3.14.5", - standalone_date="20260602", - pyodide="314.0.0a2", - pyodide_platform_tag="pyemscripten-2026.0-wasm32", - prerelease=False, - ), - # Add future pre-release CPython lines with `prerelease=True`. They are - # opt-in via `--python-version 3.15` or an explicit - # `requires-python = "==3.15.*"`; never the auto-resolved default. - # - # PythonRelease( - # short="3.15", - # standalone="3.15.0", - # standalone_date="...", - # pyodide="...", - # pyodide_platform_tag="...", - # prerelease=True, - # ), -] - -DEFAULT_PYTHON_VERSION = "3.14" +def _resolve_release_date() -> str: + return os.environ.get(RELEASE_DATE_ENV) or PYTHON_BUILD_RELEASE_DATE + + +def _load_manifest() -> dict: + """Return the python-build manifest as a dict. + + Reads ``$FLET_PYTHON_BUILD_MANIFEST`` if set; otherwise fetches the pinned + release's ``manifest.json`` (cached immutably under + ``~/.flet/cache/python-build``). Falls back to a present cache when the + network is unavailable; raises with an actionable message if neither the + network nor a cache can supply it. + """ + local = os.environ.get(MANIFEST_PATH_ENV) + if local: + with open(local, encoding="utf-8") as f: + return json.load(f) + + date = _resolve_release_date() + url = _MANIFEST_URL.format(date=date) + cache_path = get_cache_root() / "python-build" / f"manifest-{date}.json" + + if not (cache_path.exists() and cache_path.stat().st_size > 0): + cache_path.parent.mkdir(parents=True, exist_ok=True) + tmp_path = cache_path.with_suffix(cache_path.suffix + ".tmp") + try: + with urllib.request.urlopen(url) as resp, open(tmp_path, "wb") as out: + shutil.copyfileobj(resp, out) + out.flush() + os.fsync(out.fileno()) + os.replace(tmp_path, cache_path) + except BaseException as e: + tmp_path.unlink(missing_ok=True) + if not (cache_path.exists() and cache_path.stat().st_size > 0): + raise RuntimeError( + f"Could not obtain the Python build manifest for release " + f"{date} from {url}: {e}. Check your network connection, or " + f"set ${MANIFEST_PATH_ENV} to a local manifest.json." + ) from e + + with cache_path.open(encoding="utf-8") as f: + return json.load(f) + + +@lru_cache(maxsize=1) +def _load_data() -> tuple[tuple[PythonRelease, ...], str]: + manifest = _load_manifest() + releases = tuple( + PythonRelease( + short=short, + standalone=info["full_version"], + pyodide=info["pyodide_version"], + android_abis=tuple(info["android_abis"]), + prerelease=bool(info.get("prerelease", False)), + ) + for short, info in manifest["pythons"].items() + ) + return releases, manifest["default_python_version"] + + +def get_supported_python_versions() -> list[PythonRelease]: + return list(_load_data()[0]) + + +def get_default_python_version() -> str: + return _load_data()[1] def get_release(short: str) -> Optional[PythonRelease]: - for r in SUPPORTED_PYTHON_VERSIONS: + for r in get_supported_python_versions(): if r.short == short: return r return None def supported_short_versions() -> list[str]: - return [r.short for r in SUPPORTED_PYTHON_VERSIONS] + return [r.short for r in get_supported_python_versions()] class UnsupportedPythonVersionError(ValueError): @@ -92,12 +144,14 @@ def resolve_python_version( Priority: `--python-version` CLI arg → `[project].requires-python` (parsed as a PEP 440 SpecifierSet, highest matching supported short version wins) → - `DEFAULT_PYTHON_VERSION`. + the manifest's default. Raises `UnsupportedPythonVersionError` if the CLI arg names an unsupported version, or if `requires-python` excludes every supported version. """ + supported = get_supported_python_versions() + if cli_arg: release = get_release(cli_arg) if release is None: @@ -122,7 +176,7 @@ def resolve_python_version( # `>=3.14` never silently jumps to a beta CPython line. stable_matching = [ r - for r in SUPPORTED_PYTHON_VERSIONS + for r in supported if not r.prerelease and Version(r.standalone) in spec ] if stable_matching: @@ -139,7 +193,7 @@ def resolve_python_version( spec_with_pre = SpecifierSet(requires, prereleases=True) prerelease_matching = [ r - for r in SUPPORTED_PYTHON_VERSIONS + for r in supported if r.prerelease and Version(r.short) in spec_with_pre ] if prerelease_matching: @@ -154,6 +208,6 @@ def resolve_python_version( f"({', '.join(supported_short_versions())})." ) - fallback = get_release(DEFAULT_PYTHON_VERSION) + fallback = get_release(get_default_python_version()) assert fallback is not None return fallback diff --git a/sdk/python/packages/flet-cli/tests/test_python_versions.py b/sdk/python/packages/flet-cli/tests/test_python_versions.py new file mode 100644 index 0000000000..624e623791 --- /dev/null +++ b/sdk/python/packages/flet-cli/tests/test_python_versions.py @@ -0,0 +1,111 @@ +"""Tests for manifest-backed Python version resolution. + +These run fully offline: `FLET_PYTHON_BUILD_MANIFEST` points the loader at a +local fixture manifest instead of fetching python-build's release asset. +""" + +import json + +import pytest + +from flet_cli.utils import python_versions as pv + +_FIXTURE_MANIFEST = { + "release": "20260614", + "default_python_version": "3.14", + "dart_bridge_version": "1.2.3", + "pythons": { + "3.12": { + "full_version": "3.12.13", + "pyodide_version": "0.27.7", + "android_abis": ["arm64-v8a", "x86_64", "armeabi-v7a"], + "prerelease": False, + }, + "3.13": { + "full_version": "3.13.14", + "pyodide_version": "0.29.4", + "android_abis": ["arm64-v8a", "x86_64"], + "prerelease": False, + }, + "3.14": { + "full_version": "3.14.6", + "pyodide_version": "314.0.0", + "android_abis": ["arm64-v8a", "x86_64"], + "prerelease": False, + }, + # A pre-release line: opt-in only, never auto-resolved. + "3.15": { + "full_version": "3.15.0a1", + "pyodide_version": "315.0.0", + "android_abis": ["arm64-v8a", "x86_64"], + "prerelease": True, + }, + }, +} + + +@pytest.fixture(autouse=True) +def fixture_manifest(tmp_path, monkeypatch): + manifest_path = tmp_path / "manifest.json" + manifest_path.write_text(json.dumps(_FIXTURE_MANIFEST), encoding="utf-8") + monkeypatch.setenv(pv.MANIFEST_PATH_ENV, str(manifest_path)) + pv._load_data.cache_clear() + yield + pv._load_data.cache_clear() + + +def test_default_and_supported_versions(): + assert pv.get_default_python_version() == "3.14" + assert pv.supported_short_versions() == ["3.12", "3.13", "3.14", "3.15"] + + +def test_release_fields_mapped_from_manifest(): + r = pv.get_release("3.14") + assert r is not None + assert (r.short, r.standalone, r.pyodide, r.prerelease) == ( + "3.14", + "3.14.6", + "314.0.0", + False, + ) + assert pv.get_release("3.99") is None + + +def test_resolve_default_when_no_arg(): + assert pv.resolve_python_version(None).short == "3.14" + + +def test_resolve_explicit_cli_arg(): + assert pv.resolve_python_version("3.12").short == "3.12" + + +def test_resolve_unsupported_cli_arg_raises(): + with pytest.raises(pv.UnsupportedPythonVersionError): + pv.resolve_python_version("3.99") + + +def test_resolve_from_requires_python_picks_highest_stable(): + get_pyproject = lambda key: ">=3.13" # noqa: E731 + assert pv.resolve_python_version(None, get_pyproject).short == "3.14" + + +def test_resolve_from_requires_python_exact(): + get_pyproject = lambda key: "==3.12.*" # noqa: E731 + assert pv.resolve_python_version(None, get_pyproject).short == "3.12" + + +def test_requires_python_skips_prerelease_for_open_specifier(): + # `>=3.14` must not silently jump to the 3.15 pre-release line. + get_pyproject = lambda key: ">=3.14" # noqa: E731 + assert pv.resolve_python_version(None, get_pyproject).short == "3.14" + + +def test_requires_python_opts_into_prerelease_explicitly(): + get_pyproject = lambda key: "==3.15.*" # noqa: E731 + assert pv.resolve_python_version(None, get_pyproject).short == "3.15" + + +def test_requires_python_no_match_raises(): + get_pyproject = lambda key: ">=3.99" # noqa: E731 + with pytest.raises(pv.UnsupportedPythonVersionError): + pv.resolve_python_version(None, get_pyproject) diff --git a/sdk/python/packages/flet-web/src/flet_web/fastapi/flet_app.py b/sdk/python/packages/flet-web/src/flet_web/fastapi/flet_app.py index 2c920c5b02..aa8e628f08 100644 --- a/sdk/python/packages/flet-web/src/flet_web/fastapi/flet_app.py +++ b/sdk/python/packages/flet-web/src/flet_web/fastapi/flet_app.py @@ -96,6 +96,11 @@ def __init__( self.__upload_endpoint_path = upload_endpoint_path self.__secret_key = secret_key + # DataChannel mux registry keyed by channel_id minted on the Dart + # side. Populated lazily on the first Control.get_data_channel(id) + # call. Frames for unknown ids are silently dropped. + self._data_channels: dict[int, Any] = {} + app_id = self.__id weakref.finalize( self, lambda: logger.info(f"FletApp was garbage collected: {app_id}") @@ -214,6 +219,11 @@ async def __receive_loop(self): """ Receive binary frames from WebSocket and dispatch decoded client messages. + Wire format: each WebSocket binary frame is one packet — + `[type:u8][payload]`. type=0x00 is a MsgPack-encoded Flet protocol + frame; type=0x01 is a raw DataChannel frame + (`[channel_id:u32 LE][bytes]`). + On disconnect/error, terminates send loop via queue sentinel when a session is active. """ @@ -222,9 +232,23 @@ async def __receive_loop(self): try: while True: data = await self.__websocket.receive_bytes() - await self.__on_message( - msgpack.unpackb(data, ext_hook=decode_ext_from_msgpack) - ) + if not data: + continue + ptype = data[0] + if ptype == 0x00: + await self.__on_message( + msgpack.unpackb(data[1:], ext_hook=decode_ext_from_msgpack) + ) + elif ptype == 0x01: + if len(data) < 5: + logger.debug("Dropping malformed data-channel frame.") + continue + channel_id = int.from_bytes(data[1:5], "little", signed=False) + channel = self._data_channels.get(channel_id) + if channel is not None: + channel._deliver(data[5:]) + else: + logger.debug("Dropping packet with unknown type 0x%02x", ptype) except Exception as e: if not isinstance(e, WebSocketDisconnect): logger.warning(f"Receive loop error: {e}", exc_info=True) @@ -383,16 +407,40 @@ def send_message(self, message: ClientMessage): """ Serialize and enqueue a server message for transport to the client. + Wire format: one packet per `send_bytes` call — + `[0x00][msgpack body]`. WebSocket preserves message boundaries so + no length prefix is needed. + Args: message: Outbound protocol message. """ transport_log.debug(f"send_message: {message}") - m = msgpack.packb( + body = msgpack.packb( [message.action, message.body], default=configure_encode_object_for_msgpack(BaseControl), ) - self.__send_queue.put_nowait(m) + self.__send_queue.put_nowait(b"\x00" + body) + + def send_data_channel_frame(self, channel_id: int, payload: bytes) -> None: + """Send a raw DataChannel frame `[0x01][channel_id:u32 LE][bytes]` + over the WebSocket. Called by `_ProtocolMuxedDataChannel.send`.""" + header = b"\x01" + channel_id.to_bytes(4, "little", signed=False) + self.__send_queue.put_nowait(header + payload) + + def data_channel_for(self, channel_id: int): + """Resolve or construct the muxed DataChannel for `channel_id`.""" + from flet.data_channel import _ProtocolMuxedDataChannel + + existing = self._data_channels.get(channel_id) + if existing is not None: + return existing + channel = _ProtocolMuxedDataChannel(channel_id, self) + self._data_channels[channel_id] = channel + return channel + + def unregister_data_channel(self, channel_id: int) -> None: + self._data_channels.pop(channel_id, None) def get_upload_url(self, file_name: str, expires: int) -> str: """ diff --git a/sdk/python/packages/flet-web/src/flet_web/patch_index.py b/sdk/python/packages/flet-web/src/flet_web/patch_index.py index 4d3dbe7282..0e206cd5a3 100644 --- a/sdk/python/packages/flet-web/src/flet_web/patch_index.py +++ b/sdk/python/packages/flet-web/src/flet_web/patch_index.py @@ -65,12 +65,19 @@ def patch_index_html( # Pin the Pyodide runtime URL for this build. The web client used to fall # back to a hardcoded CDN URL when not in no-cdn mode; with multi-version # support that constant is gone, so we always inject the URL here. + # + # `.mjs` is the ES-module variant; python-worker.js (a module worker) + # loads it via dynamic `import()`. We can no longer use `.js` because + # Pyodide >= 0.29 throws "Classic web workers are not supported" inside + # any worker that has `importScripts` available — only classic workers + # have it. All supported Pyodide versions (0.27.7 / 0.29.4 / 314.0.0) + # ship `pyodide.mjs`. if pyodide_version: if no_cdn: - pyodide_url = "pyodide/pyodide.js" + pyodide_url = "pyodide/pyodide.mjs" else: pyodide_url = ( - f"https://cdn.jsdelivr.net/pyodide/v{pyodide_version}/full/pyodide.js" + f"https://cdn.jsdelivr.net/pyodide/v{pyodide_version}/full/pyodide.mjs" ) app_config.append(f'flet.pyodideUrl="{pyodide_url}";') diff --git a/sdk/python/packages/flet/integration_tests/controls/cupertino/golden/macos/cupertino_date_picker/locale.png b/sdk/python/packages/flet/integration_tests/controls/cupertino/golden/macos/cupertino_date_picker/locale.png index 52c139f4a8..d7cc84a8d0 100644 Binary files a/sdk/python/packages/flet/integration_tests/controls/cupertino/golden/macos/cupertino_date_picker/locale.png and b/sdk/python/packages/flet/integration_tests/controls/cupertino/golden/macos/cupertino_date_picker/locale.png differ diff --git a/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/date_picker/locale.png b/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/date_picker/locale.png index 48c185d151..26ece416de 100644 Binary files a/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/date_picker/locale.png and b/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/date_picker/locale.png differ diff --git a/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/date_range_picker/locale.png b/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/date_range_picker/locale.png index ac6c92a984..303b513220 100644 Binary files a/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/date_range_picker/locale.png and b/sdk/python/packages/flet/integration_tests/controls/material/golden/macos/date_range_picker/locale.png differ diff --git a/sdk/python/packages/flet/integration_tests/examples/controls/material/golden/macos/date_picker/custom_locale.png b/sdk/python/packages/flet/integration_tests/examples/controls/material/golden/macos/date_picker/custom_locale.png index 60cc9f42c2..9707d04f92 100644 Binary files a/sdk/python/packages/flet/integration_tests/examples/controls/material/golden/macos/date_picker/custom_locale.png and b/sdk/python/packages/flet/integration_tests/examples/controls/material/golden/macos/date_picker/custom_locale.png differ diff --git a/sdk/python/packages/flet/integration_tests/examples/controls/material/golden/macos/date_range_picker/custom_locale.png b/sdk/python/packages/flet/integration_tests/examples/controls/material/golden/macos/date_range_picker/custom_locale.png index 98f4143da1..d70cca7124 100644 Binary files a/sdk/python/packages/flet/integration_tests/examples/controls/material/golden/macos/date_range_picker/custom_locale.png and b/sdk/python/packages/flet/integration_tests/examples/controls/material/golden/macos/date_range_picker/custom_locale.png differ diff --git a/sdk/python/packages/flet/src/flet/__init__.py b/sdk/python/packages/flet/src/flet/__init__.py index ee854e7b27..f8053fc911 100644 --- a/sdk/python/packages/flet/src/flet/__init__.py +++ b/sdk/python/packages/flet/src/flet/__init__.py @@ -1,576 +1,626 @@ from typing import TYPE_CHECKING -from flet.app import AppCallable, app, app_async, run, run_async -from flet.components.component import Component -from flet.components.component_decorator import component -from flet.components.hooks.use_callback import use_callback -from flet.components.hooks.use_context import ( - ContextProvider, - create_context, - use_context, -) -from flet.components.hooks.use_dialog import use_dialog -from flet.components.hooks.use_effect import ( - on_mounted, - on_unmounted, - on_updated, - use_effect, -) -from flet.components.hooks.use_memo import use_memo -from flet.components.hooks.use_ref import use_ref -from flet.components.hooks.use_state import use_state -from flet.components.memo import memo -from flet.components.observable import Observable, observable -from flet.components.public_utils import unwrap_component -from flet.components.router import ( - LocationInfo, - Route, - Router, - is_route_active, - use_route_loader_data, - use_route_location, - use_route_outlet, - use_route_params, - use_view_path, -) -from flet.controls import alignment, border, border_radius, margin, padding -from flet.controls.adaptive_control import AdaptiveControl -from flet.controls.alignment import Alignment, Axis -from flet.controls.animation import ( - Animation, - AnimationCurve, - AnimationStyle, - AnimationValue, -) -from flet.controls.base_control import BaseControl, Value, control, value -from flet.controls.base_page import BasePage, PageMediaData, PageResizeEvent -from flet.controls.blur import ( - Blur, - BlurTileMode, - BlurValue, -) -from flet.controls.border import ( - Border, - BorderSide, - BorderSideStrokeAlign, - BorderSideStrokeAlignValue, - BorderStyle, -) -from flet.controls.border_radius import ( - BorderRadius, - BorderRadiusValue, -) -from flet.controls.box import ( - BlurStyle, - BoxConstraints, - BoxDecoration, - BoxFit, - BoxShadow, - BoxShadowValue, - BoxShape, - ColorFilter, - DecorationImage, - FilterQuality, -) -from flet.controls.buttons import ( - BeveledRectangleBorder, - ButtonStyle, - CircleBorder, - ContinuousRectangleBorder, - OutlinedBorder, - RoundedRectangleBorder, - ShapeBorder, - StadiumBorder, -) -from flet.controls.colors import Colors -from flet.controls.context import Context, context -from flet.controls.control import Control -from flet.controls.control_event import ( - ControlEvent, - ControlEventHandler, - Event, - EventControlType, - EventHandler, -) -from flet.controls.control_state import ( - ControlState, - ControlStateValue, -) -from flet.controls.core.animated_switcher import ( - AnimatedSwitcher, - AnimatedSwitcherTransition, -) -from flet.controls.core.autofill_group import ( - AutofillGroup, - AutofillGroupDisposeAction, - AutofillHint, -) -from flet.controls.core.column import Column -from flet.controls.core.dismissible import ( - Dismissible, - DismissibleDismissEvent, - DismissibleUpdateEvent, -) -from flet.controls.core.drag_target import ( - DragTarget, - DragTargetEvent, - DragTargetLeaveEvent, - DragWillAcceptEvent, -) -from flet.controls.core.draggable import Draggable -from flet.controls.core.flet_app import FletApp, FletAppOutputEvent -from flet.controls.core.gesture_detector import GestureDetector -from flet.controls.core.grid_view import GridView -from flet.controls.core.hero import Hero -from flet.controls.core.icon import Icon -from flet.controls.core.image import Image -from flet.controls.core.interactive_viewer import InteractiveViewer -from flet.controls.core.keyboard_listener import ( - KeyboardListener, - KeyDownEvent, - KeyRepeatEvent, - KeyUpEvent, -) -from flet.controls.core.list_view import ListView -from flet.controls.core.markdown import ( - Markdown, - MarkdownCodeTheme, - MarkdownCustomCodeTheme, - MarkdownExtensionSet, - MarkdownStyleSheet, -) -from flet.controls.core.merge_semantics import MergeSemantics -from flet.controls.core.page_view import PageView -from flet.controls.core.pagelet import Pagelet -from flet.controls.core.placeholder import Placeholder -from flet.controls.core.reorderable_drag_handle import ReorderableDragHandle -from flet.controls.core.responsive_row import ResponsiveRow -from flet.controls.core.rotated_box import RotatedBox -from flet.controls.core.row import Row -from flet.controls.core.safe_area import SafeArea -from flet.controls.core.screenshot import Screenshot -from flet.controls.core.semantics import Semantics -from flet.controls.core.shader_mask import ShaderMask -from flet.controls.core.shimmer import Shimmer, ShimmerDirection -from flet.controls.core.stack import Stack, StackFit -from flet.controls.core.text import ( - Text, - TextAffinity, - TextSelection, - TextSelectionChangeCause, - TextSelectionChangeEvent, -) -from flet.controls.core.text_span import TextSpan -from flet.controls.core.transparent_pointer import TransparentPointer -from flet.controls.core.view import View -from flet.controls.core.window import ( - Window, - WindowEvent, - WindowEventType, - WindowResizeEdge, -) -from flet.controls.core.window_drag_area import WindowDragArea -from flet.controls.cupertino import cupertino_colors -from flet.controls.cupertino.cupertino_action_sheet import CupertinoActionSheet -from flet.controls.cupertino.cupertino_action_sheet_action import ( - CupertinoActionSheetAction, -) -from flet.controls.cupertino.cupertino_activity_indicator import ( - CupertinoActivityIndicator, -) -from flet.controls.cupertino.cupertino_alert_dialog import CupertinoAlertDialog -from flet.controls.cupertino.cupertino_app_bar import CupertinoAppBar -from flet.controls.cupertino.cupertino_bottom_sheet import CupertinoBottomSheet -from flet.controls.cupertino.cupertino_button import ( - CupertinoButton, - CupertinoButtonSize, -) -from flet.controls.cupertino.cupertino_checkbox import CupertinoCheckbox -from flet.controls.cupertino.cupertino_colors import CupertinoColors -from flet.controls.cupertino.cupertino_context_menu import CupertinoContextMenu -from flet.controls.cupertino.cupertino_context_menu_action import ( - CupertinoContextMenuAction, -) -from flet.controls.cupertino.cupertino_date_picker import ( - CupertinoDatePicker, - CupertinoDatePickerDateOrder, - CupertinoDatePickerMode, -) -from flet.controls.cupertino.cupertino_dialog_action import CupertinoDialogAction -from flet.controls.cupertino.cupertino_filled_button import CupertinoFilledButton -from flet.controls.cupertino.cupertino_list_tile import CupertinoListTile -from flet.controls.cupertino.cupertino_navigation_bar import CupertinoNavigationBar -from flet.controls.cupertino.cupertino_picker import CupertinoPicker -from flet.controls.cupertino.cupertino_radio import CupertinoRadio -from flet.controls.cupertino.cupertino_segmented_button import CupertinoSegmentedButton -from flet.controls.cupertino.cupertino_slider import CupertinoSlider -from flet.controls.cupertino.cupertino_sliding_segmented_button import ( - CupertinoSlidingSegmentedButton, -) -from flet.controls.cupertino.cupertino_switch import CupertinoSwitch -from flet.controls.cupertino.cupertino_textfield import ( - CupertinoTextField, - OverlayVisibilityMode, -) -from flet.controls.cupertino.cupertino_timer_picker import ( - CupertinoTimerPicker, - CupertinoTimerPickerMode, -) -from flet.controls.cupertino.cupertino_tinted_button import CupertinoTintedButton -from flet.controls.device_info import ( - AndroidBuildVersion, - AndroidDeviceInfo, - DeviceInfo, - IosDeviceInfo, - IosUtsname, - LinuxDeviceInfo, - MacOsDeviceInfo, - WebBrowserName, - WebDeviceInfo, - WindowsDeviceInfo, -) -from flet.controls.dialog_control import DialogControl -from flet.controls.duration import ( - DateTimeValue, - Duration, - DurationValue, -) -from flet.controls.events import ( - DragDownEvent, - DragEndEvent, - DragStartEvent, - DragUpdateEvent, - ForcePressEvent, - HoverEvent, - LongPressDownEvent, - LongPressEndEvent, - LongPressMoveUpdateEvent, - LongPressStartEvent, - MultiTapEvent, - PointerEvent, - ScaleEndEvent, - ScaleStartEvent, - ScaleUpdateEvent, - ScrollEvent, - TapEvent, - TapMoveEvent, -) -from flet.controls.exceptions import ( - FletException, - FletPageDisconnectedException, - FletUnimplementedPlatformException, - FletUnsupportedPlatformException, -) -from flet.controls.geometry import Rect, Size -from flet.controls.gradients import ( - Gradient, - GradientTileMode, - LinearGradient, - RadialGradient, - SweepGradient, -) -from flet.controls.icon_data import IconData -from flet.controls.id_counter import IdCounter -from flet.controls.keys import Key, KeyValue, ScrollKey, ValueKey -from flet.controls.layout_control import ( - ConstrainedControl, - LayoutControl, - LayoutSizeChangeEvent, -) -from flet.controls.margin import Margin, MarginValue -from flet.controls.material import dropdown, dropdownm2 -from flet.controls.material.alert_dialog import AlertDialog -from flet.controls.material.app_bar import AppBar -from flet.controls.material.auto_complete import ( - AutoComplete, - AutoCompleteSelectEvent, - AutoCompleteSuggestion, -) -from flet.controls.material.badge import Badge, BadgeValue -from flet.controls.material.banner import Banner -from flet.controls.material.bottom_app_bar import BottomAppBar -from flet.controls.material.bottom_sheet import BottomSheet -from flet.controls.material.button import Button -from flet.controls.material.card import Card, CardVariant -from flet.controls.material.checkbox import Checkbox -from flet.controls.material.chip import Chip -from flet.controls.material.circle_avatar import CircleAvatar -from flet.controls.material.container import Container -from flet.controls.material.context_menu import ( - ContextMenu, - ContextMenuDismissEvent, - ContextMenuSelectEvent, - ContextMenuTrigger, -) -from flet.controls.material.datatable import ( - DataCell, - DataColumn, - DataColumnSortEvent, - DataRow, - DataTable, -) -from flet.controls.material.date_picker import ( - DatePicker, - DatePickerEntryMode, - DatePickerEntryModeChangeEvent, - DatePickerMode, -) -from flet.controls.material.date_range_picker import DateRangePicker -from flet.controls.material.divider import Divider -from flet.controls.material.dropdown import Dropdown, DropdownOption -from flet.controls.material.dropdownm2 import DropdownM2 -from flet.controls.material.elevated_button import ElevatedButton -from flet.controls.material.expansion_panel import ( - ExpansionPanel, - ExpansionPanelList, - ExpansionPanelListChangeEvent, -) -from flet.controls.material.expansion_tile import ExpansionTile, TileAffinity -from flet.controls.material.filled_button import FilledButton -from flet.controls.material.filled_tonal_button import FilledTonalButton -from flet.controls.material.floating_action_button import FloatingActionButton -from flet.controls.material.form_field_control import FormFieldControl, InputBorder -from flet.controls.material.icon_button import ( - FilledIconButton, - FilledTonalIconButton, - IconButton, - OutlinedIconButton, -) -from flet.controls.material.list_tile import ( - ListTile, - ListTileStyle, - ListTileTitleAlignment, -) -from flet.controls.material.menu_bar import MenuBar, MenuStyle -from flet.controls.material.menu_item_button import MenuItemButton -from flet.controls.material.navigation_bar import ( - NavigationBar, - NavigationBarDestination, - NavigationBarLabelBehavior, -) -from flet.controls.material.navigation_drawer import ( - NavigationDrawer, - NavigationDrawerDestination, -) -from flet.controls.material.navigation_rail import ( - NavigationRail, - NavigationRailDestination, - NavigationRailLabelType, -) -from flet.controls.material.outlined_button import OutlinedButton -from flet.controls.material.popup_menu_button import ( - PopupMenuButton, - PopupMenuItem, - PopupMenuPosition, -) -from flet.controls.material.progress_bar import ProgressBar -from flet.controls.material.progress_ring import ProgressRing -from flet.controls.material.radio import Radio -from flet.controls.material.radio_group import RadioGroup -from flet.controls.material.range_slider import RangeSlider -from flet.controls.material.reorderable_list_view import ( - OnReorderEvent, - ReorderableListView, -) -from flet.controls.material.search_bar import SearchBar -from flet.controls.material.segmented_button import Segment, SegmentedButton -from flet.controls.material.selection_area import SelectionArea -from flet.controls.material.slider import Slider, SliderInteraction -from flet.controls.material.snack_bar import ( - DismissDirection, - SnackBar, - SnackBarAction, - SnackBarBehavior, -) -from flet.controls.material.submenu_button import SubmenuButton -from flet.controls.material.switch import Switch -from flet.controls.material.tabs import ( - Tab, - TabAlignment, - TabBar, - TabBarHoverEvent, - TabBarIndicatorSize, - TabBarView, - TabIndicatorAnimation, - Tabs, - UnderlineTabIndicator, -) -from flet.controls.material.text_button import TextButton -from flet.controls.material.textfield import ( - InputFilter, - KeyboardType, - NumbersOnlyInputFilter, - TextCapitalization, - TextField, - TextOnlyInputFilter, -) -from flet.controls.material.time_picker import ( - TimePicker, - TimePickerEntryMode, - TimePickerEntryModeChangeEvent, - TimePickerHourFormat, -) -from flet.controls.material.tooltip import Tooltip, TooltipTriggerMode, TooltipValue -from flet.controls.material.vertical_divider import VerticalDivider -from flet.controls.multi_view import MultiView -from flet.controls.padding import Padding, PaddingValue -from flet.controls.page import ( - AppLifecycleStateChangeEvent, - KeyboardEvent, - LocaleChangeEvent, - LoginEvent, - MultiViewAddEvent, - MultiViewRemoveEvent, - Page, - PlatformBrightnessChangeEvent, - RouteChangeEvent, - ViewPopEvent, - ViewsPopUntilEvent, -) -from flet.controls.painting import ( - Paint, - PaintGradient, - PaintingStyle, - PaintLinearGradient, - PaintRadialGradient, - PaintSweepGradient, -) -from flet.controls.query_string import QueryString -from flet.controls.ref import Ref -from flet.controls.scrollable_control import ( - OnScrollEvent, - ScrollableControl, - Scrollbar, - ScrollbarOrientation, - ScrollDirection, - ScrollType, -) -from flet.controls.services.accelerometer import ( - Accelerometer, - AccelerometerReadingEvent, -) -from flet.controls.services.barometer import Barometer, BarometerReadingEvent -from flet.controls.services.battery import ( - Battery, - BatteryState, - BatteryStateChangeEvent, -) -from flet.controls.services.browser_context_menu import BrowserContextMenu -from flet.controls.services.clipboard import Clipboard -from flet.controls.services.connectivity import ( - Connectivity, - ConnectivityChangeEvent, - ConnectivityType, -) -from flet.controls.services.file_picker import ( - FilePicker, - FilePickerFile, - FilePickerFileType, - FilePickerUploadEvent, - FilePickerUploadFile, -) -from flet.controls.services.gyroscope import Gyroscope, GyroscopeReadingEvent -from flet.controls.services.haptic_feedback import HapticFeedback -from flet.controls.services.magnetometer import Magnetometer, MagnetometerReadingEvent -from flet.controls.services.screen_brightness import ( - ScreenBrightness, - ScreenBrightnessChangeEvent, -) -from flet.controls.services.semantics_service import Assertiveness, SemanticsService -from flet.controls.services.sensor_error_event import SensorErrorEvent -from flet.controls.services.service import Service -from flet.controls.services.shake_detector import ShakeDetector -from flet.controls.services.share import ( - Share, - ShareCupertinoActivityType, - ShareFile, - ShareResult, - ShareResultStatus, -) -from flet.controls.services.shared_preferences import SharedPreferences -from flet.controls.services.storage_paths import StoragePaths -from flet.controls.services.url_launcher import ( - BrowserConfiguration, - LaunchMode, - UrlLauncher, - WebViewConfiguration, -) -from flet.controls.services.user_accelerometer import ( - UserAccelerometer, - UserAccelerometerReadingEvent, -) -from flet.controls.services.wakelock import Wakelock -from flet.controls.template_route import TemplateRoute -from flet.controls.text_style import ( - StrutStyle, - TextBaseline, - TextDecoration, - TextDecorationStyle, - TextOverflow, - TextStyle, - TextThemeStyle, -) -from flet.controls.transform import ( - Flip, - Matrix4, - Offset, - OffsetValue, - Rotate, - RotateValue, - Scale, - ScaleValue, - Transform, -) -from flet.controls.types import ( - AppLifecycleState, - AppView, - AutomaticNotchShape, - BlendMode, - Brightness, - CircularRectangleNotchShape, - ClipBehavior, - ColorValue, - CrossAxisAlignment, - DeviceOrientation, - FloatingActionButtonLocation, - FontWeight, - IconDataOrControl, - ImageRepeat, - LabelPosition, - Locale, - LocaleConfiguration, - MainAxisAlignment, - MouseCursor, - NotchShape, - Number, - Orientation, - PagePlatform, - PointerDeviceType, - ResponsiveNumber, - ResponsiveRowBreakpoint, - RouteUrlStrategy, - ScrollMode, - StrokeCap, - StrokeJoin, - StrOrControl, - SupportsStr, - TextAlign, - ThemeMode, - Url, - UrlTarget, - VerticalAlignment, - VisualDensity, - WebRenderer, -) -from flet.pubsub.pubsub_client import PubSubClient -from flet.pubsub.pubsub_hub import PubSubHub +# `__version__` is cheap and frequently accessed — keep it eager. from flet.version import flet_version as __version__ +# The public API is imported lazily (PEP 562). `import flet` no longer executes +# the full ~270-module package; each name is imported on first access and cached +# into module globals. The block below exists only for type checkers / IDEs. if TYPE_CHECKING: - from flet.controls.cupertino import cupertino_icons + from flet.app import ( + AppCallable, + app, + app_async, + run, + run_async, + ) + from flet.components.component import Component + from flet.components.component_decorator import component + from flet.components.hooks.use_callback import use_callback + from flet.components.hooks.use_context import ( + ContextProvider, + create_context, + use_context, + ) + from flet.components.hooks.use_dialog import use_dialog + from flet.components.hooks.use_effect import ( + on_mounted, + on_unmounted, + on_updated, + use_effect, + ) + from flet.components.hooks.use_memo import use_memo + from flet.components.hooks.use_ref import use_ref + from flet.components.hooks.use_state import use_state + from flet.components.memo import memo + from flet.components.observable import ( + Observable, + observable, + ) + from flet.components.public_utils import unwrap_component + from flet.components.router import ( + LocationInfo, + Route, + Router, + is_route_active, + use_route_loader_data, + use_route_location, + use_route_outlet, + use_route_params, + use_view_path, + ) + from flet.controls import ( + alignment, + border, + border_radius, + margin, + padding, + ) + from flet.controls.adaptive_control import AdaptiveControl + from flet.controls.alignment import ( + Alignment, + Axis, + ) + from flet.controls.animation import ( + Animation, + AnimationCurve, + AnimationStyle, + AnimationValue, + ) + from flet.controls.base_control import ( + BaseControl, + Value, + control, + value, + ) + from flet.controls.base_page import ( + BasePage, + PageMediaData, + PageResizeEvent, + ) + from flet.controls.blur import ( + Blur, + BlurTileMode, + BlurValue, + ) + from flet.controls.border import ( + Border, + BorderSide, + BorderSideStrokeAlign, + BorderSideStrokeAlignValue, + BorderStyle, + ) + from flet.controls.border_radius import ( + BorderRadius, + BorderRadiusValue, + ) + from flet.controls.box import ( + BlurStyle, + BoxConstraints, + BoxDecoration, + BoxFit, + BoxShadow, + BoxShadowValue, + BoxShape, + ColorFilter, + DecorationImage, + FilterQuality, + ) + from flet.controls.buttons import ( + BeveledRectangleBorder, + ButtonStyle, + CircleBorder, + ContinuousRectangleBorder, + OutlinedBorder, + RoundedRectangleBorder, + ShapeBorder, + StadiumBorder, + ) + from flet.controls.colors import Colors + from flet.controls.context import ( + Context, + context, + ) + from flet.controls.control import Control + from flet.controls.control_event import ( + ControlEvent, + ControlEventHandler, + Event, + EventControlType, + EventHandler, + ) + from flet.controls.control_state import ( + ControlState, + ControlStateValue, + ) + from flet.controls.core.animated_switcher import ( + AnimatedSwitcher, + AnimatedSwitcherTransition, + ) + from flet.controls.core.autofill_group import ( + AutofillGroup, + AutofillGroupDisposeAction, + AutofillHint, + ) + from flet.controls.core.column import Column + from flet.controls.core.dismissible import ( + Dismissible, + DismissibleDismissEvent, + DismissibleUpdateEvent, + ) + from flet.controls.core.drag_target import ( + DragTarget, + DragTargetEvent, + DragTargetLeaveEvent, + DragWillAcceptEvent, + ) + from flet.controls.core.draggable import Draggable + from flet.controls.core.flet_app import ( + FletApp, + FletAppOutputEvent, + ) + from flet.controls.core.gesture_detector import GestureDetector + from flet.controls.core.grid_view import GridView + from flet.controls.core.hero import Hero + from flet.controls.core.icon import Icon + from flet.controls.core.image import Image + from flet.controls.core.interactive_viewer import InteractiveViewer + from flet.controls.core.keyboard_listener import ( + KeyboardListener, + KeyDownEvent, + KeyRepeatEvent, + KeyUpEvent, + ) + from flet.controls.core.list_view import ListView + from flet.controls.core.markdown import ( + Markdown, + MarkdownCodeTheme, + MarkdownCustomCodeTheme, + MarkdownExtensionSet, + MarkdownStyleSheet, + ) + from flet.controls.core.merge_semantics import MergeSemantics + from flet.controls.core.page_view import PageView + from flet.controls.core.pagelet import Pagelet + from flet.controls.core.placeholder import Placeholder + from flet.controls.core.reorderable_drag_handle import ReorderableDragHandle + from flet.controls.core.responsive_row import ResponsiveRow + from flet.controls.core.rotated_box import RotatedBox + from flet.controls.core.row import Row + from flet.controls.core.safe_area import SafeArea + from flet.controls.core.screenshot import Screenshot + from flet.controls.core.semantics import Semantics + from flet.controls.core.shader_mask import ShaderMask + from flet.controls.core.shimmer import ( + Shimmer, + ShimmerDirection, + ) + from flet.controls.core.stack import ( + Stack, + StackFit, + ) + from flet.controls.core.text import ( + Text, + TextAffinity, + TextSelection, + TextSelectionChangeCause, + TextSelectionChangeEvent, + ) + from flet.controls.core.text_span import TextSpan + from flet.controls.core.transparent_pointer import TransparentPointer + from flet.controls.core.view import View + from flet.controls.core.window import ( + Window, + WindowEvent, + WindowEventType, + WindowResizeEdge, + ) + from flet.controls.core.window_drag_area import WindowDragArea + from flet.controls.cupertino import ( + cupertino_colors, + cupertino_icons, + ) + from flet.controls.cupertino.cupertino_action_sheet import CupertinoActionSheet + from flet.controls.cupertino.cupertino_action_sheet_action import ( + CupertinoActionSheetAction, + ) + from flet.controls.cupertino.cupertino_activity_indicator import ( + CupertinoActivityIndicator, + ) + from flet.controls.cupertino.cupertino_alert_dialog import CupertinoAlertDialog + from flet.controls.cupertino.cupertino_app_bar import CupertinoAppBar + from flet.controls.cupertino.cupertino_bottom_sheet import CupertinoBottomSheet + from flet.controls.cupertino.cupertino_button import ( + CupertinoButton, + CupertinoButtonSize, + ) + from flet.controls.cupertino.cupertino_checkbox import CupertinoCheckbox + from flet.controls.cupertino.cupertino_colors import CupertinoColors + from flet.controls.cupertino.cupertino_context_menu import CupertinoContextMenu + from flet.controls.cupertino.cupertino_context_menu_action import ( + CupertinoContextMenuAction, + ) + from flet.controls.cupertino.cupertino_date_picker import ( + CupertinoDatePicker, + CupertinoDatePickerDateOrder, + CupertinoDatePickerMode, + ) + from flet.controls.cupertino.cupertino_dialog_action import CupertinoDialogAction + from flet.controls.cupertino.cupertino_filled_button import CupertinoFilledButton from flet.controls.cupertino.cupertino_icons import CupertinoIcons - from flet.controls.material import icons + from flet.controls.cupertino.cupertino_list_tile import CupertinoListTile + from flet.controls.cupertino.cupertino_navigation_bar import CupertinoNavigationBar + from flet.controls.cupertino.cupertino_picker import CupertinoPicker + from flet.controls.cupertino.cupertino_radio import CupertinoRadio + from flet.controls.cupertino.cupertino_segmented_button import ( + CupertinoSegmentedButton, + ) + from flet.controls.cupertino.cupertino_slider import CupertinoSlider + from flet.controls.cupertino.cupertino_sliding_segmented_button import ( + CupertinoSlidingSegmentedButton, + ) + from flet.controls.cupertino.cupertino_switch import CupertinoSwitch + from flet.controls.cupertino.cupertino_textfield import ( + CupertinoTextField, + OverlayVisibilityMode, + ) + from flet.controls.cupertino.cupertino_timer_picker import ( + CupertinoTimerPicker, + CupertinoTimerPickerMode, + ) + from flet.controls.cupertino.cupertino_tinted_button import CupertinoTintedButton + from flet.controls.device_info import ( + AndroidBuildVersion, + AndroidDeviceInfo, + DeviceInfo, + IosDeviceInfo, + IosUtsname, + LinuxDeviceInfo, + MacOsDeviceInfo, + WebBrowserName, + WebDeviceInfo, + WindowsDeviceInfo, + ) + from flet.controls.dialog_control import DialogControl + from flet.controls.duration import ( + DateTimeValue, + Duration, + DurationValue, + ) + from flet.controls.events import ( + DragDownEvent, + DragEndEvent, + DragStartEvent, + DragUpdateEvent, + ForcePressEvent, + HoverEvent, + LongPressDownEvent, + LongPressEndEvent, + LongPressMoveUpdateEvent, + LongPressStartEvent, + MultiTapEvent, + PointerEvent, + ScaleEndEvent, + ScaleStartEvent, + ScaleUpdateEvent, + ScrollEvent, + TapEvent, + TapMoveEvent, + ) + from flet.controls.exceptions import ( + FletException, + FletPageDisconnectedException, + FletUnimplementedPlatformException, + FletUnsupportedPlatformException, + ) + from flet.controls.geometry import ( + Rect, + Size, + ) + from flet.controls.gradients import ( + Gradient, + GradientTileMode, + LinearGradient, + RadialGradient, + SweepGradient, + ) + from flet.controls.icon_data import IconData + from flet.controls.id_counter import IdCounter + from flet.controls.keys import ( + Key, + KeyValue, + ScrollKey, + ValueKey, + ) + from flet.controls.layout_control import ( + ConstrainedControl, + LayoutControl, + LayoutSizeChangeEvent, + ) + from flet.controls.margin import ( + Margin, + MarginValue, + ) + from flet.controls.material import ( + dropdown, + dropdownm2, + icons, + ) + from flet.controls.material.alert_dialog import AlertDialog + from flet.controls.material.app_bar import AppBar + from flet.controls.material.auto_complete import ( + AutoComplete, + AutoCompleteSelectEvent, + AutoCompleteSuggestion, + ) + from flet.controls.material.badge import ( + Badge, + BadgeValue, + ) + from flet.controls.material.banner import Banner + from flet.controls.material.bottom_app_bar import BottomAppBar + from flet.controls.material.bottom_sheet import BottomSheet + from flet.controls.material.button import Button + from flet.controls.material.card import ( + Card, + CardVariant, + ) + from flet.controls.material.checkbox import Checkbox + from flet.controls.material.chip import Chip + from flet.controls.material.circle_avatar import CircleAvatar + from flet.controls.material.container import Container + from flet.controls.material.context_menu import ( + ContextMenu, + ContextMenuDismissEvent, + ContextMenuSelectEvent, + ContextMenuTrigger, + ) + from flet.controls.material.datatable import ( + DataCell, + DataColumn, + DataColumnSortEvent, + DataRow, + DataTable, + ) + from flet.controls.material.date_picker import ( + DatePicker, + DatePickerEntryMode, + DatePickerEntryModeChangeEvent, + DatePickerMode, + ) + from flet.controls.material.date_range_picker import DateRangePicker + from flet.controls.material.divider import Divider + from flet.controls.material.dropdown import ( + Dropdown, + DropdownOption, + ) + from flet.controls.material.dropdownm2 import DropdownM2 + from flet.controls.material.elevated_button import ElevatedButton + from flet.controls.material.expansion_panel import ( + ExpansionPanel, + ExpansionPanelList, + ExpansionPanelListChangeEvent, + ) + from flet.controls.material.expansion_tile import ( + ExpansionTile, + TileAffinity, + ) + from flet.controls.material.filled_button import FilledButton + from flet.controls.material.filled_tonal_button import FilledTonalButton + from flet.controls.material.floating_action_button import FloatingActionButton + from flet.controls.material.form_field_control import ( + FormFieldControl, + InputBorder, + ) + from flet.controls.material.icon_button import ( + FilledIconButton, + FilledTonalIconButton, + IconButton, + OutlinedIconButton, + ) from flet.controls.material.icons import Icons + from flet.controls.material.list_tile import ( + ListTile, + ListTileStyle, + ListTileTitleAlignment, + ) + from flet.controls.material.menu_bar import ( + MenuBar, + MenuStyle, + ) + from flet.controls.material.menu_item_button import MenuItemButton + from flet.controls.material.navigation_bar import ( + NavigationBar, + NavigationBarDestination, + NavigationBarLabelBehavior, + ) + from flet.controls.material.navigation_drawer import ( + NavigationDrawer, + NavigationDrawerDestination, + ) + from flet.controls.material.navigation_rail import ( + NavigationRail, + NavigationRailDestination, + NavigationRailLabelType, + ) + from flet.controls.material.outlined_button import OutlinedButton + from flet.controls.material.popup_menu_button import ( + PopupMenuButton, + PopupMenuItem, + PopupMenuPosition, + ) + from flet.controls.material.progress_bar import ProgressBar + from flet.controls.material.progress_ring import ProgressRing + from flet.controls.material.radio import Radio + from flet.controls.material.radio_group import RadioGroup + from flet.controls.material.range_slider import RangeSlider + from flet.controls.material.reorderable_list_view import ( + OnReorderEvent, + ReorderableListView, + ) + from flet.controls.material.search_bar import SearchBar + from flet.controls.material.segmented_button import ( + Segment, + SegmentedButton, + ) + from flet.controls.material.selection_area import SelectionArea + from flet.controls.material.slider import ( + Slider, + SliderInteraction, + ) + from flet.controls.material.snack_bar import ( + DismissDirection, + SnackBar, + SnackBarAction, + SnackBarBehavior, + ) + from flet.controls.material.submenu_button import SubmenuButton + from flet.controls.material.switch import Switch + from flet.controls.material.tabs import ( + Tab, + TabAlignment, + TabBar, + TabBarHoverEvent, + TabBarIndicatorSize, + TabBarView, + TabIndicatorAnimation, + Tabs, + UnderlineTabIndicator, + ) + from flet.controls.material.text_button import TextButton + from flet.controls.material.textfield import ( + InputFilter, + KeyboardType, + NumbersOnlyInputFilter, + TextCapitalization, + TextField, + TextOnlyInputFilter, + ) + from flet.controls.material.time_picker import ( + TimePicker, + TimePickerEntryMode, + TimePickerEntryModeChangeEvent, + TimePickerHourFormat, + ) + from flet.controls.material.tooltip import ( + Tooltip, + TooltipTriggerMode, + TooltipValue, + ) + from flet.controls.material.vertical_divider import VerticalDivider + from flet.controls.multi_view import MultiView + from flet.controls.padding import ( + Padding, + PaddingValue, + ) + from flet.controls.page import ( + AppLifecycleStateChangeEvent, + KeyboardEvent, + LocaleChangeEvent, + LoginEvent, + MultiViewAddEvent, + MultiViewRemoveEvent, + Page, + PlatformBrightnessChangeEvent, + RouteChangeEvent, + ViewPopEvent, + ViewsPopUntilEvent, + ) + from flet.controls.painting import ( + Paint, + PaintGradient, + PaintingStyle, + PaintLinearGradient, + PaintRadialGradient, + PaintSweepGradient, + ) + from flet.controls.query_string import QueryString + from flet.controls.ref import Ref + from flet.controls.scrollable_control import ( + OnScrollEvent, + ScrollableControl, + Scrollbar, + ScrollbarOrientation, + ScrollDirection, + ScrollType, + ) + from flet.controls.services.accelerometer import ( + Accelerometer, + AccelerometerReadingEvent, + ) + from flet.controls.services.barometer import ( + Barometer, + BarometerReadingEvent, + ) + from flet.controls.services.battery import ( + Battery, + BatteryState, + BatteryStateChangeEvent, + ) + from flet.controls.services.browser_context_menu import BrowserContextMenu + from flet.controls.services.clipboard import Clipboard + from flet.controls.services.connectivity import ( + Connectivity, + ConnectivityChangeEvent, + ConnectivityType, + ) + from flet.controls.services.file_picker import ( + FilePicker, + FilePickerFile, + FilePickerFileType, + FilePickerUploadEvent, + FilePickerUploadFile, + ) + from flet.controls.services.gyroscope import ( + Gyroscope, + GyroscopeReadingEvent, + ) + from flet.controls.services.haptic_feedback import HapticFeedback + from flet.controls.services.magnetometer import ( + Magnetometer, + MagnetometerReadingEvent, + ) + from flet.controls.services.screen_brightness import ( + ScreenBrightness, + ScreenBrightnessChangeEvent, + ) + from flet.controls.services.semantics_service import ( + Assertiveness, + SemanticsService, + ) + from flet.controls.services.sensor_error_event import SensorErrorEvent + from flet.controls.services.service import Service + from flet.controls.services.shake_detector import ShakeDetector + from flet.controls.services.share import ( + Share, + ShareCupertinoActivityType, + ShareFile, + ShareResult, + ShareResultStatus, + ) + from flet.controls.services.shared_preferences import SharedPreferences + from flet.controls.services.storage_paths import StoragePaths + from flet.controls.services.url_launcher import ( + BrowserConfiguration, + LaunchMode, + UrlLauncher, + WebViewConfiguration, + ) + from flet.controls.services.user_accelerometer import ( + UserAccelerometer, + UserAccelerometerReadingEvent, + ) + from flet.controls.services.wakelock import Wakelock + from flet.controls.template_route import TemplateRoute + from flet.controls.text_style import ( + StrutStyle, + TextBaseline, + TextDecoration, + TextDecorationStyle, + TextOverflow, + TextStyle, + TextThemeStyle, + ) from flet.controls.theme import ( AppBarTheme, BadgeTheme, @@ -617,6 +667,65 @@ TimePickerTheme, TooltipTheme, ) + from flet.controls.transform import ( + Flip, + Matrix4, + Offset, + OffsetValue, + Rotate, + RotateValue, + Scale, + ScaleValue, + Transform, + ) + from flet.controls.types import ( + AppLifecycleState, + AppView, + AutomaticNotchShape, + BlendMode, + Brightness, + CircularRectangleNotchShape, + ClipBehavior, + ColorValue, + CrossAxisAlignment, + DeviceOrientation, + FloatingActionButtonLocation, + FontWeight, + IconDataOrControl, + ImageRepeat, + LabelPosition, + Locale, + LocaleConfiguration, + MainAxisAlignment, + MouseCursor, + NotchShape, + Number, + Orientation, + PagePlatform, + PointerDeviceType, + ResponsiveNumber, + ResponsiveRowBreakpoint, + RouteUrlStrategy, + ScrollMode, + StrokeCap, + StrokeJoin, + StrOrControl, + SupportsStr, + TextAlign, + ThemeMode, + Url, + UrlTarget, + VerticalAlignment, + VisualDensity, + WebRenderer, + ) + from flet.data_channel import ( + DataChannel, + DataChannelOpenEvent, + ) + from flet.pubsub.pubsub_client import PubSubClient + from flet.pubsub.pubsub_hub import PubSubHub + __all__ = [ "Accelerometer", @@ -755,6 +864,8 @@ "CupertinoTimerPickerMode", "CupertinoTintedButton", "DataCell", + "DataChannel", + "DataChannelOpenEvent", "DataColumn", "DataColumnSortEvent", "DataRow", @@ -1148,73 +1259,554 @@ "value", ] -_THEME_EXPORTS = { - "AppBarTheme", - "BadgeTheme", - "BannerTheme", - "BottomAppBarTheme", - "BottomSheetTheme", - "ButtonTheme", - "CardTheme", - "CheckboxTheme", - "ChipTheme", - "ColorScheme", - "DataTableTheme", - "DatePickerTheme", - "DialogTheme", - "DividerTheme", - "DropdownTheme", - "ExpansionTileTheme", - "FilledButtonTheme", - "FloatingActionButtonTheme", - "IconButtonTheme", - "IconTheme", - "ListTileTheme", - "NavigationBarTheme", - "NavigationDrawerTheme", - "NavigationRailTheme", - "OutlinedButtonTheme", - "PageTransitionsTheme", - "PageTransitionTheme", - "PopupMenuTheme", - "ProgressIndicatorTheme", - "RadioTheme", - "ScrollbarTheme", - "SearchBarTheme", - "SearchViewTheme", - "SegmentedButtonTheme", - "SliderTheme", - "SnackBarTheme", - "SwitchTheme", - "SystemOverlayStyle", - "TabBarTheme", - "TextButtonTheme", - "TextTheme", - "Theme", - "TimePickerTheme", - "TooltipTheme", +# Generated: exported name -> module that defines it. +_LAZY = { + "Accelerometer": "flet.controls.services.accelerometer", + "AccelerometerReadingEvent": "flet.controls.services.accelerometer", + "AdaptiveControl": "flet.controls.adaptive_control", + "AlertDialog": "flet.controls.material.alert_dialog", + "Alignment": "flet.controls.alignment", + "AndroidBuildVersion": "flet.controls.device_info", + "AndroidDeviceInfo": "flet.controls.device_info", + "AnimatedSwitcher": "flet.controls.core.animated_switcher", + "AnimatedSwitcherTransition": "flet.controls.core.animated_switcher", + "Animation": "flet.controls.animation", + "AnimationCurve": "flet.controls.animation", + "AnimationStyle": "flet.controls.animation", + "AnimationValue": "flet.controls.animation", + "AppBar": "flet.controls.material.app_bar", + "AppBarTheme": "flet.controls.theme", + "AppCallable": "flet.app", + "AppLifecycleState": "flet.controls.types", + "AppLifecycleStateChangeEvent": "flet.controls.page", + "AppView": "flet.controls.types", + "Assertiveness": "flet.controls.services.semantics_service", + "AutoComplete": "flet.controls.material.auto_complete", + "AutoCompleteSelectEvent": "flet.controls.material.auto_complete", + "AutoCompleteSuggestion": "flet.controls.material.auto_complete", + "AutofillGroup": "flet.controls.core.autofill_group", + "AutofillGroupDisposeAction": "flet.controls.core.autofill_group", + "AutofillHint": "flet.controls.core.autofill_group", + "AutomaticNotchShape": "flet.controls.types", + "Axis": "flet.controls.alignment", + "Badge": "flet.controls.material.badge", + "BadgeTheme": "flet.controls.theme", + "BadgeValue": "flet.controls.material.badge", + "Banner": "flet.controls.material.banner", + "BannerTheme": "flet.controls.theme", + "Barometer": "flet.controls.services.barometer", + "BarometerReadingEvent": "flet.controls.services.barometer", + "BaseControl": "flet.controls.base_control", + "BasePage": "flet.controls.base_page", + "Battery": "flet.controls.services.battery", + "BatteryState": "flet.controls.services.battery", + "BatteryStateChangeEvent": "flet.controls.services.battery", + "BeveledRectangleBorder": "flet.controls.buttons", + "BlendMode": "flet.controls.types", + "Blur": "flet.controls.blur", + "BlurStyle": "flet.controls.box", + "BlurTileMode": "flet.controls.blur", + "BlurValue": "flet.controls.blur", + "Border": "flet.controls.border", + "BorderRadius": "flet.controls.border_radius", + "BorderRadiusValue": "flet.controls.border_radius", + "BorderSide": "flet.controls.border", + "BorderSideStrokeAlign": "flet.controls.border", + "BorderSideStrokeAlignValue": "flet.controls.border", + "BorderStyle": "flet.controls.border", + "BottomAppBar": "flet.controls.material.bottom_app_bar", + "BottomAppBarTheme": "flet.controls.theme", + "BottomSheet": "flet.controls.material.bottom_sheet", + "BottomSheetTheme": "flet.controls.theme", + "BoxConstraints": "flet.controls.box", + "BoxDecoration": "flet.controls.box", + "BoxFit": "flet.controls.box", + "BoxShadow": "flet.controls.box", + "BoxShadowValue": "flet.controls.box", + "BoxShape": "flet.controls.box", + "Brightness": "flet.controls.types", + "BrowserConfiguration": "flet.controls.services.url_launcher", + "BrowserContextMenu": "flet.controls.services.browser_context_menu", + "Button": "flet.controls.material.button", + "ButtonStyle": "flet.controls.buttons", + "ButtonTheme": "flet.controls.theme", + "Card": "flet.controls.material.card", + "CardTheme": "flet.controls.theme", + "CardVariant": "flet.controls.material.card", + "Checkbox": "flet.controls.material.checkbox", + "CheckboxTheme": "flet.controls.theme", + "Chip": "flet.controls.material.chip", + "ChipTheme": "flet.controls.theme", + "CircleAvatar": "flet.controls.material.circle_avatar", + "CircleBorder": "flet.controls.buttons", + "CircularRectangleNotchShape": "flet.controls.types", + "ClipBehavior": "flet.controls.types", + "Clipboard": "flet.controls.services.clipboard", + "ColorFilter": "flet.controls.box", + "ColorScheme": "flet.controls.theme", + "ColorValue": "flet.controls.types", + "Colors": "flet.controls.colors", + "Column": "flet.controls.core.column", + "Component": "flet.components.component", + "Connectivity": "flet.controls.services.connectivity", + "ConnectivityChangeEvent": "flet.controls.services.connectivity", + "ConnectivityType": "flet.controls.services.connectivity", + "ConstrainedControl": "flet.controls.layout_control", + "Container": "flet.controls.material.container", + "Context": "flet.controls.context", + "ContextMenu": "flet.controls.material.context_menu", + "ContextMenuDismissEvent": "flet.controls.material.context_menu", + "ContextMenuSelectEvent": "flet.controls.material.context_menu", + "ContextMenuTrigger": "flet.controls.material.context_menu", + "ContextProvider": "flet.components.hooks.use_context", + "ContinuousRectangleBorder": "flet.controls.buttons", + "Control": "flet.controls.control", + "ControlEvent": "flet.controls.control_event", + "ControlEventHandler": "flet.controls.control_event", + "ControlState": "flet.controls.control_state", + "ControlStateValue": "flet.controls.control_state", + "CrossAxisAlignment": "flet.controls.types", + "CupertinoActionSheet": "flet.controls.cupertino.cupertino_action_sheet", + "CupertinoActionSheetAction": "flet.controls.cupertino.cupertino_action_sheet_action", # noqa: E501 + "CupertinoActivityIndicator": "flet.controls.cupertino.cupertino_activity_indicator", # noqa: E501 + "CupertinoAlertDialog": "flet.controls.cupertino.cupertino_alert_dialog", + "CupertinoAppBar": "flet.controls.cupertino.cupertino_app_bar", + "CupertinoBottomSheet": "flet.controls.cupertino.cupertino_bottom_sheet", + "CupertinoButton": "flet.controls.cupertino.cupertino_button", + "CupertinoButtonSize": "flet.controls.cupertino.cupertino_button", + "CupertinoCheckbox": "flet.controls.cupertino.cupertino_checkbox", + "CupertinoColors": "flet.controls.cupertino.cupertino_colors", + "CupertinoContextMenu": "flet.controls.cupertino.cupertino_context_menu", + "CupertinoContextMenuAction": "flet.controls.cupertino.cupertino_context_menu_action", # noqa: E501 + "CupertinoDatePicker": "flet.controls.cupertino.cupertino_date_picker", + "CupertinoDatePickerDateOrder": "flet.controls.cupertino.cupertino_date_picker", + "CupertinoDatePickerMode": "flet.controls.cupertino.cupertino_date_picker", + "CupertinoDialogAction": "flet.controls.cupertino.cupertino_dialog_action", + "CupertinoFilledButton": "flet.controls.cupertino.cupertino_filled_button", + "CupertinoIcons": "flet.controls.cupertino.cupertino_icons", + "CupertinoListTile": "flet.controls.cupertino.cupertino_list_tile", + "CupertinoNavigationBar": "flet.controls.cupertino.cupertino_navigation_bar", + "CupertinoPicker": "flet.controls.cupertino.cupertino_picker", + "CupertinoRadio": "flet.controls.cupertino.cupertino_radio", + "CupertinoSegmentedButton": "flet.controls.cupertino.cupertino_segmented_button", + "CupertinoSlider": "flet.controls.cupertino.cupertino_slider", + "CupertinoSlidingSegmentedButton": "flet.controls.cupertino.cupertino_sliding_segmented_button", # noqa: E501 + "CupertinoSwitch": "flet.controls.cupertino.cupertino_switch", + "CupertinoTextField": "flet.controls.cupertino.cupertino_textfield", + "CupertinoTimerPicker": "flet.controls.cupertino.cupertino_timer_picker", + "CupertinoTimerPickerMode": "flet.controls.cupertino.cupertino_timer_picker", + "CupertinoTintedButton": "flet.controls.cupertino.cupertino_tinted_button", + "DataCell": "flet.controls.material.datatable", + "DataChannel": "flet.data_channel", + "DataChannelOpenEvent": "flet.data_channel", + "DataColumn": "flet.controls.material.datatable", + "DataColumnSortEvent": "flet.controls.material.datatable", + "DataRow": "flet.controls.material.datatable", + "DataTable": "flet.controls.material.datatable", + "DataTableTheme": "flet.controls.theme", + "DatePicker": "flet.controls.material.date_picker", + "DatePickerEntryMode": "flet.controls.material.date_picker", + "DatePickerEntryModeChangeEvent": "flet.controls.material.date_picker", + "DatePickerMode": "flet.controls.material.date_picker", + "DatePickerTheme": "flet.controls.theme", + "DateRangePicker": "flet.controls.material.date_range_picker", + "DateTimeValue": "flet.controls.duration", + "DecorationImage": "flet.controls.box", + "DeviceInfo": "flet.controls.device_info", + "DeviceOrientation": "flet.controls.types", + "DialogControl": "flet.controls.dialog_control", + "DialogTheme": "flet.controls.theme", + "DismissDirection": "flet.controls.material.snack_bar", + "Dismissible": "flet.controls.core.dismissible", + "DismissibleDismissEvent": "flet.controls.core.dismissible", + "DismissibleUpdateEvent": "flet.controls.core.dismissible", + "Divider": "flet.controls.material.divider", + "DividerTheme": "flet.controls.theme", + "DragDownEvent": "flet.controls.events", + "DragEndEvent": "flet.controls.events", + "DragStartEvent": "flet.controls.events", + "DragTarget": "flet.controls.core.drag_target", + "DragTargetEvent": "flet.controls.core.drag_target", + "DragTargetLeaveEvent": "flet.controls.core.drag_target", + "DragUpdateEvent": "flet.controls.events", + "DragWillAcceptEvent": "flet.controls.core.drag_target", + "Draggable": "flet.controls.core.draggable", + "Dropdown": "flet.controls.material.dropdown", + "DropdownM2": "flet.controls.material.dropdownm2", + "DropdownOption": "flet.controls.material.dropdown", + "DropdownTheme": "flet.controls.theme", + "Duration": "flet.controls.duration", + "DurationValue": "flet.controls.duration", + "ElevatedButton": "flet.controls.material.elevated_button", + "Event": "flet.controls.control_event", + "EventControlType": "flet.controls.control_event", + "EventHandler": "flet.controls.control_event", + "ExpansionPanel": "flet.controls.material.expansion_panel", + "ExpansionPanelList": "flet.controls.material.expansion_panel", + "ExpansionPanelListChangeEvent": "flet.controls.material.expansion_panel", + "ExpansionTile": "flet.controls.material.expansion_tile", + "ExpansionTileTheme": "flet.controls.theme", + "FilePicker": "flet.controls.services.file_picker", + "FilePickerFile": "flet.controls.services.file_picker", + "FilePickerFileType": "flet.controls.services.file_picker", + "FilePickerUploadEvent": "flet.controls.services.file_picker", + "FilePickerUploadFile": "flet.controls.services.file_picker", + "FilledButton": "flet.controls.material.filled_button", + "FilledButtonTheme": "flet.controls.theme", + "FilledIconButton": "flet.controls.material.icon_button", + "FilledTonalButton": "flet.controls.material.filled_tonal_button", + "FilledTonalIconButton": "flet.controls.material.icon_button", + "FilterQuality": "flet.controls.box", + "FletApp": "flet.controls.core.flet_app", + "FletAppOutputEvent": "flet.controls.core.flet_app", + "FletException": "flet.controls.exceptions", + "FletPageDisconnectedException": "flet.controls.exceptions", + "FletUnimplementedPlatformException": "flet.controls.exceptions", + "FletUnsupportedPlatformException": "flet.controls.exceptions", + "Flip": "flet.controls.transform", + "FloatingActionButton": "flet.controls.material.floating_action_button", + "FloatingActionButtonLocation": "flet.controls.types", + "FloatingActionButtonTheme": "flet.controls.theme", + "FontWeight": "flet.controls.types", + "ForcePressEvent": "flet.controls.events", + "FormFieldControl": "flet.controls.material.form_field_control", + "GestureDetector": "flet.controls.core.gesture_detector", + "Gradient": "flet.controls.gradients", + "GradientTileMode": "flet.controls.gradients", + "GridView": "flet.controls.core.grid_view", + "Gyroscope": "flet.controls.services.gyroscope", + "GyroscopeReadingEvent": "flet.controls.services.gyroscope", + "HapticFeedback": "flet.controls.services.haptic_feedback", + "Hero": "flet.controls.core.hero", + "HoverEvent": "flet.controls.events", + "Icon": "flet.controls.core.icon", + "IconButton": "flet.controls.material.icon_button", + "IconButtonTheme": "flet.controls.theme", + "IconData": "flet.controls.icon_data", + "IconDataOrControl": "flet.controls.types", + "IconTheme": "flet.controls.theme", + "Icons": "flet.controls.material.icons", + "IdCounter": "flet.controls.id_counter", + "Image": "flet.controls.core.image", + "ImageRepeat": "flet.controls.types", + "InputBorder": "flet.controls.material.form_field_control", + "InputFilter": "flet.controls.material.textfield", + "InteractiveViewer": "flet.controls.core.interactive_viewer", + "IosDeviceInfo": "flet.controls.device_info", + "IosUtsname": "flet.controls.device_info", + "Key": "flet.controls.keys", + "KeyDownEvent": "flet.controls.core.keyboard_listener", + "KeyRepeatEvent": "flet.controls.core.keyboard_listener", + "KeyUpEvent": "flet.controls.core.keyboard_listener", + "KeyValue": "flet.controls.keys", + "KeyboardEvent": "flet.controls.page", + "KeyboardListener": "flet.controls.core.keyboard_listener", + "KeyboardType": "flet.controls.material.textfield", + "LabelPosition": "flet.controls.types", + "LaunchMode": "flet.controls.services.url_launcher", + "LayoutControl": "flet.controls.layout_control", + "LayoutSizeChangeEvent": "flet.controls.layout_control", + "LinearGradient": "flet.controls.gradients", + "LinuxDeviceInfo": "flet.controls.device_info", + "ListTile": "flet.controls.material.list_tile", + "ListTileStyle": "flet.controls.material.list_tile", + "ListTileTheme": "flet.controls.theme", + "ListTileTitleAlignment": "flet.controls.material.list_tile", + "ListView": "flet.controls.core.list_view", + "Locale": "flet.controls.types", + "LocaleChangeEvent": "flet.controls.page", + "LocaleConfiguration": "flet.controls.types", + "LocationInfo": "flet.components.router", + "LoginEvent": "flet.controls.page", + "LongPressDownEvent": "flet.controls.events", + "LongPressEndEvent": "flet.controls.events", + "LongPressMoveUpdateEvent": "flet.controls.events", + "LongPressStartEvent": "flet.controls.events", + "MacOsDeviceInfo": "flet.controls.device_info", + "Magnetometer": "flet.controls.services.magnetometer", + "MagnetometerReadingEvent": "flet.controls.services.magnetometer", + "MainAxisAlignment": "flet.controls.types", + "Margin": "flet.controls.margin", + "MarginValue": "flet.controls.margin", + "Markdown": "flet.controls.core.markdown", + "MarkdownCodeTheme": "flet.controls.core.markdown", + "MarkdownCustomCodeTheme": "flet.controls.core.markdown", + "MarkdownExtensionSet": "flet.controls.core.markdown", + "MarkdownStyleSheet": "flet.controls.core.markdown", + "Matrix4": "flet.controls.transform", + "MenuBar": "flet.controls.material.menu_bar", + "MenuItemButton": "flet.controls.material.menu_item_button", + "MenuStyle": "flet.controls.material.menu_bar", + "MergeSemantics": "flet.controls.core.merge_semantics", + "MouseCursor": "flet.controls.types", + "MultiTapEvent": "flet.controls.events", + "MultiView": "flet.controls.multi_view", + "MultiViewAddEvent": "flet.controls.page", + "MultiViewRemoveEvent": "flet.controls.page", + "NavigationBar": "flet.controls.material.navigation_bar", + "NavigationBarDestination": "flet.controls.material.navigation_bar", + "NavigationBarLabelBehavior": "flet.controls.material.navigation_bar", + "NavigationBarTheme": "flet.controls.theme", + "NavigationDrawer": "flet.controls.material.navigation_drawer", + "NavigationDrawerDestination": "flet.controls.material.navigation_drawer", + "NavigationDrawerTheme": "flet.controls.theme", + "NavigationRail": "flet.controls.material.navigation_rail", + "NavigationRailDestination": "flet.controls.material.navigation_rail", + "NavigationRailLabelType": "flet.controls.material.navigation_rail", + "NavigationRailTheme": "flet.controls.theme", + "NotchShape": "flet.controls.types", + "Number": "flet.controls.types", + "NumbersOnlyInputFilter": "flet.controls.material.textfield", + "Observable": "flet.components.observable", + "Offset": "flet.controls.transform", + "OffsetValue": "flet.controls.transform", + "OnReorderEvent": "flet.controls.material.reorderable_list_view", + "OnScrollEvent": "flet.controls.scrollable_control", + "Orientation": "flet.controls.types", + "OutlinedBorder": "flet.controls.buttons", + "OutlinedButton": "flet.controls.material.outlined_button", + "OutlinedButtonTheme": "flet.controls.theme", + "OutlinedIconButton": "flet.controls.material.icon_button", + "OverlayVisibilityMode": "flet.controls.cupertino.cupertino_textfield", + "Padding": "flet.controls.padding", + "PaddingValue": "flet.controls.padding", + "Page": "flet.controls.page", + "PageMediaData": "flet.controls.base_page", + "PagePlatform": "flet.controls.types", + "PageResizeEvent": "flet.controls.base_page", + "PageTransitionTheme": "flet.controls.theme", + "PageTransitionsTheme": "flet.controls.theme", + "PageView": "flet.controls.core.page_view", + "Pagelet": "flet.controls.core.pagelet", + "Paint": "flet.controls.painting", + "PaintGradient": "flet.controls.painting", + "PaintLinearGradient": "flet.controls.painting", + "PaintRadialGradient": "flet.controls.painting", + "PaintSweepGradient": "flet.controls.painting", + "PaintingStyle": "flet.controls.painting", + "Placeholder": "flet.controls.core.placeholder", + "PlatformBrightnessChangeEvent": "flet.controls.page", + "PointerDeviceType": "flet.controls.types", + "PointerEvent": "flet.controls.events", + "PopupMenuButton": "flet.controls.material.popup_menu_button", + "PopupMenuItem": "flet.controls.material.popup_menu_button", + "PopupMenuPosition": "flet.controls.material.popup_menu_button", + "PopupMenuTheme": "flet.controls.theme", + "ProgressBar": "flet.controls.material.progress_bar", + "ProgressIndicatorTheme": "flet.controls.theme", + "ProgressRing": "flet.controls.material.progress_ring", + "PubSubClient": "flet.pubsub.pubsub_client", + "PubSubHub": "flet.pubsub.pubsub_hub", + "QueryString": "flet.controls.query_string", + "RadialGradient": "flet.controls.gradients", + "Radio": "flet.controls.material.radio", + "RadioGroup": "flet.controls.material.radio_group", + "RadioTheme": "flet.controls.theme", + "RangeSlider": "flet.controls.material.range_slider", + "Rect": "flet.controls.geometry", + "Ref": "flet.controls.ref", + "ReorderableDragHandle": "flet.controls.core.reorderable_drag_handle", + "ReorderableListView": "flet.controls.material.reorderable_list_view", + "ResponsiveNumber": "flet.controls.types", + "ResponsiveRow": "flet.controls.core.responsive_row", + "ResponsiveRowBreakpoint": "flet.controls.types", + "Rotate": "flet.controls.transform", + "RotateValue": "flet.controls.transform", + "RotatedBox": "flet.controls.core.rotated_box", + "RoundedRectangleBorder": "flet.controls.buttons", + "Route": "flet.components.router", + "RouteChangeEvent": "flet.controls.page", + "RouteUrlStrategy": "flet.controls.types", + "Router": "flet.components.router", + "Row": "flet.controls.core.row", + "SafeArea": "flet.controls.core.safe_area", + "Scale": "flet.controls.transform", + "ScaleEndEvent": "flet.controls.events", + "ScaleStartEvent": "flet.controls.events", + "ScaleUpdateEvent": "flet.controls.events", + "ScaleValue": "flet.controls.transform", + "ScreenBrightness": "flet.controls.services.screen_brightness", + "ScreenBrightnessChangeEvent": "flet.controls.services.screen_brightness", + "Screenshot": "flet.controls.core.screenshot", + "ScrollDirection": "flet.controls.scrollable_control", + "ScrollEvent": "flet.controls.events", + "ScrollKey": "flet.controls.keys", + "ScrollMode": "flet.controls.types", + "ScrollType": "flet.controls.scrollable_control", + "ScrollableControl": "flet.controls.scrollable_control", + "Scrollbar": "flet.controls.scrollable_control", + "ScrollbarOrientation": "flet.controls.scrollable_control", + "ScrollbarTheme": "flet.controls.theme", + "SearchBar": "flet.controls.material.search_bar", + "SearchBarTheme": "flet.controls.theme", + "SearchViewTheme": "flet.controls.theme", + "Segment": "flet.controls.material.segmented_button", + "SegmentedButton": "flet.controls.material.segmented_button", + "SegmentedButtonTheme": "flet.controls.theme", + "SelectionArea": "flet.controls.material.selection_area", + "Semantics": "flet.controls.core.semantics", + "SemanticsService": "flet.controls.services.semantics_service", + "SensorErrorEvent": "flet.controls.services.sensor_error_event", + "Service": "flet.controls.services.service", + "ShaderMask": "flet.controls.core.shader_mask", + "ShakeDetector": "flet.controls.services.shake_detector", + "ShapeBorder": "flet.controls.buttons", + "Share": "flet.controls.services.share", + "ShareCupertinoActivityType": "flet.controls.services.share", + "ShareFile": "flet.controls.services.share", + "ShareResult": "flet.controls.services.share", + "ShareResultStatus": "flet.controls.services.share", + "SharedPreferences": "flet.controls.services.shared_preferences", + "Shimmer": "flet.controls.core.shimmer", + "ShimmerDirection": "flet.controls.core.shimmer", + "Size": "flet.controls.geometry", + "Slider": "flet.controls.material.slider", + "SliderInteraction": "flet.controls.material.slider", + "SliderTheme": "flet.controls.theme", + "SnackBar": "flet.controls.material.snack_bar", + "SnackBarAction": "flet.controls.material.snack_bar", + "SnackBarBehavior": "flet.controls.material.snack_bar", + "SnackBarTheme": "flet.controls.theme", + "Stack": "flet.controls.core.stack", + "StackFit": "flet.controls.core.stack", + "StadiumBorder": "flet.controls.buttons", + "StoragePaths": "flet.controls.services.storage_paths", + "StrOrControl": "flet.controls.types", + "StrokeCap": "flet.controls.types", + "StrokeJoin": "flet.controls.types", + "StrutStyle": "flet.controls.text_style", + "SubmenuButton": "flet.controls.material.submenu_button", + "SupportsStr": "flet.controls.types", + "SweepGradient": "flet.controls.gradients", + "Switch": "flet.controls.material.switch", + "SwitchTheme": "flet.controls.theme", + "SystemOverlayStyle": "flet.controls.theme", + "Tab": "flet.controls.material.tabs", + "TabAlignment": "flet.controls.material.tabs", + "TabBar": "flet.controls.material.tabs", + "TabBarHoverEvent": "flet.controls.material.tabs", + "TabBarIndicatorSize": "flet.controls.material.tabs", + "TabBarTheme": "flet.controls.theme", + "TabBarView": "flet.controls.material.tabs", + "TabIndicatorAnimation": "flet.controls.material.tabs", + "Tabs": "flet.controls.material.tabs", + "TapEvent": "flet.controls.events", + "TapMoveEvent": "flet.controls.events", + "TemplateRoute": "flet.controls.template_route", + "Text": "flet.controls.core.text", + "TextAffinity": "flet.controls.core.text", + "TextAlign": "flet.controls.types", + "TextBaseline": "flet.controls.text_style", + "TextButton": "flet.controls.material.text_button", + "TextButtonTheme": "flet.controls.theme", + "TextCapitalization": "flet.controls.material.textfield", + "TextDecoration": "flet.controls.text_style", + "TextDecorationStyle": "flet.controls.text_style", + "TextField": "flet.controls.material.textfield", + "TextOnlyInputFilter": "flet.controls.material.textfield", + "TextOverflow": "flet.controls.text_style", + "TextSelection": "flet.controls.core.text", + "TextSelectionChangeCause": "flet.controls.core.text", + "TextSelectionChangeEvent": "flet.controls.core.text", + "TextSpan": "flet.controls.core.text_span", + "TextStyle": "flet.controls.text_style", + "TextTheme": "flet.controls.theme", + "TextThemeStyle": "flet.controls.text_style", + "Theme": "flet.controls.theme", + "ThemeMode": "flet.controls.types", + "TileAffinity": "flet.controls.material.expansion_tile", + "TimePicker": "flet.controls.material.time_picker", + "TimePickerEntryMode": "flet.controls.material.time_picker", + "TimePickerEntryModeChangeEvent": "flet.controls.material.time_picker", + "TimePickerHourFormat": "flet.controls.material.time_picker", + "TimePickerTheme": "flet.controls.theme", + "Tooltip": "flet.controls.material.tooltip", + "TooltipTheme": "flet.controls.theme", + "TooltipTriggerMode": "flet.controls.material.tooltip", + "TooltipValue": "flet.controls.material.tooltip", + "Transform": "flet.controls.transform", + "TransparentPointer": "flet.controls.core.transparent_pointer", + "UnderlineTabIndicator": "flet.controls.material.tabs", + "Url": "flet.controls.types", + "UrlLauncher": "flet.controls.services.url_launcher", + "UrlTarget": "flet.controls.types", + "UserAccelerometer": "flet.controls.services.user_accelerometer", + "UserAccelerometerReadingEvent": "flet.controls.services.user_accelerometer", + "Value": "flet.controls.base_control", + "ValueKey": "flet.controls.keys", + "VerticalAlignment": "flet.controls.types", + "VerticalDivider": "flet.controls.material.vertical_divider", + "View": "flet.controls.core.view", + "ViewPopEvent": "flet.controls.page", + "ViewsPopUntilEvent": "flet.controls.page", + "VisualDensity": "flet.controls.types", + "Wakelock": "flet.controls.services.wakelock", + "WebBrowserName": "flet.controls.device_info", + "WebDeviceInfo": "flet.controls.device_info", + "WebRenderer": "flet.controls.types", + "WebViewConfiguration": "flet.controls.services.url_launcher", + "Window": "flet.controls.core.window", + "WindowDragArea": "flet.controls.core.window_drag_area", + "WindowEvent": "flet.controls.core.window", + "WindowEventType": "flet.controls.core.window", + "WindowResizeEdge": "flet.controls.core.window", + "WindowsDeviceInfo": "flet.controls.device_info", + "alignment": "flet.controls", + "app": "flet.app", + "app_async": "flet.app", + "border": "flet.controls", + "border_radius": "flet.controls", + "component": "flet.components.component_decorator", + "context": "flet.controls.context", + "control": "flet.controls.base_control", + "create_context": "flet.components.hooks.use_context", + "cupertino_colors": "flet.controls.cupertino", + "cupertino_icons": "flet.controls.cupertino", + "dropdown": "flet.controls.material", + "dropdownm2": "flet.controls.material", + "icons": "flet.controls.material", + "is_route_active": "flet.components.router", + "margin": "flet.controls", + "memo": "flet.components.memo", + "observable": "flet.components.observable", + "on_mounted": "flet.components.hooks.use_effect", + "on_unmounted": "flet.components.hooks.use_effect", + "on_updated": "flet.components.hooks.use_effect", + "padding": "flet.controls", + "run": "flet.app", + "run_async": "flet.app", + "unwrap_component": "flet.components.public_utils", + "use_callback": "flet.components.hooks.use_callback", + "use_context": "flet.components.hooks.use_context", + "use_dialog": "flet.components.hooks.use_dialog", + "use_effect": "flet.components.hooks.use_effect", + "use_memo": "flet.components.hooks.use_memo", + "use_ref": "flet.components.hooks.use_ref", + "use_route_loader_data": "flet.components.router", + "use_route_location": "flet.components.router", + "use_route_outlet": "flet.components.router", + "use_route_params": "flet.components.router", + "use_state": "flet.components.hooks.use_state", + "use_view_path": "flet.components.router", + "value": "flet.controls.base_control", } def __getattr__(name: str): - if name in _THEME_EXPORTS: - from flet.controls import theme - - return getattr(theme, name) - if name == "Icons": - from flet.controls.material.icons import Icons - - return Icons - if name == "CupertinoIcons": - from flet.controls.cupertino.cupertino_icons import CupertinoIcons + module_path = _LAZY.get(name) + if module_path is None: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + import importlib - return CupertinoIcons - if name == "icons": - import importlib + module = importlib.import_module(module_path) + try: + value = getattr(module, name) + except AttributeError: + # `name` is itself a submodule (e.g. `flet.alignment`), not an attribute. + value = importlib.import_module(f"{module_path}.{name}") + globals()[name] = value # cache: subsequent access skips __getattr__ + return value - return importlib.import_module("flet.controls.material.icons") - if name == "cupertino_icons": - import importlib - return importlib.import_module("flet.controls.cupertino.cupertino_icons") - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") +def __dir__(): + return list(__all__) diff --git a/sdk/python/packages/flet/src/flet/app.py b/sdk/python/packages/flet/src/flet/app.py index 4566f9bb6a..1c6a82762b 100644 --- a/sdk/python/packages/flet/src/flet/app.py +++ b/sdk/python/packages/flet/src/flet/app.py @@ -262,15 +262,26 @@ def exit_gracefully(signum, frame): signal.signal(signal.SIGINT, exit_gracefully) signal.signal(signal.SIGTERM, exit_gracefully) - conn = ( - await __run_socket_server( + # Embedded runtime can opt into the in-process dart_bridge transport + # (provided by libdart_bridge from flet-dev/serious-python) by setting + # FLET_DART_BRIDGE_PORT. Falls back to the existing socket / web + # transports when the env var is absent. + bridge_port_env = os.getenv("FLET_DART_BRIDGE_PORT") + if is_embedded() and bridge_port_env: + conn = await __run_dart_bridge_server( + port=int(bridge_port_env), + main=main or target, + before_main=before_main, + ) + elif is_socket_server: + conn = await __run_socket_server( port=port, main=main or target, before_main=before_main, blocking=is_embedded(), ) - if is_socket_server - else await __run_web_server( + else: + conn = await __run_web_server( main=main or target, before_main=before_main, host=host, @@ -283,7 +294,6 @@ def exit_gracefully(signum, frame): no_cdn=no_cdn, on_startup=on_app_startup, ) - ) logger.info("Flet app has started...") @@ -315,6 +325,13 @@ def exit_gracefully(signum, frame): with contextlib.suppress(KeyboardInterrupt): await terminate.wait() + elif is_embedded() and bridge_port_env: + # dart_bridge has no serve_forever (no socket accept loop) — the + # embedded interpreter would otherwise exit as soon as the user's + # main() returns. Park here until the host process tears us down. + with contextlib.suppress(KeyboardInterrupt): + await terminate.wait() + elif view == AppView.WEB_BROWSER or view is None or force_web_server: with contextlib.suppress(KeyboardInterrupt): await terminate.wait() @@ -410,6 +427,158 @@ async def __run_socket_server( return conn +class _DartBridgeServerHandle: + """ + Forwarding facade over the currently-active `FletDartBridgeServer`. + + On Android process reuse (libdart_bridge stays loaded, the Dart VM + restarts with fresh native port numbers), this handle replaces the + underlying connection in place via `_swap_to_port`. The outer caller + in `run_async` keeps holding the same handle through restarts, so + its `finally: await conn.close()` always closes whichever connection + is current at process-teardown time. + """ + + def __init__(self, build_conn): + # `build_conn` is a callable that creates a fresh + # FletDartBridgeServer for the given port — used both initially + # and on every restart so we don't capture stale args. + self._build_conn = build_conn + self._conn = None # type: ignore[assignment] + # Pending swap-in tasks during a restart — tracked so close() + # can wait for an in-flight swap rather than racing. + self._swap_lock = asyncio.Lock() + + @property + def page_url(self) -> str: + return self._conn.page_url if self._conn is not None else "" + + @property + def pubsubhub(self): + # `flet.app.run_async` doesn't touch this, but keep parity in case + # downstream callers (tests, alternative entry points) do. + return self._conn.pubsubhub if self._conn is not None else None + + async def start(self, port: int): + self._conn = self._build_conn(port) + await self._conn.start() + + async def _swap_to_port(self, port: int): + """Close the current connection and bring up a new one on `port`.""" + async with self._swap_lock: + old = self._conn + new = self._build_conn(port) + await new.start() + self._conn = new + if old is not None: + try: + await old.close() + except Exception: + logger.warning( + "Error closing previous dart_bridge connection during " + "session restart", + exc_info=True, + ) + + async def close(self): + async with self._swap_lock: + if self._conn is not None: + await self._conn.close() + self._conn = None + + +async def __run_dart_bridge_server( + port: int, + main: Optional[AppCallable] = None, + before_main: Optional[AppCallable] = None, +): + """ + Start Flet dart_bridge transport and return active connection object. + + This transport exchanges the same MsgPack-framed protocol as + `__run_socket_server`, but over the in-process `dart_bridge` byte channel + instead of a Unix socket — eliminating the socket file, kernel context + switches, and the connect/handshake retry loop for embedded apps. + + The returned object is a `_DartBridgeServerHandle` that forwards to the + current `FletDartBridgeServer` and swaps it transparently on Android + process reuse — a `dart_bridge.add_session_restart_handler` callback + rebuilds the connection on the new Dart native port when the new Dart + VM signals fresh ports. + + Args: + port: Dart native port (passed in via env var by the embedding side; + doubles as the keyed channel identifier). + main: User app entry handler. + before_main: Optional hook called before `main`. + + Returns: + Started dart_bridge-server handle. + """ + # Imported lazily so non-embedded runs (web server, native desktop) never + # try to load libdart_bridge. + from flet.messaging.flet_dart_bridge_server import FletDartBridgeServer + + loop = asyncio.get_running_loop() + executor = concurrent.futures.ThreadPoolExecutor() + + def _build_conn(p: int) -> FletDartBridgeServer: + return FletDartBridgeServer( + loop=loop, + port=p, + on_session_created=__get_on_session_created(main), + before_main=before_main, + executor=executor, + ) + + handle = _DartBridgeServerHandle(_build_conn) + await handle.start(port) + + # Subscribe to Dart VM restart events. The C-side + # `dart_bridge_signal_dart_session` (libdart_bridge >= 1.3.0) fires + # every registered Python callback with `{label: new_port}`. On + # pre-1.3.0 binaries the symbol is absent and Dart-side + # `signalDartSession` is a no-op, so this handler never fires and the + # flow degrades to the existing "one-Dart-VM lifetime" behavior — no + # crash, no regression. + try: + import dart_bridge # type: ignore[import-not-found] + except ImportError: + # Not running inside libdart_bridge (e.g. unit tests). Skip + # restart subscription; nothing to swap to. + return handle + + add_handler = getattr(dart_bridge, "add_session_restart_handler", None) + if add_handler is None: + # Older libdart_bridge without the restart API. Same fallback as + # above — first session only. + logger.debug( + "dart_bridge.add_session_restart_handler not available; " + "process-reuse restarts will be unsupported" + ) + return handle + + def _on_session_restart(port_map): + # Called from libdart_bridge (under the GIL, possibly off the + # asyncio loop's thread). Marshal back onto the loop and run the + # swap as a regular asyncio task. + new_port = int(port_map.get("protocol", 0)) + if new_port <= 0 or new_port == port: + return + logger.info( + "Dart VM restart detected; rebinding FletDartBridgeServer " + "from port %s → %s", + port, + new_port, + ) + loop.call_soon_threadsafe( + lambda: asyncio.create_task(handle._swap_to_port(new_port)) + ) + + add_handler(_on_session_restart) + return handle + + async def __run_web_server( main: Optional[AppCallable], before_main: Optional[AppCallable], diff --git a/sdk/python/packages/flet/src/flet/components/public_utils.py b/sdk/python/packages/flet/src/flet/components/public_utils.py index 842e9e7982..62c4f3e7d1 100644 --- a/sdk/python/packages/flet/src/flet/components/public_utils.py +++ b/sdk/python/packages/flet/src/flet/components/public_utils.py @@ -1,6 +1,12 @@ -from typing import Any +from typing import TYPE_CHECKING, Any, Optional -from flet.components.component import Component +if TYPE_CHECKING: + from flet.components.component import Component + +# Cached lazily on first call so merely importing this helper (done by Page / +# BasePage) doesn't pull the whole components subsystem into the cold-start +# import graph for apps that don't use components. +_Component: "Optional[type[Component]]" = None def unwrap_component(c: Any): @@ -17,7 +23,12 @@ def unwrap_component(c: Any): The first non-`Component` value in the chain (for example a control, list of controls/views, or `None`). """ + global _Component + if _Component is None: + from flet.components.component import Component + + _Component = Component p = c - while isinstance(p, Component): + while isinstance(p, _Component): p = p._b return p diff --git a/sdk/python/packages/flet/src/flet/controls/base_control.py b/sdk/python/packages/flet/src/flet/controls/base_control.py index 9c3921cac7..67698cc21b 100644 --- a/sdk/python/packages/flet/src/flet/controls/base_control.py +++ b/sdk/python/packages/flet/src/flet/controls/base_control.py @@ -365,6 +365,26 @@ def will_unmount(self): controls_log.debug("%s.will_unmount()", self) pass + def get_data_channel(self, channel_id: int): + """ + Resolve the [DataChannel] allocated on the Dart side for this + widget. Pattern: + + on_data_channel_open: Optional[ft.EventHandler[DataChannelOpenEvent]] = None + + def init(self): + self.on_data_channel_open = self._on_open + + def _on_open(self, e): + self._channel = self.get_data_channel(e.channel_id) + + Idempotent — the underlying Connection caches DataChannels by id, + so repeated calls return the same instance. No error path: the id + always comes from a framework-fired event, so by the time this + runs the channel exists on both sides. + """ + return self.page.session.connection.data_channel_for(channel_id) + # public methods def update(self) -> None: """ diff --git a/sdk/python/packages/flet/src/flet/controls/base_page.py b/sdk/python/packages/flet/src/flet/controls/base_page.py index 94f0c14a57..807bfe4c18 100644 --- a/sdk/python/packages/flet/src/flet/controls/base_page.py +++ b/sdk/python/packages/flet/src/flet/controls/base_page.py @@ -18,8 +18,6 @@ EventHandler, ) from flet.controls.core.view import View -from flet.controls.cupertino.cupertino_app_bar import CupertinoAppBar -from flet.controls.cupertino.cupertino_navigation_bar import CupertinoNavigationBar from flet.controls.dialog_control import DialogControl from flet.controls.duration import DurationValue from flet.controls.keys import ScrollKey @@ -50,6 +48,12 @@ _MANAGED_DIALOG_DISMISS_WRAPPER = "_managed_dialog_dismiss_wrapper" if TYPE_CHECKING: + # Annotation-only (quoted at use sites): deferred so a Page doesn't eagerly + # pull the Cupertino controls (cold-start import cost). + from flet.controls.cupertino.cupertino_app_bar import CupertinoAppBar + from flet.controls.cupertino.cupertino_navigation_bar import ( + CupertinoNavigationBar, + ) from flet.controls.theme import Theme @@ -596,7 +600,7 @@ def controls(self, value: list[BaseControl]): # appbar @property - def appbar(self) -> Union[AppBar, CupertinoAppBar, None]: + def appbar(self) -> "Union[AppBar, CupertinoAppBar, None]": """ Gets or sets the top application bar (:class:`~flet.AppBar` or \ :class:`~flet.CupertinoAppBar`) for the view. @@ -607,7 +611,7 @@ def appbar(self) -> Union[AppBar, CupertinoAppBar, None]: return self.__root_view().appbar @appbar.setter - def appbar(self, value: Union[AppBar, CupertinoAppBar, None]): + def appbar(self, value: "Union[AppBar, CupertinoAppBar, None]"): self.__root_view().appbar = value # bottom_appbar @@ -625,7 +629,9 @@ def bottom_appbar(self, value: Optional[BottomAppBar]): # navigation_bar @property - def navigation_bar(self) -> Optional[Union[NavigationBar, CupertinoNavigationBar]]: + def navigation_bar( + self, + ) -> "Optional[Union[NavigationBar, CupertinoNavigationBar]]": """ Bottom navigation bar for the root view. """ @@ -635,7 +641,7 @@ def navigation_bar(self) -> Optional[Union[NavigationBar, CupertinoNavigationBar @navigation_bar.setter def navigation_bar( self, - value: Optional[Union[NavigationBar, CupertinoNavigationBar]], + value: "Optional[Union[NavigationBar, CupertinoNavigationBar]]", ): self.__root_view().navigation_bar = value diff --git a/sdk/python/packages/flet/src/flet/controls/core/view.py b/sdk/python/packages/flet/src/flet/controls/core/view.py index 094e248d9b..289b643172 100644 --- a/sdk/python/packages/flet/src/flet/controls/core/view.py +++ b/sdk/python/packages/flet/src/flet/controls/core/view.py @@ -1,12 +1,10 @@ from dataclasses import field -from typing import Optional, Union +from typing import TYPE_CHECKING, Optional, Union from flet.controls.base_control import BaseControl, control from flet.controls.box import BoxDecoration from flet.controls.control import Control from flet.controls.control_event import ControlEventHandler -from flet.controls.cupertino.cupertino_app_bar import CupertinoAppBar -from flet.controls.cupertino.cupertino_navigation_bar import CupertinoNavigationBar from flet.controls.layout_control import LayoutControl from flet.controls.material.app_bar import AppBar from flet.controls.material.bottom_app_bar import BottomAppBar @@ -25,6 +23,14 @@ Number, ) +if TYPE_CHECKING: + # Annotation-only (quoted below): deferred so a View doesn't eagerly pull the + # Cupertino controls (cold-start import cost). + from flet.controls.cupertino.cupertino_app_bar import CupertinoAppBar + from flet.controls.cupertino.cupertino_navigation_bar import ( + CupertinoNavigationBar, + ) + __all__ = ["View"] @@ -49,7 +55,7 @@ class View(ScrollableControl, LayoutControl): program to update :attr:`flet.Page.route` when a view popped. """ - appbar: Optional[Union[AppBar, CupertinoAppBar]] = None + appbar: "Optional[Union[AppBar, CupertinoAppBar]]" = None """ An :class:`~flet.AppBar` control to display at the top of the `Page`. """ @@ -71,7 +77,7 @@ class View(ScrollableControl, LayoutControl): Describes position of :attr:`floating_action_button` """ - navigation_bar: Union[NavigationBar, CupertinoNavigationBar, None] = None + navigation_bar: "Union[NavigationBar, CupertinoNavigationBar, None]" = None """ A navigation bar (:class:`~flet.NavigationBar` or \ :class:`~flet.CupertinoNavigationBar`) control to display at the bottom of the \ diff --git a/sdk/python/packages/flet/src/flet/controls/page.py b/sdk/python/packages/flet/src/flet/controls/page.py index 2574c962a6..977ec72f04 100644 --- a/sdk/python/packages/flet/src/flet/controls/page.py +++ b/sdk/python/packages/flet/src/flet/controls/page.py @@ -18,9 +18,6 @@ ) from urllib.parse import urlparse -from flet.auth.authorization import Authorization -from flet.auth.oauth_provider import OAuthProvider -from flet.components.component import Renderer from flet.components.public_utils import unwrap_component from flet.controls.base_control import BaseControl, control from flet.controls.base_page import BasePage @@ -47,11 +44,7 @@ from flet.controls.multi_view import MultiView from flet.controls.query_string import QueryString from flet.controls.ref import Ref -from flet.controls.services.browser_context_menu import BrowserContextMenu -from flet.controls.services.clipboard import Clipboard from flet.controls.services.service import Service -from flet.controls.services.shared_preferences import SharedPreferences -from flet.controls.services.storage_paths import StoragePaths from flet.controls.services.url_launcher import UrlLauncher from flet.controls.types import ( AppLifecycleState, @@ -68,17 +61,28 @@ from flet.utils.from_dict import from_dict from flet.utils.strings import random_string -if not is_pyodide(): - from flet.auth.authorization_service import AuthorizationService - - AuthorizationImpl = AuthorizationService -else: - AuthorizationImpl = Authorization - if TYPE_CHECKING: + from flet.auth.authorization import Authorization + from flet.auth.oauth_provider import OAuthProvider from flet.messaging.session import Session from flet.pubsub.pubsub_client import PubSubClient + +def _default_authorization_impl() -> "type[Authorization]": + """Resolve the default `Authorization` implementation lazily. + + Deferring these imports keeps the auth subsystem out of the cold-start + import graph for apps that never call `Page.login`. + """ + if not is_pyodide(): + from flet.auth.authorization_service import AuthorizationService + + return AuthorizationService + from flet.auth.authorization import Authorization + + return Authorization + + try: from typing import ParamSpec except ImportError: @@ -88,7 +92,7 @@ logger = logging.getLogger("flet") -AT = TypeVar("AT", bound=Authorization) +AT = TypeVar("AT", bound="Authorization") InputT = ParamSpec("InputT") RetT = TypeVar("RetT") @@ -657,6 +661,8 @@ def render( **kwargs: Keyword arguments passed to `component`. """ + from flet.components.component import Renderer + logger.debug("Page.render()") self._notify = self.__notify self.views[0].controls = Renderer().render(component, *args, **kwargs) @@ -680,6 +686,8 @@ def render_views( **kwargs: Keyword arguments passed to `component`. """ + from flet.components.component import Renderer + logger.debug("Page.render_views()") self._notify = self.__notify self.views = Renderer().render(component, *args, **kwargs) @@ -1050,7 +1058,7 @@ def get_upload_url(self, file_name: str, expires: int) -> str: async def login( self, - provider: OAuthProvider, + provider: "OAuthProvider", fetch_user: bool = True, fetch_groups: bool = False, scope: Optional[list[str]] = None, @@ -1060,14 +1068,16 @@ async def login( ] = None, complete_page_html: Optional[str] = None, redirect_to_page: Optional[bool] = False, - authorization: type[AT] = AuthorizationImpl, - ) -> AT: + authorization: "Optional[type[AT]]" = None, + ) -> "AT": """ Starts OAuth flow. See [Authentication](https://flet.dev/docs/cookbook/authentication) guide for more information and examples. """ + if authorization is None: + authorization = _default_authorization_impl() self.__authorization = authorization( provider, fetch_user=fetch_user, @@ -1278,7 +1288,7 @@ def executor(self) -> Optional[ThreadPoolExecutor]: return self.session.connection.executor @property - def auth(self) -> Optional[Authorization]: + def auth(self) -> "Optional[Authorization]": """ The current authorization context, or `None` if the user is not authorized. """ @@ -1315,6 +1325,7 @@ def browser_context_menu(self): """ The BrowserContextMenu service for the current page. """ + from flet.controls.services.browser_context_menu import BrowserContextMenu return BrowserContextMenu() @@ -1329,6 +1340,7 @@ def shared_preferences(self): """ The SharedPreferences service for the current page. """ + from flet.controls.services.shared_preferences import SharedPreferences return SharedPreferences() @@ -1343,6 +1355,7 @@ def clipboard(self): """ The Clipboard service for the current page. """ + from flet.controls.services.clipboard import Clipboard return Clipboard() @@ -1357,6 +1370,7 @@ def storage_paths(self): """ The StoragePaths service for the current page. """ + from flet.controls.services.storage_paths import StoragePaths return StoragePaths() diff --git a/sdk/python/packages/flet/src/flet/data_channel.py b/sdk/python/packages/flet/src/flet/data_channel.py new file mode 100644 index 0000000000..ae3065c12d --- /dev/null +++ b/sdk/python/packages/flet/src/flet/data_channel.py @@ -0,0 +1,153 @@ +"""Widget-facing byte-channel API. + +A `DataChannel` is a dedicated bidirectional byte transport between a +single widget's Dart side and its Python counterpart, separate from the +Flet control protocol. Used for bulk binary data — image frames, audio +buffers, ML tensors — that would otherwise pay MsgPack encode/decode +overhead through the regular protocol channel. + +The transport implementation is chosen by the active Connection subclass: + +- `FletDartBridgeServer` (embedded native via `dart_bridge`): each + channel rides its own dedicated PythonBridge native port (4–7 GiB/s on + M2 Pro). +- `FletSocketServer` / `flet_web.fastapi.FletApp` (dev mode, web with + Python server): bytes are muxed over the active Flet protocol + transport with a 1-byte type discriminator (0x01) + 4-byte channel id. + +Allocation lives on the Dart side. The widget's Dart code calls +`FletBackend.of(context).openDataChannel()` in `initState`, then fires a +`data_channel_open` control event carrying `{channel_name, channel_id}`. +The Python widget declares `on_data_channel_open: +Optional[ft.EventHandler[DataChannelOpenEvent]] = None` and inside the +handler calls `self.get_data_channel(e.channel_id)` to attach. +""" + +from __future__ import annotations + +import contextlib +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import TYPE_CHECKING, Callable + +from flet.controls.control_event import Event + +if TYPE_CHECKING: + from flet.messaging.connection import Connection + + +@dataclass +class DataChannelOpenEvent(Event["BaseControl"]): + """Fired by Dart when it opens a DataChannel for a control. Carry the + channel id (Dart native port in embedded mode, monotonic u32 in muxed + fallback) plus a user-defined `channel_name` so widgets that open + several channels can dispatch. + + Note: the field is `channel_name`, not `name`, because `Event.name` + already carries the event's own name (`"data_channel_open"`). + """ + + channel_name: str = "" + channel_id: int = 0 + + +class DataChannel(ABC): + """Abstract widget-facing byte channel.""" + + @abstractmethod + def on_bytes(self, handler: Callable[[bytes], None] | None) -> None: + """Register a handler for bytes pushed from Dart. Pass `None` to + clear. The handler runs synchronously on whatever thread the + transport delivers from — push heavy work to a queue/worker.""" + ... + + @abstractmethod + def send(self, payload: bytes) -> None: + """Send bytes Python → Dart. Fire-and-forget.""" + ... + + @abstractmethod + def close(self) -> None: + """Release the channel. Idempotent.""" + ... + + +class _DartBridgeDataChannel(DataChannel): + """Embedded native mode: bytes flow over a dedicated PythonBridge port. + `channel_id` is the Dart native port number minted on the Dart side. + """ + + def __init__(self, port: int) -> None: + import dart_bridge # type: ignore — built-in module from libdart_bridge + + self._port = port + self._dart_bridge = dart_bridge + self._handler: Callable[[bytes], None] | None = None + self._closed = False + + def on_bytes(self, handler: Callable[[bytes], None] | None) -> None: + self._handler = handler + self._dart_bridge.set_enqueue_handler_func(self._port, handler) + + def send(self, payload: bytes) -> None: + if self._closed: + return + self._dart_bridge.send_bytes(self._port, payload) + + def close(self) -> None: + if self._closed: + return + self._closed = True + if self._handler is not None: + with contextlib.suppress(Exception): + self._dart_bridge.set_enqueue_handler_func(self._port, None) + self._handler = None + + +class _ProtocolMuxedDataChannel(DataChannel): + """Non-embedded modes: bytes ride the Flet protocol transport as + `[0x01][channel_id:u32 LE][payload]` frames. The owning Connection + routes inbound frames here via `_deliver()`. + """ + + def __init__(self, channel_id: int, conn: Connection) -> None: + self._id = channel_id + self._conn = conn + self._handler: Callable[[bytes], None] | None = None + self._closed = False + + def on_bytes(self, handler: Callable[[bytes], None] | None) -> None: + self._handler = handler + + def send(self, payload: bytes) -> None: + if self._closed: + return + # Connection knows the wire format for its transport (length + # prefix on stream transports, none on message transports). + self._conn.send_data_channel_frame(self._id, payload) + + def _deliver(self, payload: bytes) -> None: + if self._closed: + return + handler = self._handler + if handler is not None: + try: + handler(payload) + except Exception: + import logging + + logging.getLogger("flet").exception( + "DataChannel handler raised; channel id=%s", self._id + ) + + def close(self) -> None: + if self._closed: + return + self._closed = True + # Connection's unregister is best-effort — duplicate unregisters + # are safe. + unreg = getattr(self._conn, "unregister_data_channel", None) + if unreg is not None: + with contextlib.suppress(Exception): + unreg(self._id) + self._handler = None diff --git a/sdk/python/packages/flet/src/flet/messaging/connection.py b/sdk/python/packages/flet/src/flet/messaging/connection.py index aa80edd72d..b4aa05081b 100644 --- a/sdk/python/packages/flet/src/flet/messaging/connection.py +++ b/sdk/python/packages/flet/src/flet/messaging/connection.py @@ -120,3 +120,39 @@ def dispose(self): Subclasses can override this method to clean up transport-specific state. """ pass + + # ------------------------------------------------------------------ + # DataChannel transport hooks. Subclasses implement these for the + # transports that carry widget byte channels: dart_bridge mode (each + # channel = a dedicated PythonBridge port), socket / WebSocket modes + # (channel frames muxed over the same protocol transport). + # ------------------------------------------------------------------ + + def data_channel_for(self, channel_id: int): + """Return the `DataChannel` for `channel_id` allocated on the Dart + side. Idempotent — subsequent calls with the same id return the + same instance within the session. + + Called by `Control.get_data_channel(id)` after a `data_channel_open` + event arrives. + """ + raise NotImplementedError( + f"{type(self).__name__} does not support DataChannel " + "(transport-specific implementation missing)." + ) + + def send_data_channel_frame(self, channel_id: int, payload: bytes) -> None: + """Send a raw DataChannel frame for `channel_id`. Used by the + protocol-muxed implementation only — dart_bridge channels go + through their own dedicated `dart_bridge.send_bytes(port, ...)` + call instead. + """ + raise NotImplementedError( + f"{type(self).__name__} does not implement send_data_channel_frame." + ) + + def unregister_data_channel(self, channel_id: int) -> None: + """Best-effort drop of `channel_id` from the mux registry. Default + is a no-op so subclasses that don't keep one don't need to override. + """ + pass diff --git a/sdk/python/packages/flet/src/flet/messaging/flet_dart_bridge_server.py b/sdk/python/packages/flet/src/flet/messaging/flet_dart_bridge_server.py new file mode 100644 index 0000000000..e6a14383de --- /dev/null +++ b/sdk/python/packages/flet/src/flet/messaging/flet_dart_bridge_server.py @@ -0,0 +1,291 @@ +""" +In-process dart_bridge transport for Flet's embedded mode. + +`FletDartBridgeServer` is an alternative to `FletSocketServer` that exchanges +the same MsgPack-framed protocol over an in-process byte channel rather than a +Unix-domain socket. The channel is provided by the `dart_bridge` built-in +Python module, registered with CPython via `PyImport_AppendInittab` by +`libdart_bridge` (the native artifact in `flet-dev/dart-bridge`). + +Activation: `flet.app.run_async` instantiates this server when +`FLET_DART_BRIDGE_PORT` is set AND `is_embedded()` is true. The Dart-side port +acts as the channel key in both directions: + +- inbound (Dart → Python): dart_bridge invokes `__on_bytes(payload)` from C + under the GIL whenever the Dart side calls `bridge.send(bytes)`. +- outbound (Python → Dart): `send_message` calls `dart_bridge.send_bytes( + port, packed)` which non-blockingly posts to the Dart native port. + +The MsgPack protocol layer is identical to `FletSocketServer`; only the byte +transport differs. +""" + +import asyncio +import contextlib +import inspect +import logging +import traceback +from collections.abc import Awaitable +from concurrent.futures import ThreadPoolExecutor +from typing import TYPE_CHECKING, Any, Callable, Optional + +import dart_bridge # type: ignore # provided by libdart_bridge inittab +import msgpack + +from flet.controls.base_control import BaseControl +from flet.messaging.connection import Connection +from flet.messaging.protocol import ( + ClientAction, + ClientMessage, + ControlEventBody, + InvokeMethodResponseBody, + RegisterClientRequestBody, + RegisterClientResponseBody, + UpdateControlPropsBody, + configure_encode_object_for_msgpack, + decode_ext_from_msgpack, +) +from flet.messaging.session import Session +from flet.pubsub.pubsub_hub import PubSubHub + +if TYPE_CHECKING: + from flet.app import AppCallable + +logger = logging.getLogger("flet") +transport_log = logging.getLogger("flet_transport") + + +class FletDartBridgeServer(Connection): + """ + Connection transport that uses the `dart_bridge` built-in module instead + of an asyncio Unix-domain socket. See module docstring for the rationale. + + Lifecycle: `start()` registers a byte handler with `dart_bridge`; bytes + arrive synchronously on whatever OS thread Dart called `bridge.send` from, + so the handler is just a thread-safe enqueue onto an asyncio.Queue that an + inbound coroutine drains on the event loop. + """ + + def __init__( + self, + loop: asyncio.AbstractEventLoop, + port: int, + on_session_created: Optional[Callable[[Session], Awaitable[Any]]] = None, + before_main: Optional["AppCallable"] = None, + executor: Optional[ThreadPoolExecutor] = None, + ): + super().__init__() + self.__port = port + self.__on_session_created = on_session_created + self.__before_main = before_main + self.session: Optional[Session] = None + self.__inbound_queue: asyncio.Queue[bytes] = asyncio.Queue() + self.__inbound_task: Optional[asyncio.Task] = None + self.__running_tasks: set[asyncio.Task] = set() + self.loop = loop + self.executor = executor + self.pubsubhub = PubSubHub(loop=loop, executor=executor) + self.page_url = f"dartbridge://{port}" + + async def start(self): + """ + Registers the inbound byte handler with `dart_bridge` and schedules + the inbound dispatch coroutine. Returns immediately — there is no + per-client handshake at the transport level (the first + REGISTER_CLIENT frame from Dart is the application-level handshake). + """ + logger.info("Starting up dart_bridge server on port %s", self.__port) + dart_bridge.set_enqueue_handler_func(self.__port, self.__on_bytes) + self.__inbound_task = asyncio.create_task(self.__inbound_loop()) + + def __on_bytes(self, payload: bytes) -> None: + """ + Receives a byte frame from `dart_bridge`. Called synchronously under + the GIL from C — may run on a non-loop thread, so we marshal onto + the loop via `call_soon_threadsafe`. + """ + self.loop.call_soon_threadsafe(self.__inbound_queue.put_nowait, payload) + + async def __inbound_loop(self): + """ + Drains the inbound queue and dispatches each packet. Each dart_bridge + send delivers one complete packet — `[type:u8][payload]`. type=0x00 + is a MsgPack-encoded Flet control frame, decoded and dispatched as + a protocol message; type=0x01 is a raw DataChannel frame, but on + the dart_bridge transport that *only* carries the Flet protocol — + DataChannels in embedded mode get their own dedicated PythonBridge, + so a 0x01 here would be an error and we log+drop. + """ + try: + while True: + packet = await self.__inbound_queue.get() + if not packet: + continue + ptype = packet[0] + if ptype == 0x00: + try: + msg = msgpack.unpackb( + packet[1:], ext_hook=decode_ext_from_msgpack + ) + await self.__on_message(msg) + except Exception: + logger.error( + "Error dispatching dart_bridge frame", exc_info=True + ) + elif ptype == 0x01: + logger.debug( + "dart_bridge channel received a 0x01 data frame; " + "DataChannels in embedded mode should use a dedicated " + "PythonBridge, not the protocol channel." + ) + else: + logger.debug( + "dart_bridge channel received packet with unknown type 0x%02x", + ptype, + ) + except asyncio.CancelledError: + logger.debug("dart_bridge inbound loop cancelled.") + + async def __on_message(self, data: Any): + """ + Protocol dispatch — identical to `FletSocketServer.__on_message`. + Duplicated here to keep the two transports decoupled; refactor into + a shared base once both have stabilised. + """ + action = ClientAction(data[0]) + body = data[1] + transport_log.debug("_on_message: %s %s", action, body) + task = None + if action == ClientAction.REGISTER_CLIENT: + req = RegisterClientRequestBody(**body) + + # create new session + self.session = Session(self) + + # apply page patch + if not req.session_id: + self.session.apply_page_patch(req.page) + + register_error = "" + try: + if inspect.iscoroutinefunction(self.__before_main): + await self.__before_main(self.session.page) + elif callable(self.__before_main): + self.__before_main(self.session.page) + except Exception as e: + register_error = f"{e}\n{traceback.format_exc()}" + logger.error("Unhandled error in before_main() handler", exc_info=True) + + # register response + self.send_message( + ClientMessage( + ClientAction.REGISTER_CLIENT, + RegisterClientResponseBody( + session_id=self.session.id, + page_patch=self.session.get_page_patch(), + error=register_error, + ), + ) + ) + + if register_error: + self.session.error(register_error) + elif self.__on_session_created is not None: + task = asyncio.create_task(self.__on_session_created(self.session)) + + elif action == ClientAction.CONTROL_EVENT: + req = ControlEventBody(**body) + task = asyncio.create_task( + self.session.dispatch_event(req.target, req.name, req.data) + ) + + elif action == ClientAction.UPDATE_CONTROL_PROPS: + req = UpdateControlPropsBody(**body) + self.session.apply_patch(req.id, req.props) + + elif action == ClientAction.INVOKE_METHOD: + req = InvokeMethodResponseBody(**body) + self.session.handle_invoke_method_results( + req.control_id, req.call_id, req.result, req.error + ) + + else: + raise RuntimeError(f'Unknown message "{action}": {body}') + + if task: + self.__running_tasks.add(task) + task.add_done_callback(self.__running_tasks.discard) + + def send_message(self, message: ClientMessage): + """ + Encodes a protocol message and posts it to the Dart side via + `dart_bridge.send_bytes`. Non-blocking; ordering is preserved by the + Dart VM's port queue. Wire format: `[0x00][msgpack body]` — no + length prefix (the bridge preserves message boundaries). + """ + transport_log.debug("send_message: %s", message) + body = msgpack.packb( + [message.action, message.body], + default=configure_encode_object_for_msgpack(BaseControl), + ) + packet = b"\x00" + body + try: + dart_bridge.send_bytes(self.__port, packet) + except Exception: + logger.error("dart_bridge.send_bytes failed", exc_info=True) + + def data_channel_for(self, channel_id: int): + """Resolve the DataChannel for `channel_id`. In embedded native mode + each DataChannel rides its own dedicated PythonBridge native port + (the `channel_id` *is* the port). Idempotent. + """ + from flet.data_channel import _DartBridgeDataChannel + + # No registry needed: _DartBridgeDataChannel is stateless wrt port + # and the dart_bridge module routes by port number internally. We + # still cache per port to avoid duplicate handler registrations. + if not hasattr(self, "_data_channels"): + self._data_channels: dict[int, Any] = {} + existing = self._data_channels.get(channel_id) + if existing is not None: + return existing + channel = _DartBridgeDataChannel(channel_id) + self._data_channels[channel_id] = channel + return channel + + async def close(self): + """ + Releases the dart_bridge handler registration and cancels pending + inbound work. Mirrors `FletSocketServer.close()` so the caller side + in `flet.app.run_async` can treat both transports uniformly. + """ + logger.debug("Closing dart_bridge server...") + try: + dart_bridge.set_enqueue_handler_func(self.__port, None) + except Exception: + logger.debug("Error unregistering dart_bridge handler", exc_info=True) + + session = self.session + self.session = None + if session is not None: + try: + session.close() + except Exception: + logger.debug("Error closing session", exc_info=True) + + if self.__inbound_task and not self.__inbound_task.done(): + self.__inbound_task.cancel() + with contextlib.suppress(asyncio.CancelledError, Exception): + await self.__inbound_task + + for task in list(self.__running_tasks): + if not task.done(): + task.cancel() + if self.__running_tasks: + await asyncio.gather(*self.__running_tasks, return_exceptions=True) + self.__running_tasks.clear() + + if self.executor: + self.executor.shutdown(wait=False, cancel_futures=True) + + logger.debug("dart_bridge server closed.") diff --git a/sdk/python/packages/flet/src/flet/messaging/flet_socket_server.py b/sdk/python/packages/flet/src/flet/messaging/flet_socket_server.py index 73596cb4bc..597af22d5a 100644 --- a/sdk/python/packages/flet/src/flet/messaging/flet_socket_server.py +++ b/sdk/python/packages/flet/src/flet/messaging/flet_socket_server.py @@ -71,6 +71,10 @@ def __init__( self.__before_main = before_main self.__blocking = blocking self.__running_tasks = set() + # DataChannel mux registry. Keyed by channel_id minted on the Dart + # side; populated lazily on the first Control.get_data_channel(id) + # call. Frames for unknown ids are silently dropped. + self._data_channels: dict[int, Any] = {} self.loop = loop self.executor = executor self.pubsubhub = PubSubHub(loop=loop, executor=executor) @@ -244,7 +248,12 @@ async def __terminate_active_connection_locked(self, reason: str) -> None: async def __receive_loop(self, reader: asyncio.StreamReader, connection_token: int): """ - Reads and dispatches inbound MsgPack frames from the socket. + Reads and dispatches inbound packets from the socket. + + Wire format on the byte stream: each packet is prefixed with a 4-byte + little-endian length, then `[type:u8][payload]`. type=0x00 is a + MsgPack-encoded Flet protocol frame; type=0x01 is a raw DataChannel + frame (`[channel_id:u32 LE][bytes]`). The loop exits when: - socket EOF is reached; @@ -255,17 +264,34 @@ async def __receive_loop(self, reader: asyncio.StreamReader, connection_token: i reader: Socket stream reader to consume bytes from. connection_token: Token identifying the connection generation. """ - unpacker = msgpack.Unpacker(ext_hook=decode_ext_from_msgpack) try: while True: - buf = await reader.read(1024 * 1024) - if not buf: + try: + header = await reader.readexactly(4) + except asyncio.IncompleteReadError: + break # EOF mid-stream or clean close + length = int.from_bytes(header, "little", signed=False) + if length == 0: + continue + try: + packet = await reader.readexactly(length) + except asyncio.IncompleteReadError: + logger.debug("Truncated packet read; aborting receive loop.") break - unpacker.feed(buf) - for msg in unpacker: - if self.__connection_token != connection_token: - return + if self.__connection_token != connection_token: + return + ptype = packet[0] + if ptype == 0x00: + msg = msgpack.unpackb(packet[1:], ext_hook=decode_ext_from_msgpack) await self.__on_message(msg) + elif ptype == 0x01: + if len(packet) < 5: + logger.debug("Dropping malformed data-channel frame.") + continue + channel_id = int.from_bytes(packet[1:5], "little", signed=False) + self.__on_data_channel_frame(channel_id, packet[5:]) + else: + logger.debug("Dropping packet with unknown type 0x%02x", ptype) except asyncio.CancelledError: logger.debug("Receive loop cancelled.") except Exception as e: @@ -273,6 +299,14 @@ async def __receive_loop(self, reader: asyncio.StreamReader, connection_token: i finally: logger.debug("Receive loop exiting.") + def __on_data_channel_frame(self, channel_id: int, payload: bytes) -> None: + """Routes an inbound `[0x01][channel_id][payload]` frame to its + registered DataChannel. Silently drops frames for unknown ids + (handles unmount races).""" + channel = self._data_channels.get(channel_id) + if channel is not None: + channel._deliver(payload) + async def __send_loop( self, writer: asyncio.StreamWriter, @@ -390,18 +424,49 @@ def send_message(self, message: ClientMessage): """ Encodes and queues an outbound message for the active connection. + Wire format: `[length:u32 LE][0x00][msgpack body]`. The send loop + writes the bytes verbatim to the socket. + If no active send queue exists (no connected client), the message is dropped. Args: message: Protocol message to send. """ transport_log.debug("send_message: %s", message) - m = msgpack.packb( + body = msgpack.packb( [message.action, message.body], default=configure_encode_object_for_msgpack(BaseControl), ) + packet = b"\x00" + body + framed = len(packet).to_bytes(4, "little", signed=False) + packet if self.__send_queue is not None: - self.__send_queue.put_nowait(m) + self.__send_queue.put_nowait(framed) + + def send_data_channel_frame(self, channel_id: int, payload: bytes) -> None: + """Send a raw DataChannel frame `[length][0x01][channel_id:u32 LE][bytes]`. + Called by `_ProtocolMuxedDataChannel.send` on the Python side.""" + header = b"\x01" + channel_id.to_bytes(4, "little", signed=False) + packet = header + payload + framed = len(packet).to_bytes(4, "little", signed=False) + packet + if self.__send_queue is not None: + self.__send_queue.put_nowait(framed) + + def data_channel_for(self, channel_id: int): + """Resolve or construct the muxed DataChannel for `channel_id`. + Idempotent — returns the same instance per id within the session. + Called from `Control.get_data_channel(id)`. + """ + from flet.data_channel import _ProtocolMuxedDataChannel + + existing = self._data_channels.get(channel_id) + if existing is not None: + return existing + channel = _ProtocolMuxedDataChannel(channel_id, self) + self._data_channels[channel_id] = channel + return channel + + def unregister_data_channel(self, channel_id: int) -> None: + self._data_channels.pop(channel_id, None) async def close(self): """ diff --git a/sdk/python/packages/flet/src/flet/messaging/pyodide_connection.py b/sdk/python/packages/flet/src/flet/messaging/pyodide_connection.py index ee51c4f589..3a18dd6e16 100644 --- a/sdk/python/packages/flet/src/flet/messaging/pyodide_connection.py +++ b/sdk/python/packages/flet/src/flet/messaging/pyodide_connection.py @@ -50,6 +50,10 @@ def __init__( self.__before_main = before_main flet_js.start_connection = self.connect self.__running_tasks = set() + # DataChannel mux registry. Pyodide mode has no dart_bridge, so + # DataChannels ride the same postMessage transport as the Flet + # protocol — disambiguated by the wire-format type byte. + self._data_channels: dict[int, Any] = {} self.pubsubhub = PubSubHub() self.loop = asyncio.get_running_loop() @@ -69,15 +73,32 @@ async def connect(self, send_callback): async def receive_loop(self): """ - Continuously receives, decodes, and dispatches inbound client messages. + Continuously receives, decodes, and dispatches inbound packets. - This loop waits for raw messages queued by `send_from_js()`, decodes MsgPack - payloads, and forwards parsed protocol frames to `__on_message()`. + Wire format on the postMessage transport: each packet from Dart is + `[type:u8][payload]`. type=0x00 is a MsgPack-encoded Flet protocol + frame; type=0x01 is a raw DataChannel frame + (`[channel_id:u32 LE][bytes]`). """ while True: data = await self.__receive_queue.get() - message = msgpack.unpackb(data.to_py(), ext_hook=decode_ext_from_msgpack) - await self.__on_message(message) + packet = bytes(data.to_py()) + if not packet: + continue + ptype = packet[0] + if ptype == 0x00: + message = msgpack.unpackb(packet[1:], ext_hook=decode_ext_from_msgpack) + await self.__on_message(message) + elif ptype == 0x01: + if len(packet) < 5: + logger.debug("Dropping malformed data-channel frame.") + continue + channel_id = int.from_bytes(packet[1:5], "little", signed=False) + channel = self._data_channels.get(channel_id) + if channel is not None: + channel._deliver(packet[5:]) + else: + logger.debug("Dropping packet with unknown type 0x%02x", ptype) def send_from_js(self, message: Any): """ @@ -173,12 +194,37 @@ def send_message(self, message: ClientMessage): """ Serializes and sends an outbound protocol message to JavaScript. + Wire format: `[0x00][msgpack body]`. postMessage preserves message + boundaries, so no length prefix. + Args: message: Client message to serialize with MsgPack and send. """ transport_log.debug("send_message: %s", message) - m = msgpack.packb( + body = msgpack.packb( [message.action, message.body], default=configure_encode_object_for_msgpack(BaseControl), ) - self.send_callback(m) + self.send_callback(b"\x00" + body) + + def send_data_channel_frame(self, channel_id: int, payload: bytes) -> None: + """Send a raw DataChannel frame `[0x01][channel_id:u32 LE][bytes]` + over postMessage. Called by `_ProtocolMuxedDataChannel.send`.""" + header = b"\x01" + channel_id.to_bytes(4, "little", signed=False) + self.send_callback(header + payload) + + def data_channel_for(self, channel_id: int): + """Resolve or construct the muxed DataChannel for `channel_id`. + Idempotent — same id returns the same instance within the session. + """ + from flet.data_channel import _ProtocolMuxedDataChannel + + existing = self._data_channels.get(channel_id) + if existing is not None: + return existing + channel = _ProtocolMuxedDataChannel(channel_id, self) + self._data_channels[channel_id] = channel + return channel + + def unregister_data_channel(self, channel_id: int) -> None: + self._data_channels.pop(channel_id, None) diff --git a/sdk/python/packages/flet/src/flet/messaging/session.py b/sdk/python/packages/flet/src/flet/messaging/session.py index 460e800431..a3508964fa 100644 --- a/sdk/python/packages/flet/src/flet/messaging/session.py +++ b/sdk/python/packages/flet/src/flet/messaging/session.py @@ -5,9 +5,8 @@ import traceback import weakref from datetime import datetime, timedelta, timezone -from typing import Any, Optional +from typing import TYPE_CHECKING, Any, Optional -from flet.components.hooks.use_effect import EffectHook from flet.controls.base_control import BaseControl from flet.controls.context import _context_page, context from flet.controls.object_patch import ObjectPatch @@ -25,6 +24,11 @@ from flet.utils.object_model import patch_dataclass from flet.utils.strings import random_string +if TYPE_CHECKING: + # Annotation-only (quoted at use site): deferred so a Session doesn't eagerly + # pull the components/hooks subsystem (cold-start import cost). + from flet.components.hooks.use_effect import EffectHook + logger = logging.getLogger("flet") patch_logger = logging.getLogger("flet_object_patch") @@ -631,7 +635,7 @@ def schedule_update(self, control: BaseControl): self.__pending_updates.add(control) self.__updates_ready.set() - def schedule_effect(self, hook: EffectHook, is_cleanup: bool): + def schedule_effect(self, hook: "EffectHook", is_cleanup: bool): """ Queues an effect hook setup/cleanup operation for scheduler execution. diff --git a/sdk/python/templates/build/cookiecutter.json b/sdk/python/templates/build/cookiecutter.json index 405dfb8dfa..b67704878b 100644 --- a/sdk/python/templates/build/cookiecutter.json +++ b/sdk/python/templates/build/cookiecutter.json @@ -29,6 +29,7 @@ "split_per_abi": false, "no_cdn": false, "no_wasm": false, + "pyodide_version": "", "options": null, "pyproject": null, "_extensions": ["cookiecutter_extensions.FletExtension"] diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/android/app/build.gradle.kts b/sdk/python/templates/build/{{cookiecutter.out_dir}}/android/app/build.gradle.kts index 3bb7ddc6bf..3d60c6b329 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/android/app/build.gradle.kts +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/android/app/build.gradle.kts @@ -2,8 +2,9 @@ import java.util.Properties plugins { id("com.android.application") - id("org.jetbrains.kotlin.android") - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + // Kotlin is provided by the Flutter Gradle Plugin (Built-in Kotlin), so the + // app no longer applies the Kotlin Gradle Plugin itself. + // The Flutter Gradle Plugin must be applied after the Android Gradle plugin. id("dev.flutter.flutter-gradle-plugin") } @@ -22,16 +23,14 @@ android { compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion + // serious_python loads Python extension modules memory-mapped directly from the + // APK (no extraction) and ships pure Python in stored asset zips, so + // `useLegacyPackaging` / `keepDebugSymbols` from earlier Flet templates are no + // longer needed at minSdk 23+. The `pickFirsts` and `excludes` blocks below + // address two unrelated multi-source jniLibs issues that AGP can't resolve on + // its own. packaging { jniLibs { - useLegacyPackaging = true - keepDebugSymbols += listOf( - "*/arm64-v8a/libpython*.so", - "*/armeabi-v7a/libpython*.so", - "*/x86/libpython*.so", - "*/x86_64/libpython*.so", - ) - // serious_python_android ships libc++_shared.so as part of the Python runtime payload // (the cross-compiled wheels on pypi.flet.dev depend on it at link time). Many third-party // Flutter plugins (ultralytics_yolo, sentry_flutter, several ML / CV / audio plugins) also @@ -44,6 +43,13 @@ android { // will work against it. pickFirsts is the narrowly-scoped escape hatch for exactly this case -- it // only opens a hole for the matching glob; any other future duplicate-native-lib conflict still fails loudly. pickFirsts += listOf("**/libc++_shared.so") + +// flet: excluded_abis {% if cookiecutter.options.android_excluded_abis %} + // Strip native libs of ABIs not requested via `target_arch`. + // `ndk.abiFilters` alone can't do this: the Flutter Gradle plugin adds all default + // ABIs as buildType-level filters and AGP merges the two levels as a union. + excludes += listOf({% for abi in cookiecutter.options.android_excluded_abis %}"lib/{{ abi }}/**"{% if not loop.last %}, {% endif %}{% endfor %}) +// flet: end of excluded_abis {% endif %} } } @@ -52,10 +58,6 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - sourceSets["main"].java.srcDir("src/main/kotlin") {% set min_sdk_version = get_pyproject("tool.flet.android.min_sdk_version") %} @@ -112,6 +114,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + flutter { source = "../.." } diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/main.dart b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/main.dart index a4adf44dcf..73b8557f6d 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/main.dart +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/main.dart @@ -9,11 +9,17 @@ import 'package:flutter/services.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart' as path_provider; -import 'package:serious_python/serious_python.dart'; import 'package:flutter_web_plugins/url_strategy.dart'; import 'package:window_manager/window_manager.dart'; -import "python.dart"; +// `dart:ffi` (and therefore `package:serious_python/bridge.dart`, +// `package:serious_python/serious_python.dart`) isn't available on web. +// The conditional import below loads the real PythonBridge-backed runtime +// on platforms where FFI exists; on web it resolves to a stub of the same +// shape that just throws if invoked. `main` guards every use with +// `kIsWeb` so the stub is never actually called. +import 'native_runtime_stub.dart' + if (dart.library.ffi) 'native_runtime.dart' as nrt; {% for dep in cookiecutter.flutter.dependencies %} import 'package:{{ dep }}/{{ dep }}.dart' as {{ dep }}; @@ -86,6 +92,18 @@ void main(List args) async { future: prepareApp(), builder: (BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasData) { + // In production mode prepareApp() created native bridges; wire a + // PythonBridge-backed channel so FletApp talks to the embedded + // Python over the in-process FFI transport. In web + dev modes + // the bridges are absent and FletApp falls back to its URL-scheme + // factory (websocket / TCP / UDS). + final channelBuilder = nrt.channelBuilder; + // Native (non-web): high-throughput byte channels for widgets get + // their own dedicated PythonBridge each. Web (Pyodide) leaves this + // null so FletBackend's built-in ProtocolMuxedDataChannelFactory + // muxes channel bytes over the postMessage transport with + // Transferable ArrayBuffer for zero-copy. + final dataChannelFactory = nrt.dataChannelFactory; // OK - start Python program return kIsWeb || (isDesktopPlatform() && _args.isNotEmpty) ? FletApp( @@ -112,6 +130,8 @@ void main(List args) async { assetsDir: assetsDir, showAppStartupScreen: showAppStartupScreen, appStartupScreenMessage: appStartupScreenMessage, + channelBuilder: channelBuilder, + dataChannelFactory: dataChannelFactory, extensions: extensions); } }); @@ -164,7 +184,7 @@ Future prepareApp() async { } else { // production mode // extract app from asset - appDir = await extractAssetZip(assetPath, checkHash: true); + appDir = await nrt.extractAppAssets(assetPath, checkHash: true); // set current directory to app path Directory.current = appDir; @@ -197,16 +217,15 @@ Future prepareApp() async { environmentVariables.putIfAbsent( "FLET_PLATFORM", () => defaultTargetPlatform.name.toLowerCase()); - if (defaultTargetPlatform == TargetPlatform.windows) { - // use TCP on Windows - var tcpPort = await getUnusedPort(); - pageUrl = "tcp://localhost:$tcpPort"; - environmentVariables.putIfAbsent("FLET_SERVER_PORT", () => tcpPort.toString()); - } else { - // use UDS on other platforms - pageUrl = "flet_$pid.sock"; - environmentVariables.putIfAbsent("FLET_SERVER_UDS_PATH", () => pageUrl); - } + // In production we use the in-process dart_bridge FFI transport (no UDS, + // no TCP — Python and Flutter share the process). Two bridges, both + // owned by `native_runtime.dart`: + // protocol bridge — the Flet MsgPack channel (Dart ↔ Python). + // exit bridge — Python-only outbound channel carrying the exit + // code when `sys.exit(code)` is called inside the + // embedded interpreter. Replaces the legacy + // stdout-callback socket. + pageUrl = nrt.initBridges(environmentVariables); } if (!kIsWeb && assetsDir.isNotEmpty) { @@ -217,74 +236,27 @@ Future prepareApp() async { } Future runPythonApp(List args) async { - var argvItems = args.map((a) => "\"${a.replaceAll('"', '\\"')}\""); - var argv = "[${argvItems.isNotEmpty ? argvItems.join(',') : '""'}]"; - var script = pythonScript - .replaceAll("{outLogFilename}", outLogFilename.replaceAll("\\", "\\\\")) - .replaceAll('{module_name}', pythonModuleName) - .replaceAll('{argv}', argv); - - var completer = Completer(); - - ServerSocket outSocketServer; - String socketAddr = ""; - StringBuffer pythonOut = StringBuffer(); - - if (defaultTargetPlatform == TargetPlatform.windows) { - var tcpAddr = "127.0.0.1"; - outSocketServer = await ServerSocket.bind(tcpAddr, 0); + // Process-reuse path: Android may keep the OS process alive across a + // back-button quit and restart only the Dart VM. libdart_bridge stays + // loaded, Python is still up. `initBridges()` already fired + // `dart_bridge_signal_dart_session` with the new ports — Python's + // session-restart handlers have rewired by now. Don't call into + // `SeriousPython.runProgram` again (it would no-op-return immediately + // anyway, but the never-completing-future park here keeps the + // FletApp's existing FutureBuilder rendering until the OS tears us + // down for real). + if (nrt.pythonAlreadyRunning) { debugPrint( - 'Python output TCP Server is listening on port ${outSocketServer.port}'); - socketAddr = "$tcpAddr:${outSocketServer.port}"; - } else { - socketAddr = "stdout_$pid.sock"; - if (await File(socketAddr).exists()) { - await File(socketAddr).delete(); - } - outSocketServer = await ServerSocket.bind( - InternetAddress(socketAddr, type: InternetAddressType.unix), 0); - debugPrint('Python output Socket Server is listening on $socketAddr'); + "Python already initialized (process reuse) — skipping SeriousPython.runProgram"); + return Completer().future; } - - environmentVariables.putIfAbsent("FLET_PYTHON_CALLBACK_SOCKET_ADDR", () => socketAddr); - - void closeOutServer() async { - outSocketServer.close(); - - int exitCode = int.tryParse(pythonOut.toString().trim()) ?? 0; - - if (exitCode == errorExitCode) { - var out = ""; - if (await File(outLogFilename).exists()) { - out = await File(outLogFilename).readAsString(); - } - completer.complete(out); - } else { - exit(exitCode); - } - } - - outSocketServer.listen((client) { - debugPrint( - 'Connection from: ${client.remoteAddress.address}:${client.remotePort}'); - client.listen((data) { - var s = String.fromCharCodes(data); - pythonOut.write(s); - }, onError: (error) { - client.close(); - closeOutServer(); - }, onDone: () { - client.close(); - closeOutServer(); - }); - }); - - // run python async - SeriousPython.runProgram(path.join(appDir, "$pythonModuleName.pyc"), - script: script, environmentVariables: environmentVariables); - - // wait for client connection to close - return completer.future; + return nrt.runPython( + moduleName: pythonModuleName, + appDir: appDir, + outLogFilename: outLogFilename, + environmentVariables: environmentVariables, + args: args, + ); } class ErrorScreen extends StatelessWidget { @@ -377,10 +349,3 @@ class BlankScreen extends StatelessWidget { } } -Future getUnusedPort() { - return ServerSocket.bind("127.0.0.1", 0).then((socket) { - var port = socket.port; - socket.close(); - return port; - }); -} diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart new file mode 100644 index 0000000000..ab281e4901 --- /dev/null +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart @@ -0,0 +1,266 @@ +// Native-only embedded-Python runtime: PythonBridge transport, +// SeriousPython.runProgram, exit-bridge wiring. This file is selected by +// `main.dart`'s conditional import on platforms where `dart:ffi` exists +// (mobile, desktop). On web, `native_runtime_stub.dart` is used instead. +// +// Splitting this out is what lets `flet build web` compile — package +// `serious_python` pulls in `dart:ffi` / `Pointer<…>` types via +// `package:serious_python_platform_interface`, which are not part of the +// web SDK. Importing the module unconditionally from `main.dart` makes +// the web build fail with "Type 'Pointer' not found" et al., regardless +// of `kIsWeb` runtime gates. + +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flet/flet.dart'; +import 'package:flutter/foundation.dart'; +import 'package:path/path.dart' as path; +import 'package:serious_python/bridge.dart'; +import 'package:serious_python/serious_python.dart'; + +import 'python.dart'; + +// In production (embedded) mode the Flet protocol flows over an in-process +// PythonBridge — no socket file, no TCP. `_exitBridge` is a separate bridge +// dedicated to Python's exit-code transmission (replaces the legacy stdout- +// callback socket). Both stay null until [initBridges] runs. +PythonBridge? _bridge; +PythonBridge? _exitBridge; + +/// Allocate the protocol + exit bridges, stamp the required env vars onto +/// [envVars], and return the `dartbridge://` page URL. +/// +/// Also fires `dart_bridge_signal_dart_session` unconditionally. On a fresh +/// start where libdart_bridge hasn't initialized Python yet, the call is a +/// cheap no-op inside the C library. On Android process reuse — where the +/// OS kept this process alive across a Dart VM restart and Python is still +/// loaded with handlers on the previous (now-dead) ports — it dispatches +/// the new port numbers to the running Python session-restart subscribers +/// so they can rewire transparently. See libdart_bridge >= 1.3.0. +String initBridges(Map envVars) { + _bridge = PythonBridge(); + _exitBridge = PythonBridge(); + + // Signal the running Python (if any) about the new Dart native ports. + // The labels here match what `serious_python_run` reconstructs from + // env vars on its own reuse path — keep in sync. + DartBridge.instance.signalDartSession({ + "protocol": _bridge!.port, + "exit": _exitBridge!.port, + }); + + envVars.putIfAbsent("FLET_DART_BRIDGE_PORT", () => _bridge!.port.toString()); + envVars.putIfAbsent( + "FLET_DART_BRIDGE_EXIT_PORT", + () => _exitBridge!.port.toString(), + ); + return "dartbridge://${_bridge!.port}"; +} + +bool get bridgesActive => _bridge != null; + +/// True when libdart_bridge already has an embedded CPython up from a +/// previous Dart VM in the same OS process. On Android the OS may keep +/// the process alive across a back-button exit and restart only the Dart +/// VM on re-launch — this flag lets `runPythonApp` skip +/// `SeriousPython.runProgram` (which would otherwise wedge waiting on a +/// Python interpreter that's already running). Always false on a fresh +/// process, and false when running against pre-1.3.0 libdart_bridge. +bool get pythonAlreadyRunning => DartBridge.instance.isPythonInitialized; + +/// Extract the bundled app.zip into a fresh app directory and return its +/// path. Wraps `serious_python.extractAssetZip` so main.dart doesn't have +/// to import the FFI-touching package directly. +Future extractAppAssets(String assetPath, {bool checkHash = false}) => + extractAssetZip(assetPath, checkHash: checkHash); + +/// FletApp's `channelBuilder` for the embedded-Python protocol — wraps the +/// in-process [PythonBridge] in a [FletBackendChannel]. Returns null until +/// [initBridges] has run. +FletBackendChannelBuilder? get channelBuilder => _bridge == null + ? null + : ({ + required FletBackendChannelOnPacketCallback onPacket, + required FletBackendChannelOnDisconnectCallback onDisconnect, + }) => _DartBridgeBackendChannel( + _bridge!, + onPacket: onPacket, + onDisconnect: onDisconnect, + ); + +/// FletApp's `dataChannelFactory` for high-throughput byte channels — +/// each `open()` mints a fresh [PythonBridge] (dedicated native port). +DataChannelFactory? get dataChannelFactory => _PythonBridgeDataChannelFactory(); + +/// Boot the embedded interpreter and wait for it to exit. Returns the +/// captured console output on error exit; calls `exit(code)` directly on +/// normal exit codes. +Future runPython({ + required String moduleName, + required String appDir, + required String outLogFilename, + required Map environmentVariables, + required List args, +}) async { + var argvItems = args.map((a) => "\"${a.replaceAll('"', '\\"')}\""); + var argv = "[${argvItems.isNotEmpty ? argvItems.join(',') : '""'}]"; + var script = pythonScript + .replaceAll("{outLogFilename}", outLogFilename.replaceAll("\\", "\\\\")) + .replaceAll('{module_name}', moduleName) + .replaceAll('{argv}', argv); + + var completer = Completer(); + + // Subscribe to the exit-code bridge. Python's `sys.exit(code)` is patched + // (in python.dart) to encode `code` as raw UTF-8 bytes and post them via + // `dart_bridge.send_bytes(FLET_DART_BRIDGE_EXIT_PORT, ...)`. We don't need + // a streaming codec here — the channel only ever carries a single short + // payload, then Python tears down. + StringBuffer pythonExitBuf = StringBuffer(); + StreamSubscription? exitSub; + + void onExitSignal() async { + await exitSub?.cancel(); + int exitCode = int.tryParse(pythonExitBuf.toString().trim()) ?? 0; + if (exitCode == errorExitCode) { + var out = ""; + if (await File(outLogFilename).exists()) { + out = await File(outLogFilename).readAsString(); + } + completer.complete(out); + } else { + exit(exitCode); + } + } + + exitSub = _exitBridge!.messages.listen( + (data) { + pythonExitBuf.write(String.fromCharCodes(data)); + onExitSignal(); + }, + onError: (error) { + debugPrint('Exit bridge error: $error'); + onExitSignal(); + }, + onDone: onExitSignal, + cancelOnError: false, + ); + + SeriousPython.runProgram( + path.join(appDir, "$moduleName.pyc"), + script: script, + environmentVariables: environmentVariables, + ); + + return completer.future; +} + +/// `FletBackendChannel` implementation backed by a [PythonBridge]. Bytes +/// flow Dart↔Python entirely in-process; no Unix socket, no kernel context +/// switch. Each PythonBridge `send` is one complete packet on the wire — +/// `[type:u8][payload]`. No framing layer needed (the bridge preserves +/// message boundaries). +class _DartBridgeBackendChannel implements FletBackendChannel { + _DartBridgeBackendChannel( + this._bridge, { + required FletBackendChannelOnPacketCallback onPacket, + required FletBackendChannelOnDisconnectCallback onDisconnect, + }) : _onPacket = onPacket, + _onDisconnect = onDisconnect; + + final PythonBridge _bridge; + final FletBackendChannelOnPacketCallback _onPacket; + final FletBackendChannelOnDisconnectCallback _onDisconnect; + StreamSubscription? _subscription; + + @override + Future connect() async { + _subscription = _bridge.messages.listen( + _onPacket, + onError: (error, stack) { + debugPrint("PythonBridge stream error: $error"); + _onDisconnect(); + }, + onDone: () { + debugPrint("PythonBridge stream closed."); + _onDisconnect(); + }, + cancelOnError: false, + ); + } + + @override + void send(Uint8List packet) { + // Retry loop covers the brief startup window where Python hasn't yet + // called `dart_bridge.set_enqueue_handler_func` — bridge.send returns + // false in that case. Once Flet's app.py registers the handler (which + // happens before `runpy.run_module` is dispatched), bridge.send returns + // true synchronously. + if (_bridge.send(packet)) return; + _retrySend(packet); + } + + void _retrySend(Uint8List packet) { + const interval = Duration(milliseconds: 50); + const deadline = Duration(seconds: 30); + final start = DateTime.now(); + Timer.periodic(interval, (timer) { + if (_bridge.send(packet)) { + timer.cancel(); + } else if (DateTime.now().difference(start) > deadline) { + timer.cancel(); + debugPrint( + "PythonBridge send timed out: Python handler never registered.", + ); + } + }); + } + + @override + bool get isLocalConnection => true; + + @override + int get defaultReconnectIntervalMs => 0; + + @override + void disconnect() { + _subscription?.cancel(); + _subscription = null; + } +} + +/// [DataChannel] backed by a dedicated [PythonBridge] — fast path for +/// embedded native mode. Each open() mints a fresh bridge with its own +/// native port; the bridge's `port` becomes the channel id we propagate +/// to Python (via the widget's `data_channel_open` event). +class _PythonBridgeDataChannel implements DataChannel { + _PythonBridgeDataChannel(this._bridge); + final PythonBridge _bridge; + bool _closed = false; + + @override + int get id => _bridge.port; + + @override + Stream get messages => _bridge.messages; + + @override + bool send(Uint8List bytes) { + if (_closed) return false; + return _bridge.send(bytes); + } + + @override + void close() { + if (_closed) return; + _closed = true; + _bridge.close(); + } +} + +class _PythonBridgeDataChannelFactory implements DataChannelFactory { + @override + DataChannel open() => _PythonBridgeDataChannel(PythonBridge()); +} diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime_stub.dart b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime_stub.dart new file mode 100644 index 0000000000..95cacb5acc --- /dev/null +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime_stub.dart @@ -0,0 +1,33 @@ +// Web stub for `native_runtime.dart`. Selected by `main.dart`'s conditional +// import on platforms where `dart:ffi` is NOT available (web). +// +// All callers must guard with `kIsWeb` so the real native_runtime entry +// points are never invoked on web — but the stubs still need to exist for +// the file to compile. + +import 'package:flet/flet.dart'; + +String initBridges(Map envVars) => + throw UnsupportedError("Native bridges not available on web"); + +bool get bridgesActive => false; + +/// Always false on web — there's no embedded CPython to be "already +/// running". Mirrors `native_runtime.dart`'s getter so `main.dart`'s +/// process-reuse check compiles for the web target. +bool get pythonAlreadyRunning => false; + +Future extractAppAssets(String assetPath, {bool checkHash = false}) => + throw UnsupportedError("Asset extraction not available on web"); + +FletBackendChannelBuilder? get channelBuilder => null; + +DataChannelFactory? get dataChannelFactory => null; + +Future runPython({ + required String moduleName, + required String appDir, + required String outLogFilename, + required Map environmentVariables, + required List args, +}) => throw UnsupportedError("Native runtime not available on web"); diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart index 770dff1818..1065d7b89f 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/python.dart @@ -1,7 +1,7 @@ const errorExitCode = 100; const pythonScript = """ -import os, runpy, socket, sys, traceback +import os, runpy, sys, traceback # fix for cryptography package os.environ["CRYPTOGRAPHY_OPENSSL_NO_LEGACY"] = "1" @@ -57,28 +57,105 @@ initialize_ctypes() out_file = open("{outLogFilename}", "w+", buffering=1) -callback_socket_addr = os.getenv("FLET_PYTHON_CALLBACK_SOCKET_ADDR") -if ":" in callback_socket_addr: - addr, port = callback_socket_addr.split(":") - callback_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - callback_socket.connect((addr, int(port))) -else: - callback_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - callback_socket.connect(callback_socket_addr) - -sys.stdout = sys.stderr = out_file +# libdart_bridge >= 1.3.0 installs native-log file-like wrappers as +# sys.stdout / sys.stderr right after Py_Initialize so prints land in +# logcat (Android) / os_log (iOS) / stderr (desktop). Tee here so the +# error-screen capture file ALSO gets the output — both paths matter. +# On older libdart_bridge (no native install) `_native_*` will be the +# default text streams; the tee still works as a plain duplicate write. +_native_stdout = sys.stdout +_native_stderr = sys.stderr + +class _TeeWriter: + # The file half receives writes raw — preserves byte-for-byte parity + # with what Python wrote, so the error-screen capture file matches a + # plain `python` console run. + # + # The native (logcat / os_log) half is line-buffered so Python's + # `print(x)` doesn't produce two log entries — CPython implements + # `print` as `write(text)` + `write("\\n")`, and the standalone + # newline write would otherwise show as a blank logcat row after + # every print. We accumulate until we see "\\n", emit the line + # without the trailing newline, and skip purely empty lines (so + # `print()` with no args also stays out of the log). + def __init__(self, native, file_): + self._native = native + self._file = file_ + self._native_buf = "" + def write(self, text): + if not text: + return 0 + try: + self._native_buf += text + while True: + nl = self._native_buf.find("\\n") + if nl < 0: + break + line = self._native_buf[:nl] + self._native_buf = self._native_buf[nl + 1:] + if line: + self._native.write(line) + except Exception: + pass + return self._file.write(text) + def flush(self): + try: + # Drain any pending partial line (no trailing newline in the + # source stream — could happen if the user app calls + # `sys.stdout.flush()` mid-line). + if self._native_buf: + self._native.write(self._native_buf) + self._native_buf = "" + self._native.flush() + except Exception: + pass + self._file.flush() + def isatty(self): + return False + def fileno(self): + return self._file.fileno() + +sys.stdout = _TeeWriter(_native_stdout, out_file) +sys.stderr = _TeeWriter(_native_stderr, out_file) + +# Exit-code transport. The Dart side allocated a dedicated PythonBridge port +# (FLET_DART_BRIDGE_EXIT_PORT) and is listening on it. `flet_exit` posts the +# exit code as raw UTF-8 bytes through that bridge — the Dart side parses, +# then either renders the error screen (code == 100) or terminates the host +# process (any other code) using the file we wrote to above. +# +# On Android process reuse (Dart VM restarts while libdart_bridge stays +# loaded), the exit-bridge port number changes. We keep `_exit_port` in a +# one-element list so the session-restart handler below can mutate it in +# place — `flet_exit` always reads the current value. +import dart_bridge # built-in module provided by libdart_bridge +_exit_port = [int(os.environ["FLET_DART_BRIDGE_EXIT_PORT"])] def flet_exit(code=0): - callback_socket.sendall(str(code).encode()) - out_file.close() - callback_socket.close() + try: + dart_bridge.send_bytes(_exit_port[0], str(code).encode()) + finally: + out_file.close() sys.exit = flet_exit +# Subscribe to new-Dart-VM signals if the running libdart_bridge supports +# them (>= 1.3.0). On process reuse, the new VM's port-map carries the +# fresh exit-bridge port number; rewire so flet_exit talks to the right +# Dart side. Older libdart_bridge doesn't expose the handler — fall +# through silently and the existing single-VM behaviour holds. +_add_restart = getattr(dart_bridge, "add_session_restart_handler", None) +if _add_restart is not None: + def _on_dart_session_restart(port_map): + new_exit = port_map.get("exit") + if new_exit is not None: + _exit_port[0] = int(new_exit) + _add_restart(_on_dart_session_restart) + ex = None try: import certifi - + os.environ["REQUESTS_CA_BUNDLE"] = certifi.where() os.environ["SSL_CERT_FILE"] = certifi.where() diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml b/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml index cad332bbeb..22472a5acd 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml @@ -17,7 +17,12 @@ dependencies: flet: path: ../../../../../packages/flet - serious_python: 2.0.0 + serious_python: 3.0.0 + + # MsgPack codec used by the dart_bridge FletBackendChannel implementation + # in lib/main.dart — matches the wire format flet's existing socket + # transport already speaks. + msgpack_dart: ^1.0.1 package_info_plus: ^9.0.0 path_provider: ^2.1.4 @@ -32,8 +37,6 @@ dependencies: dependency_overrides: flet: path: ../../../../../packages/flet - device_info_plus: 12.3.0 # remove that in May, 2026 once CI moved to macOS 26 - connectivity_plus: 7.0.0 # remove that in May, 2026 once CI moved to macOS 26 dev_dependencies: flutter_launcher_icons: ^0.14.1 diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/index.html b/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/index.html index 53bb5e3961..891ee2f47d 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/index.html +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/index.html @@ -29,7 +29,16 @@ assetBase: "{{ cookiecutter.base_url }}", routeUrlStrategy: "{{ cookiecutter.route_url_strategy }}", canvasKitBaseUrl: "/canvaskit/", - pyodideUrl: "/pyodide/pyodide.js", + // .mjs is required: python-worker.js is a module worker and loads + // the runtime via dynamic `import()`. Pyodide >= 0.29 also refuses + // to run in classic workers. Use the local copy that `flet build` + // drops into web/pyodide/ when --no-cdn is set; otherwise pull + // from jsdelivr so the build artifact can stay slim. + {% if cookiecutter.no_cdn == "True" %} + pyodideUrl: "/pyodide/pyodide.mjs", + {% else %} + pyodideUrl: "https://cdn.jsdelivr.net/pyodide/v{{ cookiecutter.pyodide_version }}/full/pyodide.mjs", + {% endif %} pythonModuleName: "{{ cookiecutter.python_module_name }}", webRenderer: "{{ cookiecutter.web_renderer }}", fontFallbackBaseUrl: "assets/fonts/", // for Noto Emoji, use Google CDN diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/python-worker.js b/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/python-worker.js index 40c6083afc..5c743dc8e4 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/python-worker.js +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/python-worker.js @@ -38,7 +38,14 @@ self.sendPythonOutput = function (text, isStderr) { self.initPyodide = async function () { try { - importScripts(self.pyodideUrl); + // Module-worker load path. `importScripts` only exists in classic + // workers — Pyodide >= 0.29 actively refuses to load there ("Classic + // web workers are not supported"). We're a module worker, so the + // runtime ships as `pyodide.mjs` and exposes `loadPyodide` via ESM + // exports. The CDN/jsdelivr fallback URL set by patch_index.py also + // points at the .mjs variant. + const pyodideModule = await import(self.pyodideUrl); + const loadPyodide = pyodideModule.loadPyodide || self.loadPyodide; self.pyodide = await loadPyodide({ stdout: (text) => self.sendPythonOutput(text, false), stderr: (text) => self.sendPythonOutput(text, true), @@ -206,7 +213,14 @@ self.initPyodide = async function () { }; self.receiveCallback = (message) => { - self.postMessage(message.toJs()); + // `message` is a Pyodide JsProxy wrapping a Python `bytes`. `toJs()` + // gives us a fresh Uint8Array; transferring its underlying ArrayBuffer + // to the main thread skips the structured-clone copy (~hundreds of KB + // per matplotlib frame). Safe because the Uint8Array is freshly + // materialized here, and the original Python `bytes` is untouched + // (Pyodide keeps its own reference). + const bytes = message.toJs(); + self.postMessage(bytes, [bytes.buffer]); } // Same channel as `receiveCallback`, exposed under `flet_js` so the // Python python_output shim can post pre-encoded msgpack frames. diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/python.js b/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/python.js index 138fcb3f2e..89e09e6366 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/python.js +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/web/python.js @@ -13,7 +13,14 @@ globalThis.jsConnect = async function(appId, args, dartOnMessage) { }; console.log(`Starting up Python worker: ${appId}, args: ${args}`); _apps[appId] = app; - app.worker = new Worker("python-worker.js"); + // Module worker (type: "module") is required by Pyodide >= 0.29 — the + // runtime throws "Classic web workers are not supported" inside any + // worker where `importScripts` is callable. Module workers don't have + // `importScripts`, so the check passes. Older Pyodide lines (0.27.x) + // accept module workers too, so this is forward-compatible across all + // supported Python versions (3.12 → Pyodide 0.27.7, 3.13 → 0.29.4, + // 3.14 → 314.0.0). + app.worker = new Worker("python-worker.js", { type: "module" }); var error; app.worker.onmessage = (event) => { @@ -31,7 +38,11 @@ globalThis.jsConnect = async function(appId, args, dartOnMessage) { // initialize worker app.worker.postMessage({ - pyodideUrl: flet.pyodideUrl || "pyodide/pyodide.js", + // `.mjs` is the ES-module variant. python-worker.js (now a module + // worker) loads it via dynamic `import()`. The legacy `.js` + // variant relied on `importScripts`, which doesn't exist in a + // module worker. + pyodideUrl: flet.pyodideUrl || "pyodide/pyodide.mjs", args: args, documentUrl: _documentUrl, appPackageUrl: flet.appPackageUrl, diff --git a/website/blog/2026-07-01-flet-v-0-86-release-announcement.md b/website/blog/2026-07-01-flet-v-0-86-release-announcement.md new file mode 100644 index 0000000000..7e2754f898 --- /dev/null +++ b/website/blog/2026-07-01-flet-v-0-86-release-announcement.md @@ -0,0 +1,18 @@ +--- +slug: flet-v-0-86-release-announcement +title: "Flet 0.86.0: TBD" +authors: feodor +tags: [releases] +--- + +TBD + +## Conclusion + +TBD + +Flet 0.86.0 fills in two pieces that declarative apps were really missing: routing and dialogs. Combined with smoother video, real-time audio, and a healthy round of bug fixes, this release moves the `@ft.component` programming model from "promising" to "production-ready for real apps". + +Try it in your apps and share feedback in [GitHub Discussions](https://github.com/flet-dev/flet/discussions) or on [Discord](https://discord.gg/dzWXP8SHG8). + +Happy Flet-ing! diff --git a/website/docs/extend/user-extensions.md b/website/docs/extend/user-extensions.md index efcf1ada06..ff95181bfd 100644 --- a/website/docs/extend/user-extensions.md +++ b/website/docs/extend/user-extensions.md @@ -464,6 +464,161 @@ Common pitfalls: You can find source code for this example [here](https://github.com/flet-dev/flet-spinkit). +## Dedicated data channels + +Property updates and method calls between Python and Dart go over Flet's MsgPack-framed control protocol. That works well for widget state and small events but caps at ~1 GB/s with several allocations per frame. For widgets that need to move **bulk binary data** — image frames, audio buffers, file blobs, ML inference tensors — Flet provides **dedicated `DataChannel`s** that bypass MsgPack entirely and reach memory-bandwidth-class throughput on every supported platform. + +Examples of widgets that benefit: + +- A chart receiving a stream of bitmaps from Python (1080p RGBA at 60 fps ≈ 480 MB/s — far above what the control protocol can sustain) +- A camera widget pushing frames Dart → Python for ML inference +- A microphone widget streaming PCM samples to a Python DSP pipeline + +For sub-KB widget state, you don't need this — the regular property protocol is faster end-to-end at that size. Reach for `DataChannel` when payloads start at a few KB and especially when they stream at high rates. + +### How it works + +Allocation lives on the **Dart side**: the widget calls `FletBackend.of(context).openDataChannel()` in `initState`, then **fires a control event named `data_channel_open`** carrying `{channel_name, channel_id}`. The Python side declares an `on_data_channel_open` handler, retrieves the matching channel via `Control.get_data_channel(channel_id)`, and starts sending/receiving bytes. + +Transport is chosen per deployment mode automatically — dedicated `PythonBridge` per channel in embedded native (`flet build` for desktop/mobile), raw-byte frames muxed over `postMessage` with Transferable ArrayBuffer in Pyodide, muxed over the protocol socket in `flet run` dev mode, muxed over WebSocket with a Python server. **Widget code is identical across all modes.** + +### Python-side API + +```python +from typing import Callable, Optional + +import flet as ft + + +@ft.control("MyImageChart") +class MyImageChart(ft.LayoutControl): + on_data_channel_open: Optional[ft.EventHandler[ft.DataChannelOpenEvent]] = None + + def init(self) -> None: + # `init` is the @ft.control post-construct hook (runs before `did_mount`). + # Wire up the channel-capture handler here. + self._frames: Optional[ft.DataChannel] = None + if self.on_data_channel_open is None: + self.on_data_channel_open = self._capture_channel + + def _capture_channel(self, e: ft.DataChannelOpenEvent) -> None: + # `e.channel_name` is the label the Dart side put in the event + # payload — dispatch on it when a widget opens multiple channels. + # Single-channel widgets can ignore it. + self._frames = self.get_data_channel(e.channel_id) + # Optional: subscribe to bytes flowing Dart → Python. + self._frames.on_bytes(self._on_frame_from_dart) + + def push_frame(self, rgba_bytes: bytes) -> None: + """Python → Dart — fire-and-forget byte send.""" + if self._frames is not None: + self._frames.send(rgba_bytes) + + def _on_frame_from_dart(self, payload: bytes) -> None: + # Called from the transport's delivery thread (under the GIL in + # embedded native mode). Push to a `queue.Queue` and let a worker + # drain — don't do heavy CPU work here, it'll starve the transport. + ... +``` + +`ft.DataChannel` is an abstract class — instances come back from `Control.get_data_channel(channel_id)`. Its surface: + +- `send(payload: bytes)` — Python → Dart, fire-and-forget +- `on_bytes(callback: Callable[[bytes], None] | None)` — register a handler for bytes pushed from Dart; pass `None` to clear +- `close()` — release the channel (idempotent; the framework auto-closes on control unmount, you rarely need to call it explicitly) + +The `ft.DataChannelOpenEvent` fields are `channel_name: str` and `channel_id: int`. The field is `channel_name`, not `name`, because `name` is reserved on the base `Event` class for the event's own name (`"data_channel_open"`). + +### Dart-side API + +```dart +import 'package:flet/flet.dart'; + +class MyImageChartState extends State { + late final DataChannel _frames; + StreamSubscription? _sub; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_sub != null) return; // initialise lazily, once + _frames = FletBackend.of(context).openDataChannel(); + _sub = _frames.messages.listen(_onFrameFromPython); + // Tell Python about the channel via a regular control event. + widget.control.triggerEvent("data_channel_open", { + "channel_name": "frames", + "channel_id": _frames.id, + }); + } + + void _onFrameFromPython(Uint8List bytes) { + // hand to a Texture, dart:ui.Image.fromPixels, etc. + } + + void _pushFrameToPython(Uint8List bytes) { + _frames.send(bytes); + } + + @override + void dispose() { + _sub?.cancel(); + _frames.close(); + super.dispose(); + } +} +``` + +`DataChannel` Dart-side surface: + +- `int get id` — the channel identifier that goes into the `data_channel_open` event payload +- `Stream get messages` — bytes pushed from Python +- `bool send(Uint8List bytes)` — Dart → Python, fire-and-forget +- `void close()` — release the channel (idempotent) + +**Allocate the channel in `didChangeDependencies`, not `initState`** — `FletBackend.of(context)` needs an active `BuildContext` and that's the first lifecycle hook where it's safely available. + +Neither side imports `serious_python` or `dart_bridge`. The DataChannel API surface lives entirely in `package:flet` / `flet`, so your extension's dependencies stay the same. + +### Multiple channels per widget + +A control can open as many channels as it needs — each `openDataChannel()` call mints a unique id. Disambiguate them by `channel_name` in the event: + +```dart +// Dart — open two channels, label each +_frames = FletBackend.of(context).openDataChannel(); +_audio = FletBackend.of(context).openDataChannel(); +widget.control.triggerEvent("data_channel_open", { + "channel_name": "frames", + "channel_id": _frames.id, +}); +widget.control.triggerEvent("data_channel_open", { + "channel_name": "audio", + "channel_id": _audio.id, +}); +``` + +```python +# Python — dispatch on channel_name +def _on_data_channel_open(self, e: ft.DataChannelOpenEvent) -> None: + match e.channel_name: + case "frames": + self._frames = self.get_data_channel(e.channel_id) + case "audio": + self._audio = self.get_data_channel(e.channel_id) +``` + +### Threading + backpressure + +The `on_bytes` handler runs **synchronously under the GIL** on whatever OS thread the transport delivered from. For anything CPU-heavy (PNG decode, ML inference) push the payload onto a `queue.Queue` or `asyncio.Queue` and let a worker drain — blocking the delivery thread will starve the transport. + +The receiving side's queue is **unbounded by default**. If a producer outpaces the consumer (camera frames into a slow decoder, matplotlib rotation into Flutter paint), memory grows. For media-streaming widgets, implement backpressure or a drop-old policy on the producer side. A reference example is [`MatplotlibChartCanvas`](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_chart_canvas.py) — it uses a 1-byte ack frame from Dart so the producer waits per-frame, mirroring matplotlib WebAgg's `waiting` flag pattern. + +### See also + +- **First-party reference widget**: `MatplotlibChartCanvas` — a complete migration from `_invoke_method` PNG dispatch to a 1-byte-opcode data channel, including backpressure ack and the GPU / CPU rendering strategies. [Python](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-charts/src/flet_charts/matplotlib_chart_canvas.py) · [Dart](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-charts/src/flutter/flet_charts/lib/src/matplotlib_chart_canvas.dart) +- **Full design / performance notes** (wire format, cross-mode operation, concurrency model, empirical numbers): [`dedicated-data-channels.md`](https://github.com/flet-dev/serious-python/blob/main/docs/dedicated-data-channels.md) in `flet-dev/serious-python`. +- **Wire-format protocol upgrade** for anyone implementing a custom Flet backend or sidecar: [Flet protocol framing upgraded for DataChannel support](/docs/updates/breaking-changes/v0-86-0-data-channel-protocol-upgrade). + ## Examples Flet has controls that are implemented as [built-in extensions](built-in-extensions.md) and could serve as a starting point for your own controls. diff --git a/website/docs/publish/android.md b/website/docs/publish/android.md index fdd4207de4..1f38cf64f8 100644 --- a/website/docs/publish/android.md +++ b/website/docs/publish/android.md @@ -74,10 +74,17 @@ requires distributing the correct APK for each device. The following target architectures are supported: -- [`arm64-v8a`](https://developer.android.com/ndk/guides/abis#arm64-v8a) -- [`armeabi-v7a`](https://developer.android.com/ndk/guides/abis#v7a) -- [`x86_64`](https://developer.android.com/ndk/guides/abis#86-64) -- [`x86`](https://developer.android.com/ndk/guides/abis#x86) +- [`arm64-v8a`](https://developer.android.com/ndk/guides/abis#arm64-v8a) (64-bit) — Python `3.12` and above +- [`x86_64`](https://developer.android.com/ndk/guides/abis#86-64) (64-bit) — Python `3.12` and above +- [`armeabi-v7a`](https://developer.android.com/ndk/guides/abis#v7a) (32-bit) — Python `3.12` **only** + +:::note +The available architectures depend on the +[bundled Python version](index.md#bundled-python): Python dropped +32-bit Android support in `3.13` ([PEP 738](https://peps.python.org/pep-0738/)), +so targeting `armeabi-v7a` requires building with `--python-version 3.12`. By default, +an app is built for all architectures its bundled Python version supports. +::: #### Resolution order @@ -87,8 +94,9 @@ Its value is determined in the following order of precedence: 2. `[tool.flet.android].split_per_abi` 3. `false` -When enabled, 3 APKs are produced by default, one for each of the following ABIs: `arm64-v8a`, -`armeabi-v7a`, and `x86_64`. These can be customized by setting [`target architectures`](index.md#target-architecture). +When enabled, one APK is produced per ABI — by default, one for each +architecture the bundled Python version supports (see above). These can be +customized by setting [`target architectures`](index.md#target-architecture). #### Example @@ -763,6 +771,46 @@ adaptive_icon_background = "#0B6BFF" ``` + +## Android extract packages + +:::note +[Android](android.md) only. +::: + +On Android, pure Python ships in a stored zip read in place (`zipimport`) and native modules are +loaded memory-mapped from the APK. Packages that read their bundled **data files** through a real +filesystem path — `__file__` / `pkg_resources` instead of +[`importlib.resources`](https://docs.python.org/3/library/importlib.resources.html) — don't work +from inside the zip. List such "path-hungry" packages here to ship them **extracted to disk** +instead. + +Most packages that bundle data (including `certifi`) read it through `importlib.resources`, which +is zip-safe, so they need no entry here — only add packages that actually fail to find their data +when imported from the zip. + +### Resolution order + +1. [`--android-extract-packages`](../cli/flet-build.md#--android-extract-packages) +2. `[tool.flet.android].extract_packages` +3. `[tool.flet].extract_packages` + +### Example + + + +```bash +flet build apk --android-extract-packages package1 package2 +``` + + +```toml +[tool.flet.android] +extract_packages = ["package1", "package2"] +``` + + + ## ADB Tips [Android Debug Bridge (adb)](https://developer.android.com/tools/adb) is a diff --git a/website/docs/publish/index.md b/website/docs/publish/index.md index e05a44b75a..47aa41e25d 100644 --- a/website/docs/publish/index.md +++ b/website/docs/publish/index.md @@ -130,8 +130,8 @@ Supported versions and the matching CPython / Pyodide artifacts: | Short | CPython runtime | Pyodide (web) | Status | | ----- | --------------- | ------------- | -------- | -| 3.14 | 3.14.5 | 314.0.0a2 | default | -| 3.13 | 3.13.13 | 0.29.4 | stable | +| 3.14 | 3.14.6 | 314.0.0 | default | +| 3.13 | 3.13.14 | 0.29.4 | stable | | 3.12 | 3.12.13 | 0.27.7 | stable | The version is resolved in this order: @@ -1081,9 +1081,15 @@ removes known junk files and any additional globs you specify. (implies `cleanup-packages`) * `cleanup-packages`: remove junk files from site-packages (defaults to `true`) -By default, Flet does **not** compile your app files during packaging. -This allows the build process to complete even if there are syntax errors, -which can be useful for debugging or rapid iteration. +By default, Flet **compiles** both your app and the installed packages to `.pyc` +during packaging. Shipping bytecode avoids recompiling every module on each cold +start — a significant startup win on mobile, where pure Python is imported from a +stored zip and cannot cache bytecode back to disk. + +Pass `--no-compile-app` / `--no-compile-packages` (or set `[tool.flet.compile].app` +/ `[tool.flet.compile].packages` to `false`) to disable it — for example to speed +up iterative builds, or to keep `.py` source in the bundle so the build still +completes with syntax errors present and tracebacks show source lines. #### Resolution order @@ -1092,14 +1098,14 @@ The values of `compile-app` and `cleanup-app` are respectively determined in the 1. [`--compile-app`](../cli/flet-build.md#--compile-app) / [`--cleanup-app`](../cli/flet-build.md#--cleanup-app) 2. `[tool.flet..compile].app` / `[tool.flet..cleanup].app` 3. `[tool.flet.compile].app` / `[tool.flet.cleanup].app` -4. empty list / empty list +4. `True` / empty list The values of `compile-packages` and `cleanup-packages` are respectively determined in the following order of precedence: 1. [`--compile-packages`](../cli/flet-build.md#--compile-packages) / [`--cleanup-packages`](../cli/flet-build.md#--cleanup-packages) 2. `[tool.flet..compile].packages` / `[tool.flet..cleanup].packages` 3. `[tool.flet.compile].packages` / `[tool.flet.cleanup].packages` -4. `False` / `True` +4. `True` / `True` The values of `cleanup-app-files` and `cleanup-package-files` are respectively determined in the following order of precedence: @@ -1535,7 +1541,7 @@ jobs: shell: bash run: | sudo apt update --allow-releaseinfo-change - LINUX_DEPS="$(uv run python -c 'from flet.utils.linux_deps import linux_dependencies; print(" ".join(linux_dependencies))')" + LINUX_DEPS="$(uv run flet --version --json | jq -r '.linux_dependencies | join(" ")')" sudo apt-get install -y --no-install-recommends $LINUX_DEPS sudo apt-get clean diff --git a/website/docs/publish/web/static-website/index.md b/website/docs/publish/web/static-website/index.md index 45600ac618..a8f7a61dcd 100644 --- a/website/docs/publish/web/static-website/index.md +++ b/website/docs/publish/web/static-website/index.md @@ -50,7 +50,7 @@ full matrix and resolution rules. In short: | Python | Pyodide | | ------ | --------- | -| 3.14 | 314.0.0a2 | +| 3.14 | 314.0.0 | | 3.13 | 0.29.4 | | 3.12 | 0.27.7 | diff --git a/website/docs/updates/breaking-changes/index.md b/website/docs/updates/breaking-changes/index.md index 248aae0c9f..243078106e 100644 --- a/website/docs/updates/breaking-changes/index.md +++ b/website/docs/updates/breaking-changes/index.md @@ -24,6 +24,13 @@ Each guide explains the change, the reason for it, and how to migrate your code. ### Released in Flet 0.86.0 +#### Breaking changes + +- [Default bundled Python version is now 3.14](/docs/updates/breaking-changes/v0-86-0-default-bundled-python-3-14) +- [App and packages are compiled to `.pyc` by default](/docs/updates/breaking-changes/v0-86-0-compile-on-by-default) +- [`flet.version.pyodide_version` and `PYODIDE_VERSION` removed](/docs/updates/breaking-changes/v0-86-0-removed-pyodide-version-export) +- [Flet protocol framing upgraded for DataChannel support](/docs/updates/breaking-changes/v0-86-0-data-channel-protocol-upgrade) + #### Deprecations - [`flet build --clear-cache` flag deprecated](/docs/updates/breaking-changes/v0-86-0-deprecated-clear-cache-flag) diff --git a/website/docs/updates/breaking-changes/v0-86-0-compile-on-by-default.md b/website/docs/updates/breaking-changes/v0-86-0-compile-on-by-default.md new file mode 100644 index 0000000000..e62778e70d --- /dev/null +++ b/website/docs/updates/breaking-changes/v0-86-0-compile-on-by-default.md @@ -0,0 +1,78 @@ +--- +title: "App and packages are compiled to .pyc by default" +--- + +# App and packages are compiled to `.pyc` by default + +:::note +This guide is accurate as of Flet 0.86.0. Later releases might add new APIs or +additional migration paths. + +The [breaking changes and deprecations index](.) lists the guides created for each release. +::: + +## Summary + +`flet build` now **compile your app and the installed +packages to `.pyc` by default** (via `python -m compileall -b`, with the original +`.py` files removed from the bundle). Previously compilation was off unless you +opted in with `--compile-app` / `--compile-packages` or +`[tool.flet.compile]` in `pyproject.toml`. + +Shipping bytecode removes per-launch recompilation. The win is largest on +**mobile**, where pure Python is imported from a stored, in-place zip +(`zipimport`) that cannot cache bytecode back to disk — so without precompiled +`.pyc`, every module is recompiled from source on **every** cold start. On a +mid-range Android device this costs roughly 1–2 seconds of startup, scaling with +the number of modules imported. + +## Background + +Compilation has always been available behind the `--compile-app` / +`--compile-packages` flags; it just defaulted off. With the move to in-place +`zipimport` packaging on Android, leaving it off meant a recurring cold-start tax +on every launch, so the default was flipped on. + +Compiled web builds were verified to load correctly in Pyodide: the standalone +CPython used to compile and the Pyodide runtime share the same minor version +(e.g. 3.14), and CPython keeps the `.pyc` magic number stable across patch +releases of a minor version, so the bytecode is accepted. + +## Migration guide + +Most apps need no changes — builds simply start faster. + +If you relied on uncompiled builds (to keep `.py` source in the bundle for +debugging, to let a build complete despite syntax errors, or to speed up +iterative builds), opt out explicitly. + +### Opt out via the CLI flag + +The flags are now `argparse.BooleanOptionalAction`, so each gets a `--no-` form +(the original `--compile-app` / `--compile-packages` still work): + +```bash +flet build apk --no-compile-app --no-compile-packages +``` + +### Opt out in `pyproject.toml` + +```toml +[tool.flet.compile] +app = false +packages = false +``` + +Per-platform overrides also work, e.g. `[tool.flet.android.compile]`. The +resolution order is CLI flag → `[tool.flet..compile]` → +`[tool.flet.compile]` → default (`true`). + +## Timeline + +- Changed in: `0.86.0` + +## References + +- API documentation: [Compilation and cleanup](/docs/publish#compilation-and-cleanup) +- Issues and PRs: [#6598](https://github.com/flet-dev/flet/pull/6598) +- Release notes: [Flet 0.86.0](../release-notes.md) diff --git a/website/docs/updates/breaking-changes/v0-86-0-data-channel-protocol-upgrade.md b/website/docs/updates/breaking-changes/v0-86-0-data-channel-protocol-upgrade.md new file mode 100644 index 0000000000..588165486f --- /dev/null +++ b/website/docs/updates/breaking-changes/v0-86-0-data-channel-protocol-upgrade.md @@ -0,0 +1,111 @@ +--- +title: "Flet protocol framing upgraded for DataChannel support" +--- + +# Flet protocol framing upgraded for DataChannel support + +:::note +This guide is accurate as of Flet 0.86.0. Later releases might add new APIs or +additional migration paths. + +The [breaking changes and deprecations index](.) lists the guides created for each release. +::: + +## Summary + +Flet 0.86.0 introduces dedicated **data channels** for widgets that need to +move bulk binary data (image frames, audio buffers, ML tensors) between Dart +and Python without going through the MsgPack control protocol. + +Enabling this required a one-time upgrade of the Flet protocol's wire format: + +- **Stream-oriented transports** (`flet run` dev mode over UDS / TCP) now use + **4-byte little-endian length-prefixed framing**. The previous streaming + `msgpack.Unpacker.feed` decode is gone. +- **Every transport** (sockets, WebSocket, `dart_bridge` FFI, Pyodide + `postMessage`) now puts a **1-byte type discriminator** at the head of each + packet: + - `0x00` — MsgPack-encoded Flet control frame (widget state, events) — same + contents as before, just with a type byte in front. + - `0x01` — raw DataChannel frame (`[channel_id:u32 LE][payload]`). + +The new format is **not backwards-compatible**: a Flet 0.85 client cannot talk +to a Flet 0.86 server, and vice versa. The `flet-cli` dev server and the +in-process Python runtime were upgraded together in this release. + +## Background + +Flet's previous wire format on UDS / TCP relied on `msgpack.Unpacker.feed` to +re-assemble messages from arbitrary byte-stream chunks. That works for +MsgPack-only traffic but doesn't compose with a second logical stream of raw +bytes — you can't tell a partial MsgPack value from the start of a raw frame +without an out-of-band marker. + +Adding the 1-byte type byte + 4-byte length prefix solves this in the smallest +possible way: + +- Receivers read `[length][type][...]`, fan out by type byte, no streaming + decoder state to maintain. +- Message-oriented transports (WebSocket, `postMessage`, `dart_bridge`) drop + the length prefix since each message is already one packet; they keep the + type byte. +- Per-frame overhead is 5 bytes (message-oriented) or 9 bytes (stream-oriented) + — under 1% at any payload size that motivates a data channel. + +## Migration guide + +### Most users — nothing to change + +If you only use `flet build` artifacts or run `flet run` with the matching +`flet` package version (the default install pulls both at once), there's +nothing to do. The CLI and the runtime are version-locked. + +### Users running `flet-cli` and the `flet` Python package from different installs + +Make sure both come from the same release. The common gotcha is a global +`flet` install plus a project-local `flet` in `.venv` at different versions +— upgrade or pin both to ≥ `0.86.0` (or both to ≤ `0.85.x`). + +A version mismatch will surface as a connection failure during `flet run` +with a parse error in the dev-server log. + +### Users with custom backends or sidecars speaking the Flet protocol + +The wire format changed. Update your encoder/decoder: + +- **Inbound on a stream transport** (UDS / TCP): read 4 bytes (length, u32 LE), + read that many bytes, the first byte is the type discriminator, the rest is + the payload. +- **Inbound on a message transport** (WebSocket, `postMessage`): each + message's first byte is the type discriminator, the rest is the payload. +- **Outbound**: same shape, mirrored. + +Treat `0x01` (raw DataChannel) frames as opaque if you don't use data channels +— forward them along or drop them, never feed them to your MsgPack decoder. + +### Code that depended on `StreamingMsgpackDeserializer` + +The Dart-side class `package:flet/src/transport/streaming_msgpack_deserializer.dart` +is removed. There are no public consumers — it was an internal helper for +stream-transport framing that's no longer needed now that every packet is +length-delimited. If you imported it (you shouldn't have), decode each inbound +buffer with one-shot `msgpack.deserialize(bytes)` instead. + +### Custom widgets that override `MatplotlibChartCanvas` + +`MatplotlibChartCanvas` now transports its `apply_full` / `apply_diff` / +`clear` frames through a [DataChannel] rather than `_invoke_method` arguments. +The Python-facing method signatures are unchanged (`apply_full(image_bytes: +bytes)`, etc.), and they remain the documented API. But if you subclassed the +canvas and overrode the Dart-side `_invokeMethod` handler, that override no +longer fires — the Dart side now consumes a 1-byte opcode + PNG payload from +the channel directly. + +## Timeline + +- Changed in: `0.86.0` + +## References + +- API: `flet.DataChannel` (Python) and `FletBackend.openDataChannel` (Dart) — see the module docstrings in `flet/data_channel.py`; dedicated reference pages will be added in a follow-up. +- Release notes: [Flet 0.86.0](../release-notes.md) diff --git a/website/docs/updates/breaking-changes/v0-86-0-default-bundled-python-3-14.md b/website/docs/updates/breaking-changes/v0-86-0-default-bundled-python-3-14.md new file mode 100644 index 0000000000..c9a8d8cff3 --- /dev/null +++ b/website/docs/updates/breaking-changes/v0-86-0-default-bundled-python-3-14.md @@ -0,0 +1,89 @@ +--- +title: "Default bundled Python version is now 3.14" +--- + +# Default bundled Python version is now 3.14 + +:::note +This guide is accurate as of Flet 0.86.0. Later releases might add new APIs or +additional migration paths. + +The [breaking changes and deprecations index](.) lists the guides created for each release. +::: + +## Summary + +Flet 0.86.0 introduces multi-version bundled CPython support to `flet build` +and `flet publish`. The bundled Python interpreter is now selected per build +(see [Choosing a Python version](/docs/publish#choosing-a-python-version)) and +the **default is the latest supported stable** — currently CPython 3.14. +Earlier Flet releases implicitly bundled CPython 3.12 via the single-version +`serious_python` 1.x. + +Apps that depend on Python packages whose pre-built wheels aren't yet +available for 3.14 (typically packages with C/Rust extensions that haven't +caught up to the new ABI) need to pin a previous Python version. + +## Background + +Multi-version Python support landed in +[#6577](https://github.com/flet-dev/flet/pull/6577) and is tracked by the +central registry in +[`flet_cli/utils/python_versions.py`](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-cli/src/flet_cli/utils/python_versions.py). +The supported short versions are `3.12`, `3.13`, and `3.14` (each pinned to a +specific CPython patch + Pyodide release on the registry side). + +The default flips to the latest stable so new projects get the most recent +Python by default. Existing projects without a pin start picking it up too on +their next build. + +## Migration guide + +You can pin a different Python version in three equivalent ways. Use whichever +fits your project layout. + +### Pin in `pyproject.toml` (recommended) + +Add or update `[project].requires-python`: + +```toml +[project] +requires-python = ">=3.12,<3.13" +``` + +`flet build` parses the specifier and picks the **highest supported short +version** that satisfies it. So `>=3.12,<3.13` resolves to 3.12, +`>=3.13,<3.14` resolves to 3.13, and `>=3.14` resolves to 3.14 (the default). + +### Pin via the CLI flag + +Pass `--python-version` on every invocation: + +```bash +flet build apk --python-version 3.12 +flet publish --python-version 3.12 +``` + +The CLI flag overrides anything in `pyproject.toml`. + +### Pin via environment variable + +Export `SERIOUS_PYTHON_VERSION` in the shell that runs `flet build`: + +```bash +export SERIOUS_PYTHON_VERSION=3.12 +flet build apk +``` + +Useful in CI pipelines where you don't want to thread the flag through every +job. + +## Timeline + +- Changed in: `0.86.0` + +## References + +- API documentation: [Choosing a Python version](/docs/publish#choosing-a-python-version) +- Issues and PRs: [#6577](https://github.com/flet-dev/flet/pull/6577) +- Release notes: [Flet 0.86.0](../release-notes.md) diff --git a/website/docs/updates/breaking-changes/v0-86-0-removed-pyodide-version-export.md b/website/docs/updates/breaking-changes/v0-86-0-removed-pyodide-version-export.md new file mode 100644 index 0000000000..ec6e96f870 --- /dev/null +++ b/website/docs/updates/breaking-changes/v0-86-0-removed-pyodide-version-export.md @@ -0,0 +1,78 @@ +--- +title: "flet.version.pyodide_version and PYODIDE_VERSION removed" +--- + +# `flet.version.pyodide_version` and `PYODIDE_VERSION` removed + +:::note +This guide is accurate as of Flet 0.86.0. Later releases might add new APIs or +additional migration paths. + +The [breaking changes and deprecations index](.) lists the guides created for each release. +::: + +## Summary + +Flet 0.86.0 removed the module-level `flet.version.pyodide_version` attribute +and the matching `PYODIDE_VERSION` constant. The single fixed-string export no +longer makes sense now that Pyodide is selected **per build** as part of the +multi-version bundled Python support — the bundled release depends on which +Python version your app pins, not a global default. + +## Background + +Earlier Flet releases used a single Pyodide pin in `flet.version` to drive the +`flet --version` output and let tooling read which Pyodide release `flet build +web` would bundle. With +[multi-version Python support](/docs/publish#choosing-a-python-version) the +mapping is now Python-version-aware (`3.12 → 0.27.7`, `3.13 → 0.29.4`, +`3.14 → 314.0.0`). It is loaded on demand from +[python-build's date-keyed `manifest.json`](https://github.com/flet-dev/python-build) +— the single source of truth shared with serious_python — via +[`flet_cli/utils/python_versions.py`](https://github.com/flet-dev/flet/blob/main/sdk/python/packages/flet-cli/src/flet_cli/utils/python_versions.py), +so it is no longer hand-maintained. + +## Migration guide + +If you imported the constant somewhere — typically from `flet.version` — +switch to looking the release up from the new registry. + +Code before migration: + +```python +import flet.version + +print(f"Bundled Pyodide: {flet.version.pyodide_version}") +``` + +Code after migration: + +```python +from flet_cli.utils.python_versions import get_supported_python_versions + +for release in get_supported_python_versions(): + print(f" {release.short} → Pyodide {release.pyodide}") +``` + +If you only need the version that `flet build` is about to bundle, resolve +the Python release the same way the CLI does: + +```python +from flet_cli.utils.python_versions import ( + get_default_python_version, + get_release, +) + +release = get_release(get_default_python_version()) +print(f"Default Pyodide: {release.pyodide}") +``` + +## Timeline + +- Removed in: `0.86.0` + +## References + +- API documentation: [Choosing a Python version](/docs/publish#choosing-a-python-version) +- Issues and PRs: [#6577](https://github.com/flet-dev/flet/pull/6577) +- Release notes: [Flet 0.86.0](../release-notes.md) diff --git a/website/package.json b/website/package.json index ce3c2442c6..1efad5ac9a 100644 --- a/website/package.json +++ b/website/package.json @@ -12,11 +12,11 @@ }, "dependencies": { "@docsearch/docusaurus-adapter": "^4.6.2", - "@docusaurus/core": "^3.10.0", - "@docusaurus/faster": "^3.10.0", - "@docusaurus/plugin-client-redirects": "^3.10.0", - "@docusaurus/preset-classic": "^3.10.0", - "@docusaurus/theme-mermaid": "^3.10.0", + "@docusaurus/core": "^3.10.1", + "@docusaurus/faster": "^3.10.1", + "@docusaurus/plugin-client-redirects": "^3.10.1", + "@docusaurus/preset-classic": "^3.10.1", + "@docusaurus/theme-mermaid": "^3.10.1", "@hcaptcha/react-hcaptcha": "^1.0.0", "@mdx-js/react": "^3.0.0", "clsx": "^1.1.1", diff --git a/website/sidebars.yml b/website/sidebars.yml index cb7baeed73..361f2e80af 100644 --- a/website/sidebars.yml +++ b/website/sidebars.yml @@ -69,6 +69,10 @@ docs: Breaking changes and deprecations: _index: updates/breaking-changes/index.md v0.86.0: + Default bundled Python version is now 3.14: updates/breaking-changes/v0-86-0-default-bundled-python-3-14.md + App and packages are compiled to .pyc by default: updates/breaking-changes/v0-86-0-compile-on-by-default.md + flet.version.pyodide_version and PYODIDE_VERSION removed: updates/breaking-changes/v0-86-0-removed-pyodide-version-export.md + Flet protocol framing upgraded for DataChannel support: updates/breaking-changes/v0-86-0-data-channel-protocol-upgrade.md flet build --clear-cache flag deprecated: updates/breaking-changes/v0-86-0-deprecated-clear-cache-flag.md v0.85.0: Deprecated spacing and border helper functions removed: updates/breaking-changes/v0-85-0-removed-spacing-border-helpers.md diff --git a/website/yarn.lock b/website/yarn.lock index 2826c31389..138e853b70 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -2241,9 +2241,9 @@ __metadata: languageName: node linkType: hard -"@docusaurus/babel@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/babel@npm:3.10.0" +"@docusaurus/babel@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/babel@npm:3.10.1" dependencies: "@babel/core": "npm:^7.25.9" "@babel/generator": "npm:^7.25.9" @@ -2254,12 +2254,12 @@ __metadata: "@babel/preset-typescript": "npm:^7.25.9" "@babel/runtime": "npm:^7.25.9" "@babel/traverse": "npm:^7.25.9" - "@docusaurus/logger": "npm:3.10.0" - "@docusaurus/utils": "npm:3.10.0" + "@docusaurus/logger": "npm:3.10.1" + "@docusaurus/utils": "npm:3.10.1" babel-plugin-dynamic-import-node: "npm:^2.3.3" fs-extra: "npm:^11.1.1" tslib: "npm:^2.6.0" - checksum: 10c0/d79bd3e8805036e35b09ec4c7ebbf6060b07c1a375b3d27c727cc3122d25c9fddd98d450a22b70eaa9f7f3be0a7c3c5bd36819a7c1abcde4c7b4d3356248a9e3 + checksum: 10c0/3f6d2fdd6bc9c3a47683c1517e2afc7bca4b95d7b1817d68e91c1c2eaf944971d925ec03f2de8d88d40f97a0d88a1415cb2b9c64ac335c7cb6e37e6258c6f97a languageName: node linkType: hard @@ -2286,16 +2286,16 @@ __metadata: languageName: node linkType: hard -"@docusaurus/bundler@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/bundler@npm:3.10.0" +"@docusaurus/bundler@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/bundler@npm:3.10.1" dependencies: "@babel/core": "npm:^7.25.9" - "@docusaurus/babel": "npm:3.10.0" - "@docusaurus/cssnano-preset": "npm:3.10.0" - "@docusaurus/logger": "npm:3.10.0" - "@docusaurus/types": "npm:3.10.0" - "@docusaurus/utils": "npm:3.10.0" + "@docusaurus/babel": "npm:3.10.1" + "@docusaurus/cssnano-preset": "npm:3.10.1" + "@docusaurus/logger": "npm:3.10.1" + "@docusaurus/types": "npm:3.10.1" + "@docusaurus/utils": "npm:3.10.1" babel-loader: "npm:^9.2.1" clean-css: "npm:^5.3.3" copy-webpack-plugin: "npm:^11.0.0" @@ -2313,13 +2313,13 @@ __metadata: tslib: "npm:^2.6.0" url-loader: "npm:^4.1.1" webpack: "npm:^5.95.0" - webpackbar: "npm:^6.0.1" + webpackbar: "npm:^7.0.0" peerDependencies: "@docusaurus/faster": "*" peerDependenciesMeta: "@docusaurus/faster": optional: true - checksum: 10c0/49af1eba5e45126e972f943148b891c9e167e4510e6f349060ef210c648f28b5ee6344280e1ade0c2e1317bdd165ed3615aa71f95e91bd11e00a7dbb6795a0e3 + checksum: 10c0/20655bd64a5716cc3603d9097e7ca3d2526ccd7265ce3e9d23dfc9679faa08752a5604b98ba2b47eea5e183a3c4f0e383300899ae2f3897513493ec97a6beab2 languageName: node linkType: hard @@ -2360,17 +2360,17 @@ __metadata: languageName: node linkType: hard -"@docusaurus/core@npm:3.10.0, @docusaurus/core@npm:^3.10.0": - version: 3.10.0 - resolution: "@docusaurus/core@npm:3.10.0" - dependencies: - "@docusaurus/babel": "npm:3.10.0" - "@docusaurus/bundler": "npm:3.10.0" - "@docusaurus/logger": "npm:3.10.0" - "@docusaurus/mdx-loader": "npm:3.10.0" - "@docusaurus/utils": "npm:3.10.0" - "@docusaurus/utils-common": "npm:3.10.0" - "@docusaurus/utils-validation": "npm:3.10.0" +"@docusaurus/core@npm:3.10.1, @docusaurus/core@npm:^3.10.1": + version: 3.10.1 + resolution: "@docusaurus/core@npm:3.10.1" + dependencies: + "@docusaurus/babel": "npm:3.10.1" + "@docusaurus/bundler": "npm:3.10.1" + "@docusaurus/logger": "npm:3.10.1" + "@docusaurus/mdx-loader": "npm:3.10.1" + "@docusaurus/utils": "npm:3.10.1" + "@docusaurus/utils-common": "npm:3.10.1" + "@docusaurus/utils-validation": "npm:3.10.1" boxen: "npm:^6.2.1" chalk: "npm:^4.1.2" chokidar: "npm:^3.5.3" @@ -2416,7 +2416,7 @@ __metadata: optional: true bin: docusaurus: bin/docusaurus.mjs - checksum: 10c0/2a00cd5f1a22a737d37d127f5e5e6aee3ed51563884136fc76d2fa97cb71a7d577e28959f25ec2065c0e232efc003def1d6db94fcee0533063de87484bb39c86 + checksum: 10c0/006d9f57357c3196d0c33f7c5d0ce33b9f032358ed070b8ce212186169c356847cf768b3ac95b54433815c0076eda2eb94805a64bf4b2f5e47376556164e5362 languageName: node linkType: hard @@ -2476,15 +2476,15 @@ __metadata: languageName: node linkType: hard -"@docusaurus/cssnano-preset@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/cssnano-preset@npm:3.10.0" +"@docusaurus/cssnano-preset@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/cssnano-preset@npm:3.10.1" dependencies: cssnano-preset-advanced: "npm:^6.1.2" postcss: "npm:^8.5.4" postcss-sort-media-queries: "npm:^5.2.0" tslib: "npm:^2.6.0" - checksum: 10c0/635df6b05241f73b333b3d7d451d37ec56d7982a8c430afc2e8e8cf7c9e506b499b64d6bba14ccdf79b8afe84452d159516897741aa2fa838194964574da8881 + checksum: 10c0/549cf594fd9cfddd2f57bd177896c6bde8cc3821f7f07e7573d55d4c5841d7eb90d38c1b888b81479ffe33f0015b848c031ad4185f54feedf8ae2f1e2269e2ce languageName: node linkType: hard @@ -2500,11 +2500,11 @@ __metadata: languageName: node linkType: hard -"@docusaurus/faster@npm:^3.10.0": - version: 3.10.0 - resolution: "@docusaurus/faster@npm:3.10.0" +"@docusaurus/faster@npm:^3.10.1": + version: 3.10.1 + resolution: "@docusaurus/faster@npm:3.10.1" dependencies: - "@docusaurus/types": "npm:3.10.0" + "@docusaurus/types": "npm:3.10.1" "@rspack/core": "npm:^1.7.10" "@swc/core": "npm:^1.7.39" "@swc/html": "npm:^1.13.5" @@ -2516,17 +2516,17 @@ __metadata: webpack: "npm:^5.95.0" peerDependencies: "@docusaurus/types": "*" - checksum: 10c0/9e2b1b19a67443c23eceda80606a0e305a586addf991724b923f7756cfdced1a0d6d64426a1790fa81bbc0032ab56041823fe4a694d2392b0ef4ad85dc4089e8 + checksum: 10c0/98d8ca36cd4bcd775be37109c8cd3117ff8ba62446ec50b2901bca7653fbbf690ec9aa494524a27c17744a1744a3e520804100aba014e7aa5621ef3df4679359 languageName: node linkType: hard -"@docusaurus/logger@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/logger@npm:3.10.0" +"@docusaurus/logger@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/logger@npm:3.10.1" dependencies: chalk: "npm:^4.1.2" tslib: "npm:^2.6.0" - checksum: 10c0/f9bc2b7037fb7dff8a5aba06807e4f9601e422b91d0bb7e462ecdb33d71e1c9ee3d9dfb5c37af66f6f35c43310e461857af0dda96531928af3c22678fa77ec18 + checksum: 10c0/c78c676de0cf11ba5737abe8d13ebb67c4fdd8019ac8512ee18b34c27fdd5aaf32b703da3596271592be8615094507754791ac16587a24146f3830e1558a24c3 languageName: node linkType: hard @@ -2540,13 +2540,13 @@ __metadata: languageName: node linkType: hard -"@docusaurus/mdx-loader@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/mdx-loader@npm:3.10.0" +"@docusaurus/mdx-loader@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/mdx-loader@npm:3.10.1" dependencies: - "@docusaurus/logger": "npm:3.10.0" - "@docusaurus/utils": "npm:3.10.0" - "@docusaurus/utils-validation": "npm:3.10.0" + "@docusaurus/logger": "npm:3.10.1" + "@docusaurus/utils": "npm:3.10.1" + "@docusaurus/utils-validation": "npm:3.10.1" "@mdx-js/mdx": "npm:^3.0.0" "@slorber/remark-comment": "npm:^1.0.0" escape-html: "npm:^1.0.3" @@ -2571,7 +2571,7 @@ __metadata: peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - checksum: 10c0/0b94f20398a2fd39e54215895d2607d277d0cf3a80728adbbadcbf2443063e8e1082929242ccdc4ebe393c6c4010a5ccdecf6f2a8478d90b20c74d032940d33a + checksum: 10c0/2764e3e1a6fc4856746aacd627d43a403cbad87d6ec7400d30092197f36c028207cc5d267e0af2ce34df31f0a68bb2f082bbd5d308d14bedc0d874bc95a89c7b languageName: node linkType: hard @@ -2610,11 +2610,11 @@ __metadata: languageName: node linkType: hard -"@docusaurus/module-type-aliases@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/module-type-aliases@npm:3.10.0" +"@docusaurus/module-type-aliases@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/module-type-aliases@npm:3.10.1" dependencies: - "@docusaurus/types": "npm:3.10.0" + "@docusaurus/types": "npm:3.10.1" "@types/history": "npm:^4.7.11" "@types/react": "npm:*" "@types/react-router-config": "npm:*" @@ -2624,7 +2624,7 @@ __metadata: peerDependencies: react: "*" react-dom: "*" - checksum: 10c0/61952050bef257a0999db849a328655a4141d31b8d4fa4d54828da7ee8f710d7e592081a150c8b9750640bcaf78f3b7ca7165aefbcc0048c328407d582fe21b8 + checksum: 10c0/837faf66e24b9b0e2d2d276956f00cf5395a752682396013876935b6b0275b573d4cc3e9667a5110f9077c5943ddd1f47b462217a8418a5e6bdd013de8afd918 languageName: node linkType: hard @@ -2646,15 +2646,15 @@ __metadata: languageName: node linkType: hard -"@docusaurus/plugin-client-redirects@npm:^3.10.0": - version: 3.10.0 - resolution: "@docusaurus/plugin-client-redirects@npm:3.10.0" +"@docusaurus/plugin-client-redirects@npm:^3.10.1": + version: 3.10.1 + resolution: "@docusaurus/plugin-client-redirects@npm:3.10.1" dependencies: - "@docusaurus/core": "npm:3.10.0" - "@docusaurus/logger": "npm:3.10.0" - "@docusaurus/utils": "npm:3.10.0" - "@docusaurus/utils-common": "npm:3.10.0" - "@docusaurus/utils-validation": "npm:3.10.0" + "@docusaurus/core": "npm:3.10.1" + "@docusaurus/logger": "npm:3.10.1" + "@docusaurus/utils": "npm:3.10.1" + "@docusaurus/utils-common": "npm:3.10.1" + "@docusaurus/utils-validation": "npm:3.10.1" eta: "npm:^2.2.0" fs-extra: "npm:^11.1.1" lodash: "npm:^4.17.21" @@ -2662,22 +2662,22 @@ __metadata: peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - checksum: 10c0/06dced23c81d0008a9b0856cad74b76d20d93f8191bc6484770f23473e128e46524442f6f7569d2df9d1c745bea6d2c8d18a70834395bc88335b233febb314fe + checksum: 10c0/0bcd7387b6ae3132ab76c7715d440bb6b769ab509fa241720a90575b01541eefe3cd47281da812c546db0e31cffc002c4fb8629430fd1191e5d6f27ef2037483 languageName: node linkType: hard -"@docusaurus/plugin-content-blog@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/plugin-content-blog@npm:3.10.0" - dependencies: - "@docusaurus/core": "npm:3.10.0" - "@docusaurus/logger": "npm:3.10.0" - "@docusaurus/mdx-loader": "npm:3.10.0" - "@docusaurus/theme-common": "npm:3.10.0" - "@docusaurus/types": "npm:3.10.0" - "@docusaurus/utils": "npm:3.10.0" - "@docusaurus/utils-common": "npm:3.10.0" - "@docusaurus/utils-validation": "npm:3.10.0" +"@docusaurus/plugin-content-blog@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/plugin-content-blog@npm:3.10.1" + dependencies: + "@docusaurus/core": "npm:3.10.1" + "@docusaurus/logger": "npm:3.10.1" + "@docusaurus/mdx-loader": "npm:3.10.1" + "@docusaurus/theme-common": "npm:3.10.1" + "@docusaurus/types": "npm:3.10.1" + "@docusaurus/utils": "npm:3.10.1" + "@docusaurus/utils-common": "npm:3.10.1" + "@docusaurus/utils-validation": "npm:3.10.1" cheerio: "npm:1.0.0-rc.12" combine-promises: "npm:^1.1.0" feed: "npm:^4.2.2" @@ -2693,23 +2693,23 @@ __metadata: "@docusaurus/plugin-content-docs": "*" react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - checksum: 10c0/80295c4d217c45d2685d71e3e898e4e67715ce3ecf684063927e0f9c771a2156af2aefb813b61ed33d8a14bc0dbc820da9cd745b32fe4ef5baa03091165b3542 + checksum: 10c0/5c6d912bbcbbc9efa5c81e256f4074e70980c85623f5fd72a344b90bd5f23ed2c03030d8d0cb1ce38bdb2263fdf74ac3e7801a66050184ed11005adc5f49bcb2 languageName: node linkType: hard -"@docusaurus/plugin-content-docs@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/plugin-content-docs@npm:3.10.0" - dependencies: - "@docusaurus/core": "npm:3.10.0" - "@docusaurus/logger": "npm:3.10.0" - "@docusaurus/mdx-loader": "npm:3.10.0" - "@docusaurus/module-type-aliases": "npm:3.10.0" - "@docusaurus/theme-common": "npm:3.10.0" - "@docusaurus/types": "npm:3.10.0" - "@docusaurus/utils": "npm:3.10.0" - "@docusaurus/utils-common": "npm:3.10.0" - "@docusaurus/utils-validation": "npm:3.10.0" +"@docusaurus/plugin-content-docs@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/plugin-content-docs@npm:3.10.1" + dependencies: + "@docusaurus/core": "npm:3.10.1" + "@docusaurus/logger": "npm:3.10.1" + "@docusaurus/mdx-loader": "npm:3.10.1" + "@docusaurus/module-type-aliases": "npm:3.10.1" + "@docusaurus/theme-common": "npm:3.10.1" + "@docusaurus/types": "npm:3.10.1" + "@docusaurus/utils": "npm:3.10.1" + "@docusaurus/utils-common": "npm:3.10.1" + "@docusaurus/utils-validation": "npm:3.10.1" "@types/react-router-config": "npm:^5.0.7" combine-promises: "npm:^1.1.0" fs-extra: "npm:^11.1.1" @@ -2722,7 +2722,7 @@ __metadata: peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - checksum: 10c0/d1d61c85363231216e7f02731806c1519804c14b1a59bab84c386f4dfb45433081ed516cca42d8d891b9855a9ec996d53fe1a7624474a70d64515e7205beb791 + checksum: 10c0/a9d78f6979cc64aef1210f51979f2ed5100ce4a49ba266386e35e9109e4415e66bd9a9448696078f247d204949a29197faa9396bd1f014c88fcdd1aa1e98a3f8 languageName: node linkType: hard @@ -2755,129 +2755,129 @@ __metadata: languageName: node linkType: hard -"@docusaurus/plugin-content-pages@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/plugin-content-pages@npm:3.10.0" +"@docusaurus/plugin-content-pages@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/plugin-content-pages@npm:3.10.1" dependencies: - "@docusaurus/core": "npm:3.10.0" - "@docusaurus/mdx-loader": "npm:3.10.0" - "@docusaurus/types": "npm:3.10.0" - "@docusaurus/utils": "npm:3.10.0" - "@docusaurus/utils-validation": "npm:3.10.0" + "@docusaurus/core": "npm:3.10.1" + "@docusaurus/mdx-loader": "npm:3.10.1" + "@docusaurus/types": "npm:3.10.1" + "@docusaurus/utils": "npm:3.10.1" + "@docusaurus/utils-validation": "npm:3.10.1" fs-extra: "npm:^11.1.1" tslib: "npm:^2.6.0" webpack: "npm:^5.88.1" peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - checksum: 10c0/780bf847a37a2bd7732870f2f8e7395aa82c0f9cba61353225fe6c1abfe48b1403b21f2ad67983db0f0712b01be277796e8d4d51d16e082447c269fe5afadb6c + checksum: 10c0/00dab7af101b0607746d820c399bc3062279165b1b79009f2c6c8439a627818748da52f43eee0a4a874923707c89c9fce17aab72a7c88d298fa36b6efc0bacc7 languageName: node linkType: hard -"@docusaurus/plugin-css-cascade-layers@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/plugin-css-cascade-layers@npm:3.10.0" +"@docusaurus/plugin-css-cascade-layers@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/plugin-css-cascade-layers@npm:3.10.1" dependencies: - "@docusaurus/core": "npm:3.10.0" - "@docusaurus/types": "npm:3.10.0" - "@docusaurus/utils": "npm:3.10.0" - "@docusaurus/utils-validation": "npm:3.10.0" + "@docusaurus/core": "npm:3.10.1" + "@docusaurus/types": "npm:3.10.1" + "@docusaurus/utils": "npm:3.10.1" + "@docusaurus/utils-validation": "npm:3.10.1" tslib: "npm:^2.6.0" - checksum: 10c0/ebbfadc70293ff30878f263a166cd0c1e0bea24067acfc8ccb5d45adb9cc653c753fa9a27d874cd5e7855e2f7e5a35f1d337f07b6b28edabc77524f3533f47ea + checksum: 10c0/fd11a7488029226276bbdbeb1de4035ed425b8d7be0fbad3d5a0aa3a18646064f1930835bfc951837bfc813873548e1a8b23d9f52cb20120e1239a1d93128d79 languageName: node linkType: hard -"@docusaurus/plugin-debug@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/plugin-debug@npm:3.10.0" +"@docusaurus/plugin-debug@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/plugin-debug@npm:3.10.1" dependencies: - "@docusaurus/core": "npm:3.10.0" - "@docusaurus/types": "npm:3.10.0" - "@docusaurus/utils": "npm:3.10.0" + "@docusaurus/core": "npm:3.10.1" + "@docusaurus/types": "npm:3.10.1" + "@docusaurus/utils": "npm:3.10.1" fs-extra: "npm:^11.1.1" react-json-view-lite: "npm:^2.3.0" tslib: "npm:^2.6.0" peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - checksum: 10c0/575c364dcd2595928ebbc8ce6e90113e6bdcc2658ae59f3ddcd0fa2699880a81648765dc7083058bcc957bafd0f7e116c61c62e0cb6b678af97f7e719b5d2db7 + checksum: 10c0/d2978e40e83c1c4fd904dc3bc62ef1b6d52cf28e391e9dacad40712d7b14ce9ad22f0ad710631021066c296d10c364621b2c11a186694d8f5c6e59f35043f99a languageName: node linkType: hard -"@docusaurus/plugin-google-analytics@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/plugin-google-analytics@npm:3.10.0" +"@docusaurus/plugin-google-analytics@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/plugin-google-analytics@npm:3.10.1" dependencies: - "@docusaurus/core": "npm:3.10.0" - "@docusaurus/types": "npm:3.10.0" - "@docusaurus/utils-validation": "npm:3.10.0" + "@docusaurus/core": "npm:3.10.1" + "@docusaurus/types": "npm:3.10.1" + "@docusaurus/utils-validation": "npm:3.10.1" tslib: "npm:^2.6.0" peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - checksum: 10c0/f3814d3ec0c7e2040ac5f3a21a9e1dbb19d58af5a1096fe8376a8661fac92e9e77d3d48742ed7dfb0a1e635360bf1a4e2dd456b5d9d8746e490a250f9b7da097 + checksum: 10c0/c9aff3f3467d3e76a1dd8e1518e8a11b0d488b1761f0302a6cc9fdb94635b6e5673b3d993434205dc65cd6eb9677f795157887d508f01ba175c3f5e411aab2e6 languageName: node linkType: hard -"@docusaurus/plugin-google-gtag@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/plugin-google-gtag@npm:3.10.0" +"@docusaurus/plugin-google-gtag@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/plugin-google-gtag@npm:3.10.1" dependencies: - "@docusaurus/core": "npm:3.10.0" - "@docusaurus/types": "npm:3.10.0" - "@docusaurus/utils-validation": "npm:3.10.0" + "@docusaurus/core": "npm:3.10.1" + "@docusaurus/types": "npm:3.10.1" + "@docusaurus/utils-validation": "npm:3.10.1" "@types/gtag.js": "npm:^0.0.20" tslib: "npm:^2.6.0" peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - checksum: 10c0/31739d936f9ffa4c500d518816b17ac4f2ba2e75e20e5a6708eb2ed5d488465146b5a632899ab894cf8d8233306d212ac79d89c4c6a26c45f6dd15d31638444d + checksum: 10c0/6792713f813cb06dcc54ece92ac81faed01017a71079726f97adade1fa2b6665626369c788e108dee9c02e0cbfc4cc71a2e87db5d3e43f47944e7b0921bb71db languageName: node linkType: hard -"@docusaurus/plugin-google-tag-manager@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/plugin-google-tag-manager@npm:3.10.0" +"@docusaurus/plugin-google-tag-manager@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/plugin-google-tag-manager@npm:3.10.1" dependencies: - "@docusaurus/core": "npm:3.10.0" - "@docusaurus/types": "npm:3.10.0" - "@docusaurus/utils-validation": "npm:3.10.0" + "@docusaurus/core": "npm:3.10.1" + "@docusaurus/types": "npm:3.10.1" + "@docusaurus/utils-validation": "npm:3.10.1" tslib: "npm:^2.6.0" peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - checksum: 10c0/6937bd384653ef938a5b66cf56bce458cc39c33aa35a6ebc43139abb393cfc7cf7865dcf6af60a2dbf65ebb06d40303530430a52e30a119b9c3d7419e53f3a6d + checksum: 10c0/a1a01de5f876d63fec022d312c711525523ee380af3382e35d1953a577450da36039dfcd1ff7f0af0cd189cc14d268c64276def573bdc70d091db2cad56edf9f languageName: node linkType: hard -"@docusaurus/plugin-sitemap@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/plugin-sitemap@npm:3.10.0" - dependencies: - "@docusaurus/core": "npm:3.10.0" - "@docusaurus/logger": "npm:3.10.0" - "@docusaurus/types": "npm:3.10.0" - "@docusaurus/utils": "npm:3.10.0" - "@docusaurus/utils-common": "npm:3.10.0" - "@docusaurus/utils-validation": "npm:3.10.0" +"@docusaurus/plugin-sitemap@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/plugin-sitemap@npm:3.10.1" + dependencies: + "@docusaurus/core": "npm:3.10.1" + "@docusaurus/logger": "npm:3.10.1" + "@docusaurus/types": "npm:3.10.1" + "@docusaurus/utils": "npm:3.10.1" + "@docusaurus/utils-common": "npm:3.10.1" + "@docusaurus/utils-validation": "npm:3.10.1" fs-extra: "npm:^11.1.1" sitemap: "npm:^7.1.1" tslib: "npm:^2.6.0" peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - checksum: 10c0/a0538da02713caaf844cd3b489a360408bb6868ceefbe3e51e7d02223919e8349b219aac1d111e258a29be5eeaea53d712448abf9d7d860f0af89b12d6652a86 + checksum: 10c0/22c04be000a1577f9a0c169fb67ff0d4deaf618aa2a3839336cfaed667558f1db3ada5bed23ee7245596a307bbddbed62ede8e342120aa8efebe0b9d35f4da75 languageName: node linkType: hard -"@docusaurus/plugin-svgr@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/plugin-svgr@npm:3.10.0" +"@docusaurus/plugin-svgr@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/plugin-svgr@npm:3.10.1" dependencies: - "@docusaurus/core": "npm:3.10.0" - "@docusaurus/types": "npm:3.10.0" - "@docusaurus/utils": "npm:3.10.0" - "@docusaurus/utils-validation": "npm:3.10.0" + "@docusaurus/core": "npm:3.10.1" + "@docusaurus/types": "npm:3.10.1" + "@docusaurus/utils": "npm:3.10.1" + "@docusaurus/utils-validation": "npm:3.10.1" "@svgr/core": "npm:8.1.0" "@svgr/webpack": "npm:^8.1.0" tslib: "npm:^2.6.0" @@ -2885,53 +2885,53 @@ __metadata: peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - checksum: 10c0/31a049eaf82c80296b0dc4d7d7bd292bda13dbcf9f07943db4cd2b721276185cb95f6058c406ff4602f4ff408f0fb042f3ade8c8e1d009054ecfa55d99960a88 - languageName: node - linkType: hard - -"@docusaurus/preset-classic@npm:^3.10.0": - version: 3.10.0 - resolution: "@docusaurus/preset-classic@npm:3.10.0" - dependencies: - "@docusaurus/core": "npm:3.10.0" - "@docusaurus/plugin-content-blog": "npm:3.10.0" - "@docusaurus/plugin-content-docs": "npm:3.10.0" - "@docusaurus/plugin-content-pages": "npm:3.10.0" - "@docusaurus/plugin-css-cascade-layers": "npm:3.10.0" - "@docusaurus/plugin-debug": "npm:3.10.0" - "@docusaurus/plugin-google-analytics": "npm:3.10.0" - "@docusaurus/plugin-google-gtag": "npm:3.10.0" - "@docusaurus/plugin-google-tag-manager": "npm:3.10.0" - "@docusaurus/plugin-sitemap": "npm:3.10.0" - "@docusaurus/plugin-svgr": "npm:3.10.0" - "@docusaurus/theme-classic": "npm:3.10.0" - "@docusaurus/theme-common": "npm:3.10.0" - "@docusaurus/theme-search-algolia": "npm:3.10.0" - "@docusaurus/types": "npm:3.10.0" + checksum: 10c0/d866efe42351d1a66febacd6c33753f5ace2056fe86259365408edd354fb67994208a4e2c9939a921c2a20cb11b64fdd3584d34dde8d0329a63a55c9a74c488f + languageName: node + linkType: hard + +"@docusaurus/preset-classic@npm:^3.10.1": + version: 3.10.1 + resolution: "@docusaurus/preset-classic@npm:3.10.1" + dependencies: + "@docusaurus/core": "npm:3.10.1" + "@docusaurus/plugin-content-blog": "npm:3.10.1" + "@docusaurus/plugin-content-docs": "npm:3.10.1" + "@docusaurus/plugin-content-pages": "npm:3.10.1" + "@docusaurus/plugin-css-cascade-layers": "npm:3.10.1" + "@docusaurus/plugin-debug": "npm:3.10.1" + "@docusaurus/plugin-google-analytics": "npm:3.10.1" + "@docusaurus/plugin-google-gtag": "npm:3.10.1" + "@docusaurus/plugin-google-tag-manager": "npm:3.10.1" + "@docusaurus/plugin-sitemap": "npm:3.10.1" + "@docusaurus/plugin-svgr": "npm:3.10.1" + "@docusaurus/theme-classic": "npm:3.10.1" + "@docusaurus/theme-common": "npm:3.10.1" + "@docusaurus/theme-search-algolia": "npm:3.10.1" + "@docusaurus/types": "npm:3.10.1" peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - checksum: 10c0/c7d9ce9b76f309b65a3cdba6702f49adb4c518da3e3c4a4f745c5ad659cab9a9d1bf3841d49817fa4a1e3d226c2f683d6e263bb36d9d9bb6143f9fc4d36add42 - languageName: node - linkType: hard - -"@docusaurus/theme-classic@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/theme-classic@npm:3.10.0" - dependencies: - "@docusaurus/core": "npm:3.10.0" - "@docusaurus/logger": "npm:3.10.0" - "@docusaurus/mdx-loader": "npm:3.10.0" - "@docusaurus/module-type-aliases": "npm:3.10.0" - "@docusaurus/plugin-content-blog": "npm:3.10.0" - "@docusaurus/plugin-content-docs": "npm:3.10.0" - "@docusaurus/plugin-content-pages": "npm:3.10.0" - "@docusaurus/theme-common": "npm:3.10.0" - "@docusaurus/theme-translations": "npm:3.10.0" - "@docusaurus/types": "npm:3.10.0" - "@docusaurus/utils": "npm:3.10.0" - "@docusaurus/utils-common": "npm:3.10.0" - "@docusaurus/utils-validation": "npm:3.10.0" + checksum: 10c0/524675229e33f24f678ca104eab1b6328ca31c2572e5cb2a598a330bd990a8d50eb1929aaef9031c37827a22ddaf6bb19484b75ac2a5b0936f6322d951f33559 + languageName: node + linkType: hard + +"@docusaurus/theme-classic@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/theme-classic@npm:3.10.1" + dependencies: + "@docusaurus/core": "npm:3.10.1" + "@docusaurus/logger": "npm:3.10.1" + "@docusaurus/mdx-loader": "npm:3.10.1" + "@docusaurus/module-type-aliases": "npm:3.10.1" + "@docusaurus/plugin-content-blog": "npm:3.10.1" + "@docusaurus/plugin-content-docs": "npm:3.10.1" + "@docusaurus/plugin-content-pages": "npm:3.10.1" + "@docusaurus/theme-common": "npm:3.10.1" + "@docusaurus/theme-translations": "npm:3.10.1" + "@docusaurus/types": "npm:3.10.1" + "@docusaurus/utils": "npm:3.10.1" + "@docusaurus/utils-common": "npm:3.10.1" + "@docusaurus/utils-validation": "npm:3.10.1" "@mdx-js/react": "npm:^3.0.0" clsx: "npm:^2.0.0" copy-text-to-clipboard: "npm:^3.2.0" @@ -2948,18 +2948,18 @@ __metadata: peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - checksum: 10c0/920df8c75701cd462cc414440b446157b6c831432bb2fe0e506268a5a72ef7fefe58568d8fb12bfc61845e8809f5fe6900314f39e9867a0aedabd184cbaa05b9 + checksum: 10c0/55be80ca9a1880c0a923c519243233bb52025e4a0ae312907c9df52b8ab3594819da164a71377e6ed5b02eef740496eff006da41462603195c0284b977515ceb languageName: node linkType: hard -"@docusaurus/theme-common@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/theme-common@npm:3.10.0" +"@docusaurus/theme-common@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/theme-common@npm:3.10.1" dependencies: - "@docusaurus/mdx-loader": "npm:3.10.0" - "@docusaurus/module-type-aliases": "npm:3.10.0" - "@docusaurus/utils": "npm:3.10.0" - "@docusaurus/utils-common": "npm:3.10.0" + "@docusaurus/mdx-loader": "npm:3.10.1" + "@docusaurus/module-type-aliases": "npm:3.10.1" + "@docusaurus/utils": "npm:3.10.1" + "@docusaurus/utils-common": "npm:3.10.1" "@types/history": "npm:^4.7.11" "@types/react": "npm:*" "@types/react-router-config": "npm:*" @@ -2972,7 +2972,7 @@ __metadata: "@docusaurus/plugin-content-docs": "*" react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - checksum: 10c0/16cda69e916adfc2cfdeea6940264c01d56e8b87e87fca887d7d28933712333b5b60ce60a64d505ddda8da2c6538b50f3aa4e16351e3d05df9f8e590b407be6e + checksum: 10c0/f66e25b6449e03b5c8812be407e80c1c9ffc87b07395273d009a40761302e6230ab357096de95b47f77aef1d7e4d0363220cca07b7fbdeb9679770e20e0ab7a4 languageName: node linkType: hard @@ -3000,15 +3000,15 @@ __metadata: languageName: node linkType: hard -"@docusaurus/theme-mermaid@npm:^3.10.0": - version: 3.10.0 - resolution: "@docusaurus/theme-mermaid@npm:3.10.0" +"@docusaurus/theme-mermaid@npm:^3.10.1": + version: 3.10.1 + resolution: "@docusaurus/theme-mermaid@npm:3.10.1" dependencies: - "@docusaurus/core": "npm:3.10.0" - "@docusaurus/module-type-aliases": "npm:3.10.0" - "@docusaurus/theme-common": "npm:3.10.0" - "@docusaurus/types": "npm:3.10.0" - "@docusaurus/utils-validation": "npm:3.10.0" + "@docusaurus/core": "npm:3.10.1" + "@docusaurus/module-type-aliases": "npm:3.10.1" + "@docusaurus/theme-common": "npm:3.10.1" + "@docusaurus/types": "npm:3.10.1" + "@docusaurus/utils-validation": "npm:3.10.1" mermaid: "npm:>=11.6.0" tslib: "npm:^2.6.0" peerDependencies: @@ -3018,23 +3018,23 @@ __metadata: peerDependenciesMeta: "@mermaid-js/layout-elk": optional: true - checksum: 10c0/2bf6c0b0c7a7a55f7a89e6af7abef5ce2ca8f2e3c4a5e5be5b99cc9d8043135253dd73a588f008d3a5abf4133a3cc631983153e60fa05236398a7a2bac3f3cf7 + checksum: 10c0/73839669eb3e69ec565a16853b6a8fa636f3fd94d6656976835a0304bb6e7402b5dcbd7808b8b158bb0f34e61fee1d835511d88f59f48fa613eb51532614992c languageName: node linkType: hard -"@docusaurus/theme-search-algolia@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/theme-search-algolia@npm:3.10.0" +"@docusaurus/theme-search-algolia@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/theme-search-algolia@npm:3.10.1" dependencies: "@algolia/autocomplete-core": "npm:^1.19.2" "@docsearch/react": "npm:^3.9.0 || ^4.3.2" - "@docusaurus/core": "npm:3.10.0" - "@docusaurus/logger": "npm:3.10.0" - "@docusaurus/plugin-content-docs": "npm:3.10.0" - "@docusaurus/theme-common": "npm:3.10.0" - "@docusaurus/theme-translations": "npm:3.10.0" - "@docusaurus/utils": "npm:3.10.0" - "@docusaurus/utils-validation": "npm:3.10.0" + "@docusaurus/core": "npm:3.10.1" + "@docusaurus/logger": "npm:3.10.1" + "@docusaurus/plugin-content-docs": "npm:3.10.1" + "@docusaurus/theme-common": "npm:3.10.1" + "@docusaurus/theme-translations": "npm:3.10.1" + "@docusaurus/utils": "npm:3.10.1" + "@docusaurus/utils-validation": "npm:3.10.1" algoliasearch: "npm:^5.37.0" algoliasearch-helper: "npm:^3.26.0" clsx: "npm:^2.0.0" @@ -3046,17 +3046,17 @@ __metadata: peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - checksum: 10c0/63dd5f7e99457a71f0eb7916e18fa421e3194018975a52e8d8bd197abfdf5f19d85348a8a7e0713bccac413e9d5b1cb54b9c69c28a868e0473c1cf0b806f3faa + checksum: 10c0/dd558ac0c50f2374b8285f463ec23e1996247f4436cd9147fd313689d02d9113214e0368c64fdc8ca106f10b6ef93589814980ea0121cb3583ca87feae4b19c3 languageName: node linkType: hard -"@docusaurus/theme-translations@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/theme-translations@npm:3.10.0" +"@docusaurus/theme-translations@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/theme-translations@npm:3.10.1" dependencies: fs-extra: "npm:^11.1.1" tslib: "npm:^2.6.0" - checksum: 10c0/62fa157763e2ad4d8c7afea0edebce895f85da5384c48222a1f697932716c550eeda34310d473643d037ae6d41720909174abf409971fcddd0eadb63daafced6 + checksum: 10c0/2a1c871e883a82c4c5071820dcdc3bbeea5a3571978d022c1d1b820ae8b676ea5dd173b9c17542a032b9a5ec7e06f5d8a3b9de31a1e1f474f527baccfb5f9deb languageName: node linkType: hard @@ -3070,9 +3070,9 @@ __metadata: languageName: node linkType: hard -"@docusaurus/types@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/types@npm:3.10.0" +"@docusaurus/types@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/types@npm:3.10.1" dependencies: "@mdx-js/mdx": "npm:^3.0.0" "@types/history": "npm:^4.7.11" @@ -3087,7 +3087,7 @@ __metadata: peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - checksum: 10c0/0d0f5f57bb82f190385a506192d882a5072e833af55a35cb5fb69048bb4258012eebe51448b8ace9d77d05d69a99d7fd2dcae25bb4babfa205abfbca222de8d5 + checksum: 10c0/63cc1d92ec775fb0e58f156b4edc7075612943a94d15d4648b60effcbc34c70a1092569d66d552878779b48d5111abe2fc154ba6a3bca2af0383550c9f9a015b languageName: node linkType: hard @@ -3112,13 +3112,13 @@ __metadata: languageName: node linkType: hard -"@docusaurus/utils-common@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/utils-common@npm:3.10.0" +"@docusaurus/utils-common@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/utils-common@npm:3.10.1" dependencies: - "@docusaurus/types": "npm:3.10.0" + "@docusaurus/types": "npm:3.10.1" tslib: "npm:^2.6.0" - checksum: 10c0/12e54b8e29d1d8d78f85598a154fc122f4d93bdd143b55fd7a474c2d9eab431bbf13ac61e008f1c4f34ffce76578fe95b441f6a6469a752d7396f9d9c000f6e4 + checksum: 10c0/1dde7a5c538a2cbe9eba46c9a3991a773e2d9d5b9c489cb010f9284c33cba7d494c1300557f850fa6a6e857747ca835b91f6036b02a726e13d296d7a65f8d8db languageName: node linkType: hard @@ -3132,19 +3132,19 @@ __metadata: languageName: node linkType: hard -"@docusaurus/utils-validation@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/utils-validation@npm:3.10.0" +"@docusaurus/utils-validation@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/utils-validation@npm:3.10.1" dependencies: - "@docusaurus/logger": "npm:3.10.0" - "@docusaurus/utils": "npm:3.10.0" - "@docusaurus/utils-common": "npm:3.10.0" + "@docusaurus/logger": "npm:3.10.1" + "@docusaurus/utils": "npm:3.10.1" + "@docusaurus/utils-common": "npm:3.10.1" fs-extra: "npm:^11.2.0" joi: "npm:^17.9.2" js-yaml: "npm:^4.1.0" lodash: "npm:^4.17.21" tslib: "npm:^2.6.0" - checksum: 10c0/ab1aee9c9b236d4c5247f33b245c016a2ef501ef154f5f5392a98e706d448ee60c32746b4c58e4954be24393eee6db06cb3192efa8df00343176c558fca33924 + checksum: 10c0/6368eb2a879fc1c4a507873689bd0a08257e2bc72a2ba53b3629d633d97d368168720fc504a54f96a0652982fc098f29675076e1fbcaf0244947645131a3d2f9 languageName: node linkType: hard @@ -3164,13 +3164,13 @@ __metadata: languageName: node linkType: hard -"@docusaurus/utils@npm:3.10.0": - version: 3.10.0 - resolution: "@docusaurus/utils@npm:3.10.0" +"@docusaurus/utils@npm:3.10.1": + version: 3.10.1 + resolution: "@docusaurus/utils@npm:3.10.1" dependencies: - "@docusaurus/logger": "npm:3.10.0" - "@docusaurus/types": "npm:3.10.0" - "@docusaurus/utils-common": "npm:3.10.0" + "@docusaurus/logger": "npm:3.10.1" + "@docusaurus/types": "npm:3.10.1" + "@docusaurus/utils-common": "npm:3.10.1" escape-string-regexp: "npm:^4.0.0" execa: "npm:^5.1.1" file-loader: "npm:^6.2.0" @@ -3189,7 +3189,7 @@ __metadata: url-loader: "npm:^4.1.1" utility-types: "npm:^3.10.0" webpack: "npm:^5.88.1" - checksum: 10c0/0f3488c38fbc985378f93f6573cf080559207ae367b0052df2ad42d667726ec766900db68184ec1746bcf4c38c9a1289d9f54fbd71a857dc592363996295afff + checksum: 10c0/97d51da30035ef34eced1dbc937f829309a8165a07a9c66d87c5deec03fb62150cbc70b2763ffdec0286b21f1be53f4011a036e4265cb971a892f0ab12172e0a languageName: node linkType: hard @@ -5946,6 +5946,13 @@ __metadata: languageName: node linkType: hard +"ansis@npm:^3.2.0": + version: 3.17.0 + resolution: "ansis@npm:3.17.0" + checksum: 10c0/d8fa94ca7bb91e7e5f8a7d323756aa075facce07c5d02ca883673e128b2873d16f93e0dec782f98f1eeb1f2b3b4b7b60dcf0ad98fb442e75054fe857988cc5cb + languageName: node + linkType: hard + "anymatch@npm:~3.1.2": version: 3.1.3 resolution: "anymatch@npm:3.1.3" @@ -8829,11 +8836,11 @@ __metadata: resolution: "flet-dev@workspace:." dependencies: "@docsearch/docusaurus-adapter": "npm:^4.6.2" - "@docusaurus/core": "npm:^3.10.0" - "@docusaurus/faster": "npm:^3.10.0" - "@docusaurus/plugin-client-redirects": "npm:^3.10.0" - "@docusaurus/preset-classic": "npm:^3.10.0" - "@docusaurus/theme-mermaid": "npm:^3.10.0" + "@docusaurus/core": "npm:^3.10.1" + "@docusaurus/faster": "npm:^3.10.1" + "@docusaurus/plugin-client-redirects": "npm:^3.10.1" + "@docusaurus/preset-classic": "npm:^3.10.1" + "@docusaurus/theme-mermaid": "npm:^3.10.1" "@hcaptcha/react-hcaptcha": "npm:^1.0.0" "@mdx-js/react": "npm:^3.0.0" clsx: "npm:^1.1.1" @@ -15543,6 +15550,26 @@ __metadata: languageName: node linkType: hard +"webpackbar@npm:^7.0.0": + version: 7.0.0 + resolution: "webpackbar@npm:7.0.0" + dependencies: + ansis: "npm:^3.2.0" + consola: "npm:^3.2.3" + pretty-time: "npm:^1.1.0" + std-env: "npm:^3.7.0" + peerDependencies: + "@rspack/core": "*" + webpack: 3 || 4 || 5 + peerDependenciesMeta: + "@rspack/core": + optional: true + webpack: + optional: true + checksum: 10c0/03ed85edca12af824319dfcd0fe5c3a90b9e3c86400a604a55589abe0a66a682033e7de027e89aae03652b6fb8ca7fd2831d86829179304ea3121f807808f7c6 + languageName: node + linkType: hard + "websocket-driver@npm:>=0.5.1, websocket-driver@npm:^0.7.4": version: 0.7.4 resolution: "websocket-driver@npm:0.7.4"