Skip to content

Commit 5c7b50d

Browse files
drganjooFahad Zubair
andauthored
Fix Rust 1.82 compilation errors by upgrading PyO3 from 0.18 to 0.20 (#4146)
## PyO3 Upgrade to Fix Compatibility with Rust 1.82 This PR addresses a compatibility issue between `PyO3 0.18` and `Rust 1.82`, which causes compilation errors due to unexpected `cfg` condition names. When compiled with Rust 1.82, the current [PyO3 0.18](https://github.com/PyO3/pyo3/blob/v0.18.3/src/types/mod.rs#L193) dependency produces errors like: ``` error: unexpected `cfg` condition name: `addr_of` ``` This error occurs because of changes in how Rust handles conditional configuration checks in newer compiler versions. ## Changes in this PR: 1. Upgrades PyO3 from version 0.18 to 0.20 (the latest version compatible with pyo3-asyncio) 2. Updates generated functions to follow the required API pattern where required fields precede optional ones 3. API changes in pyo3, whereby in the new version dictionary.get_key returns a Result<Option<PyAny>> instead of Result<PyAny> in the older version. This upgrade ensures compatibility with Rust 1.82 while maintaining all existing functionality. ## Testing 1. Current protocol tests pass 2. An additional test has been included for required versus optional parameters. --------- Co-authored-by: Fahad Zubair <fahadzub@amazon.com>
1 parent 7d64b2f commit 5c7b50d

File tree

16 files changed

+305
-26
lines changed

16 files changed

+305
-26
lines changed

codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/PythonServerCargoDependency.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import software.amazon.smithy.rust.codegen.core.smithy.RuntimeConfig
1515
* For a dependency that is used in the client, or in both the client and the server, use [CargoDependency] directly.
1616
*/
1717
object PythonServerCargoDependency {
18-
val PyO3: CargoDependency = CargoDependency("pyo3", CratesIo("0.18"))
18+
val PyO3: CargoDependency = CargoDependency("pyo3", CratesIo("0.20"))
1919
val PyO3Asyncio: CargoDependency =
20-
CargoDependency("pyo3-asyncio", CratesIo("0.18"), features = setOf("attributes", "tokio-runtime"))
20+
CargoDependency("pyo3-asyncio", CratesIo("0.20"), features = setOf("attributes", "tokio-runtime"))
2121
val Tokio: CargoDependency = CargoDependency("tokio", CratesIo("1.20.1"), features = setOf("full"))
2222
val TokioStream: CargoDependency = CargoDependency("tokio-stream", CratesIo("0.1.12"))
2323
val Tracing: CargoDependency = CargoDependency("tracing", CratesIo("0.1"))

codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerModuleGenerator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class PythonServerModuleGenerator(
4141
rustBlockTemplate(
4242
"""
4343
##[#{pyo3}::pymodule]
44-
##[#{pyo3}(name = "$libName")]
44+
##[pyo3(name = "$libName")]
4545
pub fn python_library(py: #{pyo3}::Python<'_>, m: &#{pyo3}::types::PyModule) -> #{pyo3}::PyResult<()>
4646
""",
4747
*codegenScope,

codegen-server/python/src/main/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerStructureGenerator.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import software.amazon.smithy.rust.codegen.core.rustlang.rustInlineTemplate
1919
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
2020
import software.amazon.smithy.rust.codegen.core.rustlang.writable
2121
import software.amazon.smithy.rust.codegen.core.smithy.generators.StructureGenerator
22+
import software.amazon.smithy.rust.codegen.core.smithy.isOptional
2223
import software.amazon.smithy.rust.codegen.core.smithy.rustType
2324
import software.amazon.smithy.rust.codegen.core.util.hasTrait
2425
import software.amazon.smithy.rust.codegen.core.util.isEventStream
@@ -125,24 +126,32 @@ class PythonServerStructureGenerator(
125126
)
126127
}
127128

129+
// Python function parameters require that all required parameters appear before optional ones.
130+
// This function sorts the member fields to ensure required fields precede optional fields.
131+
private fun sortedMembers() =
132+
members.sortedBy { member ->
133+
val memberSymbol = symbolProvider.toSymbol(member)
134+
memberSymbol.isOptional()
135+
}
136+
128137
private fun renderStructSignatureMembers(): Writable =
129138
writable {
130-
forEachMember(members) { _, memberName, memberSymbol ->
139+
forEachMember(sortedMembers()) { _, memberName, memberSymbol ->
131140
val memberType = memberSymbol.rustType()
132141
rust("$memberName: ${memberType.render()},")
133142
}
134143
}
135144

136145
private fun renderStructBodyMembers(): Writable =
137146
writable {
138-
forEachMember(members) { _, memberName, _ ->
147+
forEachMember(sortedMembers()) { _, memberName, _ ->
139148
rust("$memberName,")
140149
}
141150
}
142151

143152
private fun renderConstructorSignature(): Writable =
144153
writable {
145-
forEachMember(members) { member, memberName, memberSymbol ->
154+
forEachMember(sortedMembers()) { member, memberName, memberSymbol ->
146155
val memberType = memberPythonType(member, memberSymbol)
147156
rust("/// :param $memberName ${memberType.renderAsDocstring()}:")
148157
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.rust.codegen.server.python.smithy.generators
7+
8+
import org.junit.jupiter.api.Test
9+
import software.amazon.smithy.rust.codegen.core.rustlang.RustWriter
10+
import software.amazon.smithy.rust.codegen.core.rustlang.rust
11+
import software.amazon.smithy.rust.codegen.core.rustlang.rustTemplate
12+
import software.amazon.smithy.rust.codegen.core.rustlang.writable
13+
import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
14+
import software.amazon.smithy.rust.codegen.core.testutil.unitTest
15+
import software.amazon.smithy.rust.codegen.server.python.smithy.PythonServerCargoDependency
16+
import software.amazon.smithy.rust.codegen.server.python.smithy.testutil.cargoTest
17+
import software.amazon.smithy.rust.codegen.server.python.smithy.testutil.executePythonServerCodegenVisitor
18+
import software.amazon.smithy.rust.codegen.server.python.smithy.testutil.generatePythonServerPluginContext
19+
import kotlin.io.path.appendText
20+
21+
internal class PythonServerRequiredPrecedeOptionalTest {
22+
@Test
23+
fun `mandatory fields are reordered to be before optional`() {
24+
val model =
25+
"""
26+
namespace test
27+
28+
use aws.protocols#restJson1
29+
use smithy.framework#ValidationException
30+
31+
@restJson1
32+
service SampleService {
33+
operations: [
34+
OpWithIncorrectOrder, OpWithCorrectOrder, OpWithDefaults
35+
],
36+
}
37+
38+
@http(method: "POST", uri: "/opIncorrect")
39+
operation OpWithIncorrectOrder {
40+
input:= {
41+
a: String
42+
@required
43+
b: String
44+
c: String
45+
@required
46+
d: String
47+
}
48+
output:= {
49+
a: String
50+
@required
51+
b: String
52+
c: String
53+
@required
54+
d: String
55+
}
56+
errors: [ValidationException]
57+
}
58+
59+
@http(method: "POST", uri: "/opCorrect")
60+
operation OpWithCorrectOrder {
61+
input:= {
62+
@required
63+
b: String
64+
@required
65+
d: String
66+
a: String
67+
c: String
68+
}
69+
output:= {
70+
@required
71+
b: String
72+
@required
73+
d: String
74+
a: String
75+
c: String
76+
}
77+
errors: [ValidationException]
78+
}
79+
80+
@http(method: "POST", uri: "/opWithDefaults")
81+
operation OpWithDefaults {
82+
input:= {
83+
a: String,
84+
b: String = "hi"
85+
}
86+
output:= {
87+
a: String,
88+
b: String = "hi"
89+
}
90+
}
91+
""".asSmithyModel(smithyVersion = "2")
92+
93+
val (pluginCtx, testDir) = generatePythonServerPluginContext(model)
94+
executePythonServerCodegenVisitor(pluginCtx)
95+
96+
val writer = RustWriter.forModule("service")
97+
writer.unitTest("test_required_fields") {
98+
fun createInstanceWithRequiredFieldsOnly(
99+
module: String,
100+
typeName: String,
101+
) = writable {
102+
rustTemplate(
103+
"""
104+
py.run(
105+
"data = $typeName(\"b\", \"d\")",
106+
Some(globals),
107+
Some(locals),
108+
).unwrap();
109+
110+
// Python should have been able to construct input.
111+
let data = locals
112+
.get_item("data")
113+
.expect("Python exception occurred during dictionary lookup")
114+
.unwrap()
115+
.extract::<$module::$typeName>()
116+
.unwrap();
117+
assert_eq!(data.b, "b");
118+
assert_eq!(data.d, "d");
119+
""",
120+
)
121+
}
122+
123+
fun createInstance(
124+
module: String,
125+
typeName: String,
126+
) = writable {
127+
rustTemplate(
128+
"""
129+
py.run(
130+
"data = $typeName(\"b\", \"d\", a = \"a\", c = \"c\")",
131+
Some(globals),
132+
Some(locals),
133+
).unwrap();
134+
135+
// Python should have been able to construct input.
136+
let data = locals
137+
.get_item("data")
138+
.expect("Python exception occurred during dictionary lookup")
139+
.unwrap()
140+
.extract::<$module::$typeName>()
141+
.unwrap();
142+
assert_eq!(data.b, "b");
143+
assert_eq!(data.d, "d");
144+
assert_eq!(data.a, Some("a".to_string()));
145+
assert_eq!(data.c, Some("c".to_string()));
146+
""",
147+
)
148+
}
149+
150+
fun createDefaultInstance(
151+
module: String,
152+
typeName: String,
153+
) = writable {
154+
rustTemplate(
155+
"""
156+
// Default values are not exported from Rust. However, they
157+
// are marked as non-optional.
158+
py.run(
159+
"data = $typeName(\"b\", \"a\")",
160+
Some(globals),
161+
Some(locals),
162+
).unwrap();
163+
164+
// Python should have been able to construct input.
165+
let data = locals
166+
.get_item("data")
167+
.expect("Python exception occurred during dictionary lookup")
168+
.unwrap()
169+
.extract::<$module::$typeName>()
170+
.unwrap();
171+
assert_eq!(data.a, Some("a".to_string()));
172+
assert_eq!(data.b, "b");
173+
""",
174+
)
175+
}
176+
177+
rustTemplate(
178+
"""
179+
use crate::{input, output};
180+
use #{pyo3}::{types::IntoPyDict, Python};
181+
182+
pyo3::prepare_freethreaded_python();
183+
Python::with_gil(|py| {
184+
let globals = [
185+
("OpWithIncorrectOrderInput", py.get_type::<input::OpWithIncorrectOrderInput>()),
186+
("OpWithCorrectOrderInput", py.get_type::<input::OpWithCorrectOrderInput>()),
187+
("OpWithDefaultsInput", py.get_type::<input::OpWithDefaultsInput>()),
188+
("OpWithIncorrectOrderOutput", py.get_type::<output::OpWithIncorrectOrderOutput>()),
189+
("OpWithCorrectOrderOutput", py.get_type::<output::OpWithCorrectOrderOutput>()),
190+
("OpWithDefaultsOutput", py.get_type::<output::OpWithDefaultsOutput>())
191+
]
192+
.into_py_dict(py);
193+
194+
let locals = [("OpWithIncorrectOrderInput", py.get_type::<input::OpWithIncorrectOrderInput>())].into_py_dict(py);
195+
196+
#{IncorrectOrderInputRequiredOnly}
197+
#{CorrectOrderInputRequiredOnly}
198+
#{IncorrectOrderOutputRequiredOnly}
199+
#{CorrectOrderOutputRequiredOnly}
200+
#{IncorrectOrderInput}
201+
#{CorrectOrderInput}
202+
#{IncorrectOrderOutput}
203+
#{CorrectOrderOutput}
204+
#{DefaultsInput}
205+
#{DefaultsOutput}
206+
});
207+
""",
208+
"pyo3" to PythonServerCargoDependency.PyO3.toDevDependency().toType(),
209+
"IncorrectOrderInputRequiredOnly" to createInstanceWithRequiredFieldsOnly("input", "OpWithIncorrectOrderInput"),
210+
"CorrectOrderInputRequiredOnly" to createInstanceWithRequiredFieldsOnly("input", "OpWithCorrectOrderInput"),
211+
"IncorrectOrderOutputRequiredOnly" to createInstanceWithRequiredFieldsOnly("output", "OpWithIncorrectOrderOutput"),
212+
"CorrectOrderOutputRequiredOnly" to createInstanceWithRequiredFieldsOnly("output", "OpWithCorrectOrderOutput"),
213+
"IncorrectOrderInput" to createInstance("input", "OpWithIncorrectOrderInput"),
214+
"CorrectOrderInput" to createInstance("input", "OpWithCorrectOrderInput"),
215+
"IncorrectOrderOutput" to createInstance("output", "OpWithIncorrectOrderOutput"),
216+
"CorrectOrderOutput" to createInstance("output", "OpWithCorrectOrderOutput"),
217+
"DefaultsInput" to createDefaultInstance("input", "OpWithDefaultsInput"),
218+
"DefaultsOutput" to createDefaultInstance("output", "OpWithDefaultsOutput"),
219+
)
220+
}
221+
222+
testDir.resolve("src/service.rs").appendText(writer.toString())
223+
224+
cargoTest(testDir)
225+
}
226+
}

codegen-server/python/src/test/kotlin/software/amazon/smithy/rust/codegen/server/python/smithy/generators/PythonServerTypesTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ internal class PythonServerTypesTest {
119119
120120
locals
121121
.get_item("output")
122+
.expect("Python exception occurred during dictionary lookup")
122123
.unwrap()
123124
.extract::<output::EchoOutput>()
124125
.unwrap()
@@ -212,6 +213,7 @@ internal class PythonServerTypesTest {
212213
213214
locals
214215
.get_item("output")
216+
.expect("Python exception occurred during dictionary lookup")
215217
.unwrap()
216218
.extract::<output::EchoOutput>()
217219
.unwrap()

rust-runtime/aws-smithy-http-server-python/Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "aws-smithy-http-server-python"
3-
version = "0.65.1"
3+
version = "0.66.0"
44
authors = ["Smithy Rust Server <smithy-rs-server@amazon.com>"]
55
edition = "2021"
66
license = "Apache-2.0"
@@ -29,8 +29,8 @@ lambda_http = { version = "0.8.3" }
2929
num_cpus = "1.13.1"
3030
parking_lot = "0.12.1"
3131
pin-project-lite = "0.2.14"
32-
pyo3 = "0.18.2"
33-
pyo3-asyncio = { version = "0.18.0", features = ["tokio-runtime"] }
32+
pyo3 = "0.20"
33+
pyo3-asyncio = { version = "0.20.0", features = ["tokio-runtime"] }
3434
signal-hook = { version = "0.3.14", features = ["extended-siginfo"] }
3535
socket2 = { version = "0.5.5", features = ["all"] }
3636
thiserror = "2"
@@ -46,7 +46,7 @@ pretty_assertions = "1"
4646
futures-util = { version = "0.3.29", default-features = false }
4747
tower-test = "0.4"
4848
tokio-test = "0.4"
49-
pyo3-asyncio = { version = "0.18.0", features = ["testing", "attributes", "tokio-runtime", "unstable-streams"] }
49+
pyo3-asyncio = { version = "0.20.0", features = ["testing", "attributes", "tokio-runtime", "unstable-streams"] }
5050
rcgen = "0.10.0"
5151
hyper-rustls = { version = "0.24", features = ["http2"] }
5252

rust-runtime/aws-smithy-http-server-python/src/context/layer.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,16 @@ counter = ctx.counter
107107
.unwrap();
108108

109109
(
110-
locals.get_item("req_id").unwrap().to_string(),
111-
locals.get_item("counter").unwrap().to_string(),
110+
locals
111+
.get_item("req_id")
112+
.expect("Python exception occurred during dictionary lookup")
113+
.unwrap()
114+
.to_string(),
115+
locals
116+
.get_item("counter")
117+
.expect("Python exception occurred during dictionary lookup")
118+
.unwrap()
119+
.to_string(),
112120
)
113121
});
114122
Ok::<_, Infallible>(Response::new((req_id, counter)))

rust-runtime/aws-smithy-http-server-python/src/context/testing.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ pub fn get_context(code: &str) -> PyContext {
2525
py.run(code, Some(globals), Some(locals))?;
2626
let context = locals
2727
.get_item("ctx")
28+
.expect("Python exception occurred during dictionary lookup")
2829
.expect("you should assing your context class to `ctx` variable")
2930
.into_py(py);
3031
Ok::<_, PyErr>(context)

rust-runtime/aws-smithy-http-server-python/src/error.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ impl From<PyError> for PyErr {
4444
/// :param status_code typing.Optional\[int\]:
4545
/// :rtype None:
4646
#[pyclass(name = "MiddlewareException", extends = BasePyException)]
47-
#[pyo3(text_signature = "($self, message, status_code=None)")]
4847
#[derive(Debug, Clone)]
4948
pub struct PyMiddlewareException {
5049
/// :type str:
@@ -59,6 +58,7 @@ pub struct PyMiddlewareException {
5958
#[pymethods]
6059
impl PyMiddlewareException {
6160
/// Create a new [PyMiddlewareException].
61+
#[pyo3(text_signature = "($self, message, status_code=None)")]
6262
#[new]
6363
fn newpy(message: String, status_code: Option<u16>) -> Self {
6464
Self {

rust-runtime/aws-smithy-http-server-python/src/logging.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,14 +127,14 @@ fn setup_tracing_subscriber(
127127
/// :param format typing.Optional\[typing.Literal\['compact', 'pretty', 'json'\]\]:
128128
/// :rtype None:
129129
#[pyclass(name = "TracingHandler")]
130-
#[pyo3(text_signature = "($self, level=None, logfile=None, format=None)")]
131130
#[derive(Debug)]
132131
pub struct PyTracingHandler {
133132
_guard: Option<WorkerGuard>,
134133
}
135134

136135
#[pymethods]
137136
impl PyTracingHandler {
137+
#[pyo3(text_signature = "($self, level=None, logfile=None, format=None)")]
138138
#[new]
139139
fn newpy(
140140
py: Python,

0 commit comments

Comments
 (0)