From cdbbda98640859431c0b0b79ed3e0b9cea33398e Mon Sep 17 00:00:00 2001 From: Jane R Date: Thu, 5 Jun 2025 22:16:01 -0400 Subject: [PATCH 1/7] Fixes #3275; handle reraise of KI or SystemExit without losing err msg and exit code --- newsfragments/3275.bugfix.rst | 2 ++ src/trio/_tests/test_util.py | 24 +++++++++++++++++----- src/trio/_util.py | 5 ++++- test-requirements.in | 2 +- test-requirements.txt | 38 ++++++++--------------------------- 5 files changed, 34 insertions(+), 37 deletions(-) create mode 100644 newsfragments/3275.bugfix.rst diff --git a/newsfragments/3275.bugfix.rst b/newsfragments/3275.bugfix.rst new file mode 100644 index 0000000000..8939a34167 --- /dev/null +++ b/newsfragments/3275.bugfix.rst @@ -0,0 +1,2 @@ +Handle unwrapping SystemExit exception gracefully in utility function ``raise_single_exception_from_group`` that reraises last exception from group. +Tests require pytest upgrade to 8.5.0. \ No newline at end of file diff --git a/src/trio/_tests/test_util.py b/src/trio/_tests/test_util.py index 4182ba5db4..e83e20e1a4 100644 --- a/src/trio/_tests/test_util.py +++ b/src/trio/_tests/test_util.py @@ -274,6 +274,7 @@ def test_fixup_module_metadata() -> None: mod.SomeClass().method() +@slow # 1-2 seconds async def test_raise_single_exception_from_group() -> None: excinfo: pytest.ExceptionInfo[BaseException] @@ -323,25 +324,38 @@ async def test_raise_single_exception_from_group() -> None: [ ValueError("foo"), ValueError("bar"), - KeyboardInterrupt("this exc doesn't get reraised"), + KeyboardInterrupt("is not reraised but preserve error msg"), ], ) - with pytest.raises(KeyboardInterrupt, match=r"^$") as excinfo: + with pytest.raises( + KeyboardInterrupt, + match=r"is not reraised but preserve error msg", + ) as excinfo: raise_single_exception_from_group(eg_ki) + assert excinfo.value.__cause__ is eg_ki assert excinfo.value.__context__ is None - # and same for SystemExit + # and same for SystemExit but verify code too + sysexit_exc = SystemExit("preserve error code") + sysexit_exc.code = 1 # can't set it in constructor + systemexit_ki = BaseExceptionGroup( "", [ ValueError("foo"), ValueError("bar"), - SystemExit("this exc doesn't get reraised"), + sysexit_exc, ], ) - with pytest.raises(SystemExit, match=r"^$") as excinfo: + + with pytest.raises( + SystemExit, + check=lambda e: e.code == 1, + match=r"preserve error code" + ) as excinfo: raise_single_exception_from_group(systemexit_ki) + assert excinfo.value.__cause__ is systemexit_ki assert excinfo.value.__context__ is None diff --git a/src/trio/_util.py b/src/trio/_util.py index 106423e2aa..79d1b64773 100644 --- a/src/trio/_util.py +++ b/src/trio/_util.py @@ -398,7 +398,10 @@ def raise_single_exception_from_group( # immediately bail out if there's any KI or SystemExit for e in eg.exceptions: if isinstance(e, (KeyboardInterrupt, SystemExit)): - raise type(e) from eg + new_exc = type(e)(e.args) + if isinstance(e, SystemExit): + new_exc.code = e.code # code can't be set in SystemExit constructor + raise new_exc from eg cancelled_exception: trio.Cancelled | None = None noncancelled_exception: BaseException | None = None diff --git a/test-requirements.in b/test-requirements.in index fd16b2d3bc..e7f41296a4 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -1,5 +1,5 @@ # For tests -pytest >= 5.0 # for faulthandler in core +pytest >= 8.4.0 # for faulthandler in core - > 5, for pytest.raises feature need 8.4 coverage >= 7.2.5 async_generator >= 1.9 pyright diff --git a/test-requirements.txt b/test-requirements.txt index 87036e952d..48a1b60c7d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,8 +1,6 @@ # This file was autogenerated by uv via the following command: -# uv pip compile --universal --python-version=3.9 test-requirements.in -o test-requirements.txt -alabaster==0.7.16 ; python_full_version < '3.10' - # via sphinx -alabaster==1.0.0 ; python_full_version >= '3.10' +# uv pip compile --universal --python-version=3.11 test-requirements.in -o test-requirements.txt +alabaster==1.0.0 # via sphinx astor==0.8.1 # via -r test-requirements.in @@ -52,10 +50,6 @@ distlib==0.3.9 # via virtualenv docutils==0.21.2 # via sphinx -exceptiongroup==1.2.2 ; python_full_version < '3.11' - # via - # -r test-requirements.in - # pytest filelock==3.18.0 # via virtualenv identify==2.6.10 @@ -67,8 +61,6 @@ idna==3.10 # trustme imagesize==1.4.1 # via sphinx -importlib-metadata==8.7.0 ; python_full_version < '3.10' - # via sphinx iniconfig==2.1.0 # via pytest isort==6.0.1 @@ -117,20 +109,22 @@ pre-commit==4.2.0 pycparser==2.22 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via cffi pygments==2.19.1 - # via sphinx + # via + # pytest + # sphinx pylint==3.3.6 # via -r test-requirements.in pyopenssl==25.0.0 # via -r test-requirements.in pyright==1.1.400 # via -r test-requirements.in -pytest==8.3.5 +pytest==8.4.0 # via -r test-requirements.in pyyaml==6.0.2 # via pre-commit requests==2.32.3 # via sphinx -roman-numerals-py==3.1.0 ; python_full_version >= '3.11' +roman-numerals-py==3.1.0 # via sphinx ruff==0.11.11 # via -r test-requirements.in @@ -142,11 +136,7 @@ snowballstemmer==2.2.0 # via sphinx sortedcontainers==2.4.0 # via -r test-requirements.in -sphinx==7.4.7 ; python_full_version < '3.10' - # via -r test-requirements.in -sphinx==8.1.3 ; python_full_version == '3.10.*' - # via -r test-requirements.in -sphinx==8.2.3 ; python_full_version >= '3.11' +sphinx==8.2.3 # via -r test-requirements.in sphinxcontrib-applehelp==2.0.0 # via sphinx @@ -160,13 +150,6 @@ sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 # via sphinx -tomli==2.2.1 ; python_full_version < '3.11' - # via - # black - # mypy - # pylint - # pytest - # sphinx tomlkit==0.13.2 # via pylint trustme==1.2.1 @@ -186,10 +169,7 @@ types-setuptools==80.0.0.20250429 typing-extensions==4.13.2 # via # -r test-requirements.in - # astroid - # black # mypy - # pylint # pyopenssl # pyright urllib3==2.4.0 @@ -198,5 +178,3 @@ uv==0.7.8 # via -r test-requirements.in virtualenv==20.30.0 # via pre-commit -zipp==3.21.0 ; python_full_version < '3.10' - # via importlib-metadata From bdda87d80be55a09f55b7be7b5d3bf8303a61b7c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 6 Jun 2025 02:21:04 +0000 Subject: [PATCH 2/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- newsfragments/3275.bugfix.rst | 4 ++-- src/trio/_tests/test_util.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/newsfragments/3275.bugfix.rst b/newsfragments/3275.bugfix.rst index 8939a34167..c89fe28989 100644 --- a/newsfragments/3275.bugfix.rst +++ b/newsfragments/3275.bugfix.rst @@ -1,2 +1,2 @@ -Handle unwrapping SystemExit exception gracefully in utility function ``raise_single_exception_from_group`` that reraises last exception from group. -Tests require pytest upgrade to 8.5.0. \ No newline at end of file +Handle unwrapping SystemExit exception gracefully in utility function ``raise_single_exception_from_group`` that reraises last exception from group. +Tests require pytest upgrade to 8.5.0. diff --git a/src/trio/_tests/test_util.py b/src/trio/_tests/test_util.py index e83e20e1a4..3d5423edf3 100644 --- a/src/trio/_tests/test_util.py +++ b/src/trio/_tests/test_util.py @@ -350,9 +350,7 @@ async def test_raise_single_exception_from_group() -> None: ) with pytest.raises( - SystemExit, - check=lambda e: e.code == 1, - match=r"preserve error code" + SystemExit, check=lambda e: e.code == 1, match=r"preserve error code" ) as excinfo: raise_single_exception_from_group(systemexit_ki) From ef88df0036a4f45d3575369612ca768a1ef4d49c Mon Sep 17 00:00:00 2001 From: Jane R Date: Thu, 5 Jun 2025 22:24:53 -0400 Subject: [PATCH 3/7] remove slow decorator and build test reqs for py3.9 --- src/trio/_tests/test_util.py | 1 - test-requirements.txt | 33 +++++++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/trio/_tests/test_util.py b/src/trio/_tests/test_util.py index e83e20e1a4..df266aba3b 100644 --- a/src/trio/_tests/test_util.py +++ b/src/trio/_tests/test_util.py @@ -274,7 +274,6 @@ def test_fixup_module_metadata() -> None: mod.SomeClass().method() -@slow # 1-2 seconds async def test_raise_single_exception_from_group() -> None: excinfo: pytest.ExceptionInfo[BaseException] diff --git a/test-requirements.txt b/test-requirements.txt index 48a1b60c7d..3ce35512c2 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,6 +1,8 @@ # This file was autogenerated by uv via the following command: -# uv pip compile --universal --python-version=3.11 test-requirements.in -o test-requirements.txt -alabaster==1.0.0 +# uv pip compile --universal --python-version=3.9 test-requirements.in -o test-requirements.txt +alabaster==0.7.16 ; python_full_version < '3.10' + # via sphinx +alabaster==1.0.0 ; python_full_version >= '3.10' # via sphinx astor==0.8.1 # via -r test-requirements.in @@ -50,6 +52,10 @@ distlib==0.3.9 # via virtualenv docutils==0.21.2 # via sphinx +exceptiongroup==1.3.0 ; python_full_version < '3.11' + # via + # -r test-requirements.in + # pytest filelock==3.18.0 # via virtualenv identify==2.6.10 @@ -61,6 +67,8 @@ idna==3.10 # trustme imagesize==1.4.1 # via sphinx +importlib-metadata==8.7.0 ; python_full_version < '3.10' + # via sphinx iniconfig==2.1.0 # via pytest isort==6.0.1 @@ -124,7 +132,7 @@ pyyaml==6.0.2 # via pre-commit requests==2.32.3 # via sphinx -roman-numerals-py==3.1.0 +roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx ruff==0.11.11 # via -r test-requirements.in @@ -136,7 +144,11 @@ snowballstemmer==2.2.0 # via sphinx sortedcontainers==2.4.0 # via -r test-requirements.in -sphinx==8.2.3 +sphinx==7.4.7 ; python_full_version < '3.10' + # via -r test-requirements.in +sphinx==8.1.3 ; python_full_version == '3.10.*' + # via -r test-requirements.in +sphinx==8.2.3 ; python_full_version >= '3.11' # via -r test-requirements.in sphinxcontrib-applehelp==2.0.0 # via sphinx @@ -150,6 +162,13 @@ sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 # via sphinx +tomli==2.2.1 ; python_full_version < '3.11' + # via + # black + # mypy + # pylint + # pytest + # sphinx tomlkit==0.13.2 # via pylint trustme==1.2.1 @@ -169,7 +188,11 @@ types-setuptools==80.0.0.20250429 typing-extensions==4.13.2 # via # -r test-requirements.in + # astroid + # black + # exceptiongroup # mypy + # pylint # pyopenssl # pyright urllib3==2.4.0 @@ -178,3 +201,5 @@ uv==0.7.8 # via -r test-requirements.in virtualenv==20.30.0 # via pre-commit +zipp==3.22.0 ; python_full_version < '3.10' + # via importlib-metadata From 3e538184dca4d609e8c0fdc07eae36edf4f607b4 Mon Sep 17 00:00:00 2001 From: Jane R Date: Thu, 5 Jun 2025 22:46:27 -0400 Subject: [PATCH 4/7] fix args expansion for exceptions --- src/trio/_tests/test_util.py | 12 +++++------- src/trio/_util.py | 5 +---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/trio/_tests/test_util.py b/src/trio/_tests/test_util.py index 1d3052414b..be601a43a6 100644 --- a/src/trio/_tests/test_util.py +++ b/src/trio/_tests/test_util.py @@ -323,12 +323,12 @@ async def test_raise_single_exception_from_group() -> None: [ ValueError("foo"), ValueError("bar"), - KeyboardInterrupt("is not reraised but preserve error msg"), + KeyboardInterrupt("preserve error msg"), ], ) with pytest.raises( KeyboardInterrupt, - match=r"is not reraised but preserve error msg", + match=r"^preserve error msg$", ) as excinfo: raise_single_exception_from_group(eg_ki) @@ -336,20 +336,18 @@ async def test_raise_single_exception_from_group() -> None: assert excinfo.value.__context__ is None # and same for SystemExit but verify code too - sysexit_exc = SystemExit("preserve error code") - sysexit_exc.code = 1 # can't set it in constructor - systemexit_ki = BaseExceptionGroup( "", [ ValueError("foo"), ValueError("bar"), - sysexit_exc, + SystemExit(2), ], ) with pytest.raises( - SystemExit, check=lambda e: e.code == 1, match=r"preserve error code" + SystemExit, + check=lambda e: e.code == 2 ) as excinfo: raise_single_exception_from_group(systemexit_ki) diff --git a/src/trio/_util.py b/src/trio/_util.py index 79d1b64773..54d324cab3 100644 --- a/src/trio/_util.py +++ b/src/trio/_util.py @@ -398,10 +398,7 @@ def raise_single_exception_from_group( # immediately bail out if there's any KI or SystemExit for e in eg.exceptions: if isinstance(e, (KeyboardInterrupt, SystemExit)): - new_exc = type(e)(e.args) - if isinstance(e, SystemExit): - new_exc.code = e.code # code can't be set in SystemExit constructor - raise new_exc from eg + raise type(e)(*e.args) from eg cancelled_exception: trio.Cancelled | None = None noncancelled_exception: BaseException | None = None From a23bde1055c370cea5bc9afdeeddcc960df1ae4e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 6 Jun 2025 02:46:57 +0000 Subject: [PATCH 5/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/trio/_tests/test_util.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/trio/_tests/test_util.py b/src/trio/_tests/test_util.py index be601a43a6..d411c9f05e 100644 --- a/src/trio/_tests/test_util.py +++ b/src/trio/_tests/test_util.py @@ -345,10 +345,7 @@ async def test_raise_single_exception_from_group() -> None: ], ) - with pytest.raises( - SystemExit, - check=lambda e: e.code == 2 - ) as excinfo: + with pytest.raises(SystemExit, check=lambda e: e.code == 2) as excinfo: raise_single_exception_from_group(systemexit_ki) assert excinfo.value.__cause__ is systemexit_ki From 540a19207232fda91479c2f78581fad2eb281ee1 Mon Sep 17 00:00:00 2001 From: Jane R Date: Thu, 5 Jun 2025 23:15:09 -0400 Subject: [PATCH 6/7] undo pytest 8.4 upgrade, fixed test to use excinfo --- newsfragments/3275.bugfix.rst | 3 +-- src/trio/_tests/test_util.py | 6 ++---- test-requirements.in | 4 ++-- test-requirements.txt | 13 +++++-------- 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/newsfragments/3275.bugfix.rst b/newsfragments/3275.bugfix.rst index c89fe28989..f23de60374 100644 --- a/newsfragments/3275.bugfix.rst +++ b/newsfragments/3275.bugfix.rst @@ -1,2 +1 @@ -Handle unwrapping SystemExit exception gracefully in utility function ``raise_single_exception_from_group`` that reraises last exception from group. -Tests require pytest upgrade to 8.5.0. +Handle unwrapping SystemExit/KeyboardInterrupt exception gracefully in utility function ``raise_single_exception_from_group`` that reraises last exception from group. \ No newline at end of file diff --git a/src/trio/_tests/test_util.py b/src/trio/_tests/test_util.py index be601a43a6..3a57b9cf21 100644 --- a/src/trio/_tests/test_util.py +++ b/src/trio/_tests/test_util.py @@ -345,12 +345,10 @@ async def test_raise_single_exception_from_group() -> None: ], ) - with pytest.raises( - SystemExit, - check=lambda e: e.code == 2 - ) as excinfo: + with pytest.raises(SystemExit) as excinfo: raise_single_exception_from_group(systemexit_ki) + assert excinfo.value.code == 2 assert excinfo.value.__cause__ is systemexit_ki assert excinfo.value.__context__ is None diff --git a/test-requirements.in b/test-requirements.in index e7f41296a4..84338dccfd 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -1,5 +1,5 @@ # For tests -pytest >= 8.4.0 # for faulthandler in core - > 5, for pytest.raises feature need 8.4 +pytest >= 5.0 # for faulthandler in core coverage >= 7.2.5 async_generator >= 1.9 pyright @@ -38,4 +38,4 @@ idna outcome sniffio # 1.2.1 fixes types -exceptiongroup >= 1.2.1; python_version < "3.11" +exceptiongroup >= 1.2.1; python_version < "3.11" \ No newline at end of file diff --git a/test-requirements.txt b/test-requirements.txt index 3ce35512c2..bb90d16162 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -52,7 +52,7 @@ distlib==0.3.9 # via virtualenv docutils==0.21.2 # via sphinx -exceptiongroup==1.3.0 ; python_full_version < '3.11' +exceptiongroup==1.2.2 ; python_full_version < '3.11' # via # -r test-requirements.in # pytest @@ -117,16 +117,14 @@ pre-commit==4.2.0 pycparser==2.22 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via cffi pygments==2.19.1 - # via - # pytest - # sphinx + # via sphinx pylint==3.3.6 # via -r test-requirements.in pyopenssl==25.0.0 # via -r test-requirements.in pyright==1.1.400 # via -r test-requirements.in -pytest==8.4.0 +pytest==8.3.5 # via -r test-requirements.in pyyaml==6.0.2 # via pre-commit @@ -190,7 +188,6 @@ typing-extensions==4.13.2 # -r test-requirements.in # astroid # black - # exceptiongroup # mypy # pylint # pyopenssl @@ -201,5 +198,5 @@ uv==0.7.8 # via -r test-requirements.in virtualenv==20.30.0 # via pre-commit -zipp==3.22.0 ; python_full_version < '3.10' - # via importlib-metadata +zipp==3.21.0 ; python_full_version < '3.10' + # via importlib-metadata \ No newline at end of file From bc3a2c501695337b32f8001d916feff3e6abc683 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 6 Jun 2025 03:18:51 +0000 Subject: [PATCH 7/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- newsfragments/3275.bugfix.rst | 2 +- test-requirements.in | 2 +- test-requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/newsfragments/3275.bugfix.rst b/newsfragments/3275.bugfix.rst index f23de60374..b418b26d76 100644 --- a/newsfragments/3275.bugfix.rst +++ b/newsfragments/3275.bugfix.rst @@ -1 +1 @@ -Handle unwrapping SystemExit/KeyboardInterrupt exception gracefully in utility function ``raise_single_exception_from_group`` that reraises last exception from group. \ No newline at end of file +Handle unwrapping SystemExit/KeyboardInterrupt exception gracefully in utility function ``raise_single_exception_from_group`` that reraises last exception from group. diff --git a/test-requirements.in b/test-requirements.in index 84338dccfd..fd16b2d3bc 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -38,4 +38,4 @@ idna outcome sniffio # 1.2.1 fixes types -exceptiongroup >= 1.2.1; python_version < "3.11" \ No newline at end of file +exceptiongroup >= 1.2.1; python_version < "3.11" diff --git a/test-requirements.txt b/test-requirements.txt index bb90d16162..87036e952d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -199,4 +199,4 @@ uv==0.7.8 virtualenv==20.30.0 # via pre-commit zipp==3.21.0 ; python_full_version < '3.10' - # via importlib-metadata \ No newline at end of file + # via importlib-metadata