From 430410624301c8367e6d236d2072678beaa59423 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:14:23 +0900 Subject: [PATCH 1/3] Docs: A stub class lists every Kotlin supertype with a stub class (SPEC B-7, #71) --- docs/SPEC.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/SPEC.md b/docs/SPEC.md index 6ea3e21b3..26a945cf1 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -269,6 +269,11 @@ All in `PM/commonTest`, so they run wherever the interpreter loads. declared type even when that type is nested in the object (`Alignment.End: Horizontal`, a class of the object's own module; elsewhere `androidx.compose.ui.Alignment.Horizontal`); the object's own type stays `Any` (#53, `GP/stubs/ObjectStubTest.kt`). + A stub class lists **every** Kotlin supertype that has a stub class as a base, not only the first + (`Arrangement.HorizontalOrVertical(Horizontal, Vertical)`, so a `Vertical` slot accepts it): `Any` + and `java.lang.*` are dropped, and a base already implied by another listed base's known ancestry + is dropped, so the order is a valid Python MRO (#71, `GP/stubs/SupertypeStubTest.kt`, and the + `Column`/`Row` `SpaceBetween` lines of `tools/stubs/consumer.py`). - **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. From 6a90ed15ef9ad7a12ebf2b363ac677035731c9c7 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:14:23 +0900 Subject: [PATCH 2/3] Test: Pin multiple stub bases; a Vertical slot takes Arrangement.SpaceBetween (#71) check-stubs.sh on run 37098191598's stubs: consumer.py:43 Argument "verticalArrangement" to "Column" has incompatible type "HorizontalOrVertical"; expected "Vertical". --- .../gradle/stubs/SupertypeStubTest.kt | 57 +++++++++++++++++++ tools/stubs/consumer.py | 5 +- 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/SupertypeStubTest.kt diff --git a/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/SupertypeStubTest.kt b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/SupertypeStubTest.kt new file mode 100644 index 000000000..910c7b743 --- /dev/null +++ b/python-multiplatform-gradle-plugin/src/test/kotlin/python/multiplatform/gradle/stubs/SupertypeStubTest.kt @@ -0,0 +1,57 @@ +package python.multiplatform.gradle.stubs + +import python.multiplatform.gradle.model.DeclarationModel +import python.multiplatform.gradle.model.KotlinTypeModel +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +/** + * Issue #71: a stub class lists every Kotlin supertype that has a stub class, in a valid MRO. + * `Arrangement.HorizontalOrVertical` is both a `Horizontal` and a `Vertical`; listing only the first + * made mypy reject it in a `Vertical` slot. + */ +class SupertypeStubTest { + + private val pkg = "androidx.compose.foundation.layout" + private val arrangement = "$pkg.Arrangement" + + private fun producing(binding: String, type: String, supertypes: List) = DeclarationModel( + simpleName = binding.substringAfterLast('.'), + owner = binding.substringBeforeLast('.'), + ownerIsClass = true, + receiver = null, + parameters = emptyList(), + returnType = KotlinTypeModel(type), + returnBoundaryTag = "OBJECT", + bindingName = binding, + returnSupertypes = supertypes, + ) + + private fun text(vararg d: DeclarationModel, module: String = arrangement) = + renderKotlinFqnStubs(d.toList()).getValue(module.replace('.', '/') + "/__init__.pyi") + + @Test + fun aClassListsEveryKotlinSupertypeThatHasAStubClass() { + val both = producing( + "$arrangement.SpaceBetween", "$arrangement.HorizontalOrVertical", + listOf("$arrangement.Horizontal", "$arrangement.Vertical", "kotlin.Any", "java.lang.Object"), + ) + val body = text(both) + assertTrue("class HorizontalOrVertical(Horizontal, Vertical):" in body, body) + assertTrue("class Horizontal:" in body, body) + assertTrue("class Vertical:" in body, body) + assertFalse("object" in body.substringAfter("class HorizontalOrVertical").substringBefore(":"), body) + } + + @Test + fun aBaseAnotherListedBaseAlreadyImpliesIsDroppedSoTheMroIsValid() { + // Sub is a Base and a Mid, and Mid is a Base: `class Sub(Base, Mid)` is an MRO error in Python. + val sub = producing("$arrangement.makeSub", "$arrangement.Sub", listOf("$arrangement.Base", "$arrangement.Mid")) + val mid = producing("$arrangement.makeMid", "$arrangement.Mid", listOf("$arrangement.Base")) + val body = text(sub, mid) + assertTrue("class Sub(Mid):" in body, body) + assertTrue("class Mid(Base):" in body, body) + assertFalse("class Sub(Base" in body, body) + } +} diff --git a/tools/stubs/consumer.py b/tools/stubs/consumer.py index c2d94cb62..dc28db372 100644 --- a/tools/stubs/consumer.py +++ b/tools/stubs/consumer.py @@ -7,7 +7,7 @@ and mypy runs with `warn_unused_ignores`, so a stub that stopped rejecting one fails the check too. """ -from androidx.compose.foundation.layout import Column, Row, fillMaxWidth, padding__Dp +from androidx.compose.foundation.layout import Arrangement, Column, Row, fillMaxWidth, padding__Dp from androidx.compose.material3 import Checkbox, Text from androidx.compose.ui import Modifier @@ -39,6 +39,9 @@ def content() -> None: Column(modifier=chain, content=lambda scope: None) Row(content=lambda scope: None) +# `SpaceBetween` is a `HorizontalOrVertical`: a `Vertical` slot and a `Horizontal` slot both take it (#71). +Column(verticalArrangement=Arrangement.SpaceBetween, content=lambda scope: None) +Row(horizontalArrangement=Arrangement.SpaceBetween, content=lambda scope: None) # Rejected on purpose. (`padding("sixteen")` is not among them: one `padding` overload takes a # `PaddingValues`, a type the stubs cannot name -- `PaddingValues(...)` the factory owns the name -- so From 8882509aebd73573c93d2a48b5c25027f113d5b1 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:14:23 +0900 Subject: [PATCH 3/3] Fix: Render every Kotlin supertype that has a stub class as a base (#71) --- .../gradle/stubs/PyiRendering.kt | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/stubs/PyiRendering.kt b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/stubs/PyiRendering.kt index d17f6aa47..90920a16d 100644 --- a/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/stubs/PyiRendering.kt +++ b/python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/stubs/PyiRendering.kt @@ -455,6 +455,24 @@ private class StubRenderer(declarations: List) { .mapNotNull { d -> methodReceiver(d)?.let { it to d } } .groupBy({ it.first }, { it.second }) + /** + * Issue #71: every Kotlin supertype that has a stub class is a base, not just the first one + * (`Arrangement.HorizontalOrVertical` is both a `Horizontal` and a `Vertical`). The ancestry is + * nearest-first and transitive, so a base is dropped when another candidate's own known ancestry + * already contains it (Python would reject `class C(A, B)` with B a subclass of A as an + * inconsistent MRO, and the repeat adds nothing); what remains keeps the ancestry's order. + * `kotlin.Any` and `java.lang.*` are `object`, which every Python class already has. + */ + private fun stubBasesOf(qualifiedName: String, ctx: ModuleOut): List { + val candidates = supertypes[qualifiedName].orEmpty() + .filter { it != qualifiedName && it != "kotlin.Any" && !it.startsWith("java.lang.") && classRefOf(it) != null } + .distinct() + val kept = candidates.filter { base -> + candidates.none { other -> other != base && base in supertypes[other].orEmpty() && other !in supertypes[base].orEmpty() } + } + return kept.map { classReference(it, null, ctx) }.filter { it != ANY }.distinct() + } + private fun renderClass(qualifiedName: String) { val ref = resolveRef(qualifiedName) ?: return val ctx = out(ref.pkg) @@ -467,12 +485,7 @@ private class StubRenderer(declarations: List) { } else { "Kotlin: $qualifiedName" } - supertypes[qualifiedName] - ?.firstOrNull { it != qualifiedName && it != "kotlin.Any" && !it.startsWith("java.lang.") && classRefOf(it) != null } - ?.let { base -> - val reference = classReference(base, null, ctx) - if (reference != ANY) node.bases = reference - } + stubBasesOf(qualifiedName, ctx).takeIf { it.isNotEmpty() }?.let { node.bases = it.joinToString(", ") } val attributes = sortedMapOf>() extensionsByReceiver[qualifiedName].orEmpty().sortedBy { it.bindingName }.forEach { d ->