[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/pythonnative/pythonnative/main/CONTRIBUTING.md [Back]  [Original]

### 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](https://docs.astral.sh/uv/) as the
only prerequisite. `uv sync` creates and updates `.venv` itself, so there's no
virtual environment to make and nothing to activate.

```bash
# 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 ` 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.

```bash
# 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:

```bash
# run the Hello World example
cd examples/hello-world && uv run pn run android
```

## Claiming an issue

To avoid duplicate work, claim an issue before you start on it:

1. Check the issue's assignee and any linked pull requests. If either exists, the issue is taken.
2. Comment on the issue to claim it and wait for a maintainer to assign it to you before opening a pull request.
3. 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 (highlevel)

- `src/pythonnative/`: installable library and CLI
  - Core Python modules, UI components, and utilities
  - `components/`: element factories and the typed event records (`components/events.py`)
  - `handles.py`: typed imperative handles published on `ref.current`
  - `journal.py`: the undo journal a failed render pass rolls back through
  - `native_views/`: the thin backend accessor (`get_backend` / `set_backend`) and the bridge backend that serializes commits
  - `native_modules/`: device module facades and their Python fallbacks
  - `navigation/`: navigator values, screens, container, hooks, deep-link table, and the navigation ref
  - `theme.py`, `style.py`, `store.py`: the app `Theme`, `StyleSheet` namespaces and style resolution, and `Store`
  - `lint.py`: the `pn lint` rules-of-hooks checker
  - `testing/`: the headless testing library and the shipped pytest plugin (`testing/pytest_plugin.py`)
  - `bridge/`: JSON codec and per-platform transports into the native rendering core
  - `cli/`: `pn` command
  - `project/`: config loading, template configuration, native plugin staging, and the builder behind `pn`
  - `native/`: native libraries and their tests, bundled with the Python package
    - `ios/`: `PythonNativeKit` Swift package and its XCTest target
    - `android/`: Android rendering library and its JUnit target
    - `yoga/`: vendored Yoga source, native build definitions, and the host Python binding
  - `sdk/`: native contract definitions, code generation, and generator templates
  - `devserver/static/`: browser preview assets, including the vendored Yoga JavaScript/WebAssembly distribution
  - `templates/`: Android/iOS app shells; the builder stages native libraries into generated app projects
- `tests/`: unit tests for the library, plus the Maestro E2E suite
  - `e2e/`: the comprehensive E2E suite (see [E2E tests](#e2e-tests-maestro) below and `tests/e2e/AGENTS.md`)
- `examples/`: runnable example apps
  - `hello-world/`: minimal marketing demo
  - `e2e-suite/`: comprehensive feature catalog that drives the Maestro E2E suite
  - `inbox/`: offline reference app with a custom native extension
- `scripts/`: 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 `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.
- 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/` (see [`rfcs/README.md`](https://github.com/pythonnative/pythonnative/blob/main/rfcs/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](#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 the `D` rule
  set on `src/pythonnative/`. See the
  [Documentation style guide](https://pythonnative.com/meta/style-guide/)
  for examples and Markdown/grammar conventions.

Common commands:

```bash
./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`, and `NativeModules` in
  `src/pythonnative/native/ios/Sources/PythonNativeKit/Generated/` and
  `src/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.lock` and example apps' `pn.lock` files.
- Vendored Yoga source and browser distribution files under
  `src/pythonnative/native/yoga/` and `src/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:

```bash
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:

```
(): 

[optional body]

[optional footer(s)]
```

### Commit message character set

- Encoding: UTF8 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 only
- `feat`: userfacing feature or capability
- `fix`: bug fix
- `perf`: performance improvements
- `refactor`: code change that neither fixes a bug nor adds a feature
- `revert`: revert of a previous commit
- `style`: 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 and `pn` command (`src/pythonnative/cli/`)
  - `component`: the `@component` decorator, `Component`, and `memo` (`component.py`)
  - `components`: declarative element-creating functions and their shared props (`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`)
  - `handles`: typed imperative handles published on refs (`handles.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`)
  - `journal`: render undo journal (`journal.py`)
  - `layout`: Yoga layout integration and the host binding (`layout.py`, `native/yoga/`)
  - `lint`: the `pn lint` rules-of-hooks checker (`lint.py`)
  - `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 backend accessor and bridge backend (`native_views/`)
  - `navigation`: navigation state, container, navigators, screens, hooks, linking, and ref (`navigation/`)
  - `net`: awaitable HTTP client (`net.py`)
  - `package`: `src/pythonnative/__init__.py` exports and package boundary
  - `platform`: `Platform.OS`/`Platform.select` and version detection (`platform.py`)
  - `platform_metrics`: platform-reported metrics like safe-area insets and bar heights (`platform_metrics.py`)
  - `preview`: `pn start` / `pn preview` session and the browser preview page (`preview.py`, `devserver/static/`)
  - `project`: `pythonnative.toml` config, 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 and `use_persisted_state` (`storage.py`)
  - `store`: the `Store` app-state container and `use_store` (`store.py`)
  - `scheduler`: render batching and transition queues (`scheduler.py`)
  - `style`: the `Style` type, style resolution, and `StyleSheet` (`style.py`)
  - `testing`: public test utilities and the pytest plugin (`testing/`)
  - `theme`: `Theme`, `ThemeProvider`, `use_theme`, and `use_styles` (`theme.py`)
  - `utils`: shared utilities (`utils.py`)

- Other scopes:
  - `deps`: dependency updates and version pins
  - `examples`: example apps under `examples/`
  - `mkdocs`: documentation site (MkDocs/Material) configuration and content under `docs/`
  - `pyproject`: `pyproject.toml` packaging/build metadata
  - `repo`: repository metadata and toplevel files (`README.md`, `CONTRIBUTING.md`, `.gitignore`, licenses)
  - `scripts`: developer scripts under `scripts/` (e.g., `check.sh`)
  - `templates`: Android/iOS project templates under `src/pythonnative/templates/`
  - `kit`: the Swift rendering core (`native/ios/`)
  - `runtime-android`: the Kotlin rendering core (`native/android/`)
  - `tests`: unit/integration/E2E tests under `tests/`
  - `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 toplevel updates.

Examples:

```text
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):

