Skip to content

Commit a931a8b

Browse files
authored
gh-117174: Add a new route in linecache to fetch interactive source code (#117500)
1 parent ecdf6b1 commit a931a8b

16 files changed

+142
-93
lines changed

Lib/_pyrepl/console.py

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from abc import ABC, abstractmethod
2525
import ast
2626
import code
27+
import linecache
2728
from dataclasses import dataclass, field
2829
import os.path
2930
import sys
@@ -205,6 +206,7 @@ def runsource(self, source, filename="<input>", symbol="single"):
205206
item = wrapper([stmt])
206207
try:
207208
code = self.compile.compiler(item, filename, the_symbol)
209+
linecache._register_code(code, source, filename)
208210
except SyntaxError as e:
209211
if e.args[0] == "'await' outside function":
210212
python = os.path.basename(sys.executable)

Lib/_pyrepl/simple_interact.py

-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
from __future__ import annotations
2727

2828
import _sitebuiltins
29-
import linecache
3029
import functools
3130
import os
3231
import sys
@@ -148,7 +147,6 @@ def maybe_run_command(statement: str) -> bool:
148147
continue
149148

150149
input_name = f"<python-input-{input_n}>"
151-
linecache._register_code(input_name, statement, "<stdin>") # type: ignore[attr-defined]
152150
more = console.push(_strip_final_indent(statement), filename=input_name, _symbol="single") # type: ignore[call-arg]
153151
assert not more
154152
input_n += 1

Lib/inspect.py

+2
Original file line numberDiff line numberDiff line change
@@ -969,6 +969,8 @@ def findsource(object):
969969
module = getmodule(object, file)
970970
if module:
971971
lines = linecache.getlines(file, module.__dict__)
972+
if not lines and file.startswith('<') and hasattr(object, "__code__"):
973+
lines = linecache._getlines_from_code(object.__code__)
972974
else:
973975
lines = linecache.getlines(file)
974976
if not lines:

Lib/linecache.py

+35-8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# The cache. Maps filenames to either a thunk which will provide source code,
1212
# or a tuple (size, mtime, lines, fullname) once loaded.
1313
cache = {}
14+
_interactive_cache = {}
1415

1516

1617
def clearcache():
@@ -44,6 +45,22 @@ def getlines(filename, module_globals=None):
4445
return []
4546

4647

48+
def _getline_from_code(filename, lineno):
49+
lines = _getlines_from_code(filename)
50+
if 1 <= lineno <= len(lines):
51+
return lines[lineno - 1]
52+
return ''
53+
54+
55+
def _getlines_from_code(code):
56+
code_id = id(code)
57+
if code_id in _interactive_cache:
58+
entry = _interactive_cache[code_id]
59+
if len(entry) != 1:
60+
return _interactive_cache[code_id][2]
61+
return []
62+
63+
4764
def checkcache(filename=None):
4865
"""Discard cache entries that are out of date.
4966
(This is not checked upon each call!)"""
@@ -88,9 +105,13 @@ def updatecache(filename, module_globals=None):
88105
# These imports are not at top level because linecache is in the critical
89106
# path of the interpreter startup and importing os and sys take a lot of time
90107
# and slows down the startup sequence.
91-
import os
92-
import sys
93-
import tokenize
108+
try:
109+
import os
110+
import sys
111+
import tokenize
112+
except ImportError:
113+
# These import can fail if the interpreter is shutting down
114+
return []
94115

95116
if filename in cache:
96117
if len(cache[filename]) != 1:
@@ -196,8 +217,14 @@ def get_lines(name=name, *args, **kwargs):
196217

197218

198219
def _register_code(code, string, name):
199-
cache[code] = (
200-
len(string),
201-
None,
202-
[line + '\n' for line in string.splitlines()],
203-
name)
220+
entry = (len(string),
221+
None,
222+
[line + '\n' for line in string.splitlines()],
223+
name)
224+
stack = [code]
225+
while stack:
226+
code = stack.pop()
227+
for const in code.co_consts:
228+
if isinstance(const, type(code)):
229+
stack.append(const)
230+
_interactive_cache[id(code)] = entry

Lib/test/test_capi/test_exceptions.py

-2
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,7 @@ def foo():
8585
warnings = proc.err.splitlines()
8686
self.assertEqual(warnings, [
8787
b'<string>:6: RuntimeWarning: Testing PyErr_WarnEx',
88-
b' foo() # line 6',
8988
b'<string>:9: RuntimeWarning: Testing PyErr_WarnEx',
90-
b' foo() # line 9',
9189
])
9290

9391
def test_warn_during_finalization(self):

Lib/test/test_gdb/gdb_sample.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Sample script for use by test_gdb
2+
from _typing import _idfunc
23

34
def foo(a, b, c):
45
bar(a=a, b=b, c=c)
@@ -7,6 +8,6 @@ def bar(a, b, c):
78
baz(a, b, c)
89

910
def baz(*args):
10-
id(42)
11+
_idfunc(42)
1112

1213
foo(1, 2, 3)

Lib/test/test_gdb/test_backtrace.py

+15-13
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ def test_bt(self):
2020
self.assertMultilineMatches(bt,
2121
r'''^.*
2222
Traceback \(most recent call first\):
23-
<built-in method id of module object .*>
24-
File ".*gdb_sample.py", line 10, in baz
25-
id\(42\)
26-
File ".*gdb_sample.py", line 7, in bar
23+
<built-in method _idfunc of module object .*>
24+
File ".*gdb_sample.py", line 11, in baz
25+
_idfunc\(42\)
26+
File ".*gdb_sample.py", line 8, in bar
2727
baz\(a, b, c\)
28-
File ".*gdb_sample.py", line 4, in foo
28+
File ".*gdb_sample.py", line 5, in foo
2929
bar\(a=a, b=b, c=c\)
30-
File ".*gdb_sample.py", line 12, in <module>
30+
File ".*gdb_sample.py", line 13, in <module>
3131
foo\(1, 2, 3\)
3232
''')
3333

@@ -39,11 +39,11 @@ def test_bt_full(self):
3939
cmds_after_breakpoint=['py-bt-full'])
4040
self.assertMultilineMatches(bt,
4141
r'''^.*
42-
#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 7, in bar \(a=1, b=2, c=3\)
42+
#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 8, in bar \(a=1, b=2, c=3\)
4343
baz\(a, b, c\)
44-
#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 4, in foo \(a=1, b=2, c=3\)
44+
#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 5, in foo \(a=1, b=2, c=3\)
4545
bar\(a=a, b=b, c=c\)
46-
#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 12, in <module> \(\)
46+
#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 13, in <module> \(\)
4747
foo\(1, 2, 3\)
4848
''')
4949

