Skip to content
This repository was archived by the owner on Jan 19, 2025. It is now read-only.

Commit 4672a46

Browse files
authored
feat: support variadic parameters (#813)
* feat(gui): default documentation parser if nothing else matches * test(parser): test creation of parameter list * feat(parser): include variadic parameters in parameter list * refactor(parser): remove old code * test(parser): add another test * fix(parser): get tests running again * fix(parser): get tests passing again * fix(parser): remove prints * chore(data): recreate API data * feat(gui): handle variadic parameters * feat(gui): update parameter assignment filter * feat(gui): show stars on names of variadic parameters * feat(gui): wrap buttons to the right of declaration name in selection view * feat(gui): hide report buttons on declarations that cannot be annotated * fix(parser): don't create annotations for variadic parameters * test(backend): failing tests * fix(backend): get one test going * fix(backend): get two more tests going * feat(backend): handle variadic parameters * test(backend): more failing tests * feat(backend): get these tests working as well * fix(backend): constructors did not have implicit self parameter * test(parser): more tests * style(parser): fix lint errors (no Python 3.10 for now) * style: apply automatic fixes of linters Co-authored-by: lars-reimann <[email protected]>
1 parent 45f5e5b commit 4672a46

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+22829
-8643
lines changed

api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import com.larsreimann.api_editor.model.ComparisonOperator.LESS_THAN
55
import com.larsreimann.api_editor.model.ComparisonOperator.LESS_THAN_OR_EQUALS
66
import com.larsreimann.api_editor.model.ComparisonOperator.UNRESTRICTED
77
import com.larsreimann.api_editor.model.PythonParameterAssignment.IMPLICIT
8+
import com.larsreimann.api_editor.model.PythonParameterAssignment.NAMED_VARARG
89
import com.larsreimann.api_editor.model.PythonParameterAssignment.NAME_ONLY
10+
import com.larsreimann.api_editor.model.PythonParameterAssignment.POSITIONAL_VARARG
911
import com.larsreimann.api_editor.model.PythonParameterAssignment.POSITION_ONLY
1012
import com.larsreimann.api_editor.model.PythonParameterAssignment.POSITION_OR_NAME
1113
import com.larsreimann.api_editor.mutable_model.PythonArgument
@@ -23,9 +25,11 @@ import com.larsreimann.api_editor.mutable_model.PythonFunction
2325
import com.larsreimann.api_editor.mutable_model.PythonInt
2426
import com.larsreimann.api_editor.mutable_model.PythonMemberAccess
2527
import com.larsreimann.api_editor.mutable_model.PythonModule
28+
import com.larsreimann.api_editor.mutable_model.PythonNamedSpread
2629
import com.larsreimann.api_editor.mutable_model.PythonNamedType
2730
import com.larsreimann.api_editor.mutable_model.PythonNone
2831
import com.larsreimann.api_editor.mutable_model.PythonParameter
32+
import com.larsreimann.api_editor.mutable_model.PythonPositionalSpread
2933
import com.larsreimann.api_editor.mutable_model.PythonReference
3034
import com.larsreimann.api_editor.mutable_model.PythonString
3135
import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression
@@ -262,23 +266,33 @@ fun List<PythonParameter>.toPythonCode(): String {
262266
val positionOrNameParametersString = assignedByToParameter[POSITION_OR_NAME]
263267
?.joinToString { it.toPythonCode() }
264268
?: ""
269+
val positionalVarargParametersString = assignedByToParameter[POSITIONAL_VARARG]
270+
?.joinToString { it.toPythonCode() }
271+
?: ""
265272
var nameOnlyParametersString = assignedByToParameter[NAME_ONLY]
266273
?.joinToString { it.toPythonCode() }
267274
?: ""
275+
val namedVarargsParametersString = assignedByToParameter[NAMED_VARARG]
276+
?.joinToString { it.toPythonCode() }
277+
?: ""
268278

269279
if (positionOnlyParametersString.isNotBlank()) {
270280
positionOnlyParametersString = "$positionOnlyParametersString, /"
271281
}
272282

273-
if (nameOnlyParametersString.isNotBlank()) {
283+
// If there is already a positional vararg parameter, the star must not be added since the parameter already acts as
284+
// the boundary.
285+
if (positionalVarargParametersString.isBlank() && nameOnlyParametersString.isNotBlank()) {
274286
nameOnlyParametersString = "*, $nameOnlyParametersString"
275287
}
276288

277289
val parameterStrings = listOf(
278290
implicitParametersString,
279291
positionOnlyParametersString,
280292
positionOrNameParametersString,
281-
nameOnlyParametersString
293+
positionalVarargParametersString,
294+
nameOnlyParametersString,
295+
namedVarargsParametersString
282296
)
283297

284298
return parameterStrings
@@ -289,6 +303,11 @@ fun List<PythonParameter>.toPythonCode(): String {
289303
fun PythonParameter.toPythonCode() = buildString {
290304
val typeStringOrNull = type.toPythonCodeOrNull()
291305

306+
if (assignedBy == POSITIONAL_VARARG) {
307+
append("*")
308+
} else if (assignedBy == NAMED_VARARG) {
309+
append("**")
310+
}
292311
append(name)
293312
if (typeStringOrNull != null) {
294313
append(": $typeStringOrNull")
@@ -309,7 +328,9 @@ fun PythonExpression.toPythonCode(): String {
309328
is PythonFloat -> value.toString()
310329
is PythonInt -> value.toString()
311330
is PythonMemberAccess -> "${receiver!!.toPythonCode()}.${member!!.toPythonCode()}"
331+
is PythonNamedSpread -> "**${argument!!.toPythonCode()}"
312332
is PythonNone -> "None"
333+
is PythonPositionalSpread -> "*${argument!!.toPythonCode()}"
313334
is PythonReference -> declaration!!.name
314335
is PythonString -> "'$value'"
315336
is PythonStringifiedExpression -> string

api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/model/packageData.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,9 @@ enum class PythonParameterAssignment {
206206
IMPLICIT,
207207
POSITION_ONLY,
208208
POSITION_OR_NAME,
209+
POSITIONAL_VARARG,
209210
NAME_ONLY,
211+
NAMED_VARARG,
210212
}
211213

212214
@Serializable

api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import com.larsreimann.api_editor.model.EditorAnnotation
77
import com.larsreimann.api_editor.model.PythonFromImport
88
import com.larsreimann.api_editor.model.PythonImport
99
import com.larsreimann.api_editor.model.PythonParameterAssignment
10+
import com.larsreimann.api_editor.model.PythonParameterAssignment.NAMED_VARARG
11+
import com.larsreimann.api_editor.model.PythonParameterAssignment.POSITIONAL_VARARG
1012
import com.larsreimann.modeling.ModelNode
1113
import com.larsreimann.modeling.ancestorsOrSelf
1214

@@ -204,6 +206,8 @@ class PythonParameter(
204206
fun isRequired() = defaultValue == null
205207

206208
fun isOptional() = defaultValue != null
209+
210+
fun isVariadic() = assignedBy == POSITIONAL_VARARG || assignedBy == NAMED_VARARG
207211
}
208212

209213
class PythonResult(
@@ -265,6 +269,14 @@ class PythonMemberAccess(
265269
}
266270
}
267271

272+
class PythonNamedSpread(argument: PythonExpression) : PythonExpression() {
273+
var argument by ContainmentReference(argument)
274+
}
275+
276+
class PythonPositionalSpread(argument: PythonExpression) : PythonExpression() {
277+
var argument by ContainmentReference(argument)
278+
}
279+
268280
class PythonReference(declaration: PythonDeclaration) : PythonExpression() {
269281
var declaration by CrossReference(declaration)
270282
}

api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.larsreimann.api_editor.transformation
22

33
import com.larsreimann.api_editor.model.PythonParameterAssignment
4+
import com.larsreimann.api_editor.model.PythonParameterAssignment.IMPLICIT
45
import com.larsreimann.api_editor.mutable_model.PythonAttribute
56
import com.larsreimann.api_editor.mutable_model.PythonCall
67
import com.larsreimann.api_editor.mutable_model.PythonClass
@@ -41,14 +42,16 @@ fun PythonPackage.reorderParameters() {
4142

4243
private fun ModelNode.MutableContainmentList<PythonParameter>.reorderParameters() {
4344
val groups = this.groupBy { it.assignedBy }
44-
this.addAll(groups[PythonParameterAssignment.IMPLICIT].orEmpty())
45+
this.addAll(groups[IMPLICIT].orEmpty())
4546
this.addAll(groups[PythonParameterAssignment.POSITION_ONLY].orEmpty())
4647
this.addAll(groups[PythonParameterAssignment.POSITION_OR_NAME].orEmpty())
48+
this.addAll(groups[PythonParameterAssignment.POSITIONAL_VARARG].orEmpty())
4749
this.addAll(groups[PythonParameterAssignment.NAME_ONLY].orEmpty())
50+
this.addAll(groups[PythonParameterAssignment.NAMED_VARARG].orEmpty())
4851
}
4952

5053
/**
51-
* Converts `__init__` methods to constructors or adds a constructor without parameters if none exists.
54+
* Converts `__init__` methods to constructors or adds a constructor without explicit parameters if none exists.
5255
*/
5356
fun PythonPackage.extractConstructors() {
5457
this.descendants { it is PythonFunction }
@@ -62,19 +65,26 @@ private fun PythonClass.createConstructor() {
6265
null -> {
6366
if (this.originalClass != null) {
6467
this.constructor = PythonConstructor(
65-
parameters = emptyList(),
68+
parameters = listOf(
69+
PythonParameter(
70+
name = "self",
71+
assignedBy = IMPLICIT
72+
)
73+
),
6674
callToOriginalAPI = PythonCall(
6775
receiver = PythonStringifiedExpression(this.originalClass!!.qualifiedName)
6876
)
6977
)
7078
}
7179
}
80+
7281
else -> {
7382
constructorMethod.callToOriginalAPI?.let { callToOriginalAPI ->
7483
val newReceiver = when (val receiver = callToOriginalAPI.receiver) {
7584
is PythonStringifiedExpression -> PythonStringifiedExpression(
7685
receiver.string.removeSuffix(".__init__")
7786
)
87+
7888
null -> throw IllegalStateException("Receiver of call is null: $callToOriginalAPI")
7989
else -> receiver
8090
}
@@ -105,7 +115,7 @@ fun PythonPackage.createAttributesForParametersOfConstructor() {
105115
private fun PythonClass.createAttributesForParametersOfConstructor() {
106116
this.constructor
107117
?.parameters
108-
?.filter { it.assignedBy != PythonParameterAssignment.IMPLICIT }
118+
?.filter { it.assignedBy != IMPLICIT && !it.isVariadic() }
109119
?.forEach {
110120
this.attributes += PythonAttribute(
111121
name = it.name,

api-editor/backend/src/main/kotlin/com/larsreimann/api_editor/transformation/Preprocessor.kt

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import com.larsreimann.api_editor.mutable_model.PythonAttribute
77
import com.larsreimann.api_editor.mutable_model.PythonCall
88
import com.larsreimann.api_editor.mutable_model.PythonClass
99
import com.larsreimann.api_editor.mutable_model.PythonFunction
10+
import com.larsreimann.api_editor.mutable_model.PythonNamedSpread
1011
import com.larsreimann.api_editor.mutable_model.PythonPackage
1112
import com.larsreimann.api_editor.mutable_model.PythonParameter
13+
import com.larsreimann.api_editor.mutable_model.PythonPositionalSpread
1214
import com.larsreimann.api_editor.mutable_model.PythonReference
1315
import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression
1416
import com.larsreimann.modeling.closest
@@ -54,17 +56,17 @@ fun PythonPackage.addOriginalDeclarations() {
5456
this.descendants()
5557
.forEach {
5658
when (it) {
57-
is PythonClass -> it.addOriginalDeclarations()
58-
is PythonFunction -> it.addOriginalDeclarations()
59+
is PythonClass -> it.addOriginalDeclaration()
60+
is PythonFunction -> it.addOriginalDeclaration()
5961
}
6062
}
6163
}
6264

63-
private fun PythonClass.addOriginalDeclarations() {
65+
private fun PythonClass.addOriginalDeclaration() {
6466
this.originalClass = OriginalPythonClass(this.qualifiedName())
6567
}
6668

67-
private fun PythonFunction.addOriginalDeclarations() {
69+
private fun PythonFunction.addOriginalDeclaration() {
6870
val containingClass = closest<PythonClass>()
6971
this.callToOriginalAPI = PythonCall(
7072
receiver = PythonStringifiedExpression(
@@ -82,7 +84,11 @@ private fun PythonFunction.addOriginalDeclarations() {
8284
PythonParameterAssignment.NAME_ONLY -> it.name
8385
else -> null
8486
},
85-
value = PythonReference(it)
87+
value = when (it.assignedBy) {
88+
PythonParameterAssignment.POSITIONAL_VARARG -> PythonPositionalSpread(PythonReference(it))
89+
PythonParameterAssignment.NAMED_VARARG -> PythonNamedSpread(PythonReference(it))
90+
else -> PythonReference(it)
91+
}
8692
)
8793
}
8894
)
@@ -135,6 +141,10 @@ fun PythonPackage.updateParameterAssignment() {
135141
}
136142

137143
private fun PythonParameter.updateParameterAssignment() {
144+
if (isVariadic()) {
145+
return
146+
}
147+
138148
this.assignedBy = when {
139149
this.isImplicit() -> PythonParameterAssignment.IMPLICIT
140150
this.isRequired() -> PythonParameterAssignment.POSITION_OR_NAME

api-editor/backend/src/test/kotlin/com/larsreimann/api_editor/codegen/MethodPythonCodeGeneratorFullPipelineTest.kt

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ class MethodPythonCodeGeneratorFullPipelineTest {
9898
|from __future__ import annotations
9999
|
100100
|class testClass:
101-
| def __init__():
101+
| def __init__(self):
102102
| self.instance = testModule.testClass()
103103
|
104104
| def testMethod(self, testGroup: TestGroup, testParameter3):
@@ -154,7 +154,7 @@ class MethodPythonCodeGeneratorFullPipelineTest {
154154
|from __future__ import annotations
155155
|
156156
|class testClass:
157-
| def __init__():
157+
| def __init__(self):
158158
| self.instance = testModule.testClass()
159159
|
160160
| def testMethod(self, testGroup: TestGroup, testParameter3):
@@ -209,7 +209,7 @@ class MethodPythonCodeGeneratorFullPipelineTest {
209209
|from __future__ import annotations
210210
|
211211
|class testClass:
212-
| def __init__():
212+
| def __init__(self):
213213
| self.instance = testModule.testClass()
214214
|
215215
| def testMethod(self, testParameter1, testGroup: TestGroup):
@@ -259,7 +259,7 @@ class MethodPythonCodeGeneratorFullPipelineTest {
259259
|from __future__ import annotations
260260
|
261261
|class testClass:
262-
| def __init__():
262+
| def __init__(self):
263263
| self.instance = testModule.testClass()
264264
|
265265
| def testMethod(self, testParameter2, testParameter3, *, testParameter1=0.5):
@@ -303,7 +303,7 @@ class MethodPythonCodeGeneratorFullPipelineTest {
303303
|from __future__ import annotations
304304
|
305305
|class testClass:
306-
| def __init__():
306+
| def __init__(self):
307307
| self.instance = testModule.testClass()
308308
|
309309
| def testMethod(self, testParameter1, testParameter2, testParameter3):
@@ -351,7 +351,7 @@ class MethodPythonCodeGeneratorFullPipelineTest {
351351
|from dataclasses import dataclass
352352
|
353353
|class testClass:
354-
| def __init__():
354+
| def __init__(self):
355355
| self.instance = testModule.testClass()
356356
|
357357
| def testMethod(self, testGroup: TestGroup, testParameter3):
@@ -415,7 +415,7 @@ class MethodPythonCodeGeneratorFullPipelineTest {
415415
|from dataclasses import dataclass
416416
|
417417
|class testClass:
418-
| def __init__():
418+
| def __init__(self):
419419
| self.instance = testModule.testClass()
420420
|
421421
| def testMethod(self, testGroup: TestGroup, testParameter3):
@@ -474,7 +474,7 @@ class MethodPythonCodeGeneratorFullPipelineTest {
474474
|from dataclasses import dataclass
475475
|
476476
|class testClass:
477-
| def __init__():
477+
| def __init__(self):
478478
| self.instance = testModule.testClass()
479479
|
480480
| def testMethod(self, testParameter1: TestEnum, testParameter2, testParameter3):
@@ -523,7 +523,7 @@ class MethodPythonCodeGeneratorFullPipelineTest {
523523
|from __future__ import annotations
524524
|
525525
|class testClass:
526-
| def __init__():
526+
| def __init__(self):
527527
| self.instance = testModule.testClass()
528528
|
529529
| def testMethod(self, testParameter1, testGroup: TestGroup):
@@ -565,7 +565,7 @@ class MethodPythonCodeGeneratorFullPipelineTest {
565565
|from __future__ import annotations
566566
|
567567
|class testClass:
568-
| def __init__():
568+
| def __init__(self):
569569
| self.instance = testModule.testClass()
570570
|
571571
| def testMethod(self, testParameter1, testGroup: TestGroup):
@@ -611,7 +611,7 @@ class MethodPythonCodeGeneratorFullPipelineTest {
611611
|from __future__ import annotations
612612
|
613613
|class testClass:
614-
| def __init__():
614+
| def __init__(self):
615615
| self.instance = testModule.testClass()
616616
|
617617
| def testMethod(self, testParameter1, testGroup: TestGroup):
@@ -651,7 +651,7 @@ class MethodPythonCodeGeneratorFullPipelineTest {
651651
|from __future__ import annotations
652652
|
653653
|class testClass:
654-
| def __init__():
654+
| def __init__(self):
655655
| self.instance = testModule.testClass()
656656
|
657657
| def testMethod(self, testGroup: TestGroup, testParameter3):

0 commit comments

Comments
 (0)