diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 7a186c46..7b8e7ac0 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -7,7 +7,7 @@ use crate::attributes::{ use crate::deprecations::Deprecations; use crate::pyimpl::PyClassMethodsType; use crate::pymethod::{impl_py_getter_def, impl_py_setter_def, PropertyType}; -use crate::utils; +use crate::utils::{self, unwrap_group}; use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::ext::IdentExt; @@ -93,7 +93,7 @@ impl PyClassArgs { // We allow arbitrary expressions here so you can e.g. use `8*64` self.freelist = Some(syn::Expr::clone(right)); } - "name" => match &**right { + "name" => match unwrap_group(&**right) { syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit), .. @@ -114,7 +114,7 @@ impl PyClassArgs { } _ => expected!("type name (e.g. \"Name\")"), }, - "extends" => match &**right { + "extends" => match unwrap_group(&**right) { syn::Expr::Path(exp) => { self.base = syn::TypePath { path: exp.path.clone(), @@ -124,7 +124,7 @@ impl PyClassArgs { } _ => expected!("type path (e.g., my_mod::BaseClass)"), }, - "module" => match &**right { + "module" => match unwrap_group(&**right) { syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit), .. diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index b92ef586..cc35c108 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -117,3 +117,10 @@ pub fn ensure_not_async_fn(sig: &syn::Signature) -> syn::Result<()> { }; Ok(()) } + +pub fn unwrap_group(mut expr: &syn::Expr) -> &syn::Expr { + while let syn::Expr::Group(g) = expr { + expr = &*g.expr; + } + expr +} diff --git a/tests/test_macros.rs b/tests/test_macros.rs new file mode 100644 index 00000000..46c31b4c --- /dev/null +++ b/tests/test_macros.rs @@ -0,0 +1,91 @@ +//! Ensure that pyo3 macros can be used inside macro_rules! + +use pyo3::prelude::*; +use pyo3::wrap_pyfunction; + +#[macro_use] +mod common; + +macro_rules! make_struct_using_macro { + // Ensure that one doesn't need to fall back on the escape type: tt + // in order to macro create pyclass. + ($class_name:ident, $py_name:literal) => { + #[pyclass(name=$py_name)] + struct $class_name {} + }; +} + +make_struct_using_macro!(MyBaseClass, "MyClass"); + +macro_rules! set_extends_via_macro { + ($class_name:ident, $base_class:path) => { + // Try and pass a variable into the extends parameter + #[pyclass(extends=$base_class)] + struct $class_name {} + }; +} + +set_extends_via_macro!(MyClass2, MyBaseClass); + +// +// Check that pyfunctiona nd text_signature can be called with macro arguments. +// + +macro_rules! fn_macro { + ($sig:literal, $a_exp:expr, $b_exp:expr, $c_exp: expr) => { + // Try and pass a variable into the extends parameter + #[pyfunction($a_exp, $b_exp, "*", $c_exp)] + #[pyo3(text_signature = $sig)] + fn my_function_in_macro(a: i32, b: Option, c: i32) { + let _ = (a, b, c); + } + }; +} + +fn_macro!("(a, b=None, *, c=42)", a, b = "None", c = 42); + +macro_rules! property_rename_via_macro { + ($prop_name:ident) => { + #[pyclass] + struct ClassWithProperty { + member: u64, + } + + #[pymethods] + impl ClassWithProperty { + #[getter($prop_name)] + fn get_member(&self) -> u64 { + self.member + } + + #[setter($prop_name)] + fn set_member(&mut self, member: u64) { + self.member = member; + } + } + }; +} + +property_rename_via_macro!(my_new_property_name); + +#[test] +fn test_macro_rules_interactions() { + Python::with_gil(|py| { + let my_base = py.get_type::(); + py_assert!(py, my_base, "my_base.__name__ == 'MyClass'"); + + let my_func = wrap_pyfunction!(my_function_in_macro, py).unwrap(); + py_assert!( + py, + my_func, + "my_func.__text_signature__ == '(a, b=None, *, c=42)'" + ); + + let renamed_prop = py.get_type::(); + py_assert!( + py, + renamed_prop, + "hasattr(renamed_prop, 'my_new_property_name')" + ); + }); +}