Skip to content

Commit 2395e2c

Browse files
Add support for displaying numpy scalars (#752)
1 parent ee6a780 commit 2395e2c

5 files changed

Lines changed: 166 additions & 1 deletion

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ jobs:
4343
- uses: actions/setup-python@v4
4444
with:
4545
python-version: 3.9
46+
- name: Install optional numpy dependency
47+
run: pip install numpy>=2
4648
- name: Test
4749
id: test
4850
continue-on-error: true

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ classifiers = [
1515
]
1616
dynamic = ["version"]
1717

18+
[project.optional-dependencies]
19+
test = ["numpy"]
20+
1821
[tool.maturin]
1922
bindings = "bin"
2023

src/python_data_access.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,13 +472,53 @@ where
472472
format!("{}", value.ob_fval)
473473
} else if value_type_name == "NoneType" {
474474
"None".to_owned()
475+
} else if value_type_name.starts_with("numpy.") {
476+
match value_type_name {
477+
"numpy.bool" => format_obval::<bool, P>(addr, process)?,
478+
"numpy.uint8" => format_obval::<u8, P>(addr, process)?,
479+
"numpy.uint16" => format_obval::<u16, P>(addr, process)?,
480+
"numpy.uint32" => format_obval::<u32, P>(addr, process)?,
481+
"numpy.uint64" => format_obval::<u64, P>(addr, process)?,
482+
"numpy.int8" => format_obval::<i8, P>(addr, process)?,
483+
"numpy.int16" => format_obval::<i16, P>(addr, process)?,
484+
"numpy.int32" => format_obval::<i32, P>(addr, process)?,
485+
"numpy.int64" => format_obval::<i64, P>(addr, process)?,
486+
"numpy.float32" => format_obval::<f32, P>(addr, process)?,
487+
"numpy.float64" => format_obval::<f64, P>(addr, process)?,
488+
_ => format!("<{} at 0x{:x}>", value_type_name, addr),
489+
}
475490
} else {
476491
format!("<{} at 0x{:x}>", value_type_name, addr)
477492
};
478493

479494
Ok(formatted)
480495
}
481496

