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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions .github/workflows/stubs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
name: Kotlin-name stubs

# Issue #32: pythonx-compose consumes the binder's Kotlin-name `.pyi` stubs for the real Compose jars
# but cannot run this repository's Gradle build. This job runs the stub task over the Compose version the
# build resolves (`compose-plugin` in gradle/libs.versions.toml) and publishes the result.
#
# - every push to develop and every pull request: the stubs are generated, type-checked against
# tools/stubs/consumer.py with mypy (issue #31), and uploaded as the workflow artifact `kotlin-stubs`;
# - every `v*` tag: the same stubs are also attached to the tag's GitHub release as `kotlin-stubs.zip`.

on:
push:
branches: [ "develop" ]
tags: [ "v*" ]
pull_request:
branches: [ "develop" ]
paths:
- "python-multiplatform-gradle-plugin/**"
- "ksp-fixtures/compose/**"
- "tools/stubs/**"
- "gradle/libs.versions.toml"
- ".github/workflows/stubs.yml"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: stubs-${{ github.ref }}
cancel-in-progress: true

jobs:
stubs:
name: Generate stubs
# ubuntu, not macos: two Kotlin names can differ only by case (`graphics.Shadow` the class module and
# `graphics.shadow` the package), which are two paths here and one on a case-insensitive filesystem.
runs-on: ubuntu-latest
permissions:
contents: write # attaching the stubs to a tag's release
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Java
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'zulu'
cache: 'gradle'

- name: Setup Python (for mypy)
uses: actions/setup-python@v5
with:
python-version: '3.13'

- name: Generate the stubs over the Compose jars
run: ./gradlew :ksp-fixtures:compose:generatePythonStubs --console=plain

- name: Type-check a consumer against them (mypy)
run: tools/stubs/check-stubs.sh ksp-fixtures/compose/build/generated/pythonStubs/desktopMain

- name: Assemble the artifact
run: |
set -euo pipefail
compose_version="$(sed -nE 's/^compose-plugin = "([^"]+)".*/\1/p' gradle/libs.versions.toml)"
test -n "$compose_version"
out=build/kotlin-stubs
rm -rf "$out"
mkdir -p "$out"
cp -R ksp-fixtures/compose/build/generated/pythonStubs/desktopMain/. "$out/"
cat > "$out/README.md" <<README
# Kotlin-name stubs for Compose ${compose_version}

\`.pyi\` stubs for the Kotlin declarations the python-multiplatform binder serves, under the
**Kotlin** module paths and Kotlin names (\`androidx.compose.material3.Checkbox\`, parameters by
their Kotlin names). Nothing here is renamed or Pythonic; that is pythonx-compose's layer.

| | |
|---|---|
| Compose | ${compose_version} (desktop jars) |
| python-multiplatform commit | ${GITHUB_SHA} |
| ref | ${GITHUB_REF} |
| workflow run | ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID} |
| generator | \`:ksp-fixtures:compose:generatePythonStubs\` |

Packages stubbed: the \`artifactIncludePackages\` of \`ksp-fixtures/compose/build.gradle.kts\` at
that commit.

Use: put this directory on the type checker's path (\`MYPYPATH=<this directory>\`, or the
IDE's "stub path" / interpreter paths). It is checked with mypy against
\`tools/stubs/consumer.py\` in the commit above.
README
(cd build && zip -qr kotlin-stubs.zip kotlin-stubs)

- name: Upload the stubs
uses: actions/upload-artifact@v4
with:
name: kotlin-stubs
path: build/kotlin-stubs
if-no-files-found: error

- name: Attach the stubs to the tag's release
if: startsWith(github.ref, 'refs/tags/')
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1 \
|| gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --notes "Kotlin-name stubs for the Compose version this tag builds against."
gh release upload "$GITHUB_REF_NAME" build/kotlin-stubs.zip --clobber
20 changes: 16 additions & 4 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,22 @@ All in `PM/commonTest`, so they run wherever the interpreter loads.
`CallbackDrivenRenderTest.kt`, pointer/drag render tests), `GP/artifact/ComposableBindingTest.kt`;
`planned` on Android, iOS and wasm.
- **B-7** The plugin generates `.pyi` stubs for the Kotlin-named modules only, under Kotlin names
(keyword parameters by Kotlin name, `= ...` for a Kotlin default, receiver positional-only). It emits
nothing under `pythonx` and renames nothing; a Pythonic stub product belongs to pythonx-compose.
`Status: partial` — `GP/stubs/PyiRenderingTest.kt`, `GP/stubs/KotlinNamesOnlyStubTest.kt`,
`ksp-fixtures/artifact/.../WalkedArtifactStubTest.kt`; handle-returning stubs are not wrapped.
(keyword parameters by Kotlin name, `= ...` for a Kotlin default, receiver positional-only). Types are
the declared Kotlin types: one stub class per bound type in the module of its own package, `Dp | float`
for a value class bound as its primitive, `Callable[...]` for a function type, `| None` for a nullable;
an extension function is also a callable attribute of its receiver's class; an overload set is
`@overload`ed under its base name in table-key order; a required parameter after a defaulted one is
keyword-only like `inspect.signature`. It emits nothing under `pythonx` and renames nothing; a
Pythonic stub product belongs to pythonx-compose. `Status: partial` — `GP/stubs/PyiRenderingTest.kt`,
`GP/stubs/TypedStubTest.kt`, `GP/stubs/KotlinNamesOnlyStubTest.kt`,
`ksp-fixtures/artifact/.../WalkedArtifactStubTest.kt`, `.../StubSignatureAgreesWithRuntimeTest.kt`,
`tools/stubs/check-stubs.sh` (mypy over the Compose stubs). A class whose name is also a function in
its module is `Any`.
- **B-8** CI generates the stubs over the Compose version the build resolves and publishes them
(`.github/workflows/stubs.yml`): workflow artifact `kotlin-stubs` on every push to `develop`, with a
README naming the Compose version and the commit, and `kotlin-stubs.zip` on every `v*` tag's release.
`Status: partial` — the workflow could not be run where it was written; its YAML parses and its
assemble step was executed locally.

## 7. Threading and builds

Expand Down
7 changes: 4 additions & 3 deletions docs/design/kotlin-extensions-in-python.md
Original file line number Diff line number Diff line change
Expand Up @@ -598,9 +598,10 @@ Generation belongs to the Gradle plugin (`AGENTS.md` §12.5), and it reads the s
the binder, so the stub and the binding cannot drift. **What it emits is the Kotlin surface, not a
Pythonic one** (SPEC B-7, `docs/design/pyi-generation-design.md`): one `def` per table key under the
Kotlin module path, Kotlin parameter names, `= ...` for a Kotlin default, the extension receiver as a
positional-only first parameter, and boundary-type annotations (`Dp` → `float`, `Modifier` → `int`)
with the Kotlin signature in the docstring. Extension-as-method, `@overload` sets over a base name and
`Dp | float` are what a Pythonic stub would say; that stub belongs to pythonx-compose. The section
positional-only first parameter, the declared Kotlin types as stub classes (`Modifier`, `Dp | float`,
`Callable[...]`), each extension also as a callable attribute of its receiver's class, and `@overload`
sets over a base name in table-key order, with the Kotlin signature in the docstring (issue #31). What
stays out is snake_case, `pythonx` and the Pythonic allowlist narrowing, which belong to pythonx-compose. The section
this replaces — the metaclass stub and the three measured requirements for a Pythonic stub — is in
[`docs/archive/kotlin-extensions-in-python-pyi.md`](../archive/kotlin-extensions-in-python-pyi.md).

Expand Down
Loading
Loading