Skip to content

Commit a84dae0

Browse files
Collecting multiple attribute error (#4243)
* collecting multiple errors * collecting errors from different fileds * adding changelog * Adding UI test * refactoring * Update pyo3-macros-backend/src/attributes.rs Co-authored-by: David Hewitt <[email protected]> * Update newsfragments/4243.changed.md Co-authored-by: David Hewitt <[email protected]> * Update tests/ui/invalid_pyclass_args.rs Co-authored-by: David Hewitt <[email protected]> * using pural for names * get rid of internidiate field_options_res * reset ordering --------- Co-authored-by: David Hewitt <[email protected]>
1 parent 5ac5cef commit a84dae0

File tree

5 files changed

+149
-80
lines changed

5 files changed

+149
-80
lines changed

newsfragments/4243.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Report multiple errors from `#[pyclass]` and `#[pyo3(..)]` attributes.

pyo3-macros-backend/src/attributes.rs

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -384,13 +384,41 @@ pub fn take_attributes(
384384

385385
pub fn take_pyo3_options<T: Parse>(attrs: &mut Vec<syn::Attribute>) -> Result<Vec<T>> {
386386
let mut out = Vec::new();
387-
take_attributes(attrs, |attr| {
388-
if let Some(options) = get_pyo3_options(attr)? {
389-
out.extend(options);
387+
let mut all_errors = ErrorCombiner(None);
388+
take_attributes(attrs, |attr| match get_pyo3_options(attr) {
389+
Ok(result) => {
390+
if let Some(options) = result {
391+
out.extend(options);
392+
Ok(true)
393+
} else {
394+
Ok(false)
395+
}
396+
}
397+
Err(err) => {
398+
all_errors.combine(err);
390399
Ok(true)
391-
} else {
392-
Ok(false)
393400
}
394401
})?;
402+
all_errors.ensure_empty()?;
395403
Ok(out)
396404
}
405+
406+
pub struct ErrorCombiner(pub Option<syn::Error>);
407+
408+
impl ErrorCombiner {
409+
pub fn combine(&mut self, error: syn::Error) {
410+
if let Some(existing) = &mut self.0 {
411+
existing.combine(error);
412+
} else {
413+
self.0 = Some(error);
414+
}
415+
}
416+
417+
pub fn ensure_empty(self) -> Result<()> {
418+
if let Some(error) = self.0 {
419+
Err(error)
420+
} else {
421+
Ok(())
422+
}
423+
}
424+
}

pyo3-macros-backend/src/pyclass.rs

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ use syn::{parse_quote, parse_quote_spanned, spanned::Spanned, ImplItemFn, Result
1010

1111
use crate::attributes::kw::frozen;
1212
use crate::attributes::{
13-
self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute,
14-
ModuleAttribute, NameAttribute, NameLitStr, RenameAllAttribute, StrFormatterAttribute,
13+
self, kw, take_pyo3_options, CrateAttribute, ErrorCombiner, ExtendsAttribute,
14+
FreelistAttribute, ModuleAttribute, NameAttribute, NameLitStr, RenameAllAttribute,
15+
StrFormatterAttribute,
1516
};
1617
use crate::konst::{ConstAttributes, ConstSpec};
1718
use crate::method::{FnArg, FnSpec, PyArg, RegularArg};
@@ -252,23 +253,35 @@ pub fn build_py_class(
252253
)
253254
);
254255

256+
let mut all_errors = ErrorCombiner(None);
257+
255258
let mut field_options: Vec<(&syn::Field, FieldPyO3Options)> = match &mut class.fields {
256259
syn::Fields::Named(fields) => fields
257260
.named
258261
.iter_mut()
259-
.map(|field| {
260-
FieldPyO3Options::take_pyo3_options(&mut field.attrs)
261-
.map(move |options| (&*field, options))
262-
})
263-
.collect::<Result<_>>()?,
262+
.filter_map(
263+
|field| match FieldPyO3Options::take_pyo3_options(&mut field.attrs) {
264+
Ok(options) => Some((&*field, options)),
265+
Err(e) => {
266+
all_errors.combine(e);
267+
None
268+
}
269+
},
270+
)
271+
.collect::<Vec<_>>(),
264272
syn::Fields::Unnamed(fields) => fields
265273
.unnamed
266274
.iter_mut()
267-
.map(|field| {
268-
FieldPyO3Options::take_pyo3_options(&mut field.attrs)
269-
.map(move |options| (&*field, options))
270-
})
271-
.collect::<Result<_>>()?,
275+
.filter_map(
276+
|field| match FieldPyO3Options::take_pyo3_options(&mut field.attrs) {
277+
Ok(options) => Some((&*field, options)),
278+
Err(e) => {
279+
all_errors.combine(e);
280+
None
281+
}
282+
},
283+
)
284+
.collect::<Vec<_>>(),
272285
syn::Fields::Unit => {
273286
if let Some(attr) = args.options.set_all {
274287
return Err(syn::Error::new_spanned(attr, UNIT_SET));
@@ -281,6 +294,8 @@ pub fn build_py_class(
281294
}
282295
};
283296

297+
all_errors.ensure_empty()?;
298+
284299
if let Some(attr) = args.options.get_all {
285300
for (_, FieldPyO3Options { get, .. }) in &mut field_options {
286301
if let Some(old_get) = get.replace(Annotated::Struct(attr)) {

tests/ui/invalid_pyclass_args.rs

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
use std::fmt::{Display, Formatter};
21
use pyo3::prelude::*;
2+
use std::fmt::{Display, Formatter};
33

44
#[pyclass(extend=pyo3::types::PyDict)]
55
struct TypoIntheKey {}
@@ -74,7 +74,16 @@ impl HashOptAndManualHash {
7474

7575
#[pyclass(ord)]
7676
struct InvalidOrderedStruct {
77-
inner: i32
77+
inner: i32,
78+
}
79+
80+
#[pyclass]
81+
struct MultipleErrors {
82+
#[pyo3(foo)]
83+
#[pyo3(blah)]
84+
x: i32,
85+
#[pyo3(pop)]
86+
y: i32,
7887
}
7988

8089
#[pyclass(str)]
@@ -88,9 +97,7 @@ impl Display for StrOptAndManualStr {
8897

8998
#[pymethods]
9099
impl StrOptAndManualStr {
91-
fn __str__(
92-
&self,
93-
) -> String {
100+
fn __str__(&self) -> String {
94101
todo!()
95102
}
96103
}
@@ -123,45 +130,45 @@ pub struct Point2 {
123130
#[derive(PartialEq)]
124131
struct Coord3(u32, u32, u32);
125132

126-
#[pyclass(name = "aaa", str="unsafe: {unsafe_variable}")]
133+
#[pyclass(name = "aaa", str = "unsafe: {unsafe_variable}")]
127134
struct StructRenamingWithStrFormatter {
128135
#[pyo3(name = "unsafe", get, set)]
129136
unsafe_variable: usize,
130137
}
131138

132-
#[pyclass(name = "aaa", str="unsafe: {unsafe_variable}")]
139+
#[pyclass(name = "aaa", str = "unsafe: {unsafe_variable}")]
133140
struct StructRenamingWithStrFormatter2 {
134141
unsafe_variable: usize,
135142
}
136143

137-
#[pyclass(str="unsafe: {unsafe_variable}")]
144+
#[pyclass(str = "unsafe: {unsafe_variable}")]
138145
struct StructRenamingWithStrFormatter3 {
139146
#[pyo3(name = "unsafe", get, set)]
140147
unsafe_variable: usize,
141148
}
142149

143-
#[pyclass(rename_all = "SCREAMING_SNAKE_CASE", str="{a_a}, {b_b}, {c_d_e}")]
150+
#[pyclass(rename_all = "SCREAMING_SNAKE_CASE", str = "{a_a}, {b_b}, {c_d_e}")]
144151
struct RenameAllVariantsStruct {
145152
a_a: u32,
146153
b_b: u32,
147154
c_d_e: String,
148155
}
149156

150-
#[pyclass(str="{:?}")]
157+
#[pyclass(str = "{:?}")]
151158
#[derive(Debug)]
152159
struct StructWithNoMember {
153160
a: String,
154161
b: String,
155162
}
156163

157-
#[pyclass(str="{}")]
164+
#[pyclass(str = "{}")]
158165
#[derive(Debug)]
159166
struct StructWithNoMember2 {
160167
a: String,
161168
b: String,
162169
}
163170

164-
#[pyclass(eq, str="Stuff...")]
171+
#[pyclass(eq, str = "Stuff...")]
165172
#[derive(Debug, PartialEq)]
166173
pub enum MyEnumInvalidStrFmt {
167174
Variant,

0 commit comments

Comments
 (0)