Skip to content

Commit

Permalink
docs: add concepts IPC and using multiprocessing sections
Browse files Browse the repository at this point in the history
close #53
  • Loading branch information
WSH032 committed Feb 22, 2025
1 parent 608ff81 commit 242ac45
Show file tree
Hide file tree
Showing 12 changed files with 249 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
105 changes: 102 additions & 3 deletions docs/usage/concepts/ipc.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,111 @@
# IPC Commands
# IPC

## Calling Python from the Frontend

Ref:

- <https://tauri.app/develop/calling-frontend/>
- <https://tauri.app/develop/calling-rust/>
- [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:

- <https://tauri.app/develop/calling-frontend/>
- [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:

- <https://tauri.app/develop/calling-frontend/#event-system>
- <https://tauri.app/develop/calling-rust/#event-system>
- [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][]
19 changes: 19 additions & 0 deletions docs/usage/concepts/mutiprocessing.md
Original file line number Diff line number Diff line change
@@ -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: <https://pyinstaller.org/en/v6.11.1/common-issues-and-pitfalls.html#multi-processing>.
4 changes: 4 additions & 0 deletions docs/usage/tutorial/py-js-ipc.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
8 changes: 8 additions & 0 deletions docs_src/concepts/ipc/calling_cmd.ts
Original file line number Diff line number Diff line change
@@ -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 });
7 changes: 7 additions & 0 deletions docs_src/concepts/ipc/js_channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { pyInvoke, Channel } from "tauri-plugin-pytauri-api";
// const { pyInvoke, Channel } = window.__TAURI__.pytauri;

const channel = new Channel<string>();
channel.addJsonListener((msg) => console.log(msg));

await pyInvoke("command", channel);
21 changes: 21 additions & 0 deletions docs_src/concepts/ipc/py_channel.py
Original file line number Diff line number Diff line change
@@ -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"
34 changes: 34 additions & 0 deletions docs_src/concepts/ipc/reg_cmd.py
Original file line number Diff line number Diff line change
@@ -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: ...
9 changes: 9 additions & 0 deletions docs_src/concepts/ipc/ret_exec.py
Original file line number Diff line number Diff line change
@@ -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")
30 changes: 30 additions & 0 deletions docs_src/concepts/ipc/serde_body.py
Original file line number Diff line number Diff line change
@@ -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: ...
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 13 additions & 0 deletions python/pytauri/src/pytauri/ffi/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,19 @@ class Listener:
"""[tauri::Listener](https://docs.rs/tauri/latest/tauri/trait.Listener.html)
See also: <https://tauri.app/develop/calling-rust/#event-system>
# 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
Expand Down

0 comments on commit 242ac45

Please sign in to comment.