Skip to main content

python_ast/ast/tree/
attribute.rs

1use proc_macro2::TokenStream;
2use pyo3::{Borrowed, PyAny, FromPyObject, PyResult, prelude::PyAnyMethods, types::PyTypeMethods};
3use quote::quote;
4
5use crate::{extraction_failure, CodeGen, CodeGenContext, ExprType, PythonOptions, SymbolTableScopes};
6
7use serde::{Deserialize, Serialize};
8
9#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
10//#[pyo3(transparent)]
11pub struct Attribute {
12    pub value: Box<ExprType>,
13    pub attr: String,
14    ctx: String,
15}
16
17impl<'a, 'py> FromPyObject<'a, 'py> for Attribute {
18    type Error = pyo3::PyErr;
19    fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
20        let value = ob.getattr("value").map_err(|e| extraction_failure("Attribute.value", &ob, e))?;
21        let attr = ob.getattr("attr").map_err(|e| extraction_failure("Attribute.attr", &ob, e))?;
22        let ctx = ob
23            .getattr("ctx")
24            .map_err(|e| extraction_failure("attribute context", &ob, e))?
25            .get_type()
26            .name()
27            .map_err(|e| extraction_failure("attribute context type", &ob, e))?;
28        Ok(Attribute {
29            value: Box::new(value.extract().map_err(|e| extraction_failure("Attribute.value", &ob, e))?),
30            attr: attr.extract().map_err(|e| extraction_failure("Attribute.attr", &ob, e))?,
31            ctx: ctx.to_string(),
32        })
33    }
34}
35
36impl<'a> CodeGen for Attribute {
37    type Context = CodeGenContext;
38    type Options = PythonOptions;
39    type SymbolTable = SymbolTableScopes;
40
41    fn to_rust(
42        self,
43        ctx: Self::Context,
44        options: Self::Options,
45        symbols: Self::SymbolTable,
46    ) -> Result<TokenStream, Box<dyn std::error::Error>> {
47        let value_tokens = self.value.to_rust(ctx, options, symbols)?;
48        let value_str = value_tokens.to_string();
49        let attr = crate::safe_ident(&self.attr);
50        
51        // Determine if this is a module access or a field/method access
52        // Module names are typically lowercase and match Python stdlib modules
53        let is_module_access = matches!(value_str.as_str(),
54            "sys" | "os" | "subprocess" | "json" | "urllib" | "xml" | "asyncio" |
55            "time" | "math" | "random" | "heapq" | "functools" | "textwrap" | "itertools" | "re" | "hashlib" | "csv" | "io" |
56            // `datetime` covers both the runtime module and the datetime
57            // TYPE from `from datetime import datetime` — either way the
58            // attribute is a path item (datetime::strptime, datetime::now),
59            // never a field on a value.
60            "datetime" |
61            "os :: path" | "os::path" // for nested modules
62        );
63        
64        if is_module_access {
65            // Use :: for module access (Python's sys.executable becomes sys::executable)
66            // Special handling for LazyLock static variables that need
67            // dereferencing. os::environ is NOT here: it is a live-view
68            // unit struct whose methods auto-ref.
69            let needs_deref = matches!((value_str.as_str(), self.attr.as_str()),
70                ("sys", "executable") | ("sys", "argv")
71            );
72            
73            if needs_deref {
74                // Wrap dereferenced values in parentheses to ensure correct precedence
75                // This prevents *sys::executable.to_string() and ensures (*sys::executable).to_string()
76                Ok(quote!((*#value_tokens::#attr)))
77            } else {
78                Ok(quote!(#value_tokens::#attr))
79            }
80        } else {
81            // Use . for field/method access (Python's obj.field becomes obj.field)
82            Ok(quote!(#value_tokens.#attr))
83        }
84    }
85}