|
| 1 | +use once_cell::sync::OnceCell; |
| 2 | +use pyo3::prelude::*; |
| 3 | +use pyo3::types::PyCode; |
| 4 | + |
| 5 | +/// A wrapper around Python `code` objects providing cached access to |
| 6 | +/// common attributes and line information. |
| 7 | +pub struct CodeObjectWrapper { |
| 8 | + obj: Py<PyCode>, |
| 9 | + id: usize, |
| 10 | + cache: CodeObjectCache, |
| 11 | +} |
| 12 | + |
| 13 | +#[derive(Default)] |
| 14 | +struct CodeObjectCache { |
| 15 | + filename: OnceCell<String>, |
| 16 | + qualname: OnceCell<String>, |
| 17 | + firstlineno: OnceCell<u32>, |
| 18 | + argcount: OnceCell<u16>, |
| 19 | + flags: OnceCell<u32>, |
| 20 | + lines: OnceCell<Vec<LineEntry>>, |
| 21 | +} |
| 22 | + |
| 23 | +#[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 24 | +pub struct LineEntry { |
| 25 | + pub offset: u32, |
| 26 | + pub line: u32, |
| 27 | +} |
| 28 | + |
| 29 | +impl CodeObjectWrapper { |
| 30 | + /// Construct from a `CodeType` object. Computes `id` eagerly. |
| 31 | + pub fn new(_py: Python<'_>, obj: &Bound<'_, PyCode>) -> Self { |
| 32 | + let id = obj.as_ptr() as usize; |
| 33 | + Self { |
| 34 | + obj: obj.clone().unbind(), |
| 35 | + id, |
| 36 | + cache: CodeObjectCache::default(), |
| 37 | + } |
| 38 | + } |
| 39 | + |
| 40 | + /// Borrow the owned `Py<PyCode>` as a `Bound<'py, PyCode>`. |
| 41 | + pub fn as_bound<'py>(&'py self, py: Python<'py>) -> Bound<'py, PyCode> { |
| 42 | + self.obj.bind(py).clone() |
| 43 | + } |
| 44 | + |
| 45 | + /// Return the stable identity of the code object (equivalent to `id(code)`). |
| 46 | + pub fn id(&self) -> usize { |
| 47 | + self.id |
| 48 | + } |
| 49 | + |
| 50 | + pub fn filename<'py>(&'py self, py: Python<'py>) -> PyResult<&'py str> { |
| 51 | + let value = self.cache.filename.get_or_try_init(|| -> PyResult<String> { |
| 52 | + let s: String = self |
| 53 | + .as_bound(py) |
| 54 | + .getattr("co_filename")? |
| 55 | + .extract()?; |
| 56 | + Ok(s) |
| 57 | + })?; |
| 58 | + Ok(value.as_str()) |
| 59 | + } |
| 60 | + |
| 61 | + pub fn qualname<'py>(&'py self, py: Python<'py>) -> PyResult<&'py str> { |
| 62 | + let value = self.cache.qualname.get_or_try_init(|| -> PyResult<String> { |
| 63 | + let s: String = self |
| 64 | + .as_bound(py) |
| 65 | + .getattr("co_qualname")? |
| 66 | + .extract()?; |
| 67 | + Ok(s) |
| 68 | + })?; |
| 69 | + Ok(value.as_str()) |
| 70 | + } |
| 71 | + |
| 72 | + pub fn first_line(&self, py: Python<'_>) -> PyResult<u32> { |
| 73 | + let value = *self.cache.firstlineno.get_or_try_init(|| -> PyResult<u32> { |
| 74 | + let v: u32 = self |
| 75 | + .as_bound(py) |
| 76 | + .getattr("co_firstlineno")? |
| 77 | + .extract()?; |
| 78 | + Ok(v) |
| 79 | + })?; |
| 80 | + Ok(value) |
| 81 | + } |
| 82 | + |
| 83 | + pub fn arg_count(&self, py: Python<'_>) -> PyResult<u16> { |
| 84 | + let value = *self.cache.argcount.get_or_try_init(|| -> PyResult<u16> { |
| 85 | + let v: u16 = self |
| 86 | + .as_bound(py) |
| 87 | + .getattr("co_argcount")? |
| 88 | + .extract()?; |
| 89 | + Ok(v) |
| 90 | + })?; |
| 91 | + Ok(value) |
| 92 | + } |
| 93 | + |
| 94 | + pub fn flags(&self, py: Python<'_>) -> PyResult<u32> { |
| 95 | + let value = *self.cache.flags.get_or_try_init(|| -> PyResult<u32> { |
| 96 | + let v: u32 = self |
| 97 | + .as_bound(py) |
| 98 | + .getattr("co_flags")? |
| 99 | + .extract()?; |
| 100 | + Ok(v) |
| 101 | + })?; |
| 102 | + Ok(value) |
| 103 | + } |
| 104 | + |
| 105 | + fn lines<'py>(&'py self, py: Python<'py>) -> PyResult<&'py [LineEntry]> { |
| 106 | + let vec = self.cache.lines.get_or_try_init(|| -> PyResult<Vec<LineEntry>> { |
| 107 | + let mut entries = Vec::new(); |
| 108 | + let iter = self.as_bound(py).call_method0("co_lines")?; |
| 109 | + let iter = iter.try_iter()?; |
| 110 | + for item in iter { |
| 111 | + let (start, _end, line): (u32, u32, Option<u32>) = item?.extract()?; |
| 112 | + if let Some(line) = line { |
| 113 | + entries.push(LineEntry { offset: start, line }); |
| 114 | + } |
| 115 | + } |
| 116 | + Ok(entries) |
| 117 | + })?; |
| 118 | + Ok(vec.as_slice()) |
| 119 | + } |
| 120 | + |
| 121 | + /// Return the source line for a given instruction offset using a binary search. |
| 122 | + pub fn line_for_offset(&self, py: Python<'_>, offset: u32) -> PyResult<Option<u32>> { |
| 123 | + let lines = self.lines(py)?; |
| 124 | + match lines.binary_search_by_key(&offset, |e| e.offset) { |
| 125 | + Ok(idx) => Ok(Some(lines[idx].line)), |
| 126 | + Err(0) => Ok(None), |
| 127 | + Err(idx) => Ok(Some(lines[idx - 1].line)), |
| 128 | + } |
| 129 | + } |
| 130 | +} |
0 commit comments