pyo3_stub_gen/runtime/mod.rs
1//! Runtime support for type aliases.
2//!
3//! This module provides traits and utilities for registering type aliases
4//! in Python modules at runtime, enabling type aliases defined with
5//! [`type_alias!`](crate::type_alias) to be importable from Python.
6//!
7//! # Example
8//!
9//! ```rust,ignore
10//! use pyo3::prelude::*;
11//! use pyo3_stub_gen::type_alias;
12//! use pyo3_stub_gen::runtime::PyModuleTypeAliasExt;
13//!
14//! // Define a runtime type alias
15//! type_alias!("my_module", NumberOrString = i32 | String);
16//!
17//! #[pymodule]
18//! fn my_module(m: &Bound<PyModule>) -> PyResult<()> {
19//! // Register the type alias at runtime
20//! m.add_type_alias::<NumberOrString>()?;
21//! Ok(())
22//! }
23//! ```
24
25use ::pyo3::prelude::*;
26use ::pyo3::types::PyModule;
27
28/// Trait for Rust types that can be converted to Python type objects at runtime.
29///
30/// This trait is used by [`type_alias!`](crate::type_alias) to create runtime type aliases
31/// that can be imported from Python. Unlike [`PyStubType`](crate::PyStubType) which is used
32/// for stub file generation, this trait is only needed when you want to register type aliases
33/// at runtime.
34///
35/// # Implementing for Custom Types
36///
37/// For `#[pyclass]` types, use `py.get_type::<Self>()`:
38///
39/// ```rust,ignore
40/// use pyo3::prelude::*;
41/// use pyo3_stub_gen::runtime::PyRuntimeType;
42///
43/// #[pyclass]
44/// struct MyClass;
45///
46/// impl PyRuntimeType for MyClass {
47/// fn runtime_type_object(py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
48/// Ok(py.get_type::<Self>().into_any())
49/// }
50/// }
51/// ```
52///
53/// # Note
54///
55/// This trait is automatically implemented for common Rust types (primitives, collections, etc.)
56/// and for types that use `#[gen_stub_pyclass]` derive macro.
57pub trait PyRuntimeType {
58 /// Returns the Python type object for this Rust type.
59 ///
60 /// # Examples
61 ///
62 /// - `i32` → `<class 'int'>`
63 /// - `String` → `<class 'str'>`
64 /// - `Option<T>` → `T | None`
65 /// - `Vec<T>` → `list`
66 fn runtime_type_object(py: Python<'_>) -> PyResult<Bound<'_, PyAny>>;
67}
68
69/// Implements `PyRuntimeType` for a type using `py.get_type::<$ty>()`.
70///
71/// This is a convenience macro for the common case where the runtime type object
72/// can be obtained directly via `pyo3::type_object::PyTypeInfo`.
73///
74/// # Example
75///
76/// ```rust,ignore
77/// use pyo3::prelude::*;
78/// use pyo3_stub_gen::impl_py_runtime_type;
79///
80/// #[pyclass]
81/// struct MyClass;
82///
83/// impl_py_runtime_type!(MyClass);
84/// ```
85#[macro_export]
86macro_rules! impl_py_runtime_type {
87 ($ty:ty) => {
88 impl $crate::runtime::PyRuntimeType for $ty {
89 fn runtime_type_object(
90 py: ::pyo3::Python<'_>,
91 ) -> ::pyo3::PyResult<::pyo3::Bound<'_, ::pyo3::PyAny>> {
92 Ok(py.get_type::<$ty>().into_any())
93 }
94 }
95 };
96}
97
98/// Creates a Python union type using the `|` operator (Python 3.10+).
99///
100/// # Arguments
101///
102/// * `py` - Python interpreter token
103/// * `types` - Slice of Python type objects to combine into a union
104///
105/// # Returns
106///
107/// A Python object representing the union of all input types.
108/// For a single type, returns that type unchanged.
109/// For multiple types, returns a `types.UnionType`.
110///
111/// # Errors
112///
113/// Returns an error if:
114/// - The types slice is empty
115/// - Any of the `__or__` operations fail
116///
117/// # Example
118///
119/// ```rust,ignore
120/// use pyo3::types::{PyInt, PyString};
121///
122/// let union = union_type(py, &[
123/// py.get_type::<PyInt>().into_any(),
124/// py.get_type::<PyString>().into_any(),
125/// ])?;
126/// // union is equivalent to `int | str` in Python
127/// ```
128pub fn union_type<'py>(
129 py: Python<'py>,
130 types: &[Bound<'py, PyAny>],
131) -> PyResult<Bound<'py, PyAny>> {
132 if types.is_empty() {
133 return Err(PyErr::new::<::pyo3::exceptions::PyValueError, _>(
134 "union_type requires at least one type",
135 ));
136 }
137
138 // Use Python's operator module to create union types
139 // operator.or_(type1, type2) works correctly with type objects
140 let operator = py.import("operator")?;
141 let or_fn = operator.getattr("or_")?;
142
143 let mut result = types[0].clone();
144 for ty in &types[1..] {
145 result = or_fn.call1((&result, ty))?;
146 }
147 Ok(result)
148}
149
150/// Trait for type aliases that can be registered at runtime.
151///
152/// This trait is automatically implemented by the [`type_alias!`](crate::type_alias)
153/// macro. It provides the metadata and factory method needed to register a type alias
154/// in a Python module.
155///
156/// # Associated Constants
157///
158/// * `NAME` - The Python name of the type alias
159/// * `MODULE` - The module where the type alias is defined
160///
161/// # Required Methods
162///
163/// * `create_type_object` - Creates the Python type object representing the alias
164pub trait PyTypeAlias: crate::PyStubType {
165 /// The name of the type alias in Python.
166 const NAME: &'static str;
167
168 /// The module path where this type alias is defined.
169 const MODULE: &'static str;
170
171 /// Creates the Python type object for this type alias.
172 ///
173 /// For union types, this creates a union using the `|` operator.
174 ///
175 /// # Arguments
176 ///
177 /// * `py` - Python interpreter token
178 ///
179 /// # Returns
180 ///
181 /// The Python type object representing this type alias.
182 fn create_type_object(py: Python<'_>) -> PyResult<Bound<'_, PyAny>>;
183}
184
185/// Extension trait for `Bound<PyModule>` to add type aliases.
186///
187/// This trait provides a convenient method for registering type aliases in Python modules.
188///
189/// # Example
190///
191/// ```rust,ignore
192/// use pyo3::prelude::*;
193/// use pyo3_stub_gen::runtime::PyModuleTypeAliasExt;
194///
195/// #[pymodule]
196/// fn my_module(m: &Bound<PyModule>) -> PyResult<()> {
197/// m.add_type_alias::<MyTypeAlias>()?;
198/// Ok(())
199/// }
200/// ```
201pub trait PyModuleTypeAliasExt {
202 /// Adds a type alias to this module.
203 ///
204 /// The type alias will be available as a module attribute with the name
205 /// specified by `T::NAME`.
206 ///
207 /// # Type Parameters
208 ///
209 /// * `T` - A type implementing [`PyTypeAlias`]
210 ///
211 /// # Errors
212 ///
213 /// Returns an error if:
214 /// - Creating the type object fails
215 /// - Adding the attribute to the module fails
216 fn add_type_alias<T: PyTypeAlias>(&self) -> PyResult<()>;
217}
218
219impl PyModuleTypeAliasExt for Bound<'_, PyModule> {
220 fn add_type_alias<T: PyTypeAlias>(&self) -> PyResult<()> {
221 let type_object = T::create_type_object(self.py())?;
222 self.add(T::NAME, type_object)
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use ::pyo3::type_object::PyTypeInfo;
230
231 #[test]
232 fn test_union_type_empty() {
233 pyo3::Python::initialize();
234 Python::attach(|py| {
235 let result = union_type(py, &[]);
236 assert!(result.is_err());
237 });
238 }
239
240 #[test]
241 fn test_union_type_single() {
242 pyo3::Python::initialize();
243 Python::attach(|py| {
244 use ::pyo3::types::PyInt;
245 let int_type = py.get_type::<PyInt>();
246 let result = union_type(py, &[int_type.clone().into_any()]);
247 assert!(result.is_ok());
248 // Single type should just return itself
249 let union = result.unwrap();
250 assert!(union.is(&int_type));
251 });
252 }
253
254 #[test]
255 fn test_union_type_multiple() {
256 pyo3::Python::initialize();
257 Python::attach(|py| {
258 use ::pyo3::types::{PyInt, PyString};
259 let int_type = py.get_type::<PyInt>().into_any();
260 let str_type = py.get_type::<PyString>().into_any();
261 let result = union_type(py, &[int_type, str_type]);
262 assert!(result.is_ok(), "union_type failed: {:?}", result);
263 // The result should be a union type (int | str)
264 let union = result.unwrap();
265 // Check that it's a UnionType by checking its repr
266 let repr = union.repr().unwrap().to_string();
267 assert!(repr.contains("int") && repr.contains("str"));
268 });
269 }
270
271 // Test custom #[pyclass] with union_type
272 #[::pyo3::pyclass]
273 struct TestCustomClass {
274 #[allow(dead_code)]
275 value: i32,
276 }
277
278 #[test]
279 fn test_union_type_with_pyclass() {
280 pyo3::Python::initialize();
281 Python::attach(|py| {
282 use ::pyo3::types::PyInt;
283 let int_type = PyInt::type_object(py).into_any();
284 let custom_type = TestCustomClass::type_object(py).into_any();
285 let result = union_type(py, &[int_type, custom_type]);
286 assert!(
287 result.is_ok(),
288 "union_type with pyclass failed: {:?}",
289 result
290 );
291 // The result should be a union type (int | TestCustomClass)
292 let union = result.unwrap();
293 let repr = union.repr().unwrap().to_string();
294 assert!(
295 repr.contains("int") && repr.contains("TestCustomClass"),
296 "Expected union repr to contain 'int' and 'TestCustomClass', got: {}",
297 repr
298 );
299 });
300 }
301
302 // Test type_alias! macro with custom #[pyclass]
303 #[::pyo3::pyclass]
304 struct MyCustomType;
305
306 // PyStubType implementation is required for type_alias! (stub generation)
307 impl crate::PyStubType for MyCustomType {
308 fn type_output() -> crate::TypeInfo {
309 crate::TypeInfo::builtin("MyCustomType")
310 }
311 }
312
313 // PyRuntimeType implementation is required for type_alias! (runtime registration)
314 crate::impl_py_runtime_type!(MyCustomType);
315
316 crate::type_alias!(
317 "test_module",
318 CustomTypeOrInt = MyCustomType | i32,
319 "A union of a custom pyclass and int (using Rust type i32)."
320 );
321
322 #[test]
323 fn test_type_alias_with_pyclass() {
324 pyo3::Python::initialize();
325 Python::attach(|py| {
326 let type_obj = CustomTypeOrInt::create_type_object(py);
327 assert!(
328 type_obj.is_ok(),
329 "create_type_object failed: {:?}",
330 type_obj
331 );
332 let union = type_obj.unwrap();
333 let repr = union.repr().unwrap().to_string();
334 assert!(
335 repr.contains("MyCustomType") && repr.contains("int"),
336 "Expected union repr to contain 'MyCustomType' and 'int', got: {}",
337 repr
338 );
339 });
340 }
341
342 #[test]
343 fn test_py_type_alias_constants() {
344 assert_eq!(CustomTypeOrInt::NAME, "CustomTypeOrInt");
345 assert_eq!(CustomTypeOrInt::MODULE, "test_module");
346 }
347}