From 242ac459653d426264f8bd4ecaddbc407a091fdc Mon Sep 17 00:00:00 2001
From: WSH032 <614337162@qq.com>
Date: Sat, 22 Feb 2025 17:38:51 +0800
Subject: [PATCH] docs: add concepts `IPC` and `using multiprocessing` sections
close #53
---
CHANGELOG.md | 1 +
docs/usage/concepts/ipc.md | 105 +++++++++++++++++++++++++-
docs/usage/concepts/mutiprocessing.md | 19 +++++
docs/usage/tutorial/py-js-ipc.md | 4 +
docs_src/concepts/ipc/calling_cmd.ts | 8 ++
docs_src/concepts/ipc/js_channel.ts | 7 ++
docs_src/concepts/ipc/py_channel.py | 21 ++++++
docs_src/concepts/ipc/reg_cmd.py | 34 +++++++++
docs_src/concepts/ipc/ret_exec.py | 9 +++
docs_src/concepts/ipc/serde_body.py | 30 ++++++++
mkdocs.yml | 1 +
python/pytauri/src/pytauri/ffi/lib.py | 13 ++++
12 files changed, 249 insertions(+), 3 deletions(-)
create mode 100644 docs/usage/concepts/mutiprocessing.md
create mode 100644 docs_src/concepts/ipc/calling_cmd.ts
create mode 100644 docs_src/concepts/ipc/js_channel.ts
create mode 100644 docs_src/concepts/ipc/py_channel.py
create mode 100644 docs_src/concepts/ipc/reg_cmd.py
create mode 100644 docs_src/concepts/ipc/ret_exec.py
create mode 100644 docs_src/concepts/ipc/serde_body.py
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac86966..23e7324 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Docs
+- [#85](https://github.com/WSH032/pytauri/pull/85) - docs: add concepts `IPC` and `using multiprocessing` sections.
- [#80](https://github.com/WSH032/pytauri/pull/80) - `example/nicegui-app`:
- Use `BuilderArgs.setup` for initialization instead of listening to the `RunEvent.Ready` event.
- Rewrite the `FrontServer` `startup`/`shutdown` event hook logic.
diff --git a/docs/usage/concepts/ipc.md b/docs/usage/concepts/ipc.md
index fbe072c..757b428 100644
--- a/docs/usage/concepts/ipc.md
+++ b/docs/usage/concepts/ipc.md
@@ -1,12 +1,111 @@
-# IPC Commands
+# IPC
+
+## Calling Python from the Frontend
Ref:
--
-
- [pytauri.ipc.Commands][]
+
+pytauri implements IPC API consistent with rust tauri. Reading tauri's documentation is like reading pytauri's documentation.
+
+### Commands
+
+#### Registering Commands
+
+You can register a command handler using the decorator [@Commands.command][pytauri.ipc.Commands.command].
+
+Similar to `tauri::command!`, the `handler` signature can be arbitrary. We will use [inspect.signature][] to inspect its signature and dynamically pass the required parameters.
+
+!!! info
+ You might have seen this pattern in `FastAPI`🤓.
+
+The currently supported signature pattern is [ArgumentsType][pytauri.ipc.ArgumentsType]. You must ensure that the parameter names and type annotations are correct, and `@Commands.command` will check them.
+
+```python
+--8<-- "docs_src/concepts/ipc/reg_cmd.py"
+```
+
+#### Deserializing the Body
+
+For the `body` argument, it is of type `bytes`, allowing you to pass binary data such as files between the frontend and backend.
+
+However, in most cases, we want strong type checking when calling. Rust `tauri` achieves this through `serde`, while `pytauri` uses [pydantic](https://github.com/pydantic/pydantic).
+
+!!! info
+ `pydantic` is a super-fast Python validation and serialization library written in `rust`/`pyo3` 🤓.
+
+If you use [BaseModel][pydantic.BaseModel]/[RootModel][pydantic.RootModel] as the type annotation for the `body` parameter/return value, pytauri will automatically serialize/deserialize it for you:
+
+```python
+--8<-- "docs_src/concepts/ipc/serde_body.py"
+```
+
+#### Calling Commands
+
+```typescript
+--8<-- "docs_src/concepts/ipc/calling_cmd.ts"
+```
+
+The difference between `rawPyInvoke` and `pyInvoke` is that the input and output of `rawPyInvoke` are both `ArrayBuffer`, allowing you to pass binary data.
+
+#### Returning Errors to the Frontend
+
+Similar to `FastAPI`, as long as you throw an [InvokeException][pytauri.ipc.InvokeException] in the `command`, the promise will reject with the error message.
+
+```python
+--8<-- "docs_src/concepts/ipc/ret_exec.py"
+```
+
+## Calling Frontend from Python
+
+Ref:
+
+-
- [pytauri.ipc.JavaScriptChannelId][] and [pytauri.ipc.Channel][]
- [pytauri.webview.WebviewWindow.eval][]
+
+### Channels
+
+> Channels are designed to be fast and deliver ordered data. They are used internally for streaming operations such as download progress, child process output, and WebSocket messages.
+
+To use a `channel`, you only need to add the [JavaScriptChannelId][pytauri.ipc.JavaScriptChannelId] field to the `BaseModel`/`RootModel`, and then use [JavaScriptChannelId.channel_on][pytauri.ipc.JavaScriptChannelId.channel_on] to get a [Channel][pytauri.ipc.Channel] instance.
+
+!!! info
+ `JavaScriptChannelId` itself is a `RootModel`, so you can directly use it as the `body` parameter.
+
+```python
+--8<-- "docs_src/concepts/ipc/py_channel.py"
+```
+
+```typescript
+--8<-- "docs_src/concepts/ipc/js_channel.ts"
+```
+
+!!! info
+ The `Channel` in `tauri-plugin-pytauri-api` is just a subclass of the `Channel` in `@tauri-apps/api/event`.
+
+ It adds the `addJsonListener` method to help serialize data. You can use `Channel.onmessage` to handle raw `ArrayBuffer` data.
+
+### Evaluating JavaScript
+
+You can use [WebviewWindow.eval][pytauri.webview.WebviewWindow.eval] to evaluate JavaScript code in the frontend.
+
+## Event System
+
+Ref:
+
+-
+-
- [pytauri.Listener][]
+- `pytauri.Emitter`: blocked on [#61](https://github.com/WSH032/pytauri/pull/61)
+
+> Tauri ships a simple event system you can use to have bi-directional communication between Rust and your frontend.
+>
+> The event system was designed for situations where small amounts of data need to be streamed or you need to implement a multi consumer multi producer pattern (e.g. push notification system).
+>
+> The event system is not designed for low latency or high throughput situations. See the channels section for the implementation optimized for streaming data.
+>
+> The major differences between a Tauri command and a Tauri event are that events have no strong type support, event payloads are always JSON strings making them not suitable for bigger messages and there is no support of the capabilities system to fine grain control event data and channels.
-TODO: details
+See [pytauri.Listener--example][]
diff --git a/docs/usage/concepts/mutiprocessing.md b/docs/usage/concepts/mutiprocessing.md
new file mode 100644
index 0000000..f895d1d
--- /dev/null
+++ b/docs/usage/concepts/mutiprocessing.md
@@ -0,0 +1,19 @@
+# Using multiprocessing
+
+When building as a `standalone` app, `pytauri` will automatically configure the following to support the use of [multiprocessing][]:
+
+> ref: [pytauri::standalone::PythonInterpreterBuilder](https://docs.rs/pytauri/0.2.0/pytauri/standalone/struct.PythonInterpreterBuilder.html#behavior)
+
+- Set `sys.frozen` to `True`
+- Call [multiprocessing.set_start_method][] with
+ - windows: `spawn`
+ - unix: `fork`
+- Call [multiprocessing.set_executable][] with `std::env::current_exe()`
+
+---
+
+**What you need to do** is call [multiprocessing.freeze_support][] in `__main__.py` or in the `if __name__ == "__main__":` block.
+
+If you don't do this, you will get an endless spawn loop of your application process.
+
+See: .
diff --git a/docs/usage/tutorial/py-js-ipc.md b/docs/usage/tutorial/py-js-ipc.md
index 69e4473..11e5295 100644
--- a/docs/usage/tutorial/py-js-ipc.md
+++ b/docs/usage/tutorial/py-js-ipc.md
@@ -1,5 +1,9 @@
# IPC between Python and JavaScript
+**See [concepts/ipc](../concepts/ipc.md) for more information.**
+
+---
+
pytauri implements the same IPC API as tauri. You can use it through [pytauri.Commands][].
This tutorial will demonstrate how to use pytauri's IPC API by rewriting the `fn greet` command in `src-tauri/src/lib.rs` in Python.
diff --git a/docs_src/concepts/ipc/calling_cmd.ts b/docs_src/concepts/ipc/calling_cmd.ts
new file mode 100644
index 0000000..c6eab5b
--- /dev/null
+++ b/docs_src/concepts/ipc/calling_cmd.ts
@@ -0,0 +1,8 @@
+import { pyInvoke, rawPyInvoke } from "tauri-plugin-pytauri-api";
+// or if tauri config `app.withGlobalTauri = true`:
+//
+// ```js
+// const { pyInvoke, rawPyInvoke } = window.__TAURI__.pytauri;
+// ```
+
+const output = await pyInvoke<[string]>("command", { foo: "foo", bar: 42 });
diff --git a/docs_src/concepts/ipc/js_channel.ts b/docs_src/concepts/ipc/js_channel.ts
new file mode 100644
index 0000000..69f038b
--- /dev/null
+++ b/docs_src/concepts/ipc/js_channel.ts
@@ -0,0 +1,7 @@
+import { pyInvoke, Channel } from "tauri-plugin-pytauri-api";
+// const { pyInvoke, Channel } = window.__TAURI__.pytauri;
+
+const channel = new Channel();
+channel.addJsonListener((msg) => console.log(msg));
+
+await pyInvoke("command", channel);
diff --git a/docs_src/concepts/ipc/py_channel.py b/docs_src/concepts/ipc/py_channel.py
new file mode 100644
index 0000000..89e2624
--- /dev/null
+++ b/docs_src/concepts/ipc/py_channel.py
@@ -0,0 +1,21 @@
+from pydantic import RootModel
+from pytauri import Commands
+from pytauri.ipc import Channel, JavaScriptChannelId
+from pytauri.webview import WebviewWindow
+
+commands = Commands()
+
+Msg = RootModel[str]
+
+
+@commands.command()
+async def command(
+ body: JavaScriptChannelId[Msg], webview_window: WebviewWindow
+) -> bytes:
+ channel: Channel[Msg] = body.channel_on(webview_window.as_ref_webview())
+
+ # 👇 you should do this as background task, here just keep it simple as a example
+ channel.send(b'"message"')
+ channel.send_model(Msg("message"))
+
+ return b"null"
diff --git a/docs_src/concepts/ipc/reg_cmd.py b/docs_src/concepts/ipc/reg_cmd.py
new file mode 100644
index 0000000..fc1af52
--- /dev/null
+++ b/docs_src/concepts/ipc/reg_cmd.py
@@ -0,0 +1,34 @@
+# pyright: reportRedeclaration=none
+# ruff: noqa: F811
+
+from pytauri import AppHandle, Commands
+
+commands = Commands()
+
+
+# ⭐ OK
+@commands.command()
+async def command(body: bytes) -> bytes: ...
+
+
+# ⭐ OK
+@commands.command()
+async def command(body: bytes, app_handle: AppHandle) -> bytes: ...
+
+
+# 💥 ERROR: missing/wrong type annotation
+@commands.command()
+async def command(
+ body: bytes,
+ app_handle, # pyright: ignore[reportUnknownParameterType, reportMissingParameterType] # noqa: ANN001
+) -> bytes: ...
+
+
+# 💥 ERROR: wrong parameter name
+@commands.command()
+async def command(body: bytes, foo: AppHandle) -> bytes: ...
+
+
+# 💥 ERROR: not an async function
+@commands.command() # pyright: ignore[reportArgumentType, reportUntypedFunctionDecorator]
+def command(body: bytes) -> bytes: ...
diff --git a/docs_src/concepts/ipc/ret_exec.py b/docs_src/concepts/ipc/ret_exec.py
new file mode 100644
index 0000000..a679ecc
--- /dev/null
+++ b/docs_src/concepts/ipc/ret_exec.py
@@ -0,0 +1,9 @@
+from pytauri import Commands
+from pytauri.ipc import InvokeException
+
+commands = Commands()
+
+
+@commands.command()
+async def command() -> bytes:
+ raise InvokeException("error message")
diff --git a/docs_src/concepts/ipc/serde_body.py b/docs_src/concepts/ipc/serde_body.py
new file mode 100644
index 0000000..4f94586
--- /dev/null
+++ b/docs_src/concepts/ipc/serde_body.py
@@ -0,0 +1,30 @@
+# pyright: reportRedeclaration=none
+# ruff: noqa: F811
+
+from pydantic import BaseModel, RootModel
+from pytauri import AppHandle, Commands
+
+commands = Commands()
+
+
+class Input(BaseModel):
+ foo: str
+ bar: int
+
+
+Output = RootModel[list[str]]
+
+
+# ⭐ OK
+@commands.command()
+async def command(body: Input, app_handle: AppHandle) -> Output: ...
+
+
+# ⭐ OK
+@commands.command()
+async def command(body: Input) -> bytes: ...
+
+
+# ⭐ OK
+@commands.command()
+async def command(body: bytes) -> Output: ...
diff --git a/mkdocs.yml b/mkdocs.yml
index e1b6bf2..e6beb65 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -237,6 +237,7 @@ nav:
- Concepts:
- usage/concepts/index.md
- usage/concepts/ipc.md
+ - usage/concepts/mutiprocessing.md
# DO NOT change `reference/`, it's used in `utils/gen_ref_pages.py`
- API Reference: reference/
- CONTRIBUTING:
diff --git a/python/pytauri/src/pytauri/ffi/lib.py b/python/pytauri/src/pytauri/ffi/lib.py
index ae03bdb..683be4d 100644
--- a/python/pytauri/src/pytauri/ffi/lib.py
+++ b/python/pytauri/src/pytauri/ffi/lib.py
@@ -350,6 +350,19 @@ class Listener:
"""[tauri::Listener](https://docs.rs/tauri/latest/tauri/trait.Listener.html)
See also:
+
+ # Example
+
+ ```python
+ from pytauri import AppHandle, Event, Listener
+
+
+ def listen(app_handle: AppHandle) -> None:
+ def handler(event: Event):
+ print(event.id, event.payload)
+
+ Listener.listen(app_handle, "event_name", handler)
+ ```
"""
@staticmethod