python_ast/
parser_utils.rs

1/// Generic utilities for parsing Python AST objects with consistent error handling.
2
3use pyo3::{Bound, PyAny, PyResult, prelude::PyAnyMethods, types::PyTypeMethods};
4use crate::{Node, dump};
5
6/// Generic function for extracting Python operator types with consistent error handling.
7pub fn extract_operator_type<T>(
8    ob: &Bound<PyAny>,
9    attr_name: &str,
10    context: &str,
11) -> PyResult<String>
12where
13    T: std::fmt::Debug,
14{
15    let op = ob.getattr(attr_name).map_err(|_| {
16        pyo3::exceptions::PyAttributeError::new_err(
17            ob.error_message("<unknown>", format!("error getting {}", context))
18        )
19    })?;
20
21    let op_type = op.get_type().name().map_err(|_| {
22        pyo3::exceptions::PyTypeError::new_err(
23            ob.error_message(
24                "<unknown>",
25                format!("extracting type name for {}", context),
26            )
27        )
28    })?;
29
30    op_type.extract()
31}
32
33/// Generic function for extracting operands from binary operations.
34pub fn extract_binary_operands<L, R>(
35    ob: &Bound<PyAny>,
36    left_attr: &str,
37    right_attr: &str,
38    context: &str,
39) -> PyResult<(L, R)>
40where
41    L: for<'a> pyo3::FromPyObject<'a>,
42    R: for<'a> pyo3::FromPyObject<'a>,
43{
44    let left = ob.getattr(left_attr).map_err(|_| {
45        pyo3::exceptions::PyAttributeError::new_err(
46            ob.error_message("<unknown>", format!("error getting {} left operand", context))
47        )
48    })?;
49
50    let right = ob.getattr(right_attr).map_err(|_| {
51        pyo3::exceptions::PyAttributeError::new_err(
52            ob.error_message("<unknown>", format!("error getting {} right operand", context))
53        )
54    })?;
55
56    let left = left.extract().map_err(|e| {
57        pyo3::exceptions::PyValueError::new_err(
58            format!("Failed to extract {} left operand: {}", context, e)
59        )
60    })?;
61
62    let right = right.extract().map_err(|e| {
63        pyo3::exceptions::PyValueError::new_err(
64            format!("Failed to extract {} right operand: {}", context, e)
65        )
66    })?;
67
68    Ok((left, right))
69}
70
71/// Generic function for extracting lists of items with error handling.
72pub fn extract_list<T>(
73    ob: &Bound<PyAny>,
74    attr_name: &str,
75    context: &str,
76) -> PyResult<Vec<T>>
77where
78    T: for<'a> pyo3::FromPyObject<'a>,
79{
80    let list_obj = ob.getattr(attr_name).map_err(|_| {
81        pyo3::exceptions::PyAttributeError::new_err(
82            ob.error_message("<unknown>", format!("error getting {} list", context))
83        )
84    })?;
85
86    list_obj.extract().map_err(|e| {
87        pyo3::exceptions::PyValueError::new_err(
88            format!("Failed to extract {} list: {}", context, e)
89        )
90    })
91}
92
93/// Generic function to safely extract optional attributes.
94pub fn extract_optional<T>(
95    ob: &Bound<PyAny>,
96    attr_name: &str,
97) -> Option<T>
98where
99    T: for<'a> pyo3::FromPyObject<'a>,
100{
101    ob.getattr(attr_name)
102        .ok()
103        .and_then(|attr| attr.extract().ok())
104}
105
106/// Generic function to extract position information from AST nodes.
107pub fn extract_position_info(ob: &Bound<PyAny>) -> (Option<usize>, Option<usize>, Option<usize>, Option<usize>) {
108    (
109        extract_optional(ob, "lineno"),
110        extract_optional(ob, "col_offset"),
111        extract_optional(ob, "end_lineno"),
112        extract_optional(ob, "end_col_offset"),
113    )
114}
115
116/// Trait for types that can be extracted from Python with improved error messages.
117pub trait ExtractFromPython<'a>: Sized {
118    /// Extract from Python object with context for better error messages.
119    fn extract_with_context(ob: &Bound<'a, PyAny>, context: &str) -> PyResult<Self>;
120}
121
122impl<'a, T> ExtractFromPython<'a> for T
123where
124    T: pyo3::FromPyObject<'a>,
125{
126    fn extract_with_context(ob: &Bound<'a, PyAny>, context: &str) -> PyResult<Self> {
127        ob.extract().map_err(|e| {
128            pyo3::exceptions::PyValueError::new_err(
129                format!("Failed to extract {} from Python: {} (object: {})", 
130                    context, e, dump(ob, None).unwrap_or_else(|_| "unknown".to_string()))
131            )
132        })
133    }
134}
135
136/// Utility function for consistent logging during Python object extraction.
137pub fn log_extraction(ob: &Bound<PyAny>, context: &str) {
138    if log::log_enabled!(log::Level::Debug) {
139        match dump(ob, None) {
140            Ok(dump_str) => log::debug!("Extracting {}: {}", context, dump_str),
141            Err(_) => log::debug!("Extracting {} (dump failed)", context),
142        }
143    }
144}
145
146/// Helper function to create standardized error messages for failed extractions.
147pub fn extraction_error(context: &str, details: &str) -> pyo3::PyErr {
148    pyo3::exceptions::PyValueError::new_err(
149        format!("Failed to extract {}: {}", context, details)
150    )
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use pyo3::Python;
157
158    #[test]
159    fn test_extract_optional() {
160        Python::with_gil(|py| {
161            use std::ffi::CString;
162            // Create a simple Python integer which has some attributes
163            let code = CString::new("42").unwrap();
164            let obj = py.eval(&code, None, None).unwrap();
165            
166            // Try to extract something that probably won't exist 
167            let missing: Option<String> = extract_optional(&obj, "missing_attr");
168            assert_eq!(missing, None);
169            
170            // This test mainly checks that extract_optional doesn't panic
171            // and properly returns None for missing attributes
172        });
173    }
174
175    #[test]
176    fn test_log_extraction() {
177        Python::with_gil(|py| {
178            use std::ffi::CString;
179            let code = CString::new("42").unwrap();
180            let obj = py.eval(&code, None, None).unwrap();
181            
182            // Should not panic
183            log_extraction(&obj, "test object");
184        });
185    }
186
187    #[test]
188    fn test_extraction_error() {
189        let error = extraction_error("test context", "test details");
190        let error_string = format!("{}", error);
191        assert!(error_string.contains("test context"));
192        assert!(error_string.contains("test details"));
193    }
194}
195
196/// Enhanced error handling utilities for parsing Python AST objects
197
198/// Get an attribute from a Python object with better error messaging
199pub fn get_attr_with_context<'a>(
200    ob: &Bound<'a, PyAny>,
201    attr_name: &str,
202    context: &str,
203) -> PyResult<Bound<'a, PyAny>> {
204    ob.getattr(attr_name).map_err(|e| {
205        let type_name = ob.get_type().name()
206            .map(|s| s.to_string())
207            .unwrap_or_else(|_| "<unknown>".to_string());
208        let enhanced_msg = format!(
209            "Failed to get attribute '{}' from {} ({}): {}",
210            attr_name,
211            context,
212            type_name,
213            e
214        );
215        pyo3::exceptions::PyAttributeError::new_err(enhanced_msg)
216    })
217}
218
219/// Extract a value from PyAny with better error messaging
220pub fn extract_with_context<'py, T>(
221    value: &Bound<'py, PyAny>,
222    context: &str,
223    attr_name: &str,
224) -> PyResult<T>
225where
226    T: pyo3::FromPyObject<'py>,
227{
228    value.extract().map_err(|e| {
229        let type_name = value.get_type().name()
230            .map(|s| s.to_string())
231            .unwrap_or_else(|_| "<unknown>".to_string());
232        let enhanced_msg = format!(
233            "Failed to extract {} for attribute '{}': {}. Expected type: {}, got: {}",
234            context,
235            attr_name,
236            e,
237            std::any::type_name::<T>(),
238            type_name
239        );
240        pyo3::exceptions::PyTypeError::new_err(enhanced_msg)
241    })
242}
243
244/// Extract a required attribute with enhanced error messaging  
245pub fn extract_required_attr<'py, T>(
246    ob: &Bound<'py, PyAny>,
247    attr_name: &str,
248    context: &str,
249) -> PyResult<T>
250where
251    T: pyo3::FromPyObject<'py>,
252{
253    let attr = get_attr_with_context(ob, attr_name, context)?;
254    extract_with_context(&attr, context, attr_name)
255}