Skip to content

Commit d86a7c3

Browse files
authored
Support a new --native-last argument (bloomberg#182)
Add a new ``--native-last`` command line flag to only show the C frames after the last Python frame on each thread.
1 parent 82bc1f3 commit d86a7c3

File tree

7 files changed

+202
-53
lines changed

7 files changed

+202
-53
lines changed

news/182.feature.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add a new ``--native-last`` command line flag to only show the C frames after the last Python frame.

src/pystack/__main__.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,15 @@ def generate_cli_parser() -> argparse.ArgumentParser:
166166
help="Include native (C) frames from threads not registered with "
167167
"the interpreter (implies --native)",
168168
)
169+
remote_parser.add_argument(
170+
"--native-last",
171+
action="store_const",
172+
dest="native_mode",
173+
const=NativeReportingMode.LAST,
174+
default=NativeReportingMode.OFF,
175+
help="Include native (C) frames only after the last python frame "
176+
"in the resulting stack trace",
177+
)
169178
remote_parser.add_argument(
170179
"--locals",
171180
action="store_true",
@@ -212,6 +221,15 @@ def generate_cli_parser() -> argparse.ArgumentParser:
212221
help="Include native (C) frames from threads not registered with "
213222
"the interpreter (implies --native)",
214223
)
224+
core_parser.add_argument(
225+
"--native-last",
226+
action="store_const",
227+
dest="native_mode",
228+
const=NativeReportingMode.LAST,
229+
default=NativeReportingMode.OFF,
230+
help="Include native (C) frames only after the last python frame "
231+
"in the resulting stack trace",
232+
)
215233
core_parser.add_argument(
216234
"--locals",
217235
action="store_true",
@@ -274,8 +292,7 @@ def process_remote(parser: argparse.ArgumentParser, args: argparse.Namespace) ->
274292
locals=args.locals,
275293
method=StackMethod.ALL if args.exhaustive else StackMethod.AUTO,
276294
):
277-
native = args.native_mode != NativeReportingMode.OFF
278-
print_thread(thread, native)
295+
print_thread(thread, args.native_mode)
279296

280297

281298
def format_psinfo_information(psinfo: Dict[str, Any]) -> str:
@@ -405,8 +422,7 @@ def process_core(parser: argparse.ArgumentParser, args: argparse.Namespace) -> N
405422
locals=args.locals,
406423
method=StackMethod.ALL if args.exhaustive else StackMethod.AUTO,
407424
):
408-
native = args.native_mode != NativeReportingMode.OFF
409-
print_thread(thread, native)
425+
print_thread(thread, args.native_mode)
410426

411427

412428
if __name__ == "__main__": # pragma: no cover

src/pystack/_pystack.pyi

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ class CoreFileAnalyzer:
2424
def missing_modules(self) -> List[str]: ...
2525

2626
class NativeReportingMode(enum.Enum):
27-
ALL = 1
28-
OFF = 2
29-
PYTHON = 3
27+
ALL = ...
28+
OFF = ...
29+
PYTHON = ...
30+
LAST = ...
3031

3132
class StackMethod(enum.Enum):
3233
ALL = 1

src/pystack/_pystack.pyx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class NativeReportingMode(enum.Enum):
8686
OFF = 0
8787
PYTHON = 1
8888
ALL = 1000
89+
LAST = 2000
8990

9091

9192
cdef api void log_with_python(const cppstring *message, int level) noexcept:

src/pystack/traceback_formatter.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@
44
from typing import Optional
55

66
from .colors import colored
7+
from .engine import NativeReportingMode
78
from .types import NativeFrame
89
from .types import PyCodeObject
910
from .types import PyFrame
1011
from .types import PyThread
1112
from .types import frame_type
1213

1314

14-
def print_thread(thread: PyThread, native: bool) -> None:
15-
for line in format_thread(thread, native):
15+
def print_thread(thread: PyThread, native_mode: NativeReportingMode) -> None:
16+
for line in format_thread(thread, native_mode):
1617
print(line, file=sys.stdout, flush=True)
1718

1819

@@ -62,7 +63,8 @@ def _are_the_stacks_mergeable(thread: PyThread) -> bool:
6263
return n_eval_frames == n_entry_frames
6364

6465

