Skip to content
Draft
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
faa22f4
Handle `PYTHONSTARTUP` script exceptions
johnslavik Oct 18, 2025
5b3ad2c
Add blurb
johnslavik Oct 18, 2025
00edac4
Use `console.showtraceback()` instead of `sys.excepthook()`
johnslavik Oct 18, 2025
e396622
Properly run asyncio REPL in REPL tests
johnslavik Oct 18, 2025
4079074
Move comment to a better place
johnslavik Oct 18, 2025
0440d0e
Add tests
johnslavik Oct 23, 2025
af6f657
Merge branch 'properly-run-asyncio-repl-in-repl-tests' into asyncio-r…
johnslavik Oct 23, 2025
848638d
Merge branch 'main' into asyncio-repl-handle-python-startup
johnslavik Oct 24, 2025
d158dbf
Improve test structure
johnslavik Oct 27, 2025
9355da7
Use `SHORT_TIMEOUT`
johnslavik Oct 27, 2025
3dc4cac
Revert "Use `SHORT_TIMEOUT`"
johnslavik Oct 27, 2025
c786584
Force no colorization
johnslavik Oct 27, 2025
b76db67
Linecache doesn't matter and shouldn't break tests
johnslavik Oct 28, 2025
cad1748
Don't rely on line numbering on Windows...
johnslavik Oct 29, 2025
d114ed5
Different names
johnslavik Oct 29, 2025
e6e10ad
Idiomatize and simplify
johnslavik Oct 29, 2025
ac6cd83
Merge branch 'main' into asyncio-repl-handle-python-startup
johnslavik Oct 30, 2025
149740a
Purifying `sys.path` is implied by `-P`
johnslavik Oct 30, 2025
bffac1c
Merge branch 'main' into asyncio-repl-handle-python-startup
johnslavik Nov 7, 2025
a4c307e
Separate tests for regular and asyncio REPL
johnslavik Nov 8, 2025
a774da5
Remove duplicate assertion
johnslavik Nov 8, 2025
b3ed3d4
Document `new_startup_env`
johnslavik Nov 8, 2025
df6dfd8
Fix `new_startup_env` docs
johnslavik Nov 8, 2025
b004839
More meaningful line breaks
johnslavik Nov 8, 2025
ce03cce
Better variables
johnslavik Nov 8, 2025
9e92510
Use default histfile
johnslavik Nov 8, 2025
0a50a50
Fix newline
johnslavik Nov 8, 2025
f8b8d53
Use `TestCase.enterContext`
johnslavik Nov 8, 2025
5701fef
Remove lines with ps1
johnslavik Nov 8, 2025
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Lib/asyncio/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,12 @@ def run(self):
import tokenize
with tokenize.open(startup_path) as f:
startup_code = compile(f.read(), startup_path, "exec")
exec(startup_code, console.locals)
try:
exec(startup_code, console.locals)
except SystemExit:
raise
except BaseException:
console.showtraceback()

ps1 = getattr(sys, "ps1", ">>> ")
if CAN_USE_PYREPL:
Expand Down
147 changes: 65 additions & 82 deletions Lib/test/test_repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import subprocess
import sys
import unittest
from contextlib import contextmanager
from functools import partial
from textwrap import dedent
from test import support
Expand All @@ -28,7 +29,7 @@
raise unittest.SkipTest("test module requires subprocess")


