pyo3_stub_gen/generate/
method.rs

1use crate::generate::docstring::normalize_docstring;
2use crate::stub_type::ImportRef;
3use crate::{generate::*, rule_name::RuleName, type_info::*, TypeInfo};
4use itertools::Itertools;
5use std::{collections::HashSet, fmt};
6
7pub use crate::type_info::MethodType;
8
9/// Definition of a class method.
10#[derive(Debug, Clone, PartialEq)]
11pub struct MethodDef {
12    pub name: &'static str,
13    pub parameters: Parameters,
14    pub r#return: TypeInfo,
15    pub doc: &'static str,
16    pub r#type: MethodType,
17    pub is_async: bool,
18    pub deprecated: Option<DeprecatedInfo>,
19    pub type_ignored: Option<IgnoreTarget>,
20    /// Whether this method is marked as an overload variant
21    pub is_overload: bool,
22}
23
24impl Import for MethodDef {
25    fn import(&self) -> HashSet<ImportRef> {
26        let mut import = self.r#return.import.clone();
27        import.extend(self.parameters.import());
28        // Add typing_extensions import if deprecated
29        if self.deprecated.is_some() {
30            import.insert("typing_extensions".into());
31        }
32        import
33    }
34}
35
36impl From<&MethodInfo> for MethodDef {
37    fn from(info: &MethodInfo) -> Self {
38        let doc = if info.doc.is_empty() {
39            ""
40        } else {
41            Box::leak(normalize_docstring(info.doc).into_boxed_str())
42        };
43
44        Self {
45            name: info.name,
46            parameters: Parameters::from_infos(info.parameters),
47            r#return: (info.r#return)(),
48            doc,
49            r#type: info.r#type,
50            is_async: info.is_async,
51            deprecated: info.deprecated.clone(),
52            type_ignored: info.type_ignored,
53            is_overload: info.is_overload,
54        }
55    }
56}
57
58impl fmt::Display for MethodDef {
59    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
60        let indent = indent();
61        let async_ = if self.is_async { "async " } else { "" };
62
63        // Add deprecated decorator if present
64        if let Some(deprecated) = &self.deprecated {
65            writeln!(f, "{indent}{deprecated}")?;
66        }
67
68        let params_str = if self.parameters.is_empty() {
69            String::new()
70        } else {
71            format!(", {}", self.parameters)
72        };
73
74        match self.r#type {
75            MethodType::Static => {
76                writeln!(f, "{indent}@staticmethod")?;
77                write!(f, "{indent}{async_}def {}({})", self.name, self.parameters)?;
78            }
79            MethodType::Class | MethodType::New => {
80                if self.r#type == MethodType::Class {
81                    // new is a classmethod without the decorator
82                    writeln!(f, "{indent}@classmethod")?;
83                }
84                write!(f, "{indent}{async_}def {}(cls{})", self.name, params_str)?;
85            }
86            MethodType::Instance => {
87                write!(f, "{indent}{async_}def {}(self{})", self.name, params_str)?;
88            }
89        }
90        write!(f, " -> {}:", self.r#return)?;
91
92        // Calculate type: ignore comment once
93        let type_ignore_comment = if let Some(target) = &self.type_ignored {
94            match target {
95                IgnoreTarget::All => Some("  # type: ignore".to_string()),
96                IgnoreTarget::Specified(rules) => {
97                    let rules_str = rules
98                        .iter()
99                        .map(|r| {
100                            let result = r.parse::<RuleName>().unwrap();
101                            if let RuleName::Custom(custom) = &result {
102                                log::warn!("Unknown custom rule name '{custom}' used in type ignore. Ensure this is intended.");
103                            }
104                            result
105                        })
106                        .join(",");
107                    Some(format!("  # type: ignore[{rules_str}]"))
108                }
109            }
110        } else {
111            None
112        };
113
114        let doc = self.doc;
115        if !doc.is_empty() {
116            // Add type: ignore comment for methods with docstrings
117            if let Some(comment) = &type_ignore_comment {
118                write!(f, "{comment}")?;
119            }
120            writeln!(f)?;
121            let double_indent = format!("{indent}{indent}");
122            docstring::write_docstring(f, self.doc, &double_indent)?;
123        } else {
124            write!(f, " ...")?;
125            // Add type: ignore comment for methods without docstrings
126            if let Some(comment) = &type_ignore_comment {
127                write!(f, "{comment}")?;
128            }
129            writeln!(f)?;
130        }
131        Ok(())
132    }
133}
134
135impl MethodDef {
136    /// Format method with module-qualified type names
137    ///
138    /// This method uses the target module context to qualify type identifiers
139    /// within compound type expressions based on their source modules.
140    pub fn fmt_for_module(
141        &self,
142        target_module: &str,
143        f: &mut fmt::Formatter,
144        indent: &str,
145    ) -> fmt::Result {
146        let async_ = if self.is_async { "async " } else { "" };
147
148        // Add deprecated decorator if present
149        if let Some(deprecated) = &self.deprecated {
150            writeln!(f, "{indent}{deprecated}")?;
151        }
152
153        let params_str = self.parameters.fmt_for_module(target_module);
154        let return_type = self.r#return.qualified_for_module(target_module);
155
156        let params_with_separator = if params_str.is_empty() {
157            String::new()
158        } else {
159            format!(", {}", params_str)
160        };
161
162        match self.r#type {
163            MethodType::Static => {
164                writeln!(f, "{indent}@staticmethod")?;
165                write!(f, "{indent}{async_}def {}({})", self.name, params_str)?;
166            }
167            MethodType::Class | MethodType::New => {
168                if self.r#type == MethodType::Class {
169                    // new is a classmethod without the decorator
170                    writeln!(f, "{indent}@classmethod")?;
171                }
172                write!(
173                    f,
174                    "{indent}{async_}def {}(cls{})",
175                    self.name, params_with_separator
176                )?;
177            }
178            MethodType::Instance => {
179                write!(
180                    f,
181                    "{indent}{async_}def {}(self{})",
182                    self.name, params_with_separator
183                )?;
184            }
185        }
186        write!(f, " -> {}:", return_type)?;
187
188        // Calculate type: ignore comment once
189        let type_ignore_comment = if let Some(target) = &self.type_ignored {
190            match target {
191                IgnoreTarget::All => Some("  # type: ignore".to_string()),
192                IgnoreTarget::Specified(rules) => {
193                    let rules_str = rules
194                        .iter()
195                        .map(|r| {
196                            let result = r.parse::<RuleName>().unwrap();
197                            if let RuleName::Custom(custom) = &result {
198                                log::warn!("Unknown custom rule name '{custom}' used in type ignore. Ensure this is intended.");
199                            }
200                            result
201                        })
202                        .join(",");
203                    Some(format!("  # type: ignore[{rules_str}]"))
204                }
205            }
206        } else {
207            None
208        };
209
210        let doc = self.doc;
211        if !doc.is_empty() {
212            // Add type: ignore comment for methods with docstrings
213            if let Some(comment) = &type_ignore_comment {
214                write!(f, "{comment}")?;
215            }
216            writeln!(f)?;
217            let double_indent = format!("{indent}{indent}");
218            docstring::write_docstring(f, self.doc, &double_indent)?;
219        } else {
220            write!(f, " ...")?;
221            // Add type: ignore comment for methods without docstrings
222            if let Some(comment) = &type_ignore_comment {
223                write!(f, "{comment}")?;
224            }
225            writeln!(f)?;
226        }
227        Ok(())
228    }
229}