65-
def format_thread(thread: PyThread, native: bool) -> Iterable[str]:
66+
def format_thread(thread: PyThread, native_mode: NativeReportingMode) -> Iterable[str]:
67+
native = native_mode != NativeReportingMode.OFF
6668
current_frame: Optional[PyFrame] = thread.first_frame
6769
if current_frame is None and not native:
6870
yield f"The frame stack for thread {thread.tid} is empty"
@@ -82,16 +84,22 @@ def format_thread(thread: PyThread, native: bool) -> Iterable[str]:
8284
yield from format_frame(current_frame)
8385
current_frame = current_frame.next
8486
else:
85-
yield from _format_merged_stacks(thread, current_frame)
87+
yield from _format_merged_stacks(
88+
thread, current_frame, native_mode == NativeReportingMode.LAST
89+
)
8690
yield ""
8791

8892

8993
def _format_merged_stacks(
90-
thread: PyThread, current_frame: Optional[PyFrame]
94+
thread: PyThread,
95+
current_frame: Optional[PyFrame],
96+
native_last: bool = False,
9197
) -> Iterable[str]:
98+
c_frames_list: list[str] = []
9299
for frame in thread.native_frames:
93100
if frame_type(frame, thread.python_version) == NativeFrame.FrameType.EVAL:
94101
assert current_frame is not None
102+
c_frames_list = []
95103
yield from format_frame(current_frame)
96104
current_frame = current_frame.next
97105
while current_frame and not current_frame.is_entry:
@@ -102,12 +110,18 @@ def _format_merged_stacks(
102110
continue
103111
elif frame_type(frame, thread.python_version) == NativeFrame.FrameType.OTHER:
104112
function = colored(frame.symbol, "yellow")
105-
yield (
113+
formatted_c_frame = (
106114
f' {colored("(C)", "blue")} File "{frame.path}",'
107115
f" line {frame.linenumber},"
108116
f" in {function} ({colored(frame.library, attrs=['faint'])})"
109117
)
118+
if native_last:
119+
c_frames_list.append(formatted_c_frame)
120+
else:
121+
yield formatted_c_frame
110122
else: # pragma: no cover
111123
raise ValueError(
112124
f"Invalid frame type: {frame_type(frame, thread.python_version)}"
113125
)
126+
for c_frame in c_frames_list:
127+
yield c_frame

tests/unit/test_main.py

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,9 @@ def test_process_remote_default():
207207
locals=False,
208208
method=StackMethod.AUTO,
209209
)
210-
assert print_thread_mock.mock_calls == [call(thread, False) for thread in threads]
210+
assert print_thread_mock.mock_calls == [
211+
call(thread, NativeReportingMode.OFF) for thread in threads
212+
]
211213

212214

213215
def test_process_remote_no_block():
@@ -238,13 +240,23 @@ def test_process_remote_no_block():
238240
locals=False,
239241
method=StackMethod.AUTO,
240242
)
241-
assert print_thread_mock.mock_calls == [call(thread, False) for thread in threads]
243+
assert print_thread_mock.mock_calls == [
244+
call(thread, NativeReportingMode.OFF) for thread in threads
245+
]
242246

243247

244-
def test_process_remote_native():
248+
@pytest.mark.parametrize(
249+
"argument, mode",
250+
[
251+
["--native", NativeReportingMode.PYTHON],
252+
["--native-all", NativeReportingMode.ALL],
253+
["--native-last", NativeReportingMode.LAST],
254+
],
255+
)
256+
def test_process_remote_native(argument, mode):
245257
# GIVEN
246258

247-
argv = ["pystack", "remote", "31", "--native"]
259+
argv = ["pystack", "remote", "31", argument]
248260

249261
threads = [Mock(), Mock(), Mock()]
250262

@@ -265,11 +277,11 @@ def test_process_remote_native():
265277
get_process_threads_mock.assert_called_with(
266278
31,
267279
stop_process=True,
268-
native_mode=NativeReportingMode.PYTHON,
280+
native_mode=mode,
269281
locals=False,
270282
method=StackMethod.AUTO,
271283
)
272-
assert print_thread_mock.mock_calls == [call(thread, True) for thread in threads]
284+
assert print_thread_mock.mock_calls == [call(thread, mode) for thread in threads]
273285

274286

275287
def test_process_remote_locals():
@@ -300,7 +312,9 @@ def test_process_remote_locals():
300312
locals=True,
301313
method=StackMethod.AUTO,
302314
)
303-
assert print_thread_mock.mock_calls == [call(thread, False) for thread in threads]
315+
assert print_thread_mock.mock_calls == [
316+
call(thread, NativeReportingMode.OFF) for thread in threads
317+
]
304318

