| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -44,6 +44,16 @@ jobs: | |||
| 44 | 44 | - name: Run tests | |
| 45 | 45 | run: uv run pytest -q | |
| 46 | 46 | ||
| 47 | + browser: | ||
| 48 | + runs-on: ubuntu-latest | ||
| 49 | + steps: | ||
| 50 | + - uses: actions/checkout@v4 | ||
| 51 | + - uses: astral-sh/setup-uv@v10.0.1 | ||
| 52 | + with: | ||
| 53 | + python-version: '3.13' | ||
| 54 | + - run: uv sync --locked --group dev | ||
| 55 | + - run: uv run python scripts/run-browser-tests.py | ||
| 56 | + | ||
| 47 | 57 | # The Swift half of the rendering core: decoders, component managers, | |
| 48 | 58 | # gestures, and module promises run under XCTest on a simulator. | |
| 49 | 59 | native-ios: | |
@@ -73,12 +83,21 @@ jobs: | |||
| 73 | 83 | ')" | |
| 74 | 84 | echo "Using simulator $UDID" | |
| 75 | 85 | xcodebuild test \ | |
| 76 | - -scheme PythonNativeKit \ | ||
| 86 | + -project Tests/NativeTests.xcodeproj \ | ||
| 87 | + -scheme NativeTests \ | ||
| 88 | + -parallel-testing-enabled NO \ | ||
| 77 | 89 | -destination "platform=iOS Simulator,id=$UDID" \ | |
| 78 | 90 | -derivedDataPath "$RUNNER_TEMP/pnkit" \ | |
| 79 | - -quiet | ||
| 91 | + -resultBundlePath "$RUNNER_TEMP/pnkit-results.xcresult" | ||
| 92 | + | ||
| 93 | + - name: Upload native test results | ||
| 94 | + if: failure() | ||
| 95 | + uses: actions/upload-artifact@v4 | ||
| 96 | + with: | ||
| 97 | + name: native-ios-results | ||
| 98 | + path: ${{ runner.temp }}/pnkit-results.xcresult | ||
| 80 | 99 | ||
| 81 | - # The Kotlin half has no Chaquopy dependency, so plain JUnit covers it. | ||
| 100 | + # JUnit covers contracts; Robolectric exercises native widget lifecycles. | ||
| 82 | 101 | native-android: | |
| 83 | 102 | runs-on: ubuntu-latest | |
| 84 | 103 | steps: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -53,6 +53,8 @@ jobs: | |||
| 53 | 53 | suites: navigation gestures layout styling | |
| 54 | 54 | - name: anim-misc | |
| 55 | 55 | suites: animations misc | |
| 56 | + - name: inbox | ||
| 57 | + suites: inbox | ||
| 56 | 58 | ||
| 57 | 59 | name: e2e-android (${{ matrix.name }}) | |
| 58 | 60 | ||
@@ -164,6 +166,8 @@ jobs: | |||
| 164 | 166 | suites: navigation gestures layout styling | |
| 165 | 167 | - name: anim-misc | |
| 166 | 168 | suites: animations misc | |
| 169 | + - name: inbox | ||
| 170 | + suites: inbox | ||
| 167 | 171 | ||
| 168 | 172 | name: e2e-ios (${{ matrix.name }}) | |
| 169 | 173 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -170,3 +170,6 @@ cython_debug/ | |||
| 170 | 170 | **/.gradle/ | |
| 171 | 171 | **/.kotlin/ | |
| 172 | 172 | **/.swiftpm/ | |
| 173 | + | ||
| 174 | + # Built sample extension wheels | ||
| 175 | + examples/inbox/vendor/ | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,5 +1,24 @@ | |||
| 1 | 1 | # CHANGELOG | |
| 2 | 2 | ||
| 3 | + ## Unreleased | ||
| 4 | + | ||
| 5 | + - Make element snapshots immutable and preserve ordinary Python component | ||
| 6 | + argument binding, async provider context, stable setters, and deferred updates. | ||
| 7 | + - Stage reconciliation publication and roll back failed renders; retire surfaces | ||
| 8 | + after native mount failures. Validate commits without copying unrelated records. | ||
| 9 | + - Enforce shared component, event, command, and service contracts, including | ||
| 10 | + nested values, property resets, recreation, and native result types. | ||
| 11 | + - Use one list request protocol on every backend, stable dataset and item | ||
| 12 | + revisions, bounded row ownership, and public indices that exclude headers. | ||
| 13 | + - Complete nested native stack ownership, input composition and selection, | ||
| 14 | + dynamic text sizing, cancelable native work, and durable Android notifications. | ||
| 15 | + - Test the real browser renderer and native lifecycle behavior. Package the Inbox | ||
| 16 | + extension separately and run reference-app acceptance on both mobile CI jobs. | ||
| 17 | + - Remove permissive transaction decoders, alternate headless list rendering, | ||
| 18 | + obsolete compatibility branches, and unrestricted device-option dictionaries. | ||
| 19 | + These pre-1.0 changes intentionally break superseded APIs. | ||
| 20 | + | ||
| 21 | + | ||
| 3 | 22 | ||
| 4 | 23 | ## v0.42.0 (2026-09-08) | |
| 5 | 24 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -106,7 +106,7 @@ uv run black src examples tests # format | |||
| 106 | 106 | uv run --group docs mkdocs serve # preview the docs site locally | |
| 107 | 107 | ||
| 108 | 108 | # native rendering core (macOS with Xcode for the Swift package; JDK 17 for Gradle) | |
| 109 | - (cd src/pythonnative/native/ios && xcodebuild test -scheme PythonNativeKit -destination 'platform=iOS Simulator,name=iPhone 15 Pro') | ||
| 109 | + (cd src/pythonnative/native/ios && xcodebuild test -project Tests/NativeTests.xcodeproj -scheme NativeTests -destination 'platform=iOS Simulator,name=iPhone 15 Pro') | ||
| 110 | 110 | (cd src/pythonnative/native/android && ../../templates/android_template/gradlew testDebugUnitTest) | |
| 111 | 111 | ``` | |
| 112 | 112 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -48,7 +48,9 @@ A surface commit has this shape: | |||
| 48 | 48 | The renderer validates the entire operation sequence before mutation: operation | |
| 49 | 49 | arity, live tags, insertion bounds, cycles, typed values, and finite geometry. | |
| 50 | 50 | An accepted commit returns `ok`, `application`, `surface`, and the exact | |
| 51 | - `revision`. Python advances its native bookkeeping only after that acknowledgement. | ||
| 51 | + `revision`, plus optional native timing metrics. Python advances its bookkeeping | ||
| 52 | + only after that acknowledgment. A rejection with `failed: false` reports | ||
| 53 | + validation failure before mutation and preserves the previous revision. | ||
| 52 | 54 | A failed surface rejects further incremental updates. A new application identity | |
| 53 | 55 | and a complete remount establish a clean surface; replaying a partial commit | |
| 54 | 56 | isn't a recovery strategy. The current app host uses one surface. | |
@@ -73,14 +75,24 @@ core, and the browser preview uses the corresponding Yoga WebAssembly package. | |||
| 73 | 75 | ||
| 74 | 76 | The application sends styles as props. Native leaf managers measure text, | |
| 75 | 77 | controls, and images next to their widgets. One `Layout.compute` request returns | |
| 76 | - a batch of changed frames. Python doesn't make one measurement RPC per leaf. | ||
| 78 | + a batch of changed frames with `application`, `surface`, and `revision`. | ||
| 79 | + Geometry from another surface or revision is ignored. Drawing-only prop updates | ||
| 80 | + skip the request entirely. Python doesn't make one measurement RPC per leaf. | ||
| 81 | + | ||
| 82 | + Commit and required layout acknowledgments precede ref and effect publication. | ||
| 83 | + Native navigation animations and later platform layout changes may continue after | ||
| 84 | + that boundary; their geometry returns through the same identified layout channel. | ||
| 77 | 85 | ||
| 78 | 86 | A native screen, portal, modal, or recycled row can be physically attached to a | |
| 79 | 87 | platform container while remaining a child in the same Python component tree. | |
| 80 | 88 | Context, error boundaries, suspense boundaries, and task ownership follow that | |
| 81 | 89 | logical tree. Lists use stable keys and data revisions to validate row requests. | |
| 82 | 90 | UIKit collection views and Android recycler views own physical cells; a bounded | |
| 83 | - window of ordinary keyed Python row components supplies their contents. | ||
| 91 | + window of ordinary keyed Python row components supplies their contents. Headless | ||
| 92 | + tests simulate the same viewport and row requests. Dataset revisions change when | ||
| 93 | + data or rendering inputs change; per-item revisions avoid reloading unchanged | ||
| 94 | + cells. Global and section headers don't change public item indices. Row instances | ||
| 95 | + can unmount outside the window, so persistent item state belongs in app data. | ||
| 84 | 96 | ||
| 85 | 97 | ## Modules, contracts, and animations | |
| 86 | 98 | ||
@@ -99,3 +111,12 @@ Animation graphs describe values, arithmetic, interpolation, colors, and | |||
| 99 | 111 | clamping. Native timing, spring, and decay drivers evaluate connected bindings | |
| 100 | 112 | on the UI thread. Scroll and gesture mappings feed graph values directly, so | |
| 101 | 113 | visual updates can continue while the Python application thread is busy. | |
| 114 | + | ||
| 115 | + ## Profiling the boundary | ||
| 116 | + | ||
| 117 | + `PN_PROFILE` traces separate rendering, validation, transport, native mutation, | ||
| 118 | + and layout. Native duration samples appear on a separate trace track, placed at | ||
| 119 | + host receipt without assuming synchronized clocks. Work counters report copied | ||
| 120 | + validation records, operations, native views visited, and changed frames. A | ||
| 121 | + one-property regression test uses 10,000 records to check work growth directly. | ||
| 122 | + These counters and host timings aren't device frame-rate claims. | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -34,15 +34,19 @@ tree: | |||
| 34 | 34 | as needed using [`InsertOp`][pythonnative.mutations.InsertOp] / | |
| 35 | 35 | [`DestroyOp`][pythonnative.mutations.DestroyOp]. | |
| 36 | 36 | ||
| 37 | - The reconciler runs each render function synchronously to completion on the | ||
| 38 | - application thread. Expensive Python work delays callbacks and commits. | ||
| 37 | + The reconciler runs synchronous render functions to completion on the | ||
| 38 | + application thread. Async components capture their provider environment across | ||
| 39 | + awaits. Changes to their inputs cancel obsolete work before it can publish. | ||
| 40 | + Expensive Python work delays callbacks and commits. | ||
| 39 | 41 | Native scrolling and supported animation drivers run independently; see | |
| 40 | 42 | [Runtime limits](architecture.md#runtime-limits). | |
| 41 | 43 | ||
| 42 | 44 | ## Commits are transactions | |
| 43 | 45 | ||
| 44 | - The diff phase is pure: it only accumulates ops. At the end of the | ||
| 45 | - pass the staged mutations are applied through a single | ||
| 46 | + Rendering records changes in an undo journal and stages native operations, | ||
| 47 | + callbacks, refs, and effects. A render that fails restores the committed tree | ||
| 48 | + without publishing those changes. At the end of the pass, staged mutations are | ||
| 49 | + applied through a single | ||
| 46 | 50 | [`apply_mutations`][pythonnative.native_views.NativeViewRegistry.apply_mutations] | |
| 47 | 51 | call. The renderer validates the versioned transaction before applying it | |
| 48 | 52 | and acknowledges the committed revision. Layout then runs beside the native | |
@@ -182,3 +186,22 @@ bottleneck. | |||
| 182 | 186 | - Browse the algorithm in code: [Reconciler API](../api/reconciler.md). | |
| 183 | 187 | - Understand handlers underneath the diff: [Native views](native-views.md). | |
| 184 | 188 | - See how mounting interacts with hooks: [Lifecycle](lifecycle.md). | |
| 189 | + | ||
| 190 | + ## Elements and update priority | ||
| 191 | + | ||
| 192 | + Elements hold read-only property snapshots and tuple children. Values inside | ||
| 193 | + application props aren't deep-copied; replace mutable application data when its | ||
| 194 | + meaning changes. Component construction follows the decorated function's Python | ||
| 195 | + signature, including positional-only parameters, keyword-only parameters, | ||
| 196 | + variadic children, and normal argument errors. | ||
| 197 | + | ||
| 198 | + State setters and reducer dispatch functions retain their identity across renders. | ||
| 199 | + Deferred updates stay out of urgent renders. When deferred work runs, the state | ||
| 200 | + queue replays urgent updates in their original order so an urgent edit isn't lost. | ||
| 201 | + Python rendering remains cooperative: a long synchronous function cannot be | ||
| 202 | + preempted by a later urgent update. | ||
| 203 | + | ||
| 204 | + A native mount failure retires the surface and cancels its work. Validation | ||
| 205 | + failure before native mutation preserves the previous revision and can be retried. | ||
| 206 | + Property validation copies only changed records, so a one-property update doesn't | ||
| 207 | + clone the rest of the live native tree. | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,8 +2,9 @@ | |||
| 2 | 2 | ||
| 3 | 3 | Declare an extension's interface in Python, generate its native adapters, and | |
| 4 | 4 | implement the platform behavior in Swift and Kotlin. The complete working | |
| 5 | - example is `examples/inbox`: its badge uses generated props, and its module | ||
| 6 | - has synchronous and asynchronous methods. | ||
| 5 | + example is the separately packaged `examples/inbox-extension`, consumed by | ||
| 6 | + `examples/inbox`. Its badge uses generated props, and its module has synchronous | ||
| 7 | + and asynchronous methods. | ||
| 7 | 8 | ||
| 8 | 9 | ## Define the interface | |
| 9 | 10 | ||
@@ -66,8 +67,8 @@ Set `contracts` in the plugin's `pn_plugin.json` to its schema file: | |||
| 66 | 67 | } | |
| 67 | 68 | ``` | |
| 68 | 69 | ||
| 69 | - The schema should contain the extension's own components and modules. The inbox | ||
| 70 | - example's `generate_contracts.py` demonstrates extracting those definitions. | ||
| 70 | + The schema should contain the extension's own components and modules. The | ||
| 71 | + `examples/inbox-extension/generate_contracts.py` script demonstrates extracting those definitions. | ||
| 71 | 72 | Builders merge plugin contracts with the built-ins, regenerate native bindings, | |
| 72 | 73 | and bundle the matching schema into embedded Python. Startup rejects a client | |
| 73 | 74 | whose protocol, Yoga version, or schema fingerprint doesn't match. | |
@@ -92,3 +93,35 @@ mobile binary extensions into the desktop build process. | |||
| 92 | 93 | ||
| 93 | 94 | Rebuild after changing a contract or native implementation. Fast Refresh applies | |
| 94 | 95 | to Python application behavior; it can't change a compiled native interface. | |
| 96 | + | ||
| 97 | + ## Runtime enforcement | ||
| 98 | + | ||
| 99 | + Built-in widgets and services use these contracts too. Unknown props, methods, | ||
| 100 | + events, and arguments fail validation. Nested dataclasses and `TypedDict` values | ||
| 101 | + validate required fields and reject unknown fields. Wire integers must be exact | ||
| 102 | + numbers within JavaScript's safe integer range; booleans don't count as numbers. | ||
| 103 | + Methods declare both their arguments and their return types. Python validates | ||
| 104 | + native results before exposing them to application code. | ||
| 105 | + | ||
| 106 | + A `null` update removes a prop. The renderer restores its declared default, or | ||
| 107 | + recreates the physical widget to restore a platform default. Creation-only | ||
| 108 | + changes, including `TextInput.multiline`, preserve the logical tag, component | ||
| 109 | + state, child ownership, focus, and selection where the platform permits it. | ||
| 110 | + Refs keep addressing that logical tag. `NativeField.platforms` limits where a | ||
| 111 | + field can be used; unsupported fields are rejected by the target renderer. | ||
| 112 | + | ||
| 113 | + Regenerate the repository's built-in bindings with: | ||
| 114 | + | ||
| 115 | + ```sh | ||
| 116 | + uv run python scripts/generate-native-contracts.py | ||
| 117 | + ``` | ||
| 118 | + | ||
| 119 | + The shared fixtures run in Python, Swift, Kotlin, and JavaScript. The actual | ||
| 120 | + browser renderer runs in Chrome with Yoga WebAssembly: | ||
| 121 | + | ||
| 122 | + ```sh | ||
| 123 | + uv run python scripts/run-browser-tests.py | ||
| 124 | + ``` | ||
| 125 | + | ||
| 126 | + Native service adapters are named `NameModuleAdapter`, which keeps the generated | ||
| 127 | + base class distinct from a platform's `NameModule` implementation. | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -221,7 +221,7 @@ The Swift and Kotlin halves have their own suites next to the code: | |||
| 221 | 221 | ||
| 222 | 222 | ```bash | |
| 223 | 223 | cd src/pythonnative/native/ios | |
| 224 | - xcodebuild test -scheme PythonNativeKit -destination 'platform=iOS Simulator,name=iPhone 15 Pro' | ||
| 224 | + xcodebuild test -project Tests/NativeTests.xcodeproj -scheme NativeTests -destination 'platform=iOS Simulator,name=iPhone 15 Pro' | ||
| 225 | 225 | ||
| 226 | 226 | cd src/pythonnative/templates/android_template | |
| 227 | 227 | ./gradlew :pythonnative:testDebugUnitTest | |
@@ -232,6 +232,13 @@ promise envelopes, so a wire-format change is caught on every side. Add | |||
| 232 | 232 | cases there when you change a manager or module rather than relying on | |
| 233 | 233 | the Maestro E2E suite alone. | |
| 234 | 234 | ||
| 235 | + The iOS `NativeTests` scheme runs the Swift tests inside a minimal UIKit | ||
| 236 | + app. Use that scheme for native tests: composition, selection, and focus | ||
| 237 | + need an application event loop and keyboard session, which the standalone | ||
| 238 | + Swift package test runner doesn't provide. Add new Swift test files to | ||
| 239 | + both the test directory and the Xcode project's `PythonNativeKitTests` | ||
| 240 | + target. | ||
| 241 | + | ||
| 235 | 242 | ## Testing async code | |
| 236 | 243 | ||
| 237 | 244 | `render` and every event helper settle the framework's `asyncio` loop | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,8 +1,8 @@ | |||
| 1 | 1 | """Demo screen for [`pn.TextInput`][pythonnative.TextInput]. | |
| 2 | 2 | ||
| 3 | - Maestro types into the single-line input and asserts the ``Echo:`` | ||
| 4 | - line mirrors the typed value. The multiline variant is also rendered | ||
| 5 | - so flows can confirm both modes coexist on one screen. | ||
| 3 | + Maestro types into the name input, checks its echo and selection, then | ||
| 4 | + switches between single-line and multiline widgets without losing the value. | ||
| 5 | + A separate multiline note field shows a live character count. | ||
| 6 | 6 | """ | |
| 7 | 7 | ||
| 8 | 8 | from __future__ import annotations | |
@@ -18,6 +18,8 @@ def TextInputDemo() -> pn.Element: | |||
| 18 | 18 | name, set_name = pn.use_state("") | |
| 19 | 19 | notes, set_notes = pn.use_state("") | |
| 20 | 20 | focused, set_focused = pn.use_state(False) | |
| 21 | + multiline, set_multiline = pn.use_state(False) | ||
| 22 | + selection, set_selection = pn.use_state({"start": 0, "end": 0}) | ||
| 21 | 23 | ||
| 22 | 24 | field_style = pn.style( | |
| 23 | 25 | padding=10, | |
@@ -39,6 +41,8 @@ def TextInputDemo() -> pn.Element: | |||
| 39 | 41 | value=name, | |
| 40 | 42 | placeholder="Type your name here", | |
| 41 | 43 | on_change=set_name, | |
| 44 | + multiline=multiline, | ||
| 45 | + on_selection_change=set_selection, | ||
| 42 | 46 | on_focus=lambda: set_focused(True), | |
| 43 | 47 | on_blur=lambda: set_focused(False), | |
| 44 | 48 | clear_button=True, | |
@@ -48,7 +52,11 @@ def TextInputDemo() -> pn.Element: | |||
| 48 | 52 | style=field_style, | |
| 49 | 53 | ), | |
| 50 | 54 | result_text("Echo", name or "(empty)"), | |
| 51 | - hint("Maestro types here and asserts the echo + focus update."), | ||
| 55 | + result_text("Selection", f"{selection['start']}:{selection['end']}"), | ||
| 56 | + pn.Button( | ||
| 57 | + "Use single line" if multiline else "Use multiline name", on_press=lambda: set_multiline(not multiline) | ||
| 58 | + ), | ||
| 59 | + hint("Switch the name field between modes while keeping its text."), | ||
| 52 | 60 | ), | |
| 53 | 61 | section( | |
| 54 | 62 | "Multiline", | |
| Back | FazBrowse Home | New Git URL |
0 commit comments