Skip to content

Commit 767bb0a

Browse files
committed
Add --class-name param to prepend class names to methods
Regular methods and classmethods supported. No support for static methods. This is because the frame object doesn't refer to the function, only its code, which leaves no other option than to try to find `self` or `cls` in locals. The alternative of using the `gc` module is horrendously slow even in the same process and also impractical in a remote process.
1 parent e0cbd46 commit 767bb0a

File tree

4 files changed

+117
-17
lines changed

4 files changed

+117
-17
lines changed

src/config.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ pub struct Config {
5656
#[doc(hidden)]
5757
pub lineno: LineNo,
5858
#[doc(hidden)]
59+
pub include_class_name: bool,
60+
#[doc(hidden)]
5961
pub refresh_seconds: f64,
6062
#[doc(hidden)]
6163
pub core_filename: Option<String>,
@@ -137,6 +139,7 @@ impl Default for Config {
137139
subprocesses: false,
138140
full_filenames: false,
139141
lineno: LineNo::LastInstruction,
142+
include_class_name: false,
140143
refresh_seconds: 1.0,
141144
core_filename: None,
142145
}
@@ -188,6 +191,9 @@ impl Config {
188191
let full_filenames = Arg::new("full_filenames").long("full-filenames").help(
189192
"Show full Python filenames, instead of shortening to show only the package part",
190193
);
194+
let include_class_name = Arg::new("include_class_name").long("class-name").help(
195+
"Prepend class name to method names as long as the `self`/`cls` argument naming convention is followed (doesn't work for staticmethods)",
196+
);
191197
let program = Arg::new("python_program")
192198
.help("commandline of a python program to run")
193199
.multiple_values(true);
@@ -215,6 +221,7 @@ impl Config {
215221
.arg(program.clone())
216222
.arg(pid.clone().required_unless_present("python_program"))
217223
.arg(full_filenames.clone())
224+
.arg(include_class_name.clone())
218225
.arg(
219226
Arg::new("output")
220227
.short('o')
@@ -282,6 +289,7 @@ impl Config {
282289
.arg(rate.clone())
283290
.arg(subprocesses.clone())
284291
.arg(full_filenames.clone())
292+
.arg(include_class_name.clone())
285293
.arg(gil.clone())
286294
.arg(idle.clone())
287295
.arg(top_delay.clone());
@@ -307,6 +315,7 @@ impl Config {
307315
);
308316

309317
let dump = dump.arg(full_filenames.clone())
318+
.arg(include_class_name.clone())
310319
.arg(Arg::new("locals")
311320
.short('l')
312321
.long("locals")
@@ -429,6 +438,7 @@ impl Config {
429438
.value_of("pid")
430439
.map(|p| p.parse().expect("invalid pid"));
431440
config.full_filenames = matches.occurrences_of("full_filenames") > 0;
441+
config.include_class_name = matches.occurrences_of("include_class_name") > 0;
432442
if cfg!(unwind) {
433443
config.native = matches.occurrences_of("native") > 0;
434444
}

src/python_data_access.rs

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,24 @@ const PY_TPFLAGS_BYTES_SUBCLASS: usize = 1 << 27;
275275
const PY_TPFLAGS_STRING_SUBCLASS: usize = 1 << 28;
276276
const PY_TPFLAGS_DICT_SUBCLASS: usize = 1 << 29;
277277

278+
/// Get the type name (truncating to 128 bytes if longer)
279+
pub fn extract_type_name<T, P>(process: &P, value_type: &T) -> Result<String, Error>
280+
where
281+
T: TypeObject,
282+
P: ProcessMemory,
283+
{
284+
let max_type_len = 128;
285+
let value_type_name = process.copy(value_type.name() as usize, max_type_len)?;
286+
let length = value_type_name
287+
.iter()
288+
.position(|&x| x == 0)
289+
.unwrap_or(max_type_len);
290+
291+
let string = String::from_utf8(value_type_name[..length].to_vec())?;
292+
293+
Ok(string)
294+
}
295+
278296
/// Converts a python variable in the other process to a human readable string
279297
pub fn format_variable<I, P>(
280298
process: &P,
@@ -295,14 +313,7 @@ where
295313
let value: I::Object = process.copy_struct(addr)?;
296314
let value_type = process.copy_pointer(value.ob_type())?;
297315

298-
// get the typename (truncating to 128 bytes if longer)
299-
let max_type_len = 128;
300-
let value_type_name = process.copy(value_type.name() as usize, max_type_len)?;
301-
let length = value_type_name
302-
.iter()
303-
.position(|&x| x == 0)
304-
.unwrap_or(max_type_len);
305-
let value_type_name = std::str::from_utf8(&value_type_name[..length])?;
316+
let value_type_name = extract_type_name(process, &value_type)?;
306317

307318
let format_int = |value: i64| {
308319
if value_type_name == "bool" {

src/python_spy.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,11 +238,12 @@ impl PythonSpy {
238238
continue;
239239
}
240240

241-
let mut trace = get_stack_trace(
241+
let mut trace = get_stack_trace::<I, <I as InterpreterState>::ThreadState, Process>(
242242
&thread,
243243
&self.process,
244244
self.config.dump_locals > 0,
245245
self.config.lineno,
246+
self.config.include_class_name,
246247
)?;
247248

248249
// Try getting the native thread id

src/stack_trace.rs

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ use remoteprocess::{Pid, ProcessMemory};
66
use serde_derive::Serialize;
77

88
use crate::config::{Config, LineNo};
9-
use crate::python_data_access::{copy_bytes, copy_string};
9+
use crate::python_data_access::{copy_bytes, copy_string, extract_type_name};
1010
use crate::python_interpreters::{
11-
CodeObject, FrameObject, InterpreterState, ThreadState, TupleObject,
11+
CodeObject, FrameObject, InterpreterState, Object, ThreadState, TupleObject, TypeObject,
1212
};
1313

1414
/// Call stack for a single python thread
@@ -66,6 +66,8 @@ pub struct ProcessInfo {
6666
pub parent: Option<Box<ProcessInfo>>,
6767
}
6868

69+
const PY_TPFLAGS_TYPE_SUBCLASS: usize = 1 << 31;
70+
6971
/// Given an InterpreterState, this function returns a vector of stack traces for each thread
7072
pub fn get_stack_traces<I, P>(
7173
interpreter: &I,
@@ -84,13 +86,20 @@ where
8486

8587
let lineno = config.map(|c| c.lineno).unwrap_or(LineNo::NoLine);
8688
let dump_locals = config.map(|c| c.dump_locals).unwrap_or(0);
89+
let include_class_name = config.map(|c| c.include_class_name).unwrap_or(false);
8790

8891
while !threads.is_null() {
8992
let thread = process
9093
.copy_pointer(threads)
9194
.context("Failed to copy PyThreadState")?;
9295

93-
let mut trace = get_stack_trace(&thread, process, dump_locals > 0, lineno)?;
96+
let mut trace = get_stack_trace::<I, <I as InterpreterState>::ThreadState, P>(
97+
&thread,
98+
process,
99+
dump_locals > 0,
100+
lineno,
101+
include_class_name,
102+
)?;
94103
trace.owns_gil = trace.thread_id == gil_thread_id;
95104

96105
ret.push(trace);
@@ -104,13 +113,15 @@ where
104113
}
105114

106115
/// Gets a stack trace for an individual thread
107-
pub fn get_stack_trace<T, P>(
116+
pub fn get_stack_trace<I, T, P>(
108117
thread: &T,
109118
process: &P,
110119
copy_locals: bool,
111120
lineno: LineNo,
121+
include_class_name: bool,
112122
) -> Result<StackTrace, Error>
113123
where
124+
I: InterpreterState,
114125
T: ThreadState,
115126
P: ProcessMemory,
116127
{
@@ -133,7 +144,7 @@ where
133144
.context("Failed to copy PyCodeObject")?;
134145

135146
let filename = copy_string(code.filename(), process).context("Failed to copy filename")?;
136-
let name = copy_string(code.name(), process).context("Failed to copy function name")?;
147+
let mut name = copy_string(code.name(), process).context("Failed to copy function name")?;
137148

138149
let line = match lineno {
139150
LineNo::NoLine => 0,
@@ -154,8 +165,28 @@ where
154165
},
155166
};
156167

157-
let locals = if copy_locals {
158-
Some(get_locals(&code, frame_ptr, &frame, process)?)
168+
// Grab locals, which may be needed for display or to find the method's class if the fn is
169+
// a method.
170+
let locals = if copy_locals || (include_class_name && code.argcount() > 0) {
171+
// Only copy the first local if we only want to find the class name.
172+
let first_var_only = !copy_locals;
173+
let found_locals = get_locals(&code, frame_ptr, &frame, process, first_var_only)?;
174+
175+
if include_class_name && found_locals.len() > 0 {
176+
let first_arg = &found_locals[0];
177+
if let Some(class_name) =
178+
get_class_name_from_arg::<I, P>(process, first_arg)?.as_ref()
179+
{
180+
// e.g. 'method' is turned into 'ClassName.method'
181+
name = format!("{}.{}", class_name, name);
182+
}
183+
}
184+
185+
if copy_locals {
186+
Some(found_locals)
187+
} else {
188+
None
189+
}
159190
} else {
160191
None
161192
};
@@ -229,6 +260,7 @@ fn get_locals<C: CodeObject, F: FrameObject, P: ProcessMemory>(
229260
frameptr: *const F,
230261
frame: &F,
231262
process: &P,
263+
first_var_only: bool,
232264
) -> Result<Vec<LocalVariable>, Error> {
233265
let local_count = code.nlocals() as usize;
234266
let argcount = code.argcount() as usize;
@@ -239,7 +271,12 @@ fn get_locals<C: CodeObject, F: FrameObject, P: ProcessMemory>(
239271

240272
let mut ret = Vec::new();
241273

242-
for i in 0..local_count {
274+
let vars_to_copy = if first_var_only {
275+
std::cmp::min(local_count, 1)
276+
} else {
277+
local_count
278+
};
279+
for i in 0..vars_to_copy {
243280
let nameptr: *const C::StringObject =
244281
process.copy_struct(varnames.address(code.varnames() as usize, i))?;
245282
let name = copy_string(nameptr, process)?;
@@ -257,6 +294,47 @@ fn get_locals<C: CodeObject, F: FrameObject, P: ProcessMemory>(
257294
Ok(ret)
258295
}
259296

297+
/// Get class from a `self` or `cls` argument, as long as its type matches expectations.
298+
fn get_class_name_from_arg<I, P>(
299+
process: &P,
300+
first_local: &LocalVariable,
301+
) -> Result<Option<String>, Error>
302+
where
303+
I: InterpreterState,
304+
P: ProcessMemory,
305+
{
306+
// If the first variable isn't an argument, there are no arguments, so the fn isn't a normal
307+
// method or a class method.
308+
if !first_local.arg {
309+
return Ok(None);
310+
}
311+
312+
let first_arg_name = &first_local.name;
313+
if first_arg_name != "self" && first_arg_name != "cls" {
314+
return Ok(None);
315+
}
316+
317+
let value: I::Object = process.copy_struct(first_local.addr)?;
318+
let mut value_type = process.copy_pointer(value.ob_type())?;
319+
let is_type = value_type.flags() & PY_TPFLAGS_TYPE_SUBCLASS != 0;
320+
321+
// validate that the first argument is:
322+
// - an instance of something else than `type` if it is called "self"
323+
// - an instance of `type` if it is called "cls"
324+
match (first_arg_name.as_str(), is_type) {
325+
("self", false) => {}
326+
("cls", true) => {
327+
// Copy the remote argument struct, but this time as PyTypeObject, rather than PyObject
328+
value_type = process.copy_struct(first_local.addr)?;
329+
}
330+
_ => {
331+
return Ok(None);
332+
}
333+
}
334+
335+
Ok(Some(extract_type_name(process, &value_type)?))
336+
}
337+
260338
pub fn get_gil_threadid<I: InterpreterState, P: ProcessMemory>(
261339
threadstate_address: usize,
262340
process: &P,

0 commit comments

Comments
 (0)