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

feat(bridge)!: add native rendering core and module plugins (#72) · pythonnative/pythonnative@6c7d4d4 · GitHub

feat(bridge)!: add native rendering core and module plugins (#72) · pythonnative/pythonnative@6c7d4d4 · GitHub
Skip to content

Navigation Menu

Commit 6c7d4d4

Browse files
authored
feat(bridge)!: add native rendering core and module plugins (#72)
1 parent 8ab1a29 commit 6c7d4d4

210 files changed

Lines changed: 20403 additions & 14028 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,57 @@ jobs:
4343

4444
- name: Run tests
4545
run: uv run pytest -q
46+
47+
# The Swift half of the rendering core: decoders, component managers,
48+
# gestures, and module promises run under XCTest on a simulator.
49+
native-ios:
50+
runs-on: macos-14
51+
steps:
52+
- name: Checkout
53+
uses: actions/checkout@v4
54+
55+
# Runner images ship different simulator sets, and xcodebuild
56+
# silently falls back to "My Mac" (where UIKit doesn't exist) when
57+
# a named destination is missing, so resolve an available iPhone
58+
# simulator by UDID instead of hardcoding a model name.
59+
- name: Run PythonNativeKit tests
60+
working-directory: src/pythonnative/templates/ios_template/PythonNativeKit
61+
run: |
62+
set -o pipefail
63+
UDID="$(xcrun simctl list devices available -j | python3 -c '
64+
import json, sys
65+
data = json.load(sys.stdin)["devices"]
66+
phones = [
67+
d for runtime, devices in sorted(data.items()) if "iOS" in runtime
68+
for d in devices if d.get("isAvailable") and d["name"].startswith("iPhone")
69+
]
70+
if not phones:
71+
sys.exit("no available iPhone simulator on this runner")
72+
print(phones[-1]["udid"])
73+
')"
74+
echo "Using simulator $UDID"
75+
xcodebuild test \
76+
-scheme PythonNativeKit \
77+
-destination "platform=iOS Simulator,id=$UDID" \
78+
-derivedDataPath "$RUNNER_TEMP/pnkit" \
79+
-quiet
80+
81+
# The Kotlin half has no Chaquopy dependency, so plain JUnit covers it.
82+
native-android:
83+
runs-on: ubuntu-latest
84+
steps:
85+
- name: Checkout
86+
uses: actions/checkout@v4
87+
88+
- name: Set up Java 17
89+
uses: actions/setup-java@v4
90+
with:
91+
distribution: 'temurin'
92+
java-version: '17'
93+
94+
- name: Set up Gradle
95+
uses: gradle/actions/setup-gradle@v4
96+
97+
- name: Run pythonnative module tests
98+
working-directory: src/pythonnative/templates/android_template
99+
run: ./gradlew :pythonnative:testDebugUnitTest --no-daemon

‎.github/workflows/e2e.yml‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -127,28 +127,36 @@ jobs:
127127
runs-on: macos-15
128128
# Budget for the worst case: ~8 min build/boot plus two full suite
129129
# attempts (run-e2e.sh retries the whole suite once after a driver
130-
# flake) at up to ~20 min each on a slow runner. 40 was enough for
131-
# a clean pass but cut the retry off mid-suite.
130+
# flake). The test step gets its own, shorter timeout so a hung
131+
# attempt still leaves time for the debug-output upload below;
132+
# a job-level timeout kills the runner before that step can run.
132133
timeout-minutes: 55
133134
# iOS simulators are stable for the full suite (unlike the Android
134135
# emulator; see the e2e-android note), so this split is purely a
135136
# speed optimization: the iOS Maestro run is ~22 minutes and is the
136-
# critical path for the whole E2E workflow. Sharding it across three
137+
# critical path for the whole E2E workflow. Sharding it across
137138
# simulators (same balanced groups as Android) runs them in parallel
138139
# and roughly halves wall-clock time. macOS minutes are free on this
139140
# public repo, so the extra per-shard build/boot overhead is fine.
141+
# Shards are sized so a single attempt is ~10-12 minutes of driving:
142+
# ``components`` alone was ~21 minutes per attempt on a hosted
143+
# runner, so one retry (build + two attempts) overran the job budget.
140144
# ``fail-fast: false`` so one shard's failure still lets the others
141145
# report.
142146
strategy:
143147
fail-fast: false
144148
matrix:
145149
include:
146-
- name: components
147-
suites: components
148-
- name: hooks-nav-gestures
149-
suites: hooks navigation gestures
150-
- name: layout-styling-anim-misc
151-
suites: layout styling animations misc
150+
- name: components-a
151+
suites: components-a
152+
- name: components-b
153+
suites: components-b
154+
- name: hooks
155+
suites: hooks
156+
- name: nav-gestures-layout-styling
157+
suites: navigation gestures layout styling
158+
- name: anim-misc
159+
suites: animations misc
152160

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

@@ -175,13 +183,15 @@ jobs:
175183
echo "$HOME/.maestro/bin" >> $GITHUB_PATH
176184
177185
- name: Build and run E2E tests
186+
timeout-minutes: 48
178187
run: uv run ./scripts/run-e2e.sh ios ${{ matrix.suites }}
179188
env:
180189
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
181190

182191
# Maestro writes per-flow debug output (command log, view-hierarchy
183192
# dumps, failure screenshots) under ~/.maestro/tests. Surface it when
184-
# a shard fails so CI-only failures are diagnosable without guesswork.
193+
# a shard fails (or hits the step timeout above) so CI-only failures
194+
# are diagnosable without guesswork.
185195
- name: Upload Maestro debug output
186196
if: failure()
187197
uses: actions/upload-artifact@v4

‎CONTRIBUTING.md‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,12 @@ Unsolicited pull requests for issues that are already assigned or already have a
6363

6464
- `src/pythonnative/`: installable library and CLI
6565
- `pythonnative/`: core cross‑platform UI components and utilities
66+
- `bridge/`: JSON codec and per-platform transports into the native rendering core
6667
- `cli/`: `pn` command
68+
- `project/`: config loading, template configuration, native plugin staging, and the builder behind `pn`
6769
- `templates/`: Android/iOS project templates (bundled with the package)
70+
- `ios_template/PythonNativeKit/`: Swift package with the iOS rendering core (component managers, gestures, animations, native modules) and its XCTest target
71+
- `android_template/pythonnative/`: Gradle library module with the Android rendering core and its JUnit target
6872
- `tests/`: unit tests for the library, plus the Maestro E2E suite
6973
- `e2e/`: the comprehensive E2E suite (see [E2E tests](#e2e-tests-maestro) below and `tests/e2e/AGENTS.md`)
7074
- `examples/`: runnable example apps
@@ -77,7 +81,8 @@ Unsolicited pull requests for issues that are already assigned or already have a
7781

7882
- Style: Black; lint: Ruff; typing where useful. Keep APIs stable.
7983
- Prefer explicit, descriptive names; keep platform abstractions clean.
80-
- Add/extend tests under `tests/` for new behavior.
84+
- Python never imports platform code. Everything that touches `UIView` or `android.view.View` lives in Swift (`PythonNativeKit`) or Kotlin (the `pythonnative` Gradle module) and is reached through `pythonnative.bridge`; see `docs/concepts/bridge.md` for the protocol. Bump `PROTOCOL_VERSION` on both sides when the wire format changes.
85+
- Add/extend tests under `tests/` for new behavior. Native changes get XCTest / JUnit coverage next to the code they touch.
8186
- Don't commit generated artifacts or large binaries; templates live under `src/pythonnative/templates/`.
8287
- Docstrings: Google style throughout. Ruff is configured with the Google
8388
convention (`pydocstyle.convention = "google"`) and enforces the `D` rule
@@ -94,6 +99,10 @@ uv run pytest -q # run tests
9499
uv run ruff check . # lint
95100
uv run black src examples tests # format
96101
uv run --group docs mkdocs serve # preview the docs site locally
102+
103+
# native rendering core (macOS with Xcode for the Swift package; JDK 17 for Gradle)
104+
(cd src/pythonnative/templates/ios_template/PythonNativeKit && xcodebuild test -scheme PythonNativeKit -destination 'platform=iOS Simulator,name=iPhone 15 Pro')
105+
(cd src/pythonnative/templates/android_template && ./gradlew :pythonnative:testDebugUnitTest)
97106
```
98107

99108
## Conventional Commits
@@ -132,24 +141,27 @@ Recommended scopes (choose the smallest, most accurate unit; prefer module/direc
132141
- Module/directory scopes:
133142
- `alerts`: imperative Alert/Picker helpers (`alerts.py`)
134143
- `animated`: Animated namespace and animation primitives (`animated.py`)
144+
- `bootstrap`: on-device runtime start-up called by the templates (`bootstrap.py`)
145+
- `bridge`: wire codec, transports, handshake, and native-to-Python callback (`bridge/`)
135146
- `cli`: CLI tool and `pn` command (`src/pythonnative/cli/`)
136147
- `component`: the `@component` decorator, `Component`, and `memo` (`component.py`)
137148
- `components`: declarative element-creating functions (`components/`)
138149
- `element`: Element descriptor class (`element.py`)
139150
- `events`: tag-based event routing between native views and Python callbacks (`events.py`)
140151
- `gestures`: gesture descriptors and the pure-Python recognition arbiter (`gestures.py`)
141152
- `hooks`: hooks and contexts (`hooks.py`)
142-
- `hosts`: screen hosts, native lifecycle bridge, and render scheduling (`hosts/`)
153+
- `hosts`: screen hosts, lifecycle forwarding, and render scheduling (`hosts/`)
143154
- `hot_reload`: file watcher and module reloader (`hot_reload.py`)
144155
- `layout`: pure-Python flexbox engine (`layout.py`)
145156
- `mutations`: batched mutation ops between reconciler and native backends (`mutations.py`)
146-
- `native_modules`: native API modules for device capabilities (`native_modules/`)
147-
- `native_views`: platform-specific native view creation and updates (`native_views/`)
157+
- `native_modules`: native module registry, Python facades, and desktop implementations (`native_modules/`)
158+
- `native_views`: view registry protocol, bridge backend, and desktop handlers (`native_views/`)
148159
- `navigation`: navigation state, container, navigators, hooks, and linking (`navigation/`)
149160
- `net`: awaitable HTTP client (`net.py`)
150161
- `package`: `src/pythonnative/__init__.py` exports and package boundary
151162
- `platform`: `Platform.OS`/`Platform.select` and version detection (`platform.py`)
152163
- `platform_metrics`: platform-reported metrics like safe-area insets and bar heights (`platform_metrics.py`)
164+
- `project`: `pythonnative.toml` config, template configurators, plugin staging, and the builder (`project/`)
153165
- `reconciler`: virtual view tree diffing, boundaries, and the layout pass (`reconciler/`)
154166
- `runtime`: framework-wide asyncio loop and thread-safe future helpers (`runtime.py`)
155167
- `sdk`: public extension SDK for custom native components (`sdk/`)
@@ -167,6 +179,8 @@ Recommended scopes (choose the smallest, most accurate unit; prefer module/direc
167179
- `repo`: repository metadata and top‑level files (`README.md`, `CONTRIBUTING.md`, `.gitignore`, licenses)
168180
- `scripts`: developer scripts under `scripts/` (e.g., `check.sh`)
169181
- `templates`: Android/iOS project templates under `src/pythonnative/templates/`
182+
- `kit`: the Swift rendering core (`templates/ios_template/PythonNativeKit/`)
183+
- `runtime-android`: the Kotlin rendering core (`templates/android_template/pythonnative/`)
170184
- `tests`: unit/integration/E2E tests under `tests/`
171185
- `workflows`: CI pipelines under `.github/workflows/`
172186

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
## Overview
2828

29-
PythonNative is a cross-platform toolkit for building native Android and iOS apps in Python. It provides a **declarative, React-like component model** with hooks and automatic reconciliation, powered by Chaquopy on Android and rubicon-objc on iOS. Write function components with `use_state`, `use_effect`, and friends, just like React, and let PythonNative handle creating and updating native views.
29+
PythonNative is a cross-platform toolkit for building native Android and iOS apps in Python. It provides a **declarative, React-like component model** with hooks and automatic reconciliation on top of a **native rendering core** written in Swift and Kotlin. Write function components with `use_state`, `use_effect`, and friends, just like React, and let PythonNative handle creating and updating native views.
3030

3131
## Features
3232

@@ -42,7 +42,7 @@ PythonNative is a cross-platform toolkit for building native Android and iOS app
4242
- **Native-driven animations:** The `Animated` API (timing / spring / decay / loop / stagger, awaitable or fire-and-forget) hands animations to Core Animation and `ViewPropertyAnimator` whenever possible, so no Python code runs per frame; a pure-Python ticker covers the rest. `interpolate`, arithmetic operators on animated nodes, `Animated.event` scroll binding, and `diff_clamp` cover the scroll-driven patterns (collapsing headers, parallax) that define native feel.
4343
- **Native gesture system:** Attach `Tap`, `LongPress`, `Pan`, `Swipe`, `Fling`, `Pinch`, and `Rotation` recognizers to any view via the `gestures=` prop, backed by `UIGestureRecognizer` on iOS and a unit-testable pure-Python arbiter on Android and desktop. Compose them with `Race`, `Exclusive`, and `Simultaneous` for cross-gesture arbitration (single vs. double tap, drag vs. long press).
4444
- **Virtualized lists:** `FlatList` / `SectionList` window their rows in Python over the platform scroll view: uniform, exact, or measured variable heights, grids, headers/footers, infinite scroll, and an imperative scroll controller, identical on every platform.
45-
- **Direct native bindings:** Python calls platform APIs directly through Chaquopy and rubicon-objc, with no JavaScript bridge.
45+
- **Native rendering core:** each commit is one serialized transaction applied by Swift and Kotlin component managers, the same shape as React Native's Fabric. Device APIs are native modules registered by name (TurboModules-style), callable from thin Python facades.
4646
- **Custom-component SDK:** Wrap any platform widget as a first-class element with type-checked props via `pythonnative.sdk` (`Props`, `@native_component`, `element_factory`). Plugins distributed on PyPI auto-register through the `pythonnative.handlers` entry-point group.
4747
- **CLI scaffolding:** `pn init` creates a ready-to-run project; `pn run android` and `pn run ios` build and launch your app.
4848
- **Instant desktop preview:** `pn preview` renders your app in a native desktop window via Tkinter with Fast Refresh on every save: iterate on layout, state, and navigation in milliseconds without booting a simulator or device. The reconciler, hooks, layout engine, and navigation are the same code that ships to the phone.

‎docs/api/bridge.md‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Bridge
2+
3+
The Python half of the [native bridge](../concepts/bridge.md): JSON
4+
codec, per-platform transports, protocol handshake, main-queue posting,
5+
and the single callback native uses to reach Python. App code never
6+
calls this module directly; the
7+
[`BridgeBackend`][pythonnative.native_views.bridge_backend.BridgeBackend],
8+
[`BridgeModule`][pythonnative.native_modules.registry.BridgeModule], and
9+
[`NativeScreenHost`][pythonnative.hosts.native.NativeScreenHost] do.
10+
11+
::: pythonnative.bridge
12+
options:
13+
show_root_heading: false
14+
show_root_toc_entry: false
15+
members_order: source
16+
filters: ["!^_"]
17+
18+
## Codec
19+
20+
::: pythonnative.bridge.codec
21+
options:
22+
show_root_heading: false
23+
show_root_toc_entry: false
24+
members_order: source
25+
filters: ["!^_"]
26+
27+
## Fake transport (tests)
28+
29+
::: pythonnative.bridge.fake
30+
options:
31+
show_root_heading: false
32+
show_root_toc_entry: false
33+
members_order: source
34+
filters: ["!^_"]
35+
36+
## Bootstrap
37+
38+
::: pythonnative.bootstrap
39+
options:
40+
show_root_heading: false
41+
show_root_toc_entry: false
42+
members_order: source
43+
filters: ["!^_"]
44+
45+
## Next steps
46+
47+
- Wire format and native contracts: [The native bridge](../concepts/bridge.md).
48+
- Bundling native plugins: [Configuration](../guides/configuration.md#plugins).

‎docs/api/hosts.md‎

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ pause, back press, destroy) to navigators and effects. It's also the
77
[`Stack.Navigator`][pythonnative.create_stack_navigator] talks to when
88
it pushes real native screens.
99

10-
The bundled Android (`ScreenFragment`) and iOS (`ViewController`)
11-
templates create a host via [`create_screen`][pythonnative.create_screen]
12-
and never need to be edited by app code. The desktop preview uses
10+
The bundled Android (`PNScreenFragment`) and iOS (`PNViewController`)
11+
templates create a host through the `Host` native module's `create`
12+
event and never need to be edited by app code; one
13+
[`NativeScreenHost`][pythonnative.hosts.native.NativeScreenHost] class
14+
serves both platforms because every platform-specific step goes over
15+
the [bridge](../concepts/bridge.md). The desktop preview uses
1316
[`DesktopScreenHost`][pythonnative.hosts.desktop.DesktopScreenHost].
1417

1518
::: pythonnative.hosts
@@ -30,18 +33,11 @@ and never need to be edited by app code. The desktop preview uses
3033

3134
## Platform hosts
3235

33-
::: pythonnative.hosts.android
36+
::: pythonnative.hosts.native
3437
options:
3538
show_root_heading: false
3639
show_root_toc_entry: false
37-
members: ["AndroidScreenHost"]
38-
filters: ["!^_"]
39-
40-
::: pythonnative.hosts.ios
41-
options:
42-
show_root_heading: false
43-
show_root_toc_entry: false
44-
members: ["IOSScreenHost"]
40+
members_order: source
4541
filters: ["!^_"]
4642

4743
::: pythonnative.hosts.desktop

‎docs/api/native_modules.md‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,36 @@
33
Cross-platform wrappers around device APIs that are not part of the
44
view tree: camera, GPS, file I/O, notifications, clipboard, share
55
sheet, deep links, permissions, connectivity, secure storage, battery,
6-
haptics, and biometrics. Each module is implemented twice (once per
7-
platform) and dispatches at runtime based on the `IS_ANDROID` and
8-
`IS_IOS` flags from `pythonnative.utils`, with a safe desktop fallback.
6+
haptics, and biometrics. Each module is a Swift and a Kotlin class
7+
registered by name in the native runtime; the Python classes below are
8+
facades that call them through
9+
[`native_module`][pythonnative.native_modules.registry.native_module],
10+
with a Python implementation registered for the desktop and tests.
911

1012
Both synchronous and coroutine APIs exist (chosen to match the
1113
platform call). For the call-site patterns, the reactive
1214
`use_app_state` / `use_net_info` hooks, and the runtime coroutines are
1315
scheduled on, see the [Native modules guide](../guides/native-modules.md)
1416
and the [Async + data guide](../guides/async.md).
1517

18+
## Registry
19+
20+
::: pythonnative.native_modules.registry
21+
options:
22+
show_root_heading: false
23+
show_root_toc_entry: false
24+
members_order: source
25+
filters: ["!^_"]
26+
27+
## Desktop implementations
28+
29+
::: pythonnative.native_modules.desktop
30+
options:
31+
show_root_heading: false
32+
show_root_toc_entry: false
33+
members_order: source
34+
filters: ["!^_"]
35+
1636
## Camera
1737

1838
::: pythonnative.native_modules.camera
@@ -142,3 +162,4 @@ and the [Async + data guide](../guides/async.md).
142162
## Next steps
143163

144164
- See guidance and permission setup in [Native modules guide](../guides/native-modules.md).
165+
- Write your own: [Native modules guide](../guides/native-modules.md#writing-your-own-native-module).

0 commit comments

Comments
 (0)

Footer

© 2026 GitHub, Inc.

Back | FazBrowse Home | New Git URL