497+
/// Format the numpy scalar to a string.
498+
///
499+
/// All numpy scalars have shape:
500+
/// {
501+
/// ob_base: PyObject,
502+
/// obval: <value>,
503+
/// }
504+
///
505+
/// Where `obval` can be of different sizes depending on the scalar type.
506+
/// We match the size to the value_type_name for this purpose, avoiding the
507+
/// need to build bindings for the numpy C API.
508+
///
509+
/// * `addr`: Address of the numpy scalar
510+
/// * `process`: Process memory in which the object resides
511+
fn format_obval<T, P>(addr: usize, process: &P) -> Result<String, Error>
512+
where
513+
T: std::fmt::Display,
514+
P: ProcessMemory,
515+
{
516+
let base_addr = addr as *mut u32;
517+
let offset = std::mem::size_of::<crate::python_bindings::v3_7_0::PyObject>() as isize;
518+
let result = unsafe { process.copy_pointer(base_addr.byte_offset(offset) as *const T)? };
519+
Ok(format!("{}", result))
520+
}
521+
482522
#[cfg(test)]
483523
pub mod tests {
484524
// the idea here is to create various cpython interpretator structs locally

tests/integration_test.rs

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ fn test_local_vars() {
280280
let frame = &trace.frames[0];
281281
let locals = frame.locals.as_ref().unwrap();
282282

283-
assert_eq!(locals.len(), 9);
283+
assert_eq!(locals.len(), 27);
284284

285285
let arg1 = &locals[0];
286286
assert_eq!(arg1.name, "arg1");
@@ -326,6 +326,95 @@ fn test_local_vars() {
326326
assert_eq!(local6.name, "local6");
327327
assert!(!local6.arg);
328328

329+
// Numpy scalars
330+
let local7 = &locals[9];
331+
assert_eq!(local7.name, "local7");
332+
assert_eq!(local7.repr, Some("true".to_string()));
333+
334+
let local8 = &locals[10];
335+
assert_eq!(local8.name, "local8");
336+
assert_eq!(local8.repr, Some("2".to_string()));
337+
338+
let local9 = &locals[11];
339+
assert_eq!(local9.name, "local9");
340+
assert_eq!(local9.repr, Some("3".to_string()));
341+
342+
let local10 = &locals[12];
343+
assert_eq!(local10.name, "local10");
344+
assert_eq!(local10.repr, Some("42".to_string()));
345+
346+
let local11 = &locals[13];
347+
assert_eq!(local11.name, "local11");
348+
assert_eq!(local11.repr, Some("43".to_string()));
349+
350+
let local12 = &locals[14];
351+
assert_eq!(local12.name, "local12");
352+
assert_eq!(local12.repr, Some("44".to_string()));
353+
354+
let local13 = &locals[15];
355+
assert_eq!(local13.name, "local13");
356+
assert_eq!(local13.repr, Some("45".to_string()));
357+
358+
let local14 = &locals[16];
359+
assert_eq!(local14.name, "local14");
360+
assert_eq!(local14.repr, Some("46".to_string()));
361+
362+
let local15 = &locals[17];
363+
assert_eq!(local15.name, "local15");
364+
assert_eq!(local15.repr, Some("7".to_string()));
365+
366+
let local16 = &locals[18];
367+
assert_eq!(local16.name, "local16");
368+
assert_eq!(local16.repr, Some("8".to_string()));
369+
370+
fn test_repr_prefix(local: &py_spy::stack_trace::LocalVariable, expected: &str) {
371+
assert!(
372+
local
373+
.repr
374+
.as_ref()
375+
.map(|result| result.starts_with(expected))
376+
.unwrap_or(false),
377+
"local '{}' repr = '{:?}' doesn't start with '{}'",
378+
&local.name,
379+
&local.repr,
380+
expected
381+
);
382+
}
383+
384+
let local17 = &locals[19];
385+
assert_eq!(local17.name, "local17");
386+
387+
#[cfg(not(windows))]
388+
test_repr_prefix(local17, "<numpy.ulonglong at");
389+
390+
let local18 = &locals[20];
391+
assert_eq!(local18.name, "local18");
392+
test_repr_prefix(local18, "<numpy.float16 at");
393+
394+
let local19 = &locals[21];
395+
assert_eq!(local19.name, "local19");
396+
assert_eq!(local19.repr, Some("0.5".to_string()));
397+
398+
let local20 = &locals[22];
399+
assert_eq!(local20.name, "local20");
400+
assert_eq!(local20.repr, Some("0.7".to_string()));
401+
402+
let local21 = &locals[23];
403+
assert_eq!(local21.name, "local21");
404+
test_repr_prefix(local21, "<numpy.longdouble at");
405+
406+
let local22 = &locals[24];
407+
assert_eq!(local22.name, "local22");
408+
test_repr_prefix(local22, "<numpy.complex64 at");
409+
410+
let local23 = &locals[25];
411+
assert_eq!(local23.name, "local23");
412+
test_repr_prefix(local23, "<numpy.complex128 at");
413+
414+
let local24 = &locals[26];
415+
assert_eq!(local24.name, "local24");
416+
test_repr_prefix(local24, "<numpy.clongdouble at");
417+
329418
// we only support dictionary lookup on python 3.6+ right now
330419
if runner.spy.version.major == 3 && runner.spy.version.minor >= 6 {
331420
assert_eq!(

tests/scripts/local_vars.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import time
2+
import numpy as np
23

34

45
def local_variable_lookup(arg1="foo", arg2=None, arg3=True):
@@ -9,6 +10,36 @@ def local_variable_lookup(arg1="foo", arg2=None, arg3=True):
910
local5 = {"a": False, "b": (1, 2, 3)}
1011
# https://github.com/benfred/py-spy/issues/224
1112
local6 = ("-" * 115, {"key": {"key": {"key": "value"}}})
13+
14+
# Numpy scalars
15+
# integers
16+
local7 = np.bool(True)
17+
local8 = np.byte(2)
18+
19+
local9 = np.int8(3)
20+
local10 = np.int16(42)
21+
local11 = np.int32(43)
22+
local12 = np.int64(44)
23+
24+
local13 = np.uint8(45)
25+
local14 = np.uint16(46)
26+
local15 = np.uint32(7)
27+
local16 = np.uint64(8)
28+
29+
local17 = np.ulonglong(11)
30+
31+
# Floats
32+
local18 = np.float16(0.3)
33+
local19 = np.float32(0.5)
34+
local20 = np.float64(0.7)
35+
local21 = np.longdouble(0.9)
36+
37+
# Complex
38+
local22 = np.complex64(0.3+5j)
39+
local23 = np.complex128(0.3+5j)
40+
local24 = np.clongdouble(0.3+5j)
41+
42+
1243
time.sleep(100000)
1344

1445

0 commit comments

Comments
 (0)