Skip to content

Commit c4142af

Browse files
authored
Fix internal error when source is modified (#13884)
Fix IndexError when retrieving start lineno
1 parent 66834f3 commit c4142af

File tree

3 files changed

+29
-0
lines changed

3 files changed

+29
-0
lines changed

changelog/13884.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed rare internal IndexError caused by `builtins.compile` being overridden in client code.

src/_pytest/_code/source.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> tuple[int, int | None
168168
values.append(val[0].lineno - 1 - 1)
169169
values.sort()
170170
insert_index = bisect_right(values, lineno)
171+
if insert_index == 0:
172+
return 0, None
171173
start = values[insert_index - 1]
172174
if insert_index >= len(values):
173175
end = None
@@ -216,6 +218,7 @@ def getstatementrange_ast(
216218
pass
217219

218220
# The end might still point to a comment or empty line, correct it.
221+
end = min(end, len(source.lines))
219222
while end:
220223
line = source.lines[end - 1].lstrip()
221224
if line.startswith("#") or not line:

testing/code/test_source.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import sys
88
import textwrap
99
from typing import Any
10+
from unittest.mock import patch
1011

1112
from _pytest._code import Code
1213
from _pytest._code import Frame
@@ -647,3 +648,27 @@ def __init__(self, *args):
647648
# fmt: on
648649
values = [i for i in x.source.lines if i.strip()]
649650
assert len(values) == 4
651+
652+
653+
def test_patched_compile() -> None:
654+
# ensure Source doesn't break
655+
# when compile() modifies code dynamically
656+
from builtins import compile
657+
658+
def patched_compile1(_, *args, **kwargs):
659+
return compile("", *args, **kwargs)
660+
661+
with patch("builtins.compile", new=patched_compile1):
662+
Source(patched_compile1).getstatement(1)
663+
664+
# fmt: off
665+
def patched_compile2(_, *args, **kwargs):
666+
667+
# first line of this function (the one above this one) must be empty
668+
# LINES must be equal or higher than number of lines of this function
669+
LINES = 99
670+
return compile("\ndef a():\n" + "\n" * LINES + " pass", *args, **kwargs)
671+
# fmt: on
672+
673+
with patch("builtins.compile", new=patched_compile2):
674+
Source(patched_compile2).getstatement(1)

0 commit comments

Comments
 (0)