From e84ef6c7e20724b5b2cdb3d620228c3e810e1be9 Mon Sep 17 00:00:00 2001 From: Burak Date: Fri, 10 Feb 2023 14:00:53 +0000 Subject: [PATCH] Python: Type-stub generation for SSDKs (#2149) * Initial Python stub generation * Handle default values correctly * Only generate `__init__` for classes that have constructor signatures * Preserve doc comments * Make context class generic * Put type hint into a string to fix runtime error * Run `mypy` on CI * Use `make` to build Python SSDKs while generating diffs * Escape Python types in Rust comments * Only mark class methods with * Sort imports to minimize diffs * Add type annotations for `PySocket` * Dont extend classes from `object` as every class already implicitly extended from `object` * Use `vars` instead of `inspect.getmembers` to skip inherited members of a class * Fix linting issues * Add some tests for stubgen and refactor it * Add type annotations to `PyMiddlewareException` * Fix tests on Python 3.7 Python 3.7 doesn't support reading signatures from `__text_signature__` for non-builtin functions (i.e. C/Rust functions). For testing we're using regular Python syntax for defining signature. * Provide default values for `typing.Optional[T]` types in type-stubs * Update `is_fn_like` to cover more cases * Remove `tools/smithy-rs-tool-common/` * Make `DECORATORS` an array instead of a list * Ignore missing type stub errors for `aiohttp` --- .../rust/codegen/core/rustlang/RustWriter.kt | 1 + .../server/python/smithy/PythonType.kt | 174 ++++++++ .../PythonServerCodegenDecorator.kt | 64 +++ .../generators/PythonApplicationGenerator.kt | 56 ++- .../generators/PythonServerModuleGenerator.kt | 5 + .../PythonServerStructureGenerator.kt | 21 + .../PythonTypeInformationGenerationTest.kt | 46 ++ .../examples/Makefile | 30 +- .../examples/pokemon_service.py | 26 +- .../examples/pokemon_service_server_sdk.pyi | 20 - .../examples/stubgen.py | 422 ++++++++++++++++++ .../examples/stubgen_test.py | 407 +++++++++++++++++ .../src/context.rs | 1 - .../src/error.rs | 9 +- .../src/lambda.rs | 77 +++- .../src/logging.rs | 6 + .../src/middleware/request.rs | 11 +- .../src/middleware/response.rs | 15 +- .../src/socket.rs | 10 +- .../aws-smithy-http-server-python/src/tls.rs | 13 +- .../src/types.rs | 56 +++ .../aws-smithy-http-server-python/src/util.rs | 1 - tools/ci-build/Dockerfile | 1 + tools/ci-scripts/codegen-diff-revisions.py | 9 +- 24 files changed, 1412 insertions(+), 69 deletions(-) create mode 100644 codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonType.kt create mode 100644 codegen-server/python/src/test/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonTypeInformationGenerationTest.kt delete mode 100644 rust-runtime/aws-smithy-http-server-python/examples/pokemon_service_server_sdk.pyi create mode 100644 rust-runtime/aws-smithy-http-server-python/examples/stubgen.py create mode 100644 rust-runtime/aws-smithy-http-server-python/examples/stubgen_test.py diff --git a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt index 7fb70e483f..25fb313b27 100644 --- a/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt +++ b/codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt @@ -413,6 +413,7 @@ class RustWriter private constructor( fun factory(debugMode: Boolean): Factory = Factory { fileName: String, namespace: String -> when { fileName.endsWith(".toml") -> RustWriter(fileName, namespace, "#", debugMode = debugMode) + fileName.endsWith(".py") -> RustWriter(fileName, namespace, "#", debugMode = debugMode) fileName.endsWith(".md") -> rawWriter(fileName, debugMode = debugMode) fileName == "LICENSE" -> rawWriter(fileName, debugMode = debugMode) fileName.startsWith("tests/") -> RustWriter( diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonType.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonType.kt new file mode 100644 index 0000000000..2dd2f6ba65 --- /dev/null +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonType.kt @@ -0,0 +1,174 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.server.python.smithy + +import software.amazon.smithy.rust.codegen.core.rustlang.RustType + +/** + * A hierarchy of Python types handled by Smithy codegen. + * + * Mostly copied from [RustType] and modified for Python accordingly. + */ +sealed class PythonType { + /** + * A Python type that contains [member], another [PythonType]. + * Used to generically operate over shapes that contain other shape. + */ + sealed interface Container { + val member: PythonType + val namespace: String? + val name: String + } + + /** + * Name refers to the top-level type for import purposes. + */ + abstract val name: String + + open val namespace: String? = null + + object None : PythonType() { + override val name: String = "None" + } + + object Bool : PythonType() { + override val name: String = "bool" + } + + object Int : PythonType() { + override val name: String = "int" + } + + object Float : PythonType() { + override val name: String = "float" + } + + object Str : PythonType() { + override val name: String = "str" + } + + object Any : PythonType() { + override val name: String = "Any" + override val namespace: String = "typing" + } + + data class List(override val member: PythonType) : PythonType(), Container { + override val name: String = "List" + override val namespace: String = "typing" + } + + data class Dict(val key: PythonType, override val member: PythonType) : PythonType(), Container { + override val name: String = "Dict" + override val namespace: String = "typing" + } + + data class Set(override val member: PythonType) : PythonType(), Container { + override val name: String = "Set" + override val namespace: String = "typing" + } + + data class Optional(override val member: PythonType) : PythonType(), Container { + override val name: String = "Optional" + override val namespace: String = "typing" + } + + data class Awaitable(override val member: PythonType) : PythonType(), Container { + override val name: String = "Awaitable" + override val namespace: String = "typing" + } + + data class Callable(val args: kotlin.collections.List, val rtype: PythonType) : PythonType() { + override val name: String = "Callable" + override val namespace: String = "typing" + } + + data class Union(val args: kotlin.collections.List) : PythonType() { + override val name: String = "Union" + override val namespace: String = "typing" + } + + data class Opaque(override val name: String, val rustNamespace: String? = null) : PythonType() { + // Since Python doesn't have a something like Rust's `crate::` we are using a custom placeholder here + // and in our stub generation script we will replace placeholder with the real root module name. + private val pythonRootModulePlaceholder = "__root_module_name__" + + override val namespace: String? = rustNamespace?.split("::")?.joinToString(".") { + when (it) { + "crate" -> pythonRootModulePlaceholder + // In Python, we expose submodules from `aws_smithy_http_server_python` + // like `types`, `middleware`, `tls` etc. from `__root_module__name` + "aws_smithy_http_server_python" -> pythonRootModulePlaceholder + else -> it + } + } + } +} + +/** + * Return corresponding [PythonType] for a [RustType]. + */ +fun RustType.pythonType(): PythonType = + when (this) { + is RustType.Unit -> PythonType.None + is RustType.Bool -> PythonType.Bool + is RustType.Float -> PythonType.Float + is RustType.Integer -> PythonType.Int + is RustType.String -> PythonType.Str + is RustType.Vec -> PythonType.List(this.member.pythonType()) + is RustType.Slice -> PythonType.List(this.member.pythonType()) + is RustType.HashMap -> PythonType.Dict(this.key.pythonType(), this.member.pythonType()) + is RustType.HashSet -> PythonType.Set(this.member.pythonType()) + is RustType.Reference -> this.member.pythonType() + is RustType.Option -> PythonType.Optional(this.member.pythonType()) + is RustType.Box -> this.member.pythonType() + is RustType.Dyn -> this.member.pythonType() + is RustType.Opaque -> PythonType.Opaque(this.name, this.namespace) + // TODO(Constraints): How to handle this? + // Revisit as part of https://github.com/awslabs/smithy-rs/issues/2114 + is RustType.MaybeConstrained -> this.member.pythonType() + } + +/** + * Render this type, including references and generic parameters. + * It generates something like `typing.Dict[String, String]`. + */ +fun PythonType.render(fullyQualified: Boolean = true): String { + val namespace = if (fullyQualified) { + this.namespace?.let { "$it." } ?: "" + } else "" + val base = when (this) { + is PythonType.None -> this.name + is PythonType.Bool -> this.name + is PythonType.Float -> this.name + is PythonType.Int -> this.name + is PythonType.Str -> this.name + is PythonType.Any -> this.name + is PythonType.Opaque -> this.name + is PythonType.List -> "${this.name}[${this.member.render(fullyQualified)}]" + is PythonType.Dict -> "${this.name}[${this.key.render(fullyQualified)}, ${this.member.render(fullyQualified)}]" + is PythonType.Set -> "${this.name}[${this.member.render(fullyQualified)}]" + is PythonType.Awaitable -> "${this.name}[${this.member.render(fullyQualified)}]" + is PythonType.Optional -> "${this.name}[${this.member.render(fullyQualified)}]" + is PythonType.Callable -> { + val args = this.args.joinToString(", ") { it.render(fullyQualified) } + val rtype = this.rtype.render(fullyQualified) + "${this.name}[[$args], $rtype]" + } + is PythonType.Union -> { + val args = this.args.joinToString(", ") { it.render(fullyQualified) } + "${this.name}[$args]" + } + } + return "$namespace$base" +} + +/** + * Renders [PythonType] with proper escaping for Docstrings. + */ +fun PythonType.renderAsDocstring(): String = + this.render() + .replace("[", "\\[") + .replace("]", "\\]") diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/customizations/PythonServerCodegenDecorator.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/customizations/PythonServerCodegenDecorator.kt index 8de0a3f056..15de1bab08 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/customizations/PythonServerCodegenDecorator.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/customizations/PythonServerCodegenDecorator.kt @@ -98,6 +98,7 @@ class PubUsePythonTypesDecorator : ServerCodegenDecorator { /** * Generates `pyproject.toml` for the crate. * - Configures Maturin as the build system + * - Configures Python source directory */ class PyProjectTomlDecorator : ServerCodegenDecorator { override val name: String = "PyProjectTomlDecorator" @@ -110,6 +111,11 @@ class PyProjectTomlDecorator : ServerCodegenDecorator { "requires" to listOfNotNull("maturin>=0.14,<0.15"), "build-backend" to "maturin", ).toMap(), + "tool" to listOfNotNull( + "maturin" to listOfNotNull( + "python-source" to "python", + ).toMap(), + ).toMap(), ) writeWithNoFormatting(TomlWriter().write(config)) } @@ -134,6 +140,60 @@ class PyO3ExtensionModuleDecorator : ServerCodegenDecorator { } } +/** + * Generates `__init__.py` for the Python source. + * + * This file allows Python module to be imported like: + * ``` + * import pokemon_service_server_sdk + * pokemon_service_server_sdk.App() + * ``` + * instead of: + * ``` + * from pokemon_service_server_sdk import pokemon_service_server_sdk + * ``` + */ +class InitPyDecorator : ServerCodegenDecorator { + override val name: String = "InitPyDecorator" + override val order: Byte = 0 + + override fun extras(codegenContext: ServerCodegenContext, rustCrate: RustCrate) { + val libName = codegenContext.settings.moduleName.toSnakeCase() + + rustCrate.withFile("python/$libName/__init__.py") { + writeWithNoFormatting( + """ +from .$libName import * + +__doc__ = $libName.__doc__ +if hasattr($libName, "__all__"): + __all__ = $libName.__all__ + """.trimIndent(), + ) + } + } +} + +/** + * Generates `py.typed` for the Python source. + * + * This marker file is required to be PEP 561 compliant stub package. + * Type definitions will be ignored by `mypy` if the package is not PEP 561 compliant: + * https://mypy.readthedocs.io/en/stable/running_mypy.html#missing-library-stubs-or-py-typed-marker + */ +class PyTypedMarkerDecorator : ServerCodegenDecorator { + override val name: String = "PyTypedMarkerDecorator" + override val order: Byte = 0 + + override fun extras(codegenContext: ServerCodegenContext, rustCrate: RustCrate) { + val libName = codegenContext.settings.moduleName.toSnakeCase() + + rustCrate.withFile("python/$libName/py.typed") { + writeWithNoFormatting("") + } + } +} + val DECORATORS = arrayOf( /** * Add the [InternalServerError] error to all operations. @@ -150,4 +210,8 @@ val DECORATORS = arrayOf( PyProjectTomlDecorator(), // Add PyO3 extension module feature. PyO3ExtensionModuleDecorator(), + // Generate `__init__.py` for the Python source. + InitPyDecorator(), + // Generate `py.typed` for the Python source. + PyTypedMarkerDecorator(), ) diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonApplicationGenerator.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonApplicationGenerator.kt index 793118b441..dfddfe9b8a 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonApplicationGenerator.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonApplicationGenerator.kt @@ -22,6 +22,8 @@ import software.amazon.smithy.rust.codegen.core.util.outputShape import software.amazon.smithy.rust.codegen.core.util.toPascalCase import software.amazon.smithy.rust.codegen.core.util.toSnakeCase import software.amazon.smithy.rust.codegen.server.python.smithy.PythonServerCargoDependency +import software.amazon.smithy.rust.codegen.server.python.smithy.PythonType +import software.amazon.smithy.rust.codegen.server.python.smithy.renderAsDocstring import software.amazon.smithy.rust.codegen.server.smithy.ServerCargoDependency import software.amazon.smithy.rust.codegen.server.smithy.generators.protocol.ServerProtocol @@ -103,6 +105,9 @@ class PythonApplicationGenerator( """ ##[#{pyo3}::pyclass] ##[derive(Debug)] + /// :generic Ctx: + /// :extends typing.Generic\[Ctx\]: + /// :rtype None: pub struct App { handlers: #{HashMap}, middlewares: Vec<#{SmithyPython}::PyMiddlewareHandler>, @@ -239,6 +244,12 @@ class PythonApplicationGenerator( """, *codegenScope, ) { + val middlewareRequest = PythonType.Opaque("Request", "crate::middleware") + val middlewareResponse = PythonType.Opaque("Response", "crate::middleware") + val middlewareNext = PythonType.Callable(listOf(middlewareRequest), PythonType.Awaitable(middlewareResponse)) + val middlewareFunc = PythonType.Callable(listOf(middlewareRequest, middlewareNext), PythonType.Awaitable(middlewareResponse)) + val tlsConfig = PythonType.Opaque("TlsConfig", "crate::tls") + rustTemplate( """ /// Create a new [App]. @@ -246,12 +257,20 @@ class PythonApplicationGenerator( pub fn new() -> Self { Self::default() } + /// Register a context object that will be shared between handlers. + /// + /// :param context Ctx: + /// :rtype ${PythonType.None.renderAsDocstring()}: ##[pyo3(text_signature = "(${'$'}self, context)")] pub fn context(&mut self, context: #{pyo3}::PyObject) { self.context = Some(context); } + /// Register a Python function to be executed inside a Tower middleware layer. + /// + /// :param func ${middlewareFunc.renderAsDocstring()}: + /// :rtype ${PythonType.None.renderAsDocstring()}: ##[pyo3(text_signature = "(${'$'}self, func)")] pub fn middleware(&mut self, py: #{pyo3}::Python, func: #{pyo3}::PyObject) -> #{pyo3}::PyResult<()> { let handler = #{SmithyPython}::PyMiddlewareHandler::new(py, func)?; @@ -263,8 +282,16 @@ class PythonApplicationGenerator( self.middlewares.push(handler); Ok(()) } + /// Main entrypoint: start the server on multiple workers. - ##[pyo3(text_signature = "(${'$'}self, address, port, backlog, workers, tls)")] + /// + /// :param address ${PythonType.Optional(PythonType.Str).renderAsDocstring()}: + /// :param port ${PythonType.Optional(PythonType.Int).renderAsDocstring()}: + /// :param backlog ${PythonType.Optional(PythonType.Int).renderAsDocstring()}: + /// :param workers ${PythonType.Optional(PythonType.Int).renderAsDocstring()}: + /// :param tls ${PythonType.Optional(tlsConfig).renderAsDocstring()}: + /// :rtype ${PythonType.None.renderAsDocstring()}: + ##[pyo3(text_signature = "(${'$'}self, address=None, port=None, backlog=None, workers=None, tls=None)")] pub fn run( &mut self, py: #{pyo3}::Python, @@ -277,7 +304,10 @@ class PythonApplicationGenerator( use #{SmithyPython}::PyApp; self.run_server(py, address, port, backlog, workers, tls) } + /// Lambda entrypoint: start the server on Lambda. + /// + /// :rtype ${PythonType.None.renderAsDocstring()}: ##[pyo3(text_signature = "(${'$'}self)")] pub fn run_lambda( &mut self, @@ -286,8 +316,9 @@ class PythonApplicationGenerator( use #{SmithyPython}::PyApp; self.run_lambda_handler(py) } + /// Build the service and start a single worker. - ##[pyo3(text_signature = "(${'$'}self, socket, worker_number, tls)")] + ##[pyo3(text_signature = "(${'$'}self, socket, worker_number, tls=None)")] pub fn start_worker( &mut self, py: pyo3::Python, @@ -306,10 +337,31 @@ class PythonApplicationGenerator( operations.map { operation -> val operationName = symbolProvider.toSymbol(operation).name val name = operationName.toSnakeCase() + + val input = PythonType.Opaque("${operationName}Input", "crate::input") + val output = PythonType.Opaque("${operationName}Output", "crate::output") + val context = PythonType.Opaque("Ctx") + val returnType = PythonType.Union(listOf(output, PythonType.Awaitable(output))) + val handler = PythonType.Union( + listOf( + PythonType.Callable( + listOf(input, context), + returnType, + ), + PythonType.Callable( + listOf(input), + returnType, + ), + ), + ) + rustTemplate( """ /// Method to register `$name` Python implementation inside the handlers map. /// It can be used as a function decorator in Python. + /// + /// :param func ${handler.renderAsDocstring()}: + /// :rtype ${PythonType.None.renderAsDocstring()}: ##[pyo3(text_signature = "(${'$'}self, func)")] pub fn $name(&mut self, py: #{pyo3}::Python, func: #{pyo3}::PyObject) -> #{pyo3}::PyResult<()> { use #{SmithyPython}::PyApp; diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerModuleGenerator.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerModuleGenerator.kt index 9d9d4d2617..df7e246880 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerModuleGenerator.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerModuleGenerator.kt @@ -99,6 +99,7 @@ class PythonServerModuleGenerator( let types = #{pyo3}::types::PyModule::new(py, "types")?; types.add_class::<#{SmithyPython}::types::Blob>()?; types.add_class::<#{SmithyPython}::types::DateTime>()?; + types.add_class::<#{SmithyPython}::types::Format>()?; types.add_class::<#{SmithyPython}::types::ByteStream>()?; #{pyo3}::py_run!( py, @@ -185,6 +186,10 @@ class PythonServerModuleGenerator( """ let aws_lambda = #{pyo3}::types::PyModule::new(py, "aws_lambda")?; aws_lambda.add_class::<#{SmithyPython}::lambda::PyLambdaContext>()?; + aws_lambda.add_class::<#{SmithyPython}::lambda::PyClientApplication>()?; + aws_lambda.add_class::<#{SmithyPython}::lambda::PyClientContext>()?; + aws_lambda.add_class::<#{SmithyPython}::lambda::PyCognitoIdentity>()?; + aws_lambda.add_class::<#{SmithyPython}::lambda::PyConfig>()?; pyo3::py_run!( py, aws_lambda, diff --git a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerStructureGenerator.kt b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerStructureGenerator.kt index 436d956fa2..8b103a2821 100644 --- a/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerStructureGenerator.kt +++ b/codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerStructureGenerator.kt @@ -23,6 +23,9 @@ import software.amazon.smithy.rust.codegen.core.smithy.generators.StructureGener import software.amazon.smithy.rust.codegen.core.smithy.rustType import software.amazon.smithy.rust.codegen.core.util.hasTrait import software.amazon.smithy.rust.codegen.server.python.smithy.PythonServerCargoDependency +import software.amazon.smithy.rust.codegen.server.python.smithy.PythonType +import software.amazon.smithy.rust.codegen.server.python.smithy.pythonType +import software.amazon.smithy.rust.codegen.server.python.smithy.renderAsDocstring /** * To share structures defined in Rust with Python, `pyo3` provides the `PyClass` trait. @@ -52,6 +55,7 @@ class PythonServerStructureGenerator( } else { Attribute(pyO3.resolve("pyclass")).render(writer) } + writer.rustTemplate("#{ConstructorSignature:W}", "ConstructorSignature" to renderConstructorSignature()) super.renderStructure() renderPyO3Methods() } @@ -65,6 +69,7 @@ class PythonServerStructureGenerator( writer.addDependency(PythonServerCargoDependency.PyO3) // Above, we manually add dependency since we can't use a `RuntimeType` below Attribute("pyo3(get, set)").render(writer) + writer.rustTemplate("#{Signature:W}", "Signature" to renderSymbolSignature(memberSymbol)) super.renderStructureMember(writer, member, memberName, memberSymbol) } @@ -107,4 +112,20 @@ class PythonServerStructureGenerator( rust("$memberName,") } } + + private fun renderConstructorSignature(): Writable = + writable { + forEachMember(members) { _, memberName, memberSymbol -> + val memberType = memberSymbol.rustType().pythonType() + rust("/// :param $memberName ${memberType.renderAsDocstring()}:") + } + + rust("/// :rtype ${PythonType.None.renderAsDocstring()}:") + } + + private fun renderSymbolSignature(symbol: Symbol): Writable = + writable { + val pythonType = symbol.rustType().pythonType() + rust("/// :type ${pythonType.renderAsDocstring()}:") + } } diff --git a/codegen-server/python/src/test/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonTypeInformationGenerationTest.kt b/codegen-server/python/src/test/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonTypeInformationGenerationTest.kt new file mode 100644 index 0000000000..3291e2ed05 --- /dev/null +++ b/codegen-server/python/src/test/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonTypeInformationGenerationTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package software.amazon.smithy.rust.codegen.server.python.smithy.generators + +import io.kotest.matchers.string.shouldContain +import org.junit.jupiter.api.Test +import software.amazon.smithy.model.shapes.StructureShape +import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter +import software.amazon.smithy.rust.codegen.core.smithy.CodegenTarget +import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel +import software.amazon.smithy.rust.codegen.core.util.lookup +import software.amazon.smithy.rust.codegen.server.smithy.testutil.serverTestCodegenContext + +internal class PythonTypeInformationGenerationTest { + @Test + fun `generates python type information`() { + val model = """ + namespace test + + structure Foo { + @required + bar: String, + baz: Integer + } + """.asSmithyModel() + val foo = model.lookup("test#Foo") + + val codegenContext = serverTestCodegenContext(model) + val symbolProvider = codegenContext.symbolProvider + val writer = RustWriter.forModule("model") + PythonServerStructureGenerator(model, symbolProvider, writer, foo).render(CodegenTarget.SERVER) + + val result = writer.toString() + + // Constructor signature + result.shouldContain("/// :param bar str:") + result.shouldContain("/// :param baz typing.Optional\\[int\\]:") + + // Field types + result.shouldContain("/// :type str:") + result.shouldContain("/// :type typing.Optional\\[int\\]:") + } +} diff --git a/rust-runtime/aws-smithy-http-server-python/examples/Makefile b/rust-runtime/aws-smithy-http-server-python/examples/Makefile index 22be94a19e..bd4e88bba7 100644 --- a/rust-runtime/aws-smithy-http-server-python/examples/Makefile +++ b/rust-runtime/aws-smithy-http-server-python/examples/Makefile @@ -25,29 +25,35 @@ endif # Note on `--compatibility linux`: Maturin by default uses `manylinux_x_y` but it is not supported # by our current CI version (3.7.10), we can drop `--compatibility linux` when we switch to higher Python version. # For more detail: https://github.com/pypa/manylinux -build-wheel: ensure-maturin codegen - maturin build --manifest-path $(SERVER_SDK_DST)/Cargo.toml --out $(WHEELS) --compatibility linux +build-wheel: ensure-maturin + cd $(SERVER_SDK_DST) && maturin build --out $(WHEELS) --compatibility linux -build-wheel-release: ensure-maturin codegen - maturin build --manifest-path $(SERVER_SDK_DST)/Cargo.toml --out $(WHEELS) --compatibility linux --release +build-wheel-release: ensure-maturin + cd $(SERVER_SDK_DST) && maturin build --out $(WHEELS) --compatibility linux --release install-wheel: find $(WHEELS) -type f -name '*.whl' | xargs python3 -m pip install --user --force-reinstall -build: build-wheel install-wheel +generate-stubs: + python3 $(CUR_DIR)/stubgen.py pokemon_service_server_sdk $(SERVER_SDK_DST)/python/pokemon_service_server_sdk -release: build-wheel-release install-wheel +build: codegen + $(MAKE) build-wheel + $(MAKE) install-wheel + $(MAKE) generate-stubs + $(MAKE) build-wheel-release + $(MAKE) install-wheel run: build python3 $(CUR_DIR)/pokemon_service.py -run-release: release - python3 $(CUR_DIR)/pokemon_service.py - py-check: build - mypy pokemon_service.py + python3 -m mypy pokemon_service.py + +py-test: + python3 stubgen_test.py -test: build +test: build py-check py-test cargo test clippy: codegen @@ -60,6 +66,6 @@ clean: cargo clean || echo "Unable to run cargo clean" distclean: clean - rm -rf $(SERVER_SDK_DST) $(CLIENT_SDK_DST) $(WHEELS) $(CUR_DIR)/Cargo.lock + rm -rf $(SERVER_SDK_DST) $(SERVER_SDK_SRC) $(CLIENT_SDK_DST) $(CLIENT_SDK_SRC) $(WHEELS) $(CUR_DIR)/Cargo.lock .PHONY: all diff --git a/rust-runtime/aws-smithy-http-server-python/examples/pokemon_service.py b/rust-runtime/aws-smithy-http-server-python/examples/pokemon_service.py index 1f45c5fe13..a3c7bf1c93 100644 --- a/rust-runtime/aws-smithy-http-server-python/examples/pokemon_service.py +++ b/rust-runtime/aws-smithy-http-server-python/examples/pokemon_service.py @@ -8,34 +8,34 @@ import random from threading import Lock from dataclasses import dataclass -from typing import List, Optional, Callable, Awaitable +from typing import Dict, Any, List, Optional, Callable, Awaitable from pokemon_service_server_sdk import App -from pokemon_service_server_sdk.tls import TlsConfig # type: ignore -from pokemon_service_server_sdk.aws_lambda import LambdaContext # type: ignore -from pokemon_service_server_sdk.error import ResourceNotFoundException # type: ignore -from pokemon_service_server_sdk.input import ( # type: ignore +from pokemon_service_server_sdk.tls import TlsConfig +from pokemon_service_server_sdk.aws_lambda import LambdaContext +from pokemon_service_server_sdk.error import ResourceNotFoundException +from pokemon_service_server_sdk.input import ( DoNothingInput, GetPokemonSpeciesInput, GetServerStatisticsInput, CheckHealthInput, StreamPokemonRadioInput, ) -from pokemon_service_server_sdk.logging import TracingHandler # type: ignore -from pokemon_service_server_sdk.middleware import ( # type: ignore +from pokemon_service_server_sdk.logging import TracingHandler +from pokemon_service_server_sdk.middleware import ( MiddlewareException, Response, Request, ) -from pokemon_service_server_sdk.model import FlavorText, Language # type: ignore -from pokemon_service_server_sdk.output import ( # type: ignore +from pokemon_service_server_sdk.model import FlavorText, Language +from pokemon_service_server_sdk.output import ( DoNothingOutput, GetPokemonSpeciesOutput, GetServerStatisticsOutput, CheckHealthOutput, StreamPokemonRadioOutput, ) -from pokemon_service_server_sdk.types import ByteStream # type: ignore +from pokemon_service_server_sdk.types import ByteStream # Logging can bee setup using standard Python tooling. We provide # fast logging handler, Tracingandler based on Rust tracing crate. @@ -131,7 +131,7 @@ def get_random_radio_stream(self) -> str: # Entrypoint ########################################################### # Get an App instance. -app = App() +app: "App[Context]" = App() # Register the context. app.context(Context()) @@ -249,7 +249,7 @@ def check_health(_: CheckHealthInput) -> CheckHealthOutput: async def stream_pokemon_radio( _: StreamPokemonRadioInput, context: Context ) -> StreamPokemonRadioOutput: - import aiohttp + import aiohttp # type: ignore radio_url = context.get_random_radio_stream() logging.info("Random radio URL for this stream is %s", radio_url) @@ -270,7 +270,7 @@ def main() -> None: parser.add_argument("--tls-cert-path") args = parser.parse_args() - config = dict(workers=1) + config: Dict[str, Any] = dict(workers=1) if args.enable_tls: config["tls"] = TlsConfig( key_path=args.tls_key_path, diff --git a/rust-runtime/aws-smithy-http-server-python/examples/pokemon_service_server_sdk.pyi b/rust-runtime/aws-smithy-http-server-python/examples/pokemon_service_server_sdk.pyi deleted file mode 100644 index ccbd5edb2e..0000000000 --- a/rust-runtime/aws-smithy-http-server-python/examples/pokemon_service_server_sdk.pyi +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 - -# NOTE: This is manually created to surpass some mypy errors and it is incomplete, -# in future we will autogenerate correct stubs. - -from typing import Any, TypeVar, Callable - -F = TypeVar("F", bound=Callable[..., Any]) - -class App: - context: Any - run: Any - - def middleware(self, func: F) -> F: ... - def do_nothing(self, func: F) -> F: ... - def get_pokemon_species(self, func: F) -> F: ... - def get_server_statistics(self, func: F) -> F: ... - def check_health(self, func: F) -> F: ... - def stream_pokemon_radio(self, func: F) -> F: ... diff --git a/rust-runtime/aws-smithy-http-server-python/examples/stubgen.py b/rust-runtime/aws-smithy-http-server-python/examples/stubgen.py new file mode 100644 index 0000000000..30348838d2 --- /dev/null +++ b/rust-runtime/aws-smithy-http-server-python/examples/stubgen.py @@ -0,0 +1,422 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations +import re +import inspect +import textwrap +from pathlib import Path +from typing import Any, Set, Dict, List, Tuple, Optional + +ROOT_MODULE_NAME_PLACEHOLDER = "__root_module_name__" + + +class Writer: + """ + Writer provides utilities for writing Python stubs. + """ + + root_module_name: str + path: Path + subwriters: List[Writer] + imports: Set[str] + defs: List[str] + generics: Set[str] + + def __init__(self, path: Path, root_module_name: str) -> None: + self.path = path + self.root_module_name = root_module_name + self.subwriters = [] + self.imports = set([]) + self.defs = [] + self.generics = set([]) + + def fix_path(self, path: str) -> str: + """ + Returns fixed version of given type path. + It unescapes `\\[` and `\\]` and also populates placeholder for root module name. + """ + return ( + path.replace(ROOT_MODULE_NAME_PLACEHOLDER, self.root_module_name) + .replace("\\[", "[") + .replace("\\]", "]") + ) + + def submodule(self, path: Path) -> Writer: + w = Writer(path, self.root_module_name) + self.subwriters.append(w) + return w + + def include(self, path: str) -> str: + # `path` might be nested like: typing.Optional[typing.List[pokemon_service_server_sdk.model.GetPokemonSpecies]] + # we need to process every subpath in a nested path + paths = filter(lambda p: p, re.split("\\[|\\]|,| ", path)) + for subpath in paths: + parts = subpath.rsplit(".", maxsplit=1) + # add `typing` to imports for a path like `typing.List` + # but skip if the path doesn't have any namespace like `str` or `bool` + if len(parts) == 2: + self.imports.add(parts[0]) + + return path + + def fix_and_include(self, path: str) -> str: + return self.include(self.fix_path(path)) + + def define(self, code: str) -> None: + self.defs.append(code) + + def generic(self, name: str) -> None: + self.generics.add(name) + + def dump(self) -> None: + for w in self.subwriters: + w.dump() + + generics = "" + for g in sorted(self.generics): + generics += f"{g} = {self.include('typing.TypeVar')}('{g}')\n" + + self.path.parent.mkdir(parents=True, exist_ok=True) + contents = join([f"import {p}" for p in sorted(self.imports)]) + contents += "\n\n" + if generics: + contents += generics + "\n" + contents += join(self.defs) + self.path.write_text(contents) + + +class DocstringParserResult: + def __init__(self) -> None: + self.types: List[str] = [] + self.params: List[Tuple[str, str]] = [] + self.rtypes: List[str] = [] + self.generics: List[str] = [] + self.extends: List[str] = [] + + +def parse_type_directive(line: str, res: DocstringParserResult): + parts = line.split(" ", maxsplit=1) + if len(parts) != 2: + raise ValueError( + f"Invalid `:type` directive: `{line}` must be in `:type T:` format" + ) + res.types.append(parts[1].rstrip(":")) + + +def parse_rtype_directive(line: str, res: DocstringParserResult): + parts = line.split(" ", maxsplit=1) + if len(parts) != 2: + raise ValueError( + f"Invalid `:rtype` directive: `{line}` must be in `:rtype T:` format" + ) + res.rtypes.append(parts[1].rstrip(":")) + + +def parse_param_directive(line: str, res: DocstringParserResult): + parts = line.split(" ", maxsplit=2) + if len(parts) != 3: + raise ValueError( + f"Invalid `:param` directive: `{line}` must be in `:param name T:` format" + ) + name = parts[1] + ty = parts[2].rstrip(":") + res.params.append((name, ty)) + + +def parse_generic_directive(line: str, res: DocstringParserResult): + parts = line.split(" ", maxsplit=1) + if len(parts) != 2: + raise ValueError( + f"Invalid `:generic` directive: `{line}` must be in `:generic T:` format" + ) + res.generics.append(parts[1].rstrip(":")) + + +def parse_extends_directive(line: str, res: DocstringParserResult): + parts = line.split(" ", maxsplit=1) + if len(parts) != 2: + raise ValueError( + f"Invalid `:extends` directive: `{line}` must be in `:extends Base[...]:` format" + ) + res.extends.append(parts[1].rstrip(":")) + + +DocstringParserDirectives = { + "type": parse_type_directive, + "param": parse_param_directive, + "rtype": parse_rtype_directive, + "generic": parse_generic_directive, + "extends": parse_extends_directive, +} + + +class DocstringParser: + """ + DocstringParser provides utilities for parsing type information from docstring. + """ + + @staticmethod + def parse(obj: Any) -> Optional[DocstringParserResult]: + doc = inspect.getdoc(obj) + if not doc: + return None + + res = DocstringParserResult() + for line in doc.splitlines(): + line = line.strip() + for d, p in DocstringParserDirectives.items(): + if line.startswith(f":{d} ") and line.endswith(":"): + p(line, res) + return res + + @staticmethod + def parse_type(obj: Any) -> str: + result = DocstringParser.parse(obj) + if not result or len(result.types) == 0: + return "typing.Any" + return result.types[0] + + @staticmethod + def parse_function(obj: Any) -> Optional[Tuple[List[Tuple[str, str]], str]]: + result = DocstringParser.parse(obj) + if not result: + return None + + return ( + result.params, + "None" if len(result.rtypes) == 0 else result.rtypes[0], + ) + + @staticmethod + def parse_class(obj: Any) -> Tuple[List[str], List[str]]: + result = DocstringParser.parse(obj) + if not result: + return ([], []) + return (result.generics, result.extends) + + @staticmethod + def clean_doc(obj: Any) -> str: + doc = inspect.getdoc(obj) + if not doc: + return "" + + def predicate(l: str) -> bool: + for k in DocstringParserDirectives.keys(): + if l.startswith(f":{k} ") and l.endswith(":"): + return False + return True + + return "\n".join([l for l in doc.splitlines() if predicate(l)]).strip() + + +def indent(code: str, level: int = 4) -> str: + return textwrap.indent(code, level * " ") + + +def is_fn_like(obj: Any) -> bool: + return ( + inspect.isbuiltin(obj) + or inspect.ismethod(obj) + or inspect.isfunction(obj) + or inspect.ismethoddescriptor(obj) + or inspect.iscoroutine(obj) + or inspect.iscoroutinefunction(obj) + ) + + +def join(args: List[str], delim: str = "\n") -> str: + return delim.join(filter(lambda x: x, args)) + + +def make_doc(obj: Any) -> str: + doc = DocstringParser.clean_doc(obj) + doc = textwrap.dedent(doc) + if not doc: + return "" + + return join(['"""', doc, '"""']) + + +def make_field(writer: Writer, name: str, field: Any) -> str: + return f"{name}: {writer.fix_and_include(DocstringParser.parse_type(field))}" + + +def make_function( + writer: Writer, + name: str, + obj: Any, + include_docs: bool = True, + parent: Optional[Any] = None, +) -> str: + is_static_method = False + if parent and isinstance(obj, staticmethod): + # Get real method instance from `parent` if `obj` is a `staticmethod` + is_static_method = True + obj = getattr(parent, name) + + res = DocstringParser.parse_function(obj) + if not res: + # Make it `Any` if we can't parse the docstring + return f"{name}: {writer.include('typing.Any')}" + + params, rtype = res + # We're using signature for getting default values only, currently type hints are not supported + # in signatures. We can leverage signatures more if it supports type hints in future. + sig: Optional[inspect.Signature] = None + try: + sig = inspect.signature(obj) + except: + pass + + def has_default(param: str, ty: str) -> bool: + # PyO3 allows omitting `Option` params while calling a Rust function from Python, + # we should always mark `typing.Optional[T]` values as they have default values to allow same + # flexibiliy as runtime dynamics in type-stubs. + if ty.startswith("typing.Optional["): + return True + + if sig is None: + return False + + sig_param = sig.parameters.get(param) + return sig_param is not None and sig_param.default is not sig_param.empty + + receivers: List[str] = [] + attrs: List[str] = [] + if parent: + if is_static_method: + attrs.append("@staticmethod") + else: + receivers.append("self") + + def make_param(name: str, ty: str) -> str: + fixed_ty = writer.fix_and_include(ty) + param = f"{name}: {fixed_ty}" + if has_default(name, fixed_ty): + param += " = ..." + return param + + params = join(receivers + [make_param(n, t) for n, t in params], delim=", ") + attrs_str = join(attrs) + rtype = writer.fix_and_include(rtype) + body = "..." + if include_docs: + body = join([make_doc(obj), body]) + + return f""" +{attrs_str} +def {name}({params}) -> {rtype}: +{indent(body)} +""".lstrip() + + +def make_class(writer: Writer, name: str, klass: Any) -> str: + bases = list( + filter(lambda n: n != "object", map(lambda b: b.__name__, klass.__bases__)) + ) + class_sig = DocstringParser.parse_class(klass) + if class_sig: + (generics, extends) = class_sig + bases.extend(map(writer.fix_and_include, extends)) + for g in generics: + writer.generic(g) + + members: List[str] = [] + + class_vars: Dict[str, Any] = vars(klass) + for member_name, member in sorted(class_vars.items(), key=lambda k: k[0]): + if member_name.startswith("__"): + continue + + if inspect.isdatadescriptor(member): + members.append( + join( + [ + make_field(writer, member_name, member), + make_doc(member), + ] + ) + ) + elif is_fn_like(member): + members.append( + make_function(writer, member_name, member, parent=klass), + ) + elif isinstance(member, klass): + # Enum variant + members.append( + join( + [ + f"{member_name}: {name}", + make_doc(member), + ] + ) + ) + else: + print(f"Unknown member type: {member}") + + if inspect.getdoc(klass) is not None: + constructor_sig = DocstringParser.parse(klass) + if constructor_sig is not None and ( + # Make sure to only generate `__init__` if the class has a constructor defined + len(constructor_sig.rtypes) > 0 + or len(constructor_sig.params) > 0 + ): + members.append( + make_function( + writer, + "__init__", + klass, + include_docs=False, + parent=klass, + ) + ) + + bases_str = "" if len(bases) == 0 else f"({join(bases, delim=', ')})" + doc = make_doc(klass) + if doc: + doc += "\n" + body = join([doc, join(members, delim="\n\n") or "..."]) + return f"""\ +class {name}{bases_str}: +{indent(body)} +""" + + +def walk_module(writer: Writer, mod: Any): + exported = mod.__all__ + + for (name, member) in inspect.getmembers(mod): + if name not in exported: + continue + + if inspect.ismodule(member): + subpath = writer.path.parent / name / "__init__.pyi" + walk_module(writer.submodule(subpath), member) + elif inspect.isclass(member): + writer.define(make_class(writer, name, member)) + elif is_fn_like(member): + writer.define(make_function(writer, name, member)) + else: + print(f"Unknown type: {member}") + + +if __name__ == "__main__": + import argparse + import importlib + + parser = argparse.ArgumentParser() + parser.add_argument("module") + parser.add_argument("outdir") + args = parser.parse_args() + + path = Path(args.outdir) / f"{args.module}.pyi" + writer = Writer( + path, + args.module, + ) + walk_module( + writer, + importlib.import_module(args.module), + ) + writer.dump() diff --git a/rust-runtime/aws-smithy-http-server-python/examples/stubgen_test.py b/rust-runtime/aws-smithy-http-server-python/examples/stubgen_test.py new file mode 100644 index 0000000000..30c8cb5da6 --- /dev/null +++ b/rust-runtime/aws-smithy-http-server-python/examples/stubgen_test.py @@ -0,0 +1,407 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +import sys +import unittest +from types import ModuleType +from textwrap import dedent +from pathlib import Path +from tempfile import TemporaryDirectory + +from stubgen import Writer, walk_module + + +def create_module(name: str, code: str) -> ModuleType: + mod = ModuleType(name) + exec(dedent(code), mod.__dict__) + if not hasattr(mod, "__all__"): + # Manually populate `__all__` with all the members that doesn't start with `__` + mod.__all__ = [k for k in mod.__dict__.keys() if not k.startswith("__")] # type: ignore + sys.modules[name] = mod + return mod + + +class TestStubgen(unittest.TestCase): + def test_function_without_docstring(self): + self.single_mod( + """ + def foo(): + pass + """, + """ + import typing + + foo: typing.Any + """, + ) + + def test_regular_function(self): + self.single_mod( + """ + def foo(bar): + ''' + :param bar str: + :rtype bool: + ''' + pass + """, + """ + def foo(bar: str) -> bool: + ... + """, + ) + + def test_function_with_default_value(self): + self.single_mod( + """ + def foo(bar, qux=None): + ''' + :param bar int: + :param qux typing.Optional[str]: + :rtype None: + ''' + pass + """, + """ + import typing + + def foo(bar: int, qux: typing.Optional[str] = ...) -> None: + ... + """, + ) + + def test_empty_class(self): + self.single_mod( + """ + class Foo: + pass + """, + """ + class Foo: + ... + """, + ) + + def test_class(self): + self.single_mod( + """ + class Foo: + @property + def bar(self): + ''' + :type typing.List[bool]: + ''' + pass + + def qux(self, a, b, c): + ''' + :param a typing.Dict[typing.List[int]]: + :param b str: + :param c float: + :rtype typing.Union[int, str, bool]: + ''' + pass + """, + """ + import typing + + class Foo: + bar: typing.List[bool] + + def qux(self, a: typing.Dict[typing.List[int]], b: str, c: float) -> typing.Union[int, str, bool]: + ... + """, + ) + + def test_class_with_constructor_signature(self): + self.single_mod( + """ + class Foo: + ''' + :param bar str: + :rtype None: + ''' + """, + """ + class Foo: + def __init__(self, bar: str) -> None: + ... + """, + ) + + def test_class_with_static_method(self): + self.single_mod( + """ + class Foo: + @staticmethod + def bar(name): + ''' + :param name str: + :rtype typing.List[bool]: + ''' + pass + """, + """ + import typing + + class Foo: + @staticmethod + def bar(name: str) -> typing.List[bool]: + ... + """, + ) + + def test_class_with_an_undocumented_descriptor(self): + self.single_mod( + """ + class Foo: + @property + def bar(self): + pass + """, + """ + import typing + + class Foo: + bar: typing.Any + """, + ) + + def test_enum(self): + self.single_mod( + """ + class Foo: + def __init__(self, name): + pass + + Foo.Bar = Foo("Bar") + Foo.Baz = Foo("Baz") + Foo.Qux = Foo("Qux") + """, + """ + class Foo: + Bar: Foo + + Baz: Foo + + Qux: Foo + """, + ) + + def test_generic(self): + self.single_mod( + """ + class Foo: + ''' + :generic T: + :generic U: + :extends typing.Generic[T]: + :extends typing.Generic[U]: + ''' + + @property + def bar(self): + ''' + :type typing.Tuple[T, U]: + ''' + pass + + def baz(self, a): + ''' + :param a U: + :rtype T: + ''' + pass + """, + """ + import typing + + T = typing.TypeVar('T') + U = typing.TypeVar('U') + + class Foo(typing.Generic[T], typing.Generic[U]): + bar: typing.Tuple[T, U] + + def baz(self, a: U) -> T: + ... + """, + ) + + def test_items_with_docstrings(self): + self.single_mod( + """ + class Foo: + ''' + This is the docstring of Foo. + + And it has multiple lines. + + :generic T: + :extends typing.Generic[T]: + :param member T: + ''' + + @property + def bar(self): + ''' + This is the docstring of property `bar`. + + :type typing.Optional[T]: + ''' + pass + + def baz(self, t): + ''' + This is the docstring of method `baz`. + :param t T: + :rtype T: + ''' + pass + """, + ''' + import typing + + T = typing.TypeVar('T') + + class Foo(typing.Generic[T]): + """ + This is the docstring of Foo. + + And it has multiple lines. + """ + + bar: typing.Optional[T] + """ + This is the docstring of property `bar`. + """ + + def baz(self, t: T) -> T: + """ + This is the docstring of method `baz`. + """ + ... + + + def __init__(self, member: T) -> None: + ... + ''', + ) + + def test_adds_default_to_optional_types(self): + # Since PyO3 provides `impl FromPyObject for Option` and maps Python `None` to Rust `None`, + # you don't have to pass `None` explicitly. Type-stubs also shoudln't require `None`s + # to be passed explicitly (meaning they should have a default value). + + self.single_mod( + """ + def foo(bar, qux): + ''' + :param bar typing.Optional[int]: + :param qux typing.List[typing.Optional[int]]: + :rtype int: + ''' + pass + """, + """ + import typing + + def foo(bar: typing.Optional[int] = ..., qux: typing.List[typing.Optional[int]]) -> int: + ... + """, + ) + + def test_multiple_mods(self): + create_module( + "foo.bar", + """ + class Bar: + ''' + :param qux str: + :rtype None: + ''' + pass + """, + ) + + foo = create_module( + "foo", + """ + import sys + + bar = sys.modules["foo.bar"] + + class Foo: + ''' + :param a __root_module_name__.bar.Bar: + :param b typing.Optional[__root_module_name__.bar.Bar]: + :rtype None: + ''' + + @property + def a(self): + ''' + :type __root_module_name__.bar.Bar: + ''' + pass + + @property + def b(self): + ''' + :type typing.Optional[__root_module_name__.bar.Bar]: + ''' + pass + + __all__ = ["bar", "Foo"] + """, + ) + + with TemporaryDirectory() as temp_dir: + foo_path = Path(temp_dir) / "foo.pyi" + bar_path = Path(temp_dir) / "bar" / "__init__.pyi" + + writer = Writer(foo_path, "foo") + walk_module(writer, foo) + writer.dump() + + self.assert_stub( + foo_path, + """ + import foo.bar + import typing + + class Foo: + a: foo.bar.Bar + + b: typing.Optional[foo.bar.Bar] + + def __init__(self, a: foo.bar.Bar, b: typing.Optional[foo.bar.Bar] = ...) -> None: + ... + """, + ) + + self.assert_stub( + bar_path, + """ + class Bar: + def __init__(self, qux: str) -> None: + ... + """, + ) + + def single_mod(self, mod_code: str, expected_stub: str) -> None: + with TemporaryDirectory() as temp_dir: + mod = create_module("test", mod_code) + path = Path(temp_dir) / "test.pyi" + + writer = Writer(path, "test") + walk_module(writer, mod) + writer.dump() + + self.assert_stub(path, expected_stub) + + def assert_stub(self, path: Path, expected: str) -> None: + self.assertEqual(path.read_text().strip(), dedent(expected).strip()) + + +if __name__ == "__main__": + unittest.main() diff --git a/rust-runtime/aws-smithy-http-server-python/src/context.rs b/rust-runtime/aws-smithy-http-server-python/src/context.rs index 04f370a876..dbbd49f733 100644 --- a/rust-runtime/aws-smithy-http-server-python/src/context.rs +++ b/rust-runtime/aws-smithy-http-server-python/src/context.rs @@ -16,7 +16,6 @@ mod testing; /// PyContext is a wrapper for context object provided by the user. /// It injects some values (currently only [super::lambda::PyLambdaContext]) that is type-hinted by the user. /// -/// /// PyContext is initialised during the startup, it inspects the provided context object for fields /// that are type-hinted to inject some values provided by the framework (see [PyContext::new()]). /// diff --git a/rust-runtime/aws-smithy-http-server-python/src/error.rs b/rust-runtime/aws-smithy-http-server-python/src/error.rs index 06e20e9b52..01596be507 100644 --- a/rust-runtime/aws-smithy-http-server-python/src/error.rs +++ b/rust-runtime/aws-smithy-http-server-python/src/error.rs @@ -39,12 +39,19 @@ impl From for PyErr { /// /// It allows to specify a message and HTTP status code and implementing protocol specific capabilities /// to build a [aws_smithy_http_server::response::Response] from it. +/// +/// :param message str: +/// :param status_code typing.Optional\[int\]: +/// :rtype None: #[pyclass(name = "MiddlewareException", extends = BasePyException)] -#[pyo3(text_signature = "(message, status_code)")] +#[pyo3(text_signature = "($self, message, status_code=None)")] #[derive(Debug, Clone)] pub struct PyMiddlewareException { + /// :type str: #[pyo3(get, set)] message: String, + + /// :type int: #[pyo3(get, set)] status_code: u16, } diff --git a/rust-runtime/aws-smithy-http-server-python/src/lambda.rs b/rust-runtime/aws-smithy-http-server-python/src/lambda.rs index 15bb9bf1b0..3823e8d93f 100644 --- a/rust-runtime/aws-smithy-http-server-python/src/lambda.rs +++ b/rust-runtime/aws-smithy-http-server-python/src/lambda.rs @@ -11,70 +11,111 @@ use lambda_http::Context; use pyo3::pyclass; /// AWS Mobile SDK client fields. -#[pyclass] +#[pyclass(name = "ClientApplication")] #[derive(Clone)] -struct PyClientApplication { +pub struct PyClientApplication { /// The mobile app installation id + /// + /// :type str: #[pyo3(get)] installation_id: String, + /// The app title for the mobile app as registered with AWS' mobile services. + /// + /// :type str: #[pyo3(get)] app_title: String, + /// The version name of the application as registered with AWS' mobile services. + /// + /// :type str: #[pyo3(get)] app_version_name: String, + /// The app version code. + /// + /// :type str: #[pyo3(get)] app_version_code: String, + /// The package name for the mobile application invoking the function + /// + /// :type str: #[pyo3(get)] app_package_name: String, } /// Client context sent by the AWS Mobile SDK. -#[pyclass] +#[pyclass(name = "ClientContext")] #[derive(Clone)] -struct PyClientContext { +pub struct PyClientContext { /// Information about the mobile application invoking the function. + /// + /// :type ClientApplication: #[pyo3(get)] client: PyClientApplication, + /// Custom properties attached to the mobile event context. + /// + /// :type typing.Dict[str, str]: #[pyo3(get)] custom: HashMap, + /// Environment settings from the mobile client. + /// + /// :type typing.Dict[str, str]: #[pyo3(get)] environment: HashMap, } /// Cognito identity information sent with the event -#[pyclass] +#[pyclass(name = "CognitoIdentity")] #[derive(Clone)] -struct PyCognitoIdentity { +pub struct PyCognitoIdentity { /// The unique identity id for the Cognito credentials invoking the function. + /// + /// :type str: #[pyo3(get)] identity_id: String, + /// The identity pool id the caller is "registered" with. + /// + /// :type str: #[pyo3(get)] identity_pool_id: String, } /// Configuration derived from environment variables. -#[pyclass] +#[pyclass(name = "Config")] #[derive(Clone)] -struct PyConfig { +pub struct PyConfig { /// The name of the function. + /// + /// :type str: #[pyo3(get)] function_name: String, + /// The amount of memory available to the function in MB. + /// + /// :type int: #[pyo3(get)] memory: i32, + /// The version of the function being executed. + /// + /// :type str: #[pyo3(get)] version: String, + /// The name of the Amazon CloudWatch Logs stream for the function. + /// + /// :type str: #[pyo3(get)] log_stream: String, + /// The name of the Amazon CloudWatch Logs group for the function. + /// + /// :type str: #[pyo3(get)] log_group: String, } @@ -86,29 +127,49 @@ struct PyConfig { #[pyclass(name = "LambdaContext")] pub struct PyLambdaContext { /// The AWS request ID generated by the Lambda service. + /// + /// :type str: #[pyo3(get)] request_id: String, + /// The execution deadline for the current invocation in milliseconds. + /// + /// :type int: #[pyo3(get)] deadline: u64, + /// The ARN of the Lambda function being invoked. + /// + /// :type str: #[pyo3(get)] invoked_function_arn: String, + /// The X-Ray trace ID for the current invocation. + /// + /// :type typing.Optional\[str\]: #[pyo3(get)] xray_trace_id: Option, + /// The client context object sent by the AWS mobile SDK. This field is /// empty unless the function is invoked using an AWS mobile SDK. + /// + /// :type typing.Optional\[ClientContext\]: #[pyo3(get)] client_context: Option, + /// The Cognito identity that invoked the function. This field is empty /// unless the invocation request to the Lambda APIs was made using AWS /// credentials issues by Amazon Cognito Identity Pools. + /// + /// :type typing.Optional\[CognitoIdentity\]: #[pyo3(get)] identity: Option, + /// Lambda function configuration from the local environment variables. /// Includes information such as the function name, memory allocation, /// version, and log streams. + /// + /// :type Config: #[pyo3(get)] env_config: PyConfig, } diff --git a/rust-runtime/aws-smithy-http-server-python/src/logging.rs b/rust-runtime/aws-smithy-http-server-python/src/logging.rs index 2492096269..80d4966ac0 100644 --- a/rust-runtime/aws-smithy-http-server-python/src/logging.rs +++ b/rust-runtime/aws-smithy-http-server-python/src/logging.rs @@ -86,7 +86,12 @@ fn setup_tracing_subscriber( /// - A new builtin function `logging.py_tracing_event` transcodes `logging.LogRecord`s to `tracing::Event`s. This function /// is not exported in `logging.__all__`, as it is not intended to be called directly. /// - A new class `logging.TracingHandler` provides a `logging.Handler` that delivers all records to `python_tracing`. +/// +/// :param level typing.Optional\[int\]: +/// :param logfile typing.Optional\[pathlib.Path\]: +/// :rtype None: #[pyclass(name = "TracingHandler")] +#[pyo3(text_signature = "($self, level=None, logfile=None)")] #[derive(Debug)] pub struct PyTracingHandler { _guard: Option, @@ -104,6 +109,7 @@ impl PyTracingHandler { Ok(Self { _guard }) } + /// :rtype typing.Any: fn handler(&self, py: Python) -> PyResult> { let logging = py.import("logging")?; logging.setattr( diff --git a/rust-runtime/aws-smithy-http-server-python/src/middleware/request.rs b/rust-runtime/aws-smithy-http-server-python/src/middleware/request.rs index 16fc9d6d7f..d1eff86e00 100644 --- a/rust-runtime/aws-smithy-http-server-python/src/middleware/request.rs +++ b/rust-runtime/aws-smithy-http-server-python/src/middleware/request.rs @@ -17,7 +17,6 @@ use super::{PyHeaderMap, PyMiddlewareError}; /// Python-compatible [Request] object. #[pyclass(name = "Request")] -#[pyo3(text_signature = "(request)")] #[derive(Debug)] pub struct PyRequest { parts: Option, @@ -56,6 +55,8 @@ impl PyRequest { #[pymethods] impl PyRequest { /// Return the HTTP method of this request. + /// + /// :type str: #[getter] fn method(&self) -> PyResult { self.parts @@ -65,6 +66,8 @@ impl PyRequest { } /// Return the URI of this request. + /// + /// :type str: #[getter] fn uri(&self) -> PyResult { self.parts @@ -74,6 +77,8 @@ impl PyRequest { } /// Return the HTTP version of this request. + /// + /// :type str: #[getter] fn version(&self) -> PyResult { self.parts @@ -83,6 +88,8 @@ impl PyRequest { } /// Return the HTTP headers of this request. + /// + /// :type typing.MutableMapping[str, str]: #[getter] fn headers(&self) -> PyHeaderMap { self.headers.clone() @@ -90,6 +97,8 @@ impl PyRequest { /// Return the HTTP body of this request. /// Note that this is a costly operation because the whole request body is cloned. + /// + /// :type typing.Awaitable[bytes]: #[getter] fn body<'p>(&self, py: Python<'p>) -> PyResult<&'p PyAny> { let body = self.body.clone(); diff --git a/rust-runtime/aws-smithy-http-server-python/src/middleware/response.rs b/rust-runtime/aws-smithy-http-server-python/src/middleware/response.rs index 5e3619cb11..2e97af5e49 100644 --- a/rust-runtime/aws-smithy-http-server-python/src/middleware/response.rs +++ b/rust-runtime/aws-smithy-http-server-python/src/middleware/response.rs @@ -17,8 +17,13 @@ use tokio::sync::Mutex; use super::{PyHeaderMap, PyMiddlewareError}; /// Python-compatible [Response] object. +/// +/// :param status int: +/// :param headers typing.Optional[typing.Dict[str, str]]: +/// :param body typing.Optional[bytes]: +/// :rtype None: #[pyclass(name = "Response")] -#[pyo3(text_signature = "(status, headers, body)")] +#[pyo3(text_signature = "($self, status, headers=None, body=None)")] pub struct PyResponse { parts: Option, headers: PyHeaderMap, @@ -78,6 +83,8 @@ impl PyResponse { } /// Return the HTTP status of this response. + /// + /// :type int: #[getter] fn status(&self) -> PyResult { self.parts @@ -87,6 +94,8 @@ impl PyResponse { } /// Return the HTTP version of this response. + /// + /// :type str: #[getter] fn version(&self) -> PyResult { self.parts @@ -96,6 +105,8 @@ impl PyResponse { } /// Return the HTTP headers of this response. + /// + /// :type typing.MutableMapping[str, str]: #[getter] fn headers(&self) -> PyHeaderMap { self.headers.clone() @@ -103,6 +114,8 @@ impl PyResponse { /// Return the HTTP body of this response. /// Note that this is a costly operation because the whole response body is cloned. + /// + /// :type typing.Awaitable[bytes]: #[getter] fn body<'p>(&self, py: Python<'p>) -> PyResult<&'p PyAny> { let body = self.body.clone(); diff --git a/rust-runtime/aws-smithy-http-server-python/src/socket.rs b/rust-runtime/aws-smithy-http-server-python/src/socket.rs index 8243aa28c2..13900ff8c8 100644 --- a/rust-runtime/aws-smithy-http-server-python/src/socket.rs +++ b/rust-runtime/aws-smithy-http-server-python/src/socket.rs @@ -20,7 +20,12 @@ use std::net::SocketAddr; /// computing capacity of the host. /// /// [GIL]: https://wiki.python.org/moin/GlobalInterpreterLock -#[pyclass] +/// +/// :param address str: +/// :param port int: +/// :param backlog typing.Optional\[int\]: +/// :rtype None: +#[pyclass(text_signature = "($self, address, port, backlog=None)")] #[derive(Debug)] pub struct PySocket { pub(crate) inner: Socket, @@ -49,7 +54,8 @@ impl PySocket { /// Clone the inner socket allowing it to be shared between multiple /// Python processes. - #[pyo3(text_signature = "($self, socket, worker_number)")] + /// + /// :rtype PySocket: pub fn try_clone(&self) -> PyResult { let copied = self.inner.try_clone()?; Ok(PySocket { inner: copied }) diff --git a/rust-runtime/aws-smithy-http-server-python/src/tls.rs b/rust-runtime/aws-smithy-http-server-python/src/tls.rs index 0c09a224e8..538508fcec 100644 --- a/rust-runtime/aws-smithy-http-server-python/src/tls.rs +++ b/rust-runtime/aws-smithy-http-server-python/src/tls.rs @@ -20,19 +20,30 @@ use tokio_rustls::rustls::{Certificate, Error as RustTlsError, PrivateKey, Serve pub mod listener; /// PyTlsConfig represents TLS configuration created from Python. +/// +/// :param key_path pathlib.Path: +/// :param cert_path pathlib.Path: +/// :param reload_secs int: +/// :rtype None: #[pyclass( name = "TlsConfig", - text_signature = "(*, key_path, cert_path, reload)" + text_signature = "($self, *, key_path, cert_path, reload_secs=86400)" )] #[derive(Clone)] pub struct PyTlsConfig { /// Absolute path of the RSA or PKCS private key. + /// + /// :type pathlib.Path: key_path: PathBuf, /// Absolute path of the x509 certificate. + /// + /// :type pathlib.Path: cert_path: PathBuf, /// Duration to reloading certificates. + /// + /// :type int: reload_secs: u64, } diff --git a/rust-runtime/aws-smithy-http-server-python/src/types.rs b/rust-runtime/aws-smithy-http-server-python/src/types.rs index b42ced51a5..24eb72f9a8 100644 --- a/rust-runtime/aws-smithy-http-server-python/src/types.rs +++ b/rust-runtime/aws-smithy-http-server-python/src/types.rs @@ -27,6 +27,9 @@ use tokio_stream::StreamExt; use crate::PyError; /// Python Wrapper for [aws_smithy_types::Blob]. +/// +/// :param input bytes: +/// :rtype None: #[pyclass] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Blob(aws_smithy_types::Blob); @@ -58,6 +61,8 @@ impl Blob { } /// Python getter for the `Blob` byte array. + /// + /// :type bytes: #[getter(data)] pub fn get_data(&self) -> &[u8] { self.as_ref() @@ -134,6 +139,9 @@ impl DateTime { #[pymethods] impl DateTime { /// Creates a `DateTime` from a number of seconds since the Unix epoch. + /// + /// :param epoch_seconds int: + /// :rtype DateTime: #[staticmethod] pub fn from_secs(epoch_seconds: i64) -> Self { Self(aws_smithy_types::date_time::DateTime::from_secs( @@ -142,6 +150,9 @@ impl DateTime { } /// Creates a `DateTime` from a number of milliseconds since the Unix epoch. + /// + /// :param epoch_millis int: + /// :rtype DateTime: #[staticmethod] pub fn from_millis(epoch_millis: i64) -> Self { Self(aws_smithy_types::date_time::DateTime::from_secs( @@ -150,6 +161,9 @@ impl DateTime { } /// Creates a `DateTime` from a number of nanoseconds since the Unix epoch. + /// + /// :param epoch_nanos int: + /// :rtype DateTime: #[staticmethod] pub fn from_nanos(epoch_nanos: i128) -> PyResult { Ok(Self( @@ -159,6 +173,12 @@ impl DateTime { } /// Read 1 date of `format` from `s`, expecting either `delim` or EOF. + /// + /// TODO(PythonTyping): How do we represent `char` in Python? + /// + /// :param format Format: + /// :param delim str: + /// :rtype typing.Tuple[DateTime, str]: #[staticmethod] pub fn read(s: &str, format: Format, delim: char) -> PyResult<(Self, &str)> { let (self_, next) = aws_smithy_types::date_time::DateTime::read(s, format.into(), delim) @@ -167,6 +187,10 @@ impl DateTime { } /// Creates a `DateTime` from a number of seconds and a fractional second since the Unix epoch. + /// + /// :param epoch_seconds int: + /// :param fraction float: + /// :rtype DateTime: #[staticmethod] pub fn from_fractional_secs(epoch_seconds: i64, fraction: f64) -> Self { Self(aws_smithy_types::date_time::DateTime::from_fractional_secs( @@ -176,6 +200,10 @@ impl DateTime { } /// Creates a `DateTime` from a number of seconds and sub-second nanos since the Unix epoch. + /// + /// :param seconds int: + /// :param subsecond_nanos int: + /// :rtype DateTime: #[staticmethod] pub fn from_secs_and_nanos(seconds: i64, subsecond_nanos: u32) -> Self { Self(aws_smithy_types::date_time::DateTime::from_secs_and_nanos( @@ -185,6 +213,9 @@ impl DateTime { } /// Creates a `DateTime` from an `f64` representing the number of seconds since the Unix epoch. + /// + /// :param epoch_seconds float: + /// :rtype DateTime: #[staticmethod] pub fn from_secs_f64(epoch_seconds: f64) -> Self { Self(aws_smithy_types::date_time::DateTime::from_secs_f64( @@ -193,6 +224,10 @@ impl DateTime { } /// Parses a `DateTime` from a string using the given `format`. + /// + /// :param s str: + /// :param format Format: + /// :rtype DateTime: #[staticmethod] pub fn from_str(s: &str, format: Format) -> PyResult { Ok(Self( @@ -202,31 +237,43 @@ impl DateTime { } /// Returns the number of nanoseconds since the Unix epoch that this `DateTime` represents. + /// + /// :rtype int: pub fn as_nanos(&self) -> i128 { self.0.as_nanos() } /// Returns the `DateTime` value as an `f64` representing the seconds since the Unix epoch. + /// + /// :rtype float: pub fn as_secs_f64(&self) -> f64 { self.0.as_secs_f64() } /// Returns true if sub-second nanos is greater than zero. + /// + /// :rtype bool: pub fn has_subsec_nanos(&self) -> bool { self.0.has_subsec_nanos() } /// Returns the epoch seconds component of the `DateTime`. + /// + /// :rtype int: pub fn secs(&self) -> i64 { self.0.secs() } /// Returns the sub-second nanos component of the `DateTime`. + /// + /// :rtype int: pub fn subsec_nanos(&self) -> u32 { self.0.subsec_nanos() } /// Converts the `DateTime` to the number of milliseconds since the Unix epoch. + /// + /// :rtype int: pub fn to_millis(&self) -> PyResult { Ok(self.0.to_millis().map_err(PyError::DateTimeConversion)?) } @@ -283,6 +330,9 @@ impl<'date> From<&'date DateTime> for &'date aws_smithy_types::DateTime { /// /// The original Rust [ByteStream](aws_smithy_http::byte_stream::ByteStream) is wrapped inside a `Arc` to allow the type to be /// [Clone] (required by PyO3) and to allow internal mutability, required to fetch the next chunk of data. +/// +/// :param input bytes: +/// :rtype None: #[pyclass] #[derive(Debug, Clone)] pub struct ByteStream(Arc>); @@ -347,6 +397,9 @@ impl ByteStream { /// requiring Python to await this method. /// /// **NOTE:** This method will block the Rust event loop when it is running. + /// + /// :param path str: + /// :rtype ByteStream: #[staticmethod] pub fn from_path_blocking(py: Python, path: String) -> PyResult> { let byte_stream = futures::executor::block_on(async { @@ -360,6 +413,9 @@ impl ByteStream { /// Create a new [ByteStream](aws_smithy_http::byte_stream::ByteStream) from a path, forcing /// Python to await this coroutine. + /// + /// :param path str: + /// :rtype typing.Awaitable[ByteStream]: #[staticmethod] pub fn from_path(py: Python, path: String) -> PyResult<&PyAny> { pyo3_asyncio::tokio::future_into_py(py, async move { diff --git a/rust-runtime/aws-smithy-http-server-python/src/util.rs b/rust-runtime/aws-smithy-http-server-python/src/util.rs index b420c26fd4..df0b8eafc5 100644 --- a/rust-runtime/aws-smithy-http-server-python/src/util.rs +++ b/rust-runtime/aws-smithy-http-server-python/src/util.rs @@ -43,7 +43,6 @@ fn is_coroutine(py: Python, func: &PyObject) -> PyResult { } // Checks whether given Python type is `Optional[T]`. -#[allow(unused)] pub fn is_optional_of(py: Python, ty: &PyAny) -> PyResult { // for reference: https://stackoverflow.com/a/56833826 diff --git a/tools/ci-build/Dockerfile b/tools/ci-build/Dockerfile index 1b5074ffea..38dd3a50d1 100644 --- a/tools/ci-build/Dockerfile +++ b/tools/ci-build/Dockerfile @@ -154,6 +154,7 @@ ENV PATH=/opt/cargo/bin:$PATH \ # This is used primarily by the `build.gradle.kts` files in choosing how to execute build tools. If inside the image, # they will assume the tools are on the PATH, but if outside of the image, they will `cargo run` the tools. ENV SMITHY_RS_DOCKER_BUILD_IMAGE=1 +RUN pip3 install --no-cache-dir mypy==0.991 WORKDIR /home/build COPY sanity-test /home/build/sanity-test RUN /home/build/sanity-test diff --git a/tools/ci-scripts/codegen-diff-revisions.py b/tools/ci-scripts/codegen-diff-revisions.py index ae2bc42011..ca81a59178 100755 --- a/tools/ci-scripts/codegen-diff-revisions.py +++ b/tools/ci-scripts/codegen-diff-revisions.py @@ -89,29 +89,26 @@ def main(): def generate_and_commit_generated_code(revision_sha): # Clean the build artifacts before continuing run("rm -rf aws/sdk/build") + run("cd rust-runtime/aws-smithy-http-server-python/examples && make distclean", shell=True) run("./gradlew codegen-core:clean codegen-client:clean codegen-server:clean aws:sdk-codegen:clean") # Generate code run("./gradlew --rerun-tasks :aws:sdk:assemble") run("./gradlew --rerun-tasks :codegen-server-test:assemble") - run("./gradlew --rerun-tasks :codegen-server-test:python:assemble") + run("cd rust-runtime/aws-smithy-http-server-python/examples && make build", shell=True) # Move generated code into codegen-diff/ directory run(f"rm -rf {OUTPUT_PATH}") run(f"mkdir {OUTPUT_PATH}") run(f"mv aws/sdk/build/aws-sdk {OUTPUT_PATH}/") run(f"mv codegen-server-test/build/smithyprojections/codegen-server-test {OUTPUT_PATH}/") - run(f"mv codegen-server-test/python/build/smithyprojections/codegen-server-test-python {OUTPUT_PATH}/") + run(f"mv rust-runtime/aws-smithy-http-server-python/examples/pokemon-service-server-sdk/ {OUTPUT_PATH}/codegen-server-test-python/") # Clean up the server-test folder run(f"rm -rf {OUTPUT_PATH}/codegen-server-test/source") - run(f"rm -rf {OUTPUT_PATH}/codegen-server-test-python/source") run(f"find {OUTPUT_PATH}/codegen-server-test | " f"grep -E 'smithy-build-info.json|sources/manifest|model.json' | " f"xargs rm -f", shell=True) - run(f"find {OUTPUT_PATH}/codegen-server-test-python | " - f"grep -E 'smithy-build-info.json|sources/manifest|model.json' | " - f"xargs rm -f", shell=True) run(f"git add -f {OUTPUT_PATH}") run(f"git -c 'user.name=GitHub Action (generated code preview)' "