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}