FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat!: overhaul native runtime contracts and lifecycle ownership by owenthcarey · Pull Request #86 · pythonnative/pythonnative · GitHub

feat!: overhaul native runtime contracts and lifecycle ownership by owenthcarey · Pull Request #86 · pythonnative/pythonnative · GitHub
Skip to content

Navigation Menu

Repository navigation

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .gradle  (1) .html  (1) .js  (2) .json  (4) .kt  (23) .lock  (1) .md  (8) .mjs  (2) .pbxproj  (1) .py  (62) .sh  (2) .swift  (28) .toml  (3) .xcscheme  (1) .xml  (1) .yaml  (4) .yml  (2) dotfile  (1) All 18 file types selected
Only manifest files
Deleted files Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
25 changes: 22 additions & 3 deletions .github/workflows/ci.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ jobs:
- name: Run tests
run: uv run pytest -q

browser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v10.0.1
with:
python-version: '3.13'
- run: uv sync --locked --group dev
- run: uv run python scripts/run-browser-tests.py

# The Swift half of the rendering core: decoders, component managers,
# gestures, and module promises run under XCTest on a simulator.
native-ios:
Expand Down Expand Up @@ -73,12 +83,21 @@ jobs:
')"
echo "Using simulator $UDID"
xcodebuild test \
-scheme PythonNativeKit \
-project Tests/NativeTests.xcodeproj \
-scheme NativeTests \
-parallel-testing-enabled NO \
-destination "platform=iOS Simulator,id=$UDID" \
-derivedDataPath "$RUNNER_TEMP/pnkit" \
-quiet
-resultBundlePath "$RUNNER_TEMP/pnkit-results.xcresult"

- name: Upload native test results
if: failure()
uses: actions/upload-artifact@v4
with:
name: native-ios-results
path: ${{ runner.temp }}/pnkit-results.xcresult

# The Kotlin half has no Chaquopy dependency, so plain JUnit covers it.
# JUnit covers contracts; Robolectric exercises native widget lifecycles.
native-android:
runs-on: ubuntu-latest
steps:
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/e2e.yml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ jobs:
suites: navigation gestures layout styling
- name: anim-misc
suites: animations misc
- name: inbox
suites: inbox

name: e2e-android (${{ matrix.name }})

Expand Down Expand Up @@ -164,6 +166,8 @@ jobs:
suites: navigation gestures layout styling
- name: anim-misc
suites: animations misc
- name: inbox
suites: inbox

name: e2e-ios (${{ matrix.name }})

Expand Down
3 changes: 3 additions & 0 deletions .gitignore
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,6 @@ cython_debug/
**/.gradle/
**/.kotlin/
**/.swiftpm/

# Built sample extension wheels
examples/inbox/vendor/
19 changes: 19 additions & 0 deletions CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# CHANGELOG

## Unreleased

- Make element snapshots immutable and preserve ordinary Python component
argument binding, async provider context, stable setters, and deferred updates.
- Stage reconciliation publication and roll back failed renders; retire surfaces
after native mount failures. Validate commits without copying unrelated records.
- Enforce shared component, event, command, and service contracts, including
nested values, property resets, recreation, and native result types.
- Use one list request protocol on every backend, stable dataset and item
revisions, bounded row ownership, and public indices that exclude headers.
- Complete nested native stack ownership, input composition and selection,
dynamic text sizing, cancelable native work, and durable Android notifications.
- Test the real browser renderer and native lifecycle behavior. Package the Inbox
extension separately and run reference-app acceptance on both mobile CI jobs.
- Remove permissive transaction decoders, alternate headless list rendering,
obsolete compatibility branches, and unrestricted device-option dictionaries.
These pre-1.0 changes intentionally break superseded APIs.



## v0.42.0 (2026-09-08)

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ uv run black src examples tests # format
uv run --group docs mkdocs serve # preview the docs site locally

