Skip to content

Commit 88d827b

Browse files
authored
Merge pull request #68 from influxdata/crepererum/py_typing_optional
feat: support `Optional[T]` in Python
2 parents faca441 + ccd37e2 commit 88d827b

File tree

3 files changed

+120
-6
lines changed

3 files changed

+120
-6
lines changed

guests/python/src/lib.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//! [CPython]: https://www.python.org/
55
//! [`pyo3`]: https://pyo3.rs/
66
use std::any::Any;
7-
use std::ops::ControlFlow;
7+
use std::ops::{ControlFlow, Range};
88
use std::sync::Arc;
99

1010
use arrow::datatypes::DataType;
@@ -25,6 +25,9 @@ mod error;
2525
mod inspect;
2626
mod signature;
2727

28+
/// Supported Python version range.
29+
const PYTHON_VERSION_RANGE: Range<(u8, u8, u8)> = (3, 14, 0)..(3, 15, 0);
30+
2831
/// A test UDF that demonstrate that we can call Python.
2932
#[derive(Debug)]
3033
struct PythonScalarUDF {
@@ -267,10 +270,21 @@ fn root() -> Option<Vec<u8>> {
267270
/// <no Python frame>
268271
/// ```
269272
///
273+
/// This also checks if the running Python version is supported.
274+
///
270275
///
271276
/// [Python Standard Library]: https://docs.python.org/3/library/index.html
272277
fn init_python() {
273278
Python::initialize();
279+
280+
Python::attach(|py| {
281+
let version_info = py.version_info();
282+
let version_tuple = (version_info.major, version_info.minor, version_info.patch);
283+
assert!(
284+
PYTHON_VERSION_RANGE.contains(&version_tuple),
285+
"Unsupported python version: {version_tuple:?}, supported range is {PYTHON_VERSION_RANGE:?}",
286+
);
287+
});
274288
}
275289

276290
/// Generate UDFs from given Python string.

guests/python/src/signature.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ pub(crate) enum PythonType {
5151
/// - <https://docs.python.org/3/library/stdtypes.html#the-null-object>
5252
///
5353
/// Nullable types are represented using a union with another type, e.g. `int | None`.
54+
///
55+
/// There used to be an older representation too: `typing.Optional[int]`. As of Python 3.14, this results in the same
56+
/// representation as `int | None`. See <https://docs.python.org/3.14/whatsnew/3.14.html#typing>. So we support both.
5457
#[derive(Debug)]
5558
pub(crate) struct PythonNullableType {
5659
/// Python type.

host/tests/integration_tests/python/runtime/null_handling.rs

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def add(x: int, y: int) -> int:
2626
}
2727

2828
#[tokio::test(flavor = "multi_thread")]
29-
async fn test_first_arg_optional() {
29+
async fn test_first_arg_optional_union() {
3030
const CODE: &str = "
3131
def add(x: int | None, y: int) -> int:
3232
if x is None:
@@ -42,7 +42,25 @@ def add(x: int | None, y: int) -> int:
4242
}
4343

4444
#[tokio::test(flavor = "multi_thread")]
45-
async fn test_second_arg_optional() {
45+
async fn test_first_arg_optional_old_alias() {
46+
const CODE: &str = "
47+
from typing import Optional
48+
49+
def add(x: Optional[int], y: int) -> int:
50+
if x is None:
51+
x = 9
52+
assert y is not None
53+
return x + y
54+
";
55+
56+
assert_eq!(
57+
xy_null_test(CODE).await.as_ref(),
58+
&Int64Array::from_iter([None, Some(29), None, Some(44)]) as &dyn Array,
59+
);
60+
}
61+
62+
#[tokio::test(flavor = "multi_thread")]
63+
async fn test_second_arg_optional_union() {
4664
const CODE: &str = "
4765
def add(x: int, y: int | None) -> int:
4866
assert x is not None
@@ -58,7 +76,25 @@ def add(x: int, y: int | None) -> int:
5876
}
5977

6078
#[tokio::test(flavor = "multi_thread")]
61-
async fn test_both_args_optional() {
79+
async fn test_second_arg_optional_old_alias() {
80+
const CODE: &str = "
81+
from typing import Optional
82+
83+
def add(x: int, y: Optional[int]) -> int:
84+
assert x is not None
85+
if y is None:
86+
y = 90
87+
return x + y
88+
";
89+
90+
assert_eq!(
91+
xy_null_test(CODE).await.as_ref(),
92+
&Int64Array::from_iter([None, None, Some(93), Some(44)]) as &dyn Array,
93+
);
94+
}
95+
96+
#[tokio::test(flavor = "multi_thread")]
97+
async fn test_both_args_optional_union() {
6298
const CODE: &str = "
6399
def add(x: int | None, y: int | None) -> int:
64100
if x is None:
@@ -75,7 +111,26 @@ def add(x: int | None, y: int | None) -> int:
75111
}
76112

77113
#[tokio::test(flavor = "multi_thread")]
78-
async fn test_optional_passthrough() {
114+
async fn test_both_args_optional_old_alias() {
115+
const CODE: &str = "
116+
from typing import Optional
117+
118+
def add(x: Optional[int], y: Optional[int]) -> int:
119+
if x is None:
120+
x = 9
121+
if y is None:
122+
y = 90
123+
return x + y
124+
";
125+
126+
assert_eq!(
127+
xy_null_test(CODE).await.as_ref(),
128+
&Int64Array::from_iter([Some(99), Some(29), Some(93), Some(44)]) as &dyn Array,
129+
);
130+
}
131+
132+
#[tokio::test(flavor = "multi_thread")]
133+
async fn test_optional_passthrough_union() {
79134
const CODE: &str = "
80135
def add(x: int | None, y: int | None) -> int | None:
81136
if x is None or y is None:
@@ -90,7 +145,24 @@ def add(x: int | None, y: int | None) -> int | None:
90145
}
91146

92147
#[tokio::test(flavor = "multi_thread")]
93-
async fn test_optional_flip() {
148+
async fn test_optional_passthrough_old_alias() {
149+
const CODE: &str = "
150+
from typing import Optional
151+
152+
def add(x: Optional[int], y: Optional[int]) -> Optional[int]:
153+
if x is None or y is None:
154+
return None
155+
return x + y
156+
";
157+
158+
assert_eq!(
159+
xy_null_test(CODE).await.as_ref(),
160+
&Int64Array::from_iter([None, None, None, Some(44)]) as &dyn Array,
161+
);
162+
}
163+
164+
#[tokio::test(flavor = "multi_thread")]
165+
async fn test_optional_flip_union() {
94166
const CODE: &str = "
95167
def add(x: int | None, y: int | None) -> int | None:
96168
if x is None:
@@ -112,6 +184,31 @@ def add(x: int | None, y: int | None) -> int | None:
112184
);
113185
}
114186

187+
#[tokio::test(flavor = "multi_thread")]
188+
async fn test_optional_flip_old_alias() {
189+
const CODE: &str = "
190+
from typing import Optional
191+
192+
def add(x: Optional[int], y: Optional[int]) -> Optional[int]:
193+
if x is None:
194+
x = 9
195+
else:
196+
return None
197+
198+
if y is None:
199+
y = 90
200+
else:
201+
return None
202+
203+
return x + y
204+
";
205+
206+
assert_eq!(
207+
xy_null_test(CODE).await.as_ref(),
208+
&Int64Array::from_iter([Some(99), None, None, None]) as &dyn Array,
209+
);
210+
}
211+
115212
async fn xy_null_test(code: &str) -> ArrayRef {
116213
let udf = python_scalar_udf(code).await.unwrap();
117214
udf.invoke_with_args(ScalarFunctionArgs {

0 commit comments

Comments
 (0)