Skip to content

Commit e1d8c65

Browse files
authored
gh-110805: Allow the repl to show source code and complete tracebacks (#110775)
1 parent 898f531 commit e1d8c65

File tree

11 files changed

+191
-19
lines changed

11 files changed

+191
-19
lines changed

Include/internal/pycore_interp.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ struct _is {
233233

234234
/* the initial PyInterpreterState.threads.head */
235235
PyThreadState _initial_thread;
236+
Py_ssize_t _interactive_src_count;
236237
};
237238

238239

Include/internal/pycore_parser.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,17 @@ extern struct _mod* _PyParser_ASTFromFile(
5858
PyCompilerFlags *flags,
5959
int *errcode,
6060
PyArena *arena);
61-
61+
extern struct _mod* _PyParser_InteractiveASTFromFile(
62+
FILE *fp,
63+
PyObject *filename_ob,
64+
const char *enc,
65+
int mode,
66+
const char *ps1,
67+
const char *ps2,
68+
PyCompilerFlags *flags,
69+
int *errcode,
70+
PyObject **interactive_src,
71+
PyArena *arena);
6272

6373
#ifdef __cplusplus
6474
}

Lib/linecache.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,10 @@ def lazycache(filename, module_globals):
180180
cache[filename] = (get_lines,)
181181
return True
182182
return False
183+
184+
def _register_code(code, string, name):
185+
cache[code] = (
186+
len(string),
187+
None,
188+
[line + '\n' for line in string.splitlines()],
189+
name)

Lib/test/test_cmd_line_script.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ def check_repl_stderr_flush(self, separate_stderr=False):
203203
stderr = p.stderr if separate_stderr else p.stdout
204204
self.assertIn(b'Traceback ', stderr.readline())
205205
self.assertIn(b'File "<stdin>"', stderr.readline())
206+
self.assertIn(b'1/0', stderr.readline())
207+
self.assertIn(b' ~^~', stderr.readline())
206208
self.assertIn(b'ZeroDivisionError', stderr.readline())
207209

208210
def test_repl_stdout_flush(self):

Lib/test/test_repl.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,68 @@ def test_close_stdin(self):
131131
self.assertEqual(process.returncode, 0)
132132
self.assertIn('before close', output)
133133

134+
def test_interactive_traceback_reporting(self):
135+
user_input = "1 / 0 / 3 / 4"
136+
p = spawn_repl()
137+
p.stdin.write(user_input)
138+
output = kill_python(p)
139+
self.assertEqual(p.returncode, 0)
140+
141+
traceback_lines = output.splitlines()[-6:-1]
142+
expected_lines = [
143+
"Traceback (most recent call last):",
144+
" File \"<stdin>\", line 1, in <module>",
145+
" 1 / 0 / 3 / 4",
146+
" ~~^~~",
147+
"ZeroDivisionError: division by zero",
148+
]
149+
self.assertEqual(traceback_lines, expected_lines)
150+
151+
def test_interactive_traceback_reporting_multiple_input(self):
152+
user_input1 = dedent("""
153+
def foo(x):
154+
1 / x
155+
156+
""")
157+
p = spawn_repl()
158+
p.stdin.write(user_input1)
159+
user_input2 = "foo(0)"
160+
p.stdin.write(user_input2)
161+
output = kill_python(p)
162+
self.assertEqual(p.returncode, 0)
163+
164+
traceback_lines = output.splitlines()[-7:-1]
165+
expected_lines = [
166+
' File "<stdin>", line 1, in <module>',
167+
' foo(0)',
168+
' File "<stdin>", line 2, in foo',
169+
' 1 / x',
170+
' ~~^~~',
171+
'ZeroDivisionError: division by zero'
172+
]
173+
self.assertEqual(traceback_lines, expected_lines)
174+
175+
def test_interactive_source_is_in_linecache(self):
176+
user_input = dedent("""
177+
def foo(x):
178+
return x + 1
179+
180+
def bar(x):
181+
return foo(x) + 2
182+
""")
183+
p = spawn_repl()
184+
p.stdin.write(user_input)
185+
user_input2 = dedent("""
186+
import linecache
187+
print(linecache.cache['<python-input-1>'])
188+
""")
189+
p.stdin.write(user_input2)
190+
output = kill_python(p)
191+
self.assertEqual(p.returncode, 0)
192+
expected = "(30, None, [\'def foo(x):\\n\', \' return x + 1\\n\', \'\\n\'], \'<stdin>\')"
193+
self.assertIn(expected, output, expected)
194+
195+
134196