# native rendering core (macOS with Xcode for the Swift package; JDK 17 for Gradle)
(cd src/pythonnative/native/ios && xcodebuild test -scheme PythonNativeKit -destination 'platform=iOS Simulator,name=iPhone 15 Pro')
(cd src/pythonnative/native/ios && xcodebuild test -project Tests/NativeTests.xcodeproj -scheme NativeTests -destination 'platform=iOS Simulator,name=iPhone 15 Pro')
(cd src/pythonnative/native/android && ../../templates/android_template/gradlew testDebugUnitTest)
```

Expand Down
27 changes: 24 additions & 3 deletions docs/concepts/bridge.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ A surface commit has this shape:
The renderer validates the entire operation sequence before mutation: operation
arity, live tags, insertion bounds, cycles, typed values, and finite geometry.
An accepted commit returns `ok`, `application`, `surface`, and the exact
`revision`. Python advances its native bookkeeping only after that acknowledgement.
`revision`, plus optional native timing metrics. Python advances its bookkeeping
only after that acknowledgment. A rejection with `failed: false` reports
validation failure before mutation and preserves the previous revision.
A failed surface rejects further incremental updates. A new application identity
and a complete remount establish a clean surface; replaying a partial commit
isn't a recovery strategy. The current app host uses one surface.
Expand All @@ -73,14 +75,24 @@ core, and the browser preview uses the corresponding Yoga WebAssembly package.

The application sends styles as props. Native leaf managers measure text,
controls, and images next to their widgets. One `Layout.compute` request returns
a batch of changed frames. Python doesn't make one measurement RPC per leaf.
a batch of changed frames with `application`, `surface`, and `revision`.
Geometry from another surface or revision is ignored. Drawing-only prop updates
skip the request entirely. Python doesn't make one measurement RPC per leaf.

Commit and required layout acknowledgments precede ref and effect publication.
Native navigation animations and later platform layout changes may continue after
that boundary; their geometry returns through the same identified layout channel.

A native screen, portal, modal, or recycled row can be physically attached to a
platform container while remaining a child in the same Python component tree.
Context, error boundaries, suspense boundaries, and task ownership follow that
logical tree. Lists use stable keys and data revisions to validate row requests.
UIKit collection views and Android recycler views own physical cells; a bounded
window of ordinary keyed Python row components supplies their contents.
window of ordinary keyed Python row components supplies their contents. Headless
tests simulate the same viewport and row requests. Dataset revisions change when
data or rendering inputs change; per-item revisions avoid reloading unchanged
cells. Global and section headers don't change public item indices. Row instances
can unmount outside the window, so persistent item state belongs in app data.

## Modules, contracts, and animations

Expand All @@ -99,3 +111,12 @@ Animation graphs describe values, arithmetic, interpolation, colors, and
clamping. Native timing, spring, and decay drivers evaluate connected bindings
on the UI thread. Scroll and gesture mappings feed graph values directly, so
visual updates can continue while the Python application thread is busy.

## Profiling the boundary

`PN_PROFILE` traces separate rendering, validation, transport, native mutation,
and layout. Native duration samples appear on a separate trace track, placed at
host receipt without assuming synchronized clocks. Work counters report copied
validation records, operations, native views visited, and changed frames. A
one-property regression test uses 10,000 records to check work growth directly.
These counters and host timings aren't device frame-rate claims.
31 changes: 27 additions & 4 deletions docs/concepts/reconciliation.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,19 @@ tree:
as needed using [`InsertOp`][pythonnative.mutations.InsertOp] /
[`DestroyOp`][pythonnative.mutations.DestroyOp].

The reconciler runs each render function synchronously to completion on the
application thread. Expensive Python work delays callbacks and commits.
The reconciler runs synchronous render functions to completion on the
application thread. Async components capture their provider environment across
awaits. Changes to their inputs cancel obsolete work before it can publish.
Expensive Python work delays callbacks and commits.
Native scrolling and supported animation drivers run independently; see
[Runtime limits](architecture.md#runtime-limits).

## Commits are transactions

The diff phase is pure: it only accumulates ops. At the end of the
pass the staged mutations are applied through a single
Rendering records changes in an undo journal and stages native operations,
callbacks, refs, and effects. A render that fails restores the committed tree
without publishing those changes. At the end of the pass, staged mutations are
applied through a single
[`apply_mutations`][pythonnative.native_views.NativeViewRegistry.apply_mutations]
call. The renderer validates the versioned transaction before applying it
and acknowledges the committed revision. Layout then runs beside the native
Expand Down Expand Up @@ -182,3 +186,22 @@ bottleneck.
- Browse the algorithm in code: [Reconciler API](../api/reconciler.md).
- Understand handlers underneath the diff: [Native views](native-views.md).
- See how mounting interacts with hooks: [Lifecycle](lifecycle.md).

## Elements and update priority

Elements hold read-only property snapshots and tuple children. Values inside
application props aren't deep-copied; replace mutable application data when its
meaning changes. Component construction follows the decorated function's Python
signature, including positional-only parameters, keyword-only parameters,
variadic children, and normal argument errors.

State setters and reducer dispatch functions retain their identity across renders.
Deferred updates stay out of urgent renders. When deferred work runs, the state
queue replays urgent updates in their original order so an urgent edit isn't lost.
Python rendering remains cooperative: a long synchronous function cannot be
preempted by a later urgent update.

A native mount failure retires the surface and cancels its work. Validation
failure before native mutation preserves the previous revision and can be retried.
Property validation copies only changed records, so a one-property update doesn't
clone the rest of the live native tree.
41 changes: 37 additions & 4 deletions docs/guides/native-contracts.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

Declare an extension's interface in Python, generate its native adapters, and
implement the platform behavior in Swift and Kotlin. The complete working
example is `examples/inbox`: its badge uses generated props, and its module
has synchronous and asynchronous methods.
example is the separately packaged `examples/inbox-extension`, consumed by
`examples/inbox`. Its badge uses generated props, and its module has synchronous
and asynchronous methods.

## Define the interface

Expand Down Expand Up @@ -66,8 +67,8 @@ Set `contracts` in the plugin's `pn_plugin.json` to its schema file:
}
```

