Skip to main content

python_ast/ast/tree/
import.rs

1use tracing::debug;
2use proc_macro2::TokenStream;
3use pyo3::FromPyObject;
4use quote::quote;
5use serde::{Deserialize, Serialize};
6
7use crate::{CodeGen, CodeGenContext, PythonOptions, SymbolTableNode, SymbolTableScopes};
8
9/// Python stdlib modules that the stdpython runtime crate provides. Imports
10/// of these resolve under the runtime crate; anything else is assumed to be
11/// a sibling module of the generated crate.
12pub(crate) fn is_stdpython_module(name: &str) -> bool {
13    matches!(
14        name,
15        "os" | "sys"
16            | "re"
17            | "io"
18            | "argparse"
19            | "json"
20            | "math"
21            | "random"
22            | "datetime"
23            | "time"
24            | "collections"
25            | "itertools"
26            | "functools"
27            | "heapq"
28            | "copy"
29            | "textwrap"
30            | "hashlib"
31            | "csv"
32            | "glob"
33            | "pathlib"
34            | "tempfile"
35            | "subprocess"
36            | "string"
37            | "sysconfig"
38            | "venv"
39    )
40}
41
42/// Runtime modules that only exist on stdpython's std tier: they touch the
43/// OS (or, for math, std's float intrinsics), so the no_std profile has
44/// nothing to lower them to. json/string/collections/itertools live on the
45/// alloc tier and stay importable.
46pub(crate) fn is_std_only_module(name: &str) -> bool {
47    matches!(
48        name,
49        "os" | "sys"
50            | "re"
51            | "io"
52            | "argparse"
53            | "math"
54            | "random"
55            | "datetime"
56            | "time"
57            | "glob"
58            | "pathlib"
59            | "tempfile"
60            | "subprocess"
61            | "sysconfig"
62            | "venv"
63    )
64}
65
66/// The conversion-time error for a std-tier import under the no_std
67/// profile. Failing here beats failing later with an unresolved-name error
68/// in the generated crate.
69fn std_only_import_error(module: &str) -> Box<dyn std::error::Error> {
70    format!(
71        "`import {}` requires stdpython's std tier (it needs the OS), which the \
72         no_std profile does not provide; remove the import or convert without \
73         the no_std profile",
74        module
75    )
76    .into()
77}
78
79#[derive(Clone, Debug, FromPyObject, Serialize, Deserialize, PartialEq)]
80pub struct Alias {
81    pub name: String,
82    pub asname: Option<String>,
83}
84
85#[derive(Clone, Debug, FromPyObject, Serialize, Deserialize, PartialEq)]
86pub struct Import {
87    pub names: Vec<Alias>,
88}
89
90/// An Import (or FromImport) statement causes 2 things to occur:
91/// 1. Declares the imported object within the existing scope.
92/// 2. Causes the referenced module to be compiled into the program (only once).
93
94impl CodeGen for Import {
95    type Context = CodeGenContext;
96    type Options = PythonOptions;
97    type SymbolTable = SymbolTableScopes;
98
99    fn find_symbols(self, symbols: Self::SymbolTable) -> Self::SymbolTable {
100        let mut symbols = symbols;
101        for alias in self.names.iter() {
102            symbols.insert(alias.name.clone(), SymbolTableNode::Import(self.clone()));
103            if let Some(a) = alias.asname.clone() {
104                symbols.insert(a, SymbolTableNode::Alias(alias.name.clone()))
105            }
106        }
107        symbols
108    }
109
110    fn to_rust(
111        self,
112        ctx: Self::Context,
113        options: Self::Options,
114        _symbols: Self::SymbolTable,
115    ) -> Result<TokenStream, Box<dyn std::error::Error>> {
116        let mut tokens = TokenStream::new();
117        for alias in self.names.iter() {
118            if options.no_std {
119                let root = alias.name.split('.').next().unwrap_or(&alias.name);
120                if is_std_only_module(root) {
121                    return Err(std_only_import_error(&alias.name));
122                }
123            }
124            // Check if this is a Python standard library module that needs special handling
125            let rust_import = match alias.name.as_str() {
126                // Runtime-provided modules are already in scope through
127                // `use stdpython::*` (each is re-exported at the crate
128                // root), so the import lowers to nothing — a bare
129                // `use math;` would not even resolve.
130                name if is_stdpython_module(name) => {
131                    if let Some(asname) = &alias.asname {
132                        // The module can't be re-bound with `use` (it's a
133                        // glob-imported name, not a path), so aliasing
134                        // would silently leave the alias undefined.
135                        return Err(format!(
136                            "`import {} as {}`: aliasing runtime modules is not \
137                             supported yet; use `import {}`",
138                            name, asname, name
139                        )
140                        .into());
141                    }
142                    quote! {}
143                }
144                // Python stdlib modules that don't have direct Rust equivalents
145                "urllib" | "xml" | "asyncio" => {
146                    // These will be provided by the stdpython runtime
147                    // Generate a comment instead of a use statement
148                    quote! {
149                        // Python module '{}' will be provided by stdpython runtime
150                    }
151                }
152                "os.path" => {
153                    quote! {
154                        // Python os.path module will be provided by stdpython runtime
155                    }
156                }
157                _ => {
158                    // Handle other imports normally
159                    let names = if alias.name.contains('.') {
160                        let parts: Vec<&str> = alias.name.split('.').collect();
161                        let idents: Vec<_> = parts.iter().map(|part| crate::safe_ident(part)).collect();
162                        quote!(#(#idents)::*)
163                    } else {
164                        let single_name = crate::safe_ident(&alias.name);
165                        quote!(#single_name)
166                    };
167                    
168                    match &alias.asname {
169                        None => {
170                            quote! {use #names;}
171                        }
172                        Some(n) => {
173                            let name = crate::safe_ident(n);
174                            quote! {use #names as #name;}
175                        }
176                    }
177                }
178            };
179            
180            tokens.extend(rust_import);
181        }
182        debug!("context: {:?}", ctx);
183        debug!("options: {:?}", options);
184        debug!("tokens: {}", tokens);
185        Ok(tokens)
186    }
187}
188
189#[derive(Clone, Debug, FromPyObject, Serialize, Deserialize, PartialEq)]
190pub struct ImportFrom {
191    pub module: String,
192    pub names: Vec<Alias>,
193    pub level: usize,
194}
195
196impl CodeGen for ImportFrom {
197    type Context = CodeGenContext;
198    type Options = PythonOptions;
199    type SymbolTable = SymbolTableScopes;
200
201    fn find_symbols(self, symbols: Self::SymbolTable) -> Self::SymbolTable {
202        let mut symbols = symbols;
203        for alias in self.names.iter() {
204            symbols.insert(
205                alias.name.clone(),
206                SymbolTableNode::ImportFrom(self.clone()),
207            );
208        }
209        symbols
210    }
211
212    fn to_rust(
213        self,
214        ctx: Self::Context,
215        _options: Self::Options,
216        _symbols: Self::SymbolTable,
217    ) -> Result<TokenStream, Box<dyn std::error::Error>> {
218        debug!("ctx: {:?}", ctx);
219
220        // typing imports (Optional, List, Dict, ...) are annotation-only:
221        // annotations map to Rust types directly, so the import itself
222        // lowers to nothing.
223        if self.module.split('.').next() == Some("typing") {
224            return Ok(TokenStream::new());
225        }
226
227        if _options.no_std {
228            let root = self.module.split('.').next().unwrap_or(&self.module);
229            if is_std_only_module(root) {
230                return Err(std_only_import_error(&self.module));
231            }
232        }
233
234        // `from X import y` must bring `y` into scope; previously this
235        // emitted nothing and later uses of `y` were undefined. `use` paths
236        // can't resolve through glob imports, so anchor the path explicitly:
237        // stdlib modules live under the stdpython runtime crate, and
238        // anything else is assumed to be a sibling module of the generated
239        // crate. Wildcard imports map to glob uses.
240        let parts: Vec<&str> = self
241            .module
242            .split('.')
243            .filter(|part| !part.is_empty())
244            .collect();
245        let module_path: Vec<_> = parts.iter().map(|part| crate::safe_ident(part)).collect();
246        let root = if parts.first().is_some_and(|first| is_stdpython_module(first)) {
247            let runtime = crate::safe_ident(&_options.stdpython);
248            quote!(#runtime)
249        } else {
250            quote!(crate)
251        };
252
253        let mut tokens = TokenStream::new();
254        for alias in self.names.iter() {
255            // functools.partial/lru_cache/cache have no runtime symbols:
256            // partial lowers to a closure at each call site, and the
257            // cache decorators rewrite the function definition itself,
258            // so the imports emit nothing (an uncalled bare reference is
259            // then a loud unresolved-name error).
260            if self.module == "functools"
261                && matches!(alias.name.as_str(), "partial" | "lru_cache" | "cache")
262            {
263                continue;
264            }
265            if alias.name == "*" {
266                tokens.extend(quote! { use #root #(::#module_path)*::*; });
267                continue;
268            }
269            // Some runtime functions split into arity/keyword-specific
270            // variants (accumulate with initial=, product with repeat=,
271            // ...); importing the Python name brings its variants along so
272            // the call lowering can pick one. For these names, the BASE
273            // import is allow(unused_imports) too: the lowering may rewrite
274            // every call site to a variant (accumulate/product always are),
275            // orphaning the bare name through no fault of the source
276            // Python. Names without variants keep the plain import, so a
277            // genuinely unused `from itertools import pairwise` still
278            // surfaces as the source weakness it is.
279            let variants: &[&str] = match (self.module.as_str(), alias.name.as_str()) {
280                ("itertools", "accumulate") => &[
281                    "accumulate_sum",
282                    "accumulate_func",
283                    "accumulate_sum_initial",
284                    "accumulate_func_initial",
285                ],
286                ("itertools", "product") => &[
287                    "product2",
288                    "product3",
289                    "product_repeat2",
290                    "product_repeat3",
291                ],
292                ("itertools", "zip_longest") => &["zip_longest_fill"],
293                ("itertools", "groupby") => &["groupby_key"],
294                ("functools", "reduce") => &["reduce_initial"],
295                ("re", "findall") => &["findall2", "findall3"],
296                ("io", "StringIO") => &["StringIO_seeded"],
297                ("hashlib", "md5") => &["md5_new"],
298                ("hashlib", "sha1") => &["sha1_new"],
299                ("hashlib", "sha256") => &["sha256_new"],
300                ("hashlib", "sha512") => &["sha512_new"],
301                _ => &[],
302            };
303
304            let name = crate::safe_ident(&alias.name);
305            let import = match &alias.asname {
306                None if variants.is_empty() => {
307                    quote! { use #root #(::#module_path)*::#name; }
308                }
309                None => quote! {
310                    #[allow(unused_imports)]
311                    use #root #(::#module_path)*::#name;
312                },
313                Some(asname) => {
314                    let asname = crate::safe_ident(asname);
315                    quote! { use #root #(::#module_path)*::#name as #asname; }
316                }
317            };
318            tokens.extend(import);
319
320            for variant in variants {
321                let v = crate::safe_ident(variant);
322                tokens.extend(quote! {
323                    #[allow(unused_imports)]
324                    use #root #(::#module_path)*::#v;
325                });
326            }
327        }
328        Ok(tokens)
329    }
330}