305319

306320
def test_process_remote_native_no_block(capsys):
@@ -357,7 +371,9 @@ def test_process_remote_exhaustive():
357371
locals=False,
358372
method=StackMethod.ALL,
359373
)
360-
assert print_thread_mock.mock_calls == [call(thread, False) for thread in threads]
374+
assert print_thread_mock.mock_calls == [
375+
call(thread, NativeReportingMode.OFF) for thread in threads
376+
]
361377

362378

363379
@pytest.mark.parametrize(
@@ -432,7 +448,9 @@ def test_process_core_default_without_executable():
432448
locals=False,
433449
method=StackMethod.AUTO,
434450
)
435-
assert print_thread_mock.mock_calls == [call(thread, False) for thread in threads]
451+
assert print_thread_mock.mock_calls == [
452+
call(thread, NativeReportingMode.OFF) for thread in threads
453+
]
436454

437455

438456
def test_process_core_default_gzip_without_executable():
@@ -486,7 +504,9 @@ def test_process_core_default_gzip_without_executable():
486504
locals=False,
487505
method=StackMethod.AUTO,
488506
)
489-
assert print_thread_mock.mock_calls == [call(thread, False) for thread in threads]
507+
assert print_thread_mock.mock_calls == [
508+
call(thread, NativeReportingMode.OFF) for thread in threads
509+
]
490510
gzip_open_mock.assert_called_with(Path("corefile.gz"), "rb")
491511

492512

@@ -580,14 +600,17 @@ def test_process_core_default_with_executable():
580600
locals=False,
581601
method=StackMethod.AUTO,
582602
)
583-
assert print_thread_mock.mock_calls == [call(thread, False) for thread in threads]
603+
assert print_thread_mock.mock_calls == [
604+
call(thread, NativeReportingMode.OFF) for thread in threads
605+
]
584606

585607

586608
@pytest.mark.parametrize(
587609
"argument, mode",
588610
[
589611
["--native", NativeReportingMode.PYTHON],
590612
["--native-all", NativeReportingMode.ALL],
613+
["--native-last", NativeReportingMode.LAST],
591614
],
592615
)
593616
def test_process_core_native(argument, mode):
@@ -627,7 +650,7 @@ def test_process_core_native(argument, mode):
627650
locals=False,
628651
method=StackMethod.AUTO,
629652
)
630-
assert print_thread_mock.mock_calls == [call(thread, True) for thread in threads]
653+
assert print_thread_mock.mock_calls == [call(thread, mode) for thread in threads]
631654

632655

633656
def test_process_core_locals():
@@ -667,7 +690,9 @@ def test_process_core_locals():
667690
locals=True,
668691
method=StackMethod.AUTO,
669692
)
670-
assert print_thread_mock.mock_calls == [call(thread, False) for thread in threads]
693+
assert print_thread_mock.mock_calls == [
694+
call(thread, NativeReportingMode.OFF) for thread in threads
695+
]
671696

672697

673698
def test_process_core_with_search_path():
@@ -714,7 +739,9 @@ def test_process_core_with_search_path():
714739
locals=False,
715740
method=StackMethod.AUTO,
716741
)
717-
assert print_thread_mock.mock_calls == [call(thread, False) for thread in threads]
742+
assert print_thread_mock.mock_calls == [
743+
call(thread, NativeReportingMode.OFF) for thread in threads
744+
]
718745

719746

720747
def test_process_core_with_search_root():
@@ -762,7 +789,9 @@ def test_process_core_with_search_root():
762789
locals=False,
763790
method=StackMethod.AUTO,
764791
)
765-
assert print_thread_mock.mock_calls == [call(thread, False) for thread in threads]
792+
assert print_thread_mock.mock_calls == [
793+
call(thread, NativeReportingMode.OFF) for thread in threads
794+
]
766795

767796

768797
def test_process_core_with_not_readable_search_root():
@@ -947,7 +976,9 @@ def test_process_core_exhaustive():
947976
locals=False,
948977
method=StackMethod.ALL,
949978
)
950-
assert print_thread_mock.mock_calls == [call(thread, False) for thread in threads]
979+
assert print_thread_mock.mock_calls == [
980+
call(thread, NativeReportingMode.OFF) for thread in threads
981+
]
951982

952983

953984
def test_default_colored_output():

0 commit comments

Comments
 (0)