Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 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
393aaad
Merge branch 'main' into asyncio-repl-handle-python-startup
johnslavik Dec 16, 2025
875fd2a
Employ `asyncio.Runner` in the asyncio REPL
johnslavik Dec 16, 2025
4377d82
Revert "Employ `asyncio.Runner` in the asyncio REPL"
johnslavik Dec 16, 2025
c4e488e
Merge branch 'main' into asyncio-repl-handle-python-startup
johnslavik Dec 18, 2025
b9ffea7
Add a todo for future
johnslavik Dec 20, 2025
ab799e6
Merge branch 'main' into asyncio-repl-handle-python-startup
johnslavik Jan 3, 2026
2f8f99b
Merge branch 'main' into asyncio-repl-handle-python-startup
johnslavik Jan 4, 2026
6020996
Rename the decorator
johnslavik Jan 4, 2026
125efa6
Colorize the news entry
johnslavik Jan 4, 2026
cc624d1
Simplify `new_pythonstartup_env`
johnslavik Jan 4, 2026
812d22f
Simplify tests by a gazillion procent 🚀🚀🚀
johnslavik Jan 4, 2026
6749b83
Simplify the tests further 🚀
johnslavik Jan 4, 2026
e76426d
And even further
johnslavik Jan 4, 2026
376c7a4
Rephrase the news entry
johnslavik Jan 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Lib/asyncio/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,13 @@ 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)
# TODO: Revisit in GH-143023
except SystemExit:
raise
Comment on lines +108 to +109
Copy link
Member Author

@johnslavik johnslavik Dec 20, 2025

Choose a reason for hiding this comment

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

I really don't like this, but it resembles the original behavior of the REPLs -- see GH-143023.
I don't think this is correct, but it's not super clear to me, so please chime in to GH-143023 to put your two cents in.

Suggested change
except SystemExit:
raise
# TODO: Revisit in GH-143023
except SystemExit:
raise

Copy link
Member Author

@johnslavik johnslavik Dec 20, 2025

Choose a reason for hiding this comment

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

GH-143023 is also why I didn't add a test for this.

Copy link
Member Author

Choose a reason for hiding this comment

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

We've got some good news, this is not bad! I'll add a test shortly.

except BaseException:
console.showtraceback()

ps1 = getattr(sys, "ps1", ">>> ")
if CAN_USE_PYREPL:
Expand Down
100 changes: 98 additions & 2 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 Down Expand Up @@ -67,6 +68,19 @@ def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=F
spawn_asyncio_repl = partial(spawn_repl, "-m", "asyncio", custom=True)


@contextmanager
def new_pythonstartup_env(*, code: str, histfile: str = ".pythonhist"):
"""Create environment variables for a PYTHONSTARTUP script in a temporary directory."""
with os_helper.temp_dir() as tmpdir:
filename = os.path.join(tmpdir, "pythonstartup.py")
with open(filename, "w") as f:
f.write(code)
yield {
"PYTHONSTARTUP": filename,
"PYTHON_HISTORY": os.path.join(tmpdir, histfile)
}


def run_on_interactive_mode(source):
"""Spawn a new Python interpreter, pass the given
input source code from the stdin and return the
Expand Down Expand Up @@ -260,8 +274,6 @@ def make_repl(env):
""") % script
self.assertIn(expected, output)



def test_runsource_show_syntax_error_location(self):
user_input = dedent("""def f(x, x): ...
""")
Expand Down Expand Up @@ -356,6 +368,45 @@ def test_asyncio_repl_is_ok(self):

self.assertEqual(exit_code, 0, "".join(output))

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('notice from pythonstartup')"
startup_env = self.enterContext(new_pythonstartup_env(code=startup_code))

p = spawn_repl("-q", env=os.environ | startup_env, isolated=False)
p.stdin.write("1/0")
output = kill_python(p)
self.assertStartsWith(output, 'notice from pythonstartup')
expected = dedent("""\
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
1/0
~^~
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"
startup_env = self.enterContext(new_pythonstartup_env(code=startup_code))

p = spawn_repl("-q", env=os.environ | startup_env, isolated=False)
p.stdin.write("foo()")
output = kill_python(p)
expected = dedent(f"""\
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
foo()
~~~^^
File "{startup_env['PYTHONSTARTUP']}", line 2, in foo
1/0
~^~
ZeroDivisionError: division by zero
""")
self.assertIn(expected, output)


@support.force_not_colorized_test_class
class TestInteractiveModeSyntaxErrors(unittest.TestCase):
Expand All @@ -376,6 +427,7 @@ def f():
self.assertEqual(traceback_lines, expected_lines)


@support.force_not_colorized_test_class
Copy link
Member Author

@johnslavik johnslavik Nov 8, 2025

Choose a reason for hiding this comment

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

I'm not sure this is needed, but I think it's OK to keep it as is.

class TestAsyncioREPL(unittest.TestCase):
def test_multiple_statements_fail_early(self):
user_input = "1 / 0; print(f'afterwards: {1+1}')"
Expand Down Expand Up @@ -426,6 +478,50 @@ def test_quiet_mode(self):
self.assertEqual(p.returncode, 0)
self.assertEqual(output[:3], ">>>")

def test_pythonstartup_success(self):
startup_code = dedent("print('notice from pythonstartup in asyncio repl')")
startup_env = self.enterContext(
new_pythonstartup_env(code=startup_code, histfile=".asyncio_history"))

p = spawn_repl(
"-qm", "asyncio",
env=os.environ | startup_env,
isolated=False,
custom=True)
p.stdin.write("1/0")
output = kill_python(p)
self.assertStartsWith(output, 'notice from pythonstartup in asyncio repl')

expected = dedent("""\
File "<stdin>", line 1, in <module>
1/0
~^~
ZeroDivisionError: division by zero
""")
self.assertIn(expected, output)

def test_pythonstartup_failure(self):
startup_code = "def foo():\n 1/0\n"
startup_env = self.enterContext(
new_pythonstartup_env(code=startup_code, histfile=".asyncio_history"))

p = spawn_repl(
"-qm", "asyncio",
env=os.environ | startup_env,
isolated=False,
custom=True)
p.stdin.write("foo()")
output = kill_python(p)
expected = dedent(f"""\
File "<stdin>", line 1, in <module>
foo()
~~~^^
File "{startup_env['PYTHONSTARTUP']}", line 2, in foo
1/0
~^~
ZeroDivisionError: division by zero
""")
self.assertIn(expected, output)

if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
The :mod:`asyncio` REPL now properly handles exceptions in :envvar:`PYTHONSTARTUP` scripts.
Previously, any startup exception could prevent the REPL from starting or even cause
a fatal error.

Patch by Bartosz Sławecki in :gh:`140287`.
Loading