def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=False, **kw):
def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=False, isolated=True, **kw):
"""Run the Python REPL with the given arguments.

kw is extra keyword args to pass to subprocess.Popen. Returns a Popen
Expand All @@ -42,7 +43,11 @@ def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=F
# path may be used by PyConfig_Get("module_search_paths") to build the
# default module search path.
stdin_fname = os.path.join(os.path.dirname(sys.executable), "<stdin>")
cmd_line = [stdin_fname, '-I']
cmd_line = [stdin_fname]
# Isolated mode implies -E, -P and -s, purifies sys.path and ignores PYTHON*
# variables.
if isolated:
cmd_line.append('-I')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To reuse this routine and spawn_asyncio_repl below, we need to have a knob to not pass -I since that ignores PYTHONSTARTUP completely.

The asyncio REPL currently doesn't comply to that, which is a separate issue I'll report having learnt this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracking the -I edge case in #140648.

# Don't re-run the built-in REPL from interactive mode
# if we're testing a custom REPL (such as the asyncio REPL).
if not custom:
Expand Down Expand Up @@ -197,68 +202,6 @@ def foo(x):
]
self.assertEqual(traceback_lines, expected_lines)

def test_pythonstartup_error_reporting(self):
# errors based on https://github.com/python/cpython/issues/137576

def make_repl(env):
return subprocess.Popen(
[os.path.join(os.path.dirname(sys.executable), '<stdin>'), "-i"],
executable=sys.executable,
text=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
env=env,
)

# case 1: error in user input, but PYTHONSTARTUP is fine
with os_helper.temp_dir() as tmpdir:
script = os.path.join(tmpdir, "pythonstartup.py")
with open(script, "w") as f:
f.write("print('from pythonstartup')" + os.linesep)

env = os.environ.copy()
env['PYTHONSTARTUP'] = script
env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".pythonhist")
p = make_repl(env)
p.stdin.write("1/0")
output = kill_python(p)
expected = dedent("""
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
1/0
~^~
ZeroDivisionError: division by zero
""")
self.assertIn("from pythonstartup", output)
self.assertIn(expected, output)

# case 2: error in PYTHONSTARTUP triggered by user input
with os_helper.temp_dir() as tmpdir:
script = os.path.join(tmpdir, "pythonstartup.py")
with open(script, "w") as f:
f.write("def foo():\n 1/0\n")

env = os.environ.copy()
env['PYTHONSTARTUP'] = script
env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".pythonhist")
p = make_repl(env)
p.stdin.write('foo()')
output = kill_python(p)
expected = dedent("""
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
foo()
~~~^^
File "%s", line 2, in foo
1/0
~^~
ZeroDivisionError: division by zero
""") % script
self.assertIn(expected, output)



def test_runsource_show_syntax_error_location(self):
user_input = dedent("""def f(x, x): ...
""")
Expand Down Expand Up @@ -292,24 +235,6 @@ def bar(x):
expected = "(30, None, [\'def foo(x):\\n\', \' return x + 1\\n\', \'\\n\'], \'<stdin>\')"
self.assertIn(expected, output, expected)

def test_asyncio_repl_reaches_python_startup_script(self):
with os_helper.temp_dir() as tmpdir:
script = os.path.join(tmpdir, "pythonstartup.py")
with open(script, "w") as f:
f.write("print('pythonstartup done!')" + os.linesep)
f.write("exit(0)" + os.linesep)

env = os.environ.copy()
env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".asyncio_history")
env["PYTHONSTARTUP"] = script
subprocess.check_call(
[sys.executable, "-m", "asyncio"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=env,
timeout=SHORT_TIMEOUT,
)
Comment on lines -295 to -311
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered by TestPythonStartup now, so not needed here.


@unittest.skipUnless(pty, "requires pty")
def test_asyncio_repl_is_ok(self):
m, s = pty.openpty()
Expand Down Expand Up @@ -346,6 +271,64 @@ def test_asyncio_repl_is_ok(self):
self.assertEqual(exit_code, 0, "".join(output))


@contextmanager
def new_startup_env(*, code: str, histfile: str = ".pythonhist"):
with os_helper.temp_dir() as tmpdir:
filename = os.path.join(tmpdir, "pythonstartup.py")
with open(filename, "w") as f:
f.write(os.linesep.join(code.splitlines()))
yield {"PYTHONSTARTUP": filename, "PYTHON_HISTORY": os.path.join(tmpdir, histfile)}


@support.force_not_colorized_test_class
class TestPythonStartup(unittest.TestCase):
REPLS = [
("REPL", spawn_repl, ".pythonhist"),
("asyncio REPL", spawn_asyncio_repl, ".asyncio_history"),
]

def test_pythonstartup_success(self):
# errors based on https://github.com/python/cpython/issues/137576
# case 1: error in user input, but PYTHONSTARTUP is fine
startup_code = "print('from pythonstartup')"
for repl_name, repl_factory, histfile in self.REPLS:
with (
self.subTest(repl_name),
new_startup_env(code=startup_code, histfile=histfile) as startup_env
):
p = repl_factory(env=os.environ | startup_env, isolated=False)
p.stdin.write("1/0")
output = kill_python(p)

for expected in (
"from pythonstartup",
"Traceback (most recent call last):",
'File "<stdin>", line 1, in <module>',
"ZeroDivisionError: division by zero",
):
self.assertIn(expected, output)

def test_pythonstartup_failure(self):
# case 2: error in PYTHONSTARTUP triggered by user input
startup_code = "def foo():\n 1/0\n"
for repl_name, repl_factory, histfile in self.REPLS:
with (
self.subTest(repl_name),
new_startup_env(code=startup_code, histfile=histfile) as startup_env
):
p = repl_factory(env=os.environ | startup_env, isolated=False)
p.stdin.write('foo()')
output = kill_python(p)

for expected in (
"Traceback (most recent call last):",
'File "<stdin>", line 1, in <module>',
f'File "{startup_env['PYTHONSTARTUP']}", line ',
"ZeroDivisionError: division by zero",
):
self.assertIn(expected, output)


@support.force_not_colorized_test_class
class TestInteractiveModeSyntaxErrors(unittest.TestCase):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The asyncio REPL now properly handles exceptions in ``PYTHONSTARTUP``
scripts. Patch by Bartosz Sławecki in :gh:`140287`.
Loading