python_ast/ast/tree/
class_def.rs

1//! A lot of languages, Python included, have a concept of a class, which combines the definition of a data type with
2//! an interface. In dynamic languages like Python, the class itself is a memory object, that can be permutated at runtime,
3//! however, this is probably usually a bad idea. Classes can contain:
4//! 1. Methods (special functions)
5//! 2. properties (attributes of the data element)
6//! 3. Base classes (for inheritace)
7//! 4. static data
8//! 5. Additional classes
9//!
10//! There is one construct in Rust that can emcompass all of these things: a module. So, we use modules to model classes
11//! following these rules:
12//! 1. The module is given the name of the class. Contrary to other Rust modules, this is typically SnakeCase.
13//! 2. The main data type defined by the class is a struct inside the module, and called Data.
14//! 3. The Data struct can take two forms:
15//!   a. If the properties of the class can be fully inferred, Data will be a simple struct and the attributes will be defined as fields of the struct.
16//!   b. If the properties of the class cannot be fully inferred, such as if the class is accessed as a dictionary, Data will be a HashMap<String, _>,
17//!   and the values will be accessed through it.
18//! 4. Static data will be declared with lazy_static inside the module.
19//! 5. Additional classes will be nested inside the module, and therefore they appear as modules inside a module.
20//! 6. Each class also contains a trait, named Cls, which is used in inheritance.
21//! 7. Each method of the class in Python will be translated to have a prototype in Cls. If it is possible to implement the method as a default method,
22//! it will be, otherwise (if the method refers to attributes of the class), a prototype will be added to Cls, and the implementation will be done inside
23//! an impl Cls for Data block.
24//! 8. Cls will implement Clone, Default.
25
26use proc_macro2::TokenStream;
27use pyo3::FromPyObject;
28use quote::{format_ident, quote};
29
30use crate::{
31    CodeGen, CodeGenContext, ExprType, Name, PythonOptions, Statement, StatementType,
32    SymbolTableNode, SymbolTableScopes,
33};
34
35use log::debug;
36
37use serde::{Deserialize, Serialize};
38
39#[derive(Clone, Debug, Default, FromPyObject, Serialize, Deserialize, PartialEq)]
40pub struct ClassDef {
41    pub name: String,
42    pub bases: Vec<Name>,
43    pub keywords: Vec<String>,
44    pub body: Vec<Statement>,
45}
46
47impl CodeGen for ClassDef {
48    type Context = CodeGenContext;
49    type Options = PythonOptions;
50    type SymbolTable = SymbolTableScopes;
51
52    fn find_symbols(self, symbols: Self::SymbolTable) -> Self::SymbolTable {
53        let mut symbols = symbols;
54        symbols.insert(self.name.clone(), SymbolTableNode::ClassDef(self.clone()));
55        symbols
56    }
57
58    fn to_rust(
59        self,
60        _ctx: Self::Context,
61        options: Self::Options,
62        symbols: Self::SymbolTable,
63    ) -> Result<TokenStream, Box<dyn std::error::Error>> {
64        let mut streams = TokenStream::new();
65        let class_name = format_ident!("{}", self.name);
66
67        // The Python convention is that functions that begin with a single underscore,
68        // it's private. Otherwise, it's public. We formalize that by default.
69        let visibility = if self.name.starts_with("_") && !self.name.starts_with("__") {
70            format_ident!("")
71        } else if self.name.starts_with("__") && self.name.ends_with("__") {
72            format_ident!("pub(crate)")
73        } else {
74            format_ident!("pub")
75        };
76
77        // bases will be empty if there are no base classes, which prevents any base traits
78        // being added, and also prevents the : from being emitted.
79        let mut bases = TokenStream::new();
80        if self.bases.len() > 0 {
81            bases.extend(quote!(:));
82            let base_name = format_ident!("{}", self.bases[0].id);
83            bases.extend(quote!(#base_name::Cls));
84            for base in &self.bases[1..] {
85                bases.extend(quote!(+));
86                let base_name = format_ident!("{}", base.id);
87                bases.extend(quote!(#base_name));
88            }
89        }
90
91        for s in self.body.clone() {
92            streams.extend(
93                s.clone()
94                    .to_rust(CodeGenContext::Class, options.clone(), symbols.clone())
95                    .expect(format!("Failed to parse statement {:?}", s).as_str()),
96            );
97        }
98
99        let class = if let Some(docstring) = self.get_docstring() {
100            // Convert docstring to Rust doc comments
101            let doc_lines: Vec<_> = docstring
102                .lines()
103                .map(|line| {
104                    if line.trim().is_empty() {
105                        quote! { #[doc = ""] }
106                    } else {
107                        let doc_line = format!("{}", line);
108                        quote! { #[doc = #doc_line] }
109                    }
110                })
111                .collect();
112            
113            quote! {
114                #(#doc_lines)*
115                #visibility mod #class_name {
116                    use super::*;
117                    #visibility trait Cls #bases {
118                        #streams
119                    }
120                    #[derive(Clone, Default)]
121                    #visibility struct Data {
122
123                    }
124                    impl Cls for Data {}
125                }
126            }
127        } else {
128            quote! {
129                #visibility mod #class_name {
130                    use super::*;
131                    #visibility trait Cls #bases {
132                        #streams
133                    }
134                    #[derive(Clone, Default)]
135                    #visibility struct Data {
136
137                    }
138                    impl Cls for Data {}
139                }
140            }
141        };
142        debug!("class: {}", class);
143        Ok(class)
144    }
145}
146
147impl ClassDef {
148    fn get_docstring(&self) -> Option<String> {
149        if self.body.is_empty() {
150            return None;
151        }
152        
153        let expr = self.body[0].clone();
154        match expr.statement {
155            StatementType::Expr(e) => match e.value {
156                ExprType::Constant(c) => {
157                    let raw_string = c.to_string();
158                    // Clean up the docstring for Rust documentation
159                    Some(self.format_docstring(&raw_string))
160                },
161                _ => None,
162            },
163            _ => None,
164        }
165    }
166    
167    fn format_docstring(&self, raw: &str) -> String {
168        // Remove surrounding quotes
169        let content = raw.trim_matches('"');
170        
171        // Split into lines and clean up Python-style indentation
172        let lines: Vec<&str> = content.lines().collect();
173        if lines.is_empty() {
174            return String::new();
175        }
176        
177        // First line is usually the summary
178        let mut formatted = vec![lines[0].trim().to_string()];
179        
180        if lines.len() > 1 {
181            // Add empty line after summary if there are more lines
182            if !lines[0].trim().is_empty() && !lines[1].trim().is_empty() {
183                formatted.push(String::new());
184            }
185            
186            // Process remaining lines, cleaning up indentation
187            for line in lines.iter().skip(1) {
188                let cleaned = line.trim();
189                if !cleaned.is_empty() {
190                    formatted.push(cleaned.to_string());
191                }
192            }
193        }
194        
195        formatted.join("\n")
196    }
197}