Skip to content

Commit 7a07a51

Browse files
authored
Merge pull request #76 from abizer/py314-lineno-fix
Fix crash on Python 3.12+
2 parents 7088d34 + 8e18d8e commit 7a07a51

6 files changed

Lines changed: 58 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
strategy:
1717
fail-fast: false
1818
matrix:
19-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
19+
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
2020
steps:
2121
- uses: actions/checkout@v3
2222
- name: Set up Python ${{ matrix.python-version }}

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
__pycache__/
2+
*.py[cod]
3+
*$py.class
4+
5+
*.egg-info/
6+
build/
7+
dist/
8+
9+
.venv*/
10+
.pytest_cache/

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# 0.2.13 - April 14, 2026
2+
3+
## Fixed
4+
- Python 3.12+ compatibility: `tb.tb_lineno` and `frame.f_lineno` can return `None` when an instruction has no line mapping (for example at some async suspension points or on synthetic RESUME/CACHE opcodes). `extraction.get_info` now substitutes `frame.f_code.co_firstlineno` in that case instead of crashing with `AssertionError` in `source_inspection.annotate`.
5+
16
# 0.2.8 - August 25, 2022
27

38
## Fixed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
setuptools.setup(
77
python_requires=">=3.4",
88
name="stackprinter",
9-
version="0.2.12",
9+
version="0.2.13",
1010
author="cknd",
1111
author_email="ck-github@mailbox.org",
1212
description="Debug-friendly stack traces, with variable values and semantic highlighting",

stackprinter/extraction.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ def get_info(tb_or_frame, lineno=None, suppressed_vars=[]):
8383
else:
8484
raise ValueError('Cant inspect this: ' + repr(tb_or_frame))
8585

86+
# Since CPython 3.12, both `tb.tb_lineno` and `frame.f_lineno` can return
87+
# None when the current instruction has no line mapping (for example at
88+
# certain async suspension points, or on synthetic RESUME/CACHE opcodes).
89+
# Fall back to the function's first line so we can still render a frame
90+
# rather than crashing downstream formatters with a None lineno.
91+
if lineno is None:
92+
lineno = frame.f_code.co_firstlineno
93+
8694
filename = inspect.getsourcefile(frame) or inspect.getfile(frame)
8795
function = frame.f_code.co_name
8896

tests/test_formatting.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,36 @@ def test_none_tuple_formatting():
5555
def test_none_value_formatting():
5656
output = stackprinter.format((TypeError, None, None))
5757
assert output == "TypeError: None"
58+
59+
60+
def test_tb_with_none_lineno():
61+
# Regression Python 3.12+ compat.
62+
# Since CPython 3.12, `tb.tb_lineno` (and `frame.f_lineno`) return None when
63+
# the instruction at tb_lasti has no line mapping. Stackprinter used to pass
64+
# that None straight into source_inspection.annotate() and blow up on
65+
# `assert isinstance(lineno, int)`. It should now fall back gracefully.
66+
import sys
67+
import types
68+
69+
import pytest
70+
71+
captured = []
72+
73+
def target():
74+
captured.append(sys._getframe())
75+
76+
target()
77+
frame = captured[0]
78+
79+
# An out-of-range tb_lasti makes CPython's tb_lineno getter return None via
80+
# its Py_RETURN_NONE fallback path.
81+
tb = types.TracebackType(None, frame, 1 << 20, -1)
82+
if tb.tb_lineno is not None:
83+
pytest.skip(
84+
"tb.tb_lineno didn't return None on this interpreter "
85+
f"({sys.version_info}); regression only applies when it does."
86+
)
87+
88+
output = stackprinter.format((ValueError, ValueError("boom"), tb))
89+
assert "target" in output
90+
assert "ValueError: boom" in output

0 commit comments

Comments
 (0)