gh-117174: Add a new route in linecache to fetch interactive source code (#117500)

This commit is contained in:
Pablo Galindo Salgado 2025-03-10 21:54:05 +00:00 committed by GitHub
parent ecdf6b15b0
commit a931a8b324
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 142 additions and 93 deletions

View file

@ -24,6 +24,7 @@ import _colorize # type: ignore[import-not-found]
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
import ast import ast
import code import code
import linecache
from dataclasses import dataclass, field from dataclasses import dataclass, field
import os.path import os.path
import sys import sys
@ -205,6 +206,7 @@ class InteractiveColoredConsole(code.InteractiveConsole):
item = wrapper([stmt]) item = wrapper([stmt])
try: try:
code = self.compile.compiler(item, filename, the_symbol) code = self.compile.compiler(item, filename, the_symbol)
linecache._register_code(code, source, filename)
except SyntaxError as e: except SyntaxError as e:
if e.args[0] == "'await' outside function": if e.args[0] == "'await' outside function":
python = os.path.basename(sys.executable) python = os.path.basename(sys.executable)

View file

@ -26,7 +26,6 @@ allowing multiline input and multiline history entries.
from __future__ import annotations from __future__ import annotations
import _sitebuiltins import _sitebuiltins
import linecache
import functools import functools
import os import os
import sys import sys
@ -148,7 +147,6 @@ def run_multiline_interactive_console(
continue continue
input_name = f"<python-input-{input_n}>" input_name = f"<python-input-{input_n}>"
linecache._register_code(input_name, statement, "<stdin>") # type: ignore[attr-defined]
more = console.push(_strip_final_indent(statement), filename=input_name, _symbol="single") # type: ignore[call-arg] more = console.push(_strip_final_indent(statement), filename=input_name, _symbol="single") # type: ignore[call-arg]
assert not more assert not more
input_n += 1 input_n += 1

View file

@ -969,6 +969,8 @@ def findsource(object):
module = getmodule(object, file) module = getmodule(object, file)
if module: if module:
lines = linecache.getlines(file, module.__dict__) lines = linecache.getlines(file, module.__dict__)
if not lines and file.startswith('<') and hasattr(object, "__code__"):
lines = linecache._getlines_from_code(object.__code__)
else: else:
lines = linecache.getlines(file) lines = linecache.getlines(file)
if not lines: if not lines:

View file

@ -11,6 +11,7 @@ __all__ = ["getline", "clearcache", "checkcache", "lazycache"]
# The cache. Maps filenames to either a thunk which will provide source code, # The cache. Maps filenames to either a thunk which will provide source code,
# or a tuple (size, mtime, lines, fullname) once loaded. # or a tuple (size, mtime, lines, fullname) once loaded.
cache = {} cache = {}
_interactive_cache = {}
def clearcache(): def clearcache():
@ -44,6 +45,22 @@ def getlines(filename, module_globals=None):
return [] return []
def _getline_from_code(filename, lineno):
lines = _getlines_from_code(filename)
if 1 <= lineno <= len(lines):
return lines[lineno - 1]
return ''
def _getlines_from_code(code):
code_id = id(code)
if code_id in _interactive_cache:
entry = _interactive_cache[code_id]
if len(entry) != 1:
return _interactive_cache[code_id][2]
return []
def checkcache(filename=None): def checkcache(filename=None):
"""Discard cache entries that are out of date. """Discard cache entries that are out of date.
(This is not checked upon each call!)""" (This is not checked upon each call!)"""
@ -88,9 +105,13 @@ def updatecache(filename, module_globals=None):
# These imports are not at top level because linecache is in the critical # These imports are not at top level because linecache is in the critical
# path of the interpreter startup and importing os and sys take a lot of time # path of the interpreter startup and importing os and sys take a lot of time
# and slows down the startup sequence. # and slows down the startup sequence.
import os try:
import sys import os
import tokenize import sys
import tokenize
except ImportError:
# These import can fail if the interpreter is shutting down
return []
if filename in cache: if filename in cache:
if len(cache[filename]) != 1: if len(cache[filename]) != 1:
@ -196,8 +217,14 @@ def lazycache(filename, module_globals):
def _register_code(code, string, name): def _register_code(code, string, name):
cache[code] = ( entry = (len(string),
len(string), None,
None, [line + '\n' for line in string.splitlines()],
[line + '\n' for line in string.splitlines()], name)
name) stack = [code]
while stack:
code = stack.pop()
for const in code.co_consts:
if isinstance(const, type(code)):
stack.append(const)
_interactive_cache[id(code)] = entry

View file

@ -85,9 +85,7 @@ class Test_Exceptions(unittest.TestCase):
warnings = proc.err.splitlines() warnings = proc.err.splitlines()
self.assertEqual(warnings, [ self.assertEqual(warnings, [
b'<string>:6: RuntimeWarning: Testing PyErr_WarnEx', b'<string>:6: RuntimeWarning: Testing PyErr_WarnEx',
b' foo() # line 6',
b'<string>:9: RuntimeWarning: Testing PyErr_WarnEx', b'<string>:9: RuntimeWarning: Testing PyErr_WarnEx',
b' foo() # line 9',
]) ])
def test_warn_during_finalization(self): def test_warn_during_finalization(self):

View file

@ -1,4 +1,5 @@
# Sample script for use by test_gdb # Sample script for use by test_gdb
from _typing import _idfunc
def foo(a, b, c): def foo(a, b, c):
bar(a=a, b=b, c=c) bar(a=a, b=b, c=c)
@ -7,6 +8,6 @@ def bar(a, b, c):
baz(a, b, c) baz(a, b, c)
def baz(*args): def baz(*args):
id(42) _idfunc(42)
foo(1, 2, 3) foo(1, 2, 3)

View file

@ -20,14 +20,14 @@ class PyBtTests(DebuggerTests):
self.assertMultilineMatches(bt, self.assertMultilineMatches(bt,
r'''^.* r'''^.*
Traceback \(most recent call first\): Traceback \(most recent call first\):
<built-in method id of module object .*> <built-in method _idfunc of module object .*>
File ".*gdb_sample.py", line 10, in baz File ".*gdb_sample.py", line 11, in baz
id\(42\) _idfunc\(42\)
File ".*gdb_sample.py", line 7, in bar File ".*gdb_sample.py", line 8, in bar
baz\(a, b, c\) baz\(a, b, c\)
File ".*gdb_sample.py", line 4, in foo File ".*gdb_sample.py", line 5, in foo
bar\(a=a, b=b, c=c\) bar\(a=a, b=b, c=c\)
File ".*gdb_sample.py", line 12, in <module> File ".*gdb_sample.py", line 13, in <module>
foo\(1, 2, 3\) foo\(1, 2, 3\)
''') ''')
@ -39,11 +39,11 @@ Traceback \(most recent call first\):
cmds_after_breakpoint=['py-bt-full']) cmds_after_breakpoint=['py-bt-full'])
self.assertMultilineMatches(bt, self.assertMultilineMatches(bt,
r'''^.* r'''^.*
#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 7, in bar \(a=1, b=2, c=3\) #[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 8, in bar \(a=1, b=2, c=3\)
baz\(a, b, c\) baz\(a, b, c\)
#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 4, in foo \(a=1, b=2, c=3\) #[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 5, in foo \(a=1, b=2, c=3\)
bar\(a=a, b=b, c=c\) bar\(a=a, b=b, c=c\)
#[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 12, in <module> \(\) #[0-9]+ Frame 0x-?[0-9a-f]+, for file .*gdb_sample.py, line 13, in <module> \(\)
foo\(1, 2, 3\) foo\(1, 2, 3\)
''') ''')
@ -55,6 +55,7 @@ Traceback \(most recent call first\):
'Verify that "py-bt" indicates threads that are waiting for the GIL' 'Verify that "py-bt" indicates threads that are waiting for the GIL'
cmd = ''' cmd = '''
from threading import Thread from threading import Thread
from _typing import _idfunc
class TestThread(Thread): class TestThread(Thread):
# These threads would run forever, but we'll interrupt things with the # These threads would run forever, but we'll interrupt things with the
@ -70,7 +71,7 @@ for i in range(4):
t[i].start() t[i].start()
# Trigger a breakpoint on the main thread # Trigger a breakpoint on the main thread
id(42) _idfunc(42)
''' '''
# Verify with "py-bt": # Verify with "py-bt":
@ -90,8 +91,8 @@ id(42)
# unless we add LD_PRELOAD=PATH-TO-libpthread.so.1 as a workaround # unless we add LD_PRELOAD=PATH-TO-libpthread.so.1 as a workaround
def test_gc(self): def test_gc(self):
'Verify that "py-bt" indicates if a thread is garbage-collecting' 'Verify that "py-bt" indicates if a thread is garbage-collecting'
cmd = ('from gc import collect\n' cmd = ('from gc import collect; from _typing import _idfunc\n'
'id(42)\n' '_idfunc(42)\n'
'def foo():\n' 'def foo():\n'
' collect()\n' ' collect()\n'
'def bar():\n' 'def bar():\n'
@ -113,11 +114,12 @@ id(42)
"Python was compiled with optimizations") "Python was compiled with optimizations")
def test_wrapper_call(self): def test_wrapper_call(self):
cmd = textwrap.dedent(''' cmd = textwrap.dedent('''
from typing import _idfunc
class MyList(list): class MyList(list):
def __init__(self): def __init__(self):
super(*[]).__init__() # wrapper_call() super(*[]).__init__() # wrapper_call()
id("first break point") _idfunc("first break point")
l = MyList() l = MyList()
''') ''')
cmds_after_breakpoint = ['break wrapper_call', 'continue'] cmds_after_breakpoint = ['break wrapper_call', 'continue']

View file

@ -35,14 +35,14 @@ class PyListTests(DebuggerTests):
bt = self.get_stack_trace(script=SAMPLE_SCRIPT, bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
cmds_after_breakpoint=['py-list']) cmds_after_breakpoint=['py-list'])
self.assertListing(' 5 \n' self.assertListing(' 6 \n'
' 6 def bar(a, b, c):\n' ' 7 def bar(a, b, c):\n'
' 7 baz(a, b, c)\n' ' 8 baz(a, b, c)\n'
' 8 \n' ' 9 \n'
' 9 def baz(*args):\n' ' 10 def baz(*args):\n'
' >10 id(42)\n' ' >11 _idfunc(42)\n'
' 11 \n' ' 12 \n'
' 12 foo(1, 2, 3)\n', ' 13 foo(1, 2, 3)\n',
bt) bt)
def test_one_abs_arg(self): def test_one_abs_arg(self):
@ -50,25 +50,27 @@ class PyListTests(DebuggerTests):
bt = self.get_stack_trace(script=SAMPLE_SCRIPT, bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
cmds_after_breakpoint=['py-list 9']) cmds_after_breakpoint=['py-list 9'])
self.assertListing(' 9 def baz(*args):\n' self.assertListing(' 10 def baz(*args):\n'
' >10 id(42)\n' ' >11 _idfunc(42)\n'
' 11 \n' ' 12 \n'
' 12 foo(1, 2, 3)\n', ' 13 foo(1, 2, 3)\n',
bt) bt)
def test_two_abs_args(self): def test_two_abs_args(self):
'Verify the "py-list" command with two absolute arguments' 'Verify the "py-list" command with two absolute arguments'
bt = self.get_stack_trace(script=SAMPLE_SCRIPT, bt = self.get_stack_trace(script=SAMPLE_SCRIPT,
cmds_after_breakpoint=['py-list 1,3']) cmds_after_breakpoint=['py-list 1,4'])
self.assertListing(' 1 # Sample script for use by test_gdb\n' self.assertListing(' 1 # Sample script for use by test_gdb\n'
' 2 \n' ' 2 from _typing import _idfunc\n'
' 3 def foo(a, b, c):\n', ' 3 \n'
' 4 def foo(a, b, c):\n',
bt) bt)
SAMPLE_WITH_C_CALL = """ SAMPLE_WITH_C_CALL = """
from _testcapi import pyobject_vectorcall from _testcapi import pyobject_vectorcall
from _typing import _idfunc
def foo(a, b, c): def foo(a, b, c):
bar(a, b, c) bar(a, b, c)
@ -77,7 +79,7 @@ def bar(a, b, c):
pyobject_vectorcall(baz, (a, b, c), None) pyobject_vectorcall(baz, (a, b, c), None)
def baz(*args): def baz(*args):
id(42) _idfunc(42)
foo(1, 2, 3) foo(1, 2, 3)
@ -94,7 +96,7 @@ class StackNavigationTests(DebuggerTests):
cmds_after_breakpoint=['py-up', 'py-up']) cmds_after_breakpoint=['py-up', 'py-up'])
self.assertMultilineMatches(bt, self.assertMultilineMatches(bt,
r'''^.* r'''^.*
#[0-9]+ Frame 0x-?[0-9a-f]+, for file <string>, line 12, in baz \(args=\(1, 2, 3\)\) #[0-9]+ Frame 0x-?[0-9a-f]+, for file <string>, line 13, in baz \(args=\(1, 2, 3\)\)
#[0-9]+ <built-in method pyobject_vectorcall of module object at remote 0x[0-9a-f]+> #[0-9]+ <built-in method pyobject_vectorcall of module object at remote 0x[0-9a-f]+>
$''') $''')
@ -123,9 +125,9 @@ $''')
cmds_after_breakpoint=['py-up', 'py-up', 'py-down']) cmds_after_breakpoint=['py-up', 'py-up', 'py-down'])
self.assertMultilineMatches(bt, self.assertMultilineMatches(bt,
r'''^.* r'''^.*
#[0-9]+ Frame 0x-?[0-9a-f]+, for file <string>, line 12, in baz \(args=\(1, 2, 3\)\) #[0-9]+ Frame 0x-?[0-9a-f]+, for file <string>, line 13, in baz \(args=\(1, 2, 3\)\)
#[0-9]+ <built-in method pyobject_vectorcall of module object at remote 0x[0-9a-f]+> #[0-9]+ <built-in method pyobject_vectorcall of module object at remote 0x[0-9a-f]+>
#[0-9]+ Frame 0x-?[0-9a-f]+, for file <string>, line 12, in baz \(args=\(1, 2, 3\)\) #[0-9]+ Frame 0x-?[0-9a-f]+, for file <string>, line 13, in baz \(args=\(1, 2, 3\)\)
$''') $''')
class PyPrintTests(DebuggerTests): class PyPrintTests(DebuggerTests):

View file

@ -17,7 +17,7 @@ class PrettyPrintTests(DebuggerTests):
import_site=False): import_site=False):
# Given an input python source representation of data, # Given an input python source representation of data,
# run "python -c'id(DATA)'" under gdb with a breakpoint on # run "python -c'id(DATA)'" under gdb with a breakpoint on
# builtin_id and scrape out gdb's representation of the "op" # _typing__idfunc and scrape out gdb's representation of the "op"
# parameter, and verify that the gdb displays the same string # parameter, and verify that the gdb displays the same string
# #
# Verify that the gdb displays the expected string # Verify that the gdb displays the expected string
@ -29,6 +29,7 @@ class PrettyPrintTests(DebuggerTests):
# undecodable characters may lurk there in optimized mode # undecodable characters may lurk there in optimized mode
# (issue #19743). # (issue #19743).
cmds_after_breakpoint = cmds_after_breakpoint or ["backtrace 1"] cmds_after_breakpoint = cmds_after_breakpoint or ["backtrace 1"]
source = "from _typing import _idfunc\n" + source
gdb_output = self.get_stack_trace(source, breakpoint=BREAKPOINT_FN, gdb_output = self.get_stack_trace(source, breakpoint=BREAKPOINT_FN,
cmds_after_breakpoint=cmds_after_breakpoint, cmds_after_breakpoint=cmds_after_breakpoint,
import_site=import_site) import_site=import_site)
@ -36,10 +37,10 @@ class PrettyPrintTests(DebuggerTests):
# in its output, depending on the width of the terminal it's connected # in its output, depending on the width of the terminal it's connected
# to (using its "wrap_here" function) # to (using its "wrap_here" function)
m = re.search( m = re.search(
# Match '#0 builtin_id(self=..., v=...)' # Match '#0 _typing_idfunc(module=..., x=...)'
r'#0\s+builtin_id\s+\(self\=.*,\s+v=\s*(.*?)?\)' r'#0\s+_typing__idfunc\s+\(module\=.*,\s+x=\s*(.*?)?\)'
# Match ' at Python/bltinmodule.c'. # Match ' at Python/bltinmodule.c'.
# bpo-38239: builtin_id() is defined in Python/bltinmodule.c, # bpo-38239: typing_idfunc() is defined in Module/_typingmldule.c,
# but accept any "Directory\file.c" to support Link Time # but accept any "Directory\file.c" to support Link Time
# Optimization (LTO). # Optimization (LTO).
r'\s+at\s+\S*[A-Za-z]+/[A-Za-z0-9_-]+\.c', r'\s+at\s+\S*[A-Za-z]+/[A-Za-z0-9_-]+\.c',
@ -49,13 +50,13 @@ class PrettyPrintTests(DebuggerTests):
return m.group(1), gdb_output return m.group(1), gdb_output
def test_getting_backtrace(self): def test_getting_backtrace(self):
gdb_output = self.get_stack_trace('id(42)') gdb_output = self.get_stack_trace('from _typing import _idfunc;_idfunc(42)')
self.assertTrue(BREAKPOINT_FN in gdb_output) self.assertTrue(BREAKPOINT_FN in gdb_output)
def assertGdbRepr(self, val, exp_repr=None): def assertGdbRepr(self, val, exp_repr=None):
# Ensure that gdb's rendering of the value in a debugged process # Ensure that gdb's rendering of the value in a debugged process
# matches repr(value) in this process: # matches repr(value) in this process:
gdb_repr, gdb_output = self.get_gdb_repr('id(' + ascii(val) + ')') gdb_repr, gdb_output = self.get_gdb_repr('_idfunc(' + ascii(val) + ')')
if not exp_repr: if not exp_repr:
exp_repr = repr(val) exp_repr = repr(val)
self.assertEqual(gdb_repr, exp_repr, self.assertEqual(gdb_repr, exp_repr,
@ -173,7 +174,7 @@ class PrettyPrintTests(DebuggerTests):
# which happens on deletion: # which happens on deletion:
gdb_repr, gdb_output = self.get_gdb_repr('''s = set(['a','b']) gdb_repr, gdb_output = self.get_gdb_repr('''s = set(['a','b'])
s.remove('a') s.remove('a')
id(s)''') _idfunc(s)''')
self.assertEqual(gdb_repr, "{'b'}") self.assertEqual(gdb_repr, "{'b'}")
@support.requires_resource('cpu') @support.requires_resource('cpu')
@ -194,7 +195,7 @@ id(s)''')
try: try:
raise RuntimeError("I am an error") raise RuntimeError("I am an error")
except RuntimeError as e: except RuntimeError as e:
id(e) _idfunc(e)
''') ''')
self.assertEqual(gdb_repr, self.assertEqual(gdb_repr,
"RuntimeError('I am an error',)") "RuntimeError('I am an error',)")
@ -205,7 +206,7 @@ except RuntimeError as e:
try: try:
a = 1 / 0 a = 1 / 0
except ZeroDivisionError as e: except ZeroDivisionError as e:
id(e) _idfunc(e)
''') ''')
self.assertEqual(gdb_repr, self.assertEqual(gdb_repr,
"ZeroDivisionError('division by zero',)") "ZeroDivisionError('division by zero',)")
@ -217,7 +218,7 @@ class Foo:
pass pass
foo = Foo() foo = Foo()
foo.an_int = 42 foo.an_int = 42
id(foo)''') _idfunc(foo)''')
m = re.match(r'<Foo\(an_int=42\) at remote 0x-?[0-9a-f]+>', gdb_repr) m = re.match(r'<Foo\(an_int=42\) at remote 0x-?[0-9a-f]+>', gdb_repr)
self.assertTrue(m, self.assertTrue(m,
msg='Unexpected new-style class rendering %r' % gdb_repr) msg='Unexpected new-style class rendering %r' % gdb_repr)
@ -230,7 +231,7 @@ class Foo(list):
foo = Foo() foo = Foo()
foo += [1, 2, 3] foo += [1, 2, 3]
foo.an_int = 42 foo.an_int = 42
id(foo)''') _idfunc(foo)''')
m = re.match(r'<Foo\(an_int=42\) at remote 0x-?[0-9a-f]+>', gdb_repr) m = re.match(r'<Foo\(an_int=42\) at remote 0x-?[0-9a-f]+>', gdb_repr)
self.assertTrue(m, self.assertTrue(m,
@ -245,7 +246,7 @@ class Foo(tuple):
pass pass
foo = Foo((1, 2, 3)) foo = Foo((1, 2, 3))
foo.an_int = 42 foo.an_int = 42
id(foo)''') _idfunc(foo)''')
m = re.match(r'<Foo\(an_int=42\) at remote 0x-?[0-9a-f]+>', gdb_repr) m = re.match(r'<Foo\(an_int=42\) at remote 0x-?[0-9a-f]+>', gdb_repr)
self.assertTrue(m, self.assertTrue(m,
@ -283,8 +284,8 @@ id(foo)''')
def test_NULL_ptr(self): def test_NULL_ptr(self):
'Ensure that a NULL PyObject* is handled gracefully' 'Ensure that a NULL PyObject* is handled gracefully'
gdb_repr, gdb_output = ( gdb_repr, gdb_output = (
self.get_gdb_repr('id(42)', self.get_gdb_repr('_idfunc(42)',
cmds_after_breakpoint=['set variable v=0', cmds_after_breakpoint=['set variable x=0',
'backtrace']) 'backtrace'])
) )
@ -292,25 +293,25 @@ id(foo)''')
def test_NULL_ob_type(self): def test_NULL_ob_type(self):
'Ensure that a PyObject* with NULL ob_type is handled gracefully' 'Ensure that a PyObject* with NULL ob_type is handled gracefully'
self.assertSane('id(42)', self.assertSane('_idfunc(42)',
'set v->ob_type=0') 'set x->ob_type=0')
def test_corrupt_ob_type(self): def test_corrupt_ob_type(self):
'Ensure that a PyObject* with a corrupt ob_type is handled gracefully' 'Ensure that a PyObject* with a corrupt ob_type is handled gracefully'
self.assertSane('id(42)', self.assertSane('_idfunc(42)',
'set v->ob_type=0xDEADBEEF', 'set x->ob_type=0xDEADBEEF',
exprepr='42') exprepr='42')
def test_corrupt_tp_flags(self): def test_corrupt_tp_flags(self):
'Ensure that a PyObject* with a type with corrupt tp_flags is handled' 'Ensure that a PyObject* with a type with corrupt tp_flags is handled'
self.assertSane('id(42)', self.assertSane('_idfunc(42)',
'set v->ob_type->tp_flags=0x0', 'set x->ob_type->tp_flags=0x0',
exprepr='42') exprepr='42')
def test_corrupt_tp_name(self): def test_corrupt_tp_name(self):
'Ensure that a PyObject* with a type with corrupt tp_name is handled' 'Ensure that a PyObject* with a type with corrupt tp_name is handled'
self.assertSane('id(42)', self.assertSane('_idfunc(42)',
'set v->ob_type->tp_name=0xDEADBEEF', 'set x->ob_type->tp_name=0xDEADBEEF',
exprepr='42') exprepr='42')
def test_builtins_help(self): def test_builtins_help(self):
@ -321,7 +322,7 @@ id(foo)''')
# (this was the issue causing tracebacks in # (this was the issue causing tracebacks in
# http://bugs.python.org/issue8032#msg100537 ) # http://bugs.python.org/issue8032#msg100537 )
gdb_repr, gdb_output = self.get_gdb_repr('id(__builtins__.help)', import_site=True) gdb_repr, gdb_output = self.get_gdb_repr('_idfunc(__builtins__.help)', import_site=True)
m = re.match(r'<_Helper\(\) at remote 0x-?[0-9a-f]+>', gdb_repr) m = re.match(r'<_Helper\(\) at remote 0x-?[0-9a-f]+>', gdb_repr)
self.assertTrue(m, self.assertTrue(m,
@ -331,18 +332,18 @@ id(foo)''')
'''Ensure that a reference loop involving a list doesn't lead proxyval '''Ensure that a reference loop involving a list doesn't lead proxyval
into an infinite loop:''' into an infinite loop:'''
gdb_repr, gdb_output = \ gdb_repr, gdb_output = \
self.get_gdb_repr("a = [3, 4, 5] ; a.append(a) ; id(a)") self.get_gdb_repr("a = [3, 4, 5] ; a.append(a) ; _idfunc(a)")
self.assertEqual(gdb_repr, '[3, 4, 5, [...]]') self.assertEqual(gdb_repr, '[3, 4, 5, [...]]')
gdb_repr, gdb_output = \ gdb_repr, gdb_output = \
self.get_gdb_repr("a = [3, 4, 5] ; b = [a] ; a.append(b) ; id(a)") self.get_gdb_repr("a = [3, 4, 5] ; b = [a] ; a.append(b) ; _idfunc(a)")
self.assertEqual(gdb_repr, '[3, 4, 5, [[...]]]') self.assertEqual(gdb_repr, '[3, 4, 5, [[...]]]')
def test_selfreferential_dict(self): def test_selfreferential_dict(self):
'''Ensure that a reference loop involving a dict doesn't lead proxyval '''Ensure that a reference loop involving a dict doesn't lead proxyval
into an infinite loop:''' into an infinite loop:'''
gdb_repr, gdb_output = \ gdb_repr, gdb_output = \
self.get_gdb_repr("a = {} ; b = {'bar':a} ; a['foo'] = b ; id(a)") self.get_gdb_repr("a = {} ; b = {'bar':a} ; a['foo'] = b ; _idfunc(a)")
self.assertEqual(gdb_repr, "{'foo': {'bar': {...}}}") self.assertEqual(gdb_repr, "{'foo': {'bar': {...}}}")
@ -353,7 +354,7 @@ class Foo:
pass pass
foo = Foo() foo = Foo()
foo.an_attr = foo foo.an_attr = foo
id(foo)''') _idfunc(foo)''')
self.assertTrue(re.match(r'<Foo\(an_attr=<\.\.\.>\) at remote 0x-?[0-9a-f]+>', self.assertTrue(re.match(r'<Foo\(an_attr=<\.\.\.>\) at remote 0x-?[0-9a-f]+>',
gdb_repr), gdb_repr),
'Unexpected gdb representation: %r\n%s' % \ 'Unexpected gdb representation: %r\n%s' % \
@ -366,7 +367,7 @@ class Foo(object):
pass pass
foo = Foo() foo = Foo()
foo.an_attr = foo foo.an_attr = foo
id(foo)''') _idfunc(foo)''')
self.assertTrue(re.match(r'<Foo\(an_attr=<\.\.\.>\) at remote 0x-?[0-9a-f]+>', self.assertTrue(re.match(r'<Foo\(an_attr=<\.\.\.>\) at remote 0x-?[0-9a-f]+>',
gdb_repr), gdb_repr),
'Unexpected gdb representation: %r\n%s' % \ 'Unexpected gdb representation: %r\n%s' % \
@ -380,7 +381,7 @@ a = Foo()
b = Foo() b = Foo()
a.an_attr = b a.an_attr = b
b.an_attr = a b.an_attr = a
id(a)''') _idfunc(a)''')
self.assertTrue(re.match(r'<Foo\(an_attr=<Foo\(an_attr=<\.\.\.>\) at remote 0x-?[0-9a-f]+>\) at remote 0x-?[0-9a-f]+>', self.assertTrue(re.match(r'<Foo\(an_attr=<Foo\(an_attr=<\.\.\.>\) at remote 0x-?[0-9a-f]+>\) at remote 0x-?[0-9a-f]+>',
gdb_repr), gdb_repr),
'Unexpected gdb representation: %r\n%s' % \ 'Unexpected gdb representation: %r\n%s' % \
@ -388,7 +389,7 @@ id(a)''')
def test_truncation(self): def test_truncation(self):
'Verify that very long output is truncated' 'Verify that very long output is truncated'
gdb_repr, gdb_output = self.get_gdb_repr('id(list(range(1000)))') gdb_repr, gdb_output = self.get_gdb_repr('_idfunc(list(range(1000)))')
self.assertEqual(gdb_repr, self.assertEqual(gdb_repr,
"[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, " "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, "
"14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, " "14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, "
@ -415,7 +416,7 @@ id(a)''')
1024 + len('...(truncated)')) 1024 + len('...(truncated)'))
def test_builtin_method(self): def test_builtin_method(self):
gdb_repr, gdb_output = self.get_gdb_repr('import sys; id(sys.stdout.readlines)') gdb_repr, gdb_output = self.get_gdb_repr('import sys; _idfunc(sys.stdout.readlines)')
self.assertTrue(re.match(r'<built-in method readlines of _io.TextIOWrapper object at remote 0x-?[0-9a-f]+>', self.assertTrue(re.match(r'<built-in method readlines of _io.TextIOWrapper object at remote 0x-?[0-9a-f]+>',
gdb_repr), gdb_repr),
'Unexpected gdb representation: %r\n%s' % \ 'Unexpected gdb representation: %r\n%s' % \
@ -424,15 +425,16 @@ id(a)''')
def test_frames(self): def test_frames(self):
gdb_output = self.get_stack_trace(''' gdb_output = self.get_stack_trace('''
import sys import sys
from _typing import _idfunc
def foo(a, b, c): def foo(a, b, c):
return sys._getframe(0) return sys._getframe(0)
f = foo(3, 4, 5) f = foo(3, 4, 5)
id(f)''', _idfunc(f)''',
breakpoint='builtin_id', breakpoint='_typing__idfunc',
cmds_after_breakpoint=['print (PyFrameObject*)v'] cmds_after_breakpoint=['print (PyFrameObject*)x']
) )
self.assertTrue(re.match(r'.*\s+\$1 =\s+Frame 0x-?[0-9a-f]+, for file <string>, line 4, in foo \(a=3.*', self.assertTrue(re.match(r'.*\s+\$1 =\s+Frame 0x-?[0-9a-f]+, for file <string>, line 5, in foo \(a=3.*',
gdb_output, gdb_output,
re.DOTALL), re.DOTALL),
'Unexpected gdb representation: %r\n%s' % (gdb_output, gdb_output)) 'Unexpected gdb representation: %r\n%s' % (gdb_output, gdb_output))

View file

@ -16,7 +16,7 @@ CHECKOUT_HOOK_PATH = os.path.join(os.path.dirname(sys.executable),
'python-gdb.py') 'python-gdb.py')
SAMPLE_SCRIPT = os.path.join(os.path.dirname(__file__), 'gdb_sample.py') SAMPLE_SCRIPT = os.path.join(os.path.dirname(__file__), 'gdb_sample.py')
BREAKPOINT_FN = 'builtin_id' BREAKPOINT_FN = '_typing__idfunc'
PYTHONHASHSEED = '123' PYTHONHASHSEED = '123'

View file

@ -4558,11 +4558,11 @@ class MiscIOTest(unittest.TestCase):
''') ''')
proc = assert_python_ok('-X', 'warn_default_encoding', '-c', code) proc = assert_python_ok('-X', 'warn_default_encoding', '-c', code)
warnings = proc.err.splitlines() warnings = proc.err.splitlines()
self.assertEqual(len(warnings), 4) self.assertEqual(len(warnings), 2)
self.assertTrue( self.assertTrue(
warnings[0].startswith(b"<string>:5: EncodingWarning: ")) warnings[0].startswith(b"<string>:5: EncodingWarning: "))
self.assertTrue( self.assertTrue(
warnings[2].startswith(b"<string>:8: EncodingWarning: ")) warnings[1].startswith(b"<string>:8: EncodingWarning: "))
def test_text_encoding(self): def test_text_encoding(self):
# PEP 597, bpo-47000. io.text_encoding() returns "locale" or "utf-8" # PEP 597, bpo-47000. io.text_encoding() returns "locale" or "utf-8"

View file

@ -8,6 +8,7 @@ import tokenize
from importlib.machinery import ModuleSpec from importlib.machinery import ModuleSpec
from test import support from test import support
from test.support import os_helper from test.support import os_helper
from test.support.script_helper import assert_python_ok
FILENAME = linecache.__file__ FILENAME = linecache.__file__
@ -311,6 +312,12 @@ class LineCacheTests(unittest.TestCase):
# just to be sure that we did not mess with cache # just to be sure that we did not mess with cache
linecache.clearcache() linecache.clearcache()
def test_linecache_python_string(self):
cmdline = "import linecache;assert len(linecache.cache) == 0"
retcode, stdout, stderr = assert_python_ok('-c', cmdline)
self.assertEqual(retcode, 0)
self.assertEqual(stdout, b'')
self.assertEqual(stderr, b'')
class LineCacheInvalidationTests(unittest.TestCase): class LineCacheInvalidationTests(unittest.TestCase):
def setUp(self): def setUp(self):

View file

@ -213,7 +213,7 @@ class TestInteractiveInterpreter(unittest.TestCase):
p.stdin.write(user_input) p.stdin.write(user_input)
user_input2 = dedent(""" user_input2 = dedent("""
import linecache import linecache
print(linecache.cache['<stdin>-1']) print(linecache._interactive_cache[id(foo.__code__)])
""") """)
p.stdin.write(user_input2) p.stdin.write(user_input2)
output = kill_python(p) output = kill_python(p)

View file

@ -1804,9 +1804,9 @@ class RunFuncTestCase(BaseTestCase):
cp = subprocess.run([sys.executable, "-Xwarn_default_encoding", "-c", code], cp = subprocess.run([sys.executable, "-Xwarn_default_encoding", "-c", code],
capture_output=True) capture_output=True)
lines = cp.stderr.splitlines() lines = cp.stderr.splitlines()
self.assertEqual(len(lines), 4, lines) self.assertEqual(len(lines), 2, lines)
self.assertTrue(lines[0].startswith(b"<string>:2: EncodingWarning: ")) self.assertTrue(lines[0].startswith(b"<string>:2: EncodingWarning: "))
self.assertTrue(lines[2].startswith(b"<string>:3: EncodingWarning: ")) self.assertTrue(lines[1].startswith(b"<string>:3: EncodingWarning: "))
def _get_test_grp_name(): def _get_test_grp_name():

View file

@ -288,11 +288,11 @@ class FrameSummary:
""" """
__slots__ = ('filename', 'lineno', 'end_lineno', 'colno', 'end_colno', __slots__ = ('filename', 'lineno', 'end_lineno', 'colno', 'end_colno',
'name', '_lines', '_lines_dedented', 'locals') 'name', '_lines', '_lines_dedented', 'locals', '_code')
def __init__(self, filename, lineno, name, *, lookup_line=True, def __init__(self, filename, lineno, name, *, lookup_line=True,
locals=None, line=None, locals=None, line=None,
end_lineno=None, colno=None, end_colno=None): end_lineno=None, colno=None, end_colno=None, **kwargs):
"""Construct a FrameSummary. """Construct a FrameSummary.
:param lookup_line: If True, `linecache` is consulted for the source :param lookup_line: If True, `linecache` is consulted for the source
@ -308,6 +308,7 @@ class FrameSummary:
self.colno = colno self.colno = colno
self.end_colno = end_colno self.end_colno = end_colno
self.name = name self.name = name
self._code = kwargs.get("_code")
self._lines = line self._lines = line
self._lines_dedented = None self._lines_dedented = None
if lookup_line: if lookup_line:
@ -347,7 +348,10 @@ class FrameSummary:
lines = [] lines = []
for lineno in range(self.lineno, self.end_lineno + 1): for lineno in range(self.lineno, self.end_lineno + 1):
# treat errors (empty string) and empty lines (newline) as the same # treat errors (empty string) and empty lines (newline) as the same
lines.append(linecache.getline(self.filename, lineno).rstrip()) line = linecache.getline(self.filename, lineno).rstrip()
if not line and self._code is not None and self.filename.startswith("<"):
line = linecache._getline_from_code(self._code, lineno).rstrip()
lines.append(line)
self._lines = "\n".join(lines) + "\n" self._lines = "\n".join(lines) + "\n"
@property @property
@ -484,9 +488,13 @@ class StackSummary(list):
f_locals = f.f_locals f_locals = f.f_locals
else: else:
f_locals = None f_locals = None
result.append(FrameSummary( result.append(
filename, lineno, name, lookup_line=False, locals=f_locals, FrameSummary(filename, lineno, name,
end_lineno=end_lineno, colno=colno, end_colno=end_colno)) lookup_line=False, locals=f_locals,
end_lineno=end_lineno, colno=colno, end_colno=end_colno,
_code=f.f_code,
)
)
for filename in fnames: for filename in fnames:
linecache.checkcache(filename) linecache.checkcache(filename)

View file

@ -1410,7 +1410,7 @@ run_mod(mod_ty mod, PyObject *filename, PyObject *globals, PyObject *locals,
PyObject* result = PyObject_CallFunction( PyObject* result = PyObject_CallFunction(
print_tb_func, "OOO", print_tb_func, "OOO",
interactive_filename, co,
interactive_src, interactive_src,
filename filename
); );