```text
build: update packaging metadata
chore: update .gitignore patterns
docs: add project overview
```

Breaking changes:

- Use `!` after the type/scope or a `BREAKING CHANGE:` footer.

```text
feat(hosts)!: rename create_page to create_screen

BREAKING CHANGE: API renamed; update app code and templates.
```

### Multiple scopes (optional)

- Commaseparate 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 13 scopes max.

Example:

```text
feat(templates,cli): add ios template and wire pn init
```

## 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/Followups.
  - 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:

```text
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: 
Co-authored-by: Name 
```

## Pull request checklist

- PR title: Conventional Commits format (CI-enforced by `pr-lint.yml`).
- Tests: added/updated; `uv run pytest` passes.
- Lint/format: `uv run ruff check .` and `uv run black --check src examples tests` pass.
- Docs: update `README.md` and the affected pages under `docs/` if behavior changes; `uv run --group docs mkdocs build --strict` must pass.
- 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 in `src/pythonnative/__init__.py` as `__version__`. Both files are updated automatically by [python-semantic-release](https://python-semantic-release.readthedocs.io/).
- **Automated release pipeline** (on every merge to `main`):
  1. `python-semantic-release` scans Conventional Commits, updates the version files and `uv.lock`, generates `CHANGELOG.md`, and pushes the release commit and tag.
  2. A published GitHub release is created with generated release notes.
  3. The shared `Distributions` workflow builds a source archive from that exact commit, then uses [cibuildwheel](https://cibuildwheel.pypa.io/) to build all wheels from the archive.
  4. 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.
  5. 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), `fix` and `perf` (patch), and `BREAKING CHANGE` (major, or minor before 1.0). Other types, including `build` and `ci`, don't trigger a release on their own. Use a `build` or `ci` title 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:

```bash
gh workflow run release.yml --ref main -f tag=v0.40.0
```

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:

```bash
gh workflow run wheels.yml --ref main -f source-ref=v0.40.0
```

### Branch naming (suggested)

- Use lowercase kebabcase; concise ( 40 chars).
- Branch prefixes match Conventional Commit types:
  - `feat/-`
  - `fix/-`
  - `chore/`
  - `docs/`
  - `ci/`
  - `refactor/-`
  - `test/`
  - `perf/`
  - `build/`

Examples:

```text
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](https://maestro.dev/) 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//` exercises one symbol.

The dedicated `examples/hello-world` app is left in place as a small marketing demo; it's **not** the E2E target.

```bash
# 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:

```bash
# 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:

```bash
uv run ./scripts/run-e2e.sh android hooks
uv run ./scripts/run-e2e.sh ios navigation
```

Each category is one file under `tests/e2e/suites/` (`hooks.yaml`, `navigation.yaml`, `gestures.yaml`, and so on; large areas are split into lettered parts such as `components-a.yaml`). List the directory for the current set:

```bash
ls tests/e2e/suites
```

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:

1. Add a demo screen under `examples/e2e-suite/app/screens//`.
2. Append a `DemoEntry` in `examples/e2e-suite/app/registry.py`.
3. Add a Maestro flow at `tests/e2e/flows//.yaml`.
4. Append the flow to the top-level `tests/e2e/android.yaml`, `tests/e2e/ios.yaml`, and the matching `tests/e2e/suites/.yaml` (or the lettered part it belongs to).
5. Confirm `uv run python scripts/check-e2e-coverage.py` exits 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`): builds `examples/e2e-suite` on Android (Linux emulator) and iOS (macOS simulator), then runs Maestro flows by category. Triggers on pushes to `main`, PRs, and manual dispatch.
- **Packages** (`packages.yml`): resolves the PyPI compatibility matrix in `tests/packages/matrix.toml` against the live indexes with `scripts/package-matrix.py --check`, weekly and on changes to the resolver or manifest, and uploads the rendered Markdown table for `docs/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 to `main`; computes version, generates changelog, tags, creates GitHub Release, and (when `DRAFT_RELEASE` is `"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 to `main`.

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

Web Proxy Viewer  |  New URL  |  Original Page