Skip to content

Commit c25c6f7

Browse files
authored
unify IntoPyObject/FromPyObject derive attributes (PyO3#5070)
1 parent 91b3825 commit c25c6f7

File tree

8 files changed

+302
-440
lines changed

8 files changed

+302
-440
lines changed

newsfragments/5070.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix compile failure with `#[derive(IntoPyObject, FromPyObject)]` when using `#[pyo3()]` options recognised by only one of the two derives.
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
use crate::attributes::{
2+
self, get_pyo3_options, CrateAttribute, DefaultAttribute, FromPyWithAttribute,
3+
IntoPyWithAttribute, RenameAllAttribute,
4+
};
5+
use proc_macro2::Span;
6+
use syn::parse::{Parse, ParseStream};
7+
use syn::spanned::Spanned;
8+
use syn::{parenthesized, Attribute, LitStr, Result, Token};
9+
10+
/// Attributes for deriving `FromPyObject`/`IntoPyObject` scoped on containers.
11+
pub enum ContainerAttribute {
12+
/// Treat the Container as a Wrapper, operate directly on its field
13+
Transparent(attributes::kw::transparent),
14+
/// Force every field to be extracted from item of source Python object.
15+
ItemAll(attributes::kw::from_item_all),
16+
/// Change the name of an enum variant in the generated error message.
17+
ErrorAnnotation(LitStr),
18+
/// Change the path for the pyo3 crate
19+
Crate(CrateAttribute),
20+
/// Converts the field idents according to the [RenamingRule](attributes::RenamingRule) before extraction
21+
RenameAll(RenameAllAttribute),
22+
}
23+
24+
impl Parse for ContainerAttribute {
25+
fn parse(input: ParseStream<'_>) -> Result<Self> {
26+
let lookahead = input.lookahead1();
27+
if lookahead.peek(attributes::kw::transparent) {
28+
let kw: attributes::kw::transparent = input.parse()?;
29+
Ok(ContainerAttribute::Transparent(kw))
30+
} else if lookahead.peek(attributes::kw::from_item_all) {
31+
let kw: attributes::kw::from_item_all = input.parse()?;
32+
Ok(ContainerAttribute::ItemAll(kw))
33+
} else if lookahead.peek(attributes::kw::annotation) {
34+
let _: attributes::kw::annotation = input.parse()?;
35+
let _: Token![=] = input.parse()?;
36+
input.parse().map(ContainerAttribute::ErrorAnnotation)
37+
} else if lookahead.peek(Token![crate]) {
38+
input.parse().map(ContainerAttribute::Crate)
39+
} else if lookahead.peek(attributes::kw::rename_all) {
40+
input.parse().map(ContainerAttribute::RenameAll)
41+
} else {
42+
Err(lookahead.error())
43+
}
44+
}
45+
}
46+
47+
#[derive(Default)]
48+
pub struct ContainerAttributes {
49+
/// Treat the Container as a Wrapper, operate directly on its field
50+
pub transparent: Option<attributes::kw::transparent>,
51+
/// Force every field to be extracted from item of source Python object.
52+
pub from_item_all: Option<attributes::kw::from_item_all>,
53+
/// Change the name of an enum variant in the generated error message.
54+
pub annotation: Option<syn::LitStr>,
55+
/// Change the path for the pyo3 crate
56+
pub krate: Option<CrateAttribute>,
57+
/// Converts the field idents according to the [RenamingRule](attributes::RenamingRule) before extraction
58+
pub rename_all: Option<RenameAllAttribute>,
59+
}
60+
61+
impl ContainerAttributes {
62+
pub fn from_attrs(attrs: &[Attribute]) -> Result<Self> {
63+
let mut options = ContainerAttributes::default();
64+
65+
for attr in attrs {
66+
if let Some(pyo3_attrs) = get_pyo3_options(attr)? {
67+
pyo3_attrs
68+
.into_iter()
69+
.try_for_each(|opt| options.set_option(opt))?;
70+
}
71+
}
72+
Ok(options)
73+
}
74+
75+
fn set_option(&mut self, option: ContainerAttribute) -> syn::Result<()> {
76+
macro_rules! set_option {
77+
($key:ident) => {
78+
{
79+
ensure_spanned!(
80+
self.$key.is_none(),
81+
$key.span() => concat!("`", stringify!($key), "` may only be specified once")
82+
);
83+
self.$key = Some($key);
84+
}
85+
};
86+
}
87+
88+
match option {
89+
ContainerAttribute::Transparent(transparent) => set_option!(transparent),
90+
ContainerAttribute::ItemAll(from_item_all) => set_option!(from_item_all),
91+
ContainerAttribute::ErrorAnnotation(annotation) => set_option!(annotation),
92+
ContainerAttribute::Crate(krate) => set_option!(krate),
93+
ContainerAttribute::RenameAll(rename_all) => set_option!(rename_all),
94+
}
95+
Ok(())
96+
}
97+
}
98+
99+
#[derive(Clone, Debug)]
100+
pub enum FieldGetter {
101+
GetItem(attributes::kw::item, Option<syn::Lit>),
102+
GetAttr(attributes::kw::attribute, Option<syn::LitStr>),
103+
}
104+
105+
impl FieldGetter {
106+
pub fn span(&self) -> Span {
107+
match self {
108+
FieldGetter::GetItem(item, _) => item.span,
109+
FieldGetter::GetAttr(attribute, _) => attribute.span,
110+
}
111+
}
112+
}
113+
114+
pub enum FieldAttribute {
115+
Getter(FieldGetter),
116+
FromPyWith(FromPyWithAttribute),
117+
IntoPyWith(IntoPyWithAttribute),
118+
Default(DefaultAttribute),
119+
}
120+
121+
impl Parse for FieldAttribute {
122+
fn parse(input: ParseStream<'_>) -> Result<Self> {
123+
let lookahead = input.lookahead1();
124+
if lookahead.peek(attributes::kw::attribute) {
125+
let attr_kw: attributes::kw::attribute = input.parse()?;
126+
if input.peek(syn::token::Paren) {
127+
let content;
128+
let _ = parenthesized!(content in input);
129+
let attr_name: LitStr = content.parse()?;
130+
if !content.is_empty() {
131+
return Err(content.error(
132+
"expected at most one argument: `attribute` or `attribute(\"name\")`",
133+
));
134+
}
135+
ensure_spanned!(
136+
!attr_name.value().is_empty(),
137+
attr_name.span() => "attribute name cannot be empty"
138+
);
139+
Ok(Self::Getter(FieldGetter::GetAttr(attr_kw, Some(attr_name))))
140+
} else {
141+
Ok(Self::Getter(FieldGetter::GetAttr(attr_kw, None)))
142+
}
143+
} else if lookahead.peek(attributes::kw::item) {
144+
let item_kw: attributes::kw::item = input.parse()?;
145+
if input.peek(syn::token::Paren) {
146+
let content;
147+
let _ = parenthesized!(content in input);
148+
let key = content.parse()?;
149+
if !content.is_empty() {
150+
return Err(
151+
content.error("expected at most one argument: `item` or `item(key)`")
152+
);
153+
}
154+
Ok(Self::Getter(FieldGetter::GetItem(item_kw, Some(key))))
155+
} else {
156+
Ok(Self::Getter(FieldGetter::GetItem(item_kw, None)))
157+
}
158+
} else if lookahead.peek(attributes::kw::from_py_with) {
159+
input.parse().map(Self::FromPyWith)
160+
} else if lookahead.peek(attributes::kw::into_py_with) {
161+
input.parse().map(FieldAttribute::IntoPyWith)
162+
} else if lookahead.peek(Token![default]) {
163+
input.parse().map(Self::Default)
164+
} else {
165+
Err(lookahead.error())
166+
}
167+
}
168+
}
169+
170+
#[derive(Clone, Debug, Default)]
171+
pub struct FieldAttributes {
172+
pub getter: Option<FieldGetter>,
173+
pub from_py_with: Option<FromPyWithAttribute>,
174+
pub into_py_with: Option<IntoPyWithAttribute>,
175+
pub default: Option<DefaultAttribute>,
176+
}
177+
178+
impl FieldAttributes {
179+
/// Extract the field attributes.
180+
pub fn from_attrs(attrs: &[Attribute]) -> Result<Self> {
181+
let mut options = FieldAttributes::default();
182+
183+
for attr in attrs {
184+
if let Some(pyo3_attrs) = get_pyo3_options(attr)? {
185+
pyo3_attrs
186+
.into_iter()
187+
.try_for_each(|opt| options.set_option(opt))?;
188+
}
189+
}
190+
Ok(options)
191+
}
192+
193+
fn set_option(&mut self, option: FieldAttribute) -> syn::Result<()> {
194+
macro_rules! set_option {
195+
($key:ident) => {
196+
set_option!($key, concat!("`", stringify!($key), "` may only be specified once"))
197+
};
198+
($key:ident, $msg: expr) => {{
199+
ensure_spanned!(
200+
self.$key.is_none(),
201+
$key.span() => $msg
202+
);
203+
self.$key = Some($key);
204+
}}
205+
}
206+
207+
match option {
208+
FieldAttribute::Getter(getter) => {
209+
set_option!(getter, "only one of `attribute` or `item` can be provided")
210+
}
211+
FieldAttribute::FromPyWith(from_py_with) => set_option!(from_py_with),
212+
FieldAttribute::IntoPyWith(into_py_with) => set_option!(into_py_with),
213+
FieldAttribute::Default(default) => set_option!(default),
214+
}
215+
Ok(())
216+
}
217+
}

0 commit comments

Comments
 (0)