Dev server and dev client¶
The development loop is a server process (pn start) and any number of
clients: the browser preview and debug builds on simulators, emulators,
and devices. See the Development workflow
guide for how they fit together; this page documents the modules.
pythonnative.devserver¶
The PythonNative dev server: one process that serves every dev client.
pn start (and pn preview, which is pn start plus a browser
tab) runs a DevServer. It watches
the project's app/ directory, keeps a content-addressed manifest of
the sources, and speaks a small JSON protocol over WebSocket to two
kinds of peers:
- Dev clients: debug builds of the app running on a simulator,
emulator, or physical device. They sync sources into an on-device
overlay, Fast Refresh when files change, and stream their logs and
errors back to the terminal. See
pythonnative.devclient. - The browser preview: a page served by this server that renders
the app through the bridge protocol, exactly as the Swift and Kotlin
runtimes do. See
pythonnative.bridge.web.
Everything here is standard library only (asyncio streams plus a
hand-rolled RFC 6455 implementation in pythonnative.devserver.ws)
so the same
code also runs inside the embedded interpreter on device.
Modules:
| Name | Description |
|---|---|
server |
The dev server: HTTP for static assets, WebSocket for live peers. |
watcher |
Source snapshots and change detection for the dev server. |
ws |
A small RFC 6455 (WebSocket) implementation on the standard library. |
Classes:
| Name | Description |
|---|---|
DevServer |
Serve sources, assets, and the live protocol for one project. |
ServerInfo |
What the server is serving and where. |
FileWatcher |
Poll a source tree and report changes on a background thread. |
SourceSnapshot |
The state of a source tree at one instant. |
Functions:
| Name | Description |
|---|---|
lan_addresses |
Best-effort list of this machine's non-loopback IPv4 addresses. |
snapshot_sources |
Hash every synced file under |
Attributes:
| Name | Type | Description |
|---|---|---|
DEFAULT_PORT |
Default port for |
DevServer
¶
DevServer(
project_root: str,
entry_module: str = "app.main",
*,
host: str = "0.0.0.0",
port: int = DEFAULT_PORT,
project_name: str = "",
static_dir: Optional[str] = None,
log: Optional[Logger] = None,
watch: bool = True
)
Serve sources, assets, and the live protocol for one project.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
project_root
|
str
|
Directory containing |
required |
entry_module
|
str
|
The app's entry module ( |
'app.main'
|
host
|
str
|
Bind address. |
'0.0.0.0'
|
port
|
int
|
TCP port; |
DEFAULT_PORT
|
project_name
|
str
|
Shown in the preview page and |
''
|
static_dir
|
Optional[str]
|
Where the preview page's assets live. |
None
|
log
|
Optional[Logger]
|
Where to print client logs and connection events. |
None
|
watch
|
bool
|
Whether to run the file watcher. |
True
|
Methods:
| Name | Description |
|---|---|
start |
Bind the socket on a background thread and return the address. |
stop |
Close every connection and stop the thread. |
set_preview_channel |
Install the handler for the browser preview's bridge traffic. |
add_change_listener |
Be told (on the watcher thread) when sources change; returns an unsubscribe. |
manifest |
The |
status |
The |
Attributes:
| Name | Type | Description |
|---|---|---|
snapshot |
SourceSnapshot
|
The current source snapshot. |
clients |
List[DevClient]
|
Connected dev clients (a copy). |
start
¶
start() -> ServerInfo
Bind the socket on a background thread and return the address.
Raises:
| Type | Description |
|---|---|
OSError
|
When the port is taken (the CLI prints a hint). |
set_preview_channel
¶
set_preview_channel(
channel: Optional[PreviewChannel],
) -> None
Install the handler for the browser preview's bridge traffic.
add_change_listener
¶
add_change_listener(
listener: Callable[
[SourceChange, SourceSnapshot], None
],
) -> Callable[[], None]
Be told (on the watcher thread) when sources change; returns an unsubscribe.
ServerInfo
dataclass
¶
What the server is serving and where.
Methods:
| Name | Description |
|---|---|
url |
The HTTP base URL, substituting |
ws_url |
The dev-client WebSocket URL. |
Attributes:
| Name | Type | Description |
|---|---|---|
display_host |
str
|
A host suitable for a URL ( |
FileWatcher
¶
FileWatcher(
root: str,
on_change: Callable[
[SourceChange, SourceSnapshot], None
],
*,
subdirs: Sequence[str] = ("app",),
interval: float = 0.25,
settle: float = 0.08
)
Poll a source tree and report changes on a background thread.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
str
|
Project root. |
required |
on_change
|
Callable[[SourceChange, SourceSnapshot], None]
|
Called with a
|
required |
subdirs
|
Sequence[str]
|
Directories under |
('app',)
|
interval
|
float
|
Seconds between polls. |
0.25
|
settle
|
float
|
Seconds a change must be stable before it is reported, so an editor's write-then-rename or a multi-file save lands as one reload instead of several. |
0.08
|
Methods:
| Name | Description |
|---|---|
start |
Start polling on a daemon thread (idempotent). |
stop |
Stop polling and join the thread. |
poll |
Scan once and return the change since the last scan (or |
Attributes:
| Name | Type | Description |
|---|---|---|
snapshot |
SourceSnapshot
|
The most recent snapshot. |
poll
¶
poll() -> Optional[SourceChange]
Scan once and return the change since the last scan (or None).
SourceSnapshot
dataclass
¶
SourceSnapshot(
root: str,
files: Dict[str, str] = dict(),
mtimes: Dict[str, Tuple[int, int]] = dict(),
version: str = "",
)
The state of a source tree at one instant.
Attributes:
| Name | Type | Description |
|---|---|---|
root |
str
|
Absolute directory the relative paths are resolved against
(the project root; paths start with |
files |
Dict[str, str]
|
|
mtimes |
Dict[str, Tuple[int, int]]
|
|
version |
str
|
A digest of the whole tree, stable across processes for identical contents. |
Methods:
| Name | Description |
|---|---|
read |
Return the current bytes of |
diff |
Describe how to get from |
lan_addresses
¶
Best-effort list of this machine's non-loopback IPv4 addresses.
Used to print a URL a physical device on the same Wi-Fi can reach.
snapshot_sources
¶
snapshot_sources(
root: str,
subdirs: Sequence[str] = ("app",),
*,
previous: Optional[SourceSnapshot] = None
) -> SourceSnapshot
Hash every synced file under root/<subdir> for each of subdirs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
str
|
Project root. |
required |
subdirs
|
Sequence[str]
|
Directories (relative to |
('app',)
|
previous
|
Optional[SourceSnapshot]
|
An earlier snapshot; files whose |
None
|
Server¶
The dev server: HTTP for static assets, WebSocket for live peers.
One DevServer runs on its
own thread with a private asyncio loop, so it works both inside
pn start (whose main thread runs the browser preview's app) and in
tests. It exposes:
GET /: the browser preview page.GET /static/<name>: preview assets (JS, CSS).GET /manifest:{"version", "entry", "files": {path: sha256}}.GET /file/<path>: raw bytes of one synced source file.GET /status: server, project, and connected-peer information.WS /ws?role=client: the dev-client protocol (see below).WS /ws?role=preview: the browser preview's bridge channel; the server only relays text frames between the page and thePreviewChannelhandler installed by the preview.
Dev-client protocol (JSON objects, one per text frame):
- client -> server
hello:{"type": "hello", "platform", "device", "app", "files": {path: sha256}}describing what the client already holds in its overlay. - server -> client
sync:{"type": "sync", "version", "entry", "files": [{"path", "sha256", "content", "encoding"}], "removed": [...]}bringing the client up to date. The same shape is sent as"update"whenever the watcher sees a change. - client -> server
log(level,text),error(phase,text),reloaded(version,mode,modules): streamed to the terminal.
Classes:
| Name | Description |
|---|---|
ServerInfo |
What the server is serving and where. |
PreviewPeer |
A connected browser preview page; |
PreviewChannel |
What the preview installs to receive the page's bridge traffic. |
DevServer |
Serve sources, assets, and the live protocol for one project. |
Functions:
| Name | Description |
|---|---|
lan_addresses |
Best-effort list of this machine's non-loopback IPv4 addresses. |
Attributes:
| Name | Type | Description |
|---|---|---|
DEFAULT_PORT |
Default port for |
ServerInfo
dataclass
¶
What the server is serving and where.
Methods:
| Name | Description |
|---|---|
url |
The HTTP base URL, substituting |
ws_url |
The dev-client WebSocket URL. |
Attributes:
| Name | Type | Description |
|---|---|---|
display_host |
str
|
A host suitable for a URL ( |
PreviewPeer
¶
PreviewPeer(server: 'DevServer', writer: StreamWriter)
PreviewChannel
¶
Bases: Protocol
What the preview installs to receive the page's bridge traffic.
Every method is called on the server thread; implementations hop to their own thread as needed.
Methods:
| Name | Description |
|---|---|
on_preview_connected |
A page connected ( |
on_preview_message |
A text frame arrived from the page. |
on_preview_disconnected |
The page went away. |
on_preview_connected
¶
on_preview_connected(
peer: PreviewPeer, info: Dict[str, Any]
) -> None
A page connected (info carries its query parameters).
on_preview_message
¶
on_preview_message(peer: PreviewPeer, text: str) -> None
A text frame arrived from the page.
DevClient
dataclass
¶
DevServer
¶
DevServer(
project_root: str,
entry_module: str = "app.main",
*,
host: str = "0.0.0.0",
port: int = DEFAULT_PORT,
project_name: str = "",
static_dir: Optional[str] = None,
log: Optional[Logger] = None,
watch: bool = True
)
Serve sources, assets, and the live protocol for one project.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
project_root
|
str
|
Directory containing |
required |
entry_module
|
str
|
The app's entry module ( |
'app.main'
|
host
|
str
|
Bind address. |
'0.0.0.0'
|
port
|
int
|
TCP port; |
DEFAULT_PORT
|
project_name
|
str
|
Shown in the preview page and |
''
|
static_dir
|
Optional[str]
|
Where the preview page's assets live. |
None
|
log
|
Optional[Logger]
|
Where to print client logs and connection events. |
None
|
watch
|
bool
|
Whether to run the file watcher. |
True
|
Methods:
| Name | Description |
|---|---|
start |
Bind the socket on a background thread and return the address. |
stop |
Close every connection and stop the thread. |
set_preview_channel |
Install the handler for the browser preview's bridge traffic. |
add_change_listener |
Be told (on the watcher thread) when sources change; returns an unsubscribe. |
manifest |
The |
status |
The |
Attributes:
| Name | Type | Description |
|---|---|---|
snapshot |
SourceSnapshot
|
The current source snapshot. |
clients |
List[DevClient]
|
Connected dev clients (a copy). |
start
¶
start() -> ServerInfo
Bind the socket on a background thread and return the address.
Raises:
| Type | Description |
|---|---|
OSError
|
When the port is taken (the CLI prints a hint). |
set_preview_channel
¶
set_preview_channel(
channel: Optional[PreviewChannel],
) -> None
Install the handler for the browser preview's bridge traffic.
add_change_listener
¶
add_change_listener(
listener: Callable[
[SourceChange, SourceSnapshot], None
],
) -> Callable[[], None]
Be told (on the watcher thread) when sources change; returns an unsubscribe.
File watcher and source snapshots¶
Source snapshots and change detection for the dev server.
The dev server treats the project's app/ directory as a content-
addressed tree: every dev client gets the same manifest (relative path
to SHA-256), so a client can tell exactly which files it is missing
after a reconnect, and a change notification can carry the new bytes.
FileWatcher polls the
tree with os.stat rather than a native file-system event API so it
behaves identically on every host OS without a dependency; the poll
interval is short enough that saves feel instant.
Classes:
| Name | Description |
|---|---|
SourceSnapshot |
The state of a source tree at one instant. |
SourceChange |
Paths that changed (added or modified) and paths that disappeared. |
FileWatcher |
Poll a source tree and report changes on a background thread. |
Functions:
| Name | Description |
|---|---|
is_synced_file |
Whether a file under |
snapshot_sources |
Hash every synced file under |
MAX_SYNC_FILE_BYTES
module-attribute
¶
Files larger than this are left out of the sync set (they belong in a real build).
SourceSnapshot
dataclass
¶
SourceSnapshot(
root: str,
files: Dict[str, str] = dict(),
mtimes: Dict[str, Tuple[int, int]] = dict(),
version: str = "",
)
The state of a source tree at one instant.
Attributes:
| Name | Type | Description |
|---|---|---|
root |
str
|
Absolute directory the relative paths are resolved against
(the project root; paths start with |
files |
Dict[str, str]
|
|
mtimes |
Dict[str, Tuple[int, int]]
|
|
version |
str
|
A digest of the whole tree, stable across processes for identical contents. |
Methods:
| Name | Description |
|---|---|
read |
Return the current bytes of |
diff |
Describe how to get from |
SourceChange
dataclass
¶
Paths that changed (added or modified) and paths that disappeared.
FileWatcher
¶
FileWatcher(
root: str,
on_change: Callable[
[SourceChange, SourceSnapshot], None
],
*,
subdirs: Sequence[str] = ("app",),
interval: float = 0.25,
settle: float = 0.08
)
Poll a source tree and report changes on a background thread.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
str
|
Project root. |
required |
on_change
|
Callable[[SourceChange, SourceSnapshot], None]
|
Called with a
|
required |
subdirs
|
Sequence[str]
|
Directories under |
('app',)
|
interval
|
float
|
Seconds between polls. |
0.25
|
settle
|
float
|
Seconds a change must be stable before it is reported, so an editor's write-then-rename or a multi-file save lands as one reload instead of several. |
0.08
|
Methods:
| Name | Description |
|---|---|
start |
Start polling on a daemon thread (idempotent). |
stop |
Stop polling and join the thread. |
poll |
Scan once and return the change since the last scan (or |
Attributes:
| Name | Type | Description |
|---|---|---|
snapshot |
SourceSnapshot
|
The most recent snapshot. |
poll
¶
poll() -> Optional[SourceChange]
Scan once and return the change since the last scan (or None).
is_synced_file
¶
Whether a file under app/ takes part in dev sync.
Editor swap files, byte-code caches, and VCS metadata are skipped; everything else (Python modules and data files an app reads at runtime) is synced so the on-device overlay mirrors the source tree.
snapshot_sources
¶
snapshot_sources(
root: str,
subdirs: Sequence[str] = ("app",),
*,
previous: Optional[SourceSnapshot] = None
) -> SourceSnapshot
Hash every synced file under root/<subdir> for each of subdirs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
str
|
Project root. |
required |
subdirs
|
Sequence[str]
|
Directories (relative to |
('app',)
|
previous
|
Optional[SourceSnapshot]
|
An earlier snapshot; files whose |
None
|
WebSocket¶
A small RFC 6455 (WebSocket) implementation on the standard library.
The dev server and the on-device dev client both need WebSockets, and neither can assume a third-party package: the client runs inside the embedded interpreter on iOS and Android, where every dependency has to be bundled. This module provides just enough of the protocol for a trusted development network:
encode_frameandFrameDecoderhandle the wire format (masking, 7/16/64-bit lengths, fragmentation, control frames).server_handshakeandclient_handshake_requestbuild the HTTP upgrade.WebSocketClientis a blocking client meant to live on a background thread (the dev client uses one; the main thread never waits on the network).
Extensions (compression) and subprotocols are not negotiated.
Classes:
| Name | Description |
|---|---|
WebSocketError |
A framing or protocol violation on the connection. |
HandshakeError |
The HTTP upgrade did not complete. |
FrameDecoder |
Incremental frame parser that reassembles fragmented messages. |
WebSocketClient |
A blocking WebSocket client for background threads. |
Functions:
| Name | Description |
|---|---|
accept_key |
Return the |
parse_http_headers |
Split an HTTP request or response head into |
server_handshake |
Build the |
client_handshake_request |
Build a client upgrade request; returns |
encode_frame |
Encode one frame. |
encode_close |
Encode a close frame carrying |
MAX_MESSAGE_BYTES
module-attribute
¶
Upper bound on one reassembled message; anything larger is a protocol error.
HandshakeError
¶
Bases: WebSocketError
The HTTP upgrade did not complete.
FrameDecoder
¶
Incremental frame parser that reassembles fragmented messages.
Feed raw bytes with feed;
it yields complete (opcode, payload) messages. Control frames
(ping, pong, close) are yielded as they arrive, even in the middle
of a fragmented data message, as the RFC allows.
Methods:
| Name | Description |
|---|---|
feed |
Consume |
WebSocketClient
¶
A blocking WebSocket client for background threads.
recv blocks until a text message arrives and transparently
answers pings; send_text is safe to call from any thread.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str
|
|
required |
timeout
|
Optional[float]
|
Connect and read timeout in seconds. Reads that time
out raise |
30.0
|
Methods:
| Name | Description |
|---|---|
connect |
Open the TCP connection and complete the upgrade handshake. |
send_text |
Send one text message (masked, as clients must). |
recv |
Block until a text message arrives. |
close |
Send a close frame (best effort) and shut the socket. |
Attributes:
| Name | Type | Description |
|---|---|---|
connected |
bool
|
Whether the socket is open. |
recv
¶
Block until a text message arrives.
Returns None once the peer closes. Raises socket.timeout
when the read timeout elapses with no data, so callers can poll
a shutdown flag.
accept_key
¶
Return the Sec-WebSocket-Accept value for client_key.
parse_http_headers
¶
Split an HTTP request or response head into (start_line, headers).
Header names are lower-cased. raw should be the bytes up to
(and optionally including) the blank line that ends the head.
server_handshake
¶
Build the 101 Switching Protocols response for an upgrade request.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
headers
|
Dict[str, str]
|
Lower-cased request headers (see
|
required |
Raises:
| Type | Description |
|---|---|
HandshakeError
|
When the request is not a WebSocket upgrade. |
client_handshake_request
¶
Build a client upgrade request; returns (request_bytes, key).
encode_frame
¶
Encode one frame.
Clients must send masked frames and servers unmasked ones; the caller picks. Text payloads must already be UTF-8 encoded.
pythonnative.devclient¶
The on-device dev client: sync sources from pn start and Fast Refresh.
A debug build of a PythonNative app is a dev client. On launch it
connects to the dev server (pn start / pn preview / pn run)
over WebSocket, reports the sources it already holds, receives whatever
is missing, and from then on applies every save as a Fast Refresh. Its
print output and errors stream back to the terminal running the
server, so the device log viewer is optional.
How the pieces fit:
- The native template configures a writable overlay directory
(
pythonnative.hot_reload.configure_dev_environment) ahead of the bundled sources onsys.pathand exposes the server URL the CLI baked in asPN_DEV_SERVER.pythonnative.bootstrap.start(dev=True)callsstart_if_configured. - A build made with
pn run <platform> --dev-clienthas no app of its own: its bundledapp/main.pyrendersConnectScreen, where the developer types (or picks) a server URL. Once the first sync lands, the realapp.mainfrom the overlay shadows the placeholder and the screen remounts into the developer's app. The URL is remembered for the next launch.
All network I/O runs on a daemon thread; file writes happen there too,
and only the reload itself hops to the main thread through
call_on_main_thread.
Classes:
| Name | Description |
|---|---|
DevClient |
Keep this process in sync with a dev server. |
Functions:
| Name | Description |
|---|---|
normalize_server_url |
Turn whatever the developer typed into a dev-client WebSocket URL. |
saved_server_url |
The URL remembered by a previous connection ( |
current |
The running dev client, if any. |
start |
Start (or replace) the process-wide dev client for |
stop |
Stop the process-wide dev client. |
start_if_configured |
Start the dev client when the build points at a server. |
Attributes:
| Name | Type | Description |
|---|---|---|
ConnectScreen |
Any
|
Root component of |
SERVER_URL_ENV
module-attribute
¶
Environment variable carrying the dev server URL baked in by pn run.
DEV_CLIENT_ENV
module-attribute
¶
Set to 1 in --dev-client builds (the connect screen remembers URLs).
ConnectScreen
module-attribute
¶
ConnectScreen: Any = _LazyConnectScreen()
Root component of --dev-client builds until the first sync arrives.
DevClient
¶
DevClient(
url: str,
overlay: str,
*,
entry_module: str = "app.main",
forward_logs: bool = True,
log: Optional[Logger] = None
)
Keep this process in sync with a dev server.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str
|
Server URL (any form accepted by
|
required |
overlay
|
str
|
Writable directory that shadows the bundled sources. |
required |
entry_module
|
str
|
The app's entry module, for the |
'app.main'
|
forward_logs
|
bool
|
Mirror |
True
|
log
|
Optional[Logger]
|
Local logger for the client's own status lines. |
None
|
Methods:
| Name | Description |
|---|---|
add_listener |
Subscribe to |
start |
Connect on a daemon thread (idempotent). |
stop |
Disconnect and stop the thread. |
send |
Queue a message to the server (dropped when the outbox overflows offline). |
log |
Forward one log line to the server. |
report_error |
Forward an error report (a traceback) to the server. |
Attributes:
| Name | Type | Description |
|---|---|---|
state |
str
|
|
normalize_server_url
¶
Turn whatever the developer typed into a dev-client WebSocket URL.
Accepts 192.168.1.20, 192.168.1.20:8765, http://host:port,
ws://host:port, and full URLs with a path; the result always
ends in /ws?role=client.
saved_server_url
¶
The URL remembered by a previous connection (--dev-client builds).
start
¶
Start (or replace) the process-wide dev client for url.
start_if_configured
¶
Start the dev client when the build points at a server.
Called by bootstrap.start(dev=True). The URL comes from
PN_DEV_SERVER (baked in by pn run) or, for --dev-client
builds, from the URL saved by a previous connection. Returns the
client, or None when nothing is configured.
pythonnative.preview¶
pn start / pn preview: the dev server plus the browser preview.
serve runs one process that does three
jobs:
- Dev server (
pythonnative.devserver): watchesapp/, syncs sources to every connected dev client (simulators, emulators, physical devices), and relays their logs to this terminal. - Browser preview: renders the app in a browser tab. The tab is a
bridge peer like any device; the reconciler runs in this process
on the main thread and commits through
WebTransport. - Fast Refresh for the preview: every save reloads the changed modules here and refreshes the mounted screens, exactly as the dev client does on device.
The main thread runs the transport's main loop; that is the browser's stand-in for the UIKit / Android main queue. Everything else (sockets, the file watcher) lives on daemon threads and marshals work onto it.
PN_PLATFORM=web must be set before pythonnative is imported so
platform detection binds to the browser backend; the CLI re-execs
itself to guarantee that.
Classes:
| Name | Description |
|---|---|
PreviewSession |
Everything one |
Functions:
| Name | Description |
|---|---|
serve |
Run the dev server (and browser preview) until interrupted. |
PreviewSession
¶
PreviewSession(
project_root: str,
entry_module: str,
*,
host: str = "0.0.0.0",
port: int = 8765,
project_name: str = "",
log: Optional[Logger] = None
)
serve
¶
serve(
entry_module: str,
*,
project_root: Optional[str] = None,
host: str = "0.0.0.0",
port: int = 8765,
project_name: str = "",
open_browser: bool = False,
log: Optional[Logger] = None,
banner: bool = True,
ready: Optional[Callable[[PreviewSession], None]] = None
) -> None
Run the dev server (and browser preview) until interrupted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entry_module
|
str
|
The app's entry module ( |
required |
project_root
|
Optional[str]
|
Directory containing |
None
|
host
|
str
|
Bind address ( |
'0.0.0.0'
|
port
|
int
|
TCP port ( |
8765
|
project_name
|
str
|
Shown in the preview page. |
''
|
open_browser
|
bool
|
Open the preview page in the default browser. |
False
|
log
|
Optional[Logger]
|
Where status lines go (stderr by default). |
None
|
banner
|
bool
|
Print the connection banner. |
True
|
ready
|
Optional[Callable[[PreviewSession], None]]
|
Called once the server is listening (tests). |
None
|
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
OSError
|
If the port is taken. |
pythonnative.bridge.web¶
The browser preview's half of the bridge.
pn preview renders an app in a browser tab. Rather than a second
rendering backend, the page is treated as the native runtime: it
receives the very same JSON transactions the Swift and Kotlin runtimes
apply, answers the same synchronous measure / command /
animate / call requests, and raises the same
callback(kind, tag, name, payload) events. The reconciler therefore
runs the on-device
BridgeBackend
and the on-device
NativeScreenHost
unchanged; only this transport differs, and it is about moving strings
over a WebSocket.
Threads:
- The main thread owns the framework: it drains
WebTransport.run_main_loop, which is the browser's stand-in for the UIKit / Android main queue. Every callback from the page and every asyncio pump runs there. - The dev server thread owns the socket. It delivers page messages
to the transport, which either settles a waiting request (
res) or queues work for the main thread.
Synchronous requests (measure above all) block the main thread on a
threading.Event until the page answers; the page is single-threaded
but its message handling is asynchronous, so it can answer a
measure while it is itself awaiting Python (a row bind, say).
Wire format (JSON arrays):
- Python -> page:
["apply", ops],["measure", id, tag, w, h],["command", id, tag, name, args],["animate", id, tag, request],["call", id, module, method, envelope],["res", id, result],["dev", {...}]. - Page -> Python:
["res", id, result],["cb", kind, tag, name, payload](fire and forget),["req", id, kind, tag, name, payload](Python answers withres),["gesture", tag, phase, info](pointer stream for the Python gesture arbiter),["dev", {...}].
payload / args / request / result are JSON strings,
exactly the text the native protocol carries, so both sides reuse their
existing codecs. Modules the page implements (Host, Alert,
Clipboard, ...) are called there; every other native module falls
back to the Python implementations in
pythonnative.native_modules.fallback.
Classes:
| Name | Description |
|---|---|
WebTransport |
Bridge transport whose native side is a browser page. |
Attributes:
| Name | Type | Description |
|---|---|---|
BROWSER_MODULES |
Native modules the preview page implements; the rest use Python fallbacks. |
BROWSER_MODULES
module-attribute
¶
BROWSER_MODULES = frozenset(
{
"Host",
"Alert",
"Clipboard",
"Linking",
"Share",
"Haptics",
"NetInfo",
"AppState",
"Device",
}
)
Native modules the preview page implements; the rest use Python fallbacks.
REQUEST_TIMEOUT_S
module-attribute
¶
How long a synchronous request waits for the page before giving up.
WebTransport
¶
Bridge transport whose native side is a browser page.
Install it with pythonnative.bridge.set_transport and hand it to
DevServer.set_preview_channel; the page does the rest.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
log
|
Optional[Callable[[str], None]]
|
Where diagnostics go (defaults to stderr). |
None
|
Methods:
| Name | Description |
|---|---|
protocol_version |
The page speaks whatever this package speaks; they ship together. |
set_callback |
Install the native -> Python entry point ( |
apply |
Forward one commit to the page (fire and forget). |
measure |
Ask the page for the intrinsic size of |
command |
Run an imperative command on one view; returns its JSON result or |
animate |
Handle an animation request ( |
call |
Call a native module: |
post_to_main |
Queue |
run_main_loop |
Drain main-thread work until |
drain_main |
Run queued main-thread work inline (tests); returns how many jobs ran. |
stop |
Make |
on_preview_connected |
A page connected; it becomes the native side. |
on_preview_disconnected |
The page went away: fail waiting requests and tear down its screens. |
on_preview_message |
Route one frame from the page. |
send_dev |
Send a |
Attributes:
| Name | Type | Description |
|---|---|---|
on_dev_message |
Optional[Callable[[Dict[str, Any]], None]]
|
Hook for |
on_peer_changed |
Optional[Callable[[bool], None]]
|
Called on the main thread with |
connected |
bool
|
Whether a page is attached. |
on_dev_message
instance-attribute
¶
Hook for ["dev", {...}] messages from the page (runs on the main thread).
on_peer_changed
instance-attribute
¶
Called on the main thread with True on connect and False on disconnect.
protocol_version
¶
protocol_version() -> int
The page speaks whatever this package speaks; they ship together.
set_callback
¶
Install the native -> Python entry point (bridge.native_callback).
measure
¶
Ask the page for the intrinsic size of tag.
command
¶
Run an imperative command on one view; returns its JSON result or None.
animate
¶
Handle an animation request (set / start / cancel) for one view.
call
¶
Call a native module: Host.post locally, page modules over the wire, others in Python.
post_to_main
¶
post_to_main(fn: Callable[[], None]) -> None
Queue fn for the main loop (never runs inline).
run_main_loop
¶
Drain main-thread work until stop is called (or until holds).
This is the preview's event loop: the browser's stand-in for the
platform main queue. Call it from the thread that should own the
framework (the process main thread under pn preview).
drain_main
¶
Run queued main-thread work inline (tests); returns how many jobs ran.
on_preview_connected
¶
A page connected; it becomes the native side.
on_preview_disconnected
¶
on_preview_disconnected(peer: Any) -> None
The page went away: fail waiting requests and tear down its screens.
on_preview_message
¶
Route one frame from the page.
pythonnative.project.fingerprint¶
Native build fingerprints: know when pn run can skip the toolchain.
A debug build only has to be rebuilt when one of its native inputs
changes: pythonnative.toml, the bundled native template, the
pythonnative package itself, project-local native plugins, or the
build flavor (platform, SDK, release). Edits under app/ don't count;
the dev server syncs those into the running app and Fast Refresh applies
them, exactly as Metro does for a React Native debug build.
compute hashes those
inputs into one hex digest. pn run writes it next to the build
after a successful toolchain run (see
write_stamp) and, when
the digest is unchanged and a dev server is running to deliver the
latest sources, reinstalls the previous artifact instead of staging and
compiling again.
The hash covers file contents, not mtimes, so touching a file or re-cloning the repository doesn't invalidate a build.
Functions:
| Name | Description |
|---|---|
hash_tree |
Hash every file under |
compute |
Digest every native input of a build. |
read_stamp |
Return the stamp written by the last successful build, or |
write_stamp |
Record |
Attributes:
| Name | Type | Description |
|---|---|---|
STAMP_NAME |
File written into |
STAMP_NAME
module-attribute
¶
File written into build/<platform>/ after a successful native build.
hash_tree
¶
Hash every file under root (relative path + contents), deterministically.
Build outputs, caches, and bytecode are skipped so a checkout that has been built hashes the same as a fresh one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
Path
|
Directory (or single file) to hash. A missing path hashes as the empty tree. |
required |
into
|
Optional['hashlib._Hash']
|
An existing hasher to feed instead of creating one. |
None
|
Returns:
| Type | Description |
|---|---|
str
|
The hex digest (of |
compute
¶
compute(
config: AppConfig,
platform: str,
*,
template_root: Path,
lib_root: Path,
release: bool = False,
ios_sdks: Sequence[str] = (),
extra: Optional[Dict[str, str]] = None
) -> str
Digest every native input of a build.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
AppConfig
|
The loaded project configuration (its file is hashed). |
required |
platform
|
str
|
|
required |
template_root
|
Path
|
The bundled native template directory. |
required |
lib_root
|
Path
|
The |
required |
release
|
bool
|
Release builds are distinct from debug builds. |
False
|
ios_sdks
|
Sequence[str]
|
The iOS SDK slices being staged. |
()
|
extra
|
Optional[Dict[str, str]]
|
Additional key/value inputs (host arch, tool versions). |
None
|
Returns:
| Type | Description |
|---|---|
str
|
A hex SHA-256 digest. |
read_stamp
¶
Return the stamp written by the last successful build, or None.
Next steps¶
- Fast Refresh and the hot reload API for the device-side reload.
- Browser preview.