pyo3_stub_gen_derive/gen_stub/
parse_python.rs

1//! Parse Python stub syntax and generate PyFunctionInfo and MethodInfo
2//!
3//! This module provides functionality to parse Python stub syntax (type hints)
4//! and convert them into Rust metadata structures for stub generation.
5
6mod pyfunction;
7mod pymethods;
8
9pub use pyfunction::parse_python_function_stub;
10pub use pymethods::parse_python_methods_stub;
11
12use indexmap::IndexSet;
13use rustpython_parser::ast;
14use syn::{Result, Type};
15
16use super::{arg::ArgInfo, attr::DeprecatedInfo, util::TypeOrOverride};
17
18/// Remove common leading whitespace from all lines (similar to Python's textwrap.dedent)
19fn dedent(text: &str) -> String {
20    let lines: Vec<&str> = text.lines().collect();
21
22    // Find the minimum indentation (ignoring empty lines)
23    let min_indent = lines
24        .iter()
25        .filter(|line| !line.trim().is_empty())
26        .map(|line| line.len() - line.trim_start().len())
27        .min()
28        .unwrap_or(0);
29
30    // Remove the common indentation from each line
31    lines
32        .iter()
33        .map(|line| {
34            if line.len() >= min_indent {
35                &line[min_indent..]
36            } else {
37                line
38            }
39        })
40        .collect::<Vec<_>>()
41        .join("\n")
42}
43
44/// Extract docstring from function definition
45fn extract_docstring(func_def: &ast::StmtFunctionDef) -> String {
46    if let Some(ast::Stmt::Expr(expr_stmt)) = func_def.body.first() {
47        if let ast::Expr::Constant(constant) = &*expr_stmt.value {
48            if let ast::Constant::Str(s) = &constant.value {
49                return s.to_string();
50            }
51        }
52    }
53    String::new()
54}
55
56/// Extract deprecated decorator information if present
57fn extract_deprecated_from_decorators(decorators: &[ast::Expr]) -> Option<DeprecatedInfo> {
58    for decorator in decorators {
59        // Check for @deprecated or @deprecated("message")
60        match decorator {
61            ast::Expr::Name(name) if name.id.as_str() == "deprecated" => {
62                return Some(DeprecatedInfo {
63                    since: None,
64                    note: None,
65                });
66            }
67            ast::Expr::Call(call) => {
68                if let ast::Expr::Name(name) = &*call.func {
69                    if name.id.as_str() == "deprecated" {
70                        // Try to extract the message from the first argument
71                        let note = call.args.first().and_then(|arg| match arg {
72                            ast::Expr::Constant(constant) => match &constant.value {
73                                ast::Constant::Str(s) => Some(s.to_string()),
74                                _ => None,
75                            },
76                            _ => None,
77                        });
78                        return Some(DeprecatedInfo { since: None, note });
79                    }
80                }
81            }
82            _ => {}
83        }
84    }
85    None
86}
87
88/// Extract arguments from function definition
89fn extract_args(args: &ast::Arguments, imports: &[String]) -> Result<Vec<ArgInfo>> {
90    let mut arg_infos = Vec::new();
91
92    // Dummy type for TypeOrOverride (not used in ToTokens for OverrideType)
93    let dummy_type: Type = syn::parse_str("()").unwrap();
94
95    // Process positional arguments
96    for arg in &args.args {
97        let arg_name = arg.def.arg.to_string();
98
99        // Skip 'self' argument
100        if arg_name == "self" {
101            continue;
102        }
103
104        let type_override = if let Some(annotation) = &arg.def.annotation {
105            type_annotation_to_type_override(annotation, imports, dummy_type.clone())?
106        } else {
107            // No type annotation - use Any
108            TypeOrOverride::OverrideType {
109                r#type: dummy_type.clone(),
110                type_repr: "typing.Any".to_string(),
111                imports: IndexSet::from(["typing".to_string()]),
112            }
113        };
114
115        arg_infos.push(ArgInfo {
116            name: arg_name,
117            r#type: type_override,
118        });
119    }
120
121    Ok(arg_infos)
122}
123
124/// Extract return type from function definition
125fn extract_return_type(
126    returns: &Option<Box<ast::Expr>>,
127    imports: &[String],
128) -> Result<Option<TypeOrOverride>> {
129    // Dummy type for TypeOrOverride (not used in ToTokens for OverrideType)
130    let dummy_type: Type = syn::parse_str("()").unwrap();
131
132    if let Some(return_annotation) = returns {
133        Ok(Some(type_annotation_to_type_override(
134            return_annotation,
135            imports,
136            dummy_type,
137        )?))
138    } else {
139        // No return type annotation - use None (void)
140        Ok(None)
141    }
142}
143
144/// Convert Python type annotation to TypeOrOverride
145fn type_annotation_to_type_override(
146    expr: &ast::Expr,
147    imports: &[String],
148    dummy_type: Type,
149) -> Result<TypeOrOverride> {
150    let type_str = expr_to_type_string(expr);
151
152    // Convert imports to IndexSet
153    let import_set: IndexSet<String> = imports.iter().map(|s| s.to_string()).collect();
154
155    Ok(TypeOrOverride::OverrideType {
156        r#type: dummy_type,
157        type_repr: type_str,
158        imports: import_set,
159    })
160}
161
162/// Convert Python expression to type string
163fn expr_to_type_string(expr: &ast::Expr) -> String {
164    expr_to_type_string_inner(expr, false)
165}
166
167/// Convert Python expression to type string with context
168fn expr_to_type_string_inner(expr: &ast::Expr, in_subscript: bool) -> String {
169    match expr {
170        ast::Expr::Name(name) => name.id.to_string(),
171        ast::Expr::Attribute(attr) => {
172            format!(
173                "{}.{}",
174                expr_to_type_string_inner(&attr.value, false),
175                attr.attr
176            )
177        }
178        ast::Expr::Subscript(subscript) => {
179            let base = expr_to_type_string_inner(&subscript.value, false);
180            let slice = expr_to_type_string_inner(&subscript.slice, true);
181            format!("{}[{}]", base, slice)
182        }
183        ast::Expr::List(list) => {
184            let elements: Vec<String> = list
185                .elts
186                .iter()
187                .map(|e| expr_to_type_string_inner(e, false))
188                .collect();
189            format!("[{}]", elements.join(", "))
190        }
191        ast::Expr::Tuple(tuple) => {
192            let elements: Vec<String> = tuple
193                .elts
194                .iter()
195                .map(|e| expr_to_type_string_inner(e, in_subscript))
196                .collect();
197            if in_subscript {
198                // In subscript context, preserve tuple structure without extra parentheses
199                elements.join(", ")
200            } else {
201                format!("({})", elements.join(", "))
202            }
203        }
204        ast::Expr::Constant(constant) => match &constant.value {
205            ast::Constant::Int(i) => i.to_string(),
206            ast::Constant::Str(s) => format!("\"{}\"", s),
207            ast::Constant::Bool(b) => b.to_string(),
208            ast::Constant::None => "None".to_string(),
209            _ => "Any".to_string(),
210        },
211        _ => "Any".to_string(),
212    }
213}