Skip to content

Dev server and dev client

The development loop is a server process (pn start) and any number of clients: the browser preview, PythonNative Go, 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.

The server only answers peers that present the per-user dev token; see pythonnative.devserver.auth.

Modules:

Name Description
auth

The per-user development token that guards the dev server.

compat

Decide whether a dev client can run the project pn start serves.

editor

Open a source location from DevTools or an error screen in the developer's editor.

hub

Route DevTools and debugger traffic between pages, the preview, and dev clients.

packages

Sync pure-Python packages from the developer's environment to dev clients.

qr

A small, dependency-free QR code encoder for the dev server.

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
load_token

Return this user's dev token, creating it on first use.

lan_addresses

Best-effort list of this machine's non-loopback IPv4 addresses.

snapshot_sources

Hash every synced file under root/<subdir> for each of subdirs.

Attributes:

Name Type Description
DEFAULT_PORT

Default port for pn start / pn preview.

DEFAULT_PORT module-attribute

DEFAULT_PORT = 8765

Default port for pn start / pn preview.

DevServer

DevServer(
    project_root: str,
    entry_module: str = "app.main",
    *,
    host: str = "0.0.0.0",
    port: int = DEFAULT_PORT,
    project_name: str = "",
    token: Optional[str] = None,
    static_dir: Optional[str] = None,
    log: Optional[Logger] = None,
    watch: bool = True,
    config: Any = None,
    debug_port: Optional[int] = None,
    open_devtools: Optional[Callable[[], None]] = None
)

Serve sources, assets, and the live protocol for one project.

Parameters:

Name Type Description Default
project_root str

Directory containing app/ and pythonnative.toml.

required
entry_module str

The app's entry module ("app.main").

'app.main'
host str

Bind address. 0.0.0.0 so devices on the LAN can reach it; pass 127.0.0.1 to stay local.

'0.0.0.0'
port int

TCP port; 0 picks a free one.

DEFAULT_PORT
project_name str

Shown in the preview page and /status.

''
token Optional[str]

The dev token clients must present; defaults to this user's token from load_token.

None
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
config Any

The project's AppConfig, for compatibility checks, package sync, and the app's connect scheme.

None
debug_port Optional[int]

Port of the debugger proxy on 127.0.0.1 (0 picks a free one, None disables it).

None
open_devtools Optional[Callable[[], None]]

Called when an app's dev menu asks to open DevTools.

None

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.

app_scheme

The connect scheme of the project's own debug build, when the config names an app id.

connect_links

Connect links for PythonNative Go and the project's own debug build.

preview_target

Register the browser preview's in-process agent as a DevTools target (any thread).

publish_local

Publish an event for an in-process target (any thread).

broadcast_command

Send reload or menu to every connected dev client (any thread).

manifest

The /manifest document.

status

The /status document.

Attributes:

Name Type Description
snapshot SourceSnapshot

The current source snapshot.

clients List[DevClient]

Connected dev clients (a copy).

project_runtime Any

What the project needs from a dev client (computed once, on first use).

snapshot property

snapshot: SourceSnapshot

The current source snapshot.

clients property

clients: List[DevClient]

Connected dev clients (a copy).

project_runtime property

project_runtime: Any

What the project needs from a dev client (computed once, on first use).

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).

stop

stop() -> None

Close every connection and stop the thread.

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.

app_scheme

app_scheme() -> Optional[str]

The connect scheme of the project's own debug build, when the config names an app id.

connect_links(host: Optional[str] = None) -> Dict[str, Any]

Connect links for PythonNative Go and the project's own debug build.

The preferred link is Go's when the project can run in it.

preview_target

preview_target(agent: Any) -> None

Register the browser preview's in-process agent as a DevTools target (any thread).

publish_local

publish_local(
    target_id: str, topic: str, data: Any
) -> None

Publish an event for an in-process target (any thread).

broadcast_command

broadcast_command(name: str) -> None

Send reload or menu to every connected dev client (any thread).

manifest

manifest() -> Dict[str, Any]

The /manifest document.

status

status() -> Dict[str, Any]

The /status document.

ServerInfo dataclass

ServerInfo(
    host: str,
    port: int,
    project_root: str,
    entry_module: str,
    project_name: str = "",
    token: str = "",
)

What the server is serving and where.

Methods:

Name Description
url

The HTTP base URL, substituting host for the bind address.

preview_url

The browser preview URL, carrying the dev token once.

ws_url

The dev-client WebSocket URL, including the dev token.

devtools_url

The DevTools page URL, carrying the dev token once.

connect_url

