1- use crate :: attributes:: { self , CrateAttribute , RenamingRule } ;
1+ use crate :: attributes:: { CrateAttribute , RenamingRule } ;
22use proc_macro2:: { Span , TokenStream } ;
33use quote:: { quote, ToTokens } ;
44use std:: ffi:: CString ;
5- use syn:: parse:: Parse ;
65use syn:: spanned:: Spanned ;
76use syn:: visit_mut:: VisitMut ;
87use syn:: { punctuated:: Punctuated , Token } ;
@@ -134,35 +133,10 @@ enum PythonDocKind {
134133enum 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