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}