The schema should contain the extension's own components and modules. The inbox
example's `generate_contracts.py` demonstrates extracting those definitions.
The schema should contain the extension's own components and modules. The
`examples/inbox-extension/generate_contracts.py` script demonstrates extracting those definitions.
Builders merge plugin contracts with the built-ins, regenerate native bindings,
and bundle the matching schema into embedded Python. Startup rejects a client
whose protocol, Yoga version, or schema fingerprint doesn't match.
Expand All @@ -92,3 +93,35 @@ mobile binary extensions into the desktop build process.

Rebuild after changing a contract or native implementation. Fast Refresh applies
to Python application behavior; it can't change a compiled native interface.

## Runtime enforcement

Built-in widgets and services use these contracts too. Unknown props, methods,
events, and arguments fail validation. Nested dataclasses and `TypedDict` values
validate required fields and reject unknown fields. Wire integers must be exact
numbers within JavaScript's safe integer range; booleans don't count as numbers.
Methods declare both their arguments and their return types. Python validates
native results before exposing them to application code.

A `null` update removes a prop. The renderer restores its declared default, or
recreates the physical widget to restore a platform default. Creation-only
changes, including `TextInput.multiline`, preserve the logical tag, component
state, child ownership, focus, and selection where the platform permits it.
Refs keep addressing that logical tag. `NativeField.platforms` limits where a
field can be used; unsupported fields are rejected by the target renderer.

Regenerate the repository's built-in bindings with:

```sh
uv run python scripts/generate-native-contracts.py
```

The shared fixtures run in Python, Swift, Kotlin, and JavaScript. The actual
browser renderer runs in Chrome with Yoga WebAssembly:

```sh
uv run python scripts/run-browser-tests.py
```

Native service adapters are named `NameModuleAdapter`, which keeps the generated
base class distinct from a platform's `NameModule` implementation.
9 changes: 8 additions & 1 deletion docs/guides/testing.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ The Swift and Kotlin halves have their own suites next to the code:

