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
5 changes: 5 additions & 0 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,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`). An extension property whose receiver is a type nested in such an object is a `@property` of that nested class in the object's module (`Icons.Default: Filled`, `Filled.Add: ImageVector`, #68), not a marker comment in the package module.
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,24 @@ private class StubRenderer(declarations: List<DeclarationModel>) {
.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<String> {
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)
Expand All @@ -518,12 +536,7 @@ private class StubRenderer(declarations: List<DeclarationModel>) {
} 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(", ") }

constructorsByClass[qualifiedName]?.let { group ->
group.forEachIndexed { index, d ->
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>) = 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)
}
}
5 changes: 4 additions & 1 deletion tools/stubs/consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from typing import assert_type

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.material.icons import Icons
from androidx.compose.material3 import Checkbox, Icon, Text
from androidx.compose.ui import Modifier
Expand Down Expand Up @@ -43,6 +43,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)

# An extension property on a type nested in an object (issue #68): `Icons.Default` is `Icons.Filled`.
# `assert_type`, not an annotation: an `Any` (the stub before #53/#67) would satisfy an annotation.
Expand Down
Loading