Skip to content

Commit b0b9ca4

Browse files
committed
Add embedded doc mode optimizations to attributes.rs and utils.rs
1 parent ee8ef94 commit b0b9ca4

File tree

2 files changed

+72
-105
lines changed

2 files changed

+72
-105
lines changed

pyo3-macros-backend/src/attributes.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,6 @@ pub type StrFormatterAttribute = OptionalKeywordAttribute<kw::str, StringFormatt
320320
pub type TextSignatureAttribute = KeywordAttribute<kw::text_signature, TextSignatureAttributeValue>;
321321
pub type SubmoduleAttribute = kw::submodule;
322322
pub type GILUsedAttribute = KeywordAttribute<kw::gil_used, LitBool>;
323-
pub type DocModeAttribute = KeywordAttribute<kw::doc_mode, LitStr>;
324323

325324
impl<K: Parse + std::fmt::Debug, V: Parse> Parse for KeywordAttribute<K, V> {
326325
fn parse(input: ParseStream<'_>) -> Result<Self> {

pyo3-macros-backend/src/utils.rs

Lines changed: 72 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
use crate::attributes::{self, CrateAttribute, RenamingRule};
1+
use crate::attributes::{CrateAttribute, RenamingRule};
22
use proc_macro2::{Span, TokenStream};
33
use quote::{quote, ToTokens};
44
use std::ffi::CString;
5-
use syn::parse::Parse;
65
use syn::spanned::Spanned;
76
use syn::visit_mut::VisitMut;
87
use syn::{punctuated::Punctuated, Token};
@@ -134,35 +133,10 @@ enum PythonDocKind {
134133
enum DocParseMode {
135134
/// Currently generating docs for both Python and Rust.
136135
Both,
137-
/// Currently generating docs for Python only, starting from the given Span.
138-
PythonOnly(Span),
139-
/// Currently generating docs for Rust only, starting from the given Span.
140-
RustOnly(Span),
141-
}
142-
143-
impl Parse for DocParseMode {
144-
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
145-
let lookahead = input.lookahead1();
146-
if lookahead.peek(attributes::kw::doc_mode) {
147-
let attribute: attributes::DocModeAttribute = input.parse()?;
148-
let lit = attribute.value;
149-
if lit.value() == "python" {
150-
Ok(DocParseMode::PythonOnly(lit.span()))
151-
} else if lit.value() == "rust" {
152-
Ok(DocParseMode::RustOnly(lit.span()))
153-
} else {
154-
Err(syn::Error::new(
155-
lit.span(),
156-
"expected `doc_mode = \"python\"` or `doc_mode = \"rust\"",
157-
))
158-
}
159-
} else if lookahead.peek(attributes::kw::end_doc_mode) {
160-
let _: attributes::kw::end_doc_mode = input.parse()?;
161-
Ok(DocParseMode::Both)
162-
} else {
163-
Err(lookahead.error())
164-
}
165-
}
136+
/// Currently generating docs for Python only.
137+
PythonOnly,
138+
/// Currently generating docs for Rust only.
139+
RustOnly,
166140
}
167141

168142
/// Collects all #[doc = "..."] attributes into a TokenStream evaluating to a null-terminated string.
@@ -188,93 +162,87 @@ pub fn get_doc(
188162

189163
let mut mode = DocParseMode::Both;
190164

191-
let mut error: Option<syn::Error> = None;
165+
let mut to_retain = vec![]; // Collect indices of attributes to retain
192166

193-
attrs.retain(|attr| {
167+
for (i, attr) in attrs.iter().enumerate() {
194168
if attr.path().is_ident("doc") {
195-
// if not in Rust-only mode, we process the doc attribute to add to the Python docstring
196-
if !matches!(mode, DocParseMode::RustOnly(_)) {
197-
if let Ok(nv) = attr.meta.require_name_value() {
198-
if !first {
199-
current_part.push('\n');
200-
} else {
201-
first = false;
169+
if let Ok(nv) = attr.meta.require_name_value() {
170+
let include_in_python;
171+
let retain_in_rust;
172+
173+
if let syn::Expr::Lit(syn::ExprLit {
174+
lit: syn::Lit::Str(lit_str),
175+
..
176+
}) = &nv.value
177+
{
178+
// Strip single left space from literal strings, if needed.
179+
let doc_line = lit_str.value();
180+
let stripped_line = doc_line.strip_prefix(' ').unwrap_or(&doc_line);
181+
let trimmed = stripped_line.trim();
182+
183+
// Check if this is a mode switch instruction
184+
if let Some(content) = trimmed.strip_prefix("<!--").and_then(|s| s.strip_suffix("-->")) {
185+
let content_trimmed = content.trim();
186+
if content_trimmed.starts_with("pyo3_doc_mode:") {
187+
let value = content_trimmed.strip_prefix("pyo3_doc_mode:").unwrap_or("").trim();
188+
mode = match value {
189+
"python" => DocParseMode::PythonOnly,
190+
"rust" => DocParseMode::RustOnly,
191+
"both" => DocParseMode::Both,
192+
_ => return Err(syn::Error::new(lit_str.span(), format!("Invalid doc_mode: '{}'. Expected 'python', 'rust', or 'both'.", value))),
193+
};
194+
// Do not retain mode switch lines in Rust, and skip in Python
195+
continue;
196+
}
197+
}
198+
199+
// Not a mode switch, decide based on current mode
200+
include_in_python = matches!(mode, DocParseMode::Both | DocParseMode::PythonOnly);
201+
retain_in_rust = matches!(mode, DocParseMode::Both | DocParseMode::RustOnly);
202+
203+
// Include in Python doc if needed
204+
if include_in_python {
205+
if !first {
206+
current_part.push('\n');
207+
} else {
208+
first = false;
209+
}
210+
current_part.push_str(stripped_line);
202211
}
203-
if let syn::Expr::Lit(syn::ExprLit {
204-
lit: syn::Lit::Str(lit_str),
205-
..
206-
}) = &nv.value
207-
{
208-
// Strip single left space from literal strings, if needed.
209-
// e.g. `/// Hello world` expands to #[doc = " Hello world"]
210-
let doc_line = lit_str.value();
211-
current_part.push_str(doc_line.strip_prefix(' ').unwrap_or(&doc_line));
212-
} else {
213-
// This is probably a macro doc from Rust 1.54, e.g. #[doc = include_str!(...)]
212+
} else {
213+
// This is probably a macro doc, e.g. #[doc = include_str!(...)]
214+
// Decide based on current mode
215+
include_in_python = matches!(mode, DocParseMode::Both | DocParseMode::PythonOnly);
216+
retain_in_rust = matches!(mode, DocParseMode::Both | DocParseMode::RustOnly);
217+
218+
// Include in Python doc if needed
219+
if include_in_python {
214220
// Reset the string buffer, write that part, and then push this macro part too.
215221
parts.push(current_part.to_token_stream());
216222
current_part.clear();
217223
parts.push(nv.value.to_token_stream());
218224
}
219225
}
220-
}
221-
// discard doc attributes if we're in PythonOnly mode
222-
!matches!(mode, DocParseMode::PythonOnly(_))
223-
} else if attr.path().is_ident("pyo3_doc_mode") {
224-
match attr.parse_args() {
225-
Ok(new_mode) => {
226-
mode = new_mode;
226+
227+
// Collect to retain if needed
228+
if retain_in_rust {
229+
to_retain.push(i);
227230
}
228-
Err(err) => match &mut error {
229-
Some(existing_error) => existing_error.combine(err),
230-
None => {
231-
error = Some(err);
232-
}
233-
},
234231
}
235-
// we processed these attributes, remove them
236-
false
237232
} else {
238-
true
239-
}
240-
});
241-
242-
// for attr in attrs {
243-
// if attr.path().is_ident("doc") {
244-
// if let Ok(nv) = attr.meta.require_name_value() {
245-
// if !first {
246-
// current_part.push('\n');
247-
// } else {
248-
// first = false;
249-
// }
250-
// if let syn::Expr::Lit(syn::ExprLit {
251-
// lit: syn::Lit::Str(lit_str),
252-
// ..
253-
// }) = &nv.value
254-
// {
255-
// // Strip single left space from literal strings, if needed.
256-
// // e.g. `/// Hello world` expands to #[doc = " Hello world"]
257-
// let doc_line = lit_str.value();
258-
// current_part.push_str(doc_line.strip_prefix(' ').unwrap_or(&doc_line));
259-
// } else {
260-
// // This is probably a macro doc from Rust 1.54, e.g. #[doc = include_str!(...)]
261-
// // Reset the string buffer, write that part, and then push this macro part too.
262-
// parts.push(current_part.to_token_stream());
263-
// current_part.clear();
264-
// parts.push(nv.value.to_token_stream());
265-
// }
266-
// }
267-
// }
268-
// }
269-
270-
// if the mode has not been ended, we return an error
271-
match mode {
272-
DocParseMode::Both => {}
273-
DocParseMode::PythonOnly(span) | DocParseMode::RustOnly(span) => {
274-
return Err(err_spanned!(span => "doc_mode must be ended with `end_doc_mode`"));
233+
// Non-doc attributes are always retained
234+
to_retain.push(i);
275235
}
276236
}
277237

238+
// Retain only the selected attributes
239+
*attrs = to_retain.into_iter().map(|i| attrs[i].clone()).collect();
240+
241+
// Check if mode ended in Both; if not, error to enforce "pairing"
242+
if !matches!(mode, DocParseMode::Both) {
243+
return Err(err_spanned!(Span::call_site() => "doc_mode did not end in 'both' mode; consider adding <!-- pyo3_doc_mode: both --> at the end"));
244+
}
245+
278246
if !parts.is_empty() {
279247
// Doc contained macro pieces - return as `concat!` expression
280248
if !current_part.is_empty() {
@@ -477,4 +445,4 @@ pub fn expr_to_python(expr: &syn::Expr) -> String {
477445
// others, unsupported yet so defaults to `...`
478446
_ => "...".to_string(),
479447
}
480-
}
448+
}

0 commit comments

Comments
 (0)