Skip to content

Commit 20c277f

Browse files
committed
test: add CodeObjectWrapper tests
1 parent 26ea0d3 commit 20c277f

File tree

8 files changed

+318
-287
lines changed

8 files changed

+318
-287
lines changed

.agents/tasks/2025/08/21-0939-codetype-interface

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,6 @@ According to the PyO3 documentation it is preferred to use `Bound<'_, T>` instea
3333
* https://pyo3.rs/v0.25.1/types.html
3434
* https://docs.rs/pyo3/0.25.1/pyo3/index.html
3535

36-
Also please add usage examples to the design documentation
36+
Also please add usage examples to the design documentation
37+
--- FOLLOW UP TASK ---
38+
Implement the CodeObjectWrapper as designed. Update the Tracer trait as well as the callback_xxx functions accordingly. Write a comprehensive unit tests for CodeObjectWrapper.

codetracer-python-recorder/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codetracer-python-recorder/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ default = ["extension-module"]
1818
pyo3 = { version = "0.25.1" }
1919
runtime_tracing = "0.14.0"
2020
bitflags = "2.4"
21+
once_cell = "1.19"
2122

2223
[dev-dependencies]
2324
pyo3 = { version = "0.25.1", features = ["auto-initialize"] }
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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+
}

codetracer-python-recorder/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ use std::sync::atomic::{AtomicBool, Ordering};
33
use pyo3::exceptions::PyRuntimeError;
44
use pyo3::prelude::*;
55

6+
pub mod code_object;
67
pub mod tracer;
8+
pub use crate::code_object::CodeObjectWrapper;
79
pub use crate::tracer::{install_tracer, uninstall_tracer, EventSet, Tracer};
810

911
/// Global flag tracking whether tracing is active.

0 commit comments

Comments
 (0)