@@ -55,6 +55,7 @@ def test_threads(self):
5555
'Verify that "py-bt" indicates threads that are waiting for the GIL'
5656
cmd = '''
5757
from threading import Thread
58+
from _typing import _idfunc
5859
5960
class TestThread(Thread):
6061
# These threads would run forever, but we'll interrupt things with the
@@ -70,7 +71,7 @@ def run(self):
7071
t[i].start()
7172
7273
# Trigger a breakpoint on the main thread
73-
id(42)
74+
_idfunc(42)
7475
7576
'''
7677
# Verify with "py-bt":
@@ -90,8 +91,8 @@ def run(self):
9091
# unless we add LD_PRELOAD=PATH-TO-libpthread.so.1 as a workaround
9192
def test_gc(self):
9293
'Verify that "py-bt" indicates if a thread is garbage-collecting'
93-
cmd = ('from gc import collect\n'
94-
'id(42)\n'
94+
cmd = ('from gc import collect; from _typing import _idfunc\n'
95+
'_idfunc(42)\n'
9596
'def foo():\n'
9697
' collect()\n'
9798
'def bar():\n'
@@ -113,11 +114,12 @@ def test_gc(self):
113114
"Python was compiled with optimizations")
114115
def test_wrapper_call(self):
115116
cmd = textwrap.dedent('''
117+
from typing import _idfunc
116118
class MyList(list):
117119
def __init__(self):
118120
super(*[]).__init__() # wrapper_call()
119121
120-
id("first break point")
122+
_idfunc("first break point")
121123
l = MyList()
122124
''')
123125
cmds_after_breakpoint = ['break wrapper_call', 'continue']

Lib/test/test_gdb/test_misc.py

+21-19
Original file line numberDiff line numberDiff line change
@@ -35,40 +35,42 @@ def test_basic_command(self):
3535
bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
3636
cmds_after_breakpoint=['py-list'])
3737

