Skip to main content

rigetti_pyo3/
lib.rs

1// Copyright 2025 Rigetti Computing
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Helpful macros and traits for creating a Python bindings to a Rust library.
16//!
17//! # Usage
18//!
19//! See the examples directory in the source for example usage of a majority of the crate.
20//!
21//! Alternatively, check the examples on the macros in this documentation.
22
23// Covers correctness, suspicious, style, complexity, and perf
24#![deny(clippy::all)]
25#![deny(clippy::pedantic)]
26#![allow(clippy::module_name_repetitions)]
27#![deny(clippy::cargo)]
28#![allow(clippy::multiple_crate_versions)]
29#![warn(clippy::nursery)]
30// Conflicts with unreachable_pub
31#![allow(clippy::redundant_pub_crate)]
32#![deny(clippy::missing_docs_in_private_items)]
33#![deny(
34    absolute_paths_not_starting_with_crate,
35    anonymous_parameters,
36    bad_style,
37    dead_code,
38    keyword_idents,
39    improper_ctypes,
40    macro_use_extern_crate,
41    meta_variable_misuse,
42    missing_abi,
43    missing_debug_implementations,
44    missing_docs,
45    no_mangle_generic_items,
46    non_shorthand_field_patterns,
47    noop_method_call,
48    overflowing_literals,
49    path_statements,
50    patterns_in_fns_without_body,
51    semicolon_in_expressions_from_macros,
52    trivial_casts,
53    trivial_numeric_casts,
54    unconditional_recursion,
55    unreachable_pub,
56    unsafe_code,
57    unused,
58    unused_allocation,
59    unused_comparisons,
60    unused_extern_crates,
61    unused_import_braces,
62    unused_lifetimes,
63    unused_parens,
64    unused_qualifications,
65    variant_size_differences,
66    while_true
67)]
68
69mod errors;
70#[cfg(feature = "stubs")]
71pub mod stubs;
72#[cfg(feature = "async-tokio")]
73pub mod sync;
74mod traits;
75
76pub use pyo3;
77#[cfg(feature = "async-tokio")]
78pub use pyo3_async_runtimes;
79#[cfg(feature = "stubs")]
80pub use pyo3_stub_gen;
81#[cfg(feature = "async-tokio")]
82pub use tokio;
83
84use pyo3::{prelude::*, types::PyType};
85
86/// Create a crate-private function `init_submodule` to set up this submodule and call the same
87/// function on child modules (which should also use this macro).
88///
89/// This generates boilerplate for exposing classes, exceptions, functions, and child modules to
90/// the Python runtime, including a hack to allow importing from submodules, i.e.:
91///
92/// ```python,ignore
93/// from foo.bar import baz
94/// ```
95///
96/// # Example
97///
98/// ```
99/// # fn main() {
100/// use rigetti_pyo3::{create_init_submodule, exception, create_exception};
101/// use rigetti_pyo3::pyo3::{prelude::*, exceptions::PyIOError};
102///
103/// #[pyfunction]
104/// fn do_nothing() {}
105///
106/// #[pyclass]
107/// struct CoolString(String);
108///
109/// #[derive(Debug, thiserror::Error)]
110/// #[error("io error: {0}")]
111/// struct RustIOError(#[from] std::io::Error);
112///
113/// exception!(RustIOError, "example", IOError, PyIOError, "IO Error");
114///
115/// mod my_submodule {
116///     use rigetti_pyo3::create_init_submodule;
117///     use rigetti_pyo3::pyo3::pyclass;
118///
119///     #[pyclass]
120///     struct CoolInt(i32);
121///
122///     create_init_submodule! {
123///         classes: [ CoolInt ],
124///     }
125/// }
126///
127/// create_init_submodule! {
128///     /// Initialize this module and all its submodules
129///     classes: [ CoolString ],
130///     errors: [ IOError ],
131///     funcs: [ do_nothing ],
132///     submodules: [ "my_submodule": my_submodule::init_submodule ],
133/// }
134///
135/// #[pymodule]
136/// fn example(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
137///     init_submodule("example", py, m)
138/// }
139/// # }
140/// ```
141#[macro_export]
142macro_rules! create_init_submodule {
143    (
144        $(#[$meta:meta])*
145        $(classes: [ $($class: ty),+ ],)?
146        $(complex_enums: [ $($complex_enum: ty),+ ],)?
147        $(consts: [ $($const: ident),+ ],)?
148        $(errors: [ $($error: ty),+ ],)?
149        $(funcs: [ $($func: path),+ ],)?
150        $(submodules: [ $($mod_name: literal: $init_submod: path),+ ],)?
151    ) => {
152        $(#[$meta])*
153        pub(crate) fn init_submodule<'py>(_name: &str, _py: $crate::pyo3::Python<'py>, m: &$crate::pyo3::Bound<'py, $crate::pyo3::types::PyModule>) -> $crate::pyo3::PyResult<()> {
154            $($(
155            $crate::pyo3::types::PyModuleMethods::add_class::<$class>(m)?;
156            )+)?
157            $($(
158            $crate::pyo3::types::PyModuleMethods::add_class::<$complex_enum>(m)?;
159            )+)?
160            $($(
161                $crate::pyo3::types::PyModuleMethods::add(m,
162                    ::std::stringify!($const),
163                    $crate::pyo3::IntoPyObject::into_pyobject(&$const, _py)?
164                )?;
165            )+)?
166            $($(
167                $crate::pyo3::types::PyModuleMethods::add(m,
168                    $crate::pyo3::types::PyTypeMethods::name(&_py.get_type::<$error>())?,
169                    _py.get_type::<$error>()
170                )?;
171            )+)?
172            $($(
173            $crate::pyo3::types::PyModuleMethods::add_function(m, $crate::pyo3::wrap_pyfunction!($func, m)?)?;
174            )+)?
175            $(
176                let sys = $crate::pyo3::types::PyModule::import(_py, "sys")?;
177                let modules = $crate::pyo3::types::PyAnyMethods::getattr(sys.as_any(), "modules")?;
178                $(
179                let qualified_name = format!("{}.{}", _name, $mod_name);
180                let submod = $crate::pyo3::types::PyModule::new(_py, $mod_name)?;
181                $init_submod(&qualified_name, _py, &submod)?;
182                $crate::pyo3::types::PyModuleMethods::add_submodule(m, &submod)?;
183                $crate::pyo3::types::PyAnyMethods::set_item(modules.as_any(), &qualified_name, &submod)?;
184                )+
185            )?
186            $(
187                $crate::fix_complex_enums!(_py, $($complex_enum),+);
188            )?
189            Ok(())
190        }
191    }
192}
193
194/// Fix the `__qualname__` on PyO3's "complex enums" so that they can be pickled.
195///
196/// Essentially, this runs the following Python code:
197///
198/// ```python
199/// import inspect
200/// issubclass = lambda cls: inspect.isclass(cls) and issubclass(cls, typ)
201/// for name, cls in inspect.getmembers(typ, issubclass):
202///     cls.__qualname__ = f"{prefix}.{name}"
203/// ```
204///
205/// # In a Pickle
206///
207/// PyO3 processes `enum`s with non-unit variants by creating a Python class for the enum,
208/// then creating a class for each variant, subclassed from the main enum class.
209/// The subclasses end up as attributes on the main enum class,
210/// which enables syntax like `q = Qubit.Fixed(0)`;
211/// however, they're given qualified names that use `_` as a seperator instead of `.`,
212/// e.g. we get `Qubit.Fixed(0).__qualname__ == "Qubit_Fixed"`
213/// rather than `Qubit.Fixed`, as we would if we had written the inner class ourselves.
214/// As a consequence, attempting to `pickle` an instance of it
215/// will raise an error complaining that `quil.instructions.Qubit_Fixed` can't be found.
216///
217/// There are a handful of ways of making this work,
218/// but modifying the `__qualname__` seems not only simple, but correct.
219///
220/// # Usage
221///
222/// Although you can call this method directly, it is easier to use via
223/// the [`fix_complex_enums`] or [`create_init_submodule`] macros.
224/// See documentation on the former for a complete example.
225///
226/// # Errors
227///
228/// This function will fail if it's not able to access the Python `inspect` module,
229/// if that module's API changes in a future version of Python,
230/// or if it's not possible to set the `__qualname__` attribute on the class.
231///
232/// # See Also
233///
234/// - PyO3's Complex Enums: <https://pyo3.rs/v0.25.1/class#complex-enums>
235/// - Issue regarding `__qualname__`: <https://github.com/PyO3/pyo3/issues/5270>
236/// - Python's `inspect`: <https://docs.python.org/3/library/inspect.html#inspect.getmembers>
237pub fn fix_enum_qual_names(typ: &Bound<'_, PyType>) -> PyResult<()> {
238    let py = typ.py();
239    let (is_class, get_members) = __private::import_inspect(py)?;
240    __private::fix_enum_qual_names_impl(py, typ, &is_class, &get_members)
241}
242
243#[doc(hidden)]
244pub mod __private {
245    use pyo3::{
246        prelude::*,
247        types::{PyList, PyTuple, PyType, PyTypeMethods},
248    };
249
250    /// Internal function to import necessary functions from the Python `inspect` module
251    /// for use by the [`fix_enum_qual_names_impl`] function.
252    pub fn import_inspect(py: Python<'_>) -> PyResult<(Bound<'_, PyAny>, Bound<'_, PyAny>)> {
253        let inspect = PyModule::import(py, pyo3::intern!(py, "inspect"))?;
254        let is_class = inspect.getattr(pyo3::intern!(py, "isclass"))?;
255        let get_members = inspect.getattr(pyo3::intern!(py, "getmembers"))?;
256        Ok((is_class, get_members))
257    }
258
259    /// Internal implementation of [`crate::fix_enum_qual_names`].
260    ///
261    /// This amortizes the cost of the Python module import machinery
262    /// during the module initialization when there are many `fix_enum_qual_names` calls.
263    pub fn fix_enum_qual_names_impl<'py>(
264        py: Python<'py>,
265        typ: &Bound<'py, PyType>,
266        is_class: &Bound<'py, PyAny>,
267        get_members: &Bound<'py, PyAny>,
268    ) -> PyResult<()> {
269        // The additional bindings here are necessary to avoid dropping temporaries.
270        let prefix = typ.qualname()?;
271        let prefix = prefix.to_str()?;
272
273        let inner = get_members.call((typ, is_class), None)?;
274        for item in inner.cast::<PyList>()? {
275            let item = item.cast::<PyTuple>()?;
276
277            let cls = item.get_borrowed_item(1)?;
278            if cls.cast()?.is_subclass(typ)? {
279                // See https://pyo3.rs/v0.25.1/types#borroweda-py-t for info on `get_borrowed_item`.
280                let name = item.get_borrowed_item(0)?;
281                let fixed_name = format!("{prefix}.{}", name.cast()?.to_str()?);
282                cls.setattr(pyo3::intern!(py, "__qualname__"), fixed_name)?;
283            }
284        }
285
286        Ok(())
287    }
288}
289
290/// Fix the `__qualname__` on a list of complex enums so that they can be pickled.
291///
292/// The first argument should be a `Python<'py>` instance;
293/// all others should be names of `#[pyclass]`-annotated `enum`s with non-unit variants
294/// (aka "complex enums").
295///
296/// See documentation on [`fix_enum_qual_names`] for information on how this works.
297///
298/// # Notes
299///
300/// - You still must implement appropriate methods to enable `pickle` support;
301///   because PyO3 adds constructors for the enum variants, `__getnewargs__` is a great choice.
302/// - If you use this macro directly, you should do so after adding the classes to the module,
303///   since the underlying call to [`fix_enum_qual_names`] modifies the `__qualname__`.
304///   This should happen in the module initializer.
305/// - If you use [`create_init_submodule`], you can specify classes in the `complex_enums` list,
306///   and it will add the classes and apply the `__qualname__` fix in the correct order for you.
307///
308/// # Example
309///
310/// The following example demonstrates how you can use this macro to enable pickling complex enums.
311/// For completeness, it shows stub generation and use of the [`create_init_submodule`] macro,
312/// but this macro and [`fix_enum_qual_names`] can be used without these, if desired.
313///
314/// ```
315/// # fn main() { mod mainmod {
316/// use pyo3::{prelude::*, types::PyTuple};
317/// use rigetti_pyo3::{create_init_submodule, fix_complex_enums, fix_enum_qual_names};
318///
319/// // Stubs aren't required, but they're compatible with this macro (and nice to have).
320/// #[cfg(feature = "stubs")]
321/// use pyo3_stub_gen::derive::{gen_stub_pyclass_complex_enum, gen_stub_pymethods};
322///
323/// // The easiest way to apply this fix is to simply use the `create_init_submodule` macro
324/// // and specify the classes in the `complex_enums` list:
325/// mod submod {
326///     use rigetti_pyo3::create_init_submodule;
327///     use super::{Foo, Bar};
328///
329///     create_init_submodule! {
330///         complex_enums: [Foo, Bar],
331///     }
332/// }
333///
334/// // If you are setting up the module manually, you can use the function or macro directly...
335/// #[pymodule(name = "mainmod")]
336/// fn main_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
337///     let py = m.py();
338///
339///     // ...but be sure to add your classes to the module before calling either.
340///     m.add_class::<Foo>()?;
341///     m.add_class::<Bar>()?;
342///     submod::init_submodule("submod", py, m)?;
343///
344///     // You can apply the enum `__qualname__` fix via the macro or the function;
345///     // they are functionally equivalent, but the first has a slight performance optimization
346///     // by bundling together some interactions with the Python interpreter.
347///
348///     // Method one (preferred): use the macro and specify a list of complex enum types:
349///     fix_complex_enums!(py, Foo, Bar);
350///
351///     // Method two: manually call `fix_enum_qual_names` for each class:
352///     // fix_enum_qual_names(&py.get_type::<Foo>())?;
353///     // fix_enum_qual_names(&py.get_type::<Bar>())?;
354///
355///     Ok(())
356/// }
357///
358/// #[cfg_attr(feature = "stubs", gen_stub_pyclass_complex_enum)]
359/// #[pyo3::pyclass(module = "mainmod.submod")]
360/// pub enum Foo {
361///     Integer(i64),
362///     Real(f64),
363/// }
364///
365/// #[cfg_attr(feature = "stubs", gen_stub_pyclass_complex_enum)]
366/// #[pyo3::pyclass(module = "mainmod.submod")]
367/// pub enum Bar {
368///     Integer(i64),
369///     Real(f64),
370/// }
371///
372/// // Note that in order to support pickling in general,
373/// // you'll still need `__getnewargs__` or another method used by the `pickle` module.
374/// #[cfg_attr(feature = "stubs", gen_stub_pymethods)]
375/// #[pymethods]
376/// impl Foo {
377///     fn __getnewargs__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyTuple>> {
378///         match self {
379///             Foo::Integer(value) => PyTuple::new(py, [value]),
380///             Foo::Real(value) => PyTuple::new(py, [value]),
381///         }
382///     }
383/// }
384///
385/// #[cfg_attr(feature = "stubs", gen_stub_pymethods)]
386/// #[pymethods]
387/// impl Bar {
388///     fn __getnewargs__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyTuple>> {
389///         match self {
390///             Bar::Integer(value) => PyTuple::new(py, [value]),
391///             Bar::Real(value) => PyTuple::new(py, [value]),
392///         }
393///     }
394/// }
395/// # } }
396/// ```
397#[macro_export]
398macro_rules! fix_complex_enums {
399    ($py:expr, $($name:path),* $(,)?) => {
400        {
401            let py = $py;
402            // Importing once before applying the fix reduces the work done by the interpreter.
403            let (is_class, get_members) = $crate::__private::import_inspect(py)?;
404            $($crate::__private::fix_enum_qual_names_impl(py, &py.get_type::<$name>(), &is_class, &get_members)?;)*
405        }
406    };
407}
408
409/// This is essentially the example above, but with an additional test of the pickle round-trip,
410/// and a check that NOT applying the fix causes pickling to fail, despite having `__getnewargs__`.
411#[cfg(test)]
412mod test_fix_qualname {
413    use pyo3::types::{PyDict, PyTuple};
414    use pyo3::{prelude::*, py_run};
415
416    #[pyclass(module = "mymod")]
417    enum Foo {
418        Integer { value: i64 },
419        Real { value: f64 },
420    }
421
422    #[pymethods]
423    impl Foo {
424        fn __getnewargs__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyTuple>> {
425            match self {
426                Self::Integer { value } => PyTuple::new(py, [value]),
427                Self::Real { value } => PyTuple::new(py, [value]),
428            }
429        }
430    }
431
432    #[pyclass(module = "mymod")]
433    enum Bar {
434        Integer(i64),
435        Real(f64),
436    }
437
438    #[pymethods]
439    impl Bar {
440        fn __getnewargs__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyTuple>> {
441            match self {
442                Self::Integer(value) => PyTuple::new(py, [value]),
443                Self::Real(value) => PyTuple::new(py, [value]),
444            }
445        }
446    }
447
448    // This class is intentionally not "fixed" below.
449    #[pyclass(module = "mymod")]
450    enum Baz {
451        Integer(i64),
452        Real(f64),
453    }
454
455    #[pymethods]
456    impl Baz {
457        fn __getnewargs__<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyTuple>> {
458            match self {
459                Self::Integer(value) => PyTuple::new(py, [value]),
460                Self::Real(value) => PyTuple::new(py, [value]),
461            }
462        }
463    }
464
465    #[pymodule(name = "mymod")]
466    fn mymod(m: &Bound<'_, PyModule>) -> PyResult<()> {
467        let py = m.py();
468
469        m.add_class::<Foo>()?;
470        m.add_class::<Bar>()?;
471        m.add_class::<Baz>()?;
472
473        // Baz intentionally excluded.
474        fix_complex_enums!(py, Foo, Bar);
475
476        Ok(())
477    }
478
479    /// Verify that we can pickle and unpickle complex enums,
480    /// provided they've had their `__qualname__` fixed.
481    #[test]
482    fn test_fix_enum_qual_names() {
483        pyo3::append_to_inittab!(mymod);
484        Python::initialize();
485        Python::attach(|py| {
486            let locals = PyDict::new(py);
487            py_run!(
488                py,
489                *locals,
490                r#"
491import pickle
492import mymod
493from mymod import Foo
494
495objs = [
496    Foo.Integer(42),
497    Foo.Real(3.14),
498
499    # This still works even if not imported.
500    mymod.Bar.Integer(42),
501    mymod.Bar.Real(3.14),
502]
503
504for obj in objs:
505    result = pickle.loads(pickle.dumps(obj))
506    match obj:
507        case Foo.Integer(value=x) | Foo.Real(value=x):
508            assert result.value == x
509        case mymod.Bar.Integer(x) | mymod.Bar.Real(x):
510            assert result._0 == x
511        case _:
512            raise TypeError(f"Unexpected object: {obj}")
513
514# Baz doesn't have the __qualname__ fix, so pickling fails:
515from mymod import Baz
516objs = [
517    Baz.Integer(42),
518    Baz.Real(3.14),
519]
520for obj in objs:
521    try:
522        pickle.dumps(obj)
523    except pickle.PicklingError:
524        continue
525    raise TypeError(f"{obj} should not be picklable")
526"#
527            );
528        });
529    }
530}