From b2de3411cc3d3415d3a1f68bfd0ea0acc6f50299 Mon Sep 17 00:00:00 2001 From: mejrs <59372212+mejrs@users.noreply.github.com> Date: Sat, 13 Jun 2026 11:04:28 +0200 Subject: [PATCH 1/3] Hoist parsing pyclass options --- pyo3-macros-backend/src/lib.rs | 2 +- pyo3-macros/src/lib.rs | 28 ++++++++++++++++++---------- src/pyclass/create_type_object.rs | 2 +- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index a90fa73678e..a624d2de31c 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -30,7 +30,7 @@ mod quotes; pub use frompyobject::build_derive_from_pyobject; pub use intopyobject::build_derive_into_pyobject; pub use module::{pymodule_function_impl, pymodule_module_impl, PyModuleOptions}; -pub use pyclass::{build_py_class, build_py_enum, PyClassArgs}; +pub use pyclass::{build_py_class, build_py_enum, PyClassArgs, PyClassKind, PyClassPyO3Options}; pub use pyfunction::{build_py_function, PyFunctionOptions}; pub use pyimpl::{build_py_methods, PyClassMethodsType}; pub use utils::get_doc; diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index bba07366b3c..db72f903d39 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -7,7 +7,7 @@ use proc_macro2::TokenStream as TokenStream2; use pyo3_macros_backend::{ build_derive_from_pyobject, build_derive_into_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods, pymodule_function_impl, pymodule_module_impl, PyClassArgs, - PyClassMethodsType, PyFunctionOptions, PyModuleOptions, + PyClassKind, PyClassMethodsType, PyClassPyO3Options, PyFunctionOptions, PyModuleOptions, }; use quote::quote; use syn::{parse_macro_input, Item}; @@ -68,10 +68,12 @@ pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { #[proc_macro_attribute] pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream { + let options = parse_macro_input!(attr as PyClassPyO3Options); + let item = parse_macro_input!(input as Item); match item { - Item::Struct(struct_) => pyclass_impl(attr, struct_, methods_type()), - Item::Enum(enum_) => pyclass_enum_impl(attr, enum_, methods_type()), + Item::Struct(struct_) => pyclass_struct_impl(options, struct_, methods_type()), + Item::Enum(enum_) => pyclass_enum_impl(options, enum_, methods_type()), unsupported => { syn::Error::new_spanned(unsupported, "#[pyclass] only supports structs and enums.") .into_compile_error() @@ -188,13 +190,16 @@ pub fn derive_from_py_object(item: TokenStream) -> TokenStream { .into() } -fn pyclass_impl( - attrs: TokenStream, +fn pyclass_struct_impl( + options: PyClassPyO3Options, mut ast: syn::ItemStruct, methods_type: PyClassMethodsType, ) -> TokenStream { - let args = parse_macro_input!(attrs with PyClassArgs::parse_struct_args); - let expanded = build_py_class(&mut ast, args, methods_type).unwrap_or_compile_error(); + let options = PyClassArgs { + class_kind: PyClassKind::Struct, + options, + }; + let expanded = build_py_class(&mut ast, options, methods_type).unwrap_or_compile_error(); quote!( #ast @@ -204,12 +209,15 @@ fn pyclass_impl( } fn pyclass_enum_impl( - attrs: TokenStream, + options: PyClassPyO3Options, mut ast: syn::ItemEnum, methods_type: PyClassMethodsType, ) -> TokenStream { - let args = parse_macro_input!(attrs with PyClassArgs::parse_enum_args); - let expanded = build_py_enum(&mut ast, args, methods_type).unwrap_or_compile_error(); + let options = PyClassArgs { + class_kind: PyClassKind::Enum, + options, + }; + let expanded = build_py_enum(&mut ast, options, methods_type).unwrap_or_compile_error(); quote!( #ast diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index 0f13bcb8832..ec66d275455 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -15,7 +15,7 @@ use crate::{ assign_sequence_item_from_mapping, get_sequence_item_from_mapping, tp_dealloc, tp_dealloc_with_gc, PyClassImpl, PyClassItemsIter, PyObjectOffset, }, - pymethods::{_call_clear, Getter, PyGetterDef, PyMethodDefType, PySetterDef, Setter}, + pymethods::{Getter, PyGetterDef, PyMethodDefType, PySetterDef, Setter, _call_clear}, trampoline::trampoline, }, pycell::impl_::PyClassObjectLayout, From 501ff41564a57ec7f2fa54e52461e148e8ae1d00 Mon Sep 17 00:00:00 2001 From: mejrs <59372212+mejrs@users.noreply.github.com> Date: Sat, 13 Jun 2026 22:03:54 +0200 Subject: [PATCH 2/3] Eagerly expand cfg/cfg_attr in pyclasses --- pyo3-macros-backend/src/lib.rs | 2 +- pyo3-macros-backend/src/pyclass.rs | 1 + pyo3-macros-backend/src/utils.rs | 11 +- pyo3-macros/Cargo.toml | 2 +- pyo3-macros/src/cfg_eval.rs | 573 +++++++++++++++++++++++++++ pyo3-macros/src/lib.rs | 37 +- src/pyclass/create_type_object.rs | 2 +- tests/ui/cfg_eval.rs | 32 ++ tests/ui/invalid_pyclass_enum.rs | 2 +- tests/ui/invalid_pyclass_enum.stderr | 17 +- 10 files changed, 661 insertions(+), 18 deletions(-) create mode 100644 pyo3-macros/src/cfg_eval.rs create mode 100644 tests/ui/cfg_eval.rs diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index a624d2de31c..42afcd44979 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -6,7 +6,7 @@ // Listed first so that macros in this module are available in the rest of the crate. #[macro_use] -mod utils; +pub mod utils; mod attributes; mod combine_errors; diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index d0619108f9c..c4fe9932ab2 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -3211,6 +3211,7 @@ fn generate_cfg_check(variants: &[PyClassEnumUnitVariant<'_>], cls: &syn::Ident) } } + // Should be unreachable; we eagerly expand cfgs now. quote_spanned! { cls.span() => #[cfg(all(#(#conditions),*))] diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index eacc9f71c0f..f525fd7875b 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -201,10 +201,7 @@ pub struct Ctx { impl Ctx { pub(crate) fn new(attr: &Option, signature: Option<&syn::Signature>) -> Self { - let pyo3_path = match attr { - Some(attr) => PyO3CratePath::Given(attr.value.0.clone()), - None => PyO3CratePath::Default, - }; + let pyo3_path = PyO3CratePath::from_crate_path(attr); let output_span = if let Some(syn::Signature { output: syn::ReturnType::Type(_, output_type), @@ -230,6 +227,12 @@ pub enum PyO3CratePath { } impl PyO3CratePath { + pub fn from_crate_path(path: &Option) -> Self { + match path { + Some(attr) => PyO3CratePath::Given(attr.value.0.clone()), + None => PyO3CratePath::Default, + } + } pub fn to_tokens_spanned(&self, span: Span) -> TokenStream { match self { Self::Given(path) => quote::quote_spanned! { span => #path }, diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 6c9da9b1fc7..dfc30ac6268 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -22,7 +22,7 @@ experimental-inspect = ["pyo3-macros-backend/experimental-inspect"] [dependencies] proc-macro2 = { version = "1.0.60", default-features = false } quote = "1" -syn = { version = "2", features = ["full", "extra-traits"] } +syn = { version = "2", features = ["full", "extra-traits", "visit", "visit-mut"] } pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.29.0" } [lints] diff --git a/pyo3-macros/src/cfg_eval.rs b/pyo3-macros/src/cfg_eval.rs new file mode 100644 index 00000000000..502b4ac343e --- /dev/null +++ b/pyo3-macros/src/cfg_eval.rs @@ -0,0 +1,573 @@ +//! Eagerly expand cfg/cfg_attr attributes. +//! +//! This works by duplicating code and then re-emitting that. If there are multiple attributes with +//! different predicates this process will be repeated again until there are no more cfg/cfg_attr +//! attributes left. See the tests at the end of this file for examples. + +use quote::quote; +use quote::ToTokens; +use syn::parse::Parse; +use syn::parse::ParseStream; +use syn::parse_quote; +use syn::punctuated::Punctuated; +use syn::visit; +use syn::visit::Visit; +use syn::visit_mut; +use syn::visit_mut::VisitMut; +use syn::{ + AttrStyle, Attribute, Fields, FieldsNamed, FieldsUnnamed, Item, ItemEnum, ItemStruct, LitBool, + Meta, Token, Variant, +}; + +#[allow(clippy::large_enum_variant)] +#[derive(Eq, PartialEq, Clone, Debug)] +enum Predicate { + Meta(Meta), + Bool(LitBool), +} + +impl TryFrom<&Attribute> for Predicate { + type Error = (); + fn try_from(attr: &Attribute) -> Result { + if let Attribute { + style: AttrStyle::Outer, + meta: Meta::List(ml), + .. + } = &attr + { + if ml.path.is_ident("cfg") { + if let Ok(meta) = ml.parse_args::() { + return Ok(Predicate::Meta(meta)); + } else if let Ok(lb) = ml.parse_args::() { + return Ok(Predicate::Bool(lb)); + } + } + } + Err(()) + } +} + +impl ToTokens for Predicate { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + Predicate::Meta(m) => m.to_tokens(tokens), + Predicate::Bool(b) => b.to_tokens(tokens), + } + } +} + +struct CfgAttr { + predicate: Predicate, + attrs: Punctuated, +} + +impl TryFrom<&Attribute> for CfgAttr { + type Error = (); + fn try_from(attr: &Attribute) -> Result { + struct CfgAttrImpl { + predicate: Predicate, + attrs: Punctuated, + } + + impl Parse for CfgAttrImpl { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + predicate: if let Ok(meta) = input.parse::() { + Predicate::Meta(meta) + } else if let Ok(lb) = input.parse::() { + Predicate::Bool(lb) + } else { + return Err(input.error("invalid cfg predicate")); + }, + attrs: { + input.parse::()?; + input.parse_terminated(Meta::parse, Token![,])? + }, + }) + } + } + + if let Attribute { + style: AttrStyle::Outer, + meta: Meta::List(ml), + .. + } = &attr + { + if ml.path.is_ident("cfg_attr") { + if let Ok(CfgAttrImpl { predicate, attrs }) = ml.parse_args::() { + return Ok(CfgAttr { predicate, attrs }); + } + } + } + Err(()) + } +} + +/// Finds the first cfg/cfg_attr predicate. +/// +/// The order is `syn::Visit` implementation detail, we should not depend on it. +#[derive(Default)] +struct PredicateFinder { + predicate: Option, +} + +impl PredicateFinder { + fn find_first_in_attributes(&mut self, attrs: &[Attribute]) { + for attr in attrs { + if self.predicate.is_some() { + break; + } + if let Ok(cfg) = TryInto::::try_into(attr) { + self.predicate = Some(cfg); + } else if let Ok(cfg_attr) = TryInto::::try_into(attr) { + self.predicate = Some(cfg_attr.predicate); + } + } + } +} + +/// This doesn't override `visit_attribute` because we don't want to visit all attributes, +/// only the ones in places we explicitly support: +/// - struct fields +/// - enum variants +/// - enum variant fields +impl Visit<'_> for PredicateFinder { + fn visit_item_struct(&mut self, i: &ItemStruct) { + for field in &i.fields { + self.find_first_in_attributes(&field.attrs); + } + visit::visit_item_struct(self, i); + } + fn visit_item_enum(&mut self, i: &ItemEnum) { + for variant in &i.variants { + self.find_first_in_attributes(&variant.attrs); + } + visit::visit_item_enum(self, i); + } + fn visit_variant(&mut self, i: &Variant) { + for field in &i.fields { + self.find_first_in_attributes(&field.attrs); + } + visit::visit_variant(self, i); + } +} + +struct CfgHoist<'p> { + predicate: &'p Predicate, + direction: Direction, + progress: bool, +} + +impl CfgHoist<'_> { + fn edit_attributes(&mut self, attrs: &mut Vec) -> OwnerStatus { + let old_attrs = std::mem::take(attrs).into_iter(); + for attr in old_attrs { + if let Ok(predicate) = TryInto::::try_into(&attr) { + if predicate == *self.predicate { + // We've found `#[cfg($predicate)]` on a field or variant. + self.progress = true; + let _: MustDiverge = match self.direction { + Direction::Forward => { + // #[cfg($predicate)] is hoisted on top of the item, + // so we don't preserve the attribute + continue; + } + Direction::Reverse => { + // #[cfg(not($predicate))] is hoisted on top of the item, + // so we remove the owner (and its attributes with it - + // we don't care about those). + return OwnerStatus::Remove; + } + }; + } + } else if let Ok(cfg_attr) = TryInto::::try_into(&attr) { + if cfg_attr.predicate == *self.predicate { + // We've found `#[cfg_attr($predicate, a, b, c, etc..)]` on a field or variant. + self.progress = true; + let _: MustDiverge = match self.direction { + Direction::Forward => { + // #[cfg($predicate)] is hoisted on top of the item, + // so we have to put `#[a] #[b] #[c] /* etc.. */` on the owner. + for meta in cfg_attr.attrs { + attrs.push(parse_quote!(#[#meta])); + } + continue; + } + Direction::Reverse => { + // #[cfg(not($predicate))] is hoisted on top of the item, + // so we just don't keep the cfg_attr. + continue; + } + }; + } + } + // Nothing we care about, preserve it. + attrs.push(attr); + } + OwnerStatus::Keep + } +} + +impl VisitMut for CfgHoist<'_> { + fn visit_item_struct_mut(&mut self, i: &mut ItemStruct) { + if let Fields::Named(FieldsNamed { named: fields, .. }) + | Fields::Unnamed(FieldsUnnamed { + unnamed: fields, .. + }) = &mut i.fields + { + let mut old_fields = std::mem::take(fields); + let trailing = old_fields.pop_punct(); + for mut field in old_fields { + let keep = self.edit_attributes(&mut field.attrs); + if matches!(keep, OwnerStatus::Keep) { + fields.push(field); + } + } + if !fields.empty_or_trailing() { + if let Some(punct) = trailing { + fields.push_punct(punct); + } + } + } + + visit_mut::visit_item_struct_mut(self, i); + } + + fn visit_item_enum_mut(&mut self, i: &mut ItemEnum) { + let mut old_variants = std::mem::take(&mut i.variants); + let trailing = old_variants.pop_punct(); + + for mut variant in old_variants { + let keep = self.edit_attributes(&mut variant.attrs); + if matches!(keep, OwnerStatus::Keep) { + i.variants.push(variant); + } + } + if !i.variants.empty_or_trailing() { + if let Some(punct) = trailing { + i.variants.push_punct(punct); + } + } + + visit_mut::visit_item_enum_mut(self, i); + } + + fn visit_variant_mut(&mut self, i: &mut Variant) { + if let Fields::Named(FieldsNamed { named: fields, .. }) + | Fields::Unnamed(FieldsUnnamed { + unnamed: fields, .. + }) = &mut i.fields + { + let mut old_fields = std::mem::take(fields); + let trailing = old_fields.pop_punct(); + for mut field in old_fields { + let keep = self.edit_attributes(&mut field.attrs); + if matches!(keep, OwnerStatus::Keep) { + fields.push(field); + } + } + if !fields.empty_or_trailing() { + if let Some(punct) = trailing { + fields.push_punct(punct); + } + } + } + + visit_mut::visit_variant_mut(self, i); + } +} + +enum Direction { + Forward, + Reverse, +} + +enum OwnerStatus { + Keep, + Remove, +} + +enum MustDiverge {} + +#[allow(clippy::large_enum_variant)] +pub enum CfgEvalResult { + Ready(Item), + Retry(proc_macro2::TokenStream), +} + +pub fn cfg_eval_impl(this: proc_macro2::TokenStream, input: Item) -> CfgEvalResult { + let predicate = &{ + let mut finder = PredicateFinder::default(); + finder.visit_item(&input); + match finder.predicate { + Some(predicate) => predicate, + None => return CfgEvalResult::Ready(input), + } + }; + + let mut forward = input.clone(); + + let mut hoist = CfgHoist { + predicate, + direction: Direction::Forward, + progress: false, + }; + hoist.visit_item_mut(&mut forward); + assert!( + hoist.progress, + "found cfg/cfg_attr predicates but was unable to expand any" + ); + + let mut reverse = input; + let mut hoist = CfgHoist { + predicate, + direction: Direction::Reverse, + progress: false, + }; + hoist.visit_item_mut(&mut reverse); + assert!( + hoist.progress, + "found cfg/cfg_attr predicates but was unable to expand any" + ); + let retry = quote! { + #[cfg(#predicate)] + #this + #forward + + #[cfg(not(#predicate))] + #this + #reverse + + }; + CfgEvalResult::Retry(retry) +} + +#[cfg(test)] +mod tests { + use super::*; + use syn::ItemMod; + + #[test] + fn find_cfg1() { + let input: Item = parse_quote! { + pub struct Thing( + #[cfg_attr(feature = "feature_name", blah)] u8, + ); + }; + let predicate = { + let mut finder = PredicateFinder::default(); + finder.visit_item(&input); + match finder.predicate { + Some(predicate) => predicate, + None => panic!(), + } + }; + assert_eq!( + predicate, + Predicate::Meta(parse_quote!(feature = "feature_name")) + ); + } + + #[test] + fn find_cfg2() { + let input: Item = parse_quote! { + pub struct Thing( + #[cfg(true)] u8, + ); + }; + let predicate = { + let mut finder = PredicateFinder::default(); + finder.visit_item(&input); + match finder.predicate { + Some(predicate) => predicate, + None => panic!(), + } + }; + assert_eq!(predicate, Predicate::Bool(parse_quote!(true))); + } + + #[test] + fn dont_find_cfg() { + let input: Item = parse_quote! { + #[cfg(what1)] + pub struct Thing<#[cfg(what2)] GENERIC>( + u8, + ); + }; + + let mut finder = PredicateFinder::default(); + finder.visit_item(&input); + assert!(finder.predicate.is_none()); + } + + #[test] + fn test_struct() { + let this = quote! { #[macro_name(a,b,c,d)] }; + { + let first: Item = parse_quote! { + pub struct MyClass( + #[cfg_attr(not(feature = "feature_name"), helper_name(get, name = "raw"))] u8, + #[cfg_attr(all(feature = "feature_name", other_cfg), helper_name(get, name = "raw2"))] u8, + ); + }; + let expected: ItemMod = parse_quote! { + mod test { + #[cfg(not(feature = "feature_name"))] + #[macro_name(a, b, c, d)] + pub struct MyClass( + #[helper_name(get, name = "raw")] u8, + #[cfg_attr(all(feature = "feature_name", other_cfg), helper_name(get, name = "raw2"))] u8, + ); + + #[cfg(not(not(feature = "feature_name")))] + #[macro_name(a, b, c, d)] + pub struct MyClass( + u8, + #[cfg_attr(all(feature = "feature_name", other_cfg), helper_name(get, name = "raw2"))] u8, + ); + } + }; + + let CfgEvalResult::Retry(second) = cfg_eval_impl(this.clone(), first) else { + panic!() + }; + let second: ItemMod = parse_quote! { + mod test { + #second + } + }; + assert_eq!( + second, + expected, + "{} {}", + second.to_token_stream(), + expected.to_token_stream() + ); + } + + { + let second: Item = parse_quote! { + pub struct MyClass( + u8, + #[cfg_attr(all(feature = "feature_name", other_cfg), helper_name(get, name = "raw2"))] u8, + ); + }; + let expected: ItemMod = parse_quote! { + mod test { + #[cfg(all(feature = "feature_name", other_cfg))] + #[macro_name(a, b, c, d)] + pub struct MyClass(u8, #[helper_name(get, name = "raw2")] u8,); + + #[cfg(not(all(feature = "feature_name", other_cfg)))] + #[macro_name(a, b, c, d)] + pub struct MyClass(u8, u8,); + } + + }; + let CfgEvalResult::Retry(third) = cfg_eval_impl(this.clone(), second) else { + panic!("couldnt find cfgs to expand") + }; + let third: ItemMod = parse_quote! { + mod test { + #third + } + }; + assert_eq!( + third, + expected, + "{} {}", + third.to_token_stream(), + expected.to_token_stream() + ); + } + + { + let third: Item = parse_quote! { + pub struct MyClass(u8, #[helper_name(get, name = "raw2")] u8,); + }; + let CfgEvalResult::Ready(fourth) = cfg_eval_impl(this, third.clone()) else { + panic!("couldnt find cfgs to expand") + }; + assert_eq!( + fourth, + third, + "{} {}", + fourth.to_token_stream(), + third.to_token_stream() + ); + } + } + + #[test] + fn test_enum() { + let this = quote! { #[macro_name(a,b,c,d)] }; + { + let first = parse_quote! { + enum Shape { + Circle { + #[cfg_attr(cfg_name, helper_name = "what")] + radius: f64, + }, + Rectangle { + width: f64, + #[cfg(cfg_name2)] + height: f64, + }, + #[cfg(cfg_name2)] + RegularPolygon(u32, f64), + #[cfg(cfg_name)] + Nothing(), + } + }; + let expected: ItemMod = parse_quote! { + mod test { + #[cfg(cfg_name2)] + #[macro_name(a, b, c, d)] + enum Shape { + Circle { + #[cfg_attr(cfg_name, helper_name = "what")] + radius: f64, + }, + Rectangle { + width: f64, + height: f64, + }, + RegularPolygon(u32, f64), + #[cfg(cfg_name)] + Nothing(), + } + + #[cfg(not(cfg_name2))] + #[macro_name(a, b, c, d)] + enum Shape { + Circle { + #[cfg_attr(cfg_name, helper_name = "what")] + radius: f64, + }, + Rectangle { + width: f64, + }, + #[cfg(cfg_name)] + Nothing(), + } + } + }; + + let CfgEvalResult::Retry(second) = cfg_eval_impl(this.clone(), first) else { + panic!("couldnt find cfgs to expand") + }; + let second: ItemMod = parse_quote! { + mod test { + #second + } + }; + assert_eq!( + second, + expected, + "{} {}", + second.to_token_stream(), + expected.to_token_stream() + ); + } + } +} diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index db72f903d39..6227edfed03 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -2,15 +2,18 @@ //! must not contain any other public items. #![cfg_attr(docsrs, feature(doc_cfg))] +use cfg_eval::CfgEvalResult; use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use pyo3_macros_backend::{ build_derive_from_pyobject, build_derive_into_pyobject, build_py_class, build_py_enum, - build_py_function, build_py_methods, pymodule_function_impl, pymodule_module_impl, PyClassArgs, - PyClassKind, PyClassMethodsType, PyClassPyO3Options, PyFunctionOptions, PyModuleOptions, + build_py_function, build_py_methods, pymodule_function_impl, pymodule_module_impl, + utils::PyO3CratePath, PyClassArgs, PyClassKind, PyClassMethodsType, PyClassPyO3Options, + PyFunctionOptions, PyModuleOptions, }; use quote::quote; -use syn::{parse_macro_input, Item}; +use syn::{parse_macro_input, Item, ItemEnum, ItemStruct}; +mod cfg_eval; /// A proc macro used to implement Python modules. /// @@ -68,9 +71,33 @@ pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { #[proc_macro_attribute] pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream { - let options = parse_macro_input!(attr as PyClassPyO3Options); + let mut item = parse_macro_input!(input as Item); + let attrs: proc_macro2::TokenStream = attr.clone().into(); + let mut options = parse_macro_input!(attr as PyClassPyO3Options); + + let mut pyo3_attrs = Vec::new(); + if let Item::Enum(ItemEnum { attrs, .. }) | Item::Struct(ItemStruct { attrs, .. }) = &mut item { + for attr in &*attrs { + if attr.path().is_ident("pyo3") { + pyo3_attrs.push(attr.clone()); + } + } + if let Err(e) = options.take_pyo3_options(attrs) { + return e.into_compile_error().into(); + } + } + + let pyo3_path = PyO3CratePath::from_crate_path(&options.krate); + let this = quote! { + #[#pyo3_path::pyclass(#attrs)] + #(#pyo3_attrs)* + }; + + let item = match cfg_eval::cfg_eval_impl(this, item) { + CfgEvalResult::Ready(item) => item, + CfgEvalResult::Retry(ts) => return ts.into(), + }; - let item = parse_macro_input!(input as Item); match item { Item::Struct(struct_) => pyclass_struct_impl(options, struct_, methods_type()), Item::Enum(enum_) => pyclass_enum_impl(options, enum_, methods_type()), diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index ec66d275455..0f13bcb8832 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -15,7 +15,7 @@ use crate::{ assign_sequence_item_from_mapping, get_sequence_item_from_mapping, tp_dealloc, tp_dealloc_with_gc, PyClassImpl, PyClassItemsIter, PyObjectOffset, }, - pymethods::{Getter, PyGetterDef, PyMethodDefType, PySetterDef, Setter, _call_clear}, + pymethods::{_call_clear, Getter, PyGetterDef, PyMethodDefType, PySetterDef, Setter}, trampoline::trampoline, }, pycell::impl_::PyClassObjectLayout, diff --git a/tests/ui/cfg_eval.rs b/tests/ui/cfg_eval.rs new file mode 100644 index 00000000000..310dfc7b0d5 --- /dev/null +++ b/tests/ui/cfg_eval.rs @@ -0,0 +1,32 @@ +//@check-pass + +#[pyo3::pyclass] +pub struct MyTupleStruct( + #[cfg_attr(true, pyo3(get, name = "raw"))] + u8, +); + +#[pyo3::pyclass] +pub enum MyEnum { + One{ + a: i32, + #[cfg(false)] + b: usize, + }, + Two { + #[cfg_attr(any(), pyo3(get))] + field: u8, + }, + #[cfg(all())] + Three{ + y: String, + }, +} + +#[pyo3::pyclass] +pub struct MyFieldStruct{ + #[cfg_attr(true, pyo3(get, name = "raw"))] + pub f: u8, + #[cfg_attr(any(), pyo3(set, name = "what"))] + pub g: i32, +} diff --git a/tests/ui/invalid_pyclass_enum.rs b/tests/ui/invalid_pyclass_enum.rs index 9d1eb63da67..5bf1861f1f4 100644 --- a/tests/ui/invalid_pyclass_enum.rs +++ b/tests/ui/invalid_pyclass_enum.rs @@ -117,7 +117,7 @@ enum InvalidOrderedComplexEnum2 { #[pyclass(eq)] #[derive(PartialEq)] enum AllEnumVariantsDisabled { -//~^ ERROR: #[pyclass] can't be used on enums without any variants - all variants of enum `AllEnumVariantsDisabled` have been configured out by cfg attributes +//~^ ERROR: #[pyclass] can't be used on enums without any variants #[cfg(any())] DisabledA, #[cfg(not(all()))] diff --git a/tests/ui/invalid_pyclass_enum.stderr b/tests/ui/invalid_pyclass_enum.stderr index cc5faa53700..d8feb29a2f0 100644 --- a/tests/ui/invalid_pyclass_enum.stderr +++ b/tests/ui/invalid_pyclass_enum.stderr @@ -66,11 +66,18 @@ error: The `ord` option requires the `eq` option. 99 | #[pyclass(ord)] | ^^^ -error: #[pyclass] can't be used on enums without any variants - all variants of enum `AllEnumVariantsDisabled` have been configured out by cfg attributes - --> tests/ui/invalid_pyclass_enum.rs:119:6 - | -119 | enum AllEnumVariantsDisabled { - | ^^^^^^^^^^^^^^^^^^^^^^^ +error: #[pyclass] can't be used on enums without any variants + --> tests/ui/invalid_pyclass_enum.rs:119:30 + | +119 | enum AllEnumVariantsDisabled { + | ______________________________^ +120 | | +121 | | #[cfg(any())] +122 | | DisabledA, +123 | | #[cfg(not(all()))] +124 | | DisabledB, +125 | | } + | |_^ error[E0369]: binary operation `==` cannot be applied to type `&SimpleEqOptRequiresPartialEq` --> tests/ui/invalid_pyclass_enum.rs:36:11 From 03d922dd950f2d8da118a4bfd1562b0f796a13fb Mon Sep 17 00:00:00 2001 From: mejrs <59372212+mejrs@users.noreply.github.com> Date: Sat, 13 Jun 2026 22:04:01 +0200 Subject: [PATCH 3/3] Newsfragment --- newsfragments/6134.changed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/6134.changed.md diff --git a/newsfragments/6134.changed.md b/newsfragments/6134.changed.md new file mode 100644 index 00000000000..c7c1a2974dc --- /dev/null +++ b/newsfragments/6134.changed.md @@ -0,0 +1 @@ +The `#[pyclass]` macro will now eagerly evaluate `cfg` and `cfg_attr` on fields and enum variants. \ No newline at end of file