Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python: Support document type #2188

Merged
merged 7 commits into from
Jan 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig
* For a dependency that is used in the client, or in both the client and the server, use [CargoDependency] directly.
*/
object PythonServerCargoDependency {
val PyO3: CargoDependency = CargoDependency("pyo3", CratesIo("0.17"), features = setOf("extension-module"))
val PyO3: CargoDependency = CargoDependency("pyo3", CratesIo("0.17"))
val PyO3Asyncio: CargoDependency = CargoDependency("pyo3-asyncio", CratesIo("0.17"), features = setOf("attributes", "tokio-runtime"))
val Tokio: CargoDependency = CargoDependency("tokio", CratesIo("1.20.1"), features = setOf("full"))
val Tracing: CargoDependency = CargoDependency("tracing", CratesIo("0.1"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ object PythonServerRuntimeType {

fun dateTime(runtimeConfig: RuntimeConfig) =
PythonServerCargoDependency.smithyHttpServerPython(runtimeConfig).toType().resolve("types::DateTime")

fun document(runtimeConfig: RuntimeConfig) =
PythonServerCargoDependency.smithyHttpServerPython(runtimeConfig).toType().resolve("types::Document")
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package software.amazon.smithy.rust.codegen.server.python.smithy
import software.amazon.smithy.codegen.core.Symbol
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.BlobShape
import software.amazon.smithy.model.shapes.DocumentShape
import software.amazon.smithy.model.shapes.ListShape
import software.amazon.smithy.model.shapes.MapShape
import software.amazon.smithy.model.shapes.MemberShape
Expand Down Expand Up @@ -80,6 +81,10 @@ class PythonServerSymbolVisitor(
override fun blobShape(shape: BlobShape?): Symbol {
return PythonServerRuntimeType.blob(runtimeConfig).toSymbol()
}

override fun documentShape(shape: DocumentShape?): Symbol {
return PythonServerRuntimeType.document(runtimeConfig).toSymbol()
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package software.amazon.smithy.rust.codegen.server.python.smithy.customizations

import com.moandjiezana.toml.TomlWriter
import software.amazon.smithy.rust.codegen.core.rustlang.Feature
import software.amazon.smithy.rust.codegen.core.rustlang.Writable
import software.amazon.smithy.rust.codegen.core.rustlang.docs
import software.amazon.smithy.rust.codegen.core.rustlang.rust
Expand Down Expand Up @@ -57,6 +58,7 @@ class PubUsePythonTypes(private val codegenContext: ServerCodegenContext) : LibR
rustBlock("pub mod python_types") {
rust("pub use #T;", PythonServerRuntimeType.blob(codegenContext.runtimeConfig).toSymbol())
rust("pub use #T;", PythonServerRuntimeType.dateTime(codegenContext.runtimeConfig).toSymbol())
rust("pub use #T;", PythonServerRuntimeType.document(codegenContext.runtimeConfig).toSymbol())
}
}
else -> emptySection
Expand Down Expand Up @@ -114,6 +116,24 @@ class PyProjectTomlDecorator : ServerCodegenDecorator {
}
}

/**
* Adds `pyo3/extension-module` feature to default features.
*
* To be able to run `cargo test` with PyO3 we need two things:
* - Make `pyo3/extension-module` optional and default
* - Run tests with `cargo test --no-default-features`
* See: https://pyo3.rs/main/faq#i-cant-run-cargo-test-or-i-cant-build-in-a-cargo-workspace-im-having-linker-issues-like-symbol-not-found-or-undefined-reference-to-_pyexc_systemerror
*/
class PyO3ExtensionModuleDecorator : ServerCodegenDecorator {
override val name: String = "PyO3ExtensionModuleDecorator"
override val order: Byte = 0

override fun extras(codegenContext: ServerCodegenContext, rustCrate: RustCrate) {
// Add `pyo3/extension-module` to default features.
rustCrate.mergeFeature(Feature("extension-module", true, listOf("pyo3/extension-module")))
}
}

val DECORATORS = listOf(
/**
* Add the [InternalServerError] error to all operations.
Expand All @@ -128,4 +148,6 @@ val DECORATORS = listOf(
PythonExportModuleDecorator(),
// Generate `pyproject.toml` for the crate.
PyProjectTomlDecorator(),
// Add PyO3 extension module feature.
PyO3ExtensionModuleDecorator(),
)
Original file line number Diff line number Diff line change
@@ -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.testutil

import software.amazon.smithy.build.PluginContext
import software.amazon.smithy.model.Model
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig
import software.amazon.smithy.rust.codegen.core.smithy.RuntimeCrateLocation
import software.amazon.smithy.rust.codegen.core.testutil.generatePluginContext
import software.amazon.smithy.rust.codegen.core.util.runCommand
import software.amazon.smithy.rust.codegen.server.python.smithy.PythonServerCodegenVisitor
import software.amazon.smithy.rust.codegen.server.python.smithy.customizations.DECORATORS
import software.amazon.smithy.rust.codegen.server.smithy.customizations.ServerRequiredCustomizations
import software.amazon.smithy.rust.codegen.server.smithy.customize.CombinedServerCodegenDecorator
import java.io.File
import java.nio.file.Path

val TestRuntimeConfig =
RuntimeConfig(runtimeCrateLocation = RuntimeCrateLocation.Path(File("../../rust-runtime").absolutePath))

fun generatePythonServerPluginContext(model: Model) =
generatePluginContext(model, runtimeConfig = TestRuntimeConfig)

fun executePythonServerCodegenVisitor(pluginCtx: PluginContext) {
val codegenDecorator: CombinedServerCodegenDecorator =
CombinedServerCodegenDecorator.fromClasspath(
pluginCtx,
CombinedServerCodegenDecorator(DECORATORS + ServerRequiredCustomizations()),
)
PythonServerCodegenVisitor(pluginCtx, codegenDecorator).execute()
}

fun cargoTest(workdir: Path) =
// `--no-default-features` is required to disable `pyo3/extension-module` which causes linking errors
// see `PyO3ExtensionModuleDecorator`'s comments fore more detail.
"cargo test --no-default-features".runCommand(
workdir,
mapOf(
// Those are required to run tests on macOS, see: https://pyo3.rs/main/building_and_distribution#macos
"CARGO_TARGET_X86_64_APPLE_DARWIN_RUSTFLAGS" to "-C link-arg=-undefined -C link-arg=dynamic_lookup",
"CARGO_TARGET_AARCH64_APPLE_DARWIN_RUSTFLAGS" to "-C link-arg=-undefined -C link-arg=dynamic_lookup",
),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* 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 org.junit.jupiter.api.Test
import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
import software.amazon.smithy.rust.codegen.core.rustlang.rust
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
import software.amazon.smithy.rust.codegen.core.testutil.tokioTest
import software.amazon.smithy.rust.codegen.core.util.dq
import software.amazon.smithy.rust.codegen.server.python.smithy.testutil.cargoTest
import software.amazon.smithy.rust.codegen.server.python.smithy.testutil.executePythonServerCodegenVisitor
import software.amazon.smithy.rust.codegen.server.python.smithy.testutil.generatePythonServerPluginContext
import kotlin.io.path.appendText

internal class PythonServerTypesTest {
@Test
fun `document type`() {
val model = """
namespace test

use aws.protocols#restJson1

@restJson1
service Service {
operations: [
Echo,
],
}

@http(method: "POST", uri: "/echo")
operation Echo {
input: EchoInput,
output: EchoOutput,
}

structure EchoInput {
value: Document,
}

structure EchoOutput {
value: Document,
}
""".asSmithyModel()

val (pluginCtx, testDir) = generatePythonServerPluginContext(model)
executePythonServerCodegenVisitor(pluginCtx)

val testCases = listOf(
Pair(
""" { "value": 42 } """,
"""
assert input.value == 42
output = EchoOutput(value=input.value)
""",
),
Pair(
""" { "value": "foobar" } """,
"""
assert input.value == "foobar"
output = EchoOutput(value=input.value)
""",
),
Pair(
"""
{
"value": [
true,
false,
42,
42.0,
-42,
{
"nested": "value"
},
{
"nested": [1, 2, 3]
}
]
}
""",
"""
assert input.value == [True, False, 42, 42.0, -42, {"nested": "value"}, {"nested": [1, 2, 3]}]
output = EchoOutput(value=input.value)
""",
),
)

val writer = RustWriter.forModule("service")
writer.tokioTest("document_type") {
rust(
"""
use tower::Service as _;
use pyo3::{types::IntoPyDict, IntoPy, Python};
use hyper::{Body, Request, body};
use crate::{input, output};

pyo3::prepare_freethreaded_python();
""".trimIndent(),
)

testCases.forEach {
val payload = it.first.replace(" ", "").replace("\n", "")
val pythonHandler = it.second.trimIndent()
rust(
"""
let mut service = Service::builder_without_plugins()
.echo(|input: input::EchoInput| async {
Ok(Python::with_gil(|py| {
let globals = [("EchoOutput", py.get_type::<output::EchoOutput>())].into_py_dict(py);
let locals = [("input", input.into_py(py))].into_py_dict(py);

py.run(${pythonHandler.dq()}, Some(globals), Some(locals)).unwrap();

locals
.get_item("output")
.unwrap()
.extract::<output::EchoOutput>()
.unwrap()
}))
})
.build()
.unwrap();

let req = Request::builder()
.method("POST")
.uri("/echo")
.body(Body::from(${payload.dq()}))
.unwrap();

let res = service.call(req).await.unwrap();
assert!(res.status().is_success());
let body = body::to_bytes(res.into_body()).await.unwrap();
assert_eq!(body, ${payload.dq()});
""".trimIndent(),
)
}
}

testDir.resolve("src/service.rs").appendText(writer.toString())

cargoTest(testDir)
}
}
Loading