Skip to content

Commit fbbde4d

Browse files
authored
[3.12] gh-123142: Fix too wide source locations in tracebacks of exceptions from broken iterables in comprehensions (GH-123173). (#123210)
(cherry picked from commit ec89620)
1 parent 8edfa0b commit fbbde4d

File tree

8 files changed

+122
-21
lines changed

8 files changed

+122
-21
lines changed

Lib/test/support/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"LOOPBACK_TIMEOUT", "INTERNET_TIMEOUT", "SHORT_TIMEOUT", "LONG_TIMEOUT",
5959
"Py_DEBUG", "EXCEEDS_RECURSION_LIMIT", "C_RECURSION_LIMIT",
6060
"skip_on_s390x",
61+
"BrokenIter",
6162
]
6263

6364

@@ -2468,3 +2469,17 @@ def is_slot_wrapper(name, value):
24682469
value = ns[name]
24692470
if is_slot_wrapper(cls, name, value):
24702471
yield name, True
2472+
2473+
2474+
class BrokenIter:
2475+
def __init__(self, init_raises=False, next_raises=False):
2476+
if init_raises:
2477+
1/0
2478+
self.next_raises = next_raises
2479+
2480+
def __next__(self):
2481+
if self.next_raises:
2482+
1/0
2483+
2484+
def __iter__(self):
2485+
return self

Lib/test/test_compile.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,7 +1065,7 @@ def return_genexp():
10651065
x
10661066
in
10671067
y)
1068-
genexp_lines = [0, 2, 0]
1068+
genexp_lines = [0, 4, 2, 0, 4]
10691069

10701070
genexp_code = return_genexp.__code__.co_consts[1]
10711071
code_lines = self.get_code_lines(genexp_code)
@@ -1431,7 +1431,7 @@ def test_multiline_generator_expression(self):
14311431
self.assertOpcodeSourcePositionIs(compiled_code, 'JUMP_BACKWARD',
14321432
line=1, end_line=2, column=1, end_column=8, occurrence=1)
14331433
self.assertOpcodeSourcePositionIs(compiled_code, 'RETURN_CONST',
1434-
line=1, end_line=6, column=0, end_column=32, occurrence=1)
1434+
line=4, end_line=4, column=7, end_column=14, occurrence=1)
14351435

