pyo3_stub_gen_derive/gen_stub/parse_python/
pymethods.rs

1//! Parse Python class method stub syntax and generate MethodInfo
2
3use rustpython_parser::{ast, Parse};
4use syn::{Error, LitStr, Result, Type};
5
6use super::pyfunction::PythonFunctionStub;
7use super::{
8    build_parameters_from_ast, dedent, extract_deprecated_from_decorators, extract_docstring,
9    extract_return_type, has_overload_decorator,
10};
11use crate::gen_stub::{method::MethodInfo, method::MethodType, pymethods::PyMethodsInfo};
12
13/// Intermediate representation for Python method stub
14pub struct PythonMethodStub {
15    pub func_stub: PythonFunctionStub,
16    pub method_type: MethodType,
17}
18
19impl TryFrom<PythonMethodStub> for MethodInfo {
20    type Error = syn::Error;
21
22    fn try_from(stub: PythonMethodStub) -> Result<Self> {
23        let func_name = stub.func_stub.func_def.name.to_string();
24
25        // Extract docstring
26        let doc = extract_docstring(&stub.func_stub.func_def);
27
28        // Build Parameters directly from Python AST with proper kind classification
29        let parameters =
30            build_parameters_from_ast(&stub.func_stub.func_def.args, &stub.func_stub.imports)?;
31
32        // For instance/class/new methods, the first parameter (self/cls) is handled by Python's AST
33        // but we need to skip it in our parameters list since it's implied by the method type
34        // (The build_parameters_from_ast already skips 'self', so no additional filtering needed)
35
36        // Extract return type
37        let return_type =
38            extract_return_type(&stub.func_stub.func_def.returns, &stub.func_stub.imports)?;
39
40        // Try to extract deprecated decorator
41        let deprecated =
42            extract_deprecated_from_decorators(&stub.func_stub.func_def.decorator_list);
43
44        // Construct MethodInfo
45        Ok(MethodInfo {
46            name: func_name,
47            parameters,
48            r#return: return_type,
49            doc,
50            r#type: stub.method_type,
51            is_async: stub.func_stub.is_async,
52            deprecated,
53            type_ignored: None,
54            is_overload: stub.func_stub.is_overload,
55        })
56    }
57}
58
59/// Intermediate representation for Python class stub (for methods)
60pub struct PythonClassStub {
61    pub class_def: ast::StmtClassDef,
62    pub imports: Vec<String>,
63}
64
65impl PythonClassStub {
66    /// Parse Python class definition from a literal string
67    pub fn new(input: &LitStr) -> Result<Self> {
68        let stub_content = input.value();
69
70        // Remove common indentation to allow indented Python code in raw strings
71        let dedented_content = dedent(&stub_content);
72
73        // Parse Python code using rustpython-parser
74        let parsed = ast::Suite::parse(&dedented_content, "<stub>")
75            .map_err(|e| Error::new(input.span(), format!("Failed to parse Python stub: {}", e)))?;
76
77        // Extract imports and class definition
78        let mut imports = Vec::new();
79        let mut class_def: Option<ast::StmtClassDef> = None;
80
81        for stmt in parsed {
82            match stmt {
83                ast::Stmt::Import(import_stmt) => {
84                    for alias in &import_stmt.names {
85                        imports.push(alias.name.to_string());
86                    }
87                }
88                ast::Stmt::ImportFrom(import_from_stmt) => {
89                    if let Some(module) = &import_from_stmt.module {
90                        imports.push(module.to_string());
91                    }
92                }
93                ast::Stmt::ClassDef(cls_def) => {
94                    if class_def.is_some() {
95                        return Err(Error::new(
96                            input.span(),
97                            "Multiple class definitions found. Only one class is allowed per gen_methods_from_python! call",
98                        ));
99                    }
100                    class_def = Some(cls_def);
101                }
102                _ => {
103                    // Ignore other statements
104                }
105            }
106        }
107
108        // Check that exactly one class is defined
109        let class_def = class_def
110            .ok_or_else(|| Error::new(input.span(), "No class definition found in Python stub"))?;
111
112        Ok(Self { class_def, imports })
113    }
114}
115
116impl TryFrom<PythonClassStub> for PyMethodsInfo {
117    type Error = syn::Error;
118
119    fn try_from(stub: PythonClassStub) -> Result<Self> {
120        let class_name = stub.class_def.name.to_string();
121        let mut methods = Vec::new();
122
123        // Extract methods from class body
124        for stmt in &stub.class_def.body {
125            match stmt {
126                ast::Stmt::FunctionDef(func_def) => {
127                    // Determine method type
128                    let method_type = determine_method_type(func_def, &func_def.args);
129
130                    // Check if method has @overload decorator
131                    let is_overload = has_overload_decorator(&func_def.decorator_list);
132
133                    // Create PythonFunctionStub
134                    let func_stub = PythonFunctionStub {
135                        func_def: func_def.clone(),
136                        imports: stub.imports.clone(),
137                        is_async: false,
138                        is_overload,
139                    };
140
141                    // Create PythonMethodStub and convert to MethodInfo
142                    let method_stub = PythonMethodStub {
143                        func_stub,
144                        method_type,
145                    };
146                    let method = MethodInfo::try_from(method_stub)?;
147                    methods.push(method);
148                }
149                ast::Stmt::AsyncFunctionDef(func_def) => {
150                    // Check if method has @overload decorator
151                    let is_overload = has_overload_decorator(&func_def.decorator_list);
152
153                    // Convert AsyncFunctionDef to FunctionDef for uniform processing
154                    let sync_func = ast::StmtFunctionDef {
155                        range: func_def.range,
156                        name: func_def.name.clone(),
157                        type_params: func_def.type_params.clone(),
158                        args: func_def.args.clone(),
159                        body: func_def.body.clone(),
160                        decorator_list: func_def.decorator_list.clone(),
161                        returns: func_def.returns.clone(),
162                        type_comment: func_def.type_comment.clone(),
163                    };
164
165                    // Determine method type
166                    let method_type = determine_method_type(&sync_func, &sync_func.args);
167
168                    // Create PythonFunctionStub
169                    let func_stub = PythonFunctionStub {
170                        func_def: sync_func,
171                        imports: stub.imports.clone(),
172                        is_async: true,
173                        is_overload,
174                    };
175
176                    // Create PythonMethodStub and convert to MethodInfo
177                    let method_stub = PythonMethodStub {
178                        func_stub,
179                        method_type,
180                    };
181                    let method = MethodInfo::try_from(method_stub)?;
182                    methods.push(method);
183                }
184                _ => {
185                    // Ignore other statements (e.g., docstrings, pass)
186                }
187            }
188        }
189
190        if methods.is_empty() {
191            return Err(Error::new(
192                proc_macro2::Span::call_site(),
193                "No method definitions found in class body",
194            ));
195        }
196
197        // Parse class name as Type
198        let struct_id: Type = syn::parse_str(&class_name).map_err(|e| {
199            Error::new(
200                proc_macro2::Span::call_site(),
201                format!("Failed to parse class name '{}': {}", class_name, e),
202            )
203        })?;
204
205        Ok(PyMethodsInfo {
206            struct_id,
207            attrs: Vec::new(),
208            getters: Vec::new(),
209            setters: Vec::new(),
210            methods,
211        })
212    }
213}
214
215/// Parse Python class definition and return PyMethodsInfo
216pub fn parse_python_methods_stub(input: &LitStr) -> Result<PyMethodsInfo> {
217    let stub = PythonClassStub::new(input)?;
218    PyMethodsInfo::try_from(stub).map_err(|e| Error::new(input.span(), format!("{}", e)))
219}
220
221/// Determine method type from decorators and arguments
222fn determine_method_type(func_def: &ast::StmtFunctionDef, args: &ast::Arguments) -> MethodType {
223    // Check for @staticmethod decorator
224    for decorator in &func_def.decorator_list {
225        if let ast::Expr::Name(name) = decorator {
226            match name.id.as_str() {
227                "staticmethod" => return MethodType::Static,
228                "classmethod" => return MethodType::Class,
229                _ => {}
230            }
231        }
232    }
233
234    // Check if it's __new__ (constructor)
235    if func_def.name.as_str() == "__new__" {
236        return MethodType::New;
237    }
238
239    // Check first argument to determine if it's instance/class method
240    if let Some(first_arg) = args.args.first() {
241        let arg_name = first_arg.def.arg.as_str();
242        if arg_name == "self" {
243            return MethodType::Instance;
244        } else if arg_name == "cls" {
245            return MethodType::Class;
246        }
247    }
248
249    // Default to instance method
250    MethodType::Instance
251}
252
253#[cfg(test)]
254mod test {
255    use super::*;
256    use proc_macro2::TokenStream as TokenStream2;
257    use quote::{quote, ToTokens};
258
259    #[test]
260    fn test_single_method_class() -> Result<()> {
261        let stub_str: LitStr = syn::parse2(quote! {
262            r#"
263            class Incrementer:
264                def increment(self, x: int) -> int:
265                    """Increment by one"""
266            "#
267        })?;
268        let py_methods_info = parse_python_methods_stub(&stub_str)?;
269        assert_eq!(py_methods_info.methods.len(), 1);
270
271        let out = py_methods_info.methods[0].to_token_stream();
272        insta::assert_snapshot!(format_as_value(out), @r#"
273        ::pyo3_stub_gen::type_info::MethodInfo {
274            name: "increment",
275            parameters: &[
276                ::pyo3_stub_gen::type_info::ParameterInfo {
277                    name: "x",
278                    kind: ::pyo3_stub_gen::type_info::ParameterKind::PositionalOrKeyword,
279                    type_info: || ::pyo3_stub_gen::TypeInfo {
280                        name: "int".to_string(),
281                        source_module: None,
282                        import: ::std::collections::HashSet::from([]),
283                        type_refs: ::std::collections::HashMap::new(),
284                    },
285                    default: ::pyo3_stub_gen::type_info::ParameterDefault::None,
286                },
287            ],
288            r#return: || ::pyo3_stub_gen::TypeInfo {
289                name: "int".to_string(),
290                source_module: None,
291                import: ::std::collections::HashSet::from([]),
292                type_refs: ::std::collections::HashMap::new(),
293            },
294            doc: "Increment by one",
295            r#type: ::pyo3_stub_gen::type_info::MethodType::Instance,
296            is_async: false,
297            deprecated: None,
298            type_ignored: None,
299            is_overload: false,
300        }
301        "#);
302        Ok(())
303    }
304
305    #[test]
306    fn test_multiple_methods_class() -> Result<()> {
307        let stub_str: LitStr = syn::parse2(quote! {
308            r#"
309            class Incrementer:
310                def increment_1(self, x: int) -> int:
311                    """First method"""
312
313                def increment_2(self, x: float) -> float:
314                    """Second method"""
315            "#
316        })?;
317        let py_methods_info = parse_python_methods_stub(&stub_str)?;
318        assert_eq!(py_methods_info.methods.len(), 2);
319
320        assert_eq!(py_methods_info.methods[0].name, "increment_1");
321        assert_eq!(py_methods_info.methods[1].name, "increment_2");
322        Ok(())
323    }
324
325    #[test]
326    fn test_static_method_in_class() -> Result<()> {
327        let stub_str: LitStr = syn::parse2(quote! {
328            r#"
329            class MyClass:
330                @staticmethod
331                def create(name: str) -> str:
332                    """Create something"""
333            "#
334        })?;
335        let py_methods_info = parse_python_methods_stub(&stub_str)?;
336        assert_eq!(py_methods_info.methods.len(), 1);
337
338        let out = py_methods_info.methods[0].to_token_stream();
339        insta::assert_snapshot!(format_as_value(out), @r#"
340        ::pyo3_stub_gen::type_info::MethodInfo {
341            name: "create",
342            parameters: &[
343                ::pyo3_stub_gen::type_info::ParameterInfo {
344                    name: "name",
345                    kind: ::pyo3_stub_gen::type_info::ParameterKind::PositionalOrKeyword,
346                    type_info: || ::pyo3_stub_gen::TypeInfo {
347                        name: "str".to_string(),
348                        source_module: None,
349                        import: ::std::collections::HashSet::from([]),
350                        type_refs: ::std::collections::HashMap::new(),
351                    },
352                    default: ::pyo3_stub_gen::type_info::ParameterDefault::None,
353                },
354            ],
355            r#return: || ::pyo3_stub_gen::TypeInfo {
356                name: "str".to_string(),
357                source_module: None,
358                import: ::std::collections::HashSet::from([]),
359                type_refs: ::std::collections::HashMap::new(),
360            },
361            doc: "Create something",
362            r#type: ::pyo3_stub_gen::type_info::MethodType::Static,
363            is_async: false,
364            deprecated: None,
365            type_ignored: None,
366            is_overload: false,
367        }
368        "#);
369        Ok(())
370    }
371
372    #[test]
373    fn test_class_method_in_class() -> Result<()> {
374        let stub_str: LitStr = syn::parse2(quote! {
375            r#"
376            class MyClass:
377                @classmethod
378                def from_string(cls, s: str) -> int:
379                    """Create from string"""
380            "#
381        })?;
382        let py_methods_info = parse_python_methods_stub(&stub_str)?;
383        assert_eq!(py_methods_info.methods.len(), 1);
384
385        let out = py_methods_info.methods[0].to_token_stream();
386        insta::assert_snapshot!(format_as_value(out), @r#"
387        ::pyo3_stub_gen::type_info::MethodInfo {
388            name: "from_string",
389            parameters: &[
390                ::pyo3_stub_gen::type_info::ParameterInfo {
391                    name: "s",
392                    kind: ::pyo3_stub_gen::type_info::ParameterKind::PositionalOrKeyword,
393                    type_info: || ::pyo3_stub_gen::TypeInfo {
394                        name: "str".to_string(),
395                        source_module: None,
396                        import: ::std::collections::HashSet::from([]),
397                        type_refs: ::std::collections::HashMap::new(),
398                    },
399                    default: ::pyo3_stub_gen::type_info::ParameterDefault::None,
400                },
401            ],
402            r#return: || ::pyo3_stub_gen::TypeInfo {
403                name: "int".to_string(),
404                source_module: None,
405                import: ::std::collections::HashSet::from([]),
406                type_refs: ::std::collections::HashMap::new(),
407            },
408            doc: "Create from string",
409            r#type: ::pyo3_stub_gen::type_info::MethodType::Class,
410            is_async: false,
411            deprecated: None,
412            type_ignored: None,
413            is_overload: false,
414        }
415        "#);
416        Ok(())
417    }
418
419    #[test]
420    fn test_new_method_in_class() -> Result<()> {
421        let stub_str: LitStr = syn::parse2(quote! {
422            r#"
423            class MyClass:
424                def __new__(cls) -> object:
425                    """Constructor"""
426            "#
427        })?;
428        let py_methods_info = parse_python_methods_stub(&stub_str)?;
429        assert_eq!(py_methods_info.methods.len(), 1);
430
431        let out = py_methods_info.methods[0].to_token_stream();
432        insta::assert_snapshot!(format_as_value(out), @r#"
433        ::pyo3_stub_gen::type_info::MethodInfo {
434            name: "__new__",
435            parameters: &[],
436            r#return: || ::pyo3_stub_gen::TypeInfo {
437                name: "object".to_string(),
438                source_module: None,
439                import: ::std::collections::HashSet::from([]),
440                type_refs: ::std::collections::HashMap::new(),
441            },
442            doc: "Constructor",
443            r#type: ::pyo3_stub_gen::type_info::MethodType::New,
444            is_async: false,
445            deprecated: None,
446            type_ignored: None,
447            is_overload: false,
448        }
449        "#);
450        Ok(())
451    }
452
453    #[test]
454    fn test_method_with_imports_in_class() -> Result<()> {
455        let stub_str: LitStr = syn::parse2(quote! {
456            r#"
457            import typing
458            from collections.abc import Callable
459
460            class MyClass:
461                def process(self, func: Callable[[str], int]) -> typing.Optional[int]:
462                    """Process a callback"""
463            "#
464        })?;
465        let py_methods_info = parse_python_methods_stub(&stub_str)?;
466        assert_eq!(py_methods_info.methods.len(), 1);
467
468        let out = py_methods_info.methods[0].to_token_stream();
469        insta::assert_snapshot!(format_as_value(out), @r#"
470        ::pyo3_stub_gen::type_info::MethodInfo {
471            name: "process",
472            parameters: &[
473                ::pyo3_stub_gen::type_info::ParameterInfo {
474                    name: "func",
475                    kind: ::pyo3_stub_gen::type_info::ParameterKind::PositionalOrKeyword,
476                    type_info: || ::pyo3_stub_gen::TypeInfo {
477                        name: "Callable[[str], int]".to_string(),
478                        source_module: None,
479                        import: ::std::collections::HashSet::from([
480                            "typing".into(),
481                            "collections.abc".into(),
482                        ]),
483                        type_refs: ::std::collections::HashMap::new(),
484                    },
485                    default: ::pyo3_stub_gen::type_info::ParameterDefault::None,
486                },
487            ],
488            r#return: || ::pyo3_stub_gen::TypeInfo {
489                name: "typing.Optional[int]".to_string(),
490                source_module: None,
491                import: ::std::collections::HashSet::from([
492                    "typing".into(),
493                    "collections.abc".into(),
494                ]),
495                type_refs: ::std::collections::HashMap::new(),
496            },
497            doc: "Process a callback",
498            r#type: ::pyo3_stub_gen::type_info::MethodType::Instance,
499            is_async: false,
500            deprecated: None,
501            type_ignored: None,
502            is_overload: false,
503        }
504        "#);
505        Ok(())
506    }
507
508    #[test]
509    fn test_async_method_in_class() -> Result<()> {
510        let stub_str: LitStr = syn::parse2(quote! {
511            r#"
512            class MyClass:
513                async def fetch_data(self, url: str) -> str:
514                    """Fetch data asynchronously"""
515            "#
516        })?;
517        let py_methods_info = parse_python_methods_stub(&stub_str)?;
518        assert_eq!(py_methods_info.methods.len(), 1);
519
520        let out = py_methods_info.methods[0].to_token_stream();
521        insta::assert_snapshot!(format_as_value(out), @r#"
522        ::pyo3_stub_gen::type_info::MethodInfo {
523            name: "fetch_data",
524            parameters: &[
525                ::pyo3_stub_gen::type_info::ParameterInfo {
526                    name: "url",
527                    kind: ::pyo3_stub_gen::type_info::ParameterKind::PositionalOrKeyword,
528                    type_info: || ::pyo3_stub_gen::TypeInfo {
529                        name: "str".to_string(),
530                        source_module: None,
531                        import: ::std::collections::HashSet::from([]),
532                        type_refs: ::std::collections::HashMap::new(),
533                    },
534                    default: ::pyo3_stub_gen::type_info::ParameterDefault::None,
535                },
536            ],
537            r#return: || ::pyo3_stub_gen::TypeInfo {
538                name: "str".to_string(),
539                source_module: None,
540                import: ::std::collections::HashSet::from([]),
541                type_refs: ::std::collections::HashMap::new(),
542            },
543            doc: "Fetch data asynchronously",
544            r#type: ::pyo3_stub_gen::type_info::MethodType::Instance,
545            is_async: true,
546            deprecated: None,
547            type_ignored: None,
548            is_overload: false,
549        }
550        "#);
551        Ok(())
552    }
553
554    #[test]
555    fn test_rust_type_marker_in_method() -> Result<()> {
556        let stub_str: LitStr = syn::parse2(quote! {
557            r#"
558            class PyProblem:
559                def __iadd__(self, other: pyo3_stub_gen.RustType["SomeRustType"]) -> pyo3_stub_gen.RustType["PyProblem"]:
560                    """In-place addition using Rust type marker"""
561            "#
562        })?;
563        let py_methods_info = parse_python_methods_stub(&stub_str)?;
564        assert_eq!(py_methods_info.methods.len(), 1);
565
566        let out = py_methods_info.methods[0].to_token_stream();
567        insta::assert_snapshot!(format_as_value(out), @r###"
568        ::pyo3_stub_gen::type_info::MethodInfo {
569            name: "__iadd__",
570            parameters: &[
571                ::pyo3_stub_gen::type_info::ParameterInfo {
572                    name: "other",
573                    kind: ::pyo3_stub_gen::type_info::ParameterKind::PositionalOrKeyword,
574                    type_info: <SomeRustType as ::pyo3_stub_gen::PyStubType>::type_input,
575                    default: ::pyo3_stub_gen::type_info::ParameterDefault::None,
576                },
577            ],
578            r#return: <PyProblem as pyo3_stub_gen::PyStubType>::type_output,
579            doc: "In-place addition using Rust type marker",
580            r#type: ::pyo3_stub_gen::type_info::MethodType::Instance,
581            is_async: false,
582            deprecated: None,
583            type_ignored: None,
584            is_overload: false,
585        }
586        "###);
587        Ok(())
588    }
589
590    #[test]
591    fn test_keyword_only_params_with_defaults() -> Result<()> {
592        let stub_str: LitStr = syn::parse2(quote! {
593            r#"
594            import builtins
595            import typing
596
597            class Placeholder:
598                def configure(
599                    self,
600                    name: builtins.str,
601                    *,
602                    dtype: builtins.str,
603                    ndim: builtins.int,
604                    shape: typing.Optional[builtins.str],
605                    jagged: builtins.bool = False,
606                    latex: typing.Optional[builtins.str] = None,
607                    description: typing.Optional[builtins.str] = None,
608                ) -> pyo3_stub_gen.RustType["Placeholder"]:
609                    """
610                    Configure placeholder with keyword-only parameters.
611
612                    This demonstrates keyword-only parameters (after *) which should be
613                    preserved in the generated stub file.
614                    """
615            "#
616        })?;
617        let py_methods_info = parse_python_methods_stub(&stub_str)?;
618        assert_eq!(py_methods_info.methods.len(), 1);
619
620        let out = py_methods_info.to_token_stream();
621        insta::assert_snapshot!(format_as_value(out), @r#"
622        ::pyo3_stub_gen::type_info::PyMethodsInfo {
623            struct_id: std::any::TypeId::of::<Placeholder>,
624            attrs: &[],
625            getters: &[],
626            setters: &[],
627            methods: &[
628                ::pyo3_stub_gen::type_info::MethodInfo {
629                    name: "configure",
630                    parameters: &[
631                        ::pyo3_stub_gen::type_info::ParameterInfo {
632                            name: "name",
633                            kind: ::pyo3_stub_gen::type_info::ParameterKind::PositionalOrKeyword,
634                            type_info: || ::pyo3_stub_gen::TypeInfo {
635                                name: "builtins.str".to_string(),
636                                source_module: None,
637                                import: ::std::collections::HashSet::from([
638                                    "builtins".into(),
639                                    "typing".into(),
640                                ]),
641                                type_refs: ::std::collections::HashMap::new(),
642                            },
643                            default: ::pyo3_stub_gen::type_info::ParameterDefault::None,
644                        },
645                        ::pyo3_stub_gen::type_info::ParameterInfo {
646                            name: "dtype",
647                            kind: ::pyo3_stub_gen::type_info::ParameterKind::KeywordOnly,
648                            type_info: || ::pyo3_stub_gen::TypeInfo {
649                                name: "builtins.str".to_string(),
650                                source_module: None,
651                                import: ::std::collections::HashSet::from([
652                                    "builtins".into(),
653                                    "typing".into(),
654                                ]),
655                                type_refs: ::std::collections::HashMap::new(),
656                            },
657                            default: ::pyo3_stub_gen::type_info::ParameterDefault::None,
658                        },
659                        ::pyo3_stub_gen::type_info::ParameterInfo {
660                            name: "ndim",
661                            kind: ::pyo3_stub_gen::type_info::ParameterKind::KeywordOnly,
662                            type_info: || ::pyo3_stub_gen::TypeInfo {
663                                name: "builtins.int".to_string(),
664                                source_module: None,
665                                import: ::std::collections::HashSet::from([
666                                    "builtins".into(),
667                                    "typing".into(),
668                                ]),
669                                type_refs: ::std::collections::HashMap::new(),
670                            },
671                            default: ::pyo3_stub_gen::type_info::ParameterDefault::None,
672                        },
673                        ::pyo3_stub_gen::type_info::ParameterInfo {
674                            name: "shape",
675                            kind: ::pyo3_stub_gen::type_info::ParameterKind::KeywordOnly,
676                            type_info: || ::pyo3_stub_gen::TypeInfo {
677                                name: "typing.Optional[builtins.str]".to_string(),
678                                source_module: None,
679                                import: ::std::collections::HashSet::from([
680                                    "builtins".into(),
681                                    "typing".into(),
682                                ]),
683                                type_refs: ::std::collections::HashMap::new(),
684                            },
685                            default: ::pyo3_stub_gen::type_info::ParameterDefault::None,
686                        },
687                        ::pyo3_stub_gen::type_info::ParameterInfo {
688                            name: "jagged",
689                            kind: ::pyo3_stub_gen::type_info::ParameterKind::KeywordOnly,
690                            type_info: || ::pyo3_stub_gen::TypeInfo {
691                                name: "builtins.bool".to_string(),
692                                source_module: None,
693                                import: ::std::collections::HashSet::from([
694                                    "builtins".into(),
695                                    "typing".into(),
696                                ]),
697                                type_refs: ::std::collections::HashMap::new(),
698                            },
699                            default: ::pyo3_stub_gen::type_info::ParameterDefault::Expr({
700                                fn _fmt() -> String {
701                                    "False".to_string()
702                                }
703                                _fmt
704                            }),
705                        },
706                        ::pyo3_stub_gen::type_info::ParameterInfo {
707                            name: "latex",
708                            kind: ::pyo3_stub_gen::type_info::ParameterKind::KeywordOnly,
709                            type_info: || ::pyo3_stub_gen::TypeInfo {
710                                name: "typing.Optional[builtins.str]".to_string(),
711                                source_module: None,
712                                import: ::std::collections::HashSet::from([
713                                    "builtins".into(),
714                                    "typing".into(),
715                                ]),
716                                type_refs: ::std::collections::HashMap::new(),
717                            },
718                            default: ::pyo3_stub_gen::type_info::ParameterDefault::Expr({
719                                fn _fmt() -> String {
720                                    "None".to_string()
721                                }
722                                _fmt
723                            }),
724                        },
725                        ::pyo3_stub_gen::type_info::ParameterInfo {
726                            name: "description",
727                            kind: ::pyo3_stub_gen::type_info::ParameterKind::KeywordOnly,
728                            type_info: || ::pyo3_stub_gen::TypeInfo {
729                                name: "typing.Optional[builtins.str]".to_string(),
730                                source_module: None,
731                                import: ::std::collections::HashSet::from([
732                                    "builtins".into(),
733                                    "typing".into(),
734                                ]),
735                                type_refs: ::std::collections::HashMap::new(),
736                            },
737                            default: ::pyo3_stub_gen::type_info::ParameterDefault::Expr({
738                                fn _fmt() -> String {
739                                    "None".to_string()
740                                }
741                                _fmt
742                            }),
743                        },
744                    ],
745                    r#return: <Placeholder as pyo3_stub_gen::PyStubType>::type_output,
746                    doc: "\n        Configure placeholder with keyword-only parameters.\n\n        This demonstrates keyword-only parameters (after *) which should be\n        preserved in the generated stub file.\n        ",
747                    r#type: ::pyo3_stub_gen::type_info::MethodType::Instance,
748                    is_async: false,
749                    deprecated: None,
750                    type_ignored: None,
751                    is_overload: false,
752                },
753            ],
754            file: file!(),
755            line: line!(),
756            column: column!(),
757        }
758        "#);
759        Ok(())
760    }
761
762    fn format_as_value(tt: TokenStream2) -> String {
763        let ttt = quote! { const _: () = #tt; };
764        let formatted = prettyplease::unparse(&syn::parse_file(&ttt.to_string()).unwrap());
765        formatted
766            .trim()
767            .strip_prefix("const _: () = ")
768            .unwrap()
769            .strip_suffix(';')
770            .unwrap()
771            .to_string()
772    }
773}