38-
self.assertListing(' 5 \n'
39-
' 6 def bar(a, b, c):\n'
40-
' 7 baz(a, b, c)\n'
41-
' 8 \n'
42-
' 9 def baz(*args):\n'
43-
' >10 id(42)\n'
44-
' 11 \n'
45-
' 12 foo(1, 2, 3)\n',
38+
self.assertListing(' 6 \n'
39+
' 7 def bar(a, b, c):\n'
40+
' 8 baz(a, b, c)\n'
41+
' 9 \n'
42+
' 10 def baz(*args):\n'
43+
' >11 _idfunc(42)\n'
44+
' 12 \n'
45+
' 13 foo(1, 2, 3)\n',
4646
bt)
4747

4848
def test_one_abs_arg(self):
4949
'Verify the "py-list" command with one absolute argument'
5050
bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
5151
cmds_after_breakpoint=['py-list 9'])
5252

53-
self.assertListing(' 9 def baz(*args):\n'
54-
' >10 id(42)\n'
55-
' 11 \n'
56-
' 12 foo(1, 2, 3)\n',
53+
self.assertListing(' 10 def baz(*args):\n'
54+
' >11 _idfunc(42)\n'
55+
' 12 \n'
56+
' 13 foo(1, 2, 3)\n',
5757
bt)
5858

5959
def test_two_abs_args(self):
6060
'Verify the "py-list" command with two absolute arguments'
6161
bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
62-
cmds_after_breakpoint=['py-list 1,3'])
62+
cmds_after_breakpoint=['py-list 1,4'])
6363

6464
self.assertListing(' 1 # Sample script for use by test_gdb\n'
65-
' 2 \n'
66-
' 3 def foo(a, b, c):\n',
65+
' 2 from _typing import _idfunc\n'
66+
' 3 \n'
67+
' 4 def foo(a, b, c):\n',
6768
bt)
6869

6970
SAMPLE_WITH_C_CALL = """
7071
7172
from _testcapi import pyobject_vectorcall
73+
from _typing import _idfunc
7274
7375
def foo(a, b, c):
7476
bar(a, b, c)
@@ -77,7 +79,7 @@ def bar(a, b, c):
7779
pyobject_vectorcall(baz, (a, b, c), None)
7880
7981
def baz(*args):
80-
id(42)
82+
_idfunc(42)
8183
8284
foo(1, 2, 3)
8385
@@ -94,7 +96,7 @@ def test_pyup_command(self):
9496
cmds_after_breakpoint=['py-up', 'py-up'])
9597
self.assertMultilineMatches(bt,
9698
r'''^.*
97-
#[0-9]+ Frame 0x-?[0-9a-f]+, for file <string>, line 12, in baz \(args=\(1, 2, 3\)\)
99+
#[0-9]+ Frame 0x-?[0-9a-f]+, for file <string>, line 13, in baz \(args=\(1, 2, 3\)\)
98100
#[0-9]+ <built-in method pyobject_vectorcall of module object at remote 0x[0-9a-f]+>
99101
$''')
100102

@@ -123,9 +125,9 @@ def test_up_then_down(self):
123125
cmds_after_breakpoint=['py-up', 'py-up', 'py-down'])
124126
self.assertMultilineMatches(bt,
125127
r'''^.*
126-
#[0-9]+ Frame 0x-?[0-9a-f]+, for file <string>, line 12, in baz \(args=\(1, 2, 3\)\)
128+
#[0-9]+ Frame 0x-?[0-9a-f]+, for file <string>, line 13, in baz \(args=\(1, 2, 3\)\)
127129
#[0-9]+ <built-in method pyobject_vectorcall of module object at remote 0x[0-9a-f]+>
128-
#[0-9]+ Frame 0x-?[0-9a-f]+, for file <string>, line 12, in baz \(args=\(1, 2, 3\)\)
130+
#[0-9]+ Frame 0x-?[0-9a-f]+, for file <string>, line 13, in baz \(args=\(1, 2, 3\)\)
129131
$''')
130132

131133
class PyPrintTests(DebuggerTests):

0 commit comments

Comments
 (0)