```bash
cd src/pythonnative/native/ios
xcodebuild test -scheme PythonNativeKit -destination 'platform=iOS Simulator,name=iPhone 15 Pro'
xcodebuild test -project Tests/NativeTests.xcodeproj -scheme NativeTests -destination 'platform=iOS Simulator,name=iPhone 15 Pro'

cd src/pythonnative/templates/android_template
./gradlew :pythonnative:testDebugUnitTest
Expand All @@ -232,6 +232,13 @@ promise envelopes, so a wire-format change is caught on every side. Add
cases there when you change a manager or module rather than relying on
the Maestro E2E suite alone.

The iOS `NativeTests` scheme runs the Swift tests inside a minimal UIKit
app. Use that scheme for native tests: composition, selection, and focus
need an application event loop and keyboard session, which the standalone
Swift package test runner doesn't provide. Add new Swift test files to
both the test directory and the Xcode project's `PythonNativeKitTests`
target.

## Testing async code

`render` and every event helper settle the framework's `asyncio` loop
Expand Down
16 changes: 12 additions & 4 deletions examples/e2e-suite/app/screens/components/text_input.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Demo screen for [`pn.TextInput`][pythonnative.TextInput].

Maestro types into the single-line input and asserts the ``Echo:``
line mirrors the typed value. The multiline variant is also rendered
so flows can confirm both modes coexist on one screen.
Maestro types into the name input, checks its echo and selection, then
switches between single-line and multiline widgets without losing the value.
A separate multiline note field shows a live character count.
"""

from __future__ import annotations
Expand All @@ -18,6 +18,8 @@ def TextInputDemo() -> pn.Element:
name, set_name = pn.use_state("")
notes, set_notes = pn.use_state("")
focused, set_focused = pn.use_state(False)
multiline, set_multiline = pn.use_state(False)
selection, set_selection = pn.use_state({"start": 0, "end": 0})

field_style = pn.style(
padding=10,
Expand All @@ -39,6 +41,8 @@ def TextInputDemo() -> pn.Element:
value=name,
placeholder="Type your name here",
on_change=set_name,
multiline=multiline,
on_selection_change=set_selection,
on_focus=lambda: set_focused(True),
on_blur=lambda: set_focused(False),
clear_button=True,
Expand All @@ -48,7 +52,11 @@ def TextInputDemo() -> pn.Element:
style=field_style,
),
result_text("Echo", name or "(empty)"),
hint("Maestro types here and asserts the echo + focus update."),
result_text("Selection", f"{selection['start']}:{selection['end']}"),
pn.Button(
"Use single line" if multiline else "Use multiline name", on_press=lambda: set_multiline(not multiline)
),
hint("Switch the name field between modes while keeping its text."),
),
section(
"Multiline",
Expand Down
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Demo screen for typography styling.

Shows several font sizes, weights, colors, and a text-decoration
example. Maestro asserts each labelled line is present.
Shows several font sizes, weights, colors, the boolean italic style,
and text decoration. Maestro asserts each labelled line is present.
"""

from __future__ import annotations
Expand All @@ -21,7 +21,7 @@ def TypographyDemo() -> pn.Element:
pn.Text("type-headline", style=pn.style(font_size=24, font_weight="700")),
pn.Text("type-body", style=pn.style(font_size=16)),
pn.Text("type-caption", style=pn.style(font_size=12, color="#6B7280")),
pn.Text("type-italic", style=pn.style(font_size=15, font_style="italic")),
pn.Text("type-italic", style=pn.style(font_size=15, italic=True)),
pn.Text("type-underline", style=pn.style(font_size=15, text_decoration="underline")),
pn.Text(
"type-letter-spacing",
Expand Down
16 changes: 16 additions & 0 deletions examples/inbox-extension/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Inbox extension

A separately distributed PythonNative extension with a native `InboxBadge` and
an `InboxTools` service. Python annotations generate the shared contract; native
implementations inherit the generated module adapters. The `pythonnative.plugins`
entry point and package data make discovery work after wheel installation.

From the repository root:

```sh
PYTHONPATH=examples/inbox-extension/src python examples/inbox-extension/generate_contracts.py
uv build --wheel examples/inbox-extension --out-dir examples/inbox/vendor
```

The Inbox reference app consumes the wheel. Its mobile acceptance jobs build the
native extension and exercise both its badge and module calls.
15 changes: 15 additions & 0 deletions examples/inbox-extension/generate_contracts.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Regenerate the extension manifest from its public Python definitions."""

import json
from pathlib import Path

import inbox_extension # noqa: F401

from pythonnative.sdk.schema import manifest

spec = manifest()
spec["components"] = {"InboxBadge": spec["components"]["InboxBadge"]}
spec["modules"] = {"InboxTools": spec["modules"]["InboxTools"]}
Path(__file__).with_name("src").joinpath("inbox_extension/native/schema.json").write_text(
json.dumps(spec, indent=2) + "\n"
)
18 changes: 18 additions & 0 deletions examples/inbox-extension/pyproject.toml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[build-system]
requires = ["setuptools>=75"]
build-backend = "setuptools.build_meta"

[project]
name = "pn-inbox-extension"
version = "0.1.0"
requires-python = ">=3.13"
description = "PythonNative's packaged component and module SDK acceptance extension"

[project.entry-points."pythonnative.plugins"]
inbox = "inbox_extension.native"

[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools.package-data]
"inbox_extension.native" = ["*.json", "ios/*.swift", "android/**/*.kt"]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Native sources and portable contracts for the packaged Inbox extension."""
Loading
Loading

Footer

© 2026 GitHub, Inc.

Back | FazBrowse Home | New Git URL