14361436
def test_multiline_async_generator_expression(self):
14371437
snippet = textwrap.dedent("""\

Lib/test/test_dictcomps.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import traceback
12
import unittest
23

4+
from test.support import BrokenIter
5+
36
# For scope testing.
47
g = "Global variable"
58

@@ -127,6 +130,34 @@ def test_star_expression(self):
127130
self.assertEqual({i: i*i for i in [*range(4)]}, expected)
128131
self.assertEqual({i: i*i for i in (*range(4),)}, expected)
129132

133+
def test_exception_locations(self):
134+
# The location of an exception raised from __init__ or
135+
# __next__ should should be the iterator expression
136+
def init_raises():
137+
try:
138+
{x:x for x in BrokenIter(init_raises=True)}
139+
except Exception as e:
140+
return e
141+
142+
def next_raises():
143+
try:
144+
{x:x for x in BrokenIter(next_raises=True)}
145+
except Exception as e:
146+
return e
147+
148+
for func, expected in [(init_raises, "BrokenIter(init_raises=True)"),
149+
(next_raises, "BrokenIter(next_raises=True)"),
150+
]:
151+
with self.subTest(func):
152+
exc = func()
153+
f = traceback.extract_tb(exc.__traceback__)[0]
154+
indent = 16
155+
co = func.__code__
156+
self.assertEqual(f.lineno, co.co_firstlineno + 2)
157+
self.assertEqual(f.end_lineno, co.co_firstlineno + 2)
158+
self.assertEqual(f.line[f.colno - indent : f.end_colno - indent],
159+
expected)
160+
130161

131162
if __name__ == "__main__":
132163
unittest.main()

Lib/test/test_iter.py

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from test.support import cpython_only
66
from test.support.os_helper import TESTFN, unlink
77
from test.support import check_free_after_iterating, ALWAYS_EQ, NEVER_EQ
8+
from test.support import BrokenIter
89
import pickle
910
import collections.abc
1011
import functools
@@ -1148,35 +1149,22 @@ def test_exception_locations(self):
11481149
# The location of an exception raised from __init__ or
11491150
# __next__ should should be the iterator expression
11501151

1151-
class Iter:
1152-
def __init__(self, init_raises=False, next_raises=False):
1153-
if init_raises:
1154-
1/0
1155-
self.next_raises = next_raises
1156-
1157-
def __next__(self):
1158-
if self.next_raises:
1159-
1/0
1160-
1161-
def __iter__(self):
1162-
return self
1163-
11641152
def init_raises():
11651153
try:
1166-
for x in Iter(init_raises=True):
1154+
for x in BrokenIter(init_raises=True):
11671155
pass
11681156
except Exception as e:
11691157
return e
11701158

11711159
def next_raises():
11721160
try:
1173-
for x in Iter(next_raises=True):
1161+
for x in BrokenIter(next_raises=True):
11741162
pass
11751163
except Exception as e:
11761164
return e
11771165

1178-
for func, expected in [(init_raises, "Iter(init_raises=True)"),
1179-
(next_raises, "Iter(next_raises=True)"),
1166+
for func, expected in [(init_raises, "BrokenIter(init_raises=True)"),
1167+
(next_raises, "BrokenIter(next_raises=True)"),
11801168
]:
11811169
with self.subTest(func):
11821170
exc = func()

Lib/test/test_listcomps.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import doctest
22
import textwrap
3+
import traceback
34
import types
45
import unittest
56

7+
from test.support import BrokenIter
8+
69

710
doctests = """
811
########### Tests borrowed from or inspired by test_genexps.py ############
@@ -706,6 +709,35 @@ def test_multiple_comprehension_name_reuse(self):
706709
self._check_in_scopes(code, {"x": 2, "y": [3]}, ns={"x": 3}, scopes=["class"])
707710
self._check_in_scopes(code, {"x": 2, "y": [2]}, ns={"x": 3}, scopes=["function", "module"])
708711

712+
def test_exception_locations(self):
713+
# The location of an exception raised from __init__ or
714+
# __next__ should should be the iterator expression
715+
716+
def init_raises():
717+
try:
718+
[x for x in BrokenIter(init_raises=True)]
719+
except Exception as e:
720+
return e
721+
722+
def next_raises():
723+
try:
724+
[x for x in BrokenIter(next_raises=True)]
725+
except Exception as e:
726+
return e
727+
728+
for func, expected in [(init_raises, "BrokenIter(init_raises=True)"),
729+
(next_raises, "BrokenIter(next_raises=True)"),
730+
]:
731+
with self.subTest(func):
732+
exc = func()
733+
f = traceback.extract_tb(exc.__traceback__)[0]
734+
indent = 16
735+
co = func.__code__
736+
self.assertEqual(f.lineno, co.co_firstlineno + 2)
737+
self.assertEqual(f.end_lineno, co.co_firstlineno + 2)
738+
self.assertEqual(f.line[f.colno - indent : f.end_colno - indent],
739+
expected)
740+
709741
__test__ = {'doctests' : doctests}
710742

711743
def load_tests(loader, tests, pattern):

Lib/test/test_setcomps.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import doctest
2+
import traceback
23
import unittest
34

5+
from test.support import BrokenIter
6+
47

58
doctests = """
69
########### Tests mostly copied from test_listcomps.py ############
@@ -148,6 +151,35 @@
148151
149152
"""
150153

154+
class SetComprehensionTest(unittest.TestCase):
155+
def test_exception_locations(self):
156+
# The location of an exception raised from __init__ or
157+
# __next__ should should be the iterator expression
158+
159+
def init_raises():
160+
try:
161+
{x for x in BrokenIter(init_raises=True)}
162+
except Exception as e:
163+
return e
164+
165+
def next_raises():
166+
try:
167+
{x for x in BrokenIter(next_raises=True)}
168+
except Exception as e:
169+
return e
170+
171+
for func, expected in [(init_raises, "BrokenIter(init_raises=True)"),
172+
(next_raises, "BrokenIter(next_raises=True)"),
173+
]:
174+
with self.subTest(func):
175+
exc = func()
176+
f = traceback.extract_tb(exc.__traceback__)[0]
177+
indent = 16
178+
co = func.__code__
179+
self.assertEqual(f.lineno, co.co_firstlineno + 2)
180+
self.assertEqual(f.end_lineno, co.co_firstlineno + 2)
181+
self.assertEqual(f.line[f.colno - indent : f.end_colno - indent],
182+
expected)
151183

152184
__test__ = {'doctests' : doctests}
153185

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix too-wide source location in exception tracebacks coming from broken
2+
iterables in comprehensions.

Python/compile.c

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5272,14 +5272,15 @@ compiler_sync_comprehension_generator(struct compiler *c, location loc,
52725272
}
52735273
if (IS_LABEL(start)) {
52745274
VISIT(c, expr, gen->iter);
5275-
ADDOP(c, loc, GET_ITER);
5275+
ADDOP(c, LOC(gen->iter), GET_ITER);
52765276
}
52775277
}
52785278
}
5279+
52795280
if (IS_LABEL(start)) {
52805281
depth++;
52815282
USE_LABEL(c, start);
5282-
ADDOP_JUMP(c, loc, FOR_ITER, anchor);
5283+
ADDOP_JUMP(c, LOC(gen->iter), FOR_ITER, anchor);
52835284
}
52845285
VISIT(c, expr, gen->target);
52855286

0 commit comments

Comments
 (0)