Contributing¶
The canonical contributing guide lives at the repository root in
CONTRIBUTING.md.
The contents are included below so the docs site stays in sync.
Contributing to PythonNative¶
Thanks for your interest in contributing. This repository contains the PythonNative library, the pn CLI, Android/iOS project templates, example apps, and the documentation site. Contributions should keep the code reliable, cross-platform, and easy to use.
Quick start¶
Development uses Python ≥ 3.13 (the same versions the apps embed) and uv as the
only prerequisite. uv sync creates and updates .venv itself, so there's no
virtual environment to make and nothing to activate.
# install the project and its dev tooling into .venv, exactly as CI does
uv sync --locked --group dev
# one-shot: runs every CI check, in the same order
./scripts/check.sh
# run individual steps if you only want one
uv run pytest -q
uv run ruff check .
uv run black src examples tests
Dependencies are pinned in uv.lock, which is committed, so local runs and CI
resolve identically. --locked fails rather than silently re-resolving if the
lockfile has drifted from pyproject.toml. Upgrading a dependency is a
deliberate uv lock --upgrade-package <name> in its own commit.
Common library and CLI entry points. uv run puts .venv/bin on the path
for one command, so pn resolves without activating anything. Each block
starts from the repository root.
# CLI help
uv run pn --help
# scaffold a sample app into ./my_app/, then work inside it
uv run pn init my_app
cd my_app
uv run pn run android
uv run looks upward for the nearest project, so it keeps using the
repository's .venv from a subdirectory. The bundled example works the same
way:
Claiming an issue¶
To avoid duplicate work, claim an issue before you start on it:
- Check the issue's assignee and any linked pull requests. If either exists, the issue is taken.
- Comment on the issue to claim it and wait for a maintainer to assign it to you before opening a pull request.
- If you stop working on an assigned issue, leave a comment so it can be reassigned.
Unsolicited pull requests for issues that are already assigned or already have an open pull request will be closed as duplicates, even if the work is good.
Project layout (high‑level)¶
src/pythonnative/: installable library and CLI- Core Python modules, UI components, and utilities
bridge/: JSON codec and per-platform transports into the native rendering corecli/:pncommandproject/: config loading, template configuration, native plugin staging, and the builder behindpnnative/: native libraries and their tests, bundled with the Python packageios/:PythonNativeKitSwift package and its XCTest targetandroid/: Android rendering library and its JUnit targetyoga/: vendored Yoga source, native build definitions, and the host Python binding
sdk/: native contract definitions, code generation, and generator templatesdevserver/static/: browser preview assets, including the vendored Yoga JavaScript/WebAssembly distributiontemplates/: Android/iOS app shells; the builder stages native libraries into generated app projectstests/: unit tests for the library, plus the Maestro E2E suitee2e/: the comprehensive E2E suite (see E2E tests below andtests/e2e/AGENTS.md)examples/: runnable example appshello-world/: minimal marketing demoe2e-suite/: comprehensive feature catalog that drives the Maestro E2E suiteinbox/: offline reference app with a custom native extensionscripts/: helper scripts (check.sh,run-e2e.sh,check-e2e-coverage.py)README.md,pyproject.toml: repo docs and packaging
Coding guidelines¶
- Style: Black; lint: Ruff; typing where useful. Keep APIs stable.
- Prefer explicit, descriptive names; keep platform abstractions clean.
- Python never imports platform code. Everything that touches
UIVieworandroid.view.Viewlives in Swift (PythonNativeKit) or Kotlin (thepythonnativeGradle module) and is reached throughpythonnative.bridge; seedocs/concepts/bridge.mdfor the protocol. BumpPROTOCOL_VERSIONon both sides when the wire format changes. - Add/extend tests under
tests/for new behavior. Native changes get XCTest / JUnit coverage next to the code they touch. - Design large changes in an RFC first. Anything that adds or removes public API, changes the Python-to-native wire contract, changes a convention apps depend on, or spans Python, Swift, Kotlin, and the browser preview gets a document under
rfcs/(seerfcs/README.md). The RFC lands with the implementation and records what shipped, including removals. - Commit source, reviewed generated contracts, dependency locks, and vendored dependencies as described in Generated source and vendored dependencies. Don't commit local build outputs or caches.
- Docstrings: Google style throughout. Ruff is configured with the Google
convention (
pydocstyle.convention = "google") and enforces theDrule set onsrc/pythonnative/. See the Documentation style guide for examples and Markdown/grammar conventions.
Common commands:
./scripts/check.sh # run all CI checks (mirrors ci.yml)
uv sync --locked --group dev # install/refresh .venv from the lockfile
uv run pytest -q # run tests
uv run ruff check . # lint
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 -project Tests/NativeTests.xcodeproj -scheme NativeTests -destination 'platform=iOS Simulator,name=iPhone 15 Pro')
(cd src/pythonnative/native/android && ../../templates/android_template/gradlew testDebugUnitTest)
Generated source and vendored dependencies¶
Some generated files and third-party distribution files are intentional parts of the source tree. Keep these in version control:
- Native contracts:
PNContracts,NativeProps, andNativeModulesinsrc/pythonnative/native/ios/Sources/PythonNativeKit/Generated/andsrc/pythonnative/native/android/src/main/java/com/pythonnative/generated/. These support standalone native builds. Regenerate them from the Python definitions and generator templates instead of editing them by hand. - Example extension schemas, such as
examples/inbox/native/schema.json. Commit schema updates alongside their Python definitions and native implementations. - Dependency locks, including
uv.lockand example apps'pn.lockfiles. - Vendored Yoga source and browser distribution files under
src/pythonnative/native/yoga/andsrc/pythonnative/devserver/static/yoga/. The browser distribution includes its JavaScript/WebAssembly runtime, type declarations, and source maps. Preserve upstream licenses and document the version, source, and any local modifications when updating vendored code.
After changing built-in contracts or their generators, regenerate the checked-in native files from the repository root:
uv run pn codegen --output build/contracts
cp build/contracts/{PNContracts,NativeProps,NativeModules}.swift \
src/pythonnative/native/ios/Sources/PythonNativeKit/Generated/
cp build/contracts/{PNContracts,NativeProps,NativeModules}.kt \
src/pythonnative/native/android/src/main/java/com/pythonnative/generated/
uv run pytest tests/test_codegen.py
The test checks that the committed native contracts match generation in a clean
Python interpreter. The other files under build/contracts/ are local outputs;
don't add the whole directory to the commit.
Exclude staged app projects, compiled apps and libraries, package distributions,
test reports, local storage, credentials, and tool caches. Examples include
build/, dist/, .build/, .gradle/, .cxx/, .swiftpm/, and __pycache__/.
The vendored browser runtime above is an explicit exception to excluding
compiled dependencies; downloaded Python runtimes and locally compiled binaries
aren't.
Conventional Commits¶
This project uses Conventional Commits. Use the form:
Commit message character set¶
- Encoding: UTF‑8 is allowed and preferred across subjects and bodies.
- Keep the subject ≤ 72 chars; avoid emoji.
Accepted types (standard):
build: build system or external dependencies (e.g., requirements, packaging)chore: maintenance (no library behavior change)ci: continuous integration configuration (workflows, pipelines)docs: documentation onlyfeat: user‑facing feature or capabilityfix: bug fixperf: performance improvementsrefactor: code change that neither fixes a bug nor adds a featurerevert: revert of a previous commitstyle: formatting/whitespace (no code behavior)test: add/adjust tests only
Recommended scopes (choose the smallest, most accurate unit; prefer module/directory names):
- Module/directory scopes:
alerts: imperative Alert/Picker helpers (alerts.py)animated: Animated namespace and animation primitives (animated.py)bootstrap: on-device runtime start-up called by the templates (bootstrap.py)bridge: wire codec, transports, handshake, and native-to-Python callback (bridge/)cli: CLI tool andpncommand (src/pythonnative/cli/)component: the@componentdecorator,Component, andmemo(component.py)components: declarative element-creating functions (components/)element: Element descriptor class (element.py)events: tag-based event routing between native views and Python callbacks (events.py)gestures: gesture descriptors and the pure-Python recognition arbiter (gestures.py)hooks: hooks and contexts (hooks.py)hosts: screen hosts, lifecycle forwarding, and render scheduling (hosts/)devserver: dev server, file watcher, and WebSocket implementation (devserver/)devclient: on-device dev client that syncs sources and Fast Refreshes (devclient.py)hot_reload: module reloader and Fast Refresh (hot_reload.py)layout: Yoga layout integration and the host binding (layout.py,native/yoga/)mutations: batched mutation ops between reconciler and native backends (mutations.py)native_modules: native module registry, Python facades, and Python fallbacks (native_modules/)native_views: view registry protocol and bridge backend (native_views/)navigation: navigation state, container, navigators, hooks, and linking (navigation/)net: awaitable HTTP client (net.py)package:src/pythonnative/__init__.pyexports and package boundaryplatform:Platform.OS/Platform.selectand version detection (platform.py)platform_metrics: platform-reported metrics like safe-area insets and bar heights (platform_metrics.py)preview:pn start/pn previewsession and the browser preview page (preview.py,devserver/static/)project:pythonnative.tomlconfig, template configurators, plugin staging, and the builder (project/)reconciler: virtual view tree diffing, boundaries, and the layout pass (reconciler/)runtime: framework-wide asyncio loop and thread-safe future helpers (runtime.py)sdk: public extension SDK for custom native components (sdk/)storage: AsyncStorage key/value persistence anduse_persisted_state(storage.py)scheduler: render batching and transition queues (scheduler.py)style: StyleSheet and theming (style.py)testing: public test utilities (testing/)-
utils: shared utilities (utils.py) -
Other scopes:
deps: dependency updates and version pinsexamples: example apps underexamples/mkdocs: documentation site (MkDocs/Material) configuration and content underdocs/pyproject:pyproject.tomlpackaging/build metadatarepo: repository metadata and top‑level files (README.md,CONTRIBUTING.md,.gitignore, licenses)scripts: developer scripts underscripts/(e.g.,check.sh)templates: Android/iOS project templates undersrc/pythonnative/templates/kit: the Swift rendering core (native/ios/)runtime-android: the Kotlin rendering core (native/android/)tests: unit/integration/E2E tests undertests/workflows: CI pipelines under.github/workflows/
Note: Avoid redundant type==scope pairs (e.g., docs(docs)). Prefer a module scope (e.g., docs(core)) or docs(repo) for top‑level updates.
Examples:
build(deps): refresh pinned versions
chore(repo): add contributing guidelines
ci(workflows): add publish job
docs(reconciler): clarify diffing algorithm
feat(components): add Slider element
fix(cli): handle missing Android SDK gracefully
perf(reconciler): reduce allocations in list diffing
refactor(utils): extract path helpers
test: cover iOS template copy flow
Examples (no scope):
Breaking changes:
- Use
!after the type/scope or aBREAKING CHANGE:footer.
feat(hosts)!: rename create_page to create_screen
BREAKING CHANGE: API renamed; update app code and templates.
Multiple scopes (optional)¶
- Comma‑separate scopes without spaces:
type(scope1,scope2): ... - Prefer a single scope when possible; use multiple only when the change genuinely spans tightly related areas.
Scope ordering (house style):
- Put the most impacted scope first (e.g.,
repo), then any secondary scopes. - For extra consistency, alphabetize the remaining scopes after the primary.
- Keep it to 1–3 scopes max.
Example:
Pull requests and squash merges¶
- PR title: use Conventional Commit format.
- Example:
feat(cli): add init subcommand - Imperative mood; no trailing period; ≤ 72 chars;
!for breaking changes. - PR description: include brief sections: What, Why, How (brief), Testing, Risks/Impact, Docs/Follow‑ups.
- Link issues with keywords (e.g.,
Closes #123). - Merging: prefer "Squash and merge" with "Pull request title and description".
- Keep PRs focused; avoid unrelated changes in the same PR.
Recommended PR template:
What
- Short summary of the change
Why
- Motivation/user value
How (brief)
- Key implementation notes or decisions
Testing
- Local/CI coverage; links to tests if relevant
Risks/Impact
- Compat, rollout, perf, security; mitigations
Docs/Follow-ups
- Docs updated or TODO next steps
Closes #123
BREAKING CHANGE: <details if any>
Co-authored-by: Name <email>
Pull request checklist¶
- PR title: Conventional Commits format (CI-enforced by
pr-lint.yml). - Tests: added/updated;
uv run pytestpasses. - Lint/format:
uv run ruff check .anduv run black --check src examples testspass. - Docs: update
README.mdif behavior changes. - Templates: update
src/pythonnative/templates/if generator output changes. - Generated contracts and example schemas are regenerated and reviewed when changed; vendored dependencies retain their licenses and provenance.
- No local build outputs, caches, credentials, or machine-specific files are committed.
Versioning and releases¶
- The version is tracked in
pyproject.toml(project.version) and mirrored insrc/pythonnative/__init__.pyas__version__. Both files are updated automatically by python-semantic-release. - Automated release pipeline (on every merge to
main): python-semantic-releasescans Conventional Commits, updates the version files anduv.lock, generatesCHANGELOG.md, and pushes the release commit and tag.- A published GitHub release is created with generated release notes.
- The shared
Distributionsworkflow builds a source archive from that exact commit, then uses cibuildwheel to build all wheels from the archive. - Each wheel is repaired where needed, installed in an isolated environment, and tested with the Yoga layout suite and CLI. The complete artifact set must also pass platform, version, resource, and metadata checks.
- Validated distributions are attached to the GitHub release before PyPI uploads begin. PyPI uses Trusted Publishing; no API token is needed.
- The same distribution builds and checks run on PRs. The wheel matrix covers CPython 3.13 and 3.14 on Linux x86-64 and ARM64 (glibc 2.28 or newer), macOS Intel and Apple Silicon (macOS 11 or newer), and Windows x64. These are development-host wheels; mobile apps compile the bundled Yoga source in their native builds.
- Build policy lives in
scripts/cibuildwheel.toml, separately from the tagged package source, so fixed tooling can rebuild an existing release without changing its code or version. - Commit types that trigger a release:
feat(minor),fixandperf(patch), andBREAKING CHANGE(major, or minor before 1.0). Other types, includingbuildandci, don't trigger a release on their own. Use abuildorcititle for a publishing-only repair that should recover the existing version. - Tag format:
v-prefixed (for example,v0.40.0). Manual version bumps aren't needed.
Recovering a failed publication¶
Version creation and package publication are separate jobs. A GitHub release can exist even when its PyPI upload failed. Rerunning version creation won't create another release for the same commits.
To resume publication, select Actions → Release → Run workflow, choose
main, and enter the existing tag in the recovery field. If the failure
requires a workflow repair, merge that repair first. For example, with the
GitHub CLI:
Recovery verifies that the tag belongs to main and matches the package version,
then runs the same build and validation jobs. It doesn't bump the version,
rewrite the tag, or change the tagged source. Existing distribution assets are
reused byte for byte; missing assets are uploaded before publishing to PyPI.
Files already uploaded to PyPI are skipped, so a partial upload can resume.
Release runs are serialized to prevent competing uploads.
For a build-only rehearsal, run Distributions with a source-ref of the tag
or commit to check. This runs the full matrix without publishing anything:
Branch naming (suggested)¶
- Use lowercase kebab‑case; concise (≤ 40 chars).
- Branch prefixes match Conventional Commit types:
feat/<scope>-<short-desc>fix/<issue-or-bug>-<short-desc>chore/<short-desc>docs/<short-desc>ci/<short-desc>refactor/<scope>-<short-desc>test/<short-desc>perf/<short-desc>build/<short-desc>
Examples:
feat/cli-init
fix/core-threading-deadlock-123
docs/contributing
ci/publish-pypi
build/lock-versions
refactor/utils-paths
test/templates-android
fix/cli-regression
E2E tests (Maestro)¶
End-to-end tests use Maestro to drive the dedicated examples/e2e-suite app on real emulators and simulators. That app contains one screen per public symbol in pythonnative.__all__; every flow under tests/e2e/flows/<category>/ exercises one symbol.
The dedicated examples/hello-world app is left in place as a small marketing demo; it's not the E2E target.
# Install Maestro (one-time)
curl -Ls "https://get.maestro.mobile.dev" | bash
# For iOS, also install idb-companion
brew tap facebook/fb && brew install idb-companion
Build and run everything via the convenience script:
# Android (emulator must be running)
uv run ./scripts/run-e2e.sh android
# iOS (simulator must be running)
uv run ./scripts/run-e2e.sh ios
For tight iteration, run a single category instead of the full pass:
Available categories: components, hooks, navigation, layout, styling, animations, misc.
A coverage checker, scripts/check-e2e-coverage.py, gates CI: every name in pythonnative.__all__ must be covered by a demo + flow, or listed in INTENTIONAL_EXEMPTIONS with a justification.
When you add a new public symbol you must also:
- Add a demo screen under
examples/e2e-suite/app/screens/<category>/. - Append a
DemoEntryinexamples/e2e-suite/app/registry.py. - Add a Maestro flow at
tests/e2e/flows/<category>/<name>.yaml. - Append the flow to the top-level
tests/e2e/android.yaml,tests/e2e/ios.yaml, and the matchingtests/e2e/suites/<category>.yaml. - Confirm
uv run python scripts/check-e2e-coverage.pyexits 0.
tests/e2e/AGENTS.md is the deeper reference (label conventions, failure triage, naming rules); AI agents should read it before touching the suite. The e2e.yml workflow runs the suite automatically on pushes to main and PRs.
CI¶
- CI (
ci.yml): runs formatter, linter, type checker, and tests on every push and PR. - E2E (
e2e.yml): buildsexamples/e2e-suiteon Android (Linux emulator) and iOS (macOS simulator), then runs Maestro flows by category. Triggers on pushes tomain, PRs, and manual dispatch. - Packages (
packages.yml): resolves the PyPI compatibility matrix intests/packages/matrix.tomlagainst the live indexes withscripts/package-matrix.py --check, weekly and on changes to the resolver or manifest, and uploads the rendered Markdown table fordocs/guides/pypi-packages.md. - PR Lint (
pr-lint.yml): validates the PR title against Conventional Commits format (protects squash merges) and checks individual commit messages via commitlint (protects rebase merges). Recommended: add the PR title job as a required status check in branch-protection settings. - Release (
release.yml): runs on merge tomain; computes version, generates changelog, tags, creates GitHub Release, and (whenDRAFT_RELEASEis"false") publishes to PyPI. - Docs (
docs.yml): builds the MkDocs site in strict mode on every push and pull request, and deploys to GitHub Pages on push tomain.
Security and provenance¶
- Avoid bundling secrets or credentials in templates or code.
- Prefer runtime configuration via environment variables in CI.
License¶
By contributing, you agree that your contributions are licensed under the repository's MIT License.