135197
class TestInteractiveModeSyntaxErrors(unittest.TestCase):
136198

Lib/traceback.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,6 @@ def _extract_from_extended_frame_gen(klass, frame_gen, *, limit=None,
434434
co = f.f_code
435435
filename = co.co_filename
436436
name = co.co_name
437-
438437
fnames.add(filename)
439438
linecache.lazycache(filename, f.f_globals)
440439
# Must defer line lookups until we have called checkcache.
@@ -447,6 +446,7 @@ def _extract_from_extended_frame_gen(klass, frame_gen, *, limit=None,
447446
end_lineno=end_lineno, colno=colno, end_colno=end_colno))
448447
for filename in fnames:
449448
linecache.checkcache(filename)
449+
450450
# If immediate lookup was desired, trigger lookups now.
451451
if lookup_lines:
452452
for f in result:
@@ -479,8 +479,12 @@ def format_frame_summary(self, frame_summary):
479479
gets called for every frame to be printed in the stack summary.
480480
"""
481481
row = []
482-
row.append(' File "{}", line {}, in {}\n'.format(
483-
frame_summary.filename, frame_summary.lineno, frame_summary.name))
482+
if frame_summary.filename.startswith("<python-input"):
483+
row.append(' File "<stdin>", line {}, in {}\n'.format(
484+
frame_summary.lineno, frame_summary.name))
485+
else:
486+
row.append(' File "{}", line {}, in {}\n'.format(
487+
frame_summary.filename, frame_summary.lineno, frame_summary.name))
484488
if frame_summary.line:
485489
stripped_line = frame_summary.line.strip()
486490
row.append(' {}\n'.format(stripped_line))
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Allow the repl to show source code and complete tracebacks. Patch by Pablo
2+
Galindo

Parser/peg_api.c

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,18 @@ _PyParser_ASTFromFile(FILE *fp, PyObject *filename_ob, const char *enc,
2323
return NULL;
2424
}
2525
return _PyPegen_run_parser_from_file_pointer(fp, mode, filename_ob, enc, ps1, ps2,
26-
flags, errcode, arena);
26+
flags, errcode, NULL, arena);
2727
}
28+
29+
mod_ty
30+
_PyParser_InteractiveASTFromFile(FILE *fp, PyObject *filename_ob, const char *enc,
31+
int mode, const char *ps1, const char* ps2,
32+
PyCompilerFlags *flags, int *errcode,
33+
PyObject **interactive_src, PyArena *arena)
34+
{
35+
if (PySys_Audit("compile", "OO", Py_None, filename_ob) < 0) {
36+
return NULL;
37+
}
38+
return _PyPegen_run_parser_from_file_pointer(fp, mode, filename_ob, enc, ps1, ps2,
39+
flags, errcode, interactive_src, arena);
40+
}

Parser/pegen.c

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -878,7 +878,8 @@ _PyPegen_run_parser(Parser *p)
878878
mod_ty
879879
_PyPegen_run_parser_from_file_pointer(FILE *fp, int start_rule, PyObject *filename_ob,
880880
const char *enc, const char *ps1, const char *ps2,
881-
PyCompilerFlags *flags, int *errcode, PyArena *arena)
881+
PyCompilerFlags *flags, int *errcode,
882+
PyObject **interactive_src, PyArena *arena)
882883
{
883884
struct tok_state *tok = _PyTokenizer_FromFile(fp, enc, ps1, ps2);
884885
if (tok == NULL) {
@@ -908,6 +909,15 @@ _PyPegen_run_parser_from_file_pointer(FILE *fp, int start_rule, PyObject *filena
908909
result = _PyPegen_run_parser(p);
909910
_PyPegen_Parser_Free(p);
910911

912+
if (tok->fp_interactive && tok->interactive_src_start && result && interactive_src != NULL) {
913+
*interactive_src = PyUnicode_FromString(tok->interactive_src_start);
914+
if (!interactive_src || _PyArena_AddPyObject(arena, *interactive_src) < 0) {
915+
Py_XDECREF(interactive_src);
916+
result = NULL;
917+
goto error;
918+
}
919+
}
920+
911921
error:
912922
_PyTokenizer_Free(tok);
913923
return result;

Parser/pegen.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,8 @@ void *_PyPegen_nonparen_genexp_in_call(Parser *p, expr_ty args, asdl_comprehensi
350350
Parser *_PyPegen_Parser_New(struct tok_state *, int, int, int, int *, PyArena *);
351351
void _PyPegen_Parser_Free(Parser *);
352352
mod_ty _PyPegen_run_parser_from_file_pointer(FILE *, int, PyObject *, const char *,
353-
const char *, const char *, PyCompilerFlags *, int *, PyArena *);
353+
const char *, const char *, PyCompilerFlags *, int *, PyObject **,
354+
PyArena *);
354355
void *_PyPegen_run_parser(Parser *);
355356
mod_ty _PyPegen_run_parser_from_string(const char *, int, PyObject *, PyCompilerFlags *, PyArena *);
356357
asdl_stmt_seq *_PyPegen_interactive_exit(Parser *);

Python/pythonrun.c

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
/* Forward */
4141
static void flush_io(void);
4242
static PyObject *run_mod(mod_ty, PyObject *, PyObject *, PyObject *,
43-
PyCompilerFlags *, PyArena *);
43+
PyCompilerFlags *, PyArena *, PyObject*);
4444
static PyObject *run_pyc_file(FILE *, PyObject *, PyObject *,
4545
PyCompilerFlags *);
4646
static int PyRun_InteractiveOneObjectEx(FILE *, PyObject *, PyCompilerFlags *);
@@ -178,7 +178,8 @@ PyRun_InteractiveLoopFlags(FILE *fp, const char *filename, PyCompilerFlags *flag
178178
// Call _PyParser_ASTFromFile() with sys.stdin.encoding, sys.ps1 and sys.ps2
179179
static int
180180
pyrun_one_parse_ast(FILE *fp, PyObject *filename,
181-
PyCompilerFlags *flags, PyArena *arena, mod_ty *pmod)
181+
PyCompilerFlags *flags, PyArena *arena,
182+
mod_ty *pmod, PyObject** interactive_src)
182183
{
183184
PyThreadState *tstate = _PyThreadState_GET();
184185

@@ -236,9 +237,9 @@ pyrun_one_parse_ast(FILE *fp, PyObject *filename,
236237
}
237238

238239
int errcode = 0;
239-
*pmod = _PyParser_ASTFromFile(fp, filename, encoding,
240-
Py_single_input, ps1, ps2,
241-
flags, &errcode, arena);
240+
*pmod = _PyParser_InteractiveASTFromFile(fp, filename, encoding,
241+
Py_single_input, ps1, ps2,
242+
flags, &errcode, interactive_src, arena);
242243
Py_XDECREF(ps1_obj);
243244
Py_XDECREF(ps2_obj);
244245
Py_XDECREF(encoding_obj);
@@ -266,7 +267,8 @@ PyRun_InteractiveOneObjectEx(FILE *fp, PyObject *filename,
266267
}
267268

268269
mod_ty mod;
269-
int parse_res = pyrun_one_parse_ast(fp, filename, flags, arena, &mod);
270+
PyObject *interactive_src;
271+
int parse_res = pyrun_one_parse_ast(fp, filename, flags, arena, &mod, &interactive_src);
270272
if (parse_res != 0) {
271273
_PyArena_Free(arena);
272274
return parse_res;
@@ -279,7 +281,7 @@ PyRun_InteractiveOneObjectEx(FILE *fp, PyObject *filename,
279281
}
280282
PyObject *main_dict = PyModule_GetDict(main_module); // borrowed ref
281283

282-
PyObject *res = run_mod(mod, filename, main_dict, main_dict, flags, arena);
284+
PyObject *res = run_mod(mod, filename, main_dict, main_dict, flags, arena, interactive_src);
283285
_PyArena_Free(arena);
284286
Py_DECREF(main_module);
285287
if (res == NULL) {
@@ -1149,7 +1151,7 @@ PyRun_StringFlags(const char *str, int start, PyObject *globals,
11491151
str, &_Py_STR(anon_string), start, flags, arena);
11501152

11511153
if (mod != NULL)
1152-
ret = run_mod(mod, &_Py_STR(anon_string), globals, locals, flags, arena);
1154+
ret = run_mod(mod, &_Py_STR(anon_string), globals, locals, flags, arena, NULL);
11531155
_PyArena_Free(arena);
11541156
return ret;
11551157
}
@@ -1174,7 +1176,7 @@ pyrun_file(FILE *fp, PyObject *filename, int start, PyObject *globals,
11741176

11751177
PyObject *ret;
11761178
if (mod != NULL) {
1177-
ret = run_mod(mod, filename, globals, locals, flags, arena);
1179+
ret = run_mod(mod, filename, globals, locals, flags, arena, NULL);
11781180
}
11791181
else {
11801182
ret = NULL;
@@ -1262,12 +1264,70 @@ run_eval_code_obj(PyThreadState *tstate, PyCodeObject *co, PyObject *globals, Py
12621264

12631265
static PyObject *
12641266
run_mod(mod_ty mod, PyObject *filename, PyObject *globals, PyObject *locals,
1265-
PyCompilerFlags *flags, PyArena *arena)
1267+
PyCompilerFlags *flags, PyArena *arena, PyObject* interactive_src)
12661268
{
12671269
PyThreadState *tstate = _PyThreadState_GET();
1268-
PyCodeObject *co = _PyAST_Compile(mod, filename, flags, -1, arena);
1269-
if (co == NULL)
1270+
PyObject* interactive_filename = filename;
1271+
if (interactive_src) {
1272+
PyInterpreterState *interp = tstate->interp;
1273+
interactive_filename = PyUnicode_FromFormat(
1274+
"<python-input-%d>", interp->_interactive_src_count++
1275+
);
1276+
if (interactive_filename == NULL) {
1277+
return NULL;
1278+
}
1279+
}
1280+
1281+
PyCodeObject *co = _PyAST_Compile(mod, interactive_filename, flags, -1, arena);
1282+
if (co == NULL) {
1283+
Py_DECREF(interactive_filename);
12701284
return NULL;
1285+
}
1286+
1287+
if (interactive_src) {
1288+
PyObject *linecache_module = PyImport_ImportModule("linecache");
1289+
1290+
if (linecache_module == NULL) {
1291+
Py_DECREF(co);
1292+
Py_DECREF(interactive_filename);
1293+
return NULL;
1294+
}
1295+
1296+
PyObject *print_tb_func = PyObject_GetAttrString(linecache_module, "_register_code");
1297+
1298+
if (print_tb_func == NULL) {
1299+
Py_DECREF(co);
1300+
Py_DECREF(interactive_filename);
1301+
Py_DECREF(linecache_module);
1302+
return NULL;
1303+
}
1304+
1305+
if (!PyCallable_Check(print_tb_func)) {
1306+
Py_DECREF(co);
1307+
Py_DECREF(interactive_filename);
1308+
Py_DECREF(linecache_module);
1309+
Py_DECREF(print_tb_func);
1310+
PyErr_SetString(PyExc_ValueError, "linecache._register_code is not callable");
1311+
return NULL;
1312+
}
1313+
1314+
PyObject* result = PyObject_CallFunction(
1315+
print_tb_func, "OOO",
1316+
interactive_filename,
1317+
interactive_src,
1318+
filename
1319+
);
1320+
1321+
Py_DECREF(interactive_filename);
1322+
1323+
Py_DECREF(linecache_module);
1324+
Py_XDECREF(print_tb_func);
1325+
Py_XDECREF(result);
1326+
if (!result) {
1327+
Py_DECREF(co);
1328+
return NULL;
1329+
}
1330+
}
12711331

12721332
if (_PySys_Audit(tstate, "exec", "O", co) < 0) {
12731333
Py_DECREF(co);

0 commit comments

Comments
 (0)