diff --git a/CHANGELOG.md b/CHANGELOG.md index 35bd3510eb..fdd6d0073c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### New features +* Add support for **Python `multiprocessing` in packaged Flet desktop apps** built with `flet build macos`, `flet build windows`, and `flet build linux`. `multiprocessing` APIs such as `Process`, `ProcessPoolExecutor`, the `spawn`/`forkserver` start methods, and the resource tracker now work in packaged desktop apps. Previously, worker processes re-executed `sys.executable`, which pointed to the app binary itself, causing each worker to launch another GUI app instance and hang. Flet desktop runners now detect CPython multiprocessing child command lines before Flutter starts and divert them to a headless embedded Python interpreter via `dart_bridge` 1.5.0+. The Python bootstrap also runs the app module as the real `sys.modules["__main__"]` with `python -m` semantics, so top-level worker functions in `main.py` can be pickled correctly. When using `multiprocessing`, your app must follow normal Python multiprocessing rules: guard `ft.run(...)` with `if __name__ == "__main__":`, define worker functions at module top level, and do not access Flet UI objects from worker processes. See the new [Multiprocessing](https://docs.flet.dev/cookbook/multiprocessing) cookbook page. Mobile platforms remain unsupported because iOS and Android do not allow apps to spawn arbitrary child processes ([#4283](https://github.com/flet-dev/flet/issues/4283), [#6577](https://github.com/flet-dev/flet/pull/6662)) by @ndonkoHenri. * 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.1), 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` >= 4.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. @@ -24,11 +25,11 @@ ### Breaking changes -* **App files now ship unpacked in a read-only bundle, and the storage directories were reworked** (requires `serious_python` >= 4.0.0, now pinned in the `flet build` template). Your Python sources ship **unpacked inside the app bundle** next to the stdlib/site-packages (no first-launch `app.zip` extraction) on macOS/iOS/Windows/Linux; on Android they ship as a *stored* `app.zip` asset unpacked once on first launch; web is unchanged. The app directory is now **read-only**, so the Python program's **working directory** moved to a writable, app-private data dir. `FLET_APP_STORAGE_DATA` now maps to the OS *application support* dir (a `data` subdir) instead of the user's Documents folder and is the cwd; `FLET_APP_STORAGE_TEMP` now points to the OS temp dir (was the cache dir) and a new `FLET_APP_STORAGE_CACHE` exposes the cache dir. `flet run` sets the dev cwd to a hidden, git-ignored `/.flet/storage/data`. Relative **reads** of bundled files (`open("seed.json")`) must move to `__file__`/`importlib.resources` or `assets/`. See the [app files unpacked / storage dirs](/docs/updates/breaking-changes/v0-86-0-app-files-unpacked-read-only-bundle) guide by @FeodorFitsner. +* **App files now ship unpacked in a read-only bundle, and the storage directories were reworked** (requires `serious_python` >= 4.0.0, now pinned in the `flet build` template). Your Python sources ship **unpacked inside the app bundle** next to the stdlib/site-packages (no first-launch `app.zip` extraction) on macOS/iOS/Windows/Linux; on Android they ship as a *stored* `app.zip` asset unpacked once on first launch; web is unchanged. The app directory is now **read-only**, so the Python program's **working directory** moved to a writable, app-private data dir. `FLET_APP_STORAGE_DATA` now maps to the OS *application support* dir (a `data` subdir) instead of the user's Documents folder and is the cwd; `FLET_APP_STORAGE_TEMP` now points to the OS temp dir (was the cache dir) and a new `FLET_APP_STORAGE_CACHE` exposes the cache dir. `flet run` sets the dev cwd to a hidden, git-ignored `/.flet/storage/data`. Relative **reads** of bundled files (`open("seed.json")`) must move to `__file__`/`importlib.resources` or `assets/`. See the [app files unpacked / storage dirs](/docs/updates/breaking-changes/v0-86-0/app-files-unpacked-read-only-bundle) guide by @FeodorFitsner. * `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. * Android builds now include only the ABIs the bundled Python supports, sourced per-version from python-build's manifest (`pythons..android_abis`) rather than hardcoded in flet. As of python-build `20260630`, `armeabi-v7a` (32-bit ARM) is published for **3.12, 3.13 and 3.14**, so all three build it by default; an explicit `--arch ` is validated against the selected Python's supported set and fails with a clear error otherwise ([#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. +* `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 diff --git a/sdk/python/examples/cookbook/multiprocessing/cancel_task.py b/sdk/python/examples/cookbook/multiprocessing/cancel_task.py new file mode 100644 index 0000000000..e7c284dbf9 --- /dev/null +++ b/sdk/python/examples/cookbook/multiprocessing/cancel_task.py @@ -0,0 +1,69 @@ +import multiprocessing +import time + +import flet as ft + + +def long_job(seconds: int) -> None: + """Burn CPU for roughly `seconds` seconds. + + Stands in for work you cannot interrupt from Python itself, e.g. a tight + loop inside a C extension library. Runs in a worker process, which is why + it can be cancelled at any point — threads can't be stopped like that. + """ + deadline = time.monotonic() + seconds + while time.monotonic() < deadline: + sum(range(1_000_000)) + + +def main(page: ft.Page): + process = None + + def watch(p: multiprocessing.Process): + """Wait (on a background thread) for the worker to end, then report + whether it finished on its own or was cancelled.""" + p.join() + if p.exitcode == 0: + status.value = "Process finished normally." + else: + # terminate() ends the child with a negative exit code (-SIGTERM). + status.value = f"Process was terminated (exit code {p.exitcode})." + start.disabled = False + cancel.disabled = True + page.update() + + def start_job(): + nonlocal process + process = multiprocessing.Process(target=long_job, args=(30,), daemon=True) + process.start() + status.value = f"Running in process {process.pid}…" + start.disabled = True + cancel.disabled = False + page.update() + page.run_thread(watch, process) + + def cancel_job(): + if process is not None and process.is_alive(): + process.terminate() # threads can't do this + + page.add( + ft.SafeArea( + content=ft.Column( + controls=[ + ft.Row( + controls=[ + start := ft.Button("Start 30s job", on_click=start_job), + cancel := ft.Button( + "Cancel", on_click=cancel_job, disabled=True + ), + ] + ), + status := ft.Text(), + ] + ) + ) + ) + + +if __name__ == "__main__": + ft.run(main) diff --git a/sdk/python/examples/cookbook/multiprocessing/parallel_sort.py b/sdk/python/examples/cookbook/multiprocessing/parallel_sort.py new file mode 100644 index 0000000000..2a9c8e4fa3 --- /dev/null +++ b/sdk/python/examples/cookbook/multiprocessing/parallel_sort.py @@ -0,0 +1,46 @@ +import random +from concurrent.futures import ProcessPoolExecutor, as_completed + +import flet as ft + + +def sort_chunk(chunk: list[float]) -> list[float]: + """Sort one chunk of data.""" + return sorted(chunk) + + +def main(page: ft.Page): + def run_sort(): + """Orchestrate the pool from a background thread, updating the UI + from the main process as results come in.""" + chunks = [[random.random() for _ in range(250_000)] for _ in range(8)] + completed = 0 + # Without max_workers, the pool sizes itself to the machine's CPUs. + with ProcessPoolExecutor() as pool: + futures = [pool.submit(sort_chunk, c) for c in chunks] + # as_completed() yields each future as soon as its worker is done. + for _ in as_completed(futures): + completed += 1 + progress.value = completed / len(futures) + status.value = f"Sorted {completed}/{len(futures)} chunks" + page.update() + status.value = "Done!" + page.update() + + page.add( + ft.SafeArea( + content=ft.Column( + controls=[ + ft.Button( + "Start sorting", on_click=lambda: page.run_thread(run_sort) + ), + status := ft.Text("Idle"), + progress := ft.ProgressBar(value=0, width=300), + ] + ) + ) + ) + + +if __name__ == "__main__": + ft.run(main) diff --git a/sdk/python/examples/cookbook/multiprocessing/persistent_worker.py b/sdk/python/examples/cookbook/multiprocessing/persistent_worker.py new file mode 100644 index 0000000000..3531c832c1 --- /dev/null +++ b/sdk/python/examples/cookbook/multiprocessing/persistent_worker.py @@ -0,0 +1,65 @@ +import multiprocessing +import time + +import flet as ft + + +def calc_worker(conn) -> None: + """Serve calculation jobs over the pipe until told to stop. + + Runs in a worker process that stays alive across jobs, so the process + startup cost is paid once. Expensive setup (loading models, opening + datasets, warming caches) could be done once here, before the loop, + and reused by every job. Receiving `None` is the shutdown signal. + """ + while (job := conn.recv()) is not None: + started = time.perf_counter() + result = sum(i * i for i in range(job)) + conn.send((result, time.perf_counter() - started)) + conn.close() + + +def main(page: ft.Page): + # One end of the pipe goes to the worker, the other stays with the UI. + parent_conn, child_conn = multiprocessing.Pipe() + worker = multiprocessing.Process( + target=calc_worker, args=(child_conn,), daemon=True + ) + worker.start() + + def submit(): + # One job in flight at a time: the button stays disabled until the + # worker replies (a bare Pipe is not multiplexed). + button.disabled = True + status.value = "Working…" + page.update() + page.run_thread(request) + + def request(): + """Send a job to the worker and wait for its reply. + + Should be run on a background thread: conn.recv() blocks until the worker + answers, so it must stay off the UI event loop. + """ + parent_conn.send(100_000_000) + result, duration = parent_conn.recv() + status.value = ( + f"sum of squares = {result}\n{duration:.2f}s in process {worker.pid}" + ) + button.disabled = False + page.update() + + page.add( + ft.SafeArea( + content=ft.Column( + controls=[ + button := ft.Button("Compute in worker", on_click=submit), + status := ft.Text(f"Worker ready (pid {worker.pid})"), + ] + ) + ) + ) + + +if __name__ == "__main__": + ft.run(main) diff --git a/sdk/python/examples/cookbook/multiprocessing/worker_progress.py b/sdk/python/examples/cookbook/multiprocessing/worker_progress.py new file mode 100644 index 0000000000..bc89d0158d --- /dev/null +++ b/sdk/python/examples/cookbook/multiprocessing/worker_progress.py @@ -0,0 +1,58 @@ +import multiprocessing + +import flet as ft + + +def crunch(progress_queue: multiprocessing.Queue, steps: int) -> None: + """Do `steps` slices of CPU-heavy work, reporting progress after each one. + + Runs in a worker process, which has no access to the page — the queue is + the only channel back to the UI. Values are fractions 0..1; a final `None` + tells the consumer there is nothing more to read. + """ + for i in range(steps): + sum(range(50_000_000)) # one slice of real work + progress_queue.put((i + 1) / steps) + progress_queue.put(None) # sentinel: no more updates + + +def main(page: ft.Page): + def start(): + button.disabled = True + status.value = "Starting worker…" + page.update() + queue = multiprocessing.Queue() + worker = multiprocessing.Process(target=crunch, args=(queue, 20), daemon=True) + worker.start() + page.run_thread(drain, queue, worker) + + def drain(queue: multiprocessing.Queue, worker: multiprocessing.Process): + """Forward the worker's progress reports to the UI. + + Runs on a background thread: queue.get() blocks until the worker + reports again, so it must stay off the UI event loop. + """ + while (value := queue.get()) is not None: + progress.value = value + status.value = f"Crunching… {value:.0%}" + page.update() + worker.join() + status.value = "Done!" + button.disabled = False + page.update() + + page.add( + ft.SafeArea( + content=ft.Column( + controls=[ + button := ft.Button("Start", on_click=start), + progress := ft.ProgressBar(value=0, width=300), + status := ft.Text(), + ] + ) + ) + ) + + +if __name__ == "__main__": + ft.run(main) 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 033ce5e5e4..d9a6364b2d 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 @@ -17,7 +17,7 @@ class Command(BaseBuildCommand): Android (APK/AAB), and iOS (IPA and simulator .app), with a wide range of customization options for metadata, assets, splash screens, and signing. - Detailed guide with usage examples: https://flet.dev/docs/publish + Detailed usage guide: https://flet.dev/docs/publish """ def __init__(self, parser: argparse.ArgumentParser) -> None: diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/create.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/create.py index 7349f2ddee..a257686339 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/create.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/create.py @@ -24,6 +24,8 @@ class Command(BaseCommand): Create a new Flet project using a predefined template. It sets up the initial directory structure, metadata, and required files to help you get started quickly. + + Detailed guide with usage examples: https://flet.dev/docs/getting-started/create-flet-app """ def add_arguments(self, parser: argparse.ArgumentParser) -> None: diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/debug.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/debug.py index fff1b7b8d3..acdcccb53d 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/debug.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/debug.py @@ -12,6 +12,8 @@ class Command(BaseBuildCommand): """ Run a Flet Python app in debug mode on a specified platform (desktop, web, mobile). + + Detailed usage guide: https://flet.dev/blog/flet-debug-the-new-cli-for-testing-flet-apps-on-mobile-devices """ def __init__(self, parser: argparse.ArgumentParser) -> None: diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py index 290df19303..69e8f20d1f 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/publish.py @@ -24,6 +24,8 @@ class Command(BaseCommand): """ Compile and package a Flet app as a standalone static web application. + + Detailed usage guide: https://flet.dev/docs/publish/web/static-website """ def add_arguments(self, parser: argparse.ArgumentParser) -> None: diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/run.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/run.py index e0217aa2e2..116a2a9d71 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/run.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/run.py @@ -28,6 +28,8 @@ class Command(BaseCommand): """ Run a Flet application in hot reload mode. + + Detailed usage guide: https://flet.dev/docs/getting-started/running-app """ def add_arguments(self, parser: argparse.ArgumentParser) -> None: diff --git a/sdk/python/packages/flet-cli/src/flet_cli/commands/test.py b/sdk/python/packages/flet-cli/src/flet_cli/commands/test.py index accaadb764..fa5945158f 100644 --- a/sdk/python/packages/flet-cli/src/flet_cli/commands/test.py +++ b/sdk/python/packages/flet-cli/src/flet_cli/commands/test.py @@ -104,6 +104,8 @@ class Command(BaseBuildCommand): `flet build`, in test mode) so the app runs on-device with embedded Python, then runs pytest. Tests in the `tests/` directory drive the app through the `flet_app` fixture (find controls by key, tap, take/assert screenshots). + + Detailed usage guide: https://flet.dev/docs/getting-started/integration-testing """ def __init__(self, parser: argparse.ArgumentParser) -> None: 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 839244a804..6c550e8f8c 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 @@ -28,7 +28,7 @@ # 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 = "20260701" +PYTHON_BUILD_RELEASE_DATE = "20260708" RELEASE_DATE_ENV = "FLET_PYTHON_BUILD_RELEASE_DATE" MANIFEST_PATH_ENV = "FLET_PYTHON_BUILD_MANIFEST" diff --git a/sdk/python/packages/flet/src/flet/version.py b/sdk/python/packages/flet/src/flet/version.py index e869bfc3eb..3e50c2eb5b 100644 --- a/sdk/python/packages/flet/src/flet/version.py +++ b/sdk/python/packages/flet/src/flet/version.py @@ -54,6 +54,8 @@ def from_git() -> Optional[str]: capture_output=True, text=True, check=True, + # Prevent console-window-flashes per git invocation on Windows. + creationflags=sp.CREATE_NO_WINDOW if is_windows() else 0, ) tag = result.stdout.strip() return tag[1:] if tag.startswith("v") else tag diff --git a/sdk/python/templates/app/app/{{cookiecutter.out_dir}}/src/main.py b/sdk/python/templates/app/app/{{cookiecutter.out_dir}}/src/main.py index 9e936e3966..21959dfc5a 100644 --- a/sdk/python/templates/app/app/{{cookiecutter.out_dir}}/src/main.py +++ b/sdk/python/templates/app/app/{{cookiecutter.out_dir}}/src/main.py @@ -4,7 +4,7 @@ def main(page: ft.Page): counter = ft.Text("0", size=50, data=0) - def increment_click(e): + def increment_click(e: ft.Event[ft.FloatingActionButton]): counter.data += 1 counter.value = str(counter.data) @@ -22,4 +22,5 @@ def increment_click(e): ) -ft.run(main) +if __name__ == "__main__": + ft.run(main) diff --git a/sdk/python/templates/app/extension/{{cookiecutter.out_dir}}/README.md b/sdk/python/templates/app/extension/{{cookiecutter.out_dir}}/README.md index 00c9c68e54..b5eb9fdd29 100644 --- a/sdk/python/templates/app/extension/{{cookiecutter.out_dir}}/README.md +++ b/sdk/python/templates/app/extension/{{cookiecutter.out_dir}}/README.md @@ -1,37 +1,85 @@ # {{cookiecutter.project_name}} -{{cookiecutter.control_name}} control for Flet -## Installation +{{cookiecutter.project_name}} [Flet](https://flet.dev) extension. +{% if cookiecutter.description %} +{{ cookiecutter.description }} +{% endif %} + -Add dependency to `pyproject.toml` of your Flet app: +## Platform Support -* **Git dependency** + -Link to git repository: +| Platform | iOS | Android | Web | Windows | macOS | Linux | +|-----------|-----|---------|-----|---------|-------|-------| +| Supported | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -``` +## Usage + +### Installation + +Add `{{cookiecutter.project_name}}` dependency to the `pyproject.toml` of your Flet project: + +* **From Git** + +```toml dependencies = [ - "{{cookiecutter.project_name}} @ git+https://github.com/MyGithubAccount/{{cookiecutter.project_name}}", + "{{cookiecutter.project_name}} @ git+https://github.com/MY_GITHUB_ACCOUNT/{{cookiecutter.project_name}}", "flet>={{cookiecutter.flet_version}}", ] ``` -* **uv/pip dependency** + -If the package is published on pypi.org: +* **From PyPI** -``` +```toml dependencies = [ "{{cookiecutter.project_name}}", "flet>={{cookiecutter.flet_version}}", ] ``` -Build your app: +### Run your app + +A Flet extension has two sides: its Python controls/services and the native Flutter/Dart widgets behind them. +That native code must be compiled into a Flet client before your controls can render, and the +prebuilt client that a plain `flet run` uses does **not** include this extension. + +So run your app in one of these two ways: + +**1. [`flet debug`](https://flet.dev/docs/cli/flet-debug)** — all platforms: *Windows, macOS, Linux, Web, iOS, Android* + +Compiles the extension and launches your app on the target you pick. +The simplest option, and the way to go for mobile and web: + +```bash +flet debug macos # desktop & web: no device needed +flet debug android -d # mobile: connect a device/emulator first ``` -flet build macos -v + +For iOS and Android, pass `-d ` (run `flet debug --show-devices` to list connected devices). +Edits to your **Python** code are picked up the next time you run `flet debug`. + +**2. [`flet build`](https://flet.dev/docs/cli/flet-build) once, then [`flet run`](https://flet.dev/docs/cli/flet-run)** — desktop only: *Windows, macOS, Linux* + +Build a custom client that bundles the extension **once**, then use `flet run` for a fast hot-reload loop while you edit Python: + +```bash +flet build macos # or: flet build windows / flet build linux +flet run # run from the folder where build/ was created, so it reuses that client ``` -## Documentation +`flet run` auto-detects the client under `build//`, so your Python edits hot-reload instantly. +Rebuild only when the extension's **Dart** code changes. + +### Examples + +See the [examples](examples) directory. + +### Documentation + + -[Link to documentation](https://MyGithubAccount.github.io/{{cookiecutter.project_name}}/) +Detailed documentation for this package can be found [here](https://MY_GITHUB_ACCOUNT.github.io/{{cookiecutter.project_name}}/). 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 index edb09ff6a9..e3124dbd24 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/lib/native_runtime.dart @@ -11,6 +11,7 @@ // of `kIsWeb` runtime gates. import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; @@ -104,12 +105,14 @@ Future runPython({ required Map environmentVariables, required List args, }) async { - var argvItems = args.map((a) => "\"${a.replaceAll('"', '\\"')}\""); - var argv = "[${argvItems.isNotEmpty ? argvItems.join(',') : '""'}]"; + // JSON literals are valid Python literals, so every dynamic value is + // spliced into the boot script through jsonEncode: it correctly escapes + // backslashes (Windows paths), quotes, and non-ASCII. var script = pythonScript - .replaceAll("{outLogFilename}", outLogFilename.replaceAll("\\", "\\\\")) - .replaceAll('{module_name}', moduleName) - .replaceAll('{argv}', argv); + .replaceAll('{outLogFilename}', jsonEncode(outLogFilename)) + .replaceAll('{module_name}', jsonEncode(moduleName)) + .replaceAll('{argv}', jsonEncode(args.isNotEmpty ? args : [""])) + .replaceAll('{host_executable}', jsonEncode(Platform.resolvedExecutable)); var completer = Completer(); 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 1065d7b89f..b141272535 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,8 @@ const errorExitCode = 100; -const pythonScript = """ -import os, runpy, sys, traceback +const pythonScript = + """ +import importlib.util, os, sys, traceback, types # fix for cryptography package os.environ["CRYPTOGRAPHY_OPENSSL_NO_LEGACY"] = "1" @@ -53,9 +54,16 @@ def initialize_ctypes(): ctypes.CDLL.__init__ = CDLL_init_override + initialize_ctypes() -out_file = open("{outLogFilename}", "w+", buffering=1) +out_file = open( + {outLogFilename}, + "w+", + buffering=1, + encoding="utf-8", + errors="backslashreplace", # prevents encoding failures +) # libdart_bridge >= 1.3.0 installs native-log file-like wrappers as # sys.stdout / sys.stderr right after Py_Initialize so prints land in @@ -66,6 +74,7 @@ out_file = open("{outLogFilename}", "w+", buffering=1) _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 @@ -82,6 +91,7 @@ class _TeeWriter: self._native = native self._file = file_ self._native_buf = "" + def write(self, text): if not text: return 0 @@ -98,6 +108,7 @@ class _TeeWriter: except Exception: pass return self._file.write(text) + def flush(self): try: # Drain any pending partial line (no trailing newline in the @@ -110,8 +121,10 @@ class _TeeWriter: except Exception: pass self._file.flush() + def isatty(self): return False + def fileno(self): return self._file.fileno() @@ -121,8 +134,8 @@ 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. +# then either renders the error screen (code == $errorExitCode) 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 @@ -152,6 +165,53 @@ if _add_restart is not None: _exit_port[0] = int(new_exit) _add_restart(_on_dart_session_restart) + +def _sp_run_module_as_main(module_name): + # Execute with `python -m module_name` semantics, but inside the real + # sys.modules["__main__"]. `runpy.run_module(..., run_name="__main__")` + # isn't used here, as it uses a temporary namespace, causing pickle/multiprocessing + # not to reliably resolve top-level functions from the app module. + spec = importlib.util.find_spec(module_name) + if spec is None: + raise ImportError("module %r not found" % module_name) + + if spec.submodule_search_locations is not None: + # Package case: `python -m pkg` executes pkg.__main__. + main_name = module_name + ".__main__" + spec = importlib.util.find_spec(main_name) + if spec is None: + raise ImportError( + "%r is a package and cannot be directly executed: " + "no %r module" % (module_name, main_name) + ) + + if spec.loader is None or not hasattr(spec.loader, "get_code"): + raise ImportError("module %r cannot be executed" % module_name) + + code = spec.loader.get_code(spec.name) + if code is None: + raise ImportError("module %r has no executable Python code" % module_name) + + main = types.ModuleType("__main__") + main.__dict__.update( + __spec__=spec, + __file__=spec.origin, + __cached__=spec.cached, + __loader__=spec.loader, + __package__=spec.parent, + __builtins__=__builtins__, + ) + + sys.modules["__main__"] = main + sys.modules["__mp_main__"] = main # Match multiprocessing's spawn alias for the main module. + + # Prevent re-exec'd multiprocessing children from inheriting PYTHONINSPECT, or + # they may stay open in interactive mode after their command finishes. + os.environ.pop("PYTHONINSPECT", None) + + exec(code, main.__dict__) + + ex = None try: import certifi @@ -172,10 +232,21 @@ try: ssl._create_default_https_context = create_default_context sys.argv = {argv} - runpy.run_module("{module_name}", run_name="__main__") + + # multiprocessing spawn/forkserver children re-execute sys.executable. In a + # packaged Flet app, that must be the host app binary, whose native runner + # detects multiprocessing argv and runs Python headlessly instead of opening + # another GUI window. Set it before user code can import multiprocessing, + # because multiprocessing snapshots the executable during import. + if _sp_host_exe := {host_executable}: + sys.executable = _sp_host_exe + sys._base_executable = _sp_host_exe + + # Execute the configured app module as the real __main__ module. + _sp_run_module_as_main({module_name}) except Exception as e: ex = e traceback.print_exception(e) -sys.exit(0 if ex is None else 100) +sys.exit(0 if ex is None else $errorExitCode) """; diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/linux/CMakeLists.txt b/sdk/python/templates/build/{{cookiecutter.out_dir}}/linux/CMakeLists.txt index c72838044f..208fce2ac7 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/linux/CMakeLists.txt +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/linux/CMakeLists.txt @@ -73,6 +73,9 @@ apply_standard_settings(${BINARY_NAME}) # Add dependency libraries. Add any application-specific dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter) target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +# dlopen/dlsym for main.cc's multiprocessing child interception +# (a no-op with glibc >= 2.34, where libdl is merged into libc). +target_link_libraries(${BINARY_NAME} PRIVATE ${CMAKE_DL_LIBS}) # Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/linux/main.cc b/sdk/python/templates/build/{{cookiecutter.out_dir}}/linux/main.cc index e7c5c54370..f89420aa49 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/linux/main.cc +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/linux/main.cc @@ -1,6 +1,52 @@ #include "my_application.h" +#include + +// Python's multiprocessing spawn/forkserver paths, including the resource +// tracker, create child processes by re-executing sys.executable with a +// CPython-style command line. In a flet-built Linux app, sys.executable is this +// app binary. Detect that argv shape before GTK/Flutter initialization and run +// the embedded interpreter headlessly instead of starting another GUI app. +// See: https://github.com/flet-dev/flet/issues/4283 +// +// These optional entry points live in libdart_bridge.so. Load the bridge early +// and resolve them dynamically so apps built with older dart_bridge versions +// still launch; they just won't have multiprocessing child interception. +// +// Returns true when this process was handled as a multiprocessing child. In +// that case, exit_code receives the interpreter process exit code. +static bool maybe_run_python_child(int argc, char** argv, int& exit_code) { + typedef int (*sp_argv_fn)(int, char**); + + // The shared library, and its Python dependency, would be loaded moments + // later by the plugin anyway, so eagerly loading it here does not add + // meaningful cost to the normal startup path. + void* bridge = dlopen("libdart_bridge.so", RTLD_NOW); + if (!bridge) { + return false; + } + + auto is_mp_invocation = reinterpret_cast( + dlsym(bridge, "serious_python_is_mp_invocation")); + auto run_python_main = + reinterpret_cast(dlsym(bridge, "serious_python_main")); + + if (is_mp_invocation && run_python_main && + is_mp_invocation(argc, argv) != 0) { + exit_code = run_python_main(argc, argv); + return true; + } + + return false; +} + int main(int argc, char** argv) { + // Multiprocessing child re-exec? Run it headlessly and exit, instead of starting Flutter GUI. + int mp_exit_code = 0; + if (maybe_run_python_child(argc, argv, mp_exit_code)) { + return mp_exit_code; + } + g_autoptr(MyApplication) app = my_application_new(); return g_application_run(G_APPLICATION(app), argc, argv); } diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner.xcodeproj/project.pbxproj b/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner.xcodeproj/project.pbxproj index bb413260d2..bbcba4a34e 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner.xcodeproj/project.pbxproj +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F12044A3C60003C046 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C046 /* main.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; @@ -69,6 +70,7 @@ 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; 33CC10ED2044A3C60003C045 /* {{ cookiecutter.artifact_name }}.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "{{ cookiecutter.artifact_name }}.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F02044A3C60003C046 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; @@ -166,6 +168,7 @@ isa = PBXGroup; children = ( 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC10F02044A3C60003C046 /* main.swift */, 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, 33E51913231747F40026EE4D /* DebugProfile.entitlements */, 33E51914231749380026EE4D /* Release.entitlements */, @@ -370,6 +373,7 @@ files = ( 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 33CC10F12044A3C60003C046 /* main.swift in Sources */, 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner/AppDelegate.swift b/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner/AppDelegate.swift index b3c1761412..1c86ba9ccc 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner/AppDelegate.swift +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner/AppDelegate.swift @@ -1,7 +1,8 @@ import Cocoa import FlutterMacOS -@main +// See main.swift for the entry point. + class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner/main.swift b/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner/main.swift new file mode 100644 index 0000000000..7873ebf383 --- /dev/null +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/macos/Runner/main.swift @@ -0,0 +1,38 @@ +import Cocoa + +// Python's multiprocessing spawn/forkserver paths, including the resource +// tracker, create child processes by re-executing sys.executable with a +// CPython-style command line. In a flet-built macOS app, sys.executable is this +// app binary. Detect that argv shape before AppKit/Flutter initialization and +// run the embedded interpreter headlessly instead of starting another GUI app. +// See: https://github.com/flet-dev/flet/issues/4283 +// +// These optional entry points live in dart_bridge, which is force-loaded into +// the host binary by serious_python_darwin. Resolve them dynamically from the +// current process image, similar to Dart FFI's DynamicLibrary.process(), so apps +// built with older dart_bridge versions still link and launch; they just won't +// have multiprocessing child interception. +private typealias SPArgvFn = @convention(c) ( + Int32, UnsafeMutablePointer?>? +) -> Int32 + +private let processHandle = dlopen(nil, RTLD_NOW) + +private func spResolve(_ name: String) -> SPArgvFn? { + guard let processHandle, + let sym = dlsym(processHandle, name) else { + return nil + } + return unsafeBitCast(sym, to: SPArgvFn.self) +} + +if let isMpInvocation = spResolve("serious_python_is_mp_invocation"), + let runPythonMain = spResolve("serious_python_main"), + isMpInvocation(CommandLine.argc, CommandLine.unsafeArgv) != 0 +{ + exit(runPythonMain(CommandLine.argc, CommandLine.unsafeArgv)) +} + +// Not a multiprocessing child process, or the interception hooks are unavailable: +// continue with the normal macOS Flutter app launch. +_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv) diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml b/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml index cbd7147eec..1128e20407 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: flet: path: ../../../../../packages/flet - serious_python: 4.2.1 + serious_python: 4.3.0 # MsgPack codec used by the dart_bridge FletBackendChannel implementation # in lib/main.dart — matches the wire format flet's existing socket diff --git a/sdk/python/templates/build/{{cookiecutter.out_dir}}/windows/runner/main.cpp b/sdk/python/templates/build/{{cookiecutter.out_dir}}/windows/runner/main.cpp index 7c91006a81..67a07c56f9 100644 --- a/sdk/python/templates/build/{{cookiecutter.out_dir}}/windows/runner/main.cpp +++ b/sdk/python/templates/build/{{cookiecutter.out_dir}}/windows/runner/main.cpp @@ -1,12 +1,68 @@ #include #include +#include #include #include "flutter_window.h" #include "utils.h" +// Python's multiprocessing spawn path, including the resource tracker, creates +// child processes by re-executing sys.executable with a CPython-style command +// line. In a flet-built Windows app, sys.executable is this app binary. Detect +// that argv shape before Win32 window/COM/Flutter initialization and run the +// embedded interpreter headlessly instead of starting another GUI app. +// See: https://github.com/flet-dev/flet/issues/4283 +// +// These optional entry points live in dart_bridge.dll. Load the bridge early +// and resolve them dynamically so apps built with older dart_bridge versions +// still launch; they just won't have multiprocessing child interception. +// +// Returns true when this process was handled as a multiprocessing child. In +// that case, exit_code receives the interpreter process exit code. +static bool MaybeRunPythonChild(int& exit_code) { + using SpArgvFn = int (*)(int, wchar_t **); + + int argc = 0; + wchar_t **argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (!argv) { + return false; + } + + bool handled = false; + + // The DLL, and its python3XX.dll dependency, would be loaded moments later by + // the plugin anyway, so eagerly loading it here does not add meaningful cost + // to the normal startup path. + HMODULE bridge = ::LoadLibraryW(L"dart_bridge.dll"); + if (!bridge) { + bridge = ::LoadLibraryW(L"dart_bridge_d.dll"); + } + + if (bridge) { + auto is_mp_invocation = reinterpret_cast( + ::GetProcAddress(bridge, "serious_python_is_mp_invocation_w")); + auto run_python_main = reinterpret_cast( + ::GetProcAddress(bridge, "serious_python_main_w")); + + if (is_mp_invocation && run_python_main && + is_mp_invocation(argc, argv) != 0) { + exit_code = run_python_main(argc, argv); + handled = true; + } + } + + ::LocalFree(argv); + return handled; +} + int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { + // Multiprocessing child re-exec? Run it headlessly and exit, instead of starting Flutter GUI. + int mp_exit_code = 0; + if (MaybeRunPythonChild(mp_exit_code)) { + return mp_exit_code; + } + // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { diff --git a/website/docs/cookbook/multiprocessing.md b/website/docs/cookbook/multiprocessing.md new file mode 100644 index 0000000000..d2963ef488 --- /dev/null +++ b/website/docs/cookbook/multiprocessing.md @@ -0,0 +1,169 @@ +--- +title: "Multiprocessing" +--- + +import {CodeExample} from '@site/src/components/crocodocs'; + +In this cookbook recipe, you'll learn how to use Python's built-in +[`multiprocessing`](https://docs.python.org/3/library/multiprocessing.html) module — +including [`concurrent.futures.ProcessPoolExecutor`](https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor) — +from a Flet app, for true CPU parallelism across processes. + +For I/O-bound work, or work that just needs to stay off the UI thread, prefer +[async or threads](async-apps.md) — they are lighter and work on every platform. +Reach for `multiprocessing` when you need multiple CPU cores doing Python work +at the same time (number crunching, batch processing, ML inference, etc.), or +when you need process isolation for work that may fail or need to be stopped. + +:::important[Platform and Flet version support] +`multiprocessing` works in Flet desktop apps during development ([`flet run`](../cli/flet-run.md)) and +in packaged **desktop** apps built with [`flet build {macos,windows,linux}`](../cli/flet-build.md) +or [`flet debug {macos,windows,linux}`](../cli/flet-debug.md) when using [Flet v0.86.0](../updates/release-notes.md#086x) or newer. + +It is **not supported on iOS and Android** (mobile operating systems don't +allow apps to spawn arbitrary child processes) or **in the browser**. On those +platforms, prefer threads or `asyncio` instead. +::: + +## How does it work? + +In a desktop app packaged with [`flet build`](../publish/index.md), there is no separate `python` executable — +the interpreter is embedded inside your app's binary. When `multiprocessing` spawns a +worker, it re-executes that binary with a CPython helper command line; the binary +recognizes that shape and services it as a plain, windowless Python interpreter. +This also covers multiprocessing's helper processes (the resource tracker and the `forkserver`). + +## Guidelines + +These are standard +[Python `multiprocessing` guidelines](https://docs.python.org/3/library/multiprocessing.html#programming-guidelines) +— but in a packaged Flet app they are **mandatory**, not just good style. + +### Always guard your entry point + +Start your app only under the `if __name__ == "__main__":` guard. For example: + +```python +import flet as ft + +def main(page: ft.Page): + ... + +if __name__ == "__main__": + ft.run(main) +``` + +With the `spawn` and `forkserver` start methods, worker/helper processes need +to safely import your main module. `spawn` is the default on macOS and Windows; +`forkserver` is the default on Linux starting with Python 3.14. Without the +guard, a child process can try to start your whole app again. + +### Use importable, picklable worker functions + +Worker targets, arguments, and return values must be picklable so Python can +send them between processes. In practice: + +* define worker functions at module top level, not inside `main()` or inside a + button handler +* pass plain data such as numbers, strings, lists, dicts, or dataclasses +* do not pass Flet controls, `page`, database connections, open files, lambdas, + or nested functions + +Good: + +```python +def sort_chunk(chunk): + return sorted(chunk) +``` + +Avoid: + +```python +def main(page: ft.Page): + def sort_chunk(chunk): + return sorted(chunk) +``` + +The nested version is not reliably picklable because worker processes need to +import the function by name from a module. + +### Don't touch the GUI from workers + +Worker processes run in a separate interpreter with no connection to your app's +page. Pass data back through [`multiprocessing.Queue`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Queue), +[`Pipe`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Pipe), or +[pool futures](https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Future), +and update the UI from the main process. + +### Others + +- [`sys.executable`](https://docs.python.org/3/library/sys.html#sys.executable) in a packaged app points at your app's binary, not a + `python` executable. That is intentional — don't override it with + [`multiprocessing.set_executable()`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.set_executable). +- You usually do not need [`multiprocessing.freeze_support()`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.freeze_support) in Flet apps. + Calling it inside the `if __name__ == "__main__":` block is harmless, but Flet + does not rely on PyInstaller-style frozen-executable bootstrapping. +- Worker `print()` output is not connected to your app's console log; use a + [`Queue`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Queue) or file-based logging if you need worker diagnostics. +- On Linux, avoid forcing the `fork` [start method](https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods): + your app's process runs the Flutter engine with many active threads, and forking it is unsafe. + Prefer the platform default (usually `forkserver` or `spawn`), or request one explicitly with + [`multiprocessing.set_start_method()`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.set_start_method) + or [`multiprocessing.get_context()`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.get_context). +- Starting a worker costs more in a packaged app than in plain Python — each + spawned child loads your app binary's libraries before Python takes over. + Create pools and long-lived workers once and reuse them, rather than spawning + per button click (see [Keep a persistent worker](#keep-a-persistent-worker) example). + +## Examples + +### Parallel sort with live progress + +Sort chunks of data across all CPU cores and stream progress to the page: + + + +Note how the long-running orchestration is moved off the UI event handler with +[`page.run_thread`](async-apps.md#threading), while the CPU-heavy work runs in the +process pool. The worker function may live in your main module (as above) or in +a separate importable module — both work. + +### Stream progress from a worker + +To show fine-grained progress from inside a single long-running job, pass a +[`multiprocessing.Queue`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Queue) +to the worker and drain it on a background thread. The worker [`put()`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Queue.put)s progress +values and a `None` sentinel when it is done: + + + +### Keep a persistent worker + +Starting a process is not free — especially in a packaged app, where each child +loads your app binary's libraries first. For repeated jobs, start one worker +that stays alive and serves requests over a +[`Pipe`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Pipe), +paying the startup cost once. Expensive setup (loading a model, opening a +dataset) can then also happen once, in the worker: + + + +The worker is started with `daemon=True`, so it is terminated automatically +when your app exits. + +### Cancel a runaway task + +Threads cannot be forcefully stopped from the outside — a worker [`Process`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Process) can, +at any time, with [`terminate()`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Process.terminate). +This makes processes the right tool for jobs you may need to abort, such as +long calls into external libraries: + + + +A background thread [`join()`](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Process.join)s the worker and reports how it ended — normally +(exit code `0`) or via cancellation (a negative exit code). + +:::info +[Python's multiprocessing guidelines](https://docs.python.org/3/library/multiprocessing.html#programming-guidelines) +recommend avoiding process termination or doing it only for processes which never use any shared resources. +::: diff --git a/website/docs/extend/user-extensions.md b/website/docs/extend/user-extensions.md index cee0030498..586d5436a3 100644 --- a/website/docs/extend/user-extensions.md +++ b/website/docs/extend/user-extensions.md @@ -617,7 +617,7 @@ The receiving side's queue is **unbounded by default**. If a producer outpaces t - **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). +- **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). ## Boot screen diff --git a/website/docs/updates/breaking-changes/index.md b/website/docs/updates/breaking-changes/index.md index 243078106e..fc5deeb182 100644 --- a/website/docs/updates/breaking-changes/index.md +++ b/website/docs/updates/breaking-changes/index.md @@ -26,22 +26,22 @@ Each guide explains the change, the reason for it, and how to migrate your code. #### 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) +- [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) +- [`flet build --clear-cache` flag deprecated](/docs/updates/breaking-changes/v0-86-0/deprecated-clear-cache-flag) ### Released in Flet 0.85.0 #### Breaking changes -- [Deprecated spacing and border helper functions removed](/docs/updates/breaking-changes/v0-85-0-removed-spacing-border-helpers) +- [Deprecated spacing and border helper functions removed](/docs/updates/breaking-changes/v0-85-0/removed-spacing-border-helpers) #### Deprecations -- [`DragTargetEvent` coordinate fields deprecated](/docs/updates/breaking-changes/v0-85-0-deprecated-drag-target-event-coordinates) -- [`Video` control APIs deprecated](/docs/updates/breaking-changes/v0-85-0-deprecated-video-apis) +- [`DragTargetEvent` coordinate fields deprecated](/docs/updates/breaking-changes/v0-85-0/deprecated-drag-target-event-coordinates) +- [`Video` control APIs deprecated](/docs/updates/breaking-changes/v0-85-0/deprecated-video-apis) diff --git a/website/docs/updates/breaking-changes/v0-85-0-deprecated-drag-target-event-coordinates.md b/website/docs/updates/breaking-changes/v0-85-0/deprecated-drag-target-event-coordinates.md similarity index 91% rename from website/docs/updates/breaking-changes/v0-85-0-deprecated-drag-target-event-coordinates.md rename to website/docs/updates/breaking-changes/v0-85-0/deprecated-drag-target-event-coordinates.md index 43703876cc..51a70b0207 100644 --- a/website/docs/updates/breaking-changes/v0-85-0-deprecated-drag-target-event-coordinates.md +++ b/website/docs/updates/breaking-changes/v0-85-0/deprecated-drag-target-event-coordinates.md @@ -8,7 +8,7 @@ title: "DragTargetEvent coordinate fields deprecated" This guide is accurate as of Flet 0.85.0. Later releases might add new APIs or additional migration paths. -The [breaking changes and deprecations index](.) lists the guides created for each release. +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. ::: ## Summary @@ -62,4 +62,4 @@ If your code needs page-level coordinates instead, use - API documentation: [`DragTargetEvent`][flet.DragTargetEvent] - Issues and PRs: [#6387](https://github.com/flet-dev/flet/issues/6387), [#6401](https://github.com/flet-dev/flet/pull/6401) -- Release notes: [Flet 0.85.0](../release-notes.md#085x) +- Release notes: [Flet 0.85.0](../../release-notes.md#085x) diff --git a/website/docs/updates/breaking-changes/v0-85-0-deprecated-video-apis.md b/website/docs/updates/breaking-changes/v0-85-0/deprecated-video-apis.md similarity index 91% rename from website/docs/updates/breaking-changes/v0-85-0-deprecated-video-apis.md rename to website/docs/updates/breaking-changes/v0-85-0/deprecated-video-apis.md index c49986ef81..10580e1164 100644 --- a/website/docs/updates/breaking-changes/v0-85-0-deprecated-video-apis.md +++ b/website/docs/updates/breaking-changes/v0-85-0/deprecated-video-apis.md @@ -8,7 +8,7 @@ title: "Video control APIs deprecated" This guide is accurate as of Flet 0.85.0. Later releases might add new APIs or additional migration paths. -The [breaking changes and deprecations index](.) lists the guides created for each release. +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. ::: ## Summary @@ -81,4 +81,4 @@ been added to the page. - API documentation: [`Video`][flet_video.Video] - Issues and PRs: [PR #6463](https://github.com/flet-dev/flet/pull/6463) -- Release notes: [Flet 0.85.0](../release-notes.md#085x) +- Release notes: [Flet 0.85.0](../../release-notes.md#085x) diff --git a/website/docs/updates/breaking-changes/v0-85-0-removed-spacing-border-helpers.md b/website/docs/updates/breaking-changes/v0-85-0/removed-spacing-border-helpers.md similarity index 93% rename from website/docs/updates/breaking-changes/v0-85-0-removed-spacing-border-helpers.md rename to website/docs/updates/breaking-changes/v0-85-0/removed-spacing-border-helpers.md index cdad6bdb9e..de24bfee25 100644 --- a/website/docs/updates/breaking-changes/v0-85-0-removed-spacing-border-helpers.md +++ b/website/docs/updates/breaking-changes/v0-85-0/removed-spacing-border-helpers.md @@ -8,7 +8,7 @@ title: "Deprecated spacing and border helper functions removed" This guide is accurate as of Flet 0.85.0. Later releases might add new APIs or additional migration paths. -The [breaking changes and deprecations index](.) lists the guides created for each release. +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. ::: ## Summary @@ -74,4 +74,4 @@ card = ft.Container( - API documentation: [`Border`][flet.Border], [`BorderRadius`][flet.BorderRadius], [`Padding`][flet.Padding] - Issues and PRs: [#6425](https://github.com/flet-dev/flet/pull/6425) -- Release notes: [Flet 0.85.0](../release-notes.md#085x) +- Release notes: [Flet 0.85.0](../../release-notes.md#085x) diff --git a/website/docs/updates/breaking-changes/v0-86-0-app-files-unpacked-read-only-bundle.md b/website/docs/updates/breaking-changes/v0-86-0/app-files-unpacked-read-only-bundle.md similarity index 90% rename from website/docs/updates/breaking-changes/v0-86-0-app-files-unpacked-read-only-bundle.md rename to website/docs/updates/breaking-changes/v0-86-0/app-files-unpacked-read-only-bundle.md index 8ca4d4867a..fa0b2ded40 100644 --- a/website/docs/updates/breaking-changes/v0-86-0-app-files-unpacked-read-only-bundle.md +++ b/website/docs/updates/breaking-changes/v0-86-0/app-files-unpacked-read-only-bundle.md @@ -8,7 +8,7 @@ title: "App files ship unpacked in a read-only bundle; storage dirs reworked" 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. +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. ::: ## Summary @@ -114,8 +114,8 @@ ignored by Git automatically; the old visible `storage/` directory is no longer ## References -- Environment variables: [`FLET_APP_STORAGE_DATA`](../../reference/environment-variables.md#flet_app_storage_data), - [`FLET_APP_STORAGE_CACHE`](../../reference/environment-variables.md#flet_app_storage_cache), - [`FLET_APP_STORAGE_TEMP`](../../reference/environment-variables.md#flet_app_storage_temp) -- Cookbook: [Read and write files](../../cookbook/read-and-write-files.md) -- Release notes: [Flet 0.86.0](../release-notes.md) +- Environment variables: [`FLET_APP_STORAGE_DATA`](../../../reference/environment-variables.md#flet_app_storage_data), + [`FLET_APP_STORAGE_CACHE`](../../../reference/environment-variables.md#flet_app_storage_cache), + [`FLET_APP_STORAGE_TEMP`](../../../reference/environment-variables.md#flet_app_storage_temp) +- Cookbook: [Read and write files](../../../cookbook/read-and-write-files.md) +- Release notes: [Flet 0.86.0](../../release-notes.md) 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 similarity index 94% rename from website/docs/updates/breaking-changes/v0-86-0-compile-on-by-default.md rename to website/docs/updates/breaking-changes/v0-86-0/compile-on-by-default.md index e62778e70d..bf5ee97629 100644 --- 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 @@ -8,7 +8,7 @@ title: "App and packages are compiled to .pyc by default" 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. +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. ::: ## Summary @@ -75,4 +75,4 @@ resolution order is CLI flag → `[tool.flet..compile]` → - 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) +- 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 similarity index 96% rename from website/docs/updates/breaking-changes/v0-86-0-data-channel-protocol-upgrade.md rename to website/docs/updates/breaking-changes/v0-86-0/data-channel-protocol-upgrade.md index 588165486f..c0ea0ded5b 100644 --- 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 @@ -8,7 +8,7 @@ title: "Flet protocol framing upgraded for DataChannel support" 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. +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. ::: ## Summary @@ -108,4 +108,4 @@ the channel directly. ## 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) +- 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 similarity index 94% rename from website/docs/updates/breaking-changes/v0-86-0-default-bundled-python-3-14.md rename to website/docs/updates/breaking-changes/v0-86-0/default-bundled-python-3-14.md index c9a8d8cff3..61ad2e3d8d 100644 --- 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 @@ -8,7 +8,7 @@ title: "Default bundled Python version is now 3.14" 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. +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. ::: ## Summary @@ -86,4 +86,4 @@ job. - 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) +- Release notes: [Flet 0.86.0](../../release-notes.md) diff --git a/website/docs/updates/breaking-changes/v0-86-0-deprecated-clear-cache-flag.md b/website/docs/updates/breaking-changes/v0-86-0/deprecated-clear-cache-flag.md similarity index 62% rename from website/docs/updates/breaking-changes/v0-86-0-deprecated-clear-cache-flag.md rename to website/docs/updates/breaking-changes/v0-86-0/deprecated-clear-cache-flag.md index a10fd99812..a3aec1aef1 100644 --- a/website/docs/updates/breaking-changes/v0-86-0-deprecated-clear-cache-flag.md +++ b/website/docs/updates/breaking-changes/v0-86-0/deprecated-clear-cache-flag.md @@ -8,15 +8,15 @@ title: "flet build --clear-cache flag deprecated" 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. +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. ::: ## Summary Flet 0.86.0 deprecated the `--clear-cache` flag of -[`flet build`](../../cli/flet-build.md) and [`flet debug`](../../cli/flet-debug.md). +[`flet build`](../../../cli/flet-build.md) and [`flet debug`](../../../cli/flet-debug.md). -Use the new [`flet clean`](../../cli/flet-clean.md) command instead. +Use the new [`flet clean`](../../../cli/flet-clean.md) command instead. ## Context @@ -24,7 +24,7 @@ Use the new [`flet clean`](../../cli/flet-clean.md) command instead. and only when the build template hash had changed — so it was a no-op in the common case and never reset the rest of the `build` directory. -The new [`flet clean`](../../cli/flet-clean.md) command deletes the entire +The new [`flet clean`](../../../cli/flet-clean.md) command deletes the entire `build` directory (the Flutter bootstrap project, cached artifacts, hash stamps, and generated output) unconditionally, giving a predictable, full reset before the next build. @@ -44,7 +44,7 @@ flet clean flet build web ``` -`flet clean` accepts an optional [path](../../cli/flet-clean.md#python_app_path) to the app directory (defaults to the +`flet clean` accepts an optional [path](../../../cli/flet-clean.md#python_app_path) to the app directory (defaults to the current directory), so you can target another project with `flet clean path/to/app`. ## Timeline @@ -54,6 +54,6 @@ current directory), so you can target another project with `flet clean path/to/a ## References -- CLI documentation: [`flet clean`](../../cli/flet-clean.md), [`flet build`](../../cli/flet-build.md) +- CLI documentation: [`flet clean`](../../../cli/flet-clean.md), [`flet build`](../../../cli/flet-build.md) - Issues and PRs: [#6233](https://github.com/flet-dev/flet/issues/6233) -- Release notes: [Flet 0.86.0](../release-notes.md) +- 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 similarity index 93% rename from website/docs/updates/breaking-changes/v0-86-0-removed-pyodide-version-export.md rename to website/docs/updates/breaking-changes/v0-86-0/removed-pyodide-version-export.md index ec6e96f870..cbc344ca77 100644 --- 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 @@ -8,7 +8,7 @@ title: "flet.version.pyodide_version and PYODIDE_VERSION removed" 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. +The [breaking changes and deprecations index](../index.md) lists the guides created for each release. ::: ## Summary @@ -75,4 +75,4 @@ print(f"Default Pyodide: {release.pyodide}") - 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) +- Release notes: [Flet 0.86.0](../../release-notes.md) diff --git a/website/sidebars.yml b/website/sidebars.yml index 140ec95e1d..d886255eb7 100644 --- a/website/sidebars.yml +++ b/website/sidebars.yml @@ -36,6 +36,7 @@ docs: Session Storage: cookbook/session-storage.md PubSub: cookbook/pub-sub.md Subprocess: cookbook/subprocess.md + Multiprocessing: cookbook/multiprocessing.md Logging: cookbook/logging.md Authentication: cookbook/authentication.md Encrypting sensitive data: cookbook/encrypting-sensitive-data.md @@ -71,16 +72,16 @@ docs: Breaking changes and deprecations: _index: updates/breaking-changes/index.md v0.86.0: - App files ship unpacked in a read-only bundle; storage dirs reworked: updates/breaking-changes/v0-86-0-app-files-unpacked-read-only-bundle.md - 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 + App files ship unpacked in a read-only bundle; storage dirs reworked: updates/breaking-changes/v0-86-0/app-files-unpacked-read-only-bundle.md + 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 - DragTargetEvent coordinate fields deprecated: updates/breaking-changes/v0-85-0-deprecated-drag-target-event-coordinates.md - Video control APIs deprecated: updates/breaking-changes/v0-85-0-deprecated-video-apis.md + Deprecated spacing and border helper functions removed: updates/breaking-changes/v0-85-0/removed-spacing-border-helpers.md + DragTargetEvent coordinate fields deprecated: updates/breaking-changes/v0-85-0/deprecated-drag-target-event-coordinates.md + Video control APIs deprecated: updates/breaking-changes/v0-85-0/deprecated-video-apis.md Compatibility policy: updates/compatibility-policy.md Reference: _index: reference/index.md