Skip to content

Commit bbd4cfc

Browse files
authored
Merge pull request #50 from thisisthepy/work/typedpython-stubs-wire
Gradle plugin: feed stubs to typedpythonStubs
2 parents 1dd507a + 88e05bd commit bbd4cfc

3 files changed

Lines changed: 104 additions & 0 deletions

File tree

‎docs/SPEC.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,11 @@ All in `PM/commonTest`, so they run wherever the interpreter loads.
292292
README naming the Compose version and the commit, and `kotlin-stubs.zip` on every `v*` tag's release.
293293
`Status: partial` — the workflow could not be run where it was written; its YAML parses and its
294294
assemble step was executed locally.
295+
- **B-9** When a configuration named `typedpythonStubs` exists (toolchain's plugin creates it), every
296+
stubs task's output directory is added to it, with the task dependency travelling with the files, in
297+
either plugin application order. Without that configuration nothing happens and none is created.
298+
`Status: implemented` — `GP/TypedPythonStubsWiringTest.kt` (a `ProjectBuilder` project; the toolchain
299+
side that consumes the configuration is toolchain#23 and is not exercised here).
295300

296301
## 7. Threading and builds
297302

‎python-multiplatform-gradle-plugin/src/main/kotlin/python/multiplatform/gradle/PythonBindingsPlugin.kt‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ private const val OPTION_EXCLUDE_PACKAGES = "python.multiplatform.excludePackage
2121
private const val ROLE_LIBRARY = "library"
2222
private const val ROLE_APP = "app"
2323

24+
/** Created by toolchain's plugin (toolchain#23); referenced by name only. */
25+
private const val TYPEDPYTHON_STUBS_CONFIGURATION = "typedpythonStubs"
26+
2427
/**
2528
* Which KSP configurations the processor belongs on.
2629
*
@@ -509,6 +512,20 @@ class PythonBindingsPlugin : Plugin<Project> {
509512
outputDirectory.set(project.layout.buildDirectory.dir("generated/pythonStubs/$sourceSetName"))
510513
}
511514
project.tasks.matching { it.name == "prepareKotlinIdeaImport" }.configureEach { dependsOn(task) }
515+
feedTypedPythonStubs(project, task)
516+
}
517+
518+
/**
519+
* Adds [task]'s output to toolchain's `typedpythonStubs` configuration, by name only -- there is
520+
* no compile-time dependency on toolchain. `matching` + `configureEach` covers a configuration
521+
* that already exists and one created later, so plugin application order does not matter; if it
522+
* never appears, nothing happens. The provider is `flatMap`ped from the task, so resolving the
523+
* configuration runs the task.
524+
*/
525+
private fun feedTypedPythonStubs(project: Project, task: org.gradle.api.tasks.TaskProvider<PythonStubsTask>) {
526+
project.configurations.matching { it.name == TYPEDPYTHON_STUBS_CONFIGURATION }.configureEach {
527+
project.dependencies.add(name, project.files(task.flatMap { it.outputDirectory }))
528+
}
512529
}
513530

514531
/**
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package python.multiplatform.gradle
2+
3+
import org.gradle.api.Project
4+
import org.gradle.testfixtures.ProjectBuilder
5+
import kotlin.test.Test
6+
import kotlin.test.assertEquals
7+
import kotlin.test.assertFalse
8+
import kotlin.test.assertNull
9+
import kotlin.test.assertTrue
10+
11+
/**
12+
* SPEC B-9 (python-multiplatform#27): the stubs task output reaches toolchain's `typedpythonStubs`
13+
* configuration by name, with the task dependency attached, whichever side appears first.
14+
*/
15+
class TypedPythonStubsWiringTest {
16+
17+
private val configurationName = "typedpythonStubs"
18+
19+
private fun projectWith(sourceSets: List<String>): Project {
20+
val project = ProjectBuilder.builder().build()
21+
val container = project.container(DummySourceSet::class.java) { name -> DummySourceSet(name, project) }
22+
sourceSets.forEach { container.create(it) }
23+
project.extensions.add("kotlin", DummyKotlinExt(container))
24+
sourceSets.forEach { project.configurations.create("${it}Classpath") }
25+
return project
26+
}
27+
28+
private fun wire(project: Project, sourceSets: List<String>) {
29+
val extension = project.extensions.create("pythonBindingsUnderTest", PythonBindingsExtension::class.java)
30+
extension.artifactIncludePackages.set(listOf("junit.runner"))
31+
extension.artifactTargets.set(sourceSets.associate { "${it}Classpath" to it })
32+
PythonBindingsPlugin().configureArtifactBindings(project, extension)
33+
}
34+
35+
private fun stubsDir(project: Project, sourceSet: String) =
36+
project.layout.buildDirectory.dir("generated/pythonStubs/$sourceSet").get().asFile
37+
38+
@Test
39+
fun aConfigurationThatExistsBeforeTheWiringReceivesEverySourceSetsStubs() {
40+
val sets = listOf("desktopMain", "androidNativeArm64Main")
41+
val project = projectWith(sets)
42+
project.configurations.create(configurationName)
43+
wire(project, sets)
44+
45+
val files = project.configurations.getByName(configurationName).files
46+
sets.forEach { assertTrue(stubsDir(project, it) in files, "$it missing from $files") }
47+
}
48+
49+
@Test
50+
fun aConfigurationCreatedAfterTheWiringReceivesTheStubsToo() {
51+
val sets = listOf("desktopMain", "androidNativeArm64Main")
52+
val project = projectWith(sets)
53+
wire(project, sets)
54+
project.configurations.create(configurationName)
55+
56+
val files = project.configurations.getByName(configurationName).files
57+
sets.forEach { assertTrue(stubsDir(project, it) in files, "$it missing from $files") }
58+
}
59+
60+
@Test
61+
fun resolvingTheConfigurationDependsOnTheStubsTask() {
62+
val project = projectWith(listOf("desktopMain"))
63+
val configuration = project.configurations.create(configurationName)
64+
wire(project, listOf("desktopMain"))
65+
66+
val deps = configuration.buildDependencies.getDependencies(null)
67+
val names = deps.map { it.name }
68+
assertTrue(
69+
names.any { it.startsWith("generatePythonStubs") },
70+
"the task must travel with the files; dependencies were $names",
71+
)
72+
}
73+
74+
@Test
75+
fun withoutTheConfigurationNothingIsCreatedAndNothingFails() {
76+
val project = projectWith(listOf("desktopMain"))
77+
wire(project, listOf("desktopMain"))
78+
79+
assertNull(project.configurations.findByName(configurationName))
80+
assertFalse(project.tasks.names.isEmpty())
81+
}
82+
}

0 commit comments

Comments
 (0)