The server URL a connect link carries (http://host:port/?token=...).

Attributes:

Name Type Description
display_host str

A host suitable for a URL (0.0.0.0 becomes localhost).

display_host property

display_host: str

A host suitable for a URL (0.0.0.0 becomes localhost).

url

url(host: Optional[str] = None) -> str

The HTTP base URL, substituting host for the bind address.

preview_url

preview_url(host: Optional[str] = None) -> str

The browser preview URL, carrying the dev token once.

ws_url

ws_url(host: Optional[str] = None) -> str

The dev-client WebSocket URL, including the dev token.

devtools_url

devtools_url(host: Optional[str] = None) -> str

The DevTools page URL, carrying the dev token once.

connect_url

connect_url(host: Optional[str] = None) -> str

The server URL a connect link carries (http://host:port/?token=...).

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 SourceChange and the new snapshot after every poll that found changes.

required
subdirs Sequence[str]

Directories under root to watch.

('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 None).

Attributes:

Name Type Description
snapshot SourceSnapshot

The most recent snapshot.

snapshot property

snapshot: SourceSnapshot

The most recent snapshot.

start

start() -> None

Start polling on a daemon thread (idempotent).

stop

stop() -> None

Stop polling and join the thread.

poll

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 app/).

files Dict[str, str]

relative_path -> sha256 for every synced file.

mtimes Dict[str, Tuple[int, int]]

relative_path -> (mtime_ns, size) used to skip re-hashing unchanged files between polls.

version str

A digest of the whole tree, stable across processes for identical contents.

Methods:

Name Description
read

Return the current bytes of rel_path (None if it is gone).

diff

Describe how to get from self to other.

read

read(rel_path: str) -> Optional[bytes]

Return the current bytes of rel_path (None if it is gone).

diff

diff(other: 'SourceSnapshot') -> 'SourceChange'

Describe how to get from self to other.

load_token

load_token() -> str

Return this user's dev token, creating it on first use.

PN_DEV_TOKEN wins when set. Otherwise the token file is read, or created with mode 0600 (its directory with 0700) holding secrets.token_urlsafe(24). A file that other users can read is tightened to 0600; an empty one is replaced.

lan_addresses

lan_addresses() -> List[str]

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 root) to include.

('app',)
previous Optional[SourceSnapshot]

An earlier snapshot; files whose (mtime, size) are unchanged reuse their digest instead of being re-read.

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 and in tests independently of the Python application loop. It exposes:

  • GET /: the browser preview page. With a valid ?token=, the response instead sets the pn_token cookie and redirects to /, so the token doesn't stay in the address bar.
  • 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 /assets/<path>: a file under app/assets/; /assets/pn_assets.json is the asset manifest and /assets/fonts.css declares the bundled fonts for the browser preview.
  • GET /status: server, project, and connected-peer information.
  • GET /devtools: the DevTools page (/devtools/<name> for its assets), authenticated like /.
  • GET /connect: connect links, server URLs, and their QR codes.
  • 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 the PreviewChannel handler installed by the preview.

Every route except the page and /static/ requires the per-user dev token (see pythonnative.devserver.auth) as a token query parameter, an X-PN-Token header, or the pn_token cookie. Requests without it get 401; requests with a wrong one get 403. A WebSocket upgrade that carries an Origin header (every browser sends one) must also come from the server's own origin: the Origin host and port must equal the Host header. Native dev clients send no Origin.

Dev session protocol, version 2 (JSON objects, one per text frame; see RFC 0006 and pythonnative.devclient):

  • client -> server hello: {"type": "hello", "session": 2, "client": "go" | "app", "platform", "device", "app", "runtime": {...}, "files": {path: sha256}, "packages": {name: version}}.
  • server -> client incompatible (reason, fix) when the client can't run the project (see pythonnative.devserver.compat), or sync: {"type": "sync", "version", "entry", "project", "files": [{"path", "sha256", "content", "encoding"}], "removed": [...]} followed by packages for missing pure-Python distributions. The sync shape is sent as "update" whenever the watcher sees a change.
  • server -> client rpc (id, method, params), tunnel, menu, and reload; client -> server rpc_result, event (topic, data), tunnel, and packages_ready.
  • client -> server log (level, text), error (phase, text), reloaded (version, mode, modules): streamed to the terminal and DevTools.

WS /ws?role=devtools carries DevTools pages (see pythonnative.devserver.hub), and a separate listener on 127.0.0.1:5678 accepts Debug Adapter Protocol connections.

Classes:

Name Description
ServerInfo

What the server is serving and where.

PreviewPeer

A connected browser preview page; send is safe from any thread.

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 pn start / pn preview.

DEFAULT_DEBUG_PORT

Default port of the debugger proxy (debugpy's conventional port).

GO_APP_ID

PythonNative Go's application id; its connect scheme is pn-com.pythonnative.go.

DEFAULT_PORT module-attribute

DEFAULT_PORT = 8765

Default port for pn start / pn preview.

DEFAULT_DEBUG_PORT module-attribute

DEFAULT_DEBUG_PORT = 5678

Default port of the debugger proxy (debugpy's conventional port).

GO_APP_ID module-attribute

GO_APP_ID = 'com.pythonnative.go'

PythonNative Go's application id; its connect scheme is pn-com.pythonnative.go.

ServerInfo dataclass

ServerInfo(
    host: str,
    port: int,
    project_root: str,
    entry_module: str,
    project_name: str = "",
    token: str = "",
)

What the server is serving and where.

Methods:

Name Description
url

The HTTP base URL, substituting host for the bind address.

preview_url

The browser preview URL, carrying the dev token once.

ws_url

The dev-client WebSocket URL, including the dev token.

devtools_url

The DevTools page URL, carrying the dev token once.

connect_url

The server URL a connect link carries (http://host:port/?token=...).

Attributes:

Name Type Description
display_host str

A host suitable for a URL (0.0.0.0 becomes localhost).

display_host property

display_host: str

A host suitable for a URL (0.0.0.0 becomes localhost).

url

url(host: Optional[str] = None) -> str

The HTTP base URL, substituting host for the bind address.

preview_url

preview_url(host: Optional[str] = None) -> str

The browser preview URL, carrying the dev token once.

ws_url

ws_url(host: Optional[str] = None) -> str

The dev-client WebSocket URL, including the dev token.

devtools_url

devtools_url(host: Optional[str] = None) -> str

The DevTools page URL, carrying the dev token once.

connect_url

connect_url(host: Optional[str] = None) -> str

The server URL a connect link carries (http://host:port/?token=...).

PreviewPeer

PreviewPeer(server: 'DevServer', writer: StreamWriter)

A connected browser preview page; send is safe from any thread.

Methods:

Name Description
send

Queue one text frame to the page (dropped once the peer is closed).

close

Ask the server to close this page's socket.

send

send(text: str) -> None

Queue one text frame to the page (dropped once the peer is closed).

close

close() -> None

Ask the server to close this page's socket.

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 (info carries its query parameters).

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.

on_preview_disconnected

on_preview_disconnected(peer: PreviewPeer) -> None

The page went away.

DevClient dataclass

DevClient(
    id: int,
    writer: StreamWriter,
    platform: str = "unknown",
    device: str = "",
    app: str = "",
    kind: str = "app",
    connected_at: float = time(),
    files: Dict[str, str] = dict(),
    target: Optional[ClientTarget] = None,
    synced: bool = False,
)

One connected on-device dev client.

Methods:

Name Description
send

Queue one JSON message (call on the server loop).

label

A short human label for log lines.

send

send(message: Dict[str, Any]) -> None

Queue one JSON message (call on the server loop).

label

label() -> str

A short human label for log lines.

DevToolsPeer

DevToolsPeer(writer: StreamWriter)

A connected DevTools page; send is safe from the server loop.

Methods:

Name Description
send

Queue one JSON message to the page.

send

send(message: Dict[str, Any]) -> None

Queue one JSON message to the page.

DevServer

DevServer(
    project_root: str,
    entry_module: str = "app.main",
    *,
    host: str = "0.0.0.0",
    port: int = DEFAULT_PORT,
    project_name: str = "",
    token: Optional[str] = None,
    static_dir: Optional[str] = None,
    log: Optional[Logger] = None,
    watch: bool = True,
    config: Any = None,
    debug_port: Optional[int] = None,
    open_devtools: Optional[Callable[[], None]] = None
)

Serve sources, assets, and the live protocol for one project.

Parameters:

Name Type Description Default
project_root str

Directory containing app/ and pythonnative.toml.

required
entry_module str

The app's entry module ("app.main").

'app.main'
host str

Bind address. 0.0.0.0 so devices on the LAN can reach it; pass 127.0.0.1 to stay local.

'0.0.0.0'
port int

TCP port; 0 picks a free one.

DEFAULT_PORT
project_name str

Shown in the preview page and /status.

''
token Optional[str]

The dev token clients must present; defaults to this user's token from load_token.

None
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
config Any

The project's AppConfig, for compatibility checks, package sync, and the app's connect scheme.

None
debug_port Optional[int]

Port of the debugger proxy on 127.0.0.1 (0 picks a free one, None disables it).

None
open_devtools Optional[Callable[[], None]]

Called when an app's dev menu asks to open DevTools.

None

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.

app_scheme

The connect scheme of the project's own debug build, when the config names an app id.

connect_links

Connect links for PythonNative Go and the project's own debug build.

preview_target

Register the browser preview's in-process agent as a DevTools target (any thread).

publish_local

Publish an event for an in-process target (any thread).

broadcast_command

Send reload or menu to every connected dev client (any thread).

manifest

The /manifest document.

status

The /status document.

Attributes:

Name Type Description
snapshot SourceSnapshot

The current source snapshot.

clients List[DevClient]

Connected dev clients (a copy).

project_runtime Any

What the project needs from a dev client (computed once, on first use).

snapshot property

snapshot: SourceSnapshot

The current source snapshot.

clients property

clients: List[DevClient]

Connected dev clients (a copy).

project_runtime property

project_runtime: Any

What the project needs from a dev client (computed once, on first use).

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).

stop

stop() -> None

Close every connection and stop the thread.

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.

app_scheme

app_scheme() -> Optional[str]

The connect scheme of the project's own debug build, when the config names an app id.

connect_links(host: Optional[str] = None) -> Dict[str, Any]

Connect links for PythonNative Go and the project's own debug build.

The preferred link is Go's when the project can run in it.

preview_target

preview_target(agent: Any) -> None

Register the browser preview's in-process agent as a DevTools target (any thread).

publish_local

publish_local(
    target_id: str, topic: str, data: Any
) -> None

Publish an event for an in-process target (any thread).

broadcast_command

broadcast_command(name: str) -> None

Send reload or menu to every connected dev client (any thread).

manifest

manifest() -> Dict[str, Any]

The /manifest document.

status

status() -> Dict[str, Any]

The /status document.

lan_addresses

lan_addresses() -> List[str]

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.

Authentication

The per-user development token that guards the dev server.

pn start serves the application's source code, so everything that reads it has to prove it belongs to the developer running the server. The proof is a random token generated once per user and stored with mode 0600 in ~/.pythonnative/dev-token. It's reused across restarts, so debug builds that pn run launched keep connecting after the server restarts. Delete the file to issue a new token; every client then needs the new URL.

Clients present the token in one of three ways (see request_token):

  • the token query parameter (pn run bakes it into the dev-client URL, and pn start prints it in the preview URL),
  • the X-PN-Token header, or
  • the pn_token cookie, which the browser preview receives once in exchange for the query parameter.

Two environment variables override the stored token: PN_DEV_TOKEN supplies the token itself, and PN_DEV_TOKEN_FILE points at a different token file.

Functions:

Name Description
token_path

Where the token lives: $PN_DEV_TOKEN_FILE or ~/.pythonnative/dev-token.

token_source

Where load_token gets the token, for messages.

load_token

Return this user's dev token, creating it on first use.

token_matches

Compare candidate against token in constant time.

request_token

The token a request presents, if any.

with_token

Return url with its token query parameter set to token.

Attributes:

Name Type Description
TOKEN_ENV

Environment variable that supplies the token directly (skips the file).

TOKEN_FILE_ENV

Environment variable naming the token file (default ~/.pythonnative/dev-token).

QUERY_PARAM

Query parameter that carries the token in URLs.

HEADER_NAME

Request header that carries the token.

COOKIE_NAME

Cookie the browser preview holds after exchanging the query parameter.

TOKEN_ENV module-attribute

TOKEN_ENV = 'PN_DEV_TOKEN'

Environment variable that supplies the token directly (skips the file).

TOKEN_FILE_ENV module-attribute

TOKEN_FILE_ENV = 'PN_DEV_TOKEN_FILE'

Environment variable naming the token file (default ~/.pythonnative/dev-token).

QUERY_PARAM module-attribute

QUERY_PARAM = 'token'

Query parameter that carries the token in URLs.

HEADER_NAME module-attribute

HEADER_NAME = 'X-PN-Token'

Request header that carries the token.

COOKIE_NAME module-attribute

COOKIE_NAME = 'pn_token'

Cookie the browser preview holds after exchanging the query parameter.

token_path

token_path() -> Path

Where the token lives: $PN_DEV_TOKEN_FILE or ~/.pythonnative/dev-token.

token_source

token_source() -> str

Where load_token gets the token, for messages.

load_token

load_token() -> str

Return this user's dev token, creating it on first use.

PN_DEV_TOKEN wins when set. Otherwise the token file is read, or created with mode 0600 (its directory with 0700) holding secrets.token_urlsafe(24). A file that other users can read is tightened to 0600; an empty one is replaced.

token_matches

token_matches(candidate: Optional[str], token: str) -> bool

Compare candidate against token in constant time.

request_token

request_token(
    headers: Dict[str, str], query: Dict[str, str]
) -> Optional[str]

The token a request presents, if any.

Checks the token query parameter, then the X-PN-Token header, then the pn_token cookie. headers must have lower-cased names.

with_token

with_token(url: str, token: str) -> str

Return url with its token query parameter set to token.

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 app/ takes part in dev sync.

snapshot_sources

Hash every synced file under root/<subdir> for each of subdirs.

MAX_SYNC_FILE_BYTES module-attribute

MAX_SYNC_FILE_BYTES = 8 * 1024 * 1024

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 app/).

files Dict[str, str]

relative_path -> sha256 for every synced file.

mtimes Dict[str, Tuple[int, int]]

relative_path -> (mtime_ns, size) used to skip re-hashing unchanged files between polls.

version str

A digest of the whole tree, stable across processes for identical contents.

Methods:

Name Description
read

Return the current bytes of rel_path (None if it is gone).

diff

Describe how to get from self to other.

read

read(rel_path: str) -> Optional[bytes]

Return the current bytes of rel_path (None if it is gone).

diff

diff(other: 'SourceSnapshot') -> 'SourceChange'

Describe how to get from self to other.

SourceChange dataclass

SourceChange(
    changed: List[str], removed: List[str], version: str
)

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 SourceChange and the new snapshot after every poll that found changes.

required
subdirs Sequence[str]

Directories under root to watch.

('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 None).

Attributes:

Name Type Description
snapshot SourceSnapshot

The most recent snapshot.

snapshot property

snapshot: SourceSnapshot

The most recent snapshot.

start

start() -> None

Start polling on a daemon thread (idempotent).

stop

stop() -> None

Stop polling and join the thread.

poll

Scan once and return the change since the last scan (or None).

is_synced_file

is_synced_file(rel_path: str) -> bool

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 root) to include.

('app',)
previous Optional[SourceSnapshot]

An earlier snapshot; files whose (mtime, size) are unchanged reuse their digest instead of being re-read.

None

modules_for_paths

modules_for_paths(paths: Sequence[str]) -> List[str]

Map synced .py paths (app/screens/home.py) to dotted modules.

DevTools routing

Route DevTools and debugger traffic between pages, the preview, and dev clients.

The dev server's Hub keeps one Target per running app: the browser preview (whose agent lives in the pn start process) and every connected dev client. DevTools pages pick a target and send it requests; targets publish events, which the hub keeps a short history of and broadcasts to every page. Debugger connections are joined to the selected target's debug adapter.

Everything here runs on the dev server's own event loop.

Classes:

Name Description
Target

A running app DevTools can talk to.

LocalTarget

The browser preview: its agent runs in this process.

ClientTarget

A connected dev client (PythonNative Go or a debug build).

Hub

Targets, DevTools pages, event history, and debugger routing.

Page

Bases: Protocol

A connected DevTools page.

Methods:

Name Description
send

Queue one JSON message to the page.

send

send(message: Dict[str, Any]) -> None

Queue one JSON message to the page.

Target

Target()

A running app DevTools can talk to.

Methods:

Name Description
label

ios iPhone 17 Pro, Browser preview.

describe

The target's entry in the DevTools target list.

call

Run devtools request method in the app and return its result.

open_debug_stream

Start the app's debug adapter and connect to it.

label

label() -> str

ios iPhone 17 Pro, Browser preview.

describe

describe() -> Dict[str, Any]

The target's entry in the DevTools target list.

call async

call(
    method: str,
    params: Optional[Dict[str, Any]] = None,
    timeout: float = _RPC_TIMEOUT,
) -> Any

Run devtools request method in the app and return its result.

open_debug_stream async

open_debug_stream(hub: 'Hub') -> 'DebugStream'

Start the app's debug adapter and connect to it.

DebugStream

DebugStream(
    write: Callable[[bytes], None],
    close: Callable[[], None],
)

One debugger connection's path to an app's adapter.

LocalTarget

LocalTarget(agent: Any, loop: AbstractEventLoop)

Bases: Target

The browser preview: its agent runs in this process.

Parameters:

Name Type Description Default
agent Any

The preview's Agent.

required
loop AbstractEventLoop

The dev server's loop, where replies are delivered.

required

Methods:

Name Description
label

Always Browser preview.

call

Dispatch to the in-process agent and wait for its reply on this loop.

open_debug_stream

Start the preview's adapter and connect to it directly.

label

label() -> str

Always Browser preview.

call async

call(
    method: str,
    params: Optional[Dict[str, Any]] = None,
    timeout: float = _RPC_TIMEOUT,
) -> Any

Dispatch to the in-process agent and wait for its reply on this loop.

open_debug_stream async

open_debug_stream(hub: 'Hub') -> DebugStream

Start the preview's adapter and connect to it directly.

ClientTarget

ClientTarget(
    client_id: int, send: Callable[[Dict[str, Any]], None]
)

Bases: Target

A connected dev client (PythonNative Go or a debug build).

Parameters:

Name Type Description Default
client_id int

The server's connection id.

required
send Callable[[Dict[str, Any]], None]

Queues one JSON message to the client.

required

Methods:

Name Description
call

Send an rpc to the client and wait for its rpc_result.

settle

Deliver an rpc_result.

packages_ready

A packages message with a request id finished installing.

ensure_packages

Sync names (and their dependencies) to the client and wait until they're installed.

open_debug_stream

Sync debugpy, start the client's adapter, and tunnel to it.

tunnel

Deliver a tunnel message from the client.

disconnected

Fail pending requests and close streams.

call async

call(
    method: str,
    params: Optional[Dict[str, Any]] = None,
    timeout: float = _RPC_TIMEOUT,
) -> Any

Send an rpc to the client and wait for its rpc_result.

settle

settle(message: Dict[str, Any]) -> None

Deliver an rpc_result.

packages_ready

packages_ready(message: Dict[str, Any]) -> None

A packages message with a request id finished installing.

ensure_packages async

ensure_packages(
    names: List[str], timeout: float = 120.0
) -> None

Sync names (and their dependencies) to the client and wait until they're installed.

open_debug_stream async

open_debug_stream(hub: 'Hub') -> DebugStream

Sync debugpy, start the client's adapter, and tunnel to it.

tunnel

tunnel(message: Dict[str, Any]) -> None

Deliver a tunnel message from the client.

disconnected

disconnected() -> None

Fail pending requests and close streams.

Hub

Hub(
    log: Logger,
    on_open_devtools: Optional[Callable[[], None]] = None,
    on_open_editor: Optional[
        Callable[[str, int], Any]
    ] = None,
)

Targets, DevTools pages, event history, and debugger routing.

Parameters:

Name Type Description Default
log Logger

Terminal logger.

required
on_open_devtools Optional[Callable[[], None]]

Called when an app asks to open DevTools.

None

Methods:

Name Description
add_target

Register a target and tell every page.

remove_target

Forget a target and tell every page.

describe_targets

Every target, most recently connected first.

broadcast_targets

Send the target list to every page.

default_target

The debug target: the chosen one, else the newest device, else the preview.

on_event

Record and broadcast an event a target published.

log_line

Record one log line from a target.

add_page

Register a DevTools page; it receives the targets and history.

remove_page

Forget a page.

broadcast

Send message to every page.

page_request

Answer one rpc message from a page.

clear

Drop a topic's history (target_id=None for every target).

debug_connection

Join a Debug Adapter Protocol connection to the selected target's adapter.

add_target

add_target(target: Target) -> None

Register a target and tell every page.

remove_target

remove_target(target_id: str) -> None

Forget a target and tell every page.

describe_targets

describe_targets() -> List[Dict[str, Any]]

Every target, most recently connected first.

broadcast_targets

broadcast_targets() -> None

Send the target list to every page.

default_target

default_target() -> Optional[Target]

The debug target: the chosen one, else the newest device, else the preview.

on_event

on_event(target_id: str, topic: str, data: Any) -> None

Record and broadcast an event a target published.

log_line

log_line(target_id: str, level: str, text: str) -> None

Record one log line from a target.

add_page

add_page(page: Page) -> None

Register a DevTools page; it receives the targets and history.

remove_page

remove_page(page: Page) -> None

Forget a page.

broadcast

broadcast(message: Dict[str, Any]) -> None

Send message to every page.

page_request async

page_request(page: Page, message: Dict[str, Any]) -> None

Answer one rpc message from a page.

clear

clear(target_id: Optional[str], topic: str) -> None

Drop a topic's history (target_id=None for every target).

debug_connection async

debug_connection(
    reader: StreamReader, writer: StreamWriter
) -> None

Join a Debug Adapter Protocol connection to the selected target's adapter.

encode

encode(message: Dict[str, Any]) -> str

Serialize a hub message (default=str so odd values never break a page).

Compatibility checks

Decide whether a dev client can run the project pn start serves.

A dev client syncs Python sources, never native code, so the code it was built with has to match the project's:

  • PythonNative Go carries the framework and every built-in native module, nothing else. It runs a project when its framework version equals the host's (the bundled pythonnative package can't be replaced while it runs), the project adds no native plugins, and every package the project needs is pure Python.
  • A project's debug build carries the framework and plugins it was built with. The framework version and the plugin set must still match; otherwise pn run must rebuild it.

check returns an Incompatibility with a reason and the command that fixes it, which the server sends instead of a sync and the device shows.

Classes:

Name Description
Incompatibility

Why a client can't run the project, and how to fix it.

ProjectRuntime

What the project needs from a dev client.

Functions:

Name Description
project_runtime

Describe what the project at config (an AppConfig, or None) needs.

check

Return why the client that sent hello can't run project, or None.

Incompatibility dataclass

Incompatibility(reason: str, fix: str)

Why a client can't run the project, and how to fix it.

Methods:

Name Description
to_message

The incompatible message sent to the client.

to_message

to_message() -> Dict[str, Any]

The incompatible message sent to the client.

ProjectRuntime dataclass

ProjectRuntime(
    pythonnative: str,
    protocol: int,
    plugins: List[str] = list(),
    requirements: List[str] = list(),
    native_packages: List[str] = list(),
    missing_packages: List[str] = list(),
)

What the project needs from a dev client.

Attributes:

Name Type Description
pythonnative str

The host's framework version.

protocol int

The host's bridge protocol version.

plugins List[str]

Native plugin names the project uses.

requirements List[str]

[requirements].packages.

native_packages List[str]

Required distributions with compiled code.

missing_packages List[str]

Requirements that aren't installed on the host.

go_compatible property

go_compatible: bool

Whether PythonNative Go can run this project.

project_runtime

project_runtime(config: Any = None) -> ProjectRuntime

Describe what the project at config (an AppConfig, or None) needs.

check

check(
    hello: Dict[str, Any], project: ProjectRuntime
) -> Optional[Incompatibility]

Return why the client that sent hello can't run project, or None.

Package sync

Sync pure-Python packages from the developer's environment to dev clients.

PythonNative Go bundles no third-party packages, and a project's own debug build bundles the wheels its [requirements] named when it was built. The dev server fills the gap from the environment pn start runs in, which the browser preview already needs: it resolves [requirements] (and their dependencies) against the installed distributions and sends a client every pure-Python distribution its bundle lacks. The client writes them into its overlay's site-packages.

A distribution with compiled extension modules can't be synced: Go can't run that project, and a debug build must be rebuilt to include it. debugpy is the exception: its compiled modules are optional speedups, so the debugger syncs its Python sources alone.

Classes:

Name Description
HostDistribution

One installed distribution, as the dev server would sync it.

Functions:

Name Description
requirement_name

The distribution a [requirements] entry or Requires-Dist line names.

find_distribution

The installed distribution called name (any normalization), or None.

distribution_files

Classify a distribution's files for syncing.

resolve

Resolve requirements and their dependencies in this environment.

Attributes:

Name Type Description
BINARY_SUFFIXES

File suffixes that make a distribution native-only.

BINARY_SUFFIXES module-attribute

BINARY_SUFFIXES = ('.so', '.pyd', '.dylib', '.dll', '.exe')

File suffixes that make a distribution native-only.

HostDistribution dataclass

HostDistribution(
    name: str,
    version: str,
    files: List[Tuple[str, Path]] = list(),
    binary: List[str] = list(),
    site: Optional[Path] = None,
)

One installed distribution, as the dev server would sync it.

Attributes:

Name Type Description
name str

The distribution's name.

version str

Its installed version.

files List[Tuple[str, Path]]

(path relative to site-packages, absolute path) pairs.

binary List[str]

Compiled files that make it native-only.

site Optional[Path]

The site-packages directory it's installed in.

Methods:

Name Description
size

Total bytes the sync would send.

pure property

pure: bool

Whether every module is Python source.

size

size() -> int

Total bytes the sync would send.

requirement_extras

requirement_extras(requirement: str) -> Set[str]

The extras a requirement asks for: httpx[http2,socks] gives {"http2", "socks"}.

normalize

normalize(name: str) -> str

PEP 503 normalization: Foo_Bar.baz becomes foo-bar-baz.

requirement_name

requirement_name(requirement: str) -> str

The distribution a [requirements] entry or Requires-Dist line names.

find_distribution

find_distribution(name: str) -> Optional[Distribution]

The installed distribution called name (any normalization), or None.

distribution_files

distribution_files(
    dist: Distribution, *, sources_only: bool = False
) -> HostDistribution

Classify a distribution's files for syncing.

Parameters:

Name Type Description Default
dist Distribution

An installed distribution.

required
sources_only bool

Drop compiled files instead of reporting them (debugpy).

False

resolve

resolve(
    requirements: Sequence[str], *, skip: Iterable[str] = ()
) -> Tuple[List[HostDistribution], List[str]]

Resolve requirements and their dependencies in this environment.

Requested extras (httpx[http2]) bring in their dependencies. Dependencies that aren't installed are assumed to be excluded by an environment marker and are skipped.

Parameters:

Name Type Description Default
requirements Sequence[str]

[requirements].packages entries.

required
skip Iterable[str]

Distribution names the client already bundles, and pythonnative itself.

()

Returns:

Type Description
List[HostDistribution]

(distributions, missing): the installed closure (excluding

List[str]

skip), and requirement names that aren't installed at all.

Open a source location from DevTools or an error screen in the developer's editor.

Frames reported by dev clients use project-relative paths (app/main.py) for application code, because the device runs a copy of it. The dev server resolves them against the project root, and synced packages against the environment it runs in, then opens the file with:

  1. $PN_EDITOR, a command template where {file} and {line} are substituted ("code --goto {file}:{line}", "subl {file}:{line}"); without placeholders, {file}:{line} is appended;
  2. VS Code's code --goto when it's on PATH;
  3. the system's default handler for the file.

Functions:

Name Description
resolve_source

Map a reported frame path to a file on this machine, or None.

editor_command

The command that opens path at line.

open_in_editor

Open file:line and return the resolved path, or None when it doesn't resolve.

resolve_source

resolve_source(
    file: str, project_root: str, search: Sequence[str] = ()
) -> Optional[Path]

Map a reported frame path to a file on this machine, or None.

Only files inside the project or one of search (site-packages directories) resolve, so DevTools can't be used to open arbitrary paths.

editor_command

editor_command(path: Path, line: int) -> List[str]

The command that opens path at line.

open_in_editor

open_in_editor(
    file: str,
    line: int,
    project_root: str,
    search: Sequence[str] = (),
) -> Optional[str]

Open file:line and return the resolved path, or None when it doesn't resolve.

QR codes

A small, dependency-free QR code encoder for the dev server.

pn start prints a QR code for the dev-client connect link (pn-com.pythonnative.go://connect?url=...) so a phone running the dev client can join the server by pointing its camera at the terminal, and the DevTools page shows the same link as an SVG. Both renderings come from this module, which is standard library only so it also runs inside the embedded interpreter on device.

The encoder implements QR Code Model 2 as specified in ISO/IEC 18004:

  • numeric, alphanumeric, and byte (UTF-8) modes, picked automatically as the most compact single mode that holds the whole input,
  • error correction levels L, M, Q, and H,
  • versions 1 through 40, choosing the smallest version that fits,
  • Reed-Solomon error correction with the standard block interleaving, BCH-protected format and version information, and
  • all eight data masks, keeping the one with the lowest penalty score.
Example
from pythonnative.devserver import qr

code = qr.encode("pn-com.pythonnative.go://connect?url=ws%3A%2F%2F10.0.0.2%3A8081")
print(qr.to_terminal(code))
svg = qr.to_svg(code, scale=4)

Classes:

Name Description
QRCode

An encoded QR code symbol.

Functions:

Name Description
encode

Encode text as a QR code.

to_terminal

Render a QR code as Unicode half blocks for a terminal.

to_svg

Render a QR code as a compact SVG document.

QRCode dataclass

QRCode(
    version: int,
    size: int,
    modules: Tuple[Tuple[bool, ...], ...],
    error: str = "M",
    mask: int = 0,
)

An encoded QR code symbol.

Attributes:

Name Type Description
version int

The symbol version, from 1 (21 x 21 modules) to 40 (177 x 177 modules).

size int

The number of modules along each side, 4 * version + 17.

modules Tuple[Tuple[bool, ...], ...]

The module grid as rows from top to bottom, each row listing modules from left to right. True is a dark module. The grid doesn't include a quiet zone.

error str

The error correction level, one of "L", "M", "Q", or "H".

mask int

The data mask pattern applied to the symbol, from 0 to 7.

encode

encode(
    text: str,
    *,
    error: str = "M",
    version: Optional[int] = None,
    mask: Optional[int] = None
) -> QRCode

Encode text as a QR code.

The encoder uses numeric mode when text is all digits, alphanumeric mode when it only uses the 45-character QR alphabet (digits, uppercase letters, space, and $%*+-./:), and byte mode with UTF-8 otherwise. No ECI header is written; scanners detect UTF-8 on their own.

Parameters:

Name Type Description Default
text str

The text to encode.

required
error str

The error correction level: "L" (about 7% recovery), "M" (15%), "Q" (25%), or "H" (30%).

'M'
version Optional[int]

Force a specific version from 1 to 40 instead of the smallest one that fits.

None
mask Optional[int]

Force a data mask pattern from 0 to 7 instead of the one with the lowest penalty score.

None

Returns:

Type Description
QRCode

The encoded symbol.

Raises:

Type Description
ValueError

If an argument is out of range or text doesn't fit in the requested (or the largest) version.

to_terminal

to_terminal(
    code: QRCode, *, border: int = 2, invert: bool = False
) -> str

Render a QR code as Unicode half blocks for a terminal.

Each text line holds two module rows, drawn with "▀", "▄", "█", and spaces, so the symbol stays roughly square in typical monospace fonts.

Most terminals draw light text on a dark background, so by default the light modules (including the quiet zone) are drawn as filled blocks and the dark modules are left as background. That scans correctly on a dark terminal. Pass invert=True for a dark-on-light terminal, where the filled blocks become the dark modules.

Parameters:

Name Type Description Default
code QRCode

The symbol to render.

required
border int

The width of the quiet zone in modules. The standard asks for 4, but 2 scans reliably and saves terminal space.

2
invert bool

Draw dark modules as filled blocks instead of light ones.

False

Returns:

Type Description
str

The rendering, one string with a line per pair of module rows and

str

no trailing newline.

Raises:

Type Description
ValueError

If border is negative.

to_svg

to_svg(
    code: QRCode,
    *,
    border: int = 4,
    scale: int = 1,
    dark: str = "#000",
    light: str = "#fff"
) -> str

Render a QR code as a compact SVG document.

The light background is a single <rect> and every dark module is part of a single <path>. The viewBox is measured in modules and shape-rendering="crispEdges" keeps the module edges sharp at any size.

Parameters:

Name Type Description Default
code QRCode

The symbol to render.

required
border int

The width of the quiet zone in modules.

4
scale int

The width and height of one module in CSS pixels.

1
dark str

The CSS color of dark modules.

'#000'
light str

The CSS color of light modules and the quiet zone.

'#fff'

Returns:

Type Description
str

The SVG markup.

Raises:

Type Description
ValueError

If border is negative or scale isn't positive.

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:

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 Sec-WebSocket-Accept value for client_key.

parse_http_headers

Split an HTTP request or response head into (start_line, headers).

server_handshake

Build the 101 Switching Protocols response for an upgrade request.

client_handshake_request

Build a client upgrade request; returns (request_bytes, key).

encode_frame

Encode one frame.

encode_close

Encode a close frame carrying code and reason.

MAX_MESSAGE_BYTES module-attribute

MAX_MESSAGE_BYTES = 64 * 1024 * 1024

Upper bound on one reassembled message; anything larger is a protocol error.

WebSocketError

Bases: Exception

A framing or protocol violation on the connection.

HandshakeError

Bases: WebSocketError

The HTTP upgrade did not complete.

FrameDecoder

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 data and yield every message it completes.

feed

feed(data: bytes) -> Iterator[Tuple[int, bytes]]

Consume data and yield every message it completes.

WebSocketClient

WebSocketClient(
    url: str, *, timeout: Optional[float] = 30.0
)

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

ws://host:port/path (wss is not supported; dev traffic stays on the local network).

required
timeout Optional[float]

Connect and read timeout in seconds. Reads that time out raise socket.timeout, letting the owning thread check for shutdown between waits.

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.

connected property

connected: bool

Whether the socket is open.

connect

connect() -> None

Open the TCP connection and complete the upgrade handshake.

send_text

send_text(text: str) -> None

Send one text message (masked, as clients must).

recv

recv() -> Optional[str]

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.

close

close(code: int = 1000, reason: str = '') -> None

Send a close frame (best effort) and shut the socket.

accept_key

accept_key(client_key: str) -> str

Return the Sec-WebSocket-Accept value for client_key.

parse_http_headers

parse_http_headers(
    raw: bytes,
) -> Tuple[str, Dict[str, str]]

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

server_handshake(headers: Dict[str, str]) -> bytes

Build the 101 Switching Protocols response for an upgrade request.

Parameters:

Name Type Description Default
headers Dict[str, str]

Lower-cased request headers (see parse_http_headers).

required

Raises:

Type Description
HandshakeError

When the request is not a WebSocket upgrade.

client_handshake_request

client_handshake_request(
    host: str, path: str, key: Optional[str] = None
) -> Tuple[bytes, str]

Build a client upgrade request; returns (request_bytes, key).

encode_frame

encode_frame(
    opcode: int,
    payload: bytes,
    *,
    mask: bool = False,
    fin: bool = True
) -> bytes

Encode one frame.

Clients must send masked frames and servers unmasked ones; the caller picks. Text payloads must already be UTF-8 encoded.

encode_close

encode_close(
    code: int = 1000,
    reason: str = "",
    *,
    mask: bool = False
) -> bytes

Encode a close frame carrying code and reason.

pythonnative.devclient

The on-device dev client: pair with pn start, sync sources, and serve DevTools.

Every debug build is a dev client: PythonNative Go (the prebuilt, project-independent client) and a project's own pn run build alike. It connects to the dev server over WebSocket and speaks version 2 of the dev session protocol (RFC 0006):

  • Handshake. hello describes the client's runtime (framework and Python versions, bridge protocol, contract fingerprint, plugins, bundled distributions) and the sources its overlay already holds. The server answers with a sync of whatever is missing, or with incompatible and the command that fixes the mismatch.
  • Sources and packages. sync and update write files into the writable overlay that shadows the bundle (see pythonnative.hot_reload.configure_dev_environment) and apply them with Fast Refresh. packages installs pure-Python distributions from the developer's environment into the overlay's site-packages.
  • DevTools. rpc requests go to the devtools agent and are answered with rpc_result; the agent's events go back as event. tunnel messages carry TCP streams, which the debugger uses.
  • Logs. print output and errors stream back to the terminal and DevTools as log and error messages.

Clients are pointed at a server by a connect link, pn-<application id>://connect?url=<server URL>, which pn run, pn go, and a scanned QR code open. The last server is remembered, so a relaunch reconnects. Without one, PythonNative Go shows its home screen.

All network I/O runs on daemon threads; file writes happen there too, and reloads run on the application thread.

Classes:

Name Description
DevClient

Keep this process in sync with a dev server and serve DevTools.

Functions:

Name Description
dev_scheme

The connect-link URL scheme of the dev client for app_id.

connect_link

<scheme>://connect?url=<server URL>, the link a QR code carries.

parse_connect_link

The server URL inside a connect link, or None when link isn't one.

client_info

What kind of dev client this build is: {"kind": "go" | "app", "app_id", "scheme"}.

normalize_server_url

Turn whatever the developer typed into a dev-client WebSocket URL.

display_server_url

The short form of a dev-client URL (host:port), without the token.

recent_servers

Servers this client connected to, most recent first: [{"url", "project", "last"}].

current

The running dev client, if any.

start

Start (or replace) the process-wide dev client for url.

stop

Stop the process-wide dev client; the dev menu keeps working locally.

handle_link

Connect to the server a connect link names; True when link was one.

start_if_configured

Start the dev client when a connect link or a remembered server names one.

go_home

Disconnect PythonNative Go and return to its home screen.

Attributes:

Name Type Description
SESSION_PROTOCOL

Version of the dev session protocol spoken with pn start.

LINK_ENV

Launch environment variable carrying a connect link.

GoHome Any

Root component of PythonNative Go until a project arrives from a dev server.

SESSION_PROTOCOL module-attribute

SESSION_PROTOCOL = 2

Version of the dev session protocol spoken with pn start.

LINK_ENV = 'PN_CONNECT_LINK'

Launch environment variable carrying a connect link.

pn run ios and pn go ios launch with it: iOS asks the user to confirm a link opened with simctl openurl, while a launch environment variable reaches the app silently.

CLIENT_INFO_FILE module-attribute

CLIENT_INFO_FILE = '_dev_client.json'

Written into the bundled pythonnative package of debug builds: {"kind", "app_id", "scheme"}.

GoHome module-attribute

GoHome: Any = _LazyGoHome()

Root component of PythonNative Go until a project arrives from a dev server.

DevClient

DevClient(
    url: str,
    overlay: str,
    *,
    entry_module: str = "app.main",
    forward_logs: bool = True,
    log: Optional[Logger] = None,
    kind: Optional[str] = None,
    agent: bool = True
)

Keep this process in sync with a dev server and serve DevTools.

Parameters:

Name Type Description Default
url str

Server URL (any form accepted by normalize_server_url).

required
overlay str

Writable directory that shadows the bundled sources.

required
entry_module str

The app's entry module, for the hello message.

'app.main'
forward_logs bool

Mirror print output to the server.

True
log Optional[Logger]

Local logger for the client's own status lines.

None
kind Optional[str]

"go" or "app"; defaults to this build's client_info.

None
agent bool

Install the devtools agent (off in unit tests that only exercise syncing).

True

Methods:

Name Description
add_listener

Subscribe to (state, detail) changes (called on the client thread).

start

Connect on a daemon thread (idempotent).

stop

Disconnect and stop the thread.

send

Send a message to the server from any thread (queued while offline).

log

Forward one log line to the server.

report_error

Forward an error report (a traceback) to the server.

Attributes:

Name Type Description
server_label str

host:port of the server, for logs (the full URL carries the dev token).

state str

"idle", "connecting", "connected", "syncing", "incompatible", or "disconnected".

agent Any

The devtools agent this client serves, once started.

server_label property

server_label: str

host:port of the server, for logs (the full URL carries the dev token).

state property

state: str

"idle", "connecting", "connected", "syncing", "incompatible", or "disconnected".

agent property

agent: Any

The devtools agent this client serves, once started.

add_listener

add_listener(
    callback: Callable[[str, str], None],
) -> Callable[[], None]

Subscribe to (state, detail) changes (called on the client thread).

start

start() -> None

Connect on a daemon thread (idempotent).

stop

stop() -> None

Disconnect and stop the thread.

send

send(message: Dict[str, Any]) -> None

Send a message to the server from any thread (queued while offline).

log

log(text: str, level: str = 'info') -> None

Forward one log line to the server.

report_error

report_error(phase: str, text: str) -> None

Forward an error report (a traceback) to the server.

dev_scheme

dev_scheme(app_id: str) -> str

The connect-link URL scheme of the dev client for app_id.

pn- plus the application id, lowercased, with underscores turned into hyphens (URL schemes allow letters, digits, +, -, and .): com.example.my_app becomes pn-com.example.my-app.

connect_link(scheme: str, server_url: str) -> str

<scheme>://connect?url=<server URL>, the link a QR code carries.

parse_connect_link(link: str) -> Optional[str]

The server URL inside a connect link, or None when link isn't one.

client_info

client_info() -> Dict[str, Any]

What kind of dev client this build is: {"kind": "go" | "app", "app_id", "scheme"}.

The builder writes it into debug builds; a process without one (tests, the host) is an "app" client.

normalize_server_url

normalize_server_url(text: str) -> str

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, full URLs with a path such as the device URL pn start prints (http://192.168.1.20:8765/?token=...), and connect links. The result always has the path /ws and the query role=client&token=...; the dev token is kept when the input carries one.

display_server_url

display_server_url(url: str) -> str

The short form of a dev-client URL (host:port), without the token.

recent_servers

recent_servers(
    overlay: Optional[str] = None,
) -> List[Dict[str, Any]]

Servers this client connected to, most recent first: [{"url", "project", "last"}].

seed_overlay

seed_overlay(
    overlay: str, top: str, *, log: Optional[Logger] = None
) -> int

Copy the bundled top package into overlay; returns the number of files.

current

current() -> Optional[DevClient]

The running dev client, if any.

start

start(
    url: str, overlay: Optional[str] = None, **kwargs: Any
) -> DevClient

Start (or replace) the process-wide dev client for url.

stop

stop() -> None

Stop the process-wide dev client; the dev menu keeps working locally.

install_local_agent

install_local_agent() -> None

Install a devtools agent that isn't connected to a server (dev menu, inspector, monitor).

handle_link(link: str) -> bool

Connect to the server a connect link names; True when link was one.

Installed as the deep-link interceptor by pythonnative.hot_reload.configure_dev_environment. A link that arrives before the dev client starts (a cold launch from a QR code) is kept for start_if_configured.

start_if_configured

start_if_configured(
    entry_module: Optional[str] = None,
) -> Optional[DevClient]

Start the dev client when a connect link or a remembered server names one.

Called by bootstrap.start(dev=True). A connect link that launched the app (as a deep link, or in PN_CONNECT_LINK) wins; otherwise the most recent server reconnects. Returns the client, or None when nothing is configured (PythonNative Go then shows its home screen).

go_home

go_home() -> None

Disconnect PythonNative Go and return to its home screen.

Stops the client, replaces the overlay's app with the bundled home screen, stops reconnecting at launch, and remounts.

go_main_source

go_main_source() -> str

Source of the app/main.py staged into PythonNative Go.

pythonnative.preview

Development server and browser preview.

The browser is a protocol-4 renderer with Yoga WebAssembly layout. Python runs on the standard application asyncio loop; the transport's coordinator and network threads forward messages to their owners. The dev server watches app sources, synchronizes mobile clients, and applies compatible Fast Refresh.

PN_PLATFORM=web must be set before importing pythonnative; the CLI launches a child process with that environment when needed.

Classes:

Name Description
PreviewSession

Everything one pn start invocation owns; see serve.

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,
    config: Any = None,
    debug_port: Optional[int] = None
)

Everything one pn start invocation owns; see serve.

Methods:

Name Description
start

Install the transport, start the server, and begin watching.

open_devtools

Open DevTools in the developer's browser (from any thread).

run

Run the main loop until stop (or Ctrl+C).

stop_soon

Ask run to return (from any thread).

stop

Tear everything down.

urls

Every URL the server can be reached at (local first), each carrying the dev token.

start

start() -> None

Install the transport, start the server, and begin watching.

open_devtools

open_devtools() -> None

Open DevTools in the developer's browser (from any thread).

run

run() -> None

Run the main loop until stop (or Ctrl+C).

stop_soon

stop_soon() -> None

Ask run to return (from any thread).

stop

stop() -> None

Tear everything down.

urls

urls() -> List[str]

Every URL the server can be reached at (local first), each carrying the dev token.

banner_lines

banner_lines(
    session: PreviewSession, title: str
) -> List[str]

The pn start banner: a QR code for the connect link, then the URLs.

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,
    config: Any = None,
    debug_port: Optional[int] = None
) -> None

Run the dev server (and browser preview) until interrupted.

Parameters:

Name Type Description Default
entry_module str

The app's entry module ("app.main").

required
project_root Optional[str]

Directory containing app/; defaults to the current directory. Added to sys.path.

None
host str

Bind address (0.0.0.0 so devices can connect).

'0.0.0.0'
port int

TCP port (0 picks a free one).

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 (the CLI starts its key handler here; tests use it to drive the session).

None
config Any

The project's AppConfig (compatibility checks, package sync, connect links).

None
debug_port Optional[int]

Port of the debugger proxy, or None for none.

None

Raises:

Type Description
RuntimeError

If PN_PLATFORM=web was not set before PythonNative was imported (the CLI sets it for you).

OSError

If the port is taken.

pythonnative.bridge.web

WebSocket transport for the browser renderer.

Network I/O and request settlement execute independently of the application asyncio loop. Python owns the component tree; the page owns DOM widgets and Yoga layout. Revisioned commits receive explicit acknowledgements, and callbacks are queued onto the Python application thread. No asyncio loop is pumped by the browser transport.

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(
    {
        "DevSupport",
        "Host",
        "Layout",
        "Alert",
        "Clipboard",
        "Linking",
        "Share",
        "Haptics",
        "NetInfo",
        "AppState",
        "Device",
        "Keyboard",
        "AccessibilityInfo",
        "Localization",
        "WebViews",
    }
)

Native modules the preview page implements; the rest use Python fallbacks.

REQUEST_TIMEOUT_S module-attribute

REQUEST_TIMEOUT_S = 15.0

How long a synchronous request waits for the page before giving up.

WebTransport

WebTransport(
    *, log: Optional[Callable[[str], None]] = None
)

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 (bridge.native_callback).

apply

Wait for the page to acknowledge this exact commit revision.

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_application

Queue fn on the application loop, or the fallback queue if it isn't running.

run_main_loop

Drain fallback work until stop is called or until holds.

drain_main

Run fallback-queue work inline for tests and return how many jobs ran.

stop

Make run_main_loop return.

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 ["dev", {...}] message to the page (logs, reload status, ...).

Attributes:

Name Type Description
on_dev_message Optional[Callable[[Dict[str, Any]], None]]

Hook for ["dev", {...}] messages on the Python application thread.

on_peer_changed Optional[Callable[[bool], None]]

Called on the application thread with True on connect and False on disconnect.

connected bool

Whether a page is attached.

on_dev_message instance-attribute

on_dev_message: Optional[
    Callable[[Dict[str, Any]], None]
] = None

Hook for ["dev", {...}] messages on the Python application thread.

on_peer_changed instance-attribute

on_peer_changed: Optional[Callable[[bool], None]] = None

Called on the application thread with True on connect and False on disconnect.

connected property

connected: bool

Whether a page is attached.

protocol_version

protocol_version() -> int

The page speaks whatever this package speaks; they ship together.

set_callback

set_callback(callback: Callback) -> None

Install the native -> Python entry point (bridge.native_callback).

apply

apply(transaction_json: str) -> str

Wait for the page to acknowledge this exact commit revision.

command

command(
    tag: int, name: str, args_json: str
) -> Optional[str]

Run an imperative command on one view; returns its JSON result or None.

animate

animate(tag: int, request_json: str) -> Optional[str]

Handle an animation request (set / start / cancel) for one view.

call

call(
    module: str, method: str, args_json: str
) -> Optional[str]

Call a native module: Host.post locally, page modules over the wire, others in Python.

post_to_application

post_to_application(fn: Callable[[], None]) -> None

Queue fn on the application loop, or the fallback queue if it isn't running.

run_main_loop

run_main_loop(
    *, until: Optional[Callable[[], bool]] = None
) -> None

Drain fallback work until stop is called or until holds.

The preview uses this as a coordinator while application callbacks run on the standard asyncio loop. When that loop isn't running, queued jobs execute on the thread calling this method.

drain_main

drain_main(timeout: float = 0.0) -> int

Run fallback-queue work inline for tests and return how many jobs ran.

stop

stop() -> None

Make run_main_loop return.

on_preview_connected

on_preview_connected(
    peer: Any, info: Dict[str, Any]
) -> None

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

on_preview_message(peer: Any, text: str) -> None

Route one frame from the page.

send_dev

send_dev(payload: Dict[str, Any]) -> None

Send a ["dev", {...}] message to the page (logs, reload status, ...).

pythonnative.project.go

PythonNative Go: the prebuilt, project-independent dev client.

Go is an ordinary debug build of a fixed project: its app/main.py is the home screen, its identity is com.pythonnative.go, and it declares every permission the built-in modules use, so a project's camera, location, and notification code works in it. Any project that adds no native code runs in it: the dev server syncs the project's sources and pure-Python packages.

Artifacts for each release are attached to the GitHub release by scripts/build-go.py and cached per version under the shared cache (~/Library/Caches/pythonnative/go/<version>/):

  • pythonnative-go-<version>-ios-simulator-arm64.zip and ...-ios-simulator-x86_64.zip (the .app, one per Mac architecture: the iOS runtime is staged for a single architecture)
  • pythonnative-go-<version>-android.apk
  • pythonnative-go-<version>.sha256 (sha256sum format)

ensure_artifact finds the artifact for the running pythonnative version in the cache, downloads and verifies it, or builds it locally (a development checkout, offline, or --build).

Classes:

Name Description
GoError

PythonNative Go couldn't be found, downloaded, or built.

Functions:

Name Description
artifact_names

Release asset names for version: {"ios", "android", "sums"}.

cache_dir

Where Go artifacts for version (default: the running one) are cached.

go_config

Write Go's project into work_dir and load its config.

build

Build Go for platform and copy the artifact into out_dir.

package

Turn a built artifact into its release asset in out_dir.

download

Download and verify the release artifact into the cache.

ensure_artifact

Return an installable Go artifact for the running framework version.

BASE_URL_ENV module-attribute

BASE_URL_ENV = 'PN_GO_BASE_URL'

Overrides where release assets are downloaded from (a mirror, or a test server).

GoError

Bases: RuntimeError

PythonNative Go couldn't be found, downloaded, or built.

framework_version

framework_version() -> str

The running pythonnative version (Go must match it exactly).

simulator_arch

simulator_arch() -> str

The iOS Simulator architecture of this Mac (arm64 or x86_64).

artifact_names

artifact_names(
    version: str, *, arch: Optional[str] = None
) -> Dict[str, str]

Release asset names for version: {"ios", "android", "sums"}.

ios names the simulator app for arch (default: this Mac's).

cache_dir

cache_dir(version: Optional[str] = None) -> Path

Where Go artifacts for version (default: the running one) are cached.

installed_artifact

installed_artifact(
    platform: str, version: Optional[str] = None
) -> Optional[Path]

The cached, ready-to-install artifact (an .app directory or an .apk), if any.

go_config

go_config(
    work_dir: Path,
    *,
    development_team: Optional[str] = None
) -> AppConfig

Write Go's project into work_dir and load its config.

Parameters:

Name Type Description Default
work_dir Path

An empty or reusable directory; app/main.py and pythonnative.toml are (re)written there.

required
development_team Optional[str]

An Apple team id, for a build signed for physical iPhones.

None

build

build(
    platform: str,
    *,
    out_dir: Path,
    log: Optional[Logger] = None,
    device: bool = False,
    development_team: Optional[str] = None
) -> Path

Build Go for platform and copy the artifact into out_dir.

Parameters:

Name Type Description Default
platform str

"ios" or "android".

required
out_dir Path

Where the .app or .apk lands.

required
log Optional[Logger]

Progress logger.

None
device bool

Build for physical iPhones (needs development_team).

False
development_team Optional[str]

The Apple team that signs a device build.

None

Returns:

Type Description
Path

The artifact in out_dir.

package

package(
    platform: str,
    artifact: Path,
    out_dir: Path,
    version: Optional[str] = None,
) -> Path

Turn a built artifact into its release asset in out_dir.

A simulator app is named for this Mac's architecture, which is the one build produced.

write_checksums

write_checksums(
    paths: list[Path],
    out_dir: Path,
    version: Optional[str] = None,
) -> Path

Write pythonnative-go-<version>.sha256 for paths (sha256sum format).

download

download(
    platform: str,
    version: Optional[str] = None,
    *,
    fetch: Fetch = _fetch,
    log: Optional[Logger] = None
) -> Optional[Path]

Download and verify the release artifact into the cache.

Returns:

Type Description
Optional[Path]

The installed artifact, or None when the release has no Go

Optional[Path]

assets (or the network is unavailable).

Raises:

Type Description
GoError

When a download doesn't match its published checksum.

ensure_artifact

ensure_artifact(
    platform: str,
    *,
    force_build: bool = False,
    fetch: Fetch = _fetch,
    log: Optional[Logger] = None
) -> Path

Return an installable Go artifact for the running framework version.

Looks in the cache, then downloads the release asset, then builds it locally (which needs Xcode or the Android toolchain, once).

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 root (relative path + contents), deterministically.

compute

Digest every native input of a build.

read_stamp

Return the stamp written by the last successful build, or None.

write_stamp

Record fingerprint (and the artifact it produced) for the next pn run.

Attributes:

Name Type Description
STAMP_NAME

File written into build/<platform>/ after a successful native build.

STAMP_NAME module-attribute

STAMP_NAME = '.pn-native-fingerprint.json'

File written into build/<platform>/ after a successful native build.

hash_tree

hash_tree(
    root: Path, *, into: Optional["hashlib._Hash"] = None
) -> str

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 into when given).

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

"android" or "ios".

required
template_root Path

The bundled native template directory.

required
lib_root Path

The pythonnative package directory that gets bundled.

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

read_stamp(build_dir: Path) -> Optional[Dict[str, str]]

Return the stamp written by the last successful build, or None.

write_stamp

write_stamp(
    build_dir: Path,
    fingerprint: str,
    *,
    artifact: Optional[Path] = None
) -> None

Record fingerprint (and the artifact it produced) for the next pn run.

Next steps