1use pyo3::{Bound, PyAny, PyResult, prelude::PyAnyMethods, types::PyTypeMethods};
4use crate::{Node, dump};
5
6pub 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
33pub 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
71pub 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
93pub 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
106pub 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
116pub trait ExtractFromPython<'a>: Sized {
118 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
136pub 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
146pub 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 let code = CString::new("42").unwrap();
164 let obj = py.eval(&code, None, None).unwrap();
165
166 let missing: Option<String> = extract_optional(&obj, "missing_attr");
168 assert_eq!(missing, None);
169
170 });
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 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
196pub 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
219pub 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
244pub 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}