From 156b533271f89628b71cecce01d0f7754d5c0225 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 10 Jan 2022 10:32:30 +0100 Subject: [PATCH 01/32] fix: various issues with Python codegen --- .../api_editor/codegen/PythonCodeGenerator.kt | 23 +++++++++---------- .../transformation/Postprocessor.kt | 12 ++++++---- .../validation/AnnotationValidator.kt | 2 +- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt index 5eb63a05c..24f61ad53 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt @@ -56,9 +56,12 @@ private fun buildNamespace(pythonModule: PythonModule): String { ) } pythonModule.classes.forEach { pythonClass: PythonClass -> - importedModules.add( - buildParentDeclarationName(pythonClass.originalClass!!.qualifiedName) - ) + if (pythonClass.originalClass != null) { + importedModules.add( + buildParentDeclarationName(pythonClass.originalClass!!.qualifiedName) + ) + } + } var result = importedModules.joinToString("\n") { "import $it" } if (pythonModule.enums.isNotEmpty()) { @@ -152,7 +155,10 @@ private fun buildConstructor(`class`: PythonClass) = buildString { } private fun PythonConstructor.buildConstructorCall(): String { - return "self.instance = ${callToOriginalAPI!!.toPythonCode()}" + return when (callToOriginalAPI) { + null -> "" + else -> "self.instance = ${callToOriginalAPI.toPythonCode()}" + } } /** @@ -238,16 +244,9 @@ private fun buildFunctionBody(pythonFunction: PythonFunction): String { formattedBoundaries = "$formattedBoundaries\n" } - if (!pythonFunction.isMethod() || pythonFunction.isStaticMethod()) { - return ( - formattedBoundaries + - pythonFunction.callToOriginalAPI!!.toPythonCode() - ) - } - return ( formattedBoundaries + - pythonFunction.callToOriginalAPI!!.toPythonCode() + "return " + pythonFunction.callToOriginalAPI!!.toPythonCode() ) } diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt index 454138859..423634c51 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt @@ -52,10 +52,12 @@ fun PythonPackage.extractConstructors() { private fun PythonClass.createConstructor() { when (val constructorMethod = this.methods.firstOrNull { it.name == "__init__" }) { null -> { - this.constructor = PythonConstructor( - parameters = emptyList(), - callToOriginalAPI = PythonCall(receiver = this.originalClass!!.qualifiedName) - ) + if (this.originalClass != null) { + this.constructor = PythonConstructor( + parameters = emptyList(), + callToOriginalAPI = PythonCall(receiver = this.originalClass!!.qualifiedName) + ) + } } else -> { this.constructor = PythonConstructor( @@ -87,7 +89,7 @@ private fun PythonClass.createAttributesForParametersOfConstructor() { ?.forEach { this.attributes += PythonAttribute( name = it.name, - value = it.defaultValue, + value = it.name, isPublic = true, typeInDocs = it.typeInDocs, description = it.description, diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt index f474158e9..958c04759 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt @@ -186,7 +186,7 @@ class AnnotationValidator(private val annotatedPythonPackage: SerializablePython this["CalledAfter"] = mutableSetOf("CalledAfter", "Group", "Move", "Rename") this["Constant"] = mutableSetOf() this["Enum"] = mutableSetOf("Attribute", "Group", "Optional", "Rename", "Required") - this["Group"] = mutableSetOf("CalledAfter", "Group", "Move", "Rename") + this["Group"] = mutableSetOf("Boundary", "CalledAfter", "Enum", "Group", "Move", "Optional", "Rename", "Required") this["Move"] = mutableSetOf("CalledAfter", "Group", "Rename") this["Optional"] = mutableSetOf("Boundary", "Enum", "Group", "Rename") this["Rename"] = mutableSetOf( From 7fc66bdf26e9fde762b2597a03f1610b2bdeae11 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 10 Jan 2022 11:38:52 +0100 Subject: [PATCH 02/32] test: additional tests for Python codegen --- .../api_editor/codegen/PythonCodeGenerator.kt | 49 ++++--- .../codegen/PythonCodeGeneratorTest.kt | 133 +++++++++++++++--- 2 files changed, 142 insertions(+), 40 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt index 24f61ad53..609f974ee 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt @@ -135,15 +135,21 @@ private fun buildAllFunctions(pythonClass: PythonClass): List { } private fun buildConstructor(`class`: PythonClass) = buildString { - appendLine("def __init__(${buildParameters(`class`.constructor?.parameters.orEmpty())}):") + val constructor = `class`.constructor ?: return "" + appendLine("def __init__(${buildParameters(constructor.parameters)}):") appendIndented(4) { + val boundaries = buildBoundaryChecks(constructor).joinToString("\n") + if (boundaries.isNotBlank()) { + append("$boundaries\n\n") + } + val attributes = buildAttributeAssignments(`class`).joinToString("\n") if (attributes.isNotBlank()) { append("$attributes\n\n") } - val constructorCall = `class`.constructor?.buildConstructorCall() ?: "" + val constructorCall = constructor.buildConstructorCall() if (constructorCall.isNotBlank()) { append(constructorCall) } @@ -239,7 +245,7 @@ private fun PythonParameter.toPythonCode() = buildString { } private fun buildFunctionBody(pythonFunction: PythonFunction): String { - var formattedBoundaries = buildBoundaryChecks(pythonFunction).joinToString("\n".repeat(1)) + var formattedBoundaries = buildBoundaryChecks(pythonFunction).joinToString("\n") if (formattedBoundaries.isNotBlank()) { formattedBoundaries = "$formattedBoundaries\n" } @@ -250,10 +256,17 @@ private fun buildFunctionBody(pythonFunction: PythonFunction): String { ) } +private fun buildBoundaryChecks(pythonConstructor: PythonConstructor): List { + return buildBoundaryChecks(pythonConstructor.parameters) +} + private fun buildBoundaryChecks(pythonFunction: PythonFunction): List { + return buildBoundaryChecks(pythonFunction.parameters) +} + +private fun buildBoundaryChecks(parameters: List): List { val formattedBoundaries: MutableList = ArrayList() - pythonFunction - .parameters + parameters .filter { it.boundary != null } .forEach { assert(it.boundary != null) @@ -334,19 +347,7 @@ private fun buildBoundaryChecks(pythonFunction: PythonFunction): List { return formattedBoundaries } -private fun PythonExpression.toPythonCode(): String { - return when (this) { - is PythonBoolean -> value.toString() - is PythonCall -> "$receiver(${arguments.joinToString { it.toPythonCode() }})" - is PythonFloat -> value.toString() - is PythonInt -> value.toString() - is PythonString -> "'$value'" - is PythonMemberAccess -> "${receiver!!.toPythonCode()}.${member!!.toPythonCode()}" - is PythonReference -> declaration!!.name - } -} - -private fun PythonArgument.toPythonCode() = buildString { +internal fun PythonArgument.toPythonCode() = buildString { if (name != null) { append("$name=") } @@ -369,6 +370,18 @@ internal fun PythonEnum.toPythonCode() = buildString { } } +internal fun PythonExpression.toPythonCode(): String { + return when (this) { + is PythonBoolean -> value.toString().replaceFirstChar { it.uppercase() } + is PythonCall -> "$receiver(${arguments.joinToString { it.toPythonCode() }})" + is PythonFloat -> value.toString() + is PythonInt -> value.toString() + is PythonMemberAccess -> "${receiver!!.toPythonCode()}.${member!!.toPythonCode()}" + is PythonReference -> declaration!!.name + is PythonString -> "'$value'" + } +} + private fun StringBuilder.appendIndented(numberOfSpaces: Int, init: StringBuilder.() -> Unit): StringBuilder { val stringToIndent = StringBuilder().apply(init).toString() val indent = " ".repeat(numberOfSpaces) diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt index cffde4d73..3f0202a33 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt @@ -8,17 +8,21 @@ import com.larsreimann.api_editor.model.PythonParameterAssignment import com.larsreimann.api_editor.mutable_model.OriginalPythonClass import com.larsreimann.api_editor.mutable_model.PythonArgument import com.larsreimann.api_editor.mutable_model.PythonAttribute +import com.larsreimann.api_editor.mutable_model.PythonBoolean import com.larsreimann.api_editor.mutable_model.PythonCall import com.larsreimann.api_editor.mutable_model.PythonClass import com.larsreimann.api_editor.mutable_model.PythonConstructor import com.larsreimann.api_editor.mutable_model.PythonEnum import com.larsreimann.api_editor.mutable_model.PythonEnumInstance +import com.larsreimann.api_editor.mutable_model.PythonFloat import com.larsreimann.api_editor.mutable_model.PythonFunction +import com.larsreimann.api_editor.mutable_model.PythonInt import com.larsreimann.api_editor.mutable_model.PythonMemberAccess import com.larsreimann.api_editor.mutable_model.PythonModule import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference import com.larsreimann.api_editor.mutable_model.PythonResult +import com.larsreimann.api_editor.mutable_model.PythonString import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -134,13 +138,13 @@ class PythonCodeGeneratorTest { | |class test-class: | def test-class-function(self, *, only-param='defaultValue'): - | self.instance.test-class-function(only-param) + | return self.instance.test-class-function(only-param) | |def function_module(param1, param2, param3): - | test-module.function_module(param1=param1, param2=param2, param3=param3) + | return test-module.function_module(param1=param1, param2=param2, param3=param3) | |def test-function(*, test-parameter=42): - | test-module.test-function(test-parameter=test-parameter) + | return test-module.test-function(test-parameter=test-parameter) | """.trimMargin() @@ -229,10 +233,10 @@ class PythonCodeGeneratorTest { |import test-module | |def function_module(param1, param2, param3): - | test-module.function_module(param1=param1, param2=param2, param3=param3) + | return test-module.function_module(param1=param1, param2=param2, param3=param3) | |def test-function(*, test-parameter=42): - | test-module.test-function(test-parameter=test-parameter) + | return test-module.test-function(test-parameter=test-parameter) | """.trimMargin() @@ -398,7 +402,7 @@ class PythonCodeGeneratorTest { | raise ValueError('Valid values of param2 must be greater than or equal to 5.0, but {} was assigned.'.format(param2)) | if not param3 < 10.0: | raise ValueError('Valid values of param3 must be less than 10.0, but {} was assigned.'.format(param3)) - | test-module.function_module(param1=param1, param2=param2, param3=param3) + | return test-module.function_module(param1=param1, param2=param2, param3=param3) | """.trimMargin() @@ -449,7 +453,7 @@ class PythonCodeGeneratorTest { |def function_module(*, param1=5): | if not 2.0 <= param1: | raise ValueError('Valid values of param1 must be greater than or equal to 2.0, but {} was assigned.'.format(param1)) - | test-module.function_module(param1=param1) + | return test-module.function_module(param1=param1) | """.trimMargin() @@ -574,10 +578,10 @@ class PythonCodeGeneratorTest { """ |class test-class: | def test-class-function1(self, only-param): - | self.instance.test-class-function1(only-param) + | return self.instance.test-class-function1(only-param) | | def test-class-function2(self, only-param): - | self.instance.test-class-function2(only-param)""".trimMargin() + | return self.instance.test-class-function2(only-param)""".trimMargin() formattedClass shouldBe expectedFormattedClass } @@ -621,7 +625,7 @@ class PythonCodeGeneratorTest { """ |class test-class: | def test-function(self, second-param, third-param): - | self.instance.test-function(second-param, third-param=third-param)""".trimMargin() + | return self.instance.test-function(second-param, third-param=third-param)""".trimMargin() formattedClass shouldBe expectedFormattedClass } @@ -640,7 +644,7 @@ class PythonCodeGeneratorTest { val expectedFormattedFunction: String = """ |def test-function(): - | test-module.test-function()""".trimMargin() + | return test-module.test-function()""".trimMargin() formattedFunction shouldBe expectedFormattedFunction } @@ -672,7 +676,7 @@ class PythonCodeGeneratorTest { val expectedFormattedFunction: String = """ |def test-function(*, only-param=13): - | test-module.test-function(only-param)""".trimMargin() + | return test-module.test-function(only-param)""".trimMargin() formattedFunction shouldBe expectedFormattedFunction } @@ -704,7 +708,7 @@ class PythonCodeGeneratorTest { val expectedFormattedFunction: String = """ |def test-function(*, only-param=False): - | test-module.test-function(only-param)""".trimMargin() + | return test-module.test-function(only-param)""".trimMargin() formattedFunction shouldBe expectedFormattedFunction } @@ -735,7 +739,7 @@ class PythonCodeGeneratorTest { val expectedFormattedFunction: String = """ |def test-function(only-param): - | test-module.test-function(only-param=only-param)""".trimMargin() + | return test-module.test-function(only-param=only-param)""".trimMargin() formattedFunction shouldBe expectedFormattedFunction } @@ -767,7 +771,7 @@ class PythonCodeGeneratorTest { val expectedFormattedFunction: String = """ |def test-function(first-param, second-param): - | test-module.test-function(first-param, second-param)""".trimMargin() + | return test-module.test-function(first-param, second-param)""".trimMargin() formattedFunction shouldBe expectedFormattedFunction } @@ -804,7 +808,7 @@ class PythonCodeGeneratorTest { val expectedFormattedFunction: String = """ |def test-function(first-param, second-param, third-param): - | test-module.test-function(first-param, second-param, third-param=third-param)""".trimMargin() + | return test-module.test-function(first-param, second-param, third-param=third-param)""".trimMargin() formattedFunction shouldBe expectedFormattedFunction } @@ -838,7 +842,7 @@ class PythonCodeGeneratorTest { val expectedFormattedFunction: String = """ |def test-function(first-param, second-param): - | test-module.test-function(first-param, second-param=second-param)""".trimMargin() + | return test-module.test-function(first-param, second-param=second-param)""".trimMargin() formattedFunction shouldBe expectedFormattedFunction } @@ -874,7 +878,7 @@ class PythonCodeGeneratorTest { val expectedFormattedFunction: String = """ |def test-function(first-param, second-param): - | test-module.test-function(first-param, second-param=second-param) + | return test-module.test-function(first-param, second-param=second-param) """.trimMargin() formattedFunction shouldBe expectedFormattedFunction @@ -913,7 +917,7 @@ class PythonCodeGeneratorTest { val expectedFormattedFunction: String = """ |def test-function(first-param, second-param, third-param): - | test-module.test-function(first-param, second-param, third-param=third-param)""".trimMargin() + | return test-module.test-function(first-param, second-param, third-param=third-param)""".trimMargin() formattedFunction shouldBe expectedFormattedFunction } @@ -947,7 +951,7 @@ class PythonCodeGeneratorTest { |class test-class: | @staticmethod | def test-class-function1(only-param): - | test-module.test-class.test-class-function1(only-param)""".trimMargin() + | return test-module.test-class.test-class-function1(only-param)""".trimMargin() formattedClass shouldBe expectedFormattedClass } @@ -1007,7 +1011,7 @@ class PythonCodeGeneratorTest { testFunction.toPythonCode() shouldBe """ |def testFunction(testParameter): - | testModule.testFunction(testParameter.value) + | return testModule.testFunction(testParameter.value) """.trimMargin() } @@ -1045,11 +1049,32 @@ class PythonCodeGeneratorTest { testFunction.toPythonCode() shouldBe """ |def testFunction(testGroup): - | testModule.testFunction(testGroup.newParameter1, oldParameter2=testGroup.newParameter2) + | return testModule.testFunction(testGroup.newParameter1, oldParameter2=testGroup.newParameter2) """.trimMargin() } } + @Nested + inner class ArgumentToPythonCode { + + @Test + fun `should handle positional arguments`() { + val testArgument = PythonArgument(value = PythonInt(1)) + + testArgument.toPythonCode() shouldBe "1" + } + + @Test + fun `should handle named arguments`() { + val testArgument = PythonArgument( + name = "arg", + value = PythonInt(1) + ) + + testArgument.toPythonCode() shouldBe "arg=1" + } + } + @Nested inner class EnumToPythonCode { @@ -1086,4 +1111,68 @@ class PythonCodeGeneratorTest { """.trimMargin() } } + + @Nested + inner class ExpressionToPythonCode { + + @Test + fun `should handle false boolean`() { + val expression = PythonBoolean(false) + expression.toPythonCode() shouldBe "False" + } + + @Test + fun `should handle true boolean`() { + val expression = PythonBoolean(true) + expression.toPythonCode() shouldBe "True" + } + + @Test + fun `should handle calls`() { + val expression = PythonCall( + receiver = "function", + arguments = listOf( + PythonArgument(value = PythonInt(1)), + PythonArgument( + name = "param", + value = PythonInt(1) + ) + ) + ) + expression.toPythonCode() shouldBe "function(1, param=1)" + } + + @Test + fun `should handle floats`() { + val expression = PythonFloat(1.0) + expression.toPythonCode() shouldBe "1.0" + } + + @Test + fun `should handle ints`() { + val expression = PythonInt(1) + expression.toPythonCode() shouldBe "1" + } + + @Test + fun `should handle member accesses`() { + val expression = PythonMemberAccess( + receiver = PythonReference(PythonParameter(name = "param")), + member = PythonReference(PythonAttribute(name = "value")) + ) + expression.toPythonCode() shouldBe "param.value" + } + + @Test + fun `should handle references`() { + val expression = PythonReference(PythonParameter("param")) + expression.toPythonCode() shouldBe "param" + } + + @Test + fun `should handle strings`() { + val expression = PythonString("string") + expression.toPythonCode() shouldBe "'string'" + } + } } From 204a2ce28faba688b24546150ce77e89309bca6d Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 10 Jan 2022 11:51:04 +0100 Subject: [PATCH 03/32] fix: Python codegen of Booleans --- .../com/larsreimann/api_editor/model/editorAnnotations.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt b/server/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt index 681807a0d..2b72bbf74 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt @@ -129,14 +129,14 @@ sealed class DefaultValue @Serializable class DefaultBoolean(val value: Boolean) : DefaultValue() { override fun toString(): String { - return "$value" + return value.toString().replaceFirstChar { it.uppercase() } } } @Serializable class DefaultNumber(val value: Double) : DefaultValue() { override fun toString(): String { - return "$value" + return value.toString() } } From 7b0fc8c02bd3b98fbba24fdcf077e5bb69a7bc02 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 10 Jan 2022 11:59:31 +0100 Subject: [PATCH 04/32] fix: wrong member access for parameter objects --- .../api_editor/transformation/GroupAnnotationProcessor.kt | 2 +- .../api_editor/transformation/GroupAnnotationProcessorTest.kt | 4 ++-- .../transformation/ParameterAnnotationProcessorTest.kt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt index 5b6265149..6e961291f 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt @@ -69,7 +69,7 @@ private fun PythonFunction.processGroupAnnotations(module: PythonModule) { val value = it.value if (value is PythonReference && value.declaration?.name in annotation.parameters) { it.value = PythonMemberAccess( - receiver = PythonReference(declaration = groupedParameterClass), + receiver = PythonReference(declaration = groupedParameter), member = PythonReference(PythonAttribute(name = value.declaration!!.name)) ) } diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessorTest.kt index bdf3357e4..31cfc6034 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessorTest.kt @@ -90,7 +90,7 @@ class GroupAnnotationProcessorTest { val secondArgumentValue = secondArgument.value.shouldBeInstanceOf() secondArgumentValue.receiver.asClue { it.shouldBeInstanceOf() - it.declaration?.name shouldBe "TestGroup" + it.declaration?.name shouldBe "testGroup" } secondArgumentValue.member.asClue { it.shouldNotBeNull() @@ -102,7 +102,7 @@ class GroupAnnotationProcessorTest { val thirdArgumentValue = thirdArgument.value.shouldBeInstanceOf() thirdArgumentValue.receiver.asClue { it.shouldBeInstanceOf() - it.declaration?.name shouldBe "TestGroup" + it.declaration?.name shouldBe "testGroup" } thirdArgumentValue.member.asClue { it.shouldNotBeNull() diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessorTest.kt index 1101b9c7d..c619b1603 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessorTest.kt @@ -90,7 +90,7 @@ class ParameterAnnotationProcessorTest { attributes.shouldHaveSize(1) attributes[0] shouldBe attributes[0].copy( name = "testParameter", - value = "true" + value = "True" ) } @@ -158,7 +158,7 @@ class ParameterAnnotationProcessorTest { testPackage.processParameterAnnotations() testParameter.assignedBy shouldBe PythonParameterAssignment.NAME_ONLY - testParameter.defaultValue shouldBe "true" + testParameter.defaultValue shouldBe "True" } @Test From 3d43bfb41a3eba43ea483571680d549340af0977 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 10 Jan 2022 12:31:32 +0100 Subject: [PATCH 05/32] refactor: classes for types --- .../api_editor/codegen/StubCodeGenerator.kt | 50 ++++--- .../ConverterFromMutableModel.kt | 125 ------------------ .../mutable_model/ConverterToMutableModel.kt | 7 +- .../api_editor/mutable_model/PythonAst.kt | 19 ++- .../transformation/EnumAnnotationProcessor.kt | 3 +- .../GroupAnnotationProcessor.kt | 18 ++- .../ParameterAnnotationProcessor.kt | 2 +- .../transformation/Postprocessor.kt | 2 +- .../codegen/PythonCodeGeneratorTest.kt | 23 ++-- .../codegen/StubCodeGeneratorTest.kt | 21 +-- .../EnumAnnotationProcessorTest.kt | 15 ++- .../GroupAnnotationProcessorTest.kt | 23 ++-- 12 files changed, 115 insertions(+), 193 deletions(-) delete mode 100644 server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterFromMutableModel.kt diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/StubCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/StubCodeGenerator.kt index ee1e38934..aa99e4b29 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/StubCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/StubCodeGenerator.kt @@ -7,8 +7,11 @@ import com.larsreimann.api_editor.mutable_model.PythonEnum import com.larsreimann.api_editor.mutable_model.PythonEnumInstance import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonResult +import com.larsreimann.api_editor.mutable_model.PythonStringifiedType +import com.larsreimann.api_editor.mutable_model.PythonType import de.unibonn.simpleml.constant.SmlFileExtension import de.unibonn.simpleml.emf.createSmlAnnotationUse import de.unibonn.simpleml.emf.createSmlArgument @@ -124,7 +127,7 @@ internal fun PythonAttribute.toSmlAttribute(): SmlAttribute { add(createSmlDescriptionAnnotationUse(description)) } }, - type = typeInDocs.toSmlType() + type = type.toSmlType() ) } @@ -181,7 +184,7 @@ internal fun PythonParameter.toSmlParameterOrNull(): SmlParameter? { add(createSmlDescriptionAnnotationUse(description)) } }, - type = typeInDocs.toSmlType(), + type = type.toSmlType(), defaultValue = defaultValue?.toSmlExpression() ) } @@ -258,24 +261,33 @@ private fun String.snakeCaseToCamelCase(): String { // Type conversions ---------------------------------------------------------------------------------------------------- -internal fun String.toSmlType(): SmlAbstractType { +internal fun PythonType.toSmlType(): SmlAbstractType { return when (this) { - "bool" -> createSmlNamedType( - declaration = createSmlClass("Boolean") - ) - "float" -> createSmlNamedType( - declaration = createSmlClass("Float") - ) - "int" -> createSmlNamedType( - declaration = createSmlClass("Int") - ) - "str" -> createSmlNamedType( - declaration = createSmlClass("String") - ) - else -> createSmlNamedType( - declaration = createSmlClass("Any"), - isNullable = true - ) + is PythonNamedType -> { + createSmlNamedType( + declaration = createSmlClass(this.declaration!!.name) + ) + } + is PythonStringifiedType -> { + when (this.type) { + "bool" -> createSmlNamedType( + declaration = createSmlClass("Boolean") + ) + "float" -> createSmlNamedType( + declaration = createSmlClass("Float") + ) + "int" -> createSmlNamedType( + declaration = createSmlClass("Int") + ) + "str" -> createSmlNamedType( + declaration = createSmlClass("String") + ) + else -> createSmlNamedType( + declaration = createSmlClass("Any"), + isNullable = true + ) + } + } } } diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterFromMutableModel.kt b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterFromMutableModel.kt deleted file mode 100644 index 2a0ea04ae..000000000 --- a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterFromMutableModel.kt +++ /dev/null @@ -1,125 +0,0 @@ -package com.larsreimann.api_editor.mutable_model - -import com.larsreimann.api_editor.model.SerializablePythonAttribute -import com.larsreimann.api_editor.model.SerializablePythonClass -import com.larsreimann.api_editor.model.SerializablePythonEnum -import com.larsreimann.api_editor.model.SerializablePythonEnumInstance -import com.larsreimann.api_editor.model.SerializablePythonFunction -import com.larsreimann.api_editor.model.SerializablePythonModule -import com.larsreimann.api_editor.model.SerializablePythonPackage -import com.larsreimann.api_editor.model.SerializablePythonParameter -import com.larsreimann.api_editor.model.SerializablePythonResult - -fun convertPackage(pythonPackage: PythonPackage): SerializablePythonPackage { - return SerializablePythonPackage( - distribution = pythonPackage.distribution, - name = pythonPackage.name, - version = pythonPackage.version, - modules = pythonPackage.modules.map { convertModule(it) }.toMutableList(), - annotations = pythonPackage.annotations - ) -} - -fun convertModule(pythonModule: PythonModule): SerializablePythonModule { - val result = SerializablePythonModule( - name = pythonModule.name, - imports = pythonModule.imports, - fromImports = pythonModule.fromImports, - classes = pythonModule.classes.map { convertClass(it) }.toMutableList(), - functions = pythonModule.functions.map { convertFunction(it) }.toMutableList(), - annotations = pythonModule.annotations - ) - result.enums += pythonModule.enums.map { convertEnum(it) } - return result -} - -fun convertClass(pythonClass: PythonClass): SerializablePythonClass { - val result = SerializablePythonClass( - name = pythonClass.name, - qualifiedName = pythonClass.qualifiedName(), - decorators = pythonClass.decorators, - superclasses = pythonClass.superclasses, - methods = pythonClass.methods.map { convertFunction(it) }.toMutableList(), - isPublic = pythonClass.isPublic, - description = pythonClass.description, - fullDocstring = pythonClass.fullDocstring, - annotations = pythonClass.annotations - ) - result.attributes += pythonClass.attributes.map { convertAttribute(it) } - return result -} - -fun convertEnum(pythonEnum: PythonEnum): SerializablePythonEnum { - return SerializablePythonEnum( - name = pythonEnum.name, - instances = pythonEnum.instances.map { convertEnumInstance(it) }.toMutableList(), - annotations = pythonEnum.annotations - ) -} - -fun convertEnumInstance(pythonEnumInstance: PythonEnumInstance): SerializablePythonEnumInstance { - val instance = SerializablePythonEnumInstance( - name = pythonEnumInstance.name, - value = pythonEnumInstance.value - ) - instance.description = pythonEnumInstance.description - return instance -} - -fun convertFunction(pythonFunction: PythonFunction): SerializablePythonFunction { - val result = SerializablePythonFunction( - name = pythonFunction.name, - qualifiedName = pythonFunction.qualifiedName(), - decorators = pythonFunction.decorators, - parameters = pythonFunction.parameters.map { convertParameter(it) }.toMutableList(), - results = pythonFunction.results.map { convertResult(it) }.toMutableList(), - isPublic = pythonFunction.isPublic, - description = pythonFunction.description, - fullDocstring = pythonFunction.fullDocstring, - annotations = pythonFunction.annotations, - ) - result.calledAfter += pythonFunction.calledAfter - result.isPure = pythonFunction.isPure - return result -} - -fun convertAttribute(pythonAttribute: PythonAttribute): SerializablePythonAttribute { - val result = SerializablePythonAttribute( - name = pythonAttribute.name, - qualifiedName = pythonAttribute.qualifiedName(), - defaultValue = pythonAttribute.value, - isPublic = pythonAttribute.isPublic, - typeInDocs = pythonAttribute.typeInDocs, - description = pythonAttribute.description, - annotations = pythonAttribute.annotations - ) - result.boundary = pythonAttribute.boundary - return result -} - -fun convertParameter(pythonParameter: PythonParameter): SerializablePythonParameter { - val result = SerializablePythonParameter( - name = pythonParameter.name, - qualifiedName = pythonParameter.qualifiedName(), - defaultValue = pythonParameter.defaultValue, - assignedBy = pythonParameter.assignedBy, - isPublic = true, - typeInDocs = pythonParameter.typeInDocs, - description = pythonParameter.description, - annotations = pythonParameter.annotations - ) - result.boundary = pythonParameter.boundary - return result -} - -fun convertResult(pythonResult: PythonResult): SerializablePythonResult { - val result = SerializablePythonResult( - name = pythonResult.name, - type = pythonResult.type, - typeInDocs = pythonResult.typeInDocs, - description = pythonResult.description, - annotations = pythonResult.annotations - ) - result.boundary = pythonResult.boundary - return result -} diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterToMutableModel.kt b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterToMutableModel.kt index b5f13571f..eeb5974c1 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterToMutableModel.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterToMutableModel.kt @@ -81,9 +81,9 @@ fun convertFunction(pythonFunction: SerializablePythonFunction): PythonFunction fun convertAttribute(pythonAttribute: SerializablePythonAttribute): PythonAttribute { return PythonAttribute( name = pythonAttribute.name, + type = PythonStringifiedType(pythonAttribute.typeInDocs), value = pythonAttribute.defaultValue, isPublic = pythonAttribute.isPublic, - typeInDocs = pythonAttribute.typeInDocs, description = pythonAttribute.description, boundary = pythonAttribute.boundary, annotations = pythonAttribute.annotations, @@ -93,9 +93,9 @@ fun convertAttribute(pythonAttribute: SerializablePythonAttribute): PythonAttrib fun convertParameter(pythonParameter: SerializablePythonParameter): PythonParameter { return PythonParameter( name = pythonParameter.name, + type = PythonStringifiedType(pythonParameter.typeInDocs), defaultValue = pythonParameter.defaultValue, assignedBy = pythonParameter.assignedBy, - typeInDocs = pythonParameter.typeInDocs, description = pythonParameter.description, boundary = pythonParameter.boundary, annotations = pythonParameter.annotations, @@ -105,8 +105,7 @@ fun convertParameter(pythonParameter: SerializablePythonParameter): PythonParame fun convertResult(pythonResult: SerializablePythonResult): PythonResult { return PythonResult( name = pythonResult.name, - type = pythonResult.type, - typeInDocs = pythonResult.typeInDocs, + type = PythonStringifiedType(pythonResult.type), description = pythonResult.description, boundary = pythonResult.boundary, annotations = pythonResult.annotations, diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt index e2711a964..3fe7872e6 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt @@ -154,9 +154,9 @@ class PythonFunction( data class PythonAttribute( override var name: String, + var type: PythonType = PythonStringifiedType(""), var value: String? = null, var isPublic: Boolean = true, - var typeInDocs: String = "", var description: String = "", var boundary: Boundary? = null, override val annotations: MutableList = mutableListOf(), @@ -164,9 +164,9 @@ data class PythonAttribute( data class PythonParameter( override var name: String, + var type: PythonType = PythonStringifiedType(""), var defaultValue: String? = null, var assignedBy: PythonParameterAssignment = PythonParameterAssignment.POSITION_OR_NAME, - var typeInDocs: String = "", var description: String = "", var boundary: Boundary? = null, override val annotations: MutableList = mutableListOf(), @@ -179,8 +179,7 @@ data class PythonParameter( data class PythonResult( override var name: String, - var type: String = "", - var typeInDocs: String = "", + var type: PythonType = PythonStringifiedType(""), var description: String = "", var boundary: Boundary? = null, override val annotations: MutableList = mutableListOf(), @@ -232,3 +231,15 @@ data class PythonBoolean(val value: Boolean) : PythonLiteral() data class PythonFloat(val value: Double) : PythonLiteral() data class PythonInt(val value: Int) : PythonLiteral() data class PythonString(val value: String) : PythonLiteral() + +/* ******************************************************************************************************************** + * Types + * ********************************************************************************************************************/ + +sealed class PythonType : PythonAstNode() + +class PythonNamedType(declaration: PythonDeclaration) : PythonType() { + var declaration by CrossReference(declaration) +} + +data class PythonStringifiedType(val type: String) : PythonType() diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessor.kt index 290764ef1..354c37aa7 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessor.kt @@ -7,6 +7,7 @@ import com.larsreimann.api_editor.mutable_model.PythonEnum import com.larsreimann.api_editor.mutable_model.PythonEnumInstance import com.larsreimann.api_editor.mutable_model.PythonMemberAccess import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference @@ -68,7 +69,7 @@ private fun PythonParameter.processEnumAnnotations(module: PythonModule) { member = PythonReference(PythonAttribute(name = "value")) ) - this.typeInDocs = annotation.enumName + this.type = PythonNamedType(enumToAdd) this.annotations -= annotation } } diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt index 6e961291f..c54da1d59 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt @@ -8,6 +8,7 @@ import com.larsreimann.api_editor.mutable_model.PythonConstructor import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonMemberAccess import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference @@ -34,10 +35,8 @@ private fun PythonFunction.processGroupAnnotations(module: PythonModule) { .filterIsInstance() .forEach { annotation -> val firstOccurrence = this.parameters.indexOfFirst { it.name in annotation.parameters } - val groupedParameter = PythonParameter( - name = annotation.groupName.replaceFirstChar { it.lowercase() }, - typeInDocs = annotation.groupName.replaceFirstChar { it.uppercase() }, - ) + + // Create class val constructorParameters = mutableListOf( PythonParameter( name = "self", @@ -45,14 +44,21 @@ private fun PythonFunction.processGroupAnnotations(module: PythonModule) { ) ) constructorParameters += this.parameters.filter { it.name in annotation.parameters } - this.parameters.removeIf { it.name in annotation.parameters } - this.parameters.add(firstOccurrence, groupedParameter) val groupedParameterClass = PythonClass( name = annotation.groupName.replaceFirstChar { it.uppercase() }, constructor = PythonConstructor( parameters = constructorParameters ) ) + + // Update parameters + val groupedParameter = PythonParameter( + name = annotation.groupName.replaceFirstChar { it.lowercase() }, + type = PythonNamedType(groupedParameterClass) + ) + this.parameters.removeIf { it.name in annotation.parameters } + this.parameters.add(firstOccurrence, groupedParameter) + if (hasConflictingGroups(module.classes, groupedParameterClass)) { throw ConflictingGroupException( groupedParameterClass.name, diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessor.kt index d980e0984..aeb17f824 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessor.kt @@ -53,9 +53,9 @@ private fun PythonParameter.processAttributeAnnotation(annotation: AttributeAnno val containingClass = this.closest()!! containingClass.attributes += PythonAttribute( name = name, + type = type, value = annotation.defaultValue.toString(), isPublic = true, - typeInDocs = typeInDocs, description = description, boundary = boundary ) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt index 423634c51..86526b338 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt @@ -89,9 +89,9 @@ private fun PythonClass.createAttributesForParametersOfConstructor() { ?.forEach { this.attributes += PythonAttribute( name = it.name, + type = it.type, value = it.name, isPublic = true, - typeInDocs = it.typeInDocs, description = it.description, boundary = it.boundary ) diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt index 3f0202a33..f16b98112 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt @@ -23,6 +23,7 @@ import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference import com.larsreimann.api_editor.mutable_model.PythonResult import com.larsreimann.api_editor.mutable_model.PythonString +import com.larsreimann.api_editor.mutable_model.PythonStringifiedType import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -82,7 +83,7 @@ class PythonCodeGeneratorTest { results = listOf( PythonResult( name = "test-result", - type = "str" + type = PythonStringifiedType("str") ) ), callToOriginalAPI = PythonCall( @@ -111,7 +112,7 @@ class PythonCodeGeneratorTest { results = listOf( PythonResult( "test-result", - "str", + PythonStringifiedType("str"), "str" ) ), @@ -159,8 +160,8 @@ class PythonCodeGeneratorTest { val testFunction1Parameter3 = PythonParameter(name = "param3") val testFunction2Parameter = PythonParameter( "test-parameter", - "42", - PythonParameterAssignment.NAME_ONLY + defaultValue = "42", + assignedBy = PythonParameterAssignment.NAME_ONLY ) val testModule = PythonModule( name = "test-module", @@ -175,8 +176,7 @@ class PythonCodeGeneratorTest { results = mutableListOf( PythonResult( "test-result", - "str", - "str", + PythonStringifiedType("str"), "Lorem ipsum" ) ), @@ -206,8 +206,7 @@ class PythonCodeGeneratorTest { results = mutableListOf( PythonResult( "test-result", - "str", - "str", + PythonStringifiedType("str"), "Lorem ipsum" ) ), @@ -337,8 +336,8 @@ class PythonCodeGeneratorTest { ) val testParameter2 = PythonParameter( "param2", - "5", - PythonParameterAssignment.NAME_ONLY + defaultValue = "5", + assignedBy = PythonParameterAssignment.NAME_ONLY ) testParameter2.boundary = Boundary( false, @@ -349,8 +348,8 @@ class PythonCodeGeneratorTest { ) val testParameter3 = PythonParameter( "param3", - "5", - PythonParameterAssignment.NAME_ONLY + defaultValue = "5", + assignedBy = PythonParameterAssignment.NAME_ONLY ) testParameter3.boundary = Boundary( false, diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/StubCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/StubCodeGeneratorTest.kt index bc350392c..2e2daa556 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/StubCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/StubCodeGeneratorTest.kt @@ -10,6 +10,7 @@ import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonModule import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonResult +import com.larsreimann.api_editor.mutable_model.PythonStringifiedType import de.unibonn.simpleml.SimpleMLStandaloneSetup import de.unibonn.simpleml.emf.annotationUsesOrEmpty import de.unibonn.simpleml.emf.argumentsOrEmpty @@ -88,14 +89,14 @@ class StubCodeGeneratorTest { ), PythonParameter( name = "testParameter", - typeInDocs = "int", + type = PythonStringifiedType("int"), defaultValue = "10" ) ), results = listOf( PythonResult( name = "testParameter", - type = "str" + type = PythonStringifiedType("str") ) ) ) @@ -453,7 +454,7 @@ class StubCodeGeneratorTest { fun `should store type`() { val pythonAttribute = PythonAttribute( name = "testAttribute", - typeInDocs = "str" + type = PythonStringifiedType("str") ) pythonAttribute @@ -741,7 +742,7 @@ class StubCodeGeneratorTest { fun `should store type`() { val pythonParameter = PythonParameter( name = "testParameter", - typeInDocs = "str" + type = PythonStringifiedType("str") ) pythonParameter @@ -860,7 +861,7 @@ class StubCodeGeneratorTest { fun `should store type`() { val pythonResult = PythonResult( name = "testResult", - type = "str" + type = PythonStringifiedType("str") ) val type = pythonResult.toSmlResult().type.shouldBeInstanceOf() @@ -1084,35 +1085,35 @@ class StubCodeGeneratorTest { @Test fun `should convert bool to Boolean`() { - val smlType = "bool".toSmlType().shouldBeInstanceOf() + val smlType = PythonStringifiedType("bool").toSmlType().shouldBeInstanceOf() smlType.declaration.name shouldBe "Boolean" smlType.isNullable.shouldBeFalse() } @Test fun `should convert float to Float`() { - val smlType = "float".toSmlType().shouldBeInstanceOf() + val smlType = PythonStringifiedType("float").toSmlType().shouldBeInstanceOf() smlType.declaration.name shouldBe "Float" smlType.isNullable.shouldBeFalse() } @Test fun `should convert int to Int`() { - val smlType = "int".toSmlType().shouldBeInstanceOf() + val smlType = PythonStringifiedType("int").toSmlType().shouldBeInstanceOf() smlType.declaration.name shouldBe "Int" smlType.isNullable.shouldBeFalse() } @Test fun `should convert str to String`() { - val smlType = "str".toSmlType().shouldBeInstanceOf() + val smlType = PythonStringifiedType("str").toSmlType().shouldBeInstanceOf() smlType.declaration.name shouldBe "String" smlType.isNullable.shouldBeFalse() } @Test fun `should convert other types to nullable Any`() { - val smlType = "other".toSmlType().shouldBeInstanceOf() + val smlType = PythonStringifiedType("other").toSmlType().shouldBeInstanceOf() smlType.declaration.name shouldBe "Any" smlType.isNullable.shouldBeTrue() } diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessorTest.kt index 78052efbe..6bd8b92f0 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessorTest.kt @@ -10,6 +10,7 @@ import com.larsreimann.api_editor.mutable_model.PythonEnumInstance import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonMemberAccess import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference @@ -97,7 +98,12 @@ class EnumAnnotationProcessorTest { fun `should process EnumAnnotations on parameter level`() { testPackage.processEnumAnnotations() - testParameter.typeInDocs shouldBe "TestEnum" + val type = testParameter.type + type.shouldBeInstanceOf() + + val declaration = type.declaration + declaration.shouldBeInstanceOf() + declaration.name shouldBe "TestEnum" } @Test @@ -121,7 +127,12 @@ class EnumAnnotationProcessorTest { testModule.enums += mutableEnum testPackage.processEnumAnnotations() - testParameter.typeInDocs shouldBe "TestEnum" + val type = testParameter.type + type.shouldBeInstanceOf() + + val declaration = type.declaration + declaration.shouldBeInstanceOf() + declaration.name shouldBe "TestEnum" } @Test diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessorTest.kt index 31cfc6034..75a57e326 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessorTest.kt @@ -9,6 +9,7 @@ import com.larsreimann.api_editor.mutable_model.PythonConstructor import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonMemberAccess import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference @@ -118,10 +119,13 @@ class GroupAnnotationProcessorTest { val parameters = testFunction.parameters parameters.shouldHaveSize(2) parameters[0] shouldBe testParameter1 - parameters[1] shouldBe parameters[1].copy( - name = "testGroup", - typeInDocs = "TestGroup" - ) + parameters[1].asClue { + it.name shouldBe "testGroup" + + val type = it.type + type.shouldBeInstanceOf() + type.declaration?.name shouldBe "TestGroup" + } } @Test @@ -159,10 +163,13 @@ class GroupAnnotationProcessorTest { val parameters = testFunction.parameters parameters.shouldHaveSize(2) parameters[0] shouldBe testParameter1 - parameters[1] shouldBe parameters[1].copy( - name = "testGroup", - typeInDocs = "TestGroup" - ) + parameters[1].asClue { + it.name shouldBe "testGroup" + + val type = it.type + type.shouldBeInstanceOf() + type.declaration?.name shouldBe "TestGroup" + } } @Test From cad26b0a805ee2fac2738867fbea2ea9066ce7a9 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 10 Jan 2022 13:08:51 +0100 Subject: [PATCH 06/32] feat: create type hints --- .../api_editor/codegen/PythonCodeGenerator.kt | 40 +++++-- .../api_editor/codegen/StubCodeGenerator.kt | 6 +- .../api_editor/mutable_model/PythonAst.kt | 8 +- .../codegen/PythonCodeGeneratorTest.kt | 104 ++++++++++++++++-- .../codegen/StubCodeGeneratorTest.kt | 23 +++- 5 files changed, 157 insertions(+), 24 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt index 609f974ee..845d7ccff 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt @@ -17,9 +17,12 @@ import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonInt import com.larsreimann.api_editor.mutable_model.PythonMemberAccess import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference import com.larsreimann.api_editor.mutable_model.PythonString +import com.larsreimann.api_editor.mutable_model.PythonStringifiedType +import com.larsreimann.api_editor.mutable_model.PythonType /** * Builds a string containing the formatted module content @@ -237,13 +240,6 @@ private fun buildParameters(parameters: List): String { return formattedFunctionParameters } -private fun PythonParameter.toPythonCode() = buildString { - append(name) - if (defaultValue != null) { - append("=$defaultValue") - } -} - private fun buildFunctionBody(pythonFunction: PythonFunction): String { var formattedBoundaries = buildBoundaryChecks(pythonFunction).joinToString("\n") if (formattedBoundaries.isNotBlank()) { @@ -382,6 +378,36 @@ internal fun PythonExpression.toPythonCode(): String { } } +internal fun PythonParameter.toPythonCode() = buildString { + val typeStringOrNull = type.toPythonCodeOrNull() + + append(name) + if (typeStringOrNull != null) { + append(": $typeStringOrNull") + if (defaultValue != null) { + append(" = $defaultValue") + } + } else if (defaultValue != null) { + append("=$defaultValue") + } +} + +internal fun PythonType?.toPythonCodeOrNull(): String? { + return when (this) { + is PythonNamedType -> this.declaration?.name + is PythonStringifiedType -> { + when (this.type) { + "bool" -> "bool" + "float" -> "float" + "int" -> "int" + "str" -> "str" + else -> null + } + } + null -> null + } +} + private fun StringBuilder.appendIndented(numberOfSpaces: Int, init: StringBuilder.() -> Unit): StringBuilder { val stringToIndent = StringBuilder().apply(init).toString() val indent = " ".repeat(numberOfSpaces) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/StubCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/StubCodeGenerator.kt index aa99e4b29..10b0c34ca 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/StubCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/StubCodeGenerator.kt @@ -261,7 +261,7 @@ private fun String.snakeCaseToCamelCase(): String { // Type conversions ---------------------------------------------------------------------------------------------------- -internal fun PythonType.toSmlType(): SmlAbstractType { +internal fun PythonType?.toSmlType(): SmlAbstractType { return when (this) { is PythonNamedType -> { createSmlNamedType( @@ -288,6 +288,10 @@ internal fun PythonType.toSmlType(): SmlAbstractType { ) } } + null -> createSmlNamedType( + declaration = createSmlClass("Any"), + isNullable = true + ) } } diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt index 3fe7872e6..3ea3a30cb 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt @@ -154,7 +154,7 @@ class PythonFunction( data class PythonAttribute( override var name: String, - var type: PythonType = PythonStringifiedType(""), + var type: PythonType? = null, var value: String? = null, var isPublic: Boolean = true, var description: String = "", @@ -162,9 +162,9 @@ data class PythonAttribute( override val annotations: MutableList = mutableListOf(), ) : PythonDeclaration() -data class PythonParameter( +class PythonParameter( override var name: String, - var type: PythonType = PythonStringifiedType(""), + type: PythonType? = null, var defaultValue: String? = null, var assignedBy: PythonParameterAssignment = PythonParameterAssignment.POSITION_OR_NAME, var description: String = "", @@ -172,6 +172,8 @@ data class PythonParameter( override val annotations: MutableList = mutableListOf(), ) : PythonDeclaration() { + var type by CrossReference(type) + fun isRequired() = defaultValue == null fun isOptional() = defaultValue != null diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt index f16b98112..6a952f982 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt @@ -19,11 +19,13 @@ import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonInt import com.larsreimann.api_editor.mutable_model.PythonMemberAccess import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference import com.larsreimann.api_editor.mutable_model.PythonResult import com.larsreimann.api_editor.mutable_model.PythonString import com.larsreimann.api_editor.mutable_model.PythonStringifiedType +import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.shouldBe import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -1054,23 +1056,46 @@ class PythonCodeGeneratorTest { } @Nested - inner class ArgumentToPythonCode { + inner class ParameterToPythonCode { @Test - fun `should handle positional arguments`() { - val testArgument = PythonArgument(value = PythonInt(1)) + fun `should handle parameters without type and default value`() { + val testParameter = PythonParameter( + name = "param" + ) - testArgument.toPythonCode() shouldBe "1" + testParameter.toPythonCode() shouldBe "param" } @Test - fun `should handle named arguments`() { - val testArgument = PythonArgument( - name = "arg", - value = PythonInt(1) + fun `should handle parameters with type but without default value`() { + val testParameter = PythonParameter( + name = "param", + type = PythonStringifiedType("int") ) - testArgument.toPythonCode() shouldBe "arg=1" + testParameter.toPythonCode() shouldBe "param: int" + } + + @Test + fun `should handle parameters without type but with default value`() { + val testParameter = PythonParameter( + name = "param", + defaultValue = "1" + ) + + testParameter.toPythonCode() shouldBe "param=1" + } + + @Test + fun `should handle parameters with type and default value`() { + val testParameter = PythonParameter( + name = "param", + type = PythonStringifiedType("int"), + defaultValue = "1" + ) + + testParameter.toPythonCode() shouldBe "param: int = 1" } } @@ -1111,6 +1136,27 @@ class PythonCodeGeneratorTest { } } + @Nested + inner class ArgumentToPythonCode { + + @Test + fun `should handle positional arguments`() { + val testArgument = PythonArgument(value = PythonInt(1)) + + testArgument.toPythonCode() shouldBe "1" + } + + @Test + fun `should handle named arguments`() { + val testArgument = PythonArgument( + name = "arg", + value = PythonInt(1) + ) + + testArgument.toPythonCode() shouldBe "arg=1" + } + } + @Nested inner class ExpressionToPythonCode { @@ -1174,4 +1220,44 @@ class PythonCodeGeneratorTest { expression.toPythonCode() shouldBe "'string'" } } + + @Nested + inner class TypeToPythonCodeOrNull { + + @Test + fun `should handle named types`() { + val type = PythonNamedType(PythonEnum("TestEnum")) + type.toPythonCodeOrNull() shouldBe "TestEnum" + } + + @Test + fun `should convert stringified type 'bool' to Boolean`() { + val smlType = PythonStringifiedType("bool") + smlType.toPythonCodeOrNull() shouldBe "bool" + } + + @Test + fun `should convert stringified type 'float' to Float`() { + val smlType = PythonStringifiedType("float") + smlType.toPythonCodeOrNull() shouldBe "float" + } + + @Test + fun `should convert stringified type 'int' to Int`() { + val smlType = PythonStringifiedType("int") + smlType.toPythonCodeOrNull() shouldBe "int" + } + + @Test + fun `should convert stringified type 'str' to String`() { + val smlType = PythonStringifiedType("str") + smlType.toPythonCodeOrNull() shouldBe "str" + } + + @Test + fun `should return null for other types`() { + val type = PythonStringifiedType("") + type.toPythonCodeOrNull().shouldBeNull() + } + } } diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/StubCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/StubCodeGeneratorTest.kt index 2e2daa556..501bb96e1 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/StubCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/StubCodeGeneratorTest.kt @@ -8,6 +8,7 @@ import com.larsreimann.api_editor.mutable_model.PythonEnum import com.larsreimann.api_editor.mutable_model.PythonEnumInstance import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonResult import com.larsreimann.api_editor.mutable_model.PythonStringifiedType @@ -1084,28 +1085,35 @@ class StubCodeGeneratorTest { inner class TypeConversions { @Test - fun `should convert bool to Boolean`() { + fun `should convert named types`() { + val smlType = PythonNamedType(PythonEnum(name = "MyEnum")).toSmlType().shouldBeInstanceOf() + smlType.declaration.name shouldBe "MyEnum" + smlType.isNullable.shouldBeFalse() + } + + @Test + fun `should convert stringified type 'bool' to Boolean`() { val smlType = PythonStringifiedType("bool").toSmlType().shouldBeInstanceOf() smlType.declaration.name shouldBe "Boolean" smlType.isNullable.shouldBeFalse() } @Test - fun `should convert float to Float`() { + fun `should convert stringified type 'float' to Float`() { val smlType = PythonStringifiedType("float").toSmlType().shouldBeInstanceOf() smlType.declaration.name shouldBe "Float" smlType.isNullable.shouldBeFalse() } @Test - fun `should convert int to Int`() { + fun `should convert stringified type 'int' to Int`() { val smlType = PythonStringifiedType("int").toSmlType().shouldBeInstanceOf() smlType.declaration.name shouldBe "Int" smlType.isNullable.shouldBeFalse() } @Test - fun `should convert str to String`() { + fun `should convert stringified type 'str' to String`() { val smlType = PythonStringifiedType("str").toSmlType().shouldBeInstanceOf() smlType.declaration.name shouldBe "String" smlType.isNullable.shouldBeFalse() @@ -1117,6 +1125,13 @@ class StubCodeGeneratorTest { smlType.declaration.name shouldBe "Any" smlType.isNullable.shouldBeTrue() } + + @Test + fun `should convert null to nullable Any`() { + val smlType = null.toSmlType().shouldBeInstanceOf() + smlType.declaration.name shouldBe "Any" + smlType.isNullable.shouldBeTrue() + } } @Nested From b6d8757354ed110deb7989701dc2f5d409025bdd Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 10 Jan 2022 13:26:56 +0100 Subject: [PATCH 07/32] fix: reorder parameters of constructors --- .../api_editor/mutable_model/PythonAst.kt | 49 ++++++++++-- .../transformation/Postprocessor.kt | 22 ++++-- .../ParameterAnnotationProcessorTest.kt | 8 +- .../transformation/PostprocessorTest.kt | 75 ++++++++++++++----- 4 files changed, 116 insertions(+), 38 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt index 3ea3a30cb..5d049d1c7 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt @@ -90,6 +90,7 @@ class PythonClass( val methods = MutableContainmentList(methods) override fun children() = sequence { + constructor?.let { yield(it) } yieldAll(attributes) yieldAll(methods) } @@ -117,6 +118,10 @@ class PythonEnum( ) : PythonDeclaration() { val instances = MutableContainmentList(instances) + + override fun children() = sequence { + yieldAll(instances) + } } data class PythonEnumInstance( @@ -152,15 +157,22 @@ class PythonFunction( fun isStaticMethod() = isMethod() && "staticmethod" in decorators } -data class PythonAttribute( +class PythonAttribute( override var name: String, - var type: PythonType? = null, + type: PythonType? = null, var value: String? = null, var isPublic: Boolean = true, var description: String = "", var boundary: Boundary? = null, override val annotations: MutableList = mutableListOf(), -) : PythonDeclaration() +) : PythonDeclaration() { + + var type by CrossReference(type) + + override fun children() = sequence { + type?.let { yield(it) } + } +} class PythonParameter( override var name: String, @@ -174,18 +186,29 @@ class PythonParameter( var type by CrossReference(type) + override fun children() = sequence { + type?.let { yield(it) } + } + fun isRequired() = defaultValue == null fun isOptional() = defaultValue != null } -data class PythonResult( +class PythonResult( override var name: String, - var type: PythonType = PythonStringifiedType(""), + type: PythonType? = null, var description: String = "", var boundary: Boundary? = null, override val annotations: MutableList = mutableListOf(), -) : PythonDeclaration() +) : PythonDeclaration() { + + var type by CrossReference(type) + + override fun children() = sequence { + type?.let { yield(it) } + } +} /* ******************************************************************************************************************** * Expressions @@ -193,7 +216,11 @@ data class PythonResult( sealed class PythonExpression : PythonAstNode() -class PythonCall(val receiver: String, arguments: List = emptyList()) : PythonExpression() { +class PythonCall( + val receiver: String, + arguments: List = emptyList() +) : PythonExpression() { + val arguments = MutableContainmentList(arguments) override fun children() = sequence { @@ -225,6 +252,10 @@ class PythonMemberAccess( class PythonReference(declaration: PythonDeclaration) : PythonExpression() { var declaration by CrossReference(declaration) + + override fun children() = sequence { + declaration?.let { yield(it) } + } } sealed class PythonLiteral : PythonExpression() @@ -242,6 +273,10 @@ sealed class PythonType : PythonAstNode() class PythonNamedType(declaration: PythonDeclaration) : PythonType() { var declaration by CrossReference(declaration) + + override fun children() = sequence { + declaration?.let { yield(it) } + } } data class PythonStringifiedType(val type: String) : PythonType() diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt index 86526b338..76531d64c 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt @@ -7,6 +7,8 @@ import com.larsreimann.api_editor.mutable_model.PythonClass import com.larsreimann.api_editor.mutable_model.PythonConstructor import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonPackage +import com.larsreimann.api_editor.mutable_model.PythonParameter +import com.larsreimann.modeling.ModelNode import com.larsreimann.modeling.descendants /** @@ -27,16 +29,20 @@ fun PythonPackage.removeEmptyModules() { */ fun PythonPackage.reorderParameters() { this.descendants() - .filterIsInstance() - .forEach { it.reorderParameters() } + .forEach { + when (it) { + is PythonConstructor -> it.parameters.reorderParameters() + is PythonFunction -> it.parameters.reorderParameters() + } + } } -private fun PythonFunction.reorderParameters() { - val groups = this.parameters.groupBy { it.assignedBy } - this.parameters.addAll(groups[PythonParameterAssignment.IMPLICIT].orEmpty()) - this.parameters.addAll(groups[PythonParameterAssignment.POSITION_ONLY].orEmpty()) - this.parameters.addAll(groups[PythonParameterAssignment.POSITION_OR_NAME].orEmpty()) - this.parameters.addAll(groups[PythonParameterAssignment.NAME_ONLY].orEmpty()) +private fun ModelNode.MutableContainmentList.reorderParameters() { + val groups = this.groupBy { it.assignedBy } + this.addAll(groups[PythonParameterAssignment.IMPLICIT].orEmpty()) + this.addAll(groups[PythonParameterAssignment.POSITION_ONLY].orEmpty()) + this.addAll(groups[PythonParameterAssignment.POSITION_OR_NAME].orEmpty()) + this.addAll(groups[PythonParameterAssignment.NAME_ONLY].orEmpty()) } /** diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessorTest.kt index c619b1603..c262129f7 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessorTest.kt @@ -88,10 +88,10 @@ class ParameterAnnotationProcessorTest { val attributes = testClass.attributes attributes.shouldHaveSize(1) - attributes[0] shouldBe attributes[0].copy( - name = "testParameter", - value = "True" - ) + attributes[0].asClue { + it.name shouldBe "testParameter" + it.value shouldBe "True" + } } @Test diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PostprocessorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PostprocessorTest.kt index 3437fefef..a5f530cc0 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PostprocessorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PostprocessorTest.kt @@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test class PostprocessorTest { private lateinit var testFunction: PythonFunction + private lateinit var testConstructor: PythonConstructor private lateinit var testConstructorParameter: PythonParameter private lateinit var testClass: PythonClass private lateinit var testModule: PythonModule @@ -38,21 +39,22 @@ class PostprocessorTest { @BeforeEach fun reset() { testFunction = PythonFunction(name = "testFunction") + testConstructor = PythonConstructor( + parameters = listOf( + PythonParameter( + name = "self", + assignedBy = PythonParameterAssignment.IMPLICIT + ), + PythonParameter( + name = "positionOrName", + assignedBy = PythonParameterAssignment.POSITION_OR_NAME + ) + ) + ) testConstructorParameter = PythonParameter(name = "constructorParameter") testClass = PythonClass( name = "TestClass", - constructor = PythonConstructor( - parameters = listOf( - PythonParameter( - name = "self", - assignedBy = PythonParameterAssignment.IMPLICIT - ), - PythonParameter( - name = "positionOrName", - assignedBy = PythonParameterAssignment.POSITION_OR_NAME - ) - ) - ), + constructor = testConstructor, methods = listOf( PythonFunction( name = "__init__", @@ -105,7 +107,44 @@ class PostprocessorTest { inner class ReorderParameters { @Test - fun `should reorder parameters`() { + fun `should reorder parameters of constructors`() { + val implicit = PythonParameter( + name = "implicit", + assignedBy = PythonParameterAssignment.IMPLICIT + ) + val positionOnly = PythonParameter( + name = "positionOnly", + assignedBy = PythonParameterAssignment.POSITION_ONLY + ) + val positionOrName = PythonParameter( + name = "positionOrName", + assignedBy = PythonParameterAssignment.POSITION_OR_NAME + ) + val nameOnly = PythonParameter( + name = "nameOnly", + assignedBy = PythonParameterAssignment.NAME_ONLY + ) + + testConstructor.parameters.clear() + testConstructor.parameters += listOf( + nameOnly, + positionOrName, + positionOnly, + implicit + ) + + testPackage.reorderParameters() + + testConstructor.parameters.shouldContainExactly( + implicit, + positionOnly, + positionOrName, + nameOnly + ) + } + + @Test + fun `should reorder parameters of functions`() { val implicit = PythonParameter( name = "implicit", assignedBy = PythonParameterAssignment.IMPLICIT @@ -133,12 +172,10 @@ class PostprocessorTest { testPackage.reorderParameters() testFunction.parameters.shouldContainExactly( - listOf( - implicit, - positionOnly, - positionOrName, - nameOnly - ) + implicit, + positionOnly, + positionOrName, + nameOnly ) } } From bf11b352e8bd745d1f86683c4db95e97e3269573 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 10 Jan 2022 14:17:15 +0100 Subject: [PATCH 08/32] fix: usage of CrossReference vs. ContainmentReference --- .../api_editor/mutable_model/PythonAst.kt | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt index 5d049d1c7..dcef2b406 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt @@ -167,7 +167,7 @@ class PythonAttribute( override val annotations: MutableList = mutableListOf(), ) : PythonDeclaration() { - var type by CrossReference(type) + var type by ContainmentReference(type) override fun children() = sequence { type?.let { yield(it) } @@ -184,7 +184,7 @@ class PythonParameter( override val annotations: MutableList = mutableListOf(), ) : PythonDeclaration() { - var type by CrossReference(type) + var type by ContainmentReference(type) override fun children() = sequence { type?.let { yield(it) } @@ -203,7 +203,7 @@ class PythonResult( override val annotations: MutableList = mutableListOf(), ) : PythonDeclaration() { - var type by CrossReference(type) + var type by ContainmentReference(type) override fun children() = sequence { type?.let { yield(it) } @@ -252,10 +252,6 @@ class PythonMemberAccess( class PythonReference(declaration: PythonDeclaration) : PythonExpression() { var declaration by CrossReference(declaration) - - override fun children() = sequence { - declaration?.let { yield(it) } - } } sealed class PythonLiteral : PythonExpression() @@ -273,10 +269,6 @@ sealed class PythonType : PythonAstNode() class PythonNamedType(declaration: PythonDeclaration) : PythonType() { var declaration by CrossReference(declaration) - - override fun children() = sequence { - declaration?.let { yield(it) } - } } data class PythonStringifiedType(val type: String) : PythonType() From b3d3ce83c7bb85f3f332a9ea09e55fd6d587d7fa Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 10 Jan 2022 16:20:32 +0100 Subject: [PATCH 09/32] refactor: replace strings in model with richer types --- .../api_editor/codegen/PythonCodeGenerator.kt | 24 ++- .../api_editor/codegen/StubCodeGenerator.kt | 26 ++-- .../mutable_model/ConverterToMutableModel.kt | 11 +- .../api_editor/mutable_model/PythonAst.kt | 142 ++++++++++-------- .../transformation/EnumAnnotationProcessor.kt | 12 +- .../ParameterAnnotationProcessor.kt | 5 +- .../transformation/Postprocessor.kt | 31 +++- .../api_editor/transformation/Preprocessor.kt | 13 +- .../codegen/PythonCodeGeneratorTest.kt | 109 ++++++++------ .../codegen/StubCodeGeneratorTest.kt | 52 +++++-- .../EnumAnnotationProcessorTest.kt | 81 ++++++++-- .../GroupAnnotationProcessorTest.kt | 3 +- .../ParameterAnnotationProcessorTest.kt | 11 +- .../transformation/PostprocessorTest.kt | 7 +- .../transformation/PreprocessorTest.kt | 8 +- 15 files changed, 346 insertions(+), 189 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt index 845d7ccff..6bf557b50 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt @@ -11,6 +11,7 @@ import com.larsreimann.api_editor.mutable_model.PythonCall import com.larsreimann.api_editor.mutable_model.PythonClass import com.larsreimann.api_editor.mutable_model.PythonConstructor import com.larsreimann.api_editor.mutable_model.PythonEnum +import com.larsreimann.api_editor.mutable_model.PythonEnumInstance import com.larsreimann.api_editor.mutable_model.PythonExpression import com.larsreimann.api_editor.mutable_model.PythonFloat import com.larsreimann.api_editor.mutable_model.PythonFunction @@ -21,6 +22,7 @@ import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference import com.larsreimann.api_editor.mutable_model.PythonString +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import com.larsreimann.api_editor.mutable_model.PythonStringifiedType import com.larsreimann.api_editor.mutable_model.PythonType @@ -54,9 +56,10 @@ fun PythonModule.toPythonCode(): String { private fun buildNamespace(pythonModule: PythonModule): String { val importedModules = HashSet() pythonModule.functions.forEach { pythonFunction: PythonFunction -> - importedModules.add( - buildParentDeclarationName(pythonFunction.callToOriginalAPI!!.receiver) - ) + val receiver = pythonFunction.callToOriginalAPI?.receiver + if (receiver is PythonStringifiedExpression) { + importedModules.add(buildParentDeclarationName(receiver.string)) + } } pythonModule.classes.forEach { pythonClass: PythonClass -> if (pythonClass.originalClass != null) { @@ -357,7 +360,7 @@ internal fun PythonEnum.toPythonCode() = buildString { append("pass") } else { instances.forEach { - append("${it.name} = \"${it.value}\"") + append(it.toPythonCode()) if (it != instances.last()) { appendLine(",") } @@ -366,15 +369,20 @@ internal fun PythonEnum.toPythonCode() = buildString { } } +internal fun PythonEnumInstance.toPythonCode(): String { + return "$name = ${value!!.toPythonCode()}" +} + internal fun PythonExpression.toPythonCode(): String { return when (this) { is PythonBoolean -> value.toString().replaceFirstChar { it.uppercase() } - is PythonCall -> "$receiver(${arguments.joinToString { it.toPythonCode() }})" + is PythonCall -> "${receiver!!.toPythonCode()}(${arguments.joinToString { it.toPythonCode() }})" is PythonFloat -> value.toString() is PythonInt -> value.toString() is PythonMemberAccess -> "${receiver!!.toPythonCode()}.${member!!.toPythonCode()}" is PythonReference -> declaration!!.name is PythonString -> "'$value'" + is PythonStringifiedExpression -> string } } @@ -385,10 +393,10 @@ internal fun PythonParameter.toPythonCode() = buildString { if (typeStringOrNull != null) { append(": $typeStringOrNull") if (defaultValue != null) { - append(" = $defaultValue") + append(" = ${defaultValue!!.toPythonCode()}") } } else if (defaultValue != null) { - append("=$defaultValue") + append("=${defaultValue!!.toPythonCode()}") } } @@ -396,7 +404,7 @@ internal fun PythonType?.toPythonCodeOrNull(): String? { return when (this) { is PythonNamedType -> this.declaration?.name is PythonStringifiedType -> { - when (this.type) { + when (this.string) { "bool" -> "bool" "float" -> "float" "int" -> "int" diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/StubCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/StubCodeGenerator.kt index 10b0c34ca..2dcd2f247 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/StubCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/StubCodeGenerator.kt @@ -5,11 +5,13 @@ import com.larsreimann.api_editor.mutable_model.PythonAttribute import com.larsreimann.api_editor.mutable_model.PythonClass import com.larsreimann.api_editor.mutable_model.PythonEnum import com.larsreimann.api_editor.mutable_model.PythonEnumInstance +import com.larsreimann.api_editor.mutable_model.PythonExpression import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonModule import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonResult +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import com.larsreimann.api_editor.mutable_model.PythonStringifiedType import com.larsreimann.api_editor.mutable_model.PythonType import de.unibonn.simpleml.constant.SmlFileExtension @@ -269,7 +271,7 @@ internal fun PythonType?.toSmlType(): SmlAbstractType { ) } is PythonStringifiedType -> { - when (this.type) { + when (this.string) { "bool" -> createSmlNamedType( declaration = createSmlClass("Boolean") ) @@ -297,16 +299,20 @@ internal fun PythonType?.toSmlType(): SmlAbstractType { // Value conversions --------------------------------------------------------------------------------------------------- -internal fun String.toSmlExpression(): SmlAbstractExpression? { +internal fun PythonExpression.toSmlExpression(): SmlAbstractExpression? { + if (this !is PythonStringifiedExpression) { + return createSmlString("###invalid###$this###") + } + return when { - isBlank() -> null - this == "False" -> createSmlBoolean(false) - this == "True" -> createSmlBoolean(true) - this == "None" -> createSmlNull() - isIntLiteral() -> createSmlInt(toInt()) - isFloatLiteral() -> createSmlFloat(toDouble()) - isStringLiteral() -> createSmlString(substring(1, length - 1)) - else -> createSmlString("###invalid###$this###") + string.isBlank() -> null + string == "False" -> createSmlBoolean(false) + string == "True" -> createSmlBoolean(true) + string == "None" -> createSmlNull() + string.isIntLiteral() -> createSmlInt(string.toInt()) + string.isFloatLiteral() -> createSmlFloat(string.toDouble()) + string.isStringLiteral() -> createSmlString(string.substring(1, string.length - 1)) + else -> createSmlString("###invalid###$string###") } } diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterToMutableModel.kt b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterToMutableModel.kt index eeb5974c1..cf2980c18 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterToMutableModel.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/ConverterToMutableModel.kt @@ -36,12 +36,11 @@ fun convertClass(pythonClass: SerializablePythonClass): PythonClass { return PythonClass( name = pythonClass.name, decorators = pythonClass.decorators.toMutableList(), - superclasses = pythonClass.superclasses.toMutableList(), + superclasses = pythonClass.superclasses.map { PythonNamedType(PythonClass(name = it)) }.toMutableList(), attributes = pythonClass.attributes.map { convertAttribute(it) }, methods = pythonClass.methods.map { convertFunction(it) }, isPublic = pythonClass.isPublic, description = pythonClass.description, - fullDocstring = pythonClass.fullDocstring, annotations = pythonClass.annotations ) } @@ -57,7 +56,7 @@ fun convertEnum(pythonEnum: SerializablePythonEnum): PythonEnum { fun convertEnumInstance(pythonEnumInstance: SerializablePythonEnumInstance): PythonEnumInstance { return PythonEnumInstance( name = pythonEnumInstance.name, - value = pythonEnumInstance.value, + value = PythonStringifiedExpression(pythonEnumInstance.value), description = pythonEnumInstance.description, annotations = mutableListOf() ) @@ -71,8 +70,6 @@ fun convertFunction(pythonFunction: SerializablePythonFunction): PythonFunction results = pythonFunction.results.map { convertResult(it) }, isPublic = pythonFunction.isPublic, description = pythonFunction.description, - fullDocstring = pythonFunction.fullDocstring, -// calledAfter = pythonFunction.calledAfter, isPure = pythonFunction.isPure, annotations = pythonFunction.annotations, ) @@ -82,7 +79,7 @@ fun convertAttribute(pythonAttribute: SerializablePythonAttribute): PythonAttrib return PythonAttribute( name = pythonAttribute.name, type = PythonStringifiedType(pythonAttribute.typeInDocs), - value = pythonAttribute.defaultValue, + value = pythonAttribute.defaultValue?.let { PythonStringifiedExpression(it) }, isPublic = pythonAttribute.isPublic, description = pythonAttribute.description, boundary = pythonAttribute.boundary, @@ -94,7 +91,7 @@ fun convertParameter(pythonParameter: SerializablePythonParameter): PythonParame return PythonParameter( name = pythonParameter.name, type = PythonStringifiedType(pythonParameter.typeInDocs), - defaultValue = pythonParameter.defaultValue, + defaultValue = pythonParameter.defaultValue?.let { PythonStringifiedExpression(it) }, assignedBy = pythonParameter.assignedBy, description = pythonParameter.description, boundary = pythonParameter.boundary, diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt index dcef2b406..80728fefb 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt @@ -7,7 +7,6 @@ import com.larsreimann.api_editor.model.EditorAnnotation import com.larsreimann.api_editor.model.PythonFromImport import com.larsreimann.api_editor.model.PythonImport import com.larsreimann.api_editor.model.PythonParameterAssignment -import com.larsreimann.api_editor.model.SerializablePythonFunction import com.larsreimann.modeling.ModelNode import com.larsreimann.modeling.ancestorsOrSelf @@ -35,61 +34,45 @@ sealed class PythonDeclaration : PythonAstNode() { } } -class PythonPackage( - var distribution: String, - override var name: String, - var version: String, - modules: List = emptyList(), - override val annotations: MutableList = mutableListOf() -) : PythonDeclaration() { - - val modules = MutableContainmentList(modules) - - override fun children() = sequence { - yieldAll(modules) - } -} - -class PythonModule( +class PythonAttribute( override var name: String, - val imports: MutableList = mutableListOf(), - val fromImports: MutableList = mutableListOf(), - classes: List = emptyList(), - enums: List = emptyList(), - functions: List = emptyList(), - override val annotations: MutableList = mutableListOf() + type: PythonType? = null, + value: PythonExpression? = null, + var isPublic: Boolean = true, + var description: String = "", + var boundary: Boundary? = null, + override val annotations: MutableList = mutableListOf(), ) : PythonDeclaration() { - val classes = MutableContainmentList(classes) - val enums = MutableContainmentList(enums) - val functions = MutableContainmentList(functions) + var type by ContainmentReference(type) + var value by ContainmentReference(value) override fun children() = sequence { - yieldAll(classes) - yieldAll(enums) - yieldAll(functions) + type?.let { yield(it) } + value?.let { yield(it) } } } class PythonClass( override var name: String, val decorators: MutableList = mutableListOf(), - val superclasses: MutableList = mutableListOf(), + superclasses: MutableList = mutableListOf(), constructor: PythonConstructor? = null, attributes: List = emptyList(), methods: List = emptyList(), var isPublic: Boolean = true, var description: String = "", - var fullDocstring: String = "", override val annotations: MutableList = mutableListOf(), var originalClass: OriginalPythonClass? = null ) : PythonDeclaration() { + val superclasses = MutableContainmentList(superclasses) var constructor by ContainmentReference(constructor) val attributes = MutableContainmentList(attributes) val methods = MutableContainmentList(methods) override fun children() = sequence { + yieldAll(superclasses) constructor?.let { yield(it) } yieldAll(attributes) yieldAll(methods) @@ -108,8 +91,6 @@ class PythonConstructor( } } -data class OriginalPythonClass(val qualifiedName: String) - class PythonEnum( override var name: String, instances: List = emptyList(), @@ -124,12 +105,19 @@ class PythonEnum( } } -data class PythonEnumInstance( +class PythonEnumInstance( override var name: String, - val value: String = name, + value: PythonExpression = PythonString(name), var description: String = "", override val annotations: MutableList = mutableListOf() -) : PythonDeclaration() +) : PythonDeclaration() { + + var value by ContainmentReference(value) + + override fun children() = sequence { + value?.let { yield(it) } + } +} class PythonFunction( override var name: String, @@ -138,10 +126,8 @@ class PythonFunction( results: List = emptyList(), var isPublic: Boolean = true, var description: String = "", - var fullDocstring: String = "", var isPure: Boolean = false, override val annotations: MutableList = mutableListOf(), - val calledAfter: MutableList = mutableListOf(), var callToOriginalAPI: PythonCall? = null ) : PythonDeclaration() { @@ -157,27 +143,46 @@ class PythonFunction( fun isStaticMethod() = isMethod() && "staticmethod" in decorators } -class PythonAttribute( +class PythonModule( override var name: String, - type: PythonType? = null, - var value: String? = null, - var isPublic: Boolean = true, - var description: String = "", - var boundary: Boundary? = null, - override val annotations: MutableList = mutableListOf(), + val imports: MutableList = mutableListOf(), + val fromImports: MutableList = mutableListOf(), + classes: List = emptyList(), + enums: List = emptyList(), + functions: List = emptyList(), + override val annotations: MutableList = mutableListOf() ) : PythonDeclaration() { - var type by ContainmentReference(type) + val classes = MutableContainmentList(classes) + val enums = MutableContainmentList(enums) + val functions = MutableContainmentList(functions) override fun children() = sequence { - type?.let { yield(it) } + yieldAll(classes) + yieldAll(enums) + yieldAll(functions) + } +} + +class PythonPackage( + var distribution: String, + override var name: String, + var version: String, + modules: List = emptyList(), + override val annotations: MutableList = mutableListOf() +) : PythonDeclaration() { + + val modules = MutableContainmentList(modules) + + override fun children() = sequence { + yieldAll(modules) } } class PythonParameter( override var name: String, type: PythonType? = null, - var defaultValue: String? = null, + defaultValue: PythonExpression? = null, var assignedBy: PythonParameterAssignment = PythonParameterAssignment.POSITION_OR_NAME, var description: String = "", var boundary: Boundary? = null, @@ -185,9 +190,11 @@ class PythonParameter( ) : PythonDeclaration() { var type by ContainmentReference(type) + var defaultValue by ContainmentReference(defaultValue) override fun children() = sequence { type?.let { yield(it) } + defaultValue?.let { yield(it) } } fun isRequired() = defaultValue == null @@ -210,6 +217,9 @@ class PythonResult( } } +data class OriginalPythonClass(val qualifiedName: String) + + /* ******************************************************************************************************************** * Expressions * ********************************************************************************************************************/ @@ -217,24 +227,25 @@ class PythonResult( sealed class PythonExpression : PythonAstNode() class PythonCall( - val receiver: String, + receiver: PythonExpression, arguments: List = emptyList() ) : PythonExpression() { + var receiver by ContainmentReference(receiver) val arguments = MutableContainmentList(arguments) override fun children() = sequence { + receiver?.let { yield(it) } yieldAll(arguments) } } -class PythonArgument(val name: String? = null, value: PythonExpression) : PythonAstNode() { - var value by ContainmentReference(value) +sealed class PythonLiteral : PythonExpression() - override fun children() = sequence { - value?.let { yield(it) } - } -} +data class PythonBoolean(val value: Boolean) : PythonLiteral() +data class PythonFloat(val value: Double) : PythonLiteral() +data class PythonInt(val value: Int) : PythonLiteral() +data class PythonString(val value: String) : PythonLiteral() class PythonMemberAccess( receiver: PythonExpression, @@ -254,12 +265,8 @@ class PythonReference(declaration: PythonDeclaration) : PythonExpression() { var declaration by CrossReference(declaration) } -sealed class PythonLiteral : PythonExpression() +data class PythonStringifiedExpression(val string: String) : PythonExpression() -data class PythonBoolean(val value: Boolean) : PythonLiteral() -data class PythonFloat(val value: Double) : PythonLiteral() -data class PythonInt(val value: Int) : PythonLiteral() -data class PythonString(val value: String) : PythonLiteral() /* ******************************************************************************************************************** * Types @@ -271,4 +278,17 @@ class PythonNamedType(declaration: PythonDeclaration) : PythonType() { var declaration by CrossReference(declaration) } -data class PythonStringifiedType(val type: String) : PythonType() +data class PythonStringifiedType(val string: String) : PythonType() + + +/* ******************************************************************************************************************** + * Other + * ********************************************************************************************************************/ + +class PythonArgument(val name: String? = null, value: PythonExpression) : PythonAstNode() { + var value by ContainmentReference(value) + + override fun children() = sequence { + value?.let { yield(it) } + } +} diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessor.kt index 354c37aa7..c3714c2d4 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessor.kt @@ -11,6 +11,7 @@ import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference +import com.larsreimann.api_editor.mutable_model.PythonString import com.larsreimann.api_editor.transformation.processing_exceptions.ConflictingEnumException import com.larsreimann.modeling.closest import com.larsreimann.modeling.descendants @@ -39,7 +40,7 @@ private fun PythonParameter.processEnumAnnotations(module: PythonModule) { annotation.pairs.map { enumPair -> PythonEnumInstance( enumPair.instanceName, - enumPair.stringValue + PythonString(enumPair.stringValue) ) } ) @@ -78,11 +79,12 @@ private fun hasConflictingEnums( moduleEnums: List, enumToCheck: PythonEnum ): Boolean { - return moduleEnums.any { - (enumToCheck.name == it.name) && + return moduleEnums.any { enum -> + (enumToCheck.name == enum.name) && ( - enumToCheck.instances.size != it.instances.size || - !enumToCheck.instances.containsAll(it.instances) + enumToCheck.instances.size != enum.instances.size || + !enumToCheck.instances.mapNotNull { (it.value as? PythonString)?.value} + .containsAll(enum.instances.mapNotNull { (it.value as? PythonString)?.value}) ) } } diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessor.kt index aeb17f824..d09f3f8ab 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessor.kt @@ -20,6 +20,7 @@ import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference import com.larsreimann.api_editor.mutable_model.PythonString +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import com.larsreimann.modeling.closest import com.larsreimann.modeling.descendants @@ -54,7 +55,7 @@ private fun PythonParameter.processAttributeAnnotation(annotation: AttributeAnno containingClass.attributes += PythonAttribute( name = name, type = type, - value = annotation.defaultValue.toString(), + value = PythonStringifiedExpression(annotation.defaultValue.toString()), isPublic = true, description = description, boundary = boundary @@ -96,7 +97,7 @@ private fun PythonParameter.processConstantAnnotation(annotation: ConstantAnnota private fun PythonParameter.processOptionalAnnotation(annotation: OptionalAnnotation) { this.assignedBy = PythonParameterAssignment.NAME_ONLY - this.defaultValue = annotation.defaultValue.toString() + this.defaultValue = PythonStringifiedExpression(annotation.defaultValue.toString()) this.annotations.remove(annotation) } diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt index 76531d64c..9aa2c47f0 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt @@ -8,8 +8,10 @@ import com.larsreimann.api_editor.mutable_model.PythonConstructor import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import com.larsreimann.modeling.ModelNode import com.larsreimann.modeling.descendants +import java.lang.IllegalStateException /** * Removes modules that don't contain declarations. @@ -61,18 +63,31 @@ private fun PythonClass.createConstructor() { if (this.originalClass != null) { this.constructor = PythonConstructor( parameters = emptyList(), - callToOriginalAPI = PythonCall(receiver = this.originalClass!!.qualifiedName) + callToOriginalAPI = PythonCall( + receiver = PythonStringifiedExpression(this.originalClass!!.qualifiedName) + ) ) } } else -> { - this.constructor = PythonConstructor( - parameters = constructorMethod.parameters.toList(), - callToOriginalAPI = PythonCall( - receiver = constructorMethod.callToOriginalAPI!!.receiver.removeSuffix(".__init__"), - arguments = constructorMethod.callToOriginalAPI!!.arguments.toList() + constructorMethod.callToOriginalAPI?.let { callToOriginalAPI -> + val newReceiver = when (val receiver = callToOriginalAPI.receiver) { + is PythonStringifiedExpression -> PythonStringifiedExpression( + receiver.string.removeSuffix(".__init__") + ) + null -> throw IllegalStateException("Receiver of call is null: $callToOriginalAPI") + else -> receiver + } + + this.constructor = PythonConstructor( + parameters = constructorMethod.parameters.toList(), + callToOriginalAPI = PythonCall( + receiver = newReceiver, + arguments = callToOriginalAPI.arguments.toList() + ) ) - ) + } + constructorMethod.release() } @@ -96,7 +111,7 @@ private fun PythonClass.createAttributesForParametersOfConstructor() { this.attributes += PythonAttribute( name = it.name, type = it.type, - value = it.name, + value = PythonStringifiedExpression(it.name), isPublic = true, description = it.description, boundary = it.boundary diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Preprocessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Preprocessor.kt index 66856ec67..96abf2cbb 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Preprocessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Preprocessor.kt @@ -10,6 +10,7 @@ import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import com.larsreimann.modeling.closest import com.larsreimann.modeling.descendants @@ -66,11 +67,13 @@ private fun PythonClass.addOriginalDeclarations() { private fun PythonFunction.addOriginalDeclarations() { val containingClass = closest() this.callToOriginalAPI = PythonCall( - receiver = when { - name == "__init__" && containingClass != null -> containingClass.originalClass!!.qualifiedName - isMethod() -> "self.instance.$name" - else -> qualifiedName() - }, + receiver = PythonStringifiedExpression( + when { + name == "__init__" && containingClass != null -> containingClass.originalClass!!.qualifiedName + isMethod() -> "self.instance.$name" + else -> qualifiedName() + } + ), arguments = this.parameters .filter { !it.isImplicit() } .map { diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt index 6a952f982..660c9e083 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt @@ -24,6 +24,7 @@ import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference import com.larsreimann.api_editor.mutable_model.PythonResult import com.larsreimann.api_editor.mutable_model.PythonString +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import com.larsreimann.api_editor.mutable_model.PythonStringifiedType import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.shouldBe @@ -36,7 +37,7 @@ class PythonCodeGeneratorTest { // given val testMethodParameter = PythonParameter( name = "only-param", - defaultValue = "'defaultValue'", + defaultValue = PythonStringifiedExpression("'defaultValue'"), assignedBy = PythonParameterAssignment.NAME_ONLY ) val testClass = PythonClass( @@ -52,7 +53,7 @@ class PythonCodeGeneratorTest { testMethodParameter ), callToOriginalAPI = PythonCall( - receiver = "self.instance.test-class-function", + receiver = PythonStringifiedExpression("self.instance.test-class-function"), arguments = listOf( PythonArgument( value = PythonReference(testMethodParameter) @@ -68,7 +69,7 @@ class PythonCodeGeneratorTest { val testFunction1Parameter3 = PythonParameter(name = "param3") val testFunction2Parameter = PythonParameter( name = "test-parameter", - defaultValue = "42", + defaultValue = PythonStringifiedExpression("42"), assignedBy = PythonParameterAssignment.NAME_ONLY ) val testModule = PythonModule( @@ -89,7 +90,7 @@ class PythonCodeGeneratorTest { ) ), callToOriginalAPI = PythonCall( - receiver = "test-module.function_module", + receiver = PythonStringifiedExpression("test-module.function_module"), arguments = listOf( PythonArgument( name = "param1", @@ -119,7 +120,7 @@ class PythonCodeGeneratorTest { ) ), callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", + receiver = PythonStringifiedExpression("test-module.test-function"), arguments = listOf( PythonArgument( name = "test-parameter", @@ -162,7 +163,7 @@ class PythonCodeGeneratorTest { val testFunction1Parameter3 = PythonParameter(name = "param3") val testFunction2Parameter = PythonParameter( "test-parameter", - defaultValue = "42", + defaultValue = PythonStringifiedExpression("42"), assignedBy = PythonParameterAssignment.NAME_ONLY ) val testModule = PythonModule( @@ -183,7 +184,7 @@ class PythonCodeGeneratorTest { ) ), callToOriginalAPI = PythonCall( - receiver = "test-module.function_module", + receiver = PythonStringifiedExpression("test-module.function_module"), arguments = listOf( PythonArgument( name = "param1", @@ -213,7 +214,7 @@ class PythonCodeGeneratorTest { ) ), callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", + receiver = PythonStringifiedExpression("test-module.test-function"), arguments = listOf( PythonArgument( name = "test-parameter", @@ -250,7 +251,9 @@ class PythonCodeGeneratorTest { val testClass = PythonClass( name = "test-class", constructor = PythonConstructor( - callToOriginalAPI = PythonCall(receiver = "test-module.test-class") + callToOriginalAPI = PythonCall( + receiver = PythonStringifiedExpression("test-module.test-class") + ) ), originalClass = OriginalPythonClass("test-module.test-class") ) @@ -326,7 +329,7 @@ class PythonCodeGeneratorTest { // given val testParameter1 = PythonParameter( name = "param1", - defaultValue = "5", + defaultValue = PythonStringifiedExpression("5"), assignedBy = PythonParameterAssignment.NAME_ONLY ) testParameter1.boundary = Boundary( @@ -338,7 +341,7 @@ class PythonCodeGeneratorTest { ) val testParameter2 = PythonParameter( "param2", - defaultValue = "5", + defaultValue = PythonStringifiedExpression("5"), assignedBy = PythonParameterAssignment.NAME_ONLY ) testParameter2.boundary = Boundary( @@ -350,7 +353,7 @@ class PythonCodeGeneratorTest { ) val testParameter3 = PythonParameter( "param3", - defaultValue = "5", + defaultValue = PythonStringifiedExpression("5"), assignedBy = PythonParameterAssignment.NAME_ONLY ) testParameter3.boundary = Boundary( @@ -364,7 +367,7 @@ class PythonCodeGeneratorTest { name = "function_module", parameters = mutableListOf(testParameter1, testParameter2, testParameter3), callToOriginalAPI = PythonCall( - receiver = "test-module.function_module", + receiver = PythonStringifiedExpression("test-module.function_module"), arguments = listOf( PythonArgument( name = "param1", @@ -415,7 +418,7 @@ class PythonCodeGeneratorTest { // given val testParameter = PythonParameter( name = "param1", - defaultValue = "5", + defaultValue = PythonStringifiedExpression("5"), assignedBy = PythonParameterAssignment.NAME_ONLY ) testParameter.boundary = Boundary( @@ -429,7 +432,7 @@ class PythonCodeGeneratorTest { name = "function_module", parameters = listOf(testParameter), callToOriginalAPI = PythonCall( - receiver = "test-module.function_module", + receiver = PythonStringifiedExpression("test-module.function_module"), arguments = listOf( PythonArgument( name = "param1", @@ -467,7 +470,7 @@ class PythonCodeGeneratorTest { name = "TestClass", constructor = PythonConstructor( callToOriginalAPI = PythonCall( - receiver = "testModule.TestClass" + receiver = PythonStringifiedExpression("testModule.TestClass") ) ) ) @@ -484,7 +487,7 @@ class PythonCodeGeneratorTest { // given val testParameter = PythonParameter( name = "only-param", - defaultValue = "'defaultValue'", + defaultValue = PythonStringifiedExpression("'defaultValue'"), assignedBy = PythonParameterAssignment.NAME_ONLY ) val testClass = PythonClass( @@ -498,7 +501,7 @@ class PythonCodeGeneratorTest { testParameter ), callToOriginalAPI = PythonCall( - receiver = "test-module.test-class", + receiver = PythonStringifiedExpression("test-module.test-class"), arguments = listOf( PythonArgument( value = PythonReference(testParameter) @@ -546,7 +549,7 @@ class PythonCodeGeneratorTest { testMethod1Parameter ), callToOriginalAPI = PythonCall( - receiver = "self.instance.test-class-function1", + receiver = PythonStringifiedExpression("self.instance.test-class-function1"), arguments = listOf( PythonArgument(value = PythonReference(testMethod1Parameter)) ) @@ -562,7 +565,7 @@ class PythonCodeGeneratorTest { testMethod2Parameter ), callToOriginalAPI = PythonCall( - receiver = "self.instance.test-class-function2", + receiver = PythonStringifiedExpression("self.instance.test-class-function2"), arguments = listOf( PythonArgument(value = PythonReference(testMethod2Parameter)) ) @@ -603,7 +606,7 @@ class PythonCodeGeneratorTest { testParameter2 ), callToOriginalAPI = PythonCall( - receiver = "self.instance.test-function", + receiver = PythonStringifiedExpression("self.instance.test-function"), arguments = listOf( PythonArgument(value = PythonReference(testParameter1)), PythonArgument( @@ -635,7 +638,9 @@ class PythonCodeGeneratorTest { // given val testFunction = PythonFunction( name = "test-function", - callToOriginalAPI = PythonCall(receiver = "test-module.test-function") + callToOriginalAPI = PythonCall( + receiver = PythonStringifiedExpression("test-module.test-function") + ) ) // when @@ -654,7 +659,7 @@ class PythonCodeGeneratorTest { // given val testParameter = PythonParameter( name = "only-param", - defaultValue = "13", + defaultValue = PythonStringifiedExpression("13"), assignedBy = PythonParameterAssignment.NAME_ONLY ) val testFunction = PythonFunction( @@ -663,7 +668,7 @@ class PythonCodeGeneratorTest { testParameter ), callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", + receiver = PythonStringifiedExpression("test-module.test-function"), arguments = listOf( PythonArgument(value = PythonReference(testParameter)) ) @@ -686,7 +691,7 @@ class PythonCodeGeneratorTest { // given val testParameter = PythonParameter( name = "only-param", - defaultValue = "False", + defaultValue = PythonStringifiedExpression("False"), assignedBy = PythonParameterAssignment.NAME_ONLY ) val testFunction = PythonFunction( @@ -695,7 +700,7 @@ class PythonCodeGeneratorTest { testParameter ), callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", + receiver = PythonStringifiedExpression("test-module.test-function"), arguments = listOf( PythonArgument(value = PythonReference(testParameter)) ) @@ -723,7 +728,7 @@ class PythonCodeGeneratorTest { testParameter ), callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", + receiver = PythonStringifiedExpression("test-module.test-function"), arguments = listOf( PythonArgument( name = "only-param", @@ -757,7 +762,7 @@ class PythonCodeGeneratorTest { testParameter2 ), callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", + receiver = PythonStringifiedExpression("test-module.test-function"), arguments = listOf( PythonArgument(value = PythonReference(testParameter1)), PythonArgument(value = PythonReference(testParameter2)) @@ -790,7 +795,7 @@ class PythonCodeGeneratorTest { testParameter3 ), callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", + receiver = PythonStringifiedExpression("test-module.test-function"), arguments = listOf( PythonArgument(value = PythonReference(testParameter1)), PythonArgument(value = PythonReference(testParameter2)), @@ -825,7 +830,7 @@ class PythonCodeGeneratorTest { testParameter2 ), callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", + receiver = PythonStringifiedExpression("test-module.test-function"), arguments = listOf( PythonArgument(value = PythonReference(testParameter1)), PythonArgument( @@ -859,7 +864,7 @@ class PythonCodeGeneratorTest { testParameter2 ), callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", + receiver = PythonStringifiedExpression("test-module.test-function"), arguments = listOf( PythonArgument( value = PythonReference(testParameter1) @@ -899,7 +904,7 @@ class PythonCodeGeneratorTest { testParameter3 ), callToOriginalAPI = PythonCall( - receiver = "test-module.test-function", + receiver = PythonStringifiedExpression("test-module.test-function"), arguments = listOf( PythonArgument(value = PythonReference(testParameter1)), PythonArgument(value = PythonReference(testParameter2)), @@ -934,7 +939,7 @@ class PythonCodeGeneratorTest { decorators = mutableListOf("staticmethod"), parameters = listOf(testParameter), callToOriginalAPI = PythonCall( - receiver = "test-module.test-class.test-class-function1", + receiver = PythonStringifiedExpression("test-module.test-class.test-class-function1"), arguments = listOf( PythonArgument(value = PythonReference(testParameter)) ) @@ -998,7 +1003,7 @@ class PythonCodeGeneratorTest { testParameter ), callToOriginalAPI = PythonCall( - receiver = "testModule.testFunction", + receiver = PythonStringifiedExpression("testModule.testFunction"), arguments = listOf( PythonArgument( value = PythonMemberAccess( @@ -1025,7 +1030,7 @@ class PythonCodeGeneratorTest { testParameter ), callToOriginalAPI = PythonCall( - receiver = "testModule.testFunction", + receiver = PythonStringifiedExpression("testModule.testFunction"), arguments = listOf( PythonArgument( value = PythonMemberAccess( @@ -1081,7 +1086,7 @@ class PythonCodeGeneratorTest { fun `should handle parameters without type but with default value`() { val testParameter = PythonParameter( name = "param", - defaultValue = "1" + defaultValue = PythonStringifiedExpression("1") ) testParameter.toPythonCode() shouldBe "param=1" @@ -1092,7 +1097,7 @@ class PythonCodeGeneratorTest { val testParameter = PythonParameter( name = "param", type = PythonStringifiedType("int"), - defaultValue = "1" + defaultValue = PythonStringifiedExpression("1") ) testParameter.toPythonCode() shouldBe "param: int = 1" @@ -1119,23 +1124,37 @@ class PythonCodeGeneratorTest { instances = listOf( PythonEnumInstance( name = "TestEnumInstance1", - value = "inst1" + value = PythonString("inst1") ), PythonEnumInstance( name = "TestEnumInstance2", - value = "inst2" + value = PythonString("inst2") ) ) ) testEnum.toPythonCode() shouldBe """ |class TestEnum(Enum): - | TestEnumInstance1 = "inst1", - | TestEnumInstance2 = "inst2" + | TestEnumInstance1 = 'inst1', + | TestEnumInstance2 = 'inst2' """.trimMargin() } } + @Nested + inner class EnumInstanceToPythonCode { + + @Test + fun `should create Python code`() { + val testEnumInstance = PythonEnumInstance( + name = "TestEnumInstance1", + value = PythonString("inst1") + ) + + testEnumInstance.toPythonCode() shouldBe "TestEnumInstance1 = 'inst1'" + } + } + @Nested inner class ArgumentToPythonCode { @@ -1175,7 +1194,7 @@ class PythonCodeGeneratorTest { @Test fun `should handle calls`() { val expression = PythonCall( - receiver = "function", + receiver = PythonStringifiedExpression("function"), arguments = listOf( PythonArgument(value = PythonInt(1)), PythonArgument( @@ -1219,6 +1238,12 @@ class PythonCodeGeneratorTest { val expression = PythonString("string") expression.toPythonCode() shouldBe "'string'" } + + @Test + fun `should handle stringified expression`() { + val expression = PythonStringifiedExpression("1") + expression.toPythonCode() shouldBe "1" + } } @Nested diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/StubCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/StubCodeGeneratorTest.kt index 501bb96e1..e79277fe8 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/StubCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/StubCodeGeneratorTest.kt @@ -11,6 +11,8 @@ import com.larsreimann.api_editor.mutable_model.PythonModule import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonResult +import com.larsreimann.api_editor.mutable_model.PythonString +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import com.larsreimann.api_editor.mutable_model.PythonStringifiedType import de.unibonn.simpleml.SimpleMLStandaloneSetup import de.unibonn.simpleml.emf.annotationUsesOrEmpty @@ -91,7 +93,7 @@ class StubCodeGeneratorTest { PythonParameter( name = "testParameter", type = PythonStringifiedType("int"), - defaultValue = "10" + defaultValue = PythonStringifiedExpression("10") ) ), results = listOf( @@ -761,7 +763,7 @@ class StubCodeGeneratorTest { fun `should store default value`() { val pythonParameter = PythonParameter( name = "testParameter", - defaultValue = "None" + defaultValue = PythonStringifiedExpression("None") ) pythonParameter @@ -1139,54 +1141,80 @@ class StubCodeGeneratorTest { @Test fun `should convert blank strings to null`() { - " ".toSmlExpression().shouldBeNull() + PythonStringifiedExpression(" ") + .toSmlExpression() + .shouldBeNull() } @Test fun `should convert False to a false boolean literal`() { - val smlBoolean = "False".toSmlExpression().shouldBeInstanceOf() + val smlBoolean = PythonStringifiedExpression("False") + .toSmlExpression() + .shouldBeInstanceOf() smlBoolean.isTrue.shouldBeFalse() } @Test fun `should convert True to a true boolean literal`() { - val smlBoolean = "True".toSmlExpression().shouldBeInstanceOf() + val smlBoolean = PythonStringifiedExpression("True") + .toSmlExpression() + .shouldBeInstanceOf() smlBoolean.isTrue.shouldBeTrue() } @Test fun `should convert None to a null literal`() { - "None".toSmlExpression().shouldBeInstanceOf() + PythonStringifiedExpression("None") + .toSmlExpression() + .shouldBeInstanceOf() } @Test fun `should convert ints to integer literals`() { - val smlInt = "123".toSmlExpression().shouldBeInstanceOf() + val smlInt = PythonStringifiedExpression("123") + .toSmlExpression() + .shouldBeInstanceOf() smlInt.value shouldBe 123 } @Test fun `should convert floats to float literals`() { - val smlFloat = "123.45".toSmlExpression().shouldBeInstanceOf() + val smlFloat = PythonStringifiedExpression("123.45") + .toSmlExpression() + .shouldBeInstanceOf() smlFloat.value shouldBe 123.45 } @Test fun `should convert single-quoted strings to string literals`() { - val smlString = "'string'".toSmlExpression().shouldBeInstanceOf() + val smlString = PythonStringifiedExpression("'string'") + .toSmlExpression() + .shouldBeInstanceOf() smlString.value shouldBe "string" } @Test fun `should convert double-quoted strings to string literals`() { - val smlString = "\"string\"".toSmlExpression().shouldBeInstanceOf() + val smlString = PythonStringifiedExpression("\"string\"") + .toSmlExpression() + .shouldBeInstanceOf() smlString.value shouldBe "string" } @Test - fun `should convert other values to '###invalid###' strings`() { - val smlString = "unknown".toSmlExpression().shouldBeInstanceOf() + fun `should convert other stringified expressions to '###invalid###' strings`() { + val smlString = PythonStringifiedExpression("unknown") + .toSmlExpression() + .shouldBeInstanceOf() smlString.value shouldBe "###invalid###unknown###" } + + @Test + fun `should convert other expressions to '###invalid###' strings`() { + val smlString = PythonString("unknown") + .toSmlExpression() + .shouldBeInstanceOf() + smlString.value shouldBe "###invalid###PythonString(value=unknown)###" + } } } diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessorTest.kt index 6bd8b92f0..1333a3058 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessorTest.kt @@ -14,11 +14,12 @@ import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference +import com.larsreimann.api_editor.mutable_model.PythonString +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import com.larsreimann.api_editor.transformation.processing_exceptions.ConflictingEnumException import io.kotest.assertions.asClue import io.kotest.assertions.throwables.shouldThrowExactly import io.kotest.matchers.collections.shouldBeEmpty -import io.kotest.matchers.collections.shouldContain import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull @@ -52,7 +53,7 @@ class EnumAnnotationProcessorTest { name = "testFunction", parameters = listOf(testParameter), callToOriginalAPI = PythonCall( - receiver = "testModule.testFunction", + receiver = PythonStringifiedExpression("testModule.testFunction"), arguments = listOf( PythonArgument( value = PythonReference(testParameter) @@ -110,9 +111,20 @@ class EnumAnnotationProcessorTest { fun `should process EnumAnnotations on module level`() { testPackage.processEnumAnnotations() - testModule.enums[0].name shouldBe "TestEnum" - testModule.enums[0].instances shouldContain PythonEnumInstance("name1", "value1") - testModule.enums[0].instances shouldContain PythonEnumInstance("name2", "value2") + val enum = testModule.enums[0] + enum.name shouldBe "TestEnum" + + val instances = enum.instances + instances.shouldHaveSize(2) + + instances[0].asClue { + it.name shouldBe "name1" + it.value shouldBe PythonString("value1") + } + instances[1].asClue { + it.name shouldBe "name2" + it.value shouldBe PythonString("value2") + } } @Test @@ -120,8 +132,14 @@ class EnumAnnotationProcessorTest { val mutableEnum = PythonEnum( "TestEnum", mutableListOf( - PythonEnumInstance("name1", "value1"), - PythonEnumInstance("name2", "value2") + PythonEnumInstance( + name = "name1", + value = PythonString("value1") + ), + PythonEnumInstance( + name = "name2", + value = PythonString("value2") + ) ) ) testModule.enums += mutableEnum @@ -140,16 +158,35 @@ class EnumAnnotationProcessorTest { val mutableEnum = PythonEnum( "TestEnum", mutableListOf( - PythonEnumInstance("name1", "value1"), - PythonEnumInstance("name2", "value2") + PythonEnumInstance( + name = "name1", + value = PythonString("value1") + ), + PythonEnumInstance( + name = "name2", + value = PythonString("value2") + ) ) ) testModule.enums.add(mutableEnum) testPackage.processEnumAnnotations() - testModule.enums[0].name shouldBe "TestEnum" - testModule.enums[0].instances shouldContain PythonEnumInstance("name1", "value1") - testModule.enums[0].instances shouldContain PythonEnumInstance("name2", "value2") + testModule.enums.shouldHaveSize(1) + + val enum = testModule.enums[0] + enum.name shouldBe "TestEnum" + + val instances = enum.instances + instances.shouldHaveSize(2) + + instances[0].asClue { + it.name shouldBe "name1" + it.value shouldBe PythonString("value1") + } + instances[1].asClue { + it.name shouldBe "name2" + it.value shouldBe PythonString("value2") + } } @Test @@ -157,8 +194,14 @@ class EnumAnnotationProcessorTest { val mutableEnum = PythonEnum( "TestEnum", mutableListOf( - PythonEnumInstance("name1", "value1"), - PythonEnumInstance("name2", "value2") + PythonEnumInstance( + name = "name1", + value = PythonString("value1") + ), + PythonEnumInstance( + name = "name2", + value = PythonString("value2") + ) ) ) testModule.enums.add(mutableEnum) @@ -191,8 +234,14 @@ class EnumAnnotationProcessorTest { val mutableEnum = PythonEnum( "TestEnum", mutableListOf( - PythonEnumInstance("name1", "value1"), - PythonEnumInstance("name3", "value3") + PythonEnumInstance( + name = "name1", + value = PythonString("value1") + ), + PythonEnumInstance( + name = "name3", + value = PythonString("value3") + ) ) ) testModule.enums += mutableEnum diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessorTest.kt index 75a57e326..1d49c1b60 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessorTest.kt @@ -13,6 +13,7 @@ import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import com.larsreimann.api_editor.transformation.processing_exceptions.ConflictingGroupException import io.kotest.assertions.asClue import io.kotest.assertions.throwables.shouldThrowExactly @@ -53,7 +54,7 @@ class GroupAnnotationProcessorTest { ) ), callToOriginalAPI = PythonCall( - receiver = "testModule.testFunction", + receiver = PythonStringifiedExpression("testModule.testFunction"), arguments = listOf( PythonArgument(value = PythonReference(testParameter1)), PythonArgument(value = PythonReference(testParameter2)), diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessorTest.kt index c262129f7..17d9e7768 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/ParameterAnnotationProcessorTest.kt @@ -15,6 +15,7 @@ import com.larsreimann.api_editor.mutable_model.PythonModule import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import io.kotest.assertions.asClue import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldHaveSize @@ -38,7 +39,7 @@ class ParameterAnnotationProcessorTest { name = "testMethod", parameters = listOf(testParameter), callToOriginalAPI = PythonCall( - receiver = "testModule.TestClass.testMethod", + receiver = PythonStringifiedExpression("testModule.TestClass.testMethod"), arguments = listOf( PythonArgument( value = PythonReference(testParameter) @@ -90,7 +91,7 @@ class ParameterAnnotationProcessorTest { attributes.shouldHaveSize(1) attributes[0].asClue { it.name shouldBe "testParameter" - it.value shouldBe "True" + it.value shouldBe PythonStringifiedExpression("True") } } @@ -158,7 +159,7 @@ class ParameterAnnotationProcessorTest { testPackage.processParameterAnnotations() testParameter.assignedBy shouldBe PythonParameterAssignment.NAME_ONLY - testParameter.defaultValue shouldBe "True" + testParameter.defaultValue shouldBe PythonStringifiedExpression("True") } @Test @@ -174,7 +175,7 @@ class ParameterAnnotationProcessorTest { @Test fun `should process RequiredAnnotations`() { - testParameter.defaultValue = "true" + testParameter.defaultValue = PythonStringifiedExpression("true") testParameter.annotations += RequiredAnnotation testPackage.processParameterAnnotations() @@ -185,7 +186,7 @@ class ParameterAnnotationProcessorTest { @Test fun `should remove RequiredAnnotations`() { - testParameter.defaultValue = "true" + testParameter.defaultValue = PythonStringifiedExpression("true") testParameter.annotations += RequiredAnnotation testPackage.processParameterAnnotations() diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PostprocessorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PostprocessorTest.kt index a5f530cc0..8964c88e2 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PostprocessorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PostprocessorTest.kt @@ -11,6 +11,7 @@ import com.larsreimann.api_editor.mutable_model.PythonModule import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import io.kotest.assertions.asClue import io.kotest.matchers.collections.exist import io.kotest.matchers.collections.shouldBeEmpty @@ -60,7 +61,7 @@ class PostprocessorTest { name = "__init__", parameters = listOf(testConstructorParameter), callToOriginalAPI = PythonCall( - receiver = "testModule.TestClass.__init__", + receiver = PythonStringifiedExpression("testModule.TestClass.__init__"), arguments = listOf( PythonArgument(value = PythonReference(testConstructorParameter)) ) @@ -195,7 +196,7 @@ class PostprocessorTest { it.parameters.shouldBeEmpty() val callToOriginalAPI = it.callToOriginalAPI.shouldNotBeNull() - callToOriginalAPI.receiver shouldBe "testModule.TestClass" + callToOriginalAPI.receiver shouldBe PythonStringifiedExpression("testModule.TestClass") callToOriginalAPI.arguments.shouldBeEmpty() } } @@ -224,7 +225,7 @@ class PostprocessorTest { .callToOriginalAPI .asClue { it.shouldNotBeNull() - it.receiver shouldBe "testModule.TestClass" + it.receiver shouldBe PythonStringifiedExpression("testModule.TestClass") it.arguments.shouldHaveSize(1) val argument = it.arguments[0] diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PreprocessorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PreprocessorTest.kt index 114ed3370..6fb234add 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PreprocessorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/PreprocessorTest.kt @@ -9,6 +9,7 @@ import com.larsreimann.api_editor.mutable_model.PythonModule import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import io.kotest.assertions.asClue import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContain @@ -42,7 +43,7 @@ class PreprocessorTest { ) testOptionalParameter = PythonParameter( name = "testOptionalParameter", - defaultValue = "'value'", + defaultValue = PythonStringifiedExpression("'value'"), assignedBy = PythonParameterAssignment.POSITION_OR_NAME ) testGlobalFunction = PythonFunction( @@ -162,7 +163,7 @@ class PreprocessorTest { testPackage.addOriginalDeclarations() val callToOriginalAPI = testGlobalFunction.callToOriginalAPI.shouldNotBeNull() - callToOriginalAPI.receiver shouldBe "testModule.testGlobalFunction" + callToOriginalAPI.receiver shouldBe PythonStringifiedExpression("testModule.testGlobalFunction") val arguments = callToOriginalAPI.arguments arguments.shouldHaveSize(2) @@ -185,8 +186,7 @@ class PreprocessorTest { testPackage.addOriginalDeclarations() val callToOriginalAPI = testMethod.callToOriginalAPI.shouldNotBeNull() - callToOriginalAPI.receiver shouldBe "self.instance.testMethod" - + callToOriginalAPI.receiver shouldBe PythonStringifiedExpression("self.instance.testMethod") callToOriginalAPI.arguments.shouldHaveSize(0) } } From 579973b3a25b87519e000a350b68ab355e37feae Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 10 Jan 2022 16:24:59 +0100 Subject: [PATCH 10/32] fix: missing conversion of value --- .../com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt index 6bf557b50..6a005007f 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt @@ -192,7 +192,7 @@ fun PythonFunction.toPythonCode(): String { private fun buildAttributeAssignments(pythonClass: PythonClass): List { return pythonClass.attributes.map { - "self.${it.name} = ${it.value}" + "self.${it.name} = ${it.value!!.toPythonCode()}" } } From 7edc94d93d0abe15a751918cfa37b5eb3ce020ba Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 10 Jan 2022 16:58:51 +0100 Subject: [PATCH 11/32] test: Python codegen for attributes --- .../api_editor/codegen/PythonCodeGenerator.kt | 85 +++-- .../codegen/PythonCodeGeneratorTest.kt | 295 ++++++++++++------ 2 files changed, 248 insertions(+), 132 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt index 6a005007f..2a4377b04 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt @@ -6,6 +6,7 @@ import com.larsreimann.api_editor.model.PythonParameterAssignment.NAME_ONLY import com.larsreimann.api_editor.model.PythonParameterAssignment.POSITION_ONLY import com.larsreimann.api_editor.model.PythonParameterAssignment.POSITION_OR_NAME import com.larsreimann.api_editor.mutable_model.PythonArgument +import com.larsreimann.api_editor.mutable_model.PythonAttribute import com.larsreimann.api_editor.mutable_model.PythonBoolean import com.larsreimann.api_editor.mutable_model.PythonCall import com.larsreimann.api_editor.mutable_model.PythonClass @@ -133,6 +134,9 @@ fun PythonClass.toPythonCode(): String { } formattedClass += buildAllFunctions(this).joinToString("\n".repeat(2)) } + if (constructor == null && methods.isEmpty()) { + formattedClass += " pass" + } return formattedClass } @@ -145,7 +149,7 @@ private fun buildConstructor(`class`: PythonClass) = buildString { appendLine("def __init__(${buildParameters(constructor.parameters)}):") appendIndented(4) { - val boundaries = buildBoundaryChecks(constructor).joinToString("\n") + val boundaries = buildBoundaryChecks(constructor.parameters).joinToString("\n") if (boundaries.isNotBlank()) { append("$boundaries\n\n") } @@ -191,9 +195,7 @@ fun PythonFunction.toPythonCode(): String { } private fun buildAttributeAssignments(pythonClass: PythonClass): List { - return pythonClass.attributes.map { - "self.${it.name} = ${it.value!!.toPythonCode()}" - } + return pythonClass.attributes.map { it.toPythonCode() } } private fun buildParameters(parameters: List): String { @@ -244,7 +246,7 @@ private fun buildParameters(parameters: List): String { } private fun buildFunctionBody(pythonFunction: PythonFunction): String { - var formattedBoundaries = buildBoundaryChecks(pythonFunction).joinToString("\n") + var formattedBoundaries = buildBoundaryChecks(pythonFunction.parameters).joinToString("\n") if (formattedBoundaries.isNotBlank()) { formattedBoundaries = "$formattedBoundaries\n" } @@ -255,14 +257,6 @@ private fun buildFunctionBody(pythonFunction: PythonFunction): String { ) } -private fun buildBoundaryChecks(pythonConstructor: PythonConstructor): List { - return buildBoundaryChecks(pythonConstructor.parameters) -} - -private fun buildBoundaryChecks(pythonFunction: PythonFunction): List { - return buildBoundaryChecks(pythonFunction.parameters) -} - private fun buildBoundaryChecks(parameters: List): List { val formattedBoundaries: MutableList = ArrayList() parameters @@ -346,11 +340,19 @@ private fun buildBoundaryChecks(parameters: List): List return formattedBoundaries } -internal fun PythonArgument.toPythonCode() = buildString { - if (name != null) { - append("$name=") + +/* ******************************************************************************************************************** + * Declarations + * ********************************************************************************************************************/ + +internal fun PythonAttribute.toPythonCode() = buildString { + append("self.$name") + type?.toPythonCodeOrNull()?.let { + append(": $it") + } + value?.toPythonCode()?.let { + append(" = $it") } - append(value!!.toPythonCode()) } internal fun PythonEnum.toPythonCode() = buildString { @@ -373,6 +375,23 @@ internal fun PythonEnumInstance.toPythonCode(): String { return "$name = ${value!!.toPythonCode()}" } +internal fun PythonParameter.toPythonCode() = buildString { + val typeStringOrNull = type.toPythonCodeOrNull() + + append(name) + if (typeStringOrNull != null) { + append(": $typeStringOrNull") + defaultValue?.toPythonCode()?.let { append(" = $it") } + } else { + defaultValue?.toPythonCode()?.let { append("=$it") } + } +} + + +/* ******************************************************************************************************************** + * Expressions + * ********************************************************************************************************************/ + internal fun PythonExpression.toPythonCode(): String { return when (this) { is PythonBoolean -> value.toString().replaceFirstChar { it.uppercase() } @@ -386,19 +405,10 @@ internal fun PythonExpression.toPythonCode(): String { } } -internal fun PythonParameter.toPythonCode() = buildString { - val typeStringOrNull = type.toPythonCodeOrNull() - append(name) - if (typeStringOrNull != null) { - append(": $typeStringOrNull") - if (defaultValue != null) { - append(" = ${defaultValue!!.toPythonCode()}") - } - } else if (defaultValue != null) { - append("=${defaultValue!!.toPythonCode()}") - } -} +/* ******************************************************************************************************************** + * Types + * ********************************************************************************************************************/ internal fun PythonType?.toPythonCodeOrNull(): String? { return when (this) { @@ -416,6 +426,23 @@ internal fun PythonType?.toPythonCodeOrNull(): String? { } } + +/* ******************************************************************************************************************** + * Other + * ********************************************************************************************************************/ + +internal fun PythonArgument.toPythonCode() = buildString { + if (name != null) { + append("$name=") + } + append(value!!.toPythonCode()) +} + + +/* ******************************************************************************************************************** + * Util + * ********************************************************************************************************************/ + private fun StringBuilder.appendIndented(numberOfSpaces: Int, init: StringBuilder.() -> Unit): StringBuilder { val stringToIndent = StringBuilder().apply(init).toString() val indent = " ".repeat(numberOfSpaces) diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt index 660c9e083..651a1d926 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt @@ -464,24 +464,6 @@ class PythonCodeGeneratorTest { moduleContent shouldBe expectedModuleContent } - @Test - fun `should create valid code for empty classes`() { // TODO - val testClass = PythonClass( - name = "TestClass", - constructor = PythonConstructor( - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("testModule.TestClass") - ) - ) - ) - - testClass.toPythonCode() shouldBe """ - |class TestClass: - | def __init__(): - | self.instance = testModule.TestClass() - """.trimMargin() - } - @Test fun buildClassReturnsFormattedClassWithOneFunction() { // TODO // given @@ -962,32 +944,146 @@ class PythonCodeGeneratorTest { formattedClass shouldBe expectedFormattedClass } + + /* **************************************************************************************************************** + * Declarations + * ****************************************************************************************************************/ + @Nested - inner class ModuleToPythonCode { + inner class AttributeToPythonCode { @Test - fun `should import Enum if the module contains enums`() { - val testModule = PythonModule( - name = "testModule", - enums = listOf( - PythonEnum(name = "TestEnum") - ) + fun `should handle attributes without type and default value`() { + val testAttribute = PythonAttribute( + name = "attr" ) - testModule.toPythonCode() shouldBe """ - |from enum import Enum - | + testAttribute.toPythonCode() shouldBe "self.attr" + } + + @Test + fun `should handle attributes with type but without default value`() { + val testAttribute = PythonAttribute( + name = "attr", + type = PythonStringifiedType("int") + ) + + testAttribute.toPythonCode() shouldBe "self.attr: int" + } + + @Test + fun `should handle attributes without type but with default value`() { + val testAttribute = PythonAttribute( + name = "attr", + value = PythonStringifiedExpression("1") + ) + + testAttribute.toPythonCode() shouldBe "self.attr = 1" + } + + @Test + fun `should handle attributes with type and default value`() { + val testAttribute = PythonAttribute( + name = "attr", + type = PythonStringifiedType("int"), + value = PythonStringifiedExpression("1") + ) + + testAttribute.toPythonCode() shouldBe "self.attr: int = 1" + } + } + + @Nested + inner class ClassToPythonCode { // TODO + + @Test + fun `should create valid code for empty classes`() { + val testClass = PythonClass(name = "TestClass") + + testClass.toPythonCode() shouldBe """ + |class TestClass: + | pass + """.trimMargin() + } + } + + @Nested + inner class ConstructorToPythonCode { + + @Test + fun `should handle simple constructors`() { // TODO + val testConstructor = PythonConstructor() + } + + @Test + fun `should create code for parameters`() { // TODO + + } + + @Test + fun `should create code for boundaries`() { // TODO + + } + + @Test + fun `should create code for attributes`() { // TODO + + } + + @Test + fun `should create instance`() { // TODO + val testConstructor = PythonConstructor() + } + } + + @Nested + inner class EnumToPythonCode { + + @Test + fun `should create valid Python code for enums without instances`() { + val testEnum = PythonEnum(name = "TestEnum") + + testEnum.toPythonCode() shouldBe """ |class TestEnum(Enum): | pass - | """.trimMargin() } @Test - fun `should not import Enum if the module does not contain enums`() { - val testModule = PythonModule(name = "testModule") + fun `should create valid Python code for enums with instances`() { + val testEnum = PythonEnum( + name = "TestEnum", + instances = listOf( + PythonEnumInstance( + name = "TestEnumInstance1", + value = PythonString("inst1") + ), + PythonEnumInstance( + name = "TestEnumInstance2", + value = PythonString("inst2") + ) + ) + ) - testModule.toPythonCode() shouldBe "" + testEnum.toPythonCode() shouldBe """ + |class TestEnum(Enum): + | TestEnumInstance1 = 'inst1', + | TestEnumInstance2 = 'inst2' + """.trimMargin() + } + } + + @Nested + inner class EnumInstanceToPythonCode { + + @Test + fun `should create Python code`() { + val testEnumInstance = PythonEnumInstance( + name = "TestEnumInstance1", + value = PythonString("inst1") + ) + + testEnumInstance.toPythonCode() shouldBe "TestEnumInstance1 = 'inst1'" } } @@ -1058,7 +1154,36 @@ class PythonCodeGeneratorTest { | return testModule.testFunction(testGroup.newParameter1, oldParameter2=testGroup.newParameter2) """.trimMargin() } - } + } // TODO + + @Nested + inner class ModuleToPythonCode { + + @Test + fun `should import Enum if the module contains enums`() { + val testModule = PythonModule( + name = "testModule", + enums = listOf( + PythonEnum(name = "TestEnum") + ) + ) + + testModule.toPythonCode() shouldBe """ + |from enum import Enum + | + |class TestEnum(Enum): + | pass + | + """.trimMargin() + } + + @Test + fun `should not import Enum if the module does not contain enums`() { + val testModule = PythonModule(name = "testModule") + + testModule.toPythonCode() shouldBe "" + } + } // TODO @Nested inner class ParameterToPythonCode { @@ -1104,77 +1229,10 @@ class PythonCodeGeneratorTest { } } - @Nested - inner class EnumToPythonCode { - - @Test - fun `should create valid Python code for enums without instances`() { - val testEnum = PythonEnum(name = "TestEnum") - - testEnum.toPythonCode() shouldBe """ - |class TestEnum(Enum): - | pass - """.trimMargin() - } - - @Test - fun `should create valid Python code for enums with instances`() { - val testEnum = PythonEnum( - name = "TestEnum", - instances = listOf( - PythonEnumInstance( - name = "TestEnumInstance1", - value = PythonString("inst1") - ), - PythonEnumInstance( - name = "TestEnumInstance2", - value = PythonString("inst2") - ) - ) - ) - - testEnum.toPythonCode() shouldBe """ - |class TestEnum(Enum): - | TestEnumInstance1 = 'inst1', - | TestEnumInstance2 = 'inst2' - """.trimMargin() - } - } - - @Nested - inner class EnumInstanceToPythonCode { - - @Test - fun `should create Python code`() { - val testEnumInstance = PythonEnumInstance( - name = "TestEnumInstance1", - value = PythonString("inst1") - ) - - testEnumInstance.toPythonCode() shouldBe "TestEnumInstance1 = 'inst1'" - } - } - - @Nested - inner class ArgumentToPythonCode { - - @Test - fun `should handle positional arguments`() { - val testArgument = PythonArgument(value = PythonInt(1)) - - testArgument.toPythonCode() shouldBe "1" - } - - @Test - fun `should handle named arguments`() { - val testArgument = PythonArgument( - name = "arg", - value = PythonInt(1) - ) - testArgument.toPythonCode() shouldBe "arg=1" - } - } +/* **************************************************************************************************************** + * Expressions + * ****************************************************************************************************************/ @Nested inner class ExpressionToPythonCode { @@ -1246,6 +1304,11 @@ class PythonCodeGeneratorTest { } } + +/* **************************************************************************************************************** + * Types + * ****************************************************************************************************************/ + @Nested inner class TypeToPythonCodeOrNull { @@ -1285,4 +1348,30 @@ class PythonCodeGeneratorTest { type.toPythonCodeOrNull().shouldBeNull() } } + + +/* **************************************************************************************************************** + * Other + * ****************************************************************************************************************/ + + @Nested + inner class ArgumentToPythonCode { + + @Test + fun `should handle positional arguments`() { + val testArgument = PythonArgument(value = PythonInt(1)) + + testArgument.toPythonCode() shouldBe "1" + } + + @Test + fun `should handle named arguments`() { + val testArgument = PythonArgument( + name = "arg", + value = PythonInt(1) + ) + + testArgument.toPythonCode() shouldBe "arg=1" + } + } } From 4116bce19152233cb23be642aba2c9b4d9971bfb Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 10 Jan 2022 18:39:02 +0100 Subject: [PATCH 12/32] test: Python codegen for constructors --- .../api_editor/codegen/PythonCodeGenerator.kt | 281 ++-- .../codegen/PythonCodeGeneratorTest.kt | 1135 +++-------------- 2 files changed, 310 insertions(+), 1106 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt index 2a4377b04..0cc2f9416 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt @@ -1,5 +1,6 @@ package com.larsreimann.api_editor.codegen +import com.larsreimann.api_editor.model.Boundary import com.larsreimann.api_editor.model.ComparisonOperator import com.larsreimann.api_editor.model.PythonParameterAssignment.IMPLICIT import com.larsreimann.api_editor.model.PythonParameterAssignment.NAME_ONLY @@ -26,6 +27,7 @@ import com.larsreimann.api_editor.mutable_model.PythonString import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import com.larsreimann.api_editor.mutable_model.PythonStringifiedType import com.larsreimann.api_editor.mutable_model.PythonType +import com.larsreimann.modeling.closest /** * Builds a string containing the formatted module content @@ -126,7 +128,7 @@ private fun buildSeparators( fun PythonClass.toPythonCode(): String { var formattedClass = "class $name:\n" if (constructor != null) { - formattedClass += buildConstructor(this).prependIndent(" ") + formattedClass += constructor!!.toPythonCode().prependIndent(" ") } if (!methods.isEmpty()) { if (constructor != null) { @@ -144,39 +146,6 @@ private fun buildAllFunctions(pythonClass: PythonClass): List { return pythonClass.methods.map { it.toPythonCode().prependIndent(" ") } } -private fun buildConstructor(`class`: PythonClass) = buildString { - val constructor = `class`.constructor ?: return "" - - appendLine("def __init__(${buildParameters(constructor.parameters)}):") - appendIndented(4) { - val boundaries = buildBoundaryChecks(constructor.parameters).joinToString("\n") - if (boundaries.isNotBlank()) { - append("$boundaries\n\n") - } - - val attributes = buildAttributeAssignments(`class`).joinToString("\n") - if (attributes.isNotBlank()) { - append("$attributes\n\n") - } - - val constructorCall = constructor.buildConstructorCall() - if (constructorCall.isNotBlank()) { - append(constructorCall) - } - - if (attributes.isBlank() && constructorCall.isBlank()) { - append("pass") - } - } -} - -private fun PythonConstructor.buildConstructorCall(): String { - return when (callToOriginalAPI) { - null -> "" - else -> "self.instance = ${callToOriginalAPI.toPythonCode()}" - } -} - /** * Builds a string containing the formatted function content * @receiver The function whose adapter content should be built @@ -184,7 +153,7 @@ private fun PythonConstructor.buildConstructorCall(): String { */ fun PythonFunction.toPythonCode(): String { val function = """ - |def $name(${buildParameters(this.parameters)}): + |def $name(${this.parameters.toPythonCode()}): |${(buildFunctionBody(this)).prependIndent(" ")} """.trimMargin() @@ -194,59 +163,8 @@ fun PythonFunction.toPythonCode(): String { } } -private fun buildAttributeAssignments(pythonClass: PythonClass): List { - return pythonClass.attributes.map { it.toPythonCode() } -} - -private fun buildParameters(parameters: List): String { - var formattedFunctionParameters = "" - val implicitParameters: MutableList = ArrayList() - val positionOnlyParameters: MutableList = ArrayList() - val positionOrNameParameters: MutableList = ArrayList() - val nameOnlyParameters: MutableList = ArrayList() - parameters.forEach { pythonParameter: PythonParameter -> - when (pythonParameter.assignedBy) { - IMPLICIT -> implicitParameters.add(pythonParameter.toPythonCode()) - POSITION_ONLY -> positionOnlyParameters.add(pythonParameter.toPythonCode()) - POSITION_OR_NAME -> positionOrNameParameters.add(pythonParameter.toPythonCode()) - NAME_ONLY -> nameOnlyParameters.add(pythonParameter.toPythonCode()) - } - } - assert(implicitParameters.size < 2) - val hasImplicitParameter = implicitParameters.isNotEmpty() - val hasPositionOnlyParameters = positionOnlyParameters.isNotEmpty() - val hasPositionOrNameParameters = positionOrNameParameters.isNotEmpty() - val hasNameOnlyParameters = nameOnlyParameters.isNotEmpty() - - if (hasImplicitParameter) { - formattedFunctionParameters += implicitParameters[0] - if (hasPositionOnlyParameters || hasPositionOrNameParameters || hasNameOnlyParameters) { - formattedFunctionParameters += ", " - } - } - if (hasPositionOnlyParameters) { - formattedFunctionParameters += positionOnlyParameters.joinToString() - formattedFunctionParameters += when { - hasPositionOrNameParameters -> ", /, " - hasNameOnlyParameters -> ", /" - else -> ", /" - } - } - if (hasPositionOrNameParameters) { - formattedFunctionParameters += positionOrNameParameters.joinToString() - } - if (hasNameOnlyParameters) { - formattedFunctionParameters += when { - hasPositionOnlyParameters || hasPositionOrNameParameters -> ", *, " - else -> "*, " - } - formattedFunctionParameters += nameOnlyParameters.joinToString() - } - return formattedFunctionParameters -} - private fun buildFunctionBody(pythonFunction: PythonFunction): String { - var formattedBoundaries = buildBoundaryChecks(pythonFunction.parameters).joinToString("\n") + var formattedBoundaries = "" // pythonFunction.parameters.buildBoundaryChecks().joinToString("\n") if (formattedBoundaries.isNotBlank()) { formattedBoundaries = "$formattedBoundaries\n" } @@ -257,89 +175,6 @@ private fun buildFunctionBody(pythonFunction: PythonFunction): String { ) } -private fun buildBoundaryChecks(parameters: List): List { - val formattedBoundaries: MutableList = ArrayList() - parameters - .filter { it.boundary != null } - .forEach { - assert(it.boundary != null) - if (it.boundary!!.isDiscrete) { - formattedBoundaries.add( - "if not (isinstance(${it.name}, int) or (isinstance(${it.name}, float) and ${it.name}.is_integer())):\n" + - " raise ValueError('" + - it.name + - " needs to be an integer, but {} was assigned." + - "'.format(" + - it.name + - "))" - ) - } - if (it.boundary!!.lowerLimitType !== ComparisonOperator.UNRESTRICTED && it.boundary!!.upperLimitType !== ComparisonOperator.UNRESTRICTED) { - formattedBoundaries.add( - "if not ${it.boundary!!.lowerIntervalLimit} ${it.boundary!!.lowerLimitType.operator} ${it.name} ${it.boundary!!.upperLimitType.operator} ${it.boundary!!.upperIntervalLimit}:\n" + - " raise ValueError('Valid values of " + - it.name + - " must be in " + - it.boundary!!.asInterval() + - ", but {} was assigned." + - "'.format(" + - it.name + - "))" - ) - } else if (it.boundary!!.lowerLimitType === ComparisonOperator.LESS_THAN) { - formattedBoundaries.add( - "if not ${it.boundary!!.lowerIntervalLimit} < ${it.name}:\n" + - " raise ValueError('Valid values of " + - it.name + - " must be greater than " + - it.boundary!!.lowerIntervalLimit + - ", but {} was assigned." + - "'.format(" + - it.name + - "))" - ) - } else if (it.boundary!!.lowerLimitType === ComparisonOperator.LESS_THAN_OR_EQUALS) { - formattedBoundaries.add( - "if not ${it.boundary!!.lowerIntervalLimit} <= ${it.name}:\n" + - " raise ValueError('Valid values of " + - it.name + - " must be greater than or equal to " + - it.boundary!!.lowerIntervalLimit + - ", but {} was assigned." + - "'.format(" + - it.name + - "))" - ) - } - if (it.boundary!!.upperLimitType === ComparisonOperator.LESS_THAN) { - formattedBoundaries.add( - "if not ${it.name} < ${it.boundary!!.upperIntervalLimit}:\n" + - " raise ValueError('Valid values of " + - it.name + - " must be less than " + - it.boundary!!.upperIntervalLimit + - ", but {} was assigned." + - "'.format(" + - it.name + - "))" - ) - } else if (it.boundary!!.upperLimitType === ComparisonOperator.LESS_THAN) { - formattedBoundaries.add( - "if not ${it.name} <= ${it.boundary!!.upperIntervalLimit}:\n" + - " raise ValueError('Valid values of " + - it.name + - " must be less than or equal to " + - it.boundary!!.upperIntervalLimit + - ", but {} was assigned." + - "'.format(" + - it.name + - "))" - ) - } - } - return formattedBoundaries -} - /* ******************************************************************************************************************** * Declarations @@ -355,9 +190,43 @@ internal fun PythonAttribute.toPythonCode() = buildString { } } +internal fun PythonConstructor.toPythonCode() = buildString { + val parametersString = parameters.toPythonCode() + val boundariesString = parameters + .mapNotNull { it.boundary?.toPythonCode(it.name) } + .joinToString("\n") + val attributesString = closest() + ?.attributes + ?.joinToString("\n") { it.toPythonCode() } + ?: "" + val callString = callToOriginalAPI + ?.let { "self.instance = ${it.toPythonCode()}" } + ?: "" + + appendLine("def __init__($parametersString):") + if (boundariesString.isNotBlank()) { + appendIndented(boundariesString) + if (attributesString.isNotBlank() || callString.isNotBlank()) { + append("\n\n") + } + } + if (attributesString.isNotBlank()) { + appendIndented(attributesString) + if (callString.isNotBlank()) { + append("\n\n") + } + } + if (callString.isNotBlank()) { + appendIndented(callString) + } + if (boundariesString.isBlank() && attributesString.isBlank() && callString.isBlank()) { + appendIndented("pass") + } +} + internal fun PythonEnum.toPythonCode() = buildString { appendLine("class $name(Enum):") - appendIndented(4) { + appendIndented { if (instances.isEmpty()) { append("pass") } else { @@ -375,6 +244,41 @@ internal fun PythonEnumInstance.toPythonCode(): String { return "$name = ${value!!.toPythonCode()}" } +internal fun List.toPythonCode(): String { + val assignedByToParameter = this@toPythonCode.groupBy { it.assignedBy } + val implicitParametersString = assignedByToParameter[IMPLICIT] + ?.joinToString { it.toPythonCode() } + ?: "" + var positionOnlyParametersString = assignedByToParameter[POSITION_ONLY] + ?.joinToString { it.toPythonCode() } + ?: "" + val positionOrNameParametersString = assignedByToParameter[POSITION_OR_NAME] + ?.joinToString { it.toPythonCode() } + ?: "" + var nameOnlyParametersString = assignedByToParameter[NAME_ONLY] + ?.joinToString { it.toPythonCode() } + ?: "" + + if (positionOnlyParametersString.isNotBlank()) { + positionOnlyParametersString = "$positionOnlyParametersString, /" + } + + if (nameOnlyParametersString.isNotBlank()) { + nameOnlyParametersString = "*, $nameOnlyParametersString" + } + + val parameterStrings = listOf( + implicitParametersString, + positionOnlyParametersString, + positionOrNameParametersString, + nameOnlyParametersString + ) + + return parameterStrings + .filter { it.isNotBlank() } + .joinToString() +} + internal fun PythonParameter.toPythonCode() = buildString { val typeStringOrNull = type.toPythonCodeOrNull() @@ -438,14 +342,45 @@ internal fun PythonArgument.toPythonCode() = buildString { append(value!!.toPythonCode()) } +internal fun Boundary.toPythonCode(parameterName: String) = buildString { + if (isDiscrete) { + appendLine("if not (isinstance($parameterName, int) or (isinstance($parameterName, float) and $parameterName.is_integer())):") + appendIndented("raise ValueError('$parameterName' needs to be an integer, but {} was assigned.'.format($parameterName))") + appendLine() + } + + if (lowerLimitType != ComparisonOperator.UNRESTRICTED && upperLimitType != ComparisonOperator.UNRESTRICTED) { + appendLine("if not $lowerIntervalLimit ${lowerLimitType.operator} $parameterName ${upperLimitType.operator} ${upperIntervalLimit}:") + appendIndented("raise ValueError('Valid values of $parameterName must be in ${asInterval()}, but {} was assigned.'.format($parameterName))") + } else if (lowerLimitType == ComparisonOperator.LESS_THAN) { + appendLine("if not $lowerIntervalLimit < ${parameterName}:") + appendIndented("raise ValueError('Valid values of $parameterName must be greater than $lowerIntervalLimit, but {} was assigned.'.format($parameterName))") + } else if (lowerLimitType == ComparisonOperator.LESS_THAN_OR_EQUALS) { + appendLine("if not $lowerIntervalLimit <= ${parameterName}:") + appendIndented("raise ValueError('Valid values of $parameterName must be greater than or equal to $lowerIntervalLimit, but {} was assigned.'.format($parameterName))") + } else if (upperLimitType == ComparisonOperator.LESS_THAN) { + appendLine("if not $parameterName < ${upperIntervalLimit}:") + appendIndented("raise ValueError('Valid values of $parameterName must be less than $upperIntervalLimit, but {} was assigned.'.format($parameterName))") + } else if (upperLimitType == ComparisonOperator.LESS_THAN_OR_EQUALS) { + appendLine("if not $parameterName <= ${upperIntervalLimit}:") + appendIndented("raise ValueError('Valid values of $parameterName must be less than or equal to $upperIntervalLimit, but {} was assigned.'.format($parameterName))") + } +} + /* ******************************************************************************************************************** * Util * ********************************************************************************************************************/ -private fun StringBuilder.appendIndented(numberOfSpaces: Int, init: StringBuilder.() -> Unit): StringBuilder { +private fun StringBuilder.appendIndented(init: StringBuilder.() -> Unit): StringBuilder { val stringToIndent = StringBuilder().apply(init).toString() - val indent = " ".repeat(numberOfSpaces) + val indent = " ".repeat(4) append(stringToIndent.prependIndent(indent)) return this } + +private fun StringBuilder.appendIndented(value: String): StringBuilder { + val indent = " ".repeat(4) + append(value.prependIndent(indent)) + return this +} diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt index 651a1d926..4ac104b78 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt @@ -2,10 +2,7 @@ package com.larsreimann.api_editor.codegen import com.larsreimann.api_editor.model.Boundary import com.larsreimann.api_editor.model.ComparisonOperator -import com.larsreimann.api_editor.model.PythonFromImport -import com.larsreimann.api_editor.model.PythonImport import com.larsreimann.api_editor.model.PythonParameterAssignment -import com.larsreimann.api_editor.mutable_model.OriginalPythonClass import com.larsreimann.api_editor.mutable_model.PythonArgument import com.larsreimann.api_editor.mutable_model.PythonAttribute import com.larsreimann.api_editor.mutable_model.PythonBoolean @@ -22,928 +19,16 @@ import com.larsreimann.api_editor.mutable_model.PythonModule import com.larsreimann.api_editor.mutable_model.PythonNamedType import com.larsreimann.api_editor.mutable_model.PythonParameter import com.larsreimann.api_editor.mutable_model.PythonReference -import com.larsreimann.api_editor.mutable_model.PythonResult import com.larsreimann.api_editor.mutable_model.PythonString import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import com.larsreimann.api_editor.mutable_model.PythonStringifiedType import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test class PythonCodeGeneratorTest { - @Test - fun buildModuleContentReturnsFormattedModuleContent() { // TODO - // given - val testMethodParameter = PythonParameter( - name = "only-param", - defaultValue = PythonStringifiedExpression("'defaultValue'"), - assignedBy = PythonParameterAssignment.NAME_ONLY - ) - val testClass = PythonClass( - name = "test-class", - methods = listOf( - PythonFunction( - name = "test-class-function", - parameters = listOf( - PythonParameter( - name = "self", - assignedBy = PythonParameterAssignment.IMPLICIT - ), - testMethodParameter - ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("self.instance.test-class-function"), - arguments = listOf( - PythonArgument( - value = PythonReference(testMethodParameter) - ) - ) - ) - ) - ), - originalClass = OriginalPythonClass(qualifiedName = "test-module.test-class") - ) - val testFunction1Parameter1 = PythonParameter(name = "param1") - val testFunction1Parameter2 = PythonParameter(name = "param2") - val testFunction1Parameter3 = PythonParameter(name = "param3") - val testFunction2Parameter = PythonParameter( - name = "test-parameter", - defaultValue = PythonStringifiedExpression("42"), - assignedBy = PythonParameterAssignment.NAME_ONLY - ) - val testModule = PythonModule( - name = "test-module", - classes = mutableListOf(testClass), - functions = listOf( - PythonFunction( - name = "function_module", - parameters = listOf( - testFunction1Parameter1, - testFunction1Parameter2, - testFunction1Parameter3 - ), - results = listOf( - PythonResult( - name = "test-result", - type = PythonStringifiedType("str") - ) - ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("test-module.function_module"), - arguments = listOf( - PythonArgument( - name = "param1", - value = PythonReference(testFunction1Parameter1) - ), - PythonArgument( - name = "param2", - value = PythonReference(testFunction1Parameter2) - ), - PythonArgument( - name = "param3", - value = PythonReference(testFunction1Parameter3) - ) - ) - ) - ), - PythonFunction( - name = "test-function", - parameters = listOf( - testFunction2Parameter - ), - results = listOf( - PythonResult( - "test-result", - PythonStringifiedType("str"), - "str" - ) - ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("test-module.test-function"), - arguments = listOf( - PythonArgument( - name = "test-parameter", - value = PythonReference(testFunction2Parameter) - ) - ) - ) - ) - ) - ) - - // when - val moduleContent = testModule.toPythonCode() - - // then - val expectedModuleContent: String = - """ - |import test-module - | - |class test-class: - | def test-class-function(self, *, only-param='defaultValue'): - | return self.instance.test-class-function(only-param) - | - |def function_module(param1, param2, param3): - | return test-module.function_module(param1=param1, param2=param2, param3=param3) - | - |def test-function(*, test-parameter=42): - | return test-module.test-function(test-parameter=test-parameter) - | - """.trimMargin() - - moduleContent shouldBe expectedModuleContent - } - - @Test - fun buildModuleContentWithNoClassesReturnsFormattedModuleContent() { // TODO - // given - val testFunction1Parameter1 = PythonParameter(name = "param1") - val testFunction1Parameter2 = PythonParameter(name = "param2") - val testFunction1Parameter3 = PythonParameter(name = "param3") - val testFunction2Parameter = PythonParameter( - "test-parameter", - defaultValue = PythonStringifiedExpression("42"), - assignedBy = PythonParameterAssignment.NAME_ONLY - ) - val testModule = PythonModule( - name = "test-module", - functions = mutableListOf( - PythonFunction( - name = "function_module", - parameters = mutableListOf( - testFunction1Parameter1, - testFunction1Parameter2, - testFunction1Parameter3 - ), - results = mutableListOf( - PythonResult( - "test-result", - PythonStringifiedType("str"), - "Lorem ipsum" - ) - ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("test-module.function_module"), - arguments = listOf( - PythonArgument( - name = "param1", - value = PythonReference(testFunction1Parameter1) - ), - PythonArgument( - name = "param2", - value = PythonReference(testFunction1Parameter2) - ), - PythonArgument( - name = "param3", - value = PythonReference(testFunction1Parameter3) - ) - ) - ) - ), - PythonFunction( - name = "test-function", - parameters = mutableListOf( - testFunction2Parameter - ), - results = mutableListOf( - PythonResult( - "test-result", - PythonStringifiedType("str"), - "Lorem ipsum" - ) - ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("test-module.test-function"), - arguments = listOf( - PythonArgument( - name = "test-parameter", - value = PythonReference(testFunction2Parameter) - ) - ) - ) - ) - ) - ) - - // when - val moduleContent = testModule.toPythonCode() - - // then - val expectedModuleContent: String = - """ - |import test-module - | - |def function_module(param1, param2, param3): - | return test-module.function_module(param1=param1, param2=param2, param3=param3) - | - |def test-function(*, test-parameter=42): - | return test-module.test-function(test-parameter=test-parameter) - | - """.trimMargin() - - moduleContent shouldBe expectedModuleContent - } - - @Test - fun buildModuleContentWithNoFunctionsReturnsFormattedModuleContent() { // TODO - // given - val testClass = PythonClass( - name = "test-class", - constructor = PythonConstructor( - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("test-module.test-class") - ) - ), - originalClass = OriginalPythonClass("test-module.test-class") - ) - val testModule = PythonModule( - name = "test-module", - imports = mutableListOf( - PythonImport( - "test-import1", - "test-alias" - ) - ), - fromImports = mutableListOf( - PythonFromImport( - "test-from-import1", - "test-declaration1", - null - ) - ), - classes = mutableListOf(testClass) - ) - - // when - val moduleContent = testModule.toPythonCode() - - // then - val expectedModuleContent: String = - """ - |import test-module - | - |class test-class: - | def __init__(): - | self.instance = test-module.test-class() - | - """.trimMargin() - - moduleContent shouldBe expectedModuleContent - } - - @Test - fun buildModuleContentWithEmptyModuleReturnsEmptyString() { // TODO - // given - val testModule = PythonModule( - name = "test-module", - imports = mutableListOf( - PythonImport( - "test-import1", - "test-alias" - ) - ), - fromImports = mutableListOf( - PythonFromImport( - "test-from-import1", - "test-declaration1", - null - ) - ), - classes = mutableListOf(), - enums = mutableListOf(), - functions = mutableListOf() - ) - - // when - val moduleContent = testModule.toPythonCode() - - // then - val expectedModuleContent = "" - - moduleContent shouldBe expectedModuleContent - } - - @Test - fun buildModuleContentWithBoundaryAnnotationReturnsFormattedModuleContent1() { // TODO - // given - val testParameter1 = PythonParameter( - name = "param1", - defaultValue = PythonStringifiedExpression("5"), - assignedBy = PythonParameterAssignment.NAME_ONLY - ) - testParameter1.boundary = Boundary( - true, - 2.0, - ComparisonOperator.LESS_THAN, - 10.0, - ComparisonOperator.LESS_THAN_OR_EQUALS - ) - val testParameter2 = PythonParameter( - "param2", - defaultValue = PythonStringifiedExpression("5"), - assignedBy = PythonParameterAssignment.NAME_ONLY - ) - testParameter2.boundary = Boundary( - false, - 5.0, - ComparisonOperator.LESS_THAN_OR_EQUALS, - 0.0, - ComparisonOperator.UNRESTRICTED - ) - val testParameter3 = PythonParameter( - "param3", - defaultValue = PythonStringifiedExpression("5"), - assignedBy = PythonParameterAssignment.NAME_ONLY - ) - testParameter3.boundary = Boundary( - false, - 0.0, - ComparisonOperator.UNRESTRICTED, - 10.0, - ComparisonOperator.LESS_THAN - ) - val testFunction = PythonFunction( - name = "function_module", - parameters = mutableListOf(testParameter1, testParameter2, testParameter3), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("test-module.function_module"), - arguments = listOf( - PythonArgument( - name = "param1", - value = PythonReference(testParameter1) - ), - PythonArgument( - name = "param2", - value = PythonReference(testParameter2) - ), - PythonArgument( - name = "param3", - value = PythonReference(testParameter3) - ) - ) - ) - ) - val testModule = PythonModule( - name = "test-module", - functions = mutableListOf(testFunction), - ) - - // when - val moduleContent = testModule.toPythonCode() - - // then - val expectedModuleContent: String = - """ - |import test-module - | - |def function_module(*, param1=5, param2=5, param3=5): - | if not (isinstance(param1, int) or (isinstance(param1, float) and param1.is_integer())): - | raise ValueError('param1 needs to be an integer, but {} was assigned.'.format(param1)) - | if not 2.0 < param1 <= 10.0: - | raise ValueError('Valid values of param1 must be in (2.0, 10.0], but {} was assigned.'.format(param1)) - | if not 5.0 <= param2: - | raise ValueError('Valid values of param2 must be greater than or equal to 5.0, but {} was assigned.'.format(param2)) - | if not param3 < 10.0: - | raise ValueError('Valid values of param3 must be less than 10.0, but {} was assigned.'.format(param3)) - | return test-module.function_module(param1=param1, param2=param2, param3=param3) - | - """.trimMargin() - - moduleContent shouldBe expectedModuleContent - } - - @Test - fun buildModuleContentWithBoundaryAnnotationReturnsFormattedModuleContent2() { // TODO - // given - val testParameter = PythonParameter( - name = "param1", - defaultValue = PythonStringifiedExpression("5"), - assignedBy = PythonParameterAssignment.NAME_ONLY - ) - testParameter.boundary = Boundary( - false, - 2.0, - ComparisonOperator.LESS_THAN_OR_EQUALS, - 0.0, - ComparisonOperator.UNRESTRICTED - ) - val testFunction = PythonFunction( - name = "function_module", - parameters = listOf(testParameter), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("test-module.function_module"), - arguments = listOf( - PythonArgument( - name = "param1", - value = PythonReference(testParameter) - ) - ) - ) - ) - val testModule = PythonModule( - name = "test-module", - functions = listOf(testFunction), - ) - - // when - val moduleContent = testModule.toPythonCode() - - // then - val expectedModuleContent: String = - """ - |import test-module - | - |def function_module(*, param1=5): - | if not 2.0 <= param1: - | raise ValueError('Valid values of param1 must be greater than or equal to 2.0, but {} was assigned.'.format(param1)) - | return test-module.function_module(param1=param1) - | - """.trimMargin() - - moduleContent shouldBe expectedModuleContent - } - - @Test - fun buildClassReturnsFormattedClassWithOneFunction() { // TODO - // given - val testParameter = PythonParameter( - name = "only-param", - defaultValue = PythonStringifiedExpression("'defaultValue'"), - assignedBy = PythonParameterAssignment.NAME_ONLY - ) - val testClass = PythonClass( - name = "test-class", - constructor = PythonConstructor( - parameters = mutableListOf( - PythonParameter( - name = "self", - assignedBy = PythonParameterAssignment.IMPLICIT - ), - testParameter - ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("test-module.test-class"), - arguments = listOf( - PythonArgument( - value = PythonReference(testParameter) - ) - ) - ) - ), - originalClass = OriginalPythonClass(qualifiedName = "test-module.test-class") - ) - - // when - val formattedClass = testClass.toPythonCode() - - // then - val expectedFormattedClass: String = - """ - |class test-class: - | def __init__(self, *, only-param='defaultValue'): - | self.instance = test-module.test-class(only-param) - """.trimMargin() - formattedClass shouldBe expectedFormattedClass - } - - @Test - fun buildClassReturnsFormattedClassWithTwoFunctions() { // TODO - // given - val testMethod1Parameter = PythonParameter( - name = "only-param", - assignedBy = PythonParameterAssignment.POSITION_OR_NAME, - ) - val testMethod2Parameter = PythonParameter( - name = "only-param", - assignedBy = PythonParameterAssignment.POSITION_OR_NAME, - ) - val testClass = PythonClass( - name = "test-class", - methods = mutableListOf( - PythonFunction( - name = "test-class-function1", - parameters = mutableListOf( - PythonParameter( - name = "self", - assignedBy = PythonParameterAssignment.IMPLICIT, - ), - testMethod1Parameter - ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("self.instance.test-class-function1"), - arguments = listOf( - PythonArgument(value = PythonReference(testMethod1Parameter)) - ) - ) - ), - PythonFunction( - name = "test-class-function2", - parameters = mutableListOf( - PythonParameter( - name = "self", - assignedBy = PythonParameterAssignment.IMPLICIT - ), - testMethod2Parameter - ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("self.instance.test-class-function2"), - arguments = listOf( - PythonArgument(value = PythonReference(testMethod2Parameter)) - ) - ) - ) - ) - ) - - // when - val formattedClass = testClass.toPythonCode() - - // then - val expectedFormattedClass: String = - """ - |class test-class: - | def test-class-function1(self, only-param): - | return self.instance.test-class-function1(only-param) - | - | def test-class-function2(self, only-param): - | return self.instance.test-class-function2(only-param)""".trimMargin() - - formattedClass shouldBe expectedFormattedClass - } - - @Test - fun buildClassReturnsFormattedClassBasedOnOriginalDeclaration() { // TODO - // given - val testParameter1 = PythonParameter(name = "second-param") - val testParameter2 = PythonParameter(name = "third-param") - val testFunction = PythonFunction( - name = "test-function", - parameters = mutableListOf( - PythonParameter( - name = "self", - assignedBy = PythonParameterAssignment.IMPLICIT - ), - testParameter1, - testParameter2 - ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("self.instance.test-function"), - arguments = listOf( - PythonArgument(value = PythonReference(testParameter1)), - PythonArgument( - name = "third-param", - value = PythonReference(testParameter2) - ) - ) - ) - ) - val testClass = PythonClass( - name = "test-class", - methods = mutableListOf(testFunction) - ) - - // when - val formattedClass = testClass.toPythonCode() - - // then - val expectedFormattedClass: String = - """ - |class test-class: - | def test-function(self, second-param, third-param): - | return self.instance.test-function(second-param, third-param=third-param)""".trimMargin() - formattedClass shouldBe expectedFormattedClass - } - - @Test - fun buildFunctionReturnsFormattedFunctionWithNoParameters() { // TODO - // given - val testFunction = PythonFunction( - name = "test-function", - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("test-module.test-function") - ) - ) - - // when - val formattedFunction = testFunction.toPythonCode() - - // then - val expectedFormattedFunction: String = - """ - |def test-function(): - | return test-module.test-function()""".trimMargin() - formattedFunction shouldBe expectedFormattedFunction - } - - @Test - fun buildFunctionReturnsFormattedFunctionWithPositionOnlyParameter() { // TODO - // given - val testParameter = PythonParameter( - name = "only-param", - defaultValue = PythonStringifiedExpression("13"), - assignedBy = PythonParameterAssignment.NAME_ONLY - ) - val testFunction = PythonFunction( - name = "test-function", - parameters = mutableListOf( - testParameter - ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("test-module.test-function"), - arguments = listOf( - PythonArgument(value = PythonReference(testParameter)) - ) - ) - ) - - // when - val formattedFunction = testFunction.toPythonCode() - - // then - val expectedFormattedFunction: String = - """ - |def test-function(*, only-param=13): - | return test-module.test-function(only-param)""".trimMargin() - formattedFunction shouldBe expectedFormattedFunction - } - - @Test - fun buildFunctionReturnsFormattedFunctionWithPositionOrNameParameter() { // TODO - // given - val testParameter = PythonParameter( - name = "only-param", - defaultValue = PythonStringifiedExpression("False"), - assignedBy = PythonParameterAssignment.NAME_ONLY - ) - val testFunction = PythonFunction( - name = "test-function", - parameters = mutableListOf( - testParameter - ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("test-module.test-function"), - arguments = listOf( - PythonArgument(value = PythonReference(testParameter)) - ) - ) - ) - - // when - val formattedFunction = testFunction.toPythonCode() - - // then - val expectedFormattedFunction: String = - """ - |def test-function(*, only-param=False): - | return test-module.test-function(only-param)""".trimMargin() - formattedFunction shouldBe expectedFormattedFunction - } - - @Test - fun buildFunctionReturnsFormattedFunctionWithNameOnlyParameter() { // TODO - // given - val testParameter = PythonParameter(name = "only-param") - val testFunction = PythonFunction( - name = "test-function", - parameters = mutableListOf( - testParameter - ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("test-module.test-function"), - arguments = listOf( - PythonArgument( - name = "only-param", - value = PythonReference(testParameter) - ) - ) - ) - ) - - // when - val formattedFunction = testFunction.toPythonCode() - - // then - val expectedFormattedFunction: String = - """ - |def test-function(only-param): - | return test-module.test-function(only-param=only-param)""".trimMargin() - formattedFunction shouldBe expectedFormattedFunction - } - - @Test - fun buildFunctionReturnsFormattedFunctionWithPositionAndPositionOrNameParameter() { // TODO - // given - - val testParameter1 = PythonParameter(name = "first-param") - val testParameter2 = PythonParameter(name = "second-param") - val testFunction = PythonFunction( - name = "test-function", - parameters = mutableListOf( - testParameter1, - testParameter2 - ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("test-module.test-function"), - arguments = listOf( - PythonArgument(value = PythonReference(testParameter1)), - PythonArgument(value = PythonReference(testParameter2)) - ) - ) - ) - - // when - val formattedFunction = testFunction.toPythonCode() - - // then - val expectedFormattedFunction: String = - """ - |def test-function(first-param, second-param): - | return test-module.test-function(first-param, second-param)""".trimMargin() - formattedFunction shouldBe expectedFormattedFunction - } - - @Test - fun buildFunctionReturnsFormattedFunctionWithPositionAndPositionOrNameAndNameOnlyParameter() { // TODO - // given - val testParameter1 = PythonParameter(name = "first-param") - val testParameter2 = PythonParameter(name = "second-param") - val testParameter3 = PythonParameter(name = "third-param") - val testFunction = PythonFunction( - name = "test-function", - parameters = listOf( - testParameter1, - testParameter2, - testParameter3 - ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("test-module.test-function"), - arguments = listOf( - PythonArgument(value = PythonReference(testParameter1)), - PythonArgument(value = PythonReference(testParameter2)), - PythonArgument( - name = "third-param", - value = PythonReference(testParameter3) - ) - ) - ) - ) - - // when - val formattedFunction = testFunction.toPythonCode() - - // then - val expectedFormattedFunction: String = - """ - |def test-function(first-param, second-param, third-param): - | return test-module.test-function(first-param, second-param, third-param=third-param)""".trimMargin() - formattedFunction shouldBe expectedFormattedFunction - } - - @Test - fun buildFunctionReturnsFormattedFunctionWithPositionAndNameOnlyParameter() { // TODO - // given - val testParameter1 = PythonParameter(name = "first-param") - val testParameter2 = PythonParameter(name = "second-param") - val testFunction = PythonFunction( - name = "test-function", - parameters = listOf( - testParameter1, - testParameter2 - ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("test-module.test-function"), - arguments = listOf( - PythonArgument(value = PythonReference(testParameter1)), - PythonArgument( - name = "second-param", - value = PythonReference(testParameter2) - ) - ) - ) - ) - - // when - val formattedFunction = testFunction.toPythonCode() - - // then - val expectedFormattedFunction: String = - """ - |def test-function(first-param, second-param): - | return test-module.test-function(first-param, second-param=second-param)""".trimMargin() - formattedFunction shouldBe expectedFormattedFunction - } - - @Test - fun buildFunctionReturnsFormattedFunctionWithPositionOrNameAndNameOnlyParameter() { // TODO - // given - val testParameter1 = PythonParameter(name = "first-param") - val testParameter2 = PythonParameter(name = "second-param") - val testFunction = PythonFunction( - name = "test-function", - parameters = listOf( - testParameter1, - testParameter2 - ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("test-module.test-function"), - arguments = listOf( - PythonArgument( - value = PythonReference(testParameter1) - ), - PythonArgument( - name = "second-param", - value = PythonReference(testParameter2) - ) - ) - ) - ) - - // when - val formattedFunction = testFunction.toPythonCode() - - // then - val expectedFormattedFunction: String = - """ - |def test-function(first-param, second-param): - | return test-module.test-function(first-param, second-param=second-param) - """.trimMargin() - - formattedFunction shouldBe expectedFormattedFunction - } - - @Test - fun buildFunctionsReturnsFormattedFunctionBasedOnOriginalDeclaration() { // TODO - // given - val testParameter1 = PythonParameter(name = "first-param") - val testParameter2 = PythonParameter(name = "second-param") - val testParameter3 = PythonParameter(name = "third-param") - val testFunction = PythonFunction( - name = "test-function", - parameters = mutableListOf( - testParameter1, - testParameter2, - testParameter3 - ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("test-module.test-function"), - arguments = listOf( - PythonArgument(value = PythonReference(testParameter1)), - PythonArgument(value = PythonReference(testParameter2)), - PythonArgument( - name = "third-param", - value = PythonReference(testParameter3) - ) - ) - ) - ) - - // when - val formattedFunction = testFunction.toPythonCode() - - // then - val expectedFormattedFunction: String = - """ - |def test-function(first-param, second-param, third-param): - | return test-module.test-function(first-param, second-param, third-param=third-param)""".trimMargin() - formattedFunction shouldBe expectedFormattedFunction - } - - @Test - fun buildClassReturnsFormattedClassWithStaticMethodDecorator() { - // given - val testParameter = PythonParameter(name = "only-param") - val testClass = PythonClass( - name = "test-class", - methods = listOf( - PythonFunction( - name = "test-class-function1", - decorators = mutableListOf("staticmethod"), - parameters = listOf(testParameter), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("test-module.test-class.test-class-function1"), - arguments = listOf( - PythonArgument(value = PythonReference(testParameter)) - ) - ) - ) - ) - ) - - // when - val formattedClass: String = testClass.toPythonCode() - - // then - val expectedFormattedClass: String = - """ - |class test-class: - | @staticmethod - | def test-class-function1(only-param): - | return test-module.test-class.test-class-function1(only-param)""".trimMargin() - - formattedClass shouldBe expectedFormattedClass - } - /* **************************************************************************************************************** * Declarations @@ -994,7 +79,7 @@ class PythonCodeGeneratorTest { } @Nested - inner class ClassToPythonCode { // TODO + inner class ClassToPythonCode { @Test fun `should create valid code for empty classes`() { @@ -1005,34 +90,208 @@ class PythonCodeGeneratorTest { | pass """.trimMargin() } - } + } // TODO @Nested inner class ConstructorToPythonCode { + private lateinit var callToOriginalAPI: PythonCall + private lateinit var testClass: PythonClass + private lateinit var parametersWithBoundaries: List + + @BeforeEach + fun reset() { + callToOriginalAPI = PythonCall( + PythonReference( + PythonClass(name = "OriginalClass") + ) + ) + testClass = PythonClass( + name = "TestClass", + attributes = listOf( + PythonAttribute( + name = "testAttribute1", + value = PythonInt(1) + ), + PythonAttribute( + name = "testAttribute2", + value = PythonInt(2) + ) + ) + ) + parametersWithBoundaries = listOf( + PythonParameter( + name = "self", + assignedBy = PythonParameterAssignment.IMPLICIT + ), + PythonParameter( + name = "testParameter1", + boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ), + PythonParameter( + name = "testParameter2", + boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.UNRESTRICTED + ) + ) + ) + } + + @Test + fun `should create code for parameters`() { + val testConstructor = PythonConstructor( + parameters = listOf( + PythonParameter( + name = "self", + assignedBy = PythonParameterAssignment.IMPLICIT + ), + PythonParameter( + name = "positionOnly", + assignedBy = PythonParameterAssignment.POSITION_ONLY + ), + PythonParameter( + name = "positionOrName", + assignedBy = PythonParameterAssignment.POSITION_OR_NAME + ), + PythonParameter( + name = "nameOnly", + assignedBy = PythonParameterAssignment.NAME_ONLY + ) + ) + ) + + testConstructor.toPythonCode() shouldBe """ + |def __init__(self, positionOnly, /, positionOrName, *, nameOnly): + | pass + """.trimMargin() + } + @Test - fun `should handle simple constructors`() { // TODO + fun `should handle constructors (no boundaries, no attributes, no call)`() { val testConstructor = PythonConstructor() + + testConstructor.toPythonCode() shouldBe """ + |def __init__(): + | pass + """.trimMargin() } @Test - fun `should create code for parameters`() { // TODO + fun `should handle constructors (no boundaries, no attributes, call)`() { + val testConstructor = PythonConstructor(callToOriginalAPI = callToOriginalAPI) + testConstructor.toPythonCode() shouldBe """ + |def __init__(): + | self.instance = OriginalClass() + """.trimMargin() } @Test - fun `should create code for boundaries`() { // TODO + fun `should handle constructors (no boundaries, attributes, no call)`() { + val testConstructor = PythonConstructor() + testClass.constructor = testConstructor + testConstructor.toPythonCode() shouldBe """ + |def __init__(): + | self.testAttribute1 = 1 + | self.testAttribute2 = 2 + """.trimMargin() } @Test - fun `should create code for attributes`() { // TODO + fun `should handle constructors (no boundaries, attributes, call)`() { + val testConstructor = PythonConstructor(callToOriginalAPI = callToOriginalAPI) + testClass.constructor = testConstructor + testConstructor.toPythonCode() shouldBe """ + |def __init__(): + | self.testAttribute1 = 1 + | self.testAttribute2 = 2 + | + | self.instance = OriginalClass() + """.trimMargin() } @Test - fun `should create instance`() { // TODO - val testConstructor = PythonConstructor() + fun `should handle constructors (boundaries, no attributes, no call)`() { + val testConstructor = PythonConstructor(parameters = parametersWithBoundaries) + + testConstructor.toPythonCode() shouldBe """ + |def __init__(self, testParameter1, testParameter2): + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) + | if not 0.0 < testParameter2: + | raise ValueError('Valid values of testParameter2 must be greater than 0.0, but {} was assigned.'.format(testParameter2)) + """.trimMargin() + } + + @Test + fun `should handle constructors (boundaries, no attributes, call)`() { + val testConstructor = PythonConstructor( + callToOriginalAPI = callToOriginalAPI, + parameters = parametersWithBoundaries + ) + + testConstructor.toPythonCode() shouldBe """ + |def __init__(self, testParameter1, testParameter2): + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) + | if not 0.0 < testParameter2: + | raise ValueError('Valid values of testParameter2 must be greater than 0.0, but {} was assigned.'.format(testParameter2)) + | + | self.instance = OriginalClass() + """.trimMargin() + } + + @Test + fun `should handle constructors (boundaries, attributes, no call)`() { + val testConstructor = PythonConstructor( + parameters = parametersWithBoundaries + ) + testClass.constructor = testConstructor + + testConstructor.toPythonCode() shouldBe """ + |def __init__(self, testParameter1, testParameter2): + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) + | if not 0.0 < testParameter2: + | raise ValueError('Valid values of testParameter2 must be greater than 0.0, but {} was assigned.'.format(testParameter2)) + | + | self.testAttribute1 = 1 + | self.testAttribute2 = 2 + """.trimMargin() + } + + @Test + fun `should handle constructors (boundaries, attributes, call)`() { + val testConstructor = PythonConstructor( + callToOriginalAPI = callToOriginalAPI, + parameters = parametersWithBoundaries + ) + testClass.constructor = testConstructor + + testConstructor.toPythonCode() shouldBe """ + |def __init__(self, testParameter1, testParameter2): + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) + | if not 0.0 < testParameter2: + | raise ValueError('Valid values of testParameter2 must be greater than 0.0, but {} was assigned.'.format(testParameter2)) + | + | self.testAttribute1 = 1 + | self.testAttribute2 = 2 + | + | self.instance = OriginalClass() + """.trimMargin() } } @@ -1185,6 +444,11 @@ class PythonCodeGeneratorTest { } } // TODO + @Nested + inner class ParameterListToPythonCode { + + } // TODO + @Nested inner class ParameterToPythonCode { @@ -1230,9 +494,9 @@ class PythonCodeGeneratorTest { } -/* **************************************************************************************************************** - * Expressions - * ****************************************************************************************************************/ + /* **************************************************************************************************************** + * Expressions + * ****************************************************************************************************************/ @Nested inner class ExpressionToPythonCode { @@ -1305,9 +569,9 @@ class PythonCodeGeneratorTest { } -/* **************************************************************************************************************** - * Types - * ****************************************************************************************************************/ + /* **************************************************************************************************************** + * Types + * ****************************************************************************************************************/ @Nested inner class TypeToPythonCodeOrNull { @@ -1350,9 +614,9 @@ class PythonCodeGeneratorTest { } -/* **************************************************************************************************************** - * Other - * ****************************************************************************************************************/ + /* **************************************************************************************************************** + * Other + * ****************************************************************************************************************/ @Nested inner class ArgumentToPythonCode { @@ -1374,4 +638,9 @@ class PythonCodeGeneratorTest { testArgument.toPythonCode() shouldBe "arg=1" } } + + @Nested + inner class BoundaryToPythonCode { + + } // TODO } From 105e7bfb20688f9016953b91e8dd4d82e3e08e0b Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 10 Jan 2022 19:02:29 +0100 Subject: [PATCH 13/32] test: Python codegen for functions --- .../api_editor/codegen/PythonCodeGenerator.kt | 56 +++--- .../codegen/PythonCodeGeneratorTest.kt | 167 +++++++++++++----- 2 files changed, 149 insertions(+), 74 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt index 0cc2f9416..c23d9a464 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt @@ -146,35 +146,6 @@ private fun buildAllFunctions(pythonClass: PythonClass): List { return pythonClass.methods.map { it.toPythonCode().prependIndent(" ") } } -/** - * Builds a string containing the formatted function content - * @receiver The function whose adapter content should be built - * @return The string containing the formatted function content - */ -fun PythonFunction.toPythonCode(): String { - val function = """ - |def $name(${this.parameters.toPythonCode()}): - |${(buildFunctionBody(this)).prependIndent(" ")} - """.trimMargin() - - return when { - isStaticMethod() -> "@staticmethod\n$function" - else -> function - } -} - -private fun buildFunctionBody(pythonFunction: PythonFunction): String { - var formattedBoundaries = "" // pythonFunction.parameters.buildBoundaryChecks().joinToString("\n") - if (formattedBoundaries.isNotBlank()) { - formattedBoundaries = "$formattedBoundaries\n" - } - - return ( - formattedBoundaries + - "return " + pythonFunction.callToOriginalAPI!!.toPythonCode() - ) -} - /* ******************************************************************************************************************** * Declarations @@ -244,6 +215,33 @@ internal fun PythonEnumInstance.toPythonCode(): String { return "$name = ${value!!.toPythonCode()}" } +internal fun PythonFunction.toPythonCode() = buildString { + val parametersString = parameters.toPythonCode() + val boundariesString = parameters + .mapNotNull { it.boundary?.toPythonCode(it.name) } + .joinToString("\n") + val callString = callToOriginalAPI + ?.let { "return ${it.toPythonCode()}" } + ?: "" + + if (isStaticMethod()) { + appendLine("@staticmethod") + } + appendLine("def $name($parametersString):") + if (boundariesString.isNotBlank()) { + appendIndented(boundariesString) + if (callString.isNotBlank()) { + append("\n\n") + } + } + if (callString.isNotBlank()) { + appendIndented(callString) + } + if (boundariesString.isBlank() && callString.isBlank()) { + appendIndented("pass") + } +} + internal fun List.toPythonCode(): String { val assignedByToParameter = this@toPythonCode.groupBy { it.assignedBy } val implicitParametersString = assignedByToParameter[IMPLICIT] diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt index 4ac104b78..4620cbc76 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt @@ -349,71 +349,148 @@ class PythonCodeGeneratorTest { @Nested inner class FunctionToPythonCode { - @Test - fun `should access value of enum parameters`() { - val testParameter = PythonParameter(name = "testParameter") - val testFunction = PythonFunction( - name = "testFunction", - parameters = listOf( - testParameter + private lateinit var callToOriginalAPI: PythonCall + private lateinit var parametersWithBoundaries: List + + @BeforeEach + fun reset() { + callToOriginalAPI = PythonCall( + PythonReference( + PythonFunction(name = "testModule.testFunction") + ) + ) + parametersWithBoundaries = listOf( + PythonParameter( + name = "testParameter1", + boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("testModule.testFunction"), - arguments = listOf( - PythonArgument( - value = PythonMemberAccess( - receiver = PythonReference(testParameter), - member = PythonReference(PythonAttribute(name = "value")) - ) - ) + PythonParameter( + name = "testParameter2", + boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.UNRESTRICTED ) ) ) + } + + @Test + fun `should add staticmethod decorator to static methods`() { + val testFunction = PythonFunction( + name = "testFunction", + decorators = mutableListOf("staticmethod") + ) + PythonClass( + name = "TestClass", + methods = listOf(testFunction) + ) testFunction.toPythonCode() shouldBe """ - |def testFunction(testParameter): - | return testModule.testFunction(testParameter.value) + |@staticmethod + |def testFunction(): + | pass """.trimMargin() } @Test - fun `should access attribute of parameter objects`() { - val testParameter = PythonParameter(name = "testGroup") + fun `should create code for parameters`() { val testFunction = PythonFunction( name = "testFunction", parameters = listOf( - testParameter - ), - callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("testModule.testFunction"), - arguments = listOf( - PythonArgument( - value = PythonMemberAccess( - receiver = PythonReference(testParameter), - member = PythonReference( - PythonAttribute(name = "newParameter1") - ) - ) - ), - PythonArgument( - name = "oldParameter2", - value = PythonMemberAccess( - receiver = PythonReference(testParameter), - member = PythonReference( - PythonAttribute(name = "newParameter2") - ) - ) - ) + PythonParameter( + name = "self", + assignedBy = PythonParameterAssignment.IMPLICIT + ), + PythonParameter( + name = "positionOnly", + assignedBy = PythonParameterAssignment.POSITION_ONLY + ), + PythonParameter( + name = "positionOrName", + assignedBy = PythonParameterAssignment.POSITION_OR_NAME + ), + PythonParameter( + name = "nameOnly", + assignedBy = PythonParameterAssignment.NAME_ONLY ) ) ) testFunction.toPythonCode() shouldBe """ - |def testFunction(testGroup): - | return testModule.testFunction(testGroup.newParameter1, oldParameter2=testGroup.newParameter2) + |def testFunction(self, positionOnly, /, positionOrName, *, nameOnly): + | pass """.trimMargin() } - } // TODO + + @Test + fun `should handle functions (no boundaries, no call)`() { + val testFunction = PythonFunction( + name = "testFunction" + ) + + testFunction.toPythonCode() shouldBe """ + |def testFunction(): + | pass + """.trimMargin() + } + + @Test + fun `should handle functions (no boundaries, call)`() { + val testFunction = PythonFunction( + name = "testFunction", + callToOriginalAPI = callToOriginalAPI + ) + + testFunction.toPythonCode() shouldBe """ + |def testFunction(): + | return testModule.testFunction() + """.trimMargin() + } + + @Test + fun `should handle functions (boundaries, no call)`() { + val testFunction = PythonFunction( + name = "testFunction", + parameters = parametersWithBoundaries + ) + + testFunction.toPythonCode() shouldBe """ + |def testFunction(testParameter1, testParameter2): + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) + | if not 0.0 < testParameter2: + | raise ValueError('Valid values of testParameter2 must be greater than 0.0, but {} was assigned.'.format(testParameter2)) + """.trimMargin() + } + + @Test + fun `should handle functions (boundaries, call)`() { + val testFunction = PythonFunction( + name = "testFunction", + callToOriginalAPI = callToOriginalAPI, + parameters = parametersWithBoundaries + ) + + testFunction.toPythonCode() shouldBe """ + |def testFunction(testParameter1, testParameter2): + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) + | if not 0.0 < testParameter2: + | raise ValueError('Valid values of testParameter2 must be greater than 0.0, but {} was assigned.'.format(testParameter2)) + | + | return testModule.testFunction() + """.trimMargin() + } + } @Nested inner class ModuleToPythonCode { From 4eb581bad8187bbd1b9f0e979db1595c080ec98a Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 10 Jan 2022 19:16:58 +0100 Subject: [PATCH 14/32] test: Python codegen for boundaries --- .../api_editor/codegen/PythonCodeGenerator.kt | 18 +- .../codegen/PythonCodeGeneratorTest.kt | 178 +++++++++++++++++- 2 files changed, 179 insertions(+), 17 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt index c23d9a464..9d942af2d 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt @@ -1,7 +1,9 @@ package com.larsreimann.api_editor.codegen import com.larsreimann.api_editor.model.Boundary -import com.larsreimann.api_editor.model.ComparisonOperator +import com.larsreimann.api_editor.model.ComparisonOperator.LESS_THAN +import com.larsreimann.api_editor.model.ComparisonOperator.LESS_THAN_OR_EQUALS +import com.larsreimann.api_editor.model.ComparisonOperator.UNRESTRICTED import com.larsreimann.api_editor.model.PythonParameterAssignment.IMPLICIT import com.larsreimann.api_editor.model.PythonParameterAssignment.NAME_ONLY import com.larsreimann.api_editor.model.PythonParameterAssignment.POSITION_ONLY @@ -344,22 +346,24 @@ internal fun Boundary.toPythonCode(parameterName: String) = buildString { if (isDiscrete) { appendLine("if not (isinstance($parameterName, int) or (isinstance($parameterName, float) and $parameterName.is_integer())):") appendIndented("raise ValueError('$parameterName' needs to be an integer, but {} was assigned.'.format($parameterName))") - appendLine() + if (lowerLimitType != UNRESTRICTED || upperLimitType != UNRESTRICTED) { + appendLine() + } } - if (lowerLimitType != ComparisonOperator.UNRESTRICTED && upperLimitType != ComparisonOperator.UNRESTRICTED) { + if (lowerLimitType != UNRESTRICTED && upperLimitType != UNRESTRICTED) { appendLine("if not $lowerIntervalLimit ${lowerLimitType.operator} $parameterName ${upperLimitType.operator} ${upperIntervalLimit}:") appendIndented("raise ValueError('Valid values of $parameterName must be in ${asInterval()}, but {} was assigned.'.format($parameterName))") - } else if (lowerLimitType == ComparisonOperator.LESS_THAN) { + } else if (lowerLimitType == LESS_THAN) { appendLine("if not $lowerIntervalLimit < ${parameterName}:") appendIndented("raise ValueError('Valid values of $parameterName must be greater than $lowerIntervalLimit, but {} was assigned.'.format($parameterName))") - } else if (lowerLimitType == ComparisonOperator.LESS_THAN_OR_EQUALS) { + } else if (lowerLimitType == LESS_THAN_OR_EQUALS) { appendLine("if not $lowerIntervalLimit <= ${parameterName}:") appendIndented("raise ValueError('Valid values of $parameterName must be greater than or equal to $lowerIntervalLimit, but {} was assigned.'.format($parameterName))") - } else if (upperLimitType == ComparisonOperator.LESS_THAN) { + } else if (upperLimitType == LESS_THAN) { appendLine("if not $parameterName < ${upperIntervalLimit}:") appendIndented("raise ValueError('Valid values of $parameterName must be less than $upperIntervalLimit, but {} was assigned.'.format($parameterName))") - } else if (upperLimitType == ComparisonOperator.LESS_THAN_OR_EQUALS) { + } else if (upperLimitType == LESS_THAN_OR_EQUALS) { appendLine("if not $parameterName <= ${upperIntervalLimit}:") appendIndented("raise ValueError('Valid values of $parameterName must be less than or equal to $upperIntervalLimit, but {} was assigned.'.format($parameterName))") } diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt index 4620cbc76..d55c30714 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt @@ -1,7 +1,9 @@ package com.larsreimann.api_editor.codegen import com.larsreimann.api_editor.model.Boundary -import com.larsreimann.api_editor.model.ComparisonOperator +import com.larsreimann.api_editor.model.ComparisonOperator.LESS_THAN +import com.larsreimann.api_editor.model.ComparisonOperator.LESS_THAN_OR_EQUALS +import com.larsreimann.api_editor.model.ComparisonOperator.UNRESTRICTED import com.larsreimann.api_editor.model.PythonParameterAssignment import com.larsreimann.api_editor.mutable_model.PythonArgument import com.larsreimann.api_editor.mutable_model.PythonAttribute @@ -129,9 +131,9 @@ class PythonCodeGeneratorTest { boundary = Boundary( isDiscrete = false, lowerIntervalLimit = 0.0, - lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + lowerLimitType = LESS_THAN_OR_EQUALS, upperIntervalLimit = 1.0, - upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + upperLimitType = LESS_THAN_OR_EQUALS ) ), PythonParameter( @@ -139,9 +141,9 @@ class PythonCodeGeneratorTest { boundary = Boundary( isDiscrete = false, lowerIntervalLimit = 0.0, - lowerLimitType = ComparisonOperator.LESS_THAN, + lowerLimitType = LESS_THAN, upperIntervalLimit = 1.0, - upperLimitType = ComparisonOperator.UNRESTRICTED + upperLimitType = UNRESTRICTED ) ) ) @@ -365,9 +367,9 @@ class PythonCodeGeneratorTest { boundary = Boundary( isDiscrete = false, lowerIntervalLimit = 0.0, - lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + lowerLimitType = LESS_THAN_OR_EQUALS, upperIntervalLimit = 1.0, - upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + upperLimitType = LESS_THAN_OR_EQUALS ) ), PythonParameter( @@ -375,9 +377,9 @@ class PythonCodeGeneratorTest { boundary = Boundary( isDiscrete = false, lowerIntervalLimit = 0.0, - lowerLimitType = ComparisonOperator.LESS_THAN, + lowerLimitType = LESS_THAN, upperIntervalLimit = 1.0, - upperLimitType = ComparisonOperator.UNRESTRICTED + upperLimitType = UNRESTRICTED ) ) ) @@ -719,5 +721,161 @@ class PythonCodeGeneratorTest { @Nested inner class BoundaryToPythonCode { - } // TODO + @Test + fun `should add an extra check for discrete boundaries`() { + val boundary = Boundary( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = UNRESTRICTED, + upperIntervalLimit = 1.0, + upperLimitType = UNRESTRICTED + ) + + boundary.toPythonCode("testParameter") shouldBe """ + |if not (isinstance(testParameter, int) or (isinstance(testParameter, float) and testParameter.is_integer())): + | raise ValueError('testParameter' needs to be an integer, but {} was assigned.'.format(testParameter)) + """.trimMargin() + } + + @Test + fun `should handle continuous boundaries (UNRESTRICTED, UNRESTRICTED)`() { + val boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = UNRESTRICTED, + upperIntervalLimit = 1.0, + upperLimitType = UNRESTRICTED + ) + + boundary.toPythonCode("testParameter") shouldBe "" + } + + @Test + fun `should handle continuous boundaries (UNRESTRICTED, LESS_THAN)`() { + val boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = UNRESTRICTED, + upperIntervalLimit = 1.0, + upperLimitType = LESS_THAN + ) + + boundary.toPythonCode("testParameter") shouldBe """ + |if not testParameter < 1.0: + | raise ValueError('Valid values of testParameter must be less than 1.0, but {} was assigned.'.format(testParameter)) + """.trimMargin() + } + + @Test + fun `should handle continuous boundaries (UNRESTRICTED, LESS_THAN_OR_EQUALS)`() { + val boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = UNRESTRICTED, + upperIntervalLimit = 1.0, + upperLimitType = LESS_THAN_OR_EQUALS + ) + + boundary.toPythonCode("testParameter") shouldBe """ + |if not testParameter <= 1.0: + | raise ValueError('Valid values of testParameter must be less than or equal to 1.0, but {} was assigned.'.format(testParameter)) + """.trimMargin() + } + + @Test + fun `should handle continuous boundaries (LESS_THAN, UNRESTRICTED)`() { + val boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = LESS_THAN, + upperIntervalLimit = 1.0, + upperLimitType = UNRESTRICTED + ) + + boundary.toPythonCode("testParameter") shouldBe """ + |if not 0.0 < testParameter: + | raise ValueError('Valid values of testParameter must be greater than 0.0, but {} was assigned.'.format(testParameter)) + """.trimMargin() + } + + @Test + fun `should handle continuous boundaries (LESS_THAN, LESS_THAN)`() { + val boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = LESS_THAN, + upperIntervalLimit = 1.0, + upperLimitType = LESS_THAN + ) + + boundary.toPythonCode("testParameter") shouldBe """ + |if not 0.0 < testParameter < 1.0: + | raise ValueError('Valid values of testParameter must be in (0.0, 1.0), but {} was assigned.'.format(testParameter)) + """.trimMargin() + } + + @Test + fun `should handle continuous boundaries (LESS_THAN, LESS_THAN_OR_EQUALS)`() { + val boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = LESS_THAN, + upperIntervalLimit = 1.0, + upperLimitType = LESS_THAN_OR_EQUALS + ) + + boundary.toPythonCode("testParameter") shouldBe """ + |if not 0.0 < testParameter <= 1.0: + | raise ValueError('Valid values of testParameter must be in (0.0, 1.0], but {} was assigned.'.format(testParameter)) + """.trimMargin() + } + + @Test + fun `should handle continuous boundaries (LESS_THAN_OR_EQUALS, UNRESTRICTED)`() { + val boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = UNRESTRICTED + ) + + boundary.toPythonCode("testParameter") shouldBe """ + |if not 0.0 <= testParameter: + | raise ValueError('Valid values of testParameter must be greater than or equal to 0.0, but {} was assigned.'.format(testParameter)) + """.trimMargin() + } + + @Test + fun `should handle continuous boundaries (LESS_THAN_OR_EQUALS, LESS_THAN)`() { + val boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = LESS_THAN + ) + + boundary.toPythonCode("testParameter") shouldBe """ + |if not 0.0 <= testParameter < 1.0: + | raise ValueError('Valid values of testParameter must be in [0.0, 1.0), but {} was assigned.'.format(testParameter)) + """.trimMargin() + } + + @Test + fun `should handle continuous boundaries (LESS_THAN_OR_EQUALS, LESS_THAN_OR_EQUALS)`() { + val boundary = Boundary( + isDiscrete = false, + lowerIntervalLimit = 0.0, + lowerLimitType = LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = LESS_THAN_OR_EQUALS + ) + + boundary.toPythonCode("testParameter") shouldBe """ + |if not 0.0 <= testParameter <= 1.0: + | raise ValueError('Valid values of testParameter must be in [0.0, 1.0], but {} was assigned.'.format(testParameter)) + """.trimMargin() + } + } } From e0024ad8690784566a01723531e73dd353708ccc Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 10 Jan 2022 19:29:03 +0100 Subject: [PATCH 15/32] test: Python codegen for parameter lists --- .../api_editor/codegen/PythonCodeGenerator.kt | 18 +-- .../codegen/PythonCodeGeneratorTest.kt | 138 +++++++++++++++++- 2 files changed, 140 insertions(+), 16 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt index 9d942af2d..4cfe1d9d2 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt @@ -39,8 +39,8 @@ import com.larsreimann.modeling.closest fun PythonModule.toPythonCode(): String { var formattedImport = buildNamespace(this) var formattedEnums = enums.joinToString("\n") { it.toPythonCode() } - var formattedClasses = buildAllClasses(this) - var formattedFunctions = buildAllFunctions(this) + var formattedClasses = this.classes.joinToString("\n".repeat(2)) { it.toPythonCode() } + var formattedFunctions = this.functions.joinToString("\n".repeat(2)) { it.toPythonCode() } val separators = buildSeparators( formattedImport, formattedClasses, formattedFunctions ) @@ -87,14 +87,6 @@ private fun buildParentDeclarationName(qualifiedName: String): String { return qualifiedName.substring(0, separationPosition) } -private fun buildAllClasses(pythonModule: PythonModule): String { - return pythonModule.classes.joinToString("\n".repeat(2)) { it.toPythonCode() } -} - -private fun buildAllFunctions(pythonModule: PythonModule): String { - return pythonModule.functions.joinToString("\n".repeat(2)) { it.toPythonCode() } -} - private fun buildSeparators( formattedImports: String, formattedClasses: String, @@ -136,7 +128,7 @@ fun PythonClass.toPythonCode(): String { if (constructor != null) { formattedClass += "\n\n" } - formattedClass += buildAllFunctions(this).joinToString("\n".repeat(2)) + formattedClass += this.methods.map { it.toPythonCode().prependIndent(" ") }.joinToString("\n".repeat(2)) } if (constructor == null && methods.isEmpty()) { formattedClass += " pass" @@ -144,10 +136,6 @@ fun PythonClass.toPythonCode(): String { return formattedClass } -private fun buildAllFunctions(pythonClass: PythonClass): List { - return pythonClass.methods.map { it.toPythonCode().prependIndent(" ") } -} - /* ******************************************************************************************************************** * Declarations diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt index d55c30714..c260ac105 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt @@ -526,7 +526,143 @@ class PythonCodeGeneratorTest { @Nested inner class ParameterListToPythonCode { - } // TODO + private lateinit var implicit: PythonParameter + private lateinit var positionOnly: PythonParameter + private lateinit var positionOrName: PythonParameter + private lateinit var nameOnly: PythonParameter + + @BeforeEach + fun reset() { + implicit = PythonParameter( + name = "implicit", + assignedBy = PythonParameterAssignment.IMPLICIT + ) + positionOnly = PythonParameter( + name = "positionOnly", + assignedBy = PythonParameterAssignment.POSITION_ONLY + ) + positionOrName = PythonParameter( + name = "positionOrName", + assignedBy = PythonParameterAssignment.POSITION_OR_NAME + ) + nameOnly = PythonParameter( + name = "nameOnly", + assignedBy = PythonParameterAssignment.NAME_ONLY + ) + } + + @Test + fun `should handle parameter lists (no IMPLICIT, no POSITION_ONLY, no POSITION_OR_NAME, no NAME_ONLY`() { + val parameters = listOf() + + parameters.toPythonCode() shouldBe "" + } + + @Test + fun `should handle parameter lists (no IMPLICIT, no POSITION_ONLY, no POSITION_OR_NAME, NAME_ONLY`() { + val parameters = listOf(nameOnly) + + parameters.toPythonCode() shouldBe "*, nameOnly" + } + + @Test + fun `should handle parameter lists (no IMPLICIT, no POSITION_ONLY, POSITION_OR_NAME, no NAME_ONLY`() { + val parameters = listOf(positionOrName) + + parameters.toPythonCode() shouldBe "positionOrName" + } + + @Test + fun `should handle parameter lists (no IMPLICIT, no POSITION_ONLY, POSITION_OR_NAME, NAME_ONLY`() { + val parameters = listOf(positionOrName, nameOnly) + + parameters.toPythonCode() shouldBe "positionOrName, *, nameOnly" + } + + @Test + fun `should handle parameter lists (no IMPLICIT, POSITION_ONLY, no POSITION_OR_NAME, no NAME_ONLY`() { + val parameters = listOf(positionOnly) + + parameters.toPythonCode() shouldBe "positionOnly, /" + } + + @Test + fun `should handle parameter lists (no IMPLICIT, POSITION_ONLY, no POSITION_OR_NAME, NAME_ONLY`() { + val parameters = listOf(positionOnly, nameOnly) + + parameters.toPythonCode() shouldBe "positionOnly, /, *, nameOnly" + } + + @Test + fun `should handle parameter lists (no IMPLICIT, POSITION_ONLY, POSITION_OR_NAME, no NAME_ONLY`() { + val parameters = listOf(positionOnly, positionOrName) + + parameters.toPythonCode() shouldBe "positionOnly, /, positionOrName" + } + + @Test + fun `should handle parameter lists (no IMPLICIT, POSITION_ONLY, POSITION_OR_NAME, NAME_ONLY`() { + val parameters = listOf(positionOnly, positionOrName, nameOnly) + + parameters.toPythonCode() shouldBe "positionOnly, /, positionOrName, *, nameOnly" + } + + @Test + fun `should handle parameter lists (IMPLICIT, no POSITION_ONLY, no POSITION_OR_NAME, no NAME_ONLY`() { + val parameters = listOf(implicit) + + parameters.toPythonCode() shouldBe "implicit" + } + + @Test + fun `should handle parameter lists (IMPLICIT, no POSITION_ONLY, no POSITION_OR_NAME, NAME_ONLY`() { + val parameters = listOf(implicit, nameOnly) + + parameters.toPythonCode() shouldBe "implicit, *, nameOnly" + } + + @Test + fun `should handle parameter lists (IMPLICIT, no POSITION_ONLY, POSITION_OR_NAME, no NAME_ONLY`() { + val parameters = listOf(implicit, positionOrName) + + parameters.toPythonCode() shouldBe "implicit, positionOrName" + } + + @Test + fun `should handle parameter lists (IMPLICIT, no POSITION_ONLY, POSITION_OR_NAME, NAME_ONLY`() { + val parameters = listOf(implicit, positionOrName, nameOnly) + + parameters.toPythonCode() shouldBe "implicit, positionOrName, *, nameOnly" + } + + @Test + fun `should handle parameter lists (IMPLICIT, POSITION_ONLY, no POSITION_OR_NAME, no NAME_ONLY`() { + val parameters = listOf(implicit, positionOnly) + + parameters.toPythonCode() shouldBe "implicit, positionOnly, /" + } + + @Test + fun `should handle parameter lists (IMPLICIT, POSITION_ONLY, no POSITION_OR_NAME, NAME_ONLY`() { + val parameters = listOf(implicit, positionOnly, nameOnly) + + parameters.toPythonCode() shouldBe "implicit, positionOnly, /, *, nameOnly" + } + + @Test + fun `should handle parameter lists (IMPLICIT, POSITION_ONLY, POSITION_OR_NAME, no NAME_ONLY`() { + val parameters = listOf(implicit, positionOnly, positionOrName) + + parameters.toPythonCode() shouldBe "implicit, positionOnly, /, positionOrName" + } + + @Test + fun `should handle parameter lists (IMPLICIT, POSITION_ONLY, POSITION_OR_NAME, NAME_ONLY`() { + val parameters = listOf(implicit, positionOnly, positionOrName, nameOnly) + + parameters.toPythonCode() shouldBe "implicit, positionOnly, /, positionOrName, *, nameOnly" + } + } @Nested inner class ParameterToPythonCode { From f456820ca31ae465811a36d54dbd92f49bdbfa20 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 10 Jan 2022 19:40:40 +0100 Subject: [PATCH 16/32] test: Python codegen for classes --- .../api_editor/codegen/PythonCodeGenerator.kt | 44 ++++++------- .../codegen/PythonCodeGeneratorTest.kt | 66 ++++++++++++++++++- 2 files changed, 84 insertions(+), 26 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt index 4cfe1d9d2..eef5451b4 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt @@ -114,29 +114,6 @@ private fun buildSeparators( return arrayOf(importSeparator, classesSeparator, functionSeparator) } -/** - * Builds a string containing the formatted class content - * @receiver The module whose adapter content should be built - * @return The string containing the formatted class content - */ -fun PythonClass.toPythonCode(): String { - var formattedClass = "class $name:\n" - if (constructor != null) { - formattedClass += constructor!!.toPythonCode().prependIndent(" ") - } - if (!methods.isEmpty()) { - if (constructor != null) { - formattedClass += "\n\n" - } - formattedClass += this.methods.map { it.toPythonCode().prependIndent(" ") }.joinToString("\n".repeat(2)) - } - if (constructor == null && methods.isEmpty()) { - formattedClass += " pass" - } - return formattedClass -} - - /* ******************************************************************************************************************** * Declarations * ********************************************************************************************************************/ @@ -151,6 +128,27 @@ internal fun PythonAttribute.toPythonCode() = buildString { } } +internal fun PythonClass.toPythonCode() = buildString { + val constructorString = constructor?.toPythonCode() ?: "" + val methodsString = methods.joinToString("\n\n") { + it.toPythonCode().prependIndent(" ") + } + + appendLine("class $name:") + if (constructorString.isNotBlank()) { + appendIndented(constructorString) + if (methodsString.isNotBlank()) { + append("\n\n") + } + } + if (methodsString.isNotBlank()) { + append(methodsString) + } + if (constructorString.isBlank() && methodsString.isBlank()) { + appendIndented("pass") + } +} + internal fun PythonConstructor.toPythonCode() = buildString { val parametersString = parameters.toPythonCode() val boundariesString = parameters diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt index c260ac105..9635a308f 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt @@ -84,15 +84,75 @@ class PythonCodeGeneratorTest { inner class ClassToPythonCode { @Test - fun `should create valid code for empty classes`() { - val testClass = PythonClass(name = "TestClass") + fun `should create valid code without constructor and methods`() { + val testClass = PythonClass( + name = "TestClass" + ) testClass.toPythonCode() shouldBe """ |class TestClass: | pass """.trimMargin() } - } // TODO + + @Test + fun `should create valid code without constructor but with methods`() { + val testClass = PythonClass( + name = "TestClass", + methods = listOf( + PythonFunction(name = "testFunction1"), + PythonFunction(name = "testFunction2") + ) + ) + + testClass.toPythonCode() shouldBe """ + |class TestClass: + | def testFunction1(): + | pass + | + | def testFunction2(): + | pass + """.trimMargin() + } + + @Test + fun `should create valid code with constructor but without methods`() { + val testClass = PythonClass( + name = "TestClass", + constructor = PythonConstructor() + ) + + testClass.toPythonCode() shouldBe """ + |class TestClass: + | def __init__(): + | pass + """.trimMargin() + } + + @Test + fun `should create valid code with constructor and methods`() { + val testClass = PythonClass( + name = "TestClass", + constructor = PythonConstructor(), + methods = listOf( + PythonFunction(name = "testFunction1"), + PythonFunction(name = "testFunction2") + ) + ) + + testClass.toPythonCode() shouldBe """ + |class TestClass: + | def __init__(): + | pass + | + | def testFunction1(): + | pass + | + | def testFunction2(): + | pass + """.trimMargin() + } + } @Nested inner class ConstructorToPythonCode { From ebb08b61dfe2aca347612c5623f767b9a0fe5b16 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 10 Jan 2022 20:40:15 +0100 Subject: [PATCH 17/32] test: Python codegen for modules --- .../api_editor/codegen/PythonCodeGenerator.kt | 113 ++++----- .../codegen/PythonCodeGeneratorTest.kt | 221 +++++++++++++++--- 2 files changed, 234 insertions(+), 100 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt index eef5451b4..d7f179046 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt @@ -31,92 +31,57 @@ import com.larsreimann.api_editor.mutable_model.PythonStringifiedType import com.larsreimann.api_editor.mutable_model.PythonType import com.larsreimann.modeling.closest -/** - * Builds a string containing the formatted module content - * @receiver The module whose adapter content should be built - * @return The string containing the formatted module content - */ +/* ******************************************************************************************************************** + * Declarations + * ********************************************************************************************************************/ + fun PythonModule.toPythonCode(): String { - var formattedImport = buildNamespace(this) - var formattedEnums = enums.joinToString("\n") { it.toPythonCode() } - var formattedClasses = this.classes.joinToString("\n".repeat(2)) { it.toPythonCode() } - var formattedFunctions = this.functions.joinToString("\n".repeat(2)) { it.toPythonCode() } - val separators = buildSeparators( - formattedImport, formattedClasses, formattedFunctions + val strings = listOf( + importsToPythonCode(), + classes.joinToString("\n\n") { it.toPythonCode() }, + functions.joinToString("\n\n") { it.toPythonCode() }, + enums.joinToString("\n\n") { it.toPythonCode() } ) - formattedImport += separators[0] - if (formattedEnums.isNotBlank()) { - formattedEnums += "\n" - } - formattedClasses += separators[1] - formattedFunctions += separators[2] - return ( - formattedImport + - formattedEnums + - formattedClasses + - formattedFunctions - ) -} -private fun buildNamespace(pythonModule: PythonModule): String { - val importedModules = HashSet() - pythonModule.functions.forEach { pythonFunction: PythonFunction -> - val receiver = pythonFunction.callToOriginalAPI?.receiver - if (receiver is PythonStringifiedExpression) { - importedModules.add(buildParentDeclarationName(receiver.string)) - } - } - pythonModule.classes.forEach { pythonClass: PythonClass -> - if (pythonClass.originalClass != null) { - importedModules.add( - buildParentDeclarationName(pythonClass.originalClass!!.qualifiedName) - ) - } + val joinedStrings = strings + .filter { it.isNotBlank() } + .joinToString("\n\n") - } - var result = importedModules.joinToString("\n") { "import $it" } - if (pythonModule.enums.isNotEmpty()) { - result = "from enum import Enum\n$result" - } - return result + return "$joinedStrings\n" } -private fun buildParentDeclarationName(qualifiedName: String): String { - val pathSeparator = "." - val separationPosition = qualifiedName.lastIndexOf(pathSeparator) - return qualifiedName.substring(0, separationPosition) -} +private fun PythonModule.importsToPythonCode() = buildString { + val imports = functions + .mapNotNull { + when (val receiver = it.callToOriginalAPI?.receiver) { + is PythonStringifiedExpression -> receiver.string.parentQualifiedName() + else -> null + } + }.toSet() -private fun buildSeparators( - formattedImports: String, - formattedClasses: String, - formattedFunctions: String -): Array { - val importSeparator: String = if (formattedImports.isBlank()) { - "" - } else if (formattedClasses.isBlank() && formattedFunctions.isBlank()) { - "\n" - } else { - "\n\n" + val importsString = imports.joinToString("\n") { "import $it" } + val fromImportsString = when { + enums.isEmpty() -> "" + else -> "from enum import Enum" } - val classesSeparator: String = if (formattedClasses.isBlank()) { - "" - } else if (formattedFunctions.isBlank()) { - "\n" - } else { - "\n\n" + + if (importsString.isNotBlank()) { + append(importsString) + + if (fromImportsString.isNotBlank()) { + append("\n\n") + } } - val functionSeparator: String = if (formattedFunctions.isBlank()) { - "" - } else { - "\n" + if (fromImportsString.isNotBlank()) { + append(fromImportsString) } - return arrayOf(importSeparator, classesSeparator, functionSeparator) } -/* ******************************************************************************************************************** - * Declarations - * ********************************************************************************************************************/ +private fun String.parentQualifiedName(): String { + val pathSeparator = "." + val separationPosition = lastIndexOf(pathSeparator) + return substring(0, separationPosition) +} internal fun PythonAttribute.toPythonCode() = buildString { append("self.$name") diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt index 9635a308f..a8e26dbd1 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt @@ -5,6 +5,7 @@ import com.larsreimann.api_editor.model.ComparisonOperator.LESS_THAN import com.larsreimann.api_editor.model.ComparisonOperator.LESS_THAN_OR_EQUALS import com.larsreimann.api_editor.model.ComparisonOperator.UNRESTRICTED import com.larsreimann.api_editor.model.PythonParameterAssignment +import com.larsreimann.api_editor.mutable_model.OriginalPythonClass import com.larsreimann.api_editor.mutable_model.PythonArgument import com.larsreimann.api_editor.mutable_model.PythonAttribute import com.larsreimann.api_editor.mutable_model.PythonBoolean @@ -557,18 +558,133 @@ class PythonCodeGeneratorTest { @Nested inner class ModuleToPythonCode { - @Test - fun `should import Enum if the module contains enums`() { - val testModule = PythonModule( - name = "testModule", - enums = listOf( - PythonEnum(name = "TestEnum") + private lateinit var testModule: PythonModule + private lateinit var testClasses: List + private lateinit var testFunctions: List + private lateinit var testEnum: PythonEnum + + @BeforeEach + fun reset() { + testModule = PythonModule(name = "testModule") + testClasses = listOf( + PythonClass( + name = "TestClass", + methods = listOf( + PythonFunction(name = "testMethod1"), + PythonFunction(name = "testMethod2") + ) + ), + PythonClass( + name = "TestClassWithOriginalClass", + originalClass = OriginalPythonClass( + qualifiedName = "originalModule.TestClassWithOriginalClass" + ) ) ) + testFunctions = listOf( + PythonFunction(name = "testFunction"), + PythonFunction( + name = "testFunctionWithOriginalFunction", + callToOriginalAPI = PythonCall( + receiver = PythonStringifiedExpression("originalModule.testFunctionWithOriginalFunction") + ) + ) + ) + testEnum = PythonEnum(name = "TestEnum") + } + + @Test + fun `should create Python code for modules (no classes, no functions, no enums)`() { + testModule.toPythonCode() shouldBe "\n" + } + + @Test + fun `should create Python code for modules (no classes, no functions, enums)`() { + testModule.enums += testEnum + + testModule.toPythonCode() shouldBe """ + |from enum import Enum + | + |class TestEnum(Enum): + | pass + | + """.trimMargin() + } + + @Test + fun `should create Python code for modules (no classes, functions, no enums)`() { + testModule.functions += testFunctions + + testModule.toPythonCode() shouldBe """ + |import originalModule + | + |def testFunction(): + | pass + | + |def testFunctionWithOriginalFunction(): + | return originalModule.testFunctionWithOriginalFunction() + | + """.trimMargin() + } + + @Test + fun `should create Python code for modules (no classes, functions, enums)`() { + testModule.functions += testFunctions + testModule.enums += testEnum + + testModule.toPythonCode() shouldBe """ + |import originalModule + | + |from enum import Enum + | + |def testFunction(): + | pass + | + |def testFunctionWithOriginalFunction(): + | return originalModule.testFunctionWithOriginalFunction() + | + |class TestEnum(Enum): + | pass + | + """.trimMargin() + } + + @Test + fun `should create Python code for modules (classes, no functions, no enums)`() { + testModule.classes += testClasses + + testModule.toPythonCode() shouldBe """ + |class TestClass: + | def testMethod1(): + | pass + | + | def testMethod2(): + | pass + | + |class TestClassWithOriginalClass: + | pass + | + """.trimMargin() + } + + @Test + fun `should create Python code for modules (classes, no functions, enums)`() { + testModule.classes += testClasses + testModule.enums += testEnum testModule.toPythonCode() shouldBe """ |from enum import Enum | + |class TestClass: + | def testMethod1(): + | pass + | + | def testMethod2(): + | pass + | + |class TestClassWithOriginalClass: + | pass + | |class TestEnum(Enum): | pass | @@ -576,12 +692,65 @@ class PythonCodeGeneratorTest { } @Test - fun `should not import Enum if the module does not contain enums`() { - val testModule = PythonModule(name = "testModule") + fun `should create Python code for modules (classes, functions, no enums)`() { + testModule.classes += testClasses + testModule.functions += testFunctions + + testModule.toPythonCode() shouldBe """ + |import originalModule + | + |class TestClass: + | def testMethod1(): + | pass + | + | def testMethod2(): + | pass + | + |class TestClassWithOriginalClass: + | pass + | + |def testFunction(): + | pass + | + |def testFunctionWithOriginalFunction(): + | return originalModule.testFunctionWithOriginalFunction() + | + """.trimMargin() + } + + @Test + fun `should create Python code for modules (classes, functions, enums)`() { + testModule.classes += testClasses + testModule.functions += testFunctions + testModule.enums += testEnum - testModule.toPythonCode() shouldBe "" + testModule.toPythonCode() shouldBe """ + |import originalModule + | + |from enum import Enum + | + |class TestClass: + | def testMethod1(): + | pass + | + | def testMethod2(): + | pass + | + |class TestClassWithOriginalClass: + | pass + | + |def testFunction(): + | pass + | + |def testFunctionWithOriginalFunction(): + | return originalModule.testFunctionWithOriginalFunction() + | + |class TestEnum(Enum): + | pass + | + """.trimMargin() } - } // TODO + } @Nested inner class ParameterListToPythonCode { @@ -612,112 +781,112 @@ class PythonCodeGeneratorTest { } @Test - fun `should handle parameter lists (no IMPLICIT, no POSITION_ONLY, no POSITION_OR_NAME, no NAME_ONLY`() { + fun `should handle parameter lists (no IMPLICIT, no POSITION_ONLY, no POSITION_OR_NAME, no NAME_ONLY)`() { val parameters = listOf() parameters.toPythonCode() shouldBe "" } @Test - fun `should handle parameter lists (no IMPLICIT, no POSITION_ONLY, no POSITION_OR_NAME, NAME_ONLY`() { + fun `should handle parameter lists (no IMPLICIT, no POSITION_ONLY, no POSITION_OR_NAME, NAME_ONLY)`() { val parameters = listOf(nameOnly) parameters.toPythonCode() shouldBe "*, nameOnly" } @Test - fun `should handle parameter lists (no IMPLICIT, no POSITION_ONLY, POSITION_OR_NAME, no NAME_ONLY`() { + fun `should handle parameter lists (no IMPLICIT, no POSITION_ONLY, POSITION_OR_NAME, no NAME_ONLY)`() { val parameters = listOf(positionOrName) parameters.toPythonCode() shouldBe "positionOrName" } @Test - fun `should handle parameter lists (no IMPLICIT, no POSITION_ONLY, POSITION_OR_NAME, NAME_ONLY`() { + fun `should handle parameter lists (no IMPLICIT, no POSITION_ONLY, POSITION_OR_NAME, NAME_ONLY)`() { val parameters = listOf(positionOrName, nameOnly) parameters.toPythonCode() shouldBe "positionOrName, *, nameOnly" } @Test - fun `should handle parameter lists (no IMPLICIT, POSITION_ONLY, no POSITION_OR_NAME, no NAME_ONLY`() { + fun `should handle parameter lists (no IMPLICIT, POSITION_ONLY, no POSITION_OR_NAME, no NAME_ONLY)`() { val parameters = listOf(positionOnly) parameters.toPythonCode() shouldBe "positionOnly, /" } @Test - fun `should handle parameter lists (no IMPLICIT, POSITION_ONLY, no POSITION_OR_NAME, NAME_ONLY`() { + fun `should handle parameter lists (no IMPLICIT, POSITION_ONLY, no POSITION_OR_NAME, NAME_ONLY)`() { val parameters = listOf(positionOnly, nameOnly) parameters.toPythonCode() shouldBe "positionOnly, /, *, nameOnly" } @Test - fun `should handle parameter lists (no IMPLICIT, POSITION_ONLY, POSITION_OR_NAME, no NAME_ONLY`() { + fun `should handle parameter lists (no IMPLICIT, POSITION_ONLY, POSITION_OR_NAME, no NAME_ONLY)`() { val parameters = listOf(positionOnly, positionOrName) parameters.toPythonCode() shouldBe "positionOnly, /, positionOrName" } @Test - fun `should handle parameter lists (no IMPLICIT, POSITION_ONLY, POSITION_OR_NAME, NAME_ONLY`() { + fun `should handle parameter lists (no IMPLICIT, POSITION_ONLY, POSITION_OR_NAME, NAME_ONLY)`() { val parameters = listOf(positionOnly, positionOrName, nameOnly) parameters.toPythonCode() shouldBe "positionOnly, /, positionOrName, *, nameOnly" } @Test - fun `should handle parameter lists (IMPLICIT, no POSITION_ONLY, no POSITION_OR_NAME, no NAME_ONLY`() { + fun `should handle parameter lists (IMPLICIT, no POSITION_ONLY, no POSITION_OR_NAME, no NAME_ONLY)`() { val parameters = listOf(implicit) parameters.toPythonCode() shouldBe "implicit" } @Test - fun `should handle parameter lists (IMPLICIT, no POSITION_ONLY, no POSITION_OR_NAME, NAME_ONLY`() { + fun `should handle parameter lists (IMPLICIT, no POSITION_ONLY, no POSITION_OR_NAME, NAME_ONLY)`() { val parameters = listOf(implicit, nameOnly) parameters.toPythonCode() shouldBe "implicit, *, nameOnly" } @Test - fun `should handle parameter lists (IMPLICIT, no POSITION_ONLY, POSITION_OR_NAME, no NAME_ONLY`() { + fun `should handle parameter lists (IMPLICIT, no POSITION_ONLY, POSITION_OR_NAME, no NAME_ONLY)`() { val parameters = listOf(implicit, positionOrName) parameters.toPythonCode() shouldBe "implicit, positionOrName" } @Test - fun `should handle parameter lists (IMPLICIT, no POSITION_ONLY, POSITION_OR_NAME, NAME_ONLY`() { + fun `should handle parameter lists (IMPLICIT, no POSITION_ONLY, POSITION_OR_NAME, NAME_ONLY)`() { val parameters = listOf(implicit, positionOrName, nameOnly) parameters.toPythonCode() shouldBe "implicit, positionOrName, *, nameOnly" } @Test - fun `should handle parameter lists (IMPLICIT, POSITION_ONLY, no POSITION_OR_NAME, no NAME_ONLY`() { + fun `should handle parameter lists (IMPLICIT, POSITION_ONLY, no POSITION_OR_NAME, no NAME_ONLY)`() { val parameters = listOf(implicit, positionOnly) parameters.toPythonCode() shouldBe "implicit, positionOnly, /" } @Test - fun `should handle parameter lists (IMPLICIT, POSITION_ONLY, no POSITION_OR_NAME, NAME_ONLY`() { + fun `should handle parameter lists (IMPLICIT, POSITION_ONLY, no POSITION_OR_NAME, NAME_ONLY)`() { val parameters = listOf(implicit, positionOnly, nameOnly) parameters.toPythonCode() shouldBe "implicit, positionOnly, /, *, nameOnly" } @Test - fun `should handle parameter lists (IMPLICIT, POSITION_ONLY, POSITION_OR_NAME, no NAME_ONLY`() { + fun `should handle parameter lists (IMPLICIT, POSITION_ONLY, POSITION_OR_NAME, no NAME_ONLY)`() { val parameters = listOf(implicit, positionOnly, positionOrName) parameters.toPythonCode() shouldBe "implicit, positionOnly, /, positionOrName" } @Test - fun `should handle parameter lists (IMPLICIT, POSITION_ONLY, POSITION_OR_NAME, NAME_ONLY`() { + fun `should handle parameter lists (IMPLICIT, POSITION_ONLY, POSITION_OR_NAME, NAME_ONLY)`() { val parameters = listOf(implicit, positionOnly, positionOrName, nameOnly) parameters.toPythonCode() shouldBe "implicit, positionOnly, /, positionOrName, *, nameOnly" From 833c65d6e8472e52246c37d12df087400ef08d6e Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 10 Jan 2022 21:14:09 +0100 Subject: [PATCH 18/32] feat: `from __future__ import annotations` --- .../api_editor/codegen/PythonCodeGenerator.kt | 6 +++--- .../api_editor/mutable_model/PythonAst.kt | 16 +++++++++++--- .../GroupAnnotationProcessor.kt | 21 +++++++++++++++++-- .../transformation/Postprocessor.kt | 2 +- .../codegen/PythonCodeGeneratorTest.kt | 12 ++++++++++- 5 files changed, 47 insertions(+), 10 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt index d7f179046..94a33366b 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt @@ -60,9 +60,9 @@ private fun PythonModule.importsToPythonCode() = buildString { }.toSet() val importsString = imports.joinToString("\n") { "import $it" } - val fromImportsString = when { - enums.isEmpty() -> "" - else -> "from enum import Enum" + var fromImportsString = "from __future__ import annotations" + if (enums.isNotEmpty()) { + fromImportsString += "\nfrom enum import Enum" } if (importsString.isNotBlank()) { diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt index 80728fefb..465ac3b8d 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt @@ -272,13 +272,23 @@ data class PythonStringifiedExpression(val string: String) : PythonExpression() * Types * ********************************************************************************************************************/ -sealed class PythonType : PythonAstNode() +sealed class PythonType : PythonAstNode() { + abstract fun copy(): PythonType +} -class PythonNamedType(declaration: PythonDeclaration) : PythonType() { +class PythonNamedType(declaration: PythonDeclaration?) : PythonType() { var declaration by CrossReference(declaration) + + override fun copy(): PythonNamedType { + return PythonNamedType(declaration) + } } -data class PythonStringifiedType(val string: String) : PythonType() +data class PythonStringifiedType(val string: String) : PythonType() { + override fun copy(): PythonStringifiedType { + return PythonStringifiedType(string) + } +} /* ******************************************************************************************************************** diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt index c54da1d59..f73153051 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt @@ -78,10 +78,27 @@ private fun PythonFunction.processGroupAnnotations(module: PythonModule) { receiver = PythonReference(declaration = groupedParameter), member = PythonReference(PythonAttribute(name = value.declaration!!.name)) ) + } else if (value is PythonMemberAccess) { + val receiver = value.receiver + val member = value.member + if (receiver is PythonReference && member is PythonReference) { + val receiverMatches = receiver.declaration?.name in annotation.parameters + val memberMatches = member.declaration is PythonAttribute && member.declaration?.name == "value" + + if (receiverMatches && memberMatches) { + it.value = PythonMemberAccess( + receiver = PythonMemberAccess( + receiver = PythonReference(declaration = groupedParameter), + member = receiver + ), + member = member + ) + } + } } - } - this.annotations.remove(annotation) + this.annotations.remove(annotation) + } } } diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt index 9aa2c47f0..486b69a4a 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt @@ -110,7 +110,7 @@ private fun PythonClass.createAttributesForParametersOfConstructor() { ?.forEach { this.attributes += PythonAttribute( name = it.name, - type = it.type, + type = it.type?.copy(), value = PythonStringifiedExpression(it.name), isPublic = true, description = it.description, diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt index a8e26dbd1..c5499e2dd 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt @@ -595,7 +595,7 @@ class PythonCodeGeneratorTest { @Test fun `should create Python code for modules (no classes, no functions, no enums)`() { - testModule.toPythonCode() shouldBe "\n" + testModule.toPythonCode() shouldBe "from __future__ import annotations\n" } @Test @@ -603,6 +603,7 @@ class PythonCodeGeneratorTest { testModule.enums += testEnum testModule.toPythonCode() shouldBe """ + |from __future__ import annotations |from enum import Enum | |class TestEnum(Enum): @@ -618,6 +619,8 @@ class PythonCodeGeneratorTest { testModule.toPythonCode() shouldBe """ |import originalModule | + |from __future__ import annotations + | |def testFunction(): | pass | @@ -635,6 +638,7 @@ class PythonCodeGeneratorTest { testModule.toPythonCode() shouldBe """ |import originalModule | + |from __future__ import annotations |from enum import Enum | |def testFunction(): @@ -654,6 +658,8 @@ class PythonCodeGeneratorTest { testModule.classes += testClasses testModule.toPythonCode() shouldBe """ + |from __future__ import annotations + | |class TestClass: | def testMethod1(): | pass @@ -673,6 +679,7 @@ class PythonCodeGeneratorTest { testModule.enums += testEnum testModule.toPythonCode() shouldBe """ + |from __future__ import annotations |from enum import Enum | |class TestClass: @@ -699,6 +706,8 @@ class PythonCodeGeneratorTest { testModule.toPythonCode() shouldBe """ |import originalModule | + |from __future__ import annotations + | |class TestClass: | def testMethod1(): | pass @@ -727,6 +736,7 @@ class PythonCodeGeneratorTest { testModule.toPythonCode() shouldBe """ |import originalModule | + |from __future__ import annotations |from enum import Enum | |class TestClass: From b0989042b42246bf4fbbd9236fce6c75975f7c82 Mon Sep 17 00:00:00 2001 From: lars-reimann Date: Mon, 10 Jan 2022 20:22:22 +0000 Subject: [PATCH 19/32] style: apply automatic fixes of linters --- .../api_editor/codegen/PythonCodeGenerator.kt | 14 +++++--------- .../api_editor/mutable_model/PythonAst.kt | 3 --- .../transformation/EnumAnnotationProcessor.kt | 4 ++-- .../api_editor/transformation/Postprocessor.kt | 1 - .../api_editor/codegen/PythonCodeGeneratorTest.kt | 3 --- 5 files changed, 7 insertions(+), 18 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt index 94a33366b..a036ae630 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt @@ -242,7 +242,6 @@ internal fun PythonParameter.toPythonCode() = buildString { } } - /* ******************************************************************************************************************** * Expressions * ********************************************************************************************************************/ @@ -260,7 +259,6 @@ internal fun PythonExpression.toPythonCode(): String { } } - /* ******************************************************************************************************************** * Types * ********************************************************************************************************************/ @@ -281,7 +279,6 @@ internal fun PythonType?.toPythonCodeOrNull(): String? { } } - /* ******************************************************************************************************************** * Other * ********************************************************************************************************************/ @@ -303,24 +300,23 @@ internal fun Boundary.toPythonCode(parameterName: String) = buildString { } if (lowerLimitType != UNRESTRICTED && upperLimitType != UNRESTRICTED) { - appendLine("if not $lowerIntervalLimit ${lowerLimitType.operator} $parameterName ${upperLimitType.operator} ${upperIntervalLimit}:") + appendLine("if not $lowerIntervalLimit ${lowerLimitType.operator} $parameterName ${upperLimitType.operator} $upperIntervalLimit:") appendIndented("raise ValueError('Valid values of $parameterName must be in ${asInterval()}, but {} was assigned.'.format($parameterName))") } else if (lowerLimitType == LESS_THAN) { - appendLine("if not $lowerIntervalLimit < ${parameterName}:") + appendLine("if not $lowerIntervalLimit < $parameterName:") appendIndented("raise ValueError('Valid values of $parameterName must be greater than $lowerIntervalLimit, but {} was assigned.'.format($parameterName))") } else if (lowerLimitType == LESS_THAN_OR_EQUALS) { - appendLine("if not $lowerIntervalLimit <= ${parameterName}:") + appendLine("if not $lowerIntervalLimit <= $parameterName:") appendIndented("raise ValueError('Valid values of $parameterName must be greater than or equal to $lowerIntervalLimit, but {} was assigned.'.format($parameterName))") } else if (upperLimitType == LESS_THAN) { - appendLine("if not $parameterName < ${upperIntervalLimit}:") + appendLine("if not $parameterName < $upperIntervalLimit:") appendIndented("raise ValueError('Valid values of $parameterName must be less than $upperIntervalLimit, but {} was assigned.'.format($parameterName))") } else if (upperLimitType == LESS_THAN_OR_EQUALS) { - appendLine("if not $parameterName <= ${upperIntervalLimit}:") + appendLine("if not $parameterName <= $upperIntervalLimit:") appendIndented("raise ValueError('Valid values of $parameterName must be less than or equal to $upperIntervalLimit, but {} was assigned.'.format($parameterName))") } } - /* ******************************************************************************************************************** * Util * ********************************************************************************************************************/ diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt index 465ac3b8d..2930625b0 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/mutable_model/PythonAst.kt @@ -219,7 +219,6 @@ class PythonResult( data class OriginalPythonClass(val qualifiedName: String) - /* ******************************************************************************************************************** * Expressions * ********************************************************************************************************************/ @@ -267,7 +266,6 @@ class PythonReference(declaration: PythonDeclaration) : PythonExpression() { data class PythonStringifiedExpression(val string: String) : PythonExpression() - /* ******************************************************************************************************************** * Types * ********************************************************************************************************************/ @@ -290,7 +288,6 @@ data class PythonStringifiedType(val string: String) : PythonType() { } } - /* ******************************************************************************************************************** * Other * ********************************************************************************************************************/ diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessor.kt index c3714c2d4..2aa52edb7 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/EnumAnnotationProcessor.kt @@ -83,8 +83,8 @@ private fun hasConflictingEnums( (enumToCheck.name == enum.name) && ( enumToCheck.instances.size != enum.instances.size || - !enumToCheck.instances.mapNotNull { (it.value as? PythonString)?.value} - .containsAll(enum.instances.mapNotNull { (it.value as? PythonString)?.value}) + !enumToCheck.instances.mapNotNull { (it.value as? PythonString)?.value } + .containsAll(enum.instances.mapNotNull { (it.value as? PythonString)?.value }) ) } } diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt index 486b69a4a..940389321 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/Postprocessor.kt @@ -88,7 +88,6 @@ private fun PythonClass.createConstructor() { ) } - constructorMethod.release() } } diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt index c5499e2dd..a21b73cb0 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt @@ -947,7 +947,6 @@ class PythonCodeGeneratorTest { } } - /* **************************************************************************************************************** * Expressions * ****************************************************************************************************************/ @@ -1022,7 +1021,6 @@ class PythonCodeGeneratorTest { } } - /* **************************************************************************************************************** * Types * ****************************************************************************************************************/ @@ -1067,7 +1065,6 @@ class PythonCodeGeneratorTest { } } - /* **************************************************************************************************************** * Other * ****************************************************************************************************************/ From 8c89ef4a7dfbab7445c4ee88f07f7e6b0cb3e854 Mon Sep 17 00:00:00 2001 From: PaulV Date: Mon, 10 Jan 2022 23:59:22 +0100 Subject: [PATCH 20/32] add function tests and layout for constructor tests --- .../ConstructorPythonCodeGeneratorTest.kt | 481 ++++++++++++++ .../FunctionPythonCodeGeneratorTest.kt | 602 ++++++++++++++++++ .../MethodPythonCodeGeneratorTest.kt | 5 + .../PythonCodeGeneratorTest.kt | 4 +- 4 files changed, 1091 insertions(+), 1 deletion(-) create mode 100644 server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt create mode 100644 server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt create mode 100644 server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/MethodPythonCodeGeneratorTest.kt rename server/src/test/kotlin/com/larsreimann/api_editor/codegen/{ => python_code_gen}/PythonCodeGeneratorTest.kt (99%) diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt new file mode 100644 index 000000000..3dffbdb9f --- /dev/null +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt @@ -0,0 +1,481 @@ +package com.larsreimann.api_editor.codegen.python_code_gen + +import com.larsreimann.api_editor.codegen.toPythonCode +import com.larsreimann.api_editor.model.AttributeAnnotation +import com.larsreimann.api_editor.model.BoundaryAnnotation +import com.larsreimann.api_editor.model.ComparisonOperator +import com.larsreimann.api_editor.model.DefaultBoolean +import com.larsreimann.api_editor.model.DefaultNumber +import com.larsreimann.api_editor.model.DefaultString +import com.larsreimann.api_editor.model.EnumAnnotation +import com.larsreimann.api_editor.model.EnumPair +import com.larsreimann.api_editor.model.GroupAnnotation +import com.larsreimann.api_editor.model.OptionalAnnotation +import com.larsreimann.api_editor.model.RenameAnnotation +import com.larsreimann.api_editor.model.RequiredAnnotation +import com.larsreimann.api_editor.mutable_model.PythonClass +import com.larsreimann.api_editor.mutable_model.PythonFunction +import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonPackage +import com.larsreimann.api_editor.mutable_model.PythonParameter +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression +import com.larsreimann.api_editor.transformation.transform +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class ConstructorPythonCodeGeneratorTest { + private lateinit var testParameter1: PythonParameter + private lateinit var testParameter2: PythonParameter + private lateinit var testParameter3: PythonParameter + private lateinit var testClass: PythonClass + private lateinit var testFunction: PythonFunction + private lateinit var testModule: PythonModule + private lateinit var testPackage: PythonPackage + + @BeforeEach + fun reset() { + testParameter1 = PythonParameter( + name = "testParameter1", + ) + testParameter2 = PythonParameter( + name = "testParameter2", + ) + testParameter3 = PythonParameter( + name = "testParameter3", + ) + testFunction = PythonFunction( + name = "__init__", + parameters = mutableListOf( + testParameter1, + testParameter2, + testParameter3 + ) + ) + testClass = PythonClass(name = "testClass", methods = mutableListOf(testFunction)) + testModule = PythonModule( + name = "testModule", + classes = mutableListOf(testClass) + ) + testPackage = PythonPackage( + distribution = "testPackage", + name = "testPackage", + version = "1.0.0", + modules = mutableListOf(testModule) + ) + } + + @Test + fun `should process Boundary- and GroupAnnotation on function level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + // TODO + val expectedModuleContent: String = + """ + |TODO + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary-, Group- and OptionalAnnotation on function level`() { + // given + testParameter2.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter2.annotations.add( + OptionalAnnotation( + DefaultNumber(0.5) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + // TODO + val expectedModuleContent: String = + """ + |TODO + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary-, Group- and RequiredAnnotation on function level`() { + // given + testParameter2.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter2.annotations.add( + RequiredAnnotation + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + // TODO + val expectedModuleContent: String = + """ + |TODO + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary- and AttributeAnnotation on function level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter1.annotations.add( + AttributeAnnotation( + DefaultNumber(0.5) + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + // TODO + val expectedModuleContent: String = + """ + |TODO + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary- and OptionalAnnotation on function level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter1.annotations.add( + OptionalAnnotation( + DefaultNumber(0.5) + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + // TODO + val expectedModuleContent: String = + """ + |TODO + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary- and RequiredAnnotation on function level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter1.annotations.add( + RequiredAnnotation + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + // TODO + val expectedModuleContent: String = + """ + |TODO + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum- and GroupAnnotation on function level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + "TestEnum", + listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue1", "testName1") + ) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + // TODO + val expectedModuleContent: String = + """ + |TODO + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum-, Required- and GroupAnnotation on function level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + enumName = "TestEnum", + pairs = listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue1", "testName1") + ) + ) + ) + testParameter1.annotations.add( + RenameAnnotation("newName") + ) + testParameter1.annotations.add( + RequiredAnnotation + ) + testParameter1.defaultValue = PythonStringifiedExpression("toRemove") + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + // TODO + val expectedModuleContent: String = + """ + |TODO + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum- and RequiredAnnotation on function level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + enumName = "TestEnum", + pairs = listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue1", "testName1") + ) + ) + ) + testParameter1.annotations.add( + RequiredAnnotation + ) + testParameter1.defaultValue = PythonStringifiedExpression("toRemove") + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + // TODO + val expectedModuleContent: String = + """ + |TODO + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum- and AttributeAnnotation on function level`() { + // given + testParameter3.annotations.add( + EnumAnnotation( + enumName = "TestEnum", + pairs = listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue1", "testName1") + ) + ) + ) + testParameter3.annotations.add( + AttributeAnnotation( + DefaultString("testString") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + // TODO + val expectedModuleContent: String = + """ + |TODO + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Group- and RequiredAnnotation on function level`() { + // given + testParameter2.annotations.add( + RequiredAnnotation + ) + testParameter2.defaultValue = PythonStringifiedExpression("toRemove") + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + // TODO + val expectedModuleContent: String = + """ + |TODO + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Group- and OptionalAnnotation on function level`() { + // given + testParameter2.annotations.add( + OptionalAnnotation( + DefaultBoolean(false) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + // TODO + val expectedModuleContent: String = + """ + |TODO + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Group-, Required- and OptionalAnnotation on function level`() { + // given + testParameter2.annotations.add( + OptionalAnnotation( + DefaultBoolean(false) + ) + ) + testParameter3.annotations.add( + RequiredAnnotation + ) + testParameter3.defaultValue = PythonStringifiedExpression("toRemove") + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + // TODO + val expectedModuleContent: String = + """ + |TODO + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } +} diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt new file mode 100644 index 000000000..93514fd2e --- /dev/null +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt @@ -0,0 +1,602 @@ +package com.larsreimann.api_editor.codegen.python_code_gen + +import com.larsreimann.api_editor.codegen.toPythonCode +import com.larsreimann.api_editor.model.BoundaryAnnotation +import com.larsreimann.api_editor.model.ComparisonOperator +import com.larsreimann.api_editor.model.DefaultBoolean +import com.larsreimann.api_editor.model.DefaultNumber +import com.larsreimann.api_editor.model.EnumAnnotation +import com.larsreimann.api_editor.model.EnumPair +import com.larsreimann.api_editor.model.GroupAnnotation +import com.larsreimann.api_editor.model.OptionalAnnotation +import com.larsreimann.api_editor.model.RenameAnnotation +import com.larsreimann.api_editor.model.RequiredAnnotation +import com.larsreimann.api_editor.mutable_model.PythonFunction +import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonPackage +import com.larsreimann.api_editor.mutable_model.PythonParameter +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression +import com.larsreimann.api_editor.transformation.transform +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class FunctionPythonCodeGeneratorTest { + private lateinit var testParameter1: PythonParameter + private lateinit var testParameter2: PythonParameter + private lateinit var testParameter3: PythonParameter + private lateinit var testFunction: PythonFunction + private lateinit var testModule: PythonModule + private lateinit var testPackage: PythonPackage + + @BeforeEach + fun reset() { + testParameter1 = PythonParameter( + name = "testParameter1", + ) + testParameter2 = PythonParameter( + name = "testParameter2", + ) + testParameter3 = PythonParameter( + name = "testParameter3", + ) + testFunction = PythonFunction( + name = "__init__", + parameters = mutableListOf( + testParameter1, + testParameter2, + testParameter3 + ) + ) + testModule = PythonModule( + name = "testModule", + functions = mutableListOf(testFunction) + ) + testPackage = PythonPackage( + distribution = "testPackage", + name = "testPackage", + version = "1.0.0", + modules = mutableListOf(testModule) + ) + } + + @Test + fun `should process Boundary- and GroupAnnotation on function level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class TestGroup: + | def __init__(self, testParameter1, testParameter3): + | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): + | raise ValueError('testParameter1' needs to be an integer, but {} was assigned.'.format(testParameter1)) + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) + | + | self.testParameter1 = testParameter1 + | self.testParameter3 = testParameter3 + | + |def __init__(testGroup: TestGroup, testParameter2): + | return testModule.__init__(testGroup.testParameter1, testParameter2, testGroup.testParameter3) + | + |""".trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary-, Group- and OptionalAnnotation on function level`() { + // given + testParameter2.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter2.annotations.add( + OptionalAnnotation( + DefaultNumber(0.5) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class TestGroup: + | def __init__(self, testParameter3, *, testParameter2=0.5): + | if not (isinstance(testParameter2, int) or (isinstance(testParameter2, float) and testParameter2.is_integer())): + | raise ValueError('testParameter2' needs to be an integer, but {} was assigned.'.format(testParameter2)) + | if not 0.0 <= testParameter2 <= 1.0: + | raise ValueError('Valid values of testParameter2 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter2)) + | + | self.testParameter3 = testParameter3 + | self.testParameter2 = testParameter2 + | + |def __init__(testParameter1, testGroup: TestGroup): + | return testModule.__init__(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary-, Group- and RequiredAnnotation on function level`() { + // given + testParameter2.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter2.annotations.add( + RequiredAnnotation + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class TestGroup: + | def __init__(self, testParameter2, testParameter3): + | if not (isinstance(testParameter2, int) or (isinstance(testParameter2, float) and testParameter2.is_integer())): + | raise ValueError('testParameter2' needs to be an integer, but {} was assigned.'.format(testParameter2)) + | if not 0.0 <= testParameter2 <= 1.0: + | raise ValueError('Valid values of testParameter2 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter2)) + | + | self.testParameter2 = testParameter2 + | self.testParameter3 = testParameter3 + | + |def __init__(testParameter1, testGroup: TestGroup): + | return testModule.__init__(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary- and OptionalAnnotation on function level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter1.annotations.add( + OptionalAnnotation( + DefaultNumber(0.5) + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |def __init__(testParameter2, testParameter3, *, testParameter1=0.5): + | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): + | raise ValueError('testParameter1' needs to be an integer, but {} was assigned.'.format(testParameter1)) + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) + | + | return testModule.__init__(testParameter1, testParameter2, testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary- and RequiredAnnotation on function level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter1.annotations.add( + RequiredAnnotation + ) + testParameter1.defaultValue = PythonStringifiedExpression("toRemove") + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |def __init__(testParameter1, testParameter2, testParameter3): + | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): + | raise ValueError('testParameter1' needs to be an integer, but {} was assigned.'.format(testParameter1)) + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) + | + | return testModule.__init__(testParameter1, testParameter2, testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum- and GroupAnnotation on function level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + "TestEnum", + listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class TestGroup: + | def __init__(self, testParameter1: TestEnum, testParameter2): + | self.testParameter1: TestEnum = testParameter1 + | self.testParameter2 = testParameter2 + | + |def __init__(testGroup: TestGroup, testParameter3): + | return testModule.__init__(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum-, Optional- and GroupAnnotation on function level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + enumName = "TestEnum", + pairs = listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testParameter1.annotations.add( + OptionalAnnotation( + DefaultBoolean(true) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class TestGroup: + | def __init__(self, testParameter2, *, testParameter1: TestEnum = True): + | self.testParameter2 = testParameter2 + | self.testParameter1: TestEnum = testParameter1 + | + |def __init__(testGroup: TestGroup, testParameter3): + | return testModule.__init__(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum-, Required- and GroupAnnotation on function level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + enumName = "TestEnum", + pairs = listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testParameter1.annotations.add( + RenameAnnotation("newName") + ) + testParameter1.annotations.add( + RequiredAnnotation + ) + testParameter1.defaultValue = PythonStringifiedExpression("toRemove") + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class TestGroup: + | def __init__(self, testParameter2): + | self.testParameter2 = testParameter2 + | + |def __init__(newName: TestEnum, testGroup: TestGroup, testParameter3): + | return testModule.__init__(newName.value, testGroup.testParameter2, testParameter3) + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum- and RequiredAnnotation on function level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + enumName = "TestEnum", + pairs = listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testParameter1.annotations.add( + RequiredAnnotation + ) + testParameter1.defaultValue = PythonStringifiedExpression("toRemove") + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |def __init__(testParameter1: TestEnum, testParameter2, testParameter3): + | return testModule.__init__(testParameter1.value, testParameter2, testParameter3) + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Group- and RequiredAnnotation on function level`() { + // given + testParameter2.annotations.add( + RequiredAnnotation + ) + testParameter2.defaultValue = PythonStringifiedExpression("toRemove") + testParameter1.defaultValue = PythonStringifiedExpression("defaultValue") + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class TestGroup: + | def __init__(self, testParameter2, testParameter3): + | self.testParameter2 = testParameter2 + | self.testParameter3 = testParameter3 + | + |def __init__(testGroup: TestGroup, *, testParameter1=defaultValue): + | return testModule.__init__(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Group- and OptionalAnnotation on function level`() { + // given + testParameter2.annotations.add( + OptionalAnnotation( + DefaultBoolean(false) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class TestGroup: + | def __init__(self, testParameter3, *, testParameter2=False): + | self.testParameter3 = testParameter3 + | self.testParameter2 = testParameter2 + | + |def __init__(testParameter1, testGroup: TestGroup): + | return testModule.__init__(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Group-, Required- and OptionalAnnotation on function level`() { + // given + testParameter2.annotations.add( + OptionalAnnotation( + DefaultBoolean(false) + ) + ) + testParameter3.annotations.add( + RequiredAnnotation + ) + testParameter3.defaultValue = PythonStringifiedExpression("toRemove") + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class TestGroup: + | def __init__(self, testParameter3, *, testParameter2=False): + | self.testParameter3 = testParameter3 + | self.testParameter2 = testParameter2 + | + |def __init__(testParameter1, testGroup: TestGroup): + | return testModule.__init__(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } +} diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/MethodPythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/MethodPythonCodeGeneratorTest.kt new file mode 100644 index 000000000..9f12fde90 --- /dev/null +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/MethodPythonCodeGeneratorTest.kt @@ -0,0 +1,5 @@ +package com.larsreimann.api_editor.codegen.python_code_gen + +// TODO +class MethodPythonCodeGeneratorTest { +} diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/PythonCodeGeneratorTest.kt similarity index 99% rename from server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt rename to server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/PythonCodeGeneratorTest.kt index a21b73cb0..10f96ddb9 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/PythonCodeGeneratorTest.kt @@ -1,5 +1,7 @@ -package com.larsreimann.api_editor.codegen +package com.larsreimann.api_editor.codegen.python_code_gen +import com.larsreimann.api_editor.codegen.toPythonCode +import com.larsreimann.api_editor.codegen.toPythonCodeOrNull import com.larsreimann.api_editor.model.Boundary import com.larsreimann.api_editor.model.ComparisonOperator.LESS_THAN import com.larsreimann.api_editor.model.ComparisonOperator.LESS_THAN_OR_EQUALS From ec0917b675479ab3dcf937b7217ad91d495de295 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Tue, 11 Jan 2022 10:20:08 +0100 Subject: [PATCH 21/32] fix: indented blank lines --- .../api_editor/codegen/PythonCodeGenerator.kt | 23 ++++++++----- .../codegen/PythonCodeGeneratorTest.kt | 34 ++++++++++++++++--- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt index a036ae630..81b7206c5 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt @@ -95,9 +95,7 @@ internal fun PythonAttribute.toPythonCode() = buildString { internal fun PythonClass.toPythonCode() = buildString { val constructorString = constructor?.toPythonCode() ?: "" - val methodsString = methods.joinToString("\n\n") { - it.toPythonCode().prependIndent(" ") - } + val methodsString = methods.joinToString("\n\n") { it.toPythonCode() } appendLine("class $name:") if (constructorString.isNotBlank()) { @@ -107,7 +105,7 @@ internal fun PythonClass.toPythonCode() = buildString { } } if (methodsString.isNotBlank()) { - append(methodsString) + appendIndented(methodsString) } if (constructorString.isBlank() && methodsString.isBlank()) { appendIndented("pass") @@ -321,15 +319,24 @@ internal fun Boundary.toPythonCode(parameterName: String) = buildString { * Util * ********************************************************************************************************************/ +private fun String.prependIndentUnlessBlank(indent: String = " "): String { + return lineSequence() + .map { + when { + it.isBlank() -> it.trim() + else -> it.prependIndent(indent) + } + } + .joinToString("\n") +} + private fun StringBuilder.appendIndented(init: StringBuilder.() -> Unit): StringBuilder { val stringToIndent = StringBuilder().apply(init).toString() - val indent = " ".repeat(4) - append(stringToIndent.prependIndent(indent)) + append(stringToIndent.prependIndentUnlessBlank()) return this } private fun StringBuilder.appendIndented(value: String): StringBuilder { - val indent = " ".repeat(4) - append(value.prependIndent(indent)) + append(value.prependIndentUnlessBlank()) return this } diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt index a21b73cb0..296d2555d 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt @@ -85,7 +85,7 @@ class PythonCodeGeneratorTest { inner class ClassToPythonCode { @Test - fun `should create valid code without constructor and methods`() { + fun `should create valid code for classes without constructor and methods`() { val testClass = PythonClass( name = "TestClass" ) @@ -97,7 +97,7 @@ class PythonCodeGeneratorTest { } @Test - fun `should create valid code without constructor but with methods`() { + fun `should create valid code for classes without constructor but with methods`() { val testClass = PythonClass( name = "TestClass", methods = listOf( @@ -117,7 +117,7 @@ class PythonCodeGeneratorTest { } @Test - fun `should create valid code with constructor but without methods`() { + fun `should create valid code for classes with constructor but without methods`() { val testClass = PythonClass( name = "TestClass", constructor = PythonConstructor() @@ -131,7 +131,7 @@ class PythonCodeGeneratorTest { } @Test - fun `should create valid code with constructor and methods`() { + fun `should create valid code for classes with constructor and methods`() { val testClass = PythonClass( name = "TestClass", constructor = PythonConstructor(), @@ -153,6 +153,32 @@ class PythonCodeGeneratorTest { | pass """.trimMargin() } + + @Test + fun `should not indent blank lines`() { + val testClass = PythonClass( + name = "TestClass", + constructor = PythonConstructor( + callToOriginalAPI = PythonCall( + receiver = PythonStringifiedExpression("testModule.TestClass") + ) + ), + attributes = listOf( + PythonAttribute( + name = "testAttribute", + value = PythonInt(1) + ) + ) + ) + + testClass.toPythonCode() shouldBe """ + |class TestClass: + | def __init__(): + | self.testAttribute = 1 + | + | self.instance = testModule.TestClass() + """.trimMargin() + } } @Nested From 6e38af5db5c2b62cdc0f9399057df9f45096ad0f Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Tue, 11 Jan 2022 10:22:45 +0100 Subject: [PATCH 22/32] fix: forbid combinations enum+attribute and enum+optional --- .../api_editor/validation/AnnotationValidator.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt index 958c04759..5851b0cbd 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt @@ -181,14 +181,14 @@ class AnnotationValidator(private val annotatedPythonPackage: SerializablePython companion object { private var possibleCombinations = buildMap> { - this["Attribute"] = mutableSetOf("Boundary", "Enum", "Rename") + this["Attribute"] = mutableSetOf("Boundary", "Rename") this["Boundary"] = mutableSetOf("Attribute", "Group", "Optional", "Rename", "Required") this["CalledAfter"] = mutableSetOf("CalledAfter", "Group", "Move", "Rename") this["Constant"] = mutableSetOf() - this["Enum"] = mutableSetOf("Attribute", "Group", "Optional", "Rename", "Required") + this["Enum"] = mutableSetOf("Group", "Rename", "Required") this["Group"] = mutableSetOf("Boundary", "CalledAfter", "Enum", "Group", "Move", "Optional", "Rename", "Required") this["Move"] = mutableSetOf("CalledAfter", "Group", "Rename") - this["Optional"] = mutableSetOf("Boundary", "Enum", "Group", "Rename") + this["Optional"] = mutableSetOf("Boundary", "Group", "Rename") this["Rename"] = mutableSetOf( "Attribute", "Boundary", From 0f0b36b687f340c6633cd982a5ffdd1943ba8654 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Tue, 11 Jan 2022 10:38:22 +0100 Subject: [PATCH 23/32] feat: use f-strings instead of format calls --- .../api_editor/codegen/PythonCodeGenerator.kt | 12 +++--- .../codegen/PythonCodeGeneratorTest.kt | 42 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt index 81b7206c5..450d2797e 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt @@ -291,7 +291,7 @@ internal fun PythonArgument.toPythonCode() = buildString { internal fun Boundary.toPythonCode(parameterName: String) = buildString { if (isDiscrete) { appendLine("if not (isinstance($parameterName, int) or (isinstance($parameterName, float) and $parameterName.is_integer())):") - appendIndented("raise ValueError('$parameterName' needs to be an integer, but {} was assigned.'.format($parameterName))") + appendIndented("raise ValueError(f'$parameterName needs to be an integer, but {$parameterName} was assigned.')") if (lowerLimitType != UNRESTRICTED || upperLimitType != UNRESTRICTED) { appendLine() } @@ -299,19 +299,19 @@ internal fun Boundary.toPythonCode(parameterName: String) = buildString { if (lowerLimitType != UNRESTRICTED && upperLimitType != UNRESTRICTED) { appendLine("if not $lowerIntervalLimit ${lowerLimitType.operator} $parameterName ${upperLimitType.operator} $upperIntervalLimit:") - appendIndented("raise ValueError('Valid values of $parameterName must be in ${asInterval()}, but {} was assigned.'.format($parameterName))") + appendIndented("raise ValueError(f'Valid values of $parameterName must be in ${asInterval()}, but {$parameterName} was assigned.')") } else if (lowerLimitType == LESS_THAN) { appendLine("if not $lowerIntervalLimit < $parameterName:") - appendIndented("raise ValueError('Valid values of $parameterName must be greater than $lowerIntervalLimit, but {} was assigned.'.format($parameterName))") + appendIndented("raise ValueError(f'Valid values of $parameterName must be greater than $lowerIntervalLimit, but {$parameterName} was assigned.')") } else if (lowerLimitType == LESS_THAN_OR_EQUALS) { appendLine("if not $lowerIntervalLimit <= $parameterName:") - appendIndented("raise ValueError('Valid values of $parameterName must be greater than or equal to $lowerIntervalLimit, but {} was assigned.'.format($parameterName))") + appendIndented("raise ValueError(f'Valid values of $parameterName must be greater than or equal to $lowerIntervalLimit, but {$parameterName} was assigned.')") } else if (upperLimitType == LESS_THAN) { appendLine("if not $parameterName < $upperIntervalLimit:") - appendIndented("raise ValueError('Valid values of $parameterName must be less than $upperIntervalLimit, but {} was assigned.'.format($parameterName))") + appendIndented("raise ValueError(f'Valid values of $parameterName must be less than $upperIntervalLimit, but {$parameterName} was assigned.')") } else if (upperLimitType == LESS_THAN_OR_EQUALS) { appendLine("if not $parameterName <= $upperIntervalLimit:") - appendIndented("raise ValueError('Valid values of $parameterName must be less than or equal to $upperIntervalLimit, but {} was assigned.'.format($parameterName))") + appendIndented("raise ValueError(f'Valid values of $parameterName must be less than or equal to $upperIntervalLimit, but {$parameterName} was assigned.')") } } diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt index 296d2555d..26c75e680 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt @@ -318,9 +318,9 @@ class PythonCodeGeneratorTest { testConstructor.toPythonCode() shouldBe """ |def __init__(self, testParameter1, testParameter2): | if not 0.0 <= testParameter1 <= 1.0: - | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') | if not 0.0 < testParameter2: - | raise ValueError('Valid values of testParameter2 must be greater than 0.0, but {} was assigned.'.format(testParameter2)) + | raise ValueError(f'Valid values of testParameter2 must be greater than 0.0, but {testParameter2} was assigned.') """.trimMargin() } @@ -334,9 +334,9 @@ class PythonCodeGeneratorTest { testConstructor.toPythonCode() shouldBe """ |def __init__(self, testParameter1, testParameter2): | if not 0.0 <= testParameter1 <= 1.0: - | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') | if not 0.0 < testParameter2: - | raise ValueError('Valid values of testParameter2 must be greater than 0.0, but {} was assigned.'.format(testParameter2)) + | raise ValueError(f'Valid values of testParameter2 must be greater than 0.0, but {testParameter2} was assigned.') | | self.instance = OriginalClass() """.trimMargin() @@ -352,9 +352,9 @@ class PythonCodeGeneratorTest { testConstructor.toPythonCode() shouldBe """ |def __init__(self, testParameter1, testParameter2): | if not 0.0 <= testParameter1 <= 1.0: - | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') | if not 0.0 < testParameter2: - | raise ValueError('Valid values of testParameter2 must be greater than 0.0, but {} was assigned.'.format(testParameter2)) + | raise ValueError(f'Valid values of testParameter2 must be greater than 0.0, but {testParameter2} was assigned.') | | self.testAttribute1 = 1 | self.testAttribute2 = 2 @@ -372,9 +372,9 @@ class PythonCodeGeneratorTest { testConstructor.toPythonCode() shouldBe """ |def __init__(self, testParameter1, testParameter2): | if not 0.0 <= testParameter1 <= 1.0: - | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') | if not 0.0 < testParameter2: - | raise ValueError('Valid values of testParameter2 must be greater than 0.0, but {} was assigned.'.format(testParameter2)) + | raise ValueError(f'Valid values of testParameter2 must be greater than 0.0, but {testParameter2} was assigned.') | | self.testAttribute1 = 1 | self.testAttribute2 = 2 @@ -555,9 +555,9 @@ class PythonCodeGeneratorTest { testFunction.toPythonCode() shouldBe """ |def testFunction(testParameter1, testParameter2): | if not 0.0 <= testParameter1 <= 1.0: - | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') | if not 0.0 < testParameter2: - | raise ValueError('Valid values of testParameter2 must be greater than 0.0, but {} was assigned.'.format(testParameter2)) + | raise ValueError(f'Valid values of testParameter2 must be greater than 0.0, but {testParameter2} was assigned.') """.trimMargin() } @@ -572,9 +572,9 @@ class PythonCodeGeneratorTest { testFunction.toPythonCode() shouldBe """ |def testFunction(testParameter1, testParameter2): | if not 0.0 <= testParameter1 <= 1.0: - | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') | if not 0.0 < testParameter2: - | raise ValueError('Valid values of testParameter2 must be greater than 0.0, but {} was assigned.'.format(testParameter2)) + | raise ValueError(f'Valid values of testParameter2 must be greater than 0.0, but {testParameter2} was assigned.') | | return testModule.testFunction() """.trimMargin() @@ -1131,7 +1131,7 @@ class PythonCodeGeneratorTest { boundary.toPythonCode("testParameter") shouldBe """ |if not (isinstance(testParameter, int) or (isinstance(testParameter, float) and testParameter.is_integer())): - | raise ValueError('testParameter' needs to be an integer, but {} was assigned.'.format(testParameter)) + | raise ValueError(f'testParameter needs to be an integer, but {testParameter} was assigned.') """.trimMargin() } @@ -1160,7 +1160,7 @@ class PythonCodeGeneratorTest { boundary.toPythonCode("testParameter") shouldBe """ |if not testParameter < 1.0: - | raise ValueError('Valid values of testParameter must be less than 1.0, but {} was assigned.'.format(testParameter)) + | raise ValueError(f'Valid values of testParameter must be less than 1.0, but {testParameter} was assigned.') """.trimMargin() } @@ -1176,7 +1176,7 @@ class PythonCodeGeneratorTest { boundary.toPythonCode("testParameter") shouldBe """ |if not testParameter <= 1.0: - | raise ValueError('Valid values of testParameter must be less than or equal to 1.0, but {} was assigned.'.format(testParameter)) + | raise ValueError(f'Valid values of testParameter must be less than or equal to 1.0, but {testParameter} was assigned.') """.trimMargin() } @@ -1192,7 +1192,7 @@ class PythonCodeGeneratorTest { boundary.toPythonCode("testParameter") shouldBe """ |if not 0.0 < testParameter: - | raise ValueError('Valid values of testParameter must be greater than 0.0, but {} was assigned.'.format(testParameter)) + | raise ValueError(f'Valid values of testParameter must be greater than 0.0, but {testParameter} was assigned.') """.trimMargin() } @@ -1208,7 +1208,7 @@ class PythonCodeGeneratorTest { boundary.toPythonCode("testParameter") shouldBe """ |if not 0.0 < testParameter < 1.0: - | raise ValueError('Valid values of testParameter must be in (0.0, 1.0), but {} was assigned.'.format(testParameter)) + | raise ValueError(f'Valid values of testParameter must be in (0.0, 1.0), but {testParameter} was assigned.') """.trimMargin() } @@ -1224,7 +1224,7 @@ class PythonCodeGeneratorTest { boundary.toPythonCode("testParameter") shouldBe """ |if not 0.0 < testParameter <= 1.0: - | raise ValueError('Valid values of testParameter must be in (0.0, 1.0], but {} was assigned.'.format(testParameter)) + | raise ValueError(f'Valid values of testParameter must be in (0.0, 1.0], but {testParameter} was assigned.') """.trimMargin() } @@ -1240,7 +1240,7 @@ class PythonCodeGeneratorTest { boundary.toPythonCode("testParameter") shouldBe """ |if not 0.0 <= testParameter: - | raise ValueError('Valid values of testParameter must be greater than or equal to 0.0, but {} was assigned.'.format(testParameter)) + | raise ValueError(f'Valid values of testParameter must be greater than or equal to 0.0, but {testParameter} was assigned.') """.trimMargin() } @@ -1256,7 +1256,7 @@ class PythonCodeGeneratorTest { boundary.toPythonCode("testParameter") shouldBe """ |if not 0.0 <= testParameter < 1.0: - | raise ValueError('Valid values of testParameter must be in [0.0, 1.0), but {} was assigned.'.format(testParameter)) + | raise ValueError(f'Valid values of testParameter must be in [0.0, 1.0), but {testParameter} was assigned.') """.trimMargin() } @@ -1272,7 +1272,7 @@ class PythonCodeGeneratorTest { boundary.toPythonCode("testParameter") shouldBe """ |if not 0.0 <= testParameter <= 1.0: - | raise ValueError('Valid values of testParameter must be in [0.0, 1.0], but {} was assigned.'.format(testParameter)) + | raise ValueError(f'Valid values of testParameter must be in [0.0, 1.0], but {testParameter} was assigned.') """.trimMargin() } } From 4aeb030ca9e121eee5b1eebbe94a99b637ff71c7 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Tue, 11 Jan 2022 11:12:45 +0100 Subject: [PATCH 24/32] fix: missing imports for static methods and constructors --- .../api_editor/codegen/PythonCodeGenerator.kt | 30 ++++++-- .../codegen/PythonCodeGeneratorTest.kt | 77 ++++++++++++------- 2 files changed, 73 insertions(+), 34 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt index 450d2797e..f76a31f36 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGenerator.kt @@ -14,6 +14,7 @@ import com.larsreimann.api_editor.mutable_model.PythonBoolean import com.larsreimann.api_editor.mutable_model.PythonCall import com.larsreimann.api_editor.mutable_model.PythonClass import com.larsreimann.api_editor.mutable_model.PythonConstructor +import com.larsreimann.api_editor.mutable_model.PythonDeclaration import com.larsreimann.api_editor.mutable_model.PythonEnum import com.larsreimann.api_editor.mutable_model.PythonEnumInstance import com.larsreimann.api_editor.mutable_model.PythonExpression @@ -30,6 +31,7 @@ import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression import com.larsreimann.api_editor.mutable_model.PythonStringifiedType import com.larsreimann.api_editor.mutable_model.PythonType import com.larsreimann.modeling.closest +import com.larsreimann.modeling.descendants /* ******************************************************************************************************************** * Declarations @@ -51,13 +53,29 @@ fun PythonModule.toPythonCode(): String { } private fun PythonModule.importsToPythonCode() = buildString { - val imports = functions - .mapNotNull { - when (val receiver = it.callToOriginalAPI?.receiver) { - is PythonStringifiedExpression -> receiver.string.parentQualifiedName() - else -> null + val imports = buildSet { + + // Functions + this += descendants { it !is PythonDeclaration } + .filterIsInstance() + .filter { !it.isMethod() || it.isStaticMethod() } + .mapNotNull { + when (val receiver = it.callToOriginalAPI?.receiver) { + is PythonStringifiedExpression -> receiver.string.parentQualifiedName() + else -> null + } + } + + // Constructors + this += descendants() + .filterIsInstance() + .mapNotNull { + when (val receiver = it.callToOriginalAPI?.receiver) { + is PythonStringifiedExpression -> receiver.string.parentQualifiedName() + else -> null + } } - }.toSet() + } val importsString = imports.joinToString("\n") { "import $it" } var fromImportsString = "from __future__ import annotations" diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt index 26c75e680..f1ae5826c 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/PythonCodeGeneratorTest.kt @@ -5,7 +5,6 @@ import com.larsreimann.api_editor.model.ComparisonOperator.LESS_THAN import com.larsreimann.api_editor.model.ComparisonOperator.LESS_THAN_OR_EQUALS import com.larsreimann.api_editor.model.ComparisonOperator.UNRESTRICTED import com.larsreimann.api_editor.model.PythonParameterAssignment -import com.larsreimann.api_editor.mutable_model.OriginalPythonClass import com.larsreimann.api_editor.mutable_model.PythonArgument import com.larsreimann.api_editor.mutable_model.PythonAttribute import com.larsreimann.api_editor.mutable_model.PythonBoolean @@ -597,13 +596,21 @@ class PythonCodeGeneratorTest { name = "TestClass", methods = listOf( PythonFunction(name = "testMethod1"), - PythonFunction(name = "testMethod2") + PythonFunction( + name = "testMethodWithOriginalMethod", + decorators = mutableListOf("staticmethod"), + callToOriginalAPI = PythonCall( + receiver = PythonStringifiedExpression("originalModule.testMethod") + ) + ) ) ), PythonClass( - name = "TestClassWithOriginalClass", - originalClass = OriginalPythonClass( - qualifiedName = "originalModule.TestClassWithOriginalClass" + name = "TestClassWithConstructor", + constructor = PythonConstructor( + callToOriginalAPI = PythonCall( + receiver = PythonStringifiedExpression("originalModule.TestClass") + ) ) ) ) @@ -612,7 +619,7 @@ class PythonCodeGeneratorTest { PythonFunction( name = "testFunctionWithOriginalFunction", callToOriginalAPI = PythonCall( - receiver = PythonStringifiedExpression("originalModule.testFunctionWithOriginalFunction") + receiver = PythonStringifiedExpression("originalModule2.testFunction") ) ) ) @@ -643,7 +650,7 @@ class PythonCodeGeneratorTest { testModule.functions += testFunctions testModule.toPythonCode() shouldBe """ - |import originalModule + |import originalModule2 | |from __future__ import annotations | @@ -651,7 +658,7 @@ class PythonCodeGeneratorTest { | pass | |def testFunctionWithOriginalFunction(): - | return originalModule.testFunctionWithOriginalFunction() + | return originalModule2.testFunction() | """.trimMargin() } @@ -662,7 +669,7 @@ class PythonCodeGeneratorTest { testModule.enums += testEnum testModule.toPythonCode() shouldBe """ - |import originalModule + |import originalModule2 | |from __future__ import annotations |from enum import Enum @@ -671,7 +678,7 @@ class PythonCodeGeneratorTest { | pass | |def testFunctionWithOriginalFunction(): - | return originalModule.testFunctionWithOriginalFunction() + | return originalModule2.testFunction() | |class TestEnum(Enum): | pass @@ -684,17 +691,21 @@ class PythonCodeGeneratorTest { testModule.classes += testClasses testModule.toPythonCode() shouldBe """ + |import originalModule + | |from __future__ import annotations | |class TestClass: | def testMethod1(): | pass | - | def testMethod2(): - | pass + | @staticmethod + | def testMethodWithOriginalMethod(): + | return originalModule.testMethod() | - |class TestClassWithOriginalClass: - | pass + |class TestClassWithConstructor: + | def __init__(): + | self.instance = originalModule.TestClass() | """.trimMargin() } @@ -705,6 +716,8 @@ class PythonCodeGeneratorTest { testModule.enums += testEnum testModule.toPythonCode() shouldBe """ + |import originalModule + | |from __future__ import annotations |from enum import Enum | @@ -712,11 +725,13 @@ class PythonCodeGeneratorTest { | def testMethod1(): | pass | - | def testMethod2(): - | pass + | @staticmethod + | def testMethodWithOriginalMethod(): + | return originalModule.testMethod() | - |class TestClassWithOriginalClass: - | pass + |class TestClassWithConstructor: + | def __init__(): + | self.instance = originalModule.TestClass() | |class TestEnum(Enum): | pass @@ -731,6 +746,7 @@ class PythonCodeGeneratorTest { testModule.toPythonCode() shouldBe """ |import originalModule + |import originalModule2 | |from __future__ import annotations | @@ -738,17 +754,19 @@ class PythonCodeGeneratorTest { | def testMethod1(): | pass | - | def testMethod2(): - | pass + | @staticmethod + | def testMethodWithOriginalMethod(): + | return originalModule.testMethod() | - |class TestClassWithOriginalClass: - | pass + |class TestClassWithConstructor: + | def __init__(): + | self.instance = originalModule.TestClass() | |def testFunction(): | pass | |def testFunctionWithOriginalFunction(): - | return originalModule.testFunctionWithOriginalFunction() + | return originalModule2.testFunction() | """.trimMargin() } @@ -761,6 +779,7 @@ class PythonCodeGeneratorTest { testModule.toPythonCode() shouldBe """ |import originalModule + |import originalModule2 | |from __future__ import annotations |from enum import Enum @@ -769,17 +788,19 @@ class PythonCodeGeneratorTest { | def testMethod1(): | pass | - | def testMethod2(): - | pass + | @staticmethod + | def testMethodWithOriginalMethod(): + | return originalModule.testMethod() | - |class TestClassWithOriginalClass: - | pass + |class TestClassWithConstructor: + | def __init__(): + | self.instance = originalModule.TestClass() | |def testFunction(): | pass | |def testFunctionWithOriginalFunction(): - | return originalModule.testFunctionWithOriginalFunction() + | return originalModule2.testFunction() | |class TestEnum(Enum): | pass From fbdb741339baff754ed7420fb0fab49d2710fe20 Mon Sep 17 00:00:00 2001 From: PaulV Date: Tue, 11 Jan 2022 16:04:19 +0100 Subject: [PATCH 25/32] add self parameter to constructor tests --- .../ConstructorPythonCodeGeneratorTest.kt | 15 ++++-- .../FunctionPythonCodeGeneratorTest.kt | 52 ------------------- 2 files changed, 10 insertions(+), 57 deletions(-) diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt index 3dffbdb9f..da3a27a4a 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt @@ -28,6 +28,7 @@ class ConstructorPythonCodeGeneratorTest { private lateinit var testParameter1: PythonParameter private lateinit var testParameter2: PythonParameter private lateinit var testParameter3: PythonParameter + private lateinit var testParameterSelf: PythonParameter private lateinit var testClass: PythonClass private lateinit var testFunction: PythonFunction private lateinit var testModule: PythonModule @@ -35,18 +36,22 @@ class ConstructorPythonCodeGeneratorTest { @BeforeEach fun reset() { + testParameterSelf = PythonParameter( + name = "self" + ) testParameter1 = PythonParameter( - name = "testParameter1", + name = "testParameter1" ) testParameter2 = PythonParameter( - name = "testParameter2", + name = "testParameter2" ) testParameter3 = PythonParameter( - name = "testParameter3", + name = "testParameter3" ) testFunction = PythonFunction( name = "__init__", parameters = mutableListOf( + testParameterSelf, testParameter1, testParameter2, testParameter3 @@ -80,7 +85,7 @@ class ConstructorPythonCodeGeneratorTest { testFunction.annotations.add( GroupAnnotation( groupName = "TestGroup", - parameters = mutableListOf("testParameter1", "testParameter3") + parameters = mutableListOf("testParameter1", "testParameter2") ) ) // when @@ -117,7 +122,7 @@ class ConstructorPythonCodeGeneratorTest { testFunction.annotations.add( GroupAnnotation( groupName = "TestGroup", - parameters = mutableListOf("testParameter2", "testParameter3") + parameters = mutableListOf("testParameter1", "testParameter2") ) ) // when diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt index 93514fd2e..9b3a0aa42 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt @@ -337,58 +337,6 @@ class FunctionPythonCodeGeneratorTest { moduleContent shouldBe expectedModuleContent } - @Test - fun `should process Enum-, Optional- and GroupAnnotation on function level`() { - // given - testParameter1.annotations.add( - EnumAnnotation( - enumName = "TestEnum", - pairs = listOf( - EnumPair("testValue1", "testName1"), - EnumPair("testValue2", "testName2") - ) - ) - ) - testParameter1.annotations.add( - OptionalAnnotation( - DefaultBoolean(true) - ) - ) - testFunction.annotations.add( - GroupAnnotation( - groupName = "TestGroup", - parameters = mutableListOf("testParameter1", "testParameter2") - ) - ) - // when - testPackage.transform() - val moduleContent = testPackage.modules[0].toPythonCode() - - // then - val expectedModuleContent: String = - """ - |import testModule - | - |from __future__ import annotations - |from enum import Enum - | - |class TestGroup: - | def __init__(self, testParameter2, *, testParameter1: TestEnum = True): - | self.testParameter2 = testParameter2 - | self.testParameter1: TestEnum = testParameter1 - | - |def __init__(testGroup: TestGroup, testParameter3): - | return testModule.__init__(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) - | - |class TestEnum(Enum): - | testName1 = 'testValue1', - | testName2 = 'testValue2' - | - """.trimMargin() - - moduleContent shouldBe expectedModuleContent - } - @Test fun `should process Enum-, Required- and GroupAnnotation on function level`() { // given From d6d34c13a09496a37a0392e3ee790c16169bb744 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Tue, 11 Jan 2022 23:23:19 +0100 Subject: [PATCH 26/32] fix: disable combination attribute+boundary --- .../larsreimann/api_editor/validation/AnnotationValidator.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt b/server/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt index 5851b0cbd..b627c91e6 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/validation/AnnotationValidator.kt @@ -181,8 +181,8 @@ class AnnotationValidator(private val annotatedPythonPackage: SerializablePython companion object { private var possibleCombinations = buildMap> { - this["Attribute"] = mutableSetOf("Boundary", "Rename") - this["Boundary"] = mutableSetOf("Attribute", "Group", "Optional", "Rename", "Required") + this["Attribute"] = mutableSetOf("Rename") + this["Boundary"] = mutableSetOf("Group", "Optional", "Rename", "Required") this["CalledAfter"] = mutableSetOf("CalledAfter", "Group", "Move", "Rename") this["Constant"] = mutableSetOf() this["Enum"] = mutableSetOf("Group", "Rename", "Required") From 18e8aaca649fa5fb087eec603b5d6a1b8f7679df Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Tue, 11 Jan 2022 23:29:27 +0100 Subject: [PATCH 27/32] fix: ConcurrentModificationException when processing group annotations on methods --- .../api_editor/transformation/GroupAnnotationProcessor.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt index f73153051..686aa5b43 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/GroupAnnotationProcessor.kt @@ -27,6 +27,7 @@ fun PythonPackage.processGroupAnnotations() { private fun PythonModule.processGroupAnnotations() { this.descendants() .filterIsInstance() + .toList() .forEach { it.processGroupAnnotations(this) } } From e8aaf968332375c19dfe7fef6679b70e99ef04e2 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 12 Jan 2022 11:02:34 +0100 Subject: [PATCH 28/32] fix: failing test --- .../api_editor/validation/AnnotationValidatorTest.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/validation/AnnotationValidatorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/validation/AnnotationValidatorTest.kt index b97510346..e5b76f3fa 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/validation/AnnotationValidatorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/validation/AnnotationValidatorTest.kt @@ -14,6 +14,7 @@ import com.larsreimann.api_editor.model.PythonFromImport import com.larsreimann.api_editor.model.PythonImport import com.larsreimann.api_editor.model.PythonParameterAssignment import com.larsreimann.api_editor.model.RenameAnnotation +import com.larsreimann.api_editor.model.RequiredAnnotation import com.larsreimann.api_editor.model.SerializablePythonClass import com.larsreimann.api_editor.model.SerializablePythonFunction import com.larsreimann.api_editor.model.SerializablePythonModule @@ -223,10 +224,8 @@ internal class AnnotationValidatorTest { "typeInDocs", "description", mutableListOf( - AttributeAnnotation( - DefaultString("test") - ), RenameAnnotation("newName"), + RequiredAnnotation, BoundaryAnnotation( false, 1.0, From d9505aac8fec4ebc7f9959a15697e3333c2e53cd Mon Sep 17 00:00:00 2001 From: PaulV Date: Wed, 12 Jan 2022 20:21:03 +0100 Subject: [PATCH 29/32] add method and constructor tests --- .../ConstructorPythonCodeGeneratorTest.kt | 329 ++++++---- .../FunctionPythonCodeGeneratorTest.kt | 11 +- .../MethodPythonCodeGeneratorTest.kt | 596 +++++++++++++++++- 3 files changed, 821 insertions(+), 115 deletions(-) diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt index da3a27a4a..c83eac46c 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt @@ -1,17 +1,14 @@ package com.larsreimann.api_editor.codegen.python_code_gen import com.larsreimann.api_editor.codegen.toPythonCode -import com.larsreimann.api_editor.model.AttributeAnnotation import com.larsreimann.api_editor.model.BoundaryAnnotation import com.larsreimann.api_editor.model.ComparisonOperator import com.larsreimann.api_editor.model.DefaultBoolean import com.larsreimann.api_editor.model.DefaultNumber -import com.larsreimann.api_editor.model.DefaultString import com.larsreimann.api_editor.model.EnumAnnotation import com.larsreimann.api_editor.model.EnumPair import com.larsreimann.api_editor.model.GroupAnnotation import com.larsreimann.api_editor.model.OptionalAnnotation -import com.larsreimann.api_editor.model.RenameAnnotation import com.larsreimann.api_editor.model.RequiredAnnotation import com.larsreimann.api_editor.mutable_model.PythonClass import com.larsreimann.api_editor.mutable_model.PythonFunction @@ -71,7 +68,7 @@ class ConstructorPythonCodeGeneratorTest { } @Test - fun `should process Boundary- and GroupAnnotation on function level`() { + fun `should process Boundary- and GroupAnnotation on constructor level`() { // given testParameter1.annotations.add( BoundaryAnnotation( @@ -93,17 +90,36 @@ class ConstructorPythonCodeGeneratorTest { val moduleContent = testPackage.modules[0].toPythonCode() // then - // TODO val expectedModuleContent: String = """ - |TODO + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(self, testGroup: TestGroup, testParameter3): + | self.testGroup: TestGroup = testGroup + | self.testParameter3 = testParameter3 + | + | self.instance = testModule.testClass(testGroup.testParameter1, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1, testParameter2): + | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): + | raise ValueError(f'testParameter1 needs to be an integer, but {testParameter1} was assigned.') + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') + | + | self.testParameter1 = testParameter1 + | self.testParameter2 = testParameter2 + | """.trimMargin() moduleContent shouldBe expectedModuleContent } @Test - fun `should process Boundary-, Group- and OptionalAnnotation on function level`() { + fun `should process Boundary-, Group- and OptionalAnnotation on constructor level`() { // given testParameter2.annotations.add( BoundaryAnnotation( @@ -130,17 +146,36 @@ class ConstructorPythonCodeGeneratorTest { val moduleContent = testPackage.modules[0].toPythonCode() // then - // TODO val expectedModuleContent: String = """ - |TODO + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(self, testGroup: TestGroup, testParameter3): + | self.testGroup: TestGroup = testGroup + | self.testParameter3 = testParameter3 + | + | self.instance = testModule.testClass(testGroup.testParameter1, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1, *, testParameter2=0.5): + | if not (isinstance(testParameter2, int) or (isinstance(testParameter2, float) and testParameter2.is_integer())): + | raise ValueError(f'testParameter2 needs to be an integer, but {testParameter2} was assigned.') + | if not 0.0 <= testParameter2 <= 1.0: + | raise ValueError(f'Valid values of testParameter2 must be in [0.0, 1.0], but {testParameter2} was assigned.') + | + | self.testParameter1 = testParameter1 + | self.testParameter2 = testParameter2 + | """.trimMargin() moduleContent shouldBe expectedModuleContent } @Test - fun `should process Boundary-, Group- and RequiredAnnotation on function level`() { + fun `should process Boundary-, Group- and RequiredAnnotation on constructor level`() { // given testParameter2.annotations.add( BoundaryAnnotation( @@ -154,6 +189,7 @@ class ConstructorPythonCodeGeneratorTest { testParameter2.annotations.add( RequiredAnnotation ) + testParameter2.defaultValue = PythonStringifiedExpression("toRemove") testFunction.annotations.add( GroupAnnotation( groupName = "TestGroup", @@ -165,48 +201,36 @@ class ConstructorPythonCodeGeneratorTest { val moduleContent = testPackage.modules[0].toPythonCode() // then - // TODO - val expectedModuleContent: String = - """ - |TODO - """.trimMargin() - - moduleContent shouldBe expectedModuleContent - } - - @Test - fun `should process Boundary- and AttributeAnnotation on function level`() { - // given - testParameter1.annotations.add( - BoundaryAnnotation( - isDiscrete = true, - lowerIntervalLimit = 0.0, - lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, - upperIntervalLimit = 1.0, - upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS - ) - ) - testParameter1.annotations.add( - AttributeAnnotation( - DefaultNumber(0.5) - ) - ) - // when - testPackage.transform() - val moduleContent = testPackage.modules[0].toPythonCode() - - // then - // TODO val expectedModuleContent: String = """ - |TODO + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(self, testParameter1, testGroup: TestGroup): + | self.testParameter1 = testParameter1 + | self.testGroup: TestGroup = testGroup + | + | self.instance = testModule.testClass(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter2, testParameter3): + | if not (isinstance(testParameter2, int) or (isinstance(testParameter2, float) and testParameter2.is_integer())): + | raise ValueError(f'testParameter2 needs to be an integer, but {testParameter2} was assigned.') + | if not 0.0 <= testParameter2 <= 1.0: + | raise ValueError(f'Valid values of testParameter2 must be in [0.0, 1.0], but {testParameter2} was assigned.') + | + | self.testParameter2 = testParameter2 + | self.testParameter3 = testParameter3 + | """.trimMargin() moduleContent shouldBe expectedModuleContent } @Test - fun `should process Boundary- and OptionalAnnotation on function level`() { + fun `should process Boundary- and OptionalAnnotation on constructor level`() { // given testParameter1.annotations.add( BoundaryAnnotation( @@ -227,53 +251,84 @@ class ConstructorPythonCodeGeneratorTest { val moduleContent = testPackage.modules[0].toPythonCode() // then - // TODO val expectedModuleContent: String = """ - |TODO + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(self, testParameter2, testParameter3, *, testParameter1=0.5): + | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): + | raise ValueError(f'testParameter1 needs to be an integer, but {testParameter1} was assigned.') + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') + | + | self.testParameter2 = testParameter2 + | self.testParameter3 = testParameter3 + | self.testParameter1 = testParameter1 + | + | self.instance = testModule.testClass(testParameter1, testParameter2, testParameter3) + | """.trimMargin() moduleContent shouldBe expectedModuleContent } @Test - fun `should process Boundary- and RequiredAnnotation on function level`() { + fun `should process Boundary- and RequiredAnnotation on constructor level`() { // given testParameter1.annotations.add( BoundaryAnnotation( isDiscrete = true, lowerIntervalLimit = 0.0, - lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + lowerLimitType = ComparisonOperator.LESS_THAN, upperIntervalLimit = 1.0, - upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + upperLimitType = ComparisonOperator.UNRESTRICTED ) ) testParameter1.annotations.add( RequiredAnnotation ) + testParameter1.defaultValue = PythonStringifiedExpression("toRemove") // when testPackage.transform() val moduleContent = testPackage.modules[0].toPythonCode() // then - // TODO val expectedModuleContent: String = """ - |TODO + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(self, testParameter1, testParameter2, testParameter3): + | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): + | raise ValueError(f'testParameter1 needs to be an integer, but {testParameter1} was assigned.') + | if not 0.0 < testParameter1: + | raise ValueError(f'Valid values of testParameter1 must be greater than 0.0, but {testParameter1} was assigned.') + | + | self.testParameter1 = testParameter1 + | self.testParameter2 = testParameter2 + | self.testParameter3 = testParameter3 + | + | self.instance = testModule.testClass(testParameter1, testParameter2, testParameter3) + | """.trimMargin() moduleContent shouldBe expectedModuleContent } @Test - fun `should process Enum- and GroupAnnotation on function level`() { + fun `should process Enum- and GroupAnnotation on constructor level`() { // given testParameter1.annotations.add( EnumAnnotation( "TestEnum", listOf( EnumPair("testValue1", "testName1"), - EnumPair("testValue1", "testName1") + EnumPair("testValue2", "testName2") ) ) ) @@ -288,34 +343,49 @@ class ConstructorPythonCodeGeneratorTest { val moduleContent = testPackage.modules[0].toPythonCode() // then - // TODO val expectedModuleContent: String = """ - |TODO + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class testClass: + | def __init__(self, testGroup: TestGroup, testParameter3): + | self.testGroup: TestGroup = testGroup + | self.testParameter3 = testParameter3 + | + | self.instance = testModule.testClass(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1: TestEnum, testParameter2): + | self.testParameter1: TestEnum = testParameter1 + | self.testParameter2 = testParameter2 + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | """.trimMargin() moduleContent shouldBe expectedModuleContent } @Test - fun `should process Enum-, Required- and GroupAnnotation on function level`() { + fun `should process Enum-, Required- and GroupAnnotation on constructor level`() { // given testParameter1.annotations.add( EnumAnnotation( enumName = "TestEnum", pairs = listOf( EnumPair("testValue1", "testName1"), - EnumPair("testValue1", "testName1") + EnumPair("testValue2", "testName2") ) ) ) - testParameter1.annotations.add( - RenameAnnotation("newName") - ) testParameter1.annotations.add( RequiredAnnotation ) - testParameter1.defaultValue = PythonStringifiedExpression("toRemove") testFunction.annotations.add( GroupAnnotation( groupName = "TestGroup", @@ -327,24 +397,43 @@ class ConstructorPythonCodeGeneratorTest { val moduleContent = testPackage.modules[0].toPythonCode() // then - // TODO val expectedModuleContent: String = """ - |TODO + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class testClass: + | def __init__(self, testGroup: TestGroup, testParameter3): + | self.testGroup: TestGroup = testGroup + | self.testParameter3 = testParameter3 + | + | self.instance = testModule.testClass(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1: TestEnum, testParameter2): + | self.testParameter1: TestEnum = testParameter1 + | self.testParameter2 = testParameter2 + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | """.trimMargin() moduleContent shouldBe expectedModuleContent } @Test - fun `should process Enum- and RequiredAnnotation on function level`() { + fun `should process Enum- and RequiredAnnotation on constructor level`() { // given testParameter1.annotations.add( EnumAnnotation( enumName = "TestEnum", pairs = listOf( EnumPair("testValue1", "testName1"), - EnumPair("testValue1", "testName1") + EnumPair("testValue2", "testName2") ) ) ) @@ -357,48 +446,32 @@ class ConstructorPythonCodeGeneratorTest { val moduleContent = testPackage.modules[0].toPythonCode() // then - // TODO - val expectedModuleContent: String = - """ - |TODO - """.trimMargin() - - moduleContent shouldBe expectedModuleContent - } - - @Test - fun `should process Enum- and AttributeAnnotation on function level`() { - // given - testParameter3.annotations.add( - EnumAnnotation( - enumName = "TestEnum", - pairs = listOf( - EnumPair("testValue1", "testName1"), - EnumPair("testValue1", "testName1") - ) - ) - ) - testParameter3.annotations.add( - AttributeAnnotation( - DefaultString("testString") - ) - ) - // when - testPackage.transform() - val moduleContent = testPackage.modules[0].toPythonCode() - - // then - // TODO val expectedModuleContent: String = """ - |TODO + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class testClass: + | def __init__(self, testParameter1: TestEnum, testParameter2, testParameter3): + | self.testParameter1: TestEnum = testParameter1 + | self.testParameter2 = testParameter2 + | self.testParameter3 = testParameter3 + | + | self.instance = testModule.testClass(testParameter1.value, testParameter2, testParameter3) + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | """.trimMargin() moduleContent shouldBe expectedModuleContent } @Test - fun `should process Group- and RequiredAnnotation on function level`() { + fun `should process Group- and RequiredAnnotation on constructor level`() { // given testParameter2.annotations.add( RequiredAnnotation @@ -415,17 +488,31 @@ class ConstructorPythonCodeGeneratorTest { val moduleContent = testPackage.modules[0].toPythonCode() // then - // TODO val expectedModuleContent: String = """ - |TODO + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(self, testParameter1, testGroup: TestGroup): + | self.testParameter1 = testParameter1 + | self.testGroup: TestGroup = testGroup + | + | self.instance = testModule.testClass(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter2, testParameter3): + | self.testParameter2 = testParameter2 + | self.testParameter3 = testParameter3 + | """.trimMargin() moduleContent shouldBe expectedModuleContent } @Test - fun `should process Group- and OptionalAnnotation on function level`() { + fun `should process Group- and OptionalAnnotation on constructor level`() { // given testParameter2.annotations.add( OptionalAnnotation( @@ -443,17 +530,31 @@ class ConstructorPythonCodeGeneratorTest { val moduleContent = testPackage.modules[0].toPythonCode() // then - // TODO val expectedModuleContent: String = """ - |TODO + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(self, testParameter1, testGroup: TestGroup): + | self.testParameter1 = testParameter1 + | self.testGroup: TestGroup = testGroup + | + | self.instance = testModule.testClass(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter3, *, testParameter2=False): + | self.testParameter3 = testParameter3 + | self.testParameter2 = testParameter2 + | """.trimMargin() moduleContent shouldBe expectedModuleContent } @Test - fun `should process Group-, Required- and OptionalAnnotation on function level`() { + fun `should process Group-, Required- and OptionalAnnotation on constructor level`() { // given testParameter2.annotations.add( OptionalAnnotation( @@ -475,10 +576,24 @@ class ConstructorPythonCodeGeneratorTest { val moduleContent = testPackage.modules[0].toPythonCode() // then - // TODO val expectedModuleContent: String = """ - |TODO + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(self, testParameter1, testGroup: TestGroup): + | self.testParameter1 = testParameter1 + | self.testGroup: TestGroup = testGroup + | + | self.instance = testModule.testClass(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter3, *, testParameter2=False): + | self.testParameter3 = testParameter3 + | self.testParameter2 = testParameter2 + | """.trimMargin() moduleContent shouldBe expectedModuleContent diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt index 9b3a0aa42..fb9a302d8 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt @@ -9,7 +9,6 @@ import com.larsreimann.api_editor.model.EnumAnnotation import com.larsreimann.api_editor.model.EnumPair import com.larsreimann.api_editor.model.GroupAnnotation import com.larsreimann.api_editor.model.OptionalAnnotation -import com.larsreimann.api_editor.model.RenameAnnotation import com.larsreimann.api_editor.model.RequiredAnnotation import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonModule @@ -349,9 +348,6 @@ class FunctionPythonCodeGeneratorTest { ) ) ) - testParameter1.annotations.add( - RenameAnnotation("newName") - ) testParameter1.annotations.add( RequiredAnnotation ) @@ -375,11 +371,12 @@ class FunctionPythonCodeGeneratorTest { |from enum import Enum | |class TestGroup: - | def __init__(self, testParameter2): + | def __init__(self, testParameter1: TestEnum, testParameter2): + | self.testParameter1: TestEnum = testParameter1 | self.testParameter2 = testParameter2 | - |def __init__(newName: TestEnum, testGroup: TestGroup, testParameter3): - | return testModule.__init__(newName.value, testGroup.testParameter2, testParameter3) + |def __init__(testGroup: TestGroup, testParameter3): + | return testModule.__init__(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) | |class TestEnum(Enum): | testName1 = 'testValue1', diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/MethodPythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/MethodPythonCodeGeneratorTest.kt index 9f12fde90..dddda8045 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/MethodPythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/MethodPythonCodeGeneratorTest.kt @@ -1,5 +1,599 @@ package com.larsreimann.api_editor.codegen.python_code_gen -// TODO +import com.larsreimann.api_editor.codegen.toPythonCode +import com.larsreimann.api_editor.model.BoundaryAnnotation +import com.larsreimann.api_editor.model.ComparisonOperator +import com.larsreimann.api_editor.model.DefaultBoolean +import com.larsreimann.api_editor.model.DefaultNumber +import com.larsreimann.api_editor.model.EnumAnnotation +import com.larsreimann.api_editor.model.EnumPair +import com.larsreimann.api_editor.model.GroupAnnotation +import com.larsreimann.api_editor.model.OptionalAnnotation +import com.larsreimann.api_editor.model.RequiredAnnotation +import com.larsreimann.api_editor.mutable_model.PythonClass +import com.larsreimann.api_editor.mutable_model.PythonFunction +import com.larsreimann.api_editor.mutable_model.PythonInt +import com.larsreimann.api_editor.mutable_model.PythonModule +import com.larsreimann.api_editor.mutable_model.PythonPackage +import com.larsreimann.api_editor.mutable_model.PythonParameter +import com.larsreimann.api_editor.mutable_model.PythonStringifiedExpression +import com.larsreimann.api_editor.transformation.transform +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + class MethodPythonCodeGeneratorTest { + private lateinit var testParameterSelf: PythonParameter + private lateinit var testParameter1: PythonParameter + private lateinit var testParameter2: PythonParameter + private lateinit var testParameter3: PythonParameter + private lateinit var testClass: PythonClass + private lateinit var testFunction: PythonFunction + private lateinit var testModule: PythonModule + private lateinit var testPackage: PythonPackage + + @BeforeEach + fun reset() { + testParameterSelf = PythonParameter( + name = "self" + ) + testParameter1 = PythonParameter( + name = "testParameter1" + ) + testParameter2 = PythonParameter( + name = "testParameter2" + ) + testParameter3 = PythonParameter( + name = "testParameter3" + ) + testFunction = PythonFunction( + name = "testMethod", + parameters = mutableListOf( + testParameterSelf, + testParameter1, + testParameter2, + testParameter3 + ) + ) + testClass = PythonClass(name = "testClass", methods = mutableListOf(testFunction)) + testModule = PythonModule( + name = "testModule", + classes = mutableListOf(testClass) + ) + testPackage = PythonPackage( + distribution = "testPackage", + name = "testPackage", + version = "1.0.0", + modules = mutableListOf(testModule) + ) + } + + @Test + fun `should process Boundary- and GroupAnnotation on class method level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testGroup: TestGroup, testParameter3): + | return self.instance.testMethod(testGroup.testParameter1, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1, testParameter2): + | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): + | raise ValueError(f'testParameter1 needs to be an integer, but {testParameter1} was assigned.') + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') + | + | self.testParameter1 = testParameter1 + | self.testParameter2 = testParameter2 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary-, Group- and OptionalAnnotation on class method level`() { + // given + testParameter2.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.UNRESTRICTED, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN + ) + ) + testParameter2.annotations.add( + OptionalAnnotation( + DefaultNumber(0.5) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testGroup: TestGroup, testParameter3): + | return self.instance.testMethod(testGroup.testParameter1, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1, *, testParameter2=0.5): + | if not (isinstance(testParameter2, int) or (isinstance(testParameter2, float) and testParameter2.is_integer())): + | raise ValueError(f'testParameter2 needs to be an integer, but {testParameter2} was assigned.') + | if not testParameter2 < 1.0: + | raise ValueError(f'Valid values of testParameter2 must be less than 1.0, but {testParameter2} was assigned.') + | + | self.testParameter1 = testParameter1 + | self.testParameter2 = testParameter2 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary-, Group- and RequiredAnnotation on class method level`() { + // given + testParameter2.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter2.annotations.add( + RequiredAnnotation + ) + testParameter2.defaultValue = PythonInt(0) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testParameter1, testGroup: TestGroup): + | return self.instance.testMethod(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter2, testParameter3): + | if not (isinstance(testParameter2, int) or (isinstance(testParameter2, float) and testParameter2.is_integer())): + | raise ValueError(f'testParameter2 needs to be an integer, but {testParameter2} was assigned.') + | if not 0.0 <= testParameter2 <= 1.0: + | raise ValueError(f'Valid values of testParameter2 must be in [0.0, 1.0], but {testParameter2} was assigned.') + | + | self.testParameter2 = testParameter2 + | self.testParameter3 = testParameter3 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary- and OptionalAnnotation on class method level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter1.annotations.add( + OptionalAnnotation( + DefaultNumber(0.5) + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testParameter2, testParameter3, *, testParameter1=0.5): + | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): + | raise ValueError(f'testParameter1 needs to be an integer, but {testParameter1} was assigned.') + | if not 0.0 <= testParameter1 <= 1.0: + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') + | + | return self.instance.testMethod(testParameter1, testParameter2, testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary- and RequiredAnnotation on class method level`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.UNRESTRICTED + ) + ) + testParameter1.annotations.add( + RequiredAnnotation + ) + testParameter1.defaultValue = PythonStringifiedExpression("toRemove") + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testParameter1, testParameter2, testParameter3): + | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): + | raise ValueError(f'testParameter1 needs to be an integer, but {testParameter1} was assigned.') + | if not 0.0 < testParameter1: + | raise ValueError(f'Valid values of testParameter1 must be greater than 0.0, but {testParameter1} was assigned.') + | + | return self.instance.testMethod(testParameter1, testParameter2, testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum- and GroupAnnotation on class method level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + "TestEnum", + listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testGroup: TestGroup, testParameter3): + | return self.instance.testMethod(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1: TestEnum, testParameter2): + | self.testParameter1: TestEnum = testParameter1 + | self.testParameter2 = testParameter2 + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum-, Required- and GroupAnnotation on class method level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + enumName = "TestEnum", + pairs = listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testParameter1.annotations.add( + RequiredAnnotation + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testGroup: TestGroup, testParameter3): + | return self.instance.testMethod(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1: TestEnum, testParameter2): + | self.testParameter1: TestEnum = testParameter1 + | self.testParameter2 = testParameter2 + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum- and RequiredAnnotation on class method level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + enumName = "TestEnum", + pairs = listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testParameter1.annotations.add( + RequiredAnnotation + ) + testParameter1.defaultValue = PythonStringifiedExpression("toRemove") + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testParameter1: TestEnum, testParameter2, testParameter3): + | return self.instance.testMethod(testParameter1.value, testParameter2, testParameter3) + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Group- and RequiredAnnotation on class method level`() { + // given + testParameter2.annotations.add( + RequiredAnnotation + ) + testParameter2.defaultValue = PythonStringifiedExpression("toRemove") + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testParameter1, testGroup: TestGroup): + | return self.instance.testMethod(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter2, testParameter3): + | self.testParameter2 = testParameter2 + | self.testParameter3 = testParameter3 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Group- and OptionalAnnotation on class method level`() { + // given + testParameter2.annotations.add( + OptionalAnnotation( + DefaultBoolean(false) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testParameter1, testGroup: TestGroup): + | return self.instance.testMethod(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter3, *, testParameter2=False): + | self.testParameter3 = testParameter3 + | self.testParameter2 = testParameter2 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Group-, Required- and OptionalAnnotation on class method level`() { + // given + testParameter2.annotations.add( + OptionalAnnotation( + DefaultBoolean(false) + ) + ) + testParameter3.annotations.add( + RequiredAnnotation + ) + testParameter3.defaultValue = PythonStringifiedExpression("toRemove") + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testParameter1, testGroup: TestGroup): + | return self.instance.testMethod(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter3, *, testParameter2=False): + | self.testParameter3 = testParameter3 + | self.testParameter2 = testParameter2 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } } From 6c299902adcc2b39e909be232ea82cd543a63c50 Mon Sep 17 00:00:00 2001 From: PaulV Date: Wed, 12 Jan 2022 21:00:48 +0100 Subject: [PATCH 30/32] add tests for move / rename annotation --- .../ConstructorPythonCodeGeneratorTest.kt | 119 ++++++++++ .../FunctionPythonCodeGeneratorTest.kt | 214 +++++++++++++++--- .../MethodPythonCodeGeneratorTest.kt | 41 ++++ 3 files changed, 342 insertions(+), 32 deletions(-) diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt index c83eac46c..d6c32e7a3 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/ConstructorPythonCodeGeneratorTest.kt @@ -8,7 +8,9 @@ import com.larsreimann.api_editor.model.DefaultNumber import com.larsreimann.api_editor.model.EnumAnnotation import com.larsreimann.api_editor.model.EnumPair import com.larsreimann.api_editor.model.GroupAnnotation +import com.larsreimann.api_editor.model.MoveAnnotation import com.larsreimann.api_editor.model.OptionalAnnotation +import com.larsreimann.api_editor.model.RenameAnnotation import com.larsreimann.api_editor.model.RequiredAnnotation import com.larsreimann.api_editor.mutable_model.PythonClass import com.larsreimann.api_editor.mutable_model.PythonFunction @@ -598,4 +600,121 @@ class ConstructorPythonCodeGeneratorTest { moduleContent shouldBe expectedModuleContent } + + @Test + fun `should process Boundary-, Group-, OptionalAnnotation on constructor with RenameAnnotation on class level`() { + // given + testParameter2.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter2.annotations.add( + OptionalAnnotation( + DefaultNumber(0.5) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + testClass.annotations.add( + RenameAnnotation("renamedTestClass") + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class renamedTestClass: + | def __init__(self, testGroup: TestGroup, testParameter3): + | self.testGroup: TestGroup = testGroup + | self.testParameter3 = testParameter3 + | + | self.instance = testModule.testClass(testGroup.testParameter1, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1, *, testParameter2=0.5): + | if not (isinstance(testParameter2, int) or (isinstance(testParameter2, float) and testParameter2.is_integer())): + | raise ValueError(f'testParameter2 needs to be an integer, but {testParameter2} was assigned.') + | if not 0.0 <= testParameter2 <= 1.0: + | raise ValueError(f'Valid values of testParameter2 must be in [0.0, 1.0], but {testParameter2} was assigned.') + | + | self.testParameter1 = testParameter1 + | self.testParameter2 = testParameter2 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum-, GroupAnnotation on constructor with Rename-, MoveAnnotation on class level`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + "TestEnum", + listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + testClass.annotations.add( + RenameAnnotation("renamedTestClass") + ) + testClass.annotations.add( + MoveAnnotation("movedTestModule") + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class renamedTestClass: + | def __init__(self, testGroup: TestGroup, testParameter3): + | self.testGroup: TestGroup = testGroup + | self.testParameter3 = testParameter3 + | + | self.instance = testModule.testClass(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, testParameter1: TestEnum, testParameter2): + | self.testParameter1: TestEnum = testParameter1 + | self.testParameter2 = testParameter2 + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + testPackage.modules[0].name shouldBe "movedTestModule" + } } diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt index fb9a302d8..e43c9e8af 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt @@ -8,7 +8,9 @@ import com.larsreimann.api_editor.model.DefaultNumber import com.larsreimann.api_editor.model.EnumAnnotation import com.larsreimann.api_editor.model.EnumPair import com.larsreimann.api_editor.model.GroupAnnotation +import com.larsreimann.api_editor.model.MoveAnnotation import com.larsreimann.api_editor.model.OptionalAnnotation +import com.larsreimann.api_editor.model.RenameAnnotation import com.larsreimann.api_editor.model.RequiredAnnotation import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonModule @@ -40,7 +42,7 @@ class FunctionPythonCodeGeneratorTest { name = "testParameter3", ) testFunction = PythonFunction( - name = "__init__", + name = "testFunction", parameters = mutableListOf( testParameter1, testParameter2, @@ -91,17 +93,17 @@ class FunctionPythonCodeGeneratorTest { |class TestGroup: | def __init__(self, testParameter1, testParameter3): | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): - | raise ValueError('testParameter1' needs to be an integer, but {} was assigned.'.format(testParameter1)) + | raise ValueError(f'testParameter1 needs to be an integer, but {testParameter1} was assigned.') | if not 0.0 <= testParameter1 <= 1.0: - | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') | | self.testParameter1 = testParameter1 | self.testParameter3 = testParameter3 | - |def __init__(testGroup: TestGroup, testParameter2): - | return testModule.__init__(testGroup.testParameter1, testParameter2, testGroup.testParameter3) + |def testFunction(testGroup: TestGroup, testParameter2): + | return testModule.testFunction(testGroup.testParameter1, testParameter2, testGroup.testParameter3) | - |""".trimMargin() + """.trimMargin() moduleContent shouldBe expectedModuleContent } @@ -143,15 +145,15 @@ class FunctionPythonCodeGeneratorTest { |class TestGroup: | def __init__(self, testParameter3, *, testParameter2=0.5): | if not (isinstance(testParameter2, int) or (isinstance(testParameter2, float) and testParameter2.is_integer())): - | raise ValueError('testParameter2' needs to be an integer, but {} was assigned.'.format(testParameter2)) + | raise ValueError(f'testParameter2 needs to be an integer, but {testParameter2} was assigned.') | if not 0.0 <= testParameter2 <= 1.0: - | raise ValueError('Valid values of testParameter2 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter2)) + | raise ValueError(f'Valid values of testParameter2 must be in [0.0, 1.0], but {testParameter2} was assigned.') | | self.testParameter3 = testParameter3 | self.testParameter2 = testParameter2 | - |def __init__(testParameter1, testGroup: TestGroup): - | return testModule.__init__(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + |def testFunction(testParameter1, testGroup: TestGroup): + | return testModule.testFunction(testParameter1, testGroup.testParameter2, testGroup.testParameter3) | """.trimMargin() @@ -193,15 +195,15 @@ class FunctionPythonCodeGeneratorTest { |class TestGroup: | def __init__(self, testParameter2, testParameter3): | if not (isinstance(testParameter2, int) or (isinstance(testParameter2, float) and testParameter2.is_integer())): - | raise ValueError('testParameter2' needs to be an integer, but {} was assigned.'.format(testParameter2)) + | raise ValueError(f'testParameter2 needs to be an integer, but {testParameter2} was assigned.') | if not 0.0 <= testParameter2 <= 1.0: - | raise ValueError('Valid values of testParameter2 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter2)) + | raise ValueError(f'Valid values of testParameter2 must be in [0.0, 1.0], but {testParameter2} was assigned.') | | self.testParameter2 = testParameter2 | self.testParameter3 = testParameter3 | - |def __init__(testParameter1, testGroup: TestGroup): - | return testModule.__init__(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + |def testFunction(testParameter1, testGroup: TestGroup): + | return testModule.testFunction(testParameter1, testGroup.testParameter2, testGroup.testParameter3) | """.trimMargin() @@ -236,13 +238,13 @@ class FunctionPythonCodeGeneratorTest { | |from __future__ import annotations | - |def __init__(testParameter2, testParameter3, *, testParameter1=0.5): + |def testFunction(testParameter2, testParameter3, *, testParameter1=0.5): | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): | raise ValueError('testParameter1' needs to be an integer, but {} was assigned.'.format(testParameter1)) | if not 0.0 <= testParameter1 <= 1.0: | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) | - | return testModule.__init__(testParameter1, testParameter2, testParameter3) + | return testModule.testFunction(testParameter1, testParameter2, testParameter3) | """.trimMargin() @@ -276,13 +278,13 @@ class FunctionPythonCodeGeneratorTest { | |from __future__ import annotations | - |def __init__(testParameter1, testParameter2, testParameter3): + |def testFunction(testParameter1, testParameter2, testParameter3): | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): - | raise ValueError('testParameter1' needs to be an integer, but {} was assigned.'.format(testParameter1)) + | raise ValueError(f'testParameter1 needs to be an integer, but {testParameter1} was assigned.') | if not 0.0 <= testParameter1 <= 1.0: - | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') | - | return testModule.__init__(testParameter1, testParameter2, testParameter3) + | return testModule.testFunction(testParameter1, testParameter2, testParameter3) | """.trimMargin() @@ -324,8 +326,8 @@ class FunctionPythonCodeGeneratorTest { | self.testParameter1: TestEnum = testParameter1 | self.testParameter2 = testParameter2 | - |def __init__(testGroup: TestGroup, testParameter3): - | return testModule.__init__(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) + |def testFunction(testGroup: TestGroup, testParameter3): + | return testModule.testFunction(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) | |class TestEnum(Enum): | testName1 = 'testValue1', @@ -375,8 +377,8 @@ class FunctionPythonCodeGeneratorTest { | self.testParameter1: TestEnum = testParameter1 | self.testParameter2 = testParameter2 | - |def __init__(testGroup: TestGroup, testParameter3): - | return testModule.__init__(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) + |def testFunction(testGroup: TestGroup, testParameter3): + | return testModule.testFunction(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) | |class TestEnum(Enum): | testName1 = 'testValue1', @@ -415,8 +417,8 @@ class FunctionPythonCodeGeneratorTest { |from __future__ import annotations |from enum import Enum | - |def __init__(testParameter1: TestEnum, testParameter2, testParameter3): - | return testModule.__init__(testParameter1.value, testParameter2, testParameter3) + |def testFunction(testParameter1: TestEnum, testParameter2, testParameter3): + | return testModule.testFunction(testParameter1.value, testParameter2, testParameter3) | |class TestEnum(Enum): | testName1 = 'testValue1', @@ -457,8 +459,8 @@ class FunctionPythonCodeGeneratorTest { | self.testParameter2 = testParameter2 | self.testParameter3 = testParameter3 | - |def __init__(testGroup: TestGroup, *, testParameter1=defaultValue): - | return testModule.__init__(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + |def testFunction(testGroup: TestGroup, *, testParameter1=defaultValue): + | return testModule.testFunction(testParameter1, testGroup.testParameter2, testGroup.testParameter3) | """.trimMargin() @@ -495,8 +497,8 @@ class FunctionPythonCodeGeneratorTest { | self.testParameter3 = testParameter3 | self.testParameter2 = testParameter2 | - |def __init__(testParameter1, testGroup: TestGroup): - | return testModule.__init__(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + |def testFunction(testParameter1, testGroup: TestGroup): + | return testModule.testFunction(testParameter1, testGroup.testParameter2, testGroup.testParameter3) | """.trimMargin() @@ -537,8 +539,156 @@ class FunctionPythonCodeGeneratorTest { | self.testParameter3 = testParameter3 | self.testParameter2 = testParameter2 | - |def __init__(testParameter1, testGroup: TestGroup): - | return testModule.__init__(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + |def testFunction(testParameter1, testGroup: TestGroup): + | return testModule.testFunction(testParameter1, testGroup.testParameter2, testGroup.testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Boundary- and RenameAnnotation on parameter on a function with RenameAnnotation`() { + // given + testParameter1.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter1.annotations.add( + RenameAnnotation("renamedTestParameter1") + ) + testFunction.annotations.add( + RenameAnnotation("renamedTestFunction") + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |def renamedTestFunction(renamedTestParameter1, testParameter2, testParameter3): + | if not (isinstance(renamedTestParameter1, int) or (isinstance(renamedTestParameter1, float) and renamedTestParameter1.is_integer())): + | raise ValueError(f'renamedTestParameter1 needs to be an integer, but {renamedTestParameter1} was assigned.') + | if not 0.0 <= renamedTestParameter1 <= 1.0: + | raise ValueError(f'Valid values of renamedTestParameter1 must be in [0.0, 1.0], but {renamedTestParameter1} was assigned.') + | + | return testModule.testFunction(renamedTestParameter1, testParameter2, testParameter3) + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } + + @Test + fun `should process Enum- and GroupAnnotation on function with MoveAnnotation`() { + // given + testParameter1.annotations.add( + EnumAnnotation( + "TestEnum", + listOf( + EnumPair("testValue1", "testName1"), + EnumPair("testValue2", "testName2") + ) + ) + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + testFunction.annotations.add( + MoveAnnotation("movedTestModule") + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + |from enum import Enum + | + |class TestGroup: + | def __init__(self, testParameter1: TestEnum, testParameter2): + | self.testParameter1: TestEnum = testParameter1 + | self.testParameter2 = testParameter2 + | + |def testFunction(testGroup: TestGroup, testParameter3): + | return testModule.testFunction(testGroup.testParameter1.value, testGroup.testParameter2, testParameter3) + | + |class TestEnum(Enum): + | testName1 = 'testValue1', + | testName2 = 'testValue2' + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + testPackage.modules[0].name shouldBe "movedTestModule" + } + + @Test + fun `should process Rename-, Boundary-, Group- and OptionalAnnotation on function level`() { + // given + testParameter2.annotations.add( + BoundaryAnnotation( + isDiscrete = true, + lowerIntervalLimit = 0.0, + lowerLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS, + upperIntervalLimit = 1.0, + upperLimitType = ComparisonOperator.LESS_THAN_OR_EQUALS + ) + ) + testParameter2.annotations.add( + OptionalAnnotation( + DefaultNumber(0.5) + ) + ) + testParameter2.annotations.add( + RenameAnnotation("renamedTestParameter2") + ) + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter2", "testParameter3") + ) + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class TestGroup: + | def __init__(self, testParameter3, *, renamedTestParameter2): + | if not (isinstance(renamedTestParameter2, int) or (isinstance(renamedTestParameter2, float) and renamedTestParameter2.is_integer())): + | raise ValueError(f'renamedTestParameter2 needs to be an integer, but {renamedTestParameter2} was assigned.') + | if not 0.0 <= renamedTestParameter2 <= 1.0: + | raise ValueError(f'Valid values of renamedTestParameter2 must be in [0.0, 1.0], but {renamedTestParameter2} was assigned.') + | + | self.testParameter3 = testParameter3 + | self.renamedTestParameter2 = renamedTestParameter2 + | + |def testFunction(testParameter1, testGroup: TestGroup): + | return testModule.testFunction(testParameter1, renamedTestParameter2, testGroup.testParameter3) | """.trimMargin() diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/MethodPythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/MethodPythonCodeGeneratorTest.kt index dddda8045..30d390bda 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/MethodPythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/MethodPythonCodeGeneratorTest.kt @@ -9,6 +9,7 @@ import com.larsreimann.api_editor.model.EnumAnnotation import com.larsreimann.api_editor.model.EnumPair import com.larsreimann.api_editor.model.GroupAnnotation import com.larsreimann.api_editor.model.OptionalAnnotation +import com.larsreimann.api_editor.model.RenameAnnotation import com.larsreimann.api_editor.model.RequiredAnnotation import com.larsreimann.api_editor.mutable_model.PythonClass import com.larsreimann.api_editor.mutable_model.PythonFunction @@ -596,4 +597,44 @@ class MethodPythonCodeGeneratorTest { moduleContent shouldBe expectedModuleContent } + + @Test + fun `should process GroupAnnotation on class method with RenameAnnotation on parameter level`() { + // given + testFunction.annotations.add( + GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter1", "testParameter2") + ) + ) + testParameter1.annotations.add( + RenameAnnotation("renamedTestParameter1") + ) + // when + testPackage.transform() + val moduleContent = testPackage.modules[0].toPythonCode() + + // then + val expectedModuleContent: String = + """ + |import testModule + | + |from __future__ import annotations + | + |class testClass: + | def __init__(): + | self.instance = testModule.testClass() + | + | def testMethod(self, testGroup: TestGroup, testParameter3): + | return self.instance.testMethod(testGroup.renamedTestParameter1, testGroup.testParameter2, testParameter3) + | + |class TestGroup: + | def __init__(self, renamedTestParameter1, testParameter2): + | self.renamedTestParameter1 = renamedTestParameter1 + | self.testParameter2 = testParameter2 + | + """.trimMargin() + + moduleContent shouldBe expectedModuleContent + } } From 6d4a665e55d54a3011c2587eab91da25950e1952 Mon Sep 17 00:00:00 2001 From: PaulV Date: Wed, 12 Jan 2022 21:12:33 +0100 Subject: [PATCH 31/32] update one outdated test case --- .../python_code_gen/FunctionPythonCodeGeneratorTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt index e43c9e8af..dec96b66f 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt @@ -240,9 +240,9 @@ class FunctionPythonCodeGeneratorTest { | |def testFunction(testParameter2, testParameter3, *, testParameter1=0.5): | if not (isinstance(testParameter1, int) or (isinstance(testParameter1, float) and testParameter1.is_integer())): - | raise ValueError('testParameter1' needs to be an integer, but {} was assigned.'.format(testParameter1)) + | raise ValueError(f'testParameter1 needs to be an integer, but {testParameter1} was assigned.') | if not 0.0 <= testParameter1 <= 1.0: - | raise ValueError('Valid values of testParameter1 must be in [0.0, 1.0], but {} was assigned.'.format(testParameter1)) + | raise ValueError(f'Valid values of testParameter1 must be in [0.0, 1.0], but {testParameter1} was assigned.') | | return testModule.testFunction(testParameter1, testParameter2, testParameter3) | From e4ab5991ba3edeb78dda2b971737d5be116036a1 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Thu, 13 Jan 2022 17:23:46 +0100 Subject: [PATCH 32/32] fix: combination of group and rename annotations --- .../api_editor/model/editorAnnotations.kt | 2 +- .../RenameAnnotationProcessor.kt | 19 ++++++++++++++++ .../FunctionPythonCodeGeneratorTest.kt | 4 ++-- .../api_editor/server/ApplicationTest.kt | 2 +- .../RenameAnnotationProcessorTest.kt | 22 ++++++++++++++++--- .../validation/AnnotationValidatorTest.kt | 8 +++---- 6 files changed, 46 insertions(+), 11 deletions(-) diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt b/server/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt index 2b72bbf74..9aa59f7af 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/model/editorAnnotations.kt @@ -71,7 +71,7 @@ data class EnumAnnotation(val enumName: String, val pairs: List) : Edi data class EnumPair(val stringValue: String, val instanceName: String) @Serializable -data class GroupAnnotation(val groupName: String, val parameters: List) : EditorAnnotation() { +data class GroupAnnotation(val groupName: String, val parameters: MutableList) : EditorAnnotation() { @Transient override val validTargets = FUNCTIONS diff --git a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/RenameAnnotationProcessor.kt b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/RenameAnnotationProcessor.kt index 32a01f484..d049433b9 100644 --- a/server/src/main/kotlin/com/larsreimann/api_editor/transformation/RenameAnnotationProcessor.kt +++ b/server/src/main/kotlin/com/larsreimann/api_editor/transformation/RenameAnnotationProcessor.kt @@ -1,8 +1,12 @@ package com.larsreimann.api_editor.transformation +import com.larsreimann.api_editor.model.GroupAnnotation import com.larsreimann.api_editor.model.RenameAnnotation import com.larsreimann.api_editor.mutable_model.PythonDeclaration +import com.larsreimann.api_editor.mutable_model.PythonFunction import com.larsreimann.api_editor.mutable_model.PythonPackage +import com.larsreimann.api_editor.mutable_model.PythonParameter +import com.larsreimann.modeling.closest import com.larsreimann.modeling.descendants /** @@ -18,7 +22,22 @@ private fun PythonDeclaration.processRenameAnnotations() { this.annotations .filterIsInstance() .forEach { + (this as? PythonParameter)?.updateGroupAnnotationOnContainingFunction(it.newName) this.name = it.newName this.annotations.remove(it) } } + +private fun PythonParameter.updateGroupAnnotationOnContainingFunction(newName: String) { + val containingFunction = closest() ?: return + containingFunction.annotations + .filterIsInstance() + .forEach { annotation -> + annotation.parameters.replaceAll { oldName -> + when (oldName) { + this.name -> newName + else -> oldName + } + } + } +} diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt index dec96b66f..117dbeca1 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/codegen/python_code_gen/FunctionPythonCodeGeneratorTest.kt @@ -678,7 +678,7 @@ class FunctionPythonCodeGeneratorTest { |from __future__ import annotations | |class TestGroup: - | def __init__(self, testParameter3, *, renamedTestParameter2): + | def __init__(self, testParameter3, *, renamedTestParameter2=0.5): | if not (isinstance(renamedTestParameter2, int) or (isinstance(renamedTestParameter2, float) and renamedTestParameter2.is_integer())): | raise ValueError(f'renamedTestParameter2 needs to be an integer, but {renamedTestParameter2} was assigned.') | if not 0.0 <= renamedTestParameter2 <= 1.0: @@ -688,7 +688,7 @@ class FunctionPythonCodeGeneratorTest { | self.renamedTestParameter2 = renamedTestParameter2 | |def testFunction(testParameter1, testGroup: TestGroup): - | return testModule.testFunction(testParameter1, renamedTestParameter2, testGroup.testParameter3) + | return testModule.testFunction(testParameter1, testGroup.renamedTestParameter2, testGroup.testParameter3) | """.trimMargin() diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/server/ApplicationTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/server/ApplicationTest.kt index d211cd3de..a43d306d9 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/server/ApplicationTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/server/ApplicationTest.kt @@ -131,7 +131,7 @@ class ApplicationTest { ), GroupAnnotation( groupName = "test-group", - parameters = listOf( + parameters = mutableListOf( "test-parameter" ) ), diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/RenameAnnotationProcessorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/RenameAnnotationProcessorTest.kt index 697631522..bdf9feb37 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/transformation/RenameAnnotationProcessorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/transformation/RenameAnnotationProcessorTest.kt @@ -1,5 +1,6 @@ package com.larsreimann.api_editor.transformation +import com.larsreimann.api_editor.model.GroupAnnotation import com.larsreimann.api_editor.model.RenameAnnotation import com.larsreimann.api_editor.mutable_model.PythonClass import com.larsreimann.api_editor.mutable_model.PythonFunction @@ -7,6 +8,7 @@ import com.larsreimann.api_editor.mutable_model.PythonModule import com.larsreimann.api_editor.mutable_model.PythonPackage import com.larsreimann.api_editor.mutable_model.PythonParameter import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.shouldBe import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -15,6 +17,7 @@ class RenameAnnotationProcessorTest { private lateinit var testClass: PythonClass private lateinit var testFunction: PythonFunction private lateinit var testParameter: PythonParameter + private lateinit var testGroupAnnotation: GroupAnnotation private lateinit var testPackage: PythonPackage @BeforeEach @@ -25,13 +28,18 @@ class RenameAnnotationProcessorTest { ) testFunction = PythonFunction( name = "testFunction", - annotations = mutableListOf(RenameAnnotation("newTestFunction")) + annotations = mutableListOf( + RenameAnnotation("newTestFunction") + ) ) testParameter = PythonParameter( name = "testParameter", annotations = mutableListOf(RenameAnnotation("newTestParameter")) ) - + testGroupAnnotation = GroupAnnotation( + groupName = "TestGroup", + parameters = mutableListOf("testParameter") + ) testPackage = PythonPackage( distribution = "testPackage", name = "testPackage", @@ -44,7 +52,8 @@ class RenameAnnotationProcessorTest { testFunction, PythonFunction( name = "testFunction", - parameters = listOf(testParameter) + parameters = listOf(testParameter), + annotations = mutableListOf(testGroupAnnotation) ) ) ) @@ -91,6 +100,13 @@ class RenameAnnotationProcessorTest { testParameter.name shouldBe "newTestParameter" } + @Test + fun `should update GroupAnnotation on containing function`() { + testPackage.processRenameAnnotations() + + testGroupAnnotation.parameters.shouldContainExactly("newTestParameter") + } + @Test fun `should remove RenameAnnotations of parameters`() { testPackage.processRenameAnnotations() diff --git a/server/src/test/kotlin/com/larsreimann/api_editor/validation/AnnotationValidatorTest.kt b/server/src/test/kotlin/com/larsreimann/api_editor/validation/AnnotationValidatorTest.kt index e5b76f3fa..da337d85c 100644 --- a/server/src/test/kotlin/com/larsreimann/api_editor/validation/AnnotationValidatorTest.kt +++ b/server/src/test/kotlin/com/larsreimann/api_editor/validation/AnnotationValidatorTest.kt @@ -301,7 +301,7 @@ internal class AnnotationValidatorTest { CalledAfterAnnotation("calledAfterName2"), GroupAnnotation( "groupName", - listOf("test-module.test-function.test-parameter") + mutableListOf("test-module.test-function.test-parameter") ), RenameAnnotation("newName") ) @@ -548,7 +548,7 @@ internal class AnnotationValidatorTest { mutableListOf( GroupAnnotation( "paramGroup", - listOf("first-param", "second-param") + mutableListOf("first-param", "second-param") ) ) ), @@ -591,7 +591,7 @@ internal class AnnotationValidatorTest { mutableListOf( GroupAnnotation( "paramGroup", - listOf("second-param") + mutableListOf("second-param") ) ) ) @@ -641,7 +641,7 @@ internal class AnnotationValidatorTest { mutableListOf( GroupAnnotation( "paramGroup", - listOf("second-param") + mutableListOf("second-param") ) ) )