diff --git a/.github/workflows/autodeps.yml b/.github/workflows/autodeps.yml
index 8b58f8904f..309dfd373f 100644
--- a/.github/workflows/autodeps.yml
+++ b/.github/workflows/autodeps.yml
@@ -40,7 +40,7 @@ jobs:
run: python -m pip install -r test-requirements.txt
- name: Pre-commit fixes
- run: pre-commit run -a
+ run: pre-commit run -a || true
- name: Commit changes and create automerge PR
env:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9daca186a9..2427c39974 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -86,12 +86,25 @@ jobs:
dist/${{ steps.artifact-name.outputs.wheel }}
retention-days: 5
+ smoke-tests:
+ name: Smoke tests
+ needs:
+ - build
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Switch to using Python 3.11
+ uses: actions/setup-python@v6
+ with:
+ python-version: 3.11
+
- name: >-
Smoke-test:
retrieve the project source from an sdist inside the GHA artifact
uses: re-actors/checkout-python-sdist@release/v2
with:
- source-tarball-name: ${{ steps.artifact-name.outputs.sdist }}
+ source-tarball-name: ${{ needs.build.outputs.sdist-artifact-name }}
workflow-artifact-name: ${{ env.dists-artifact-name }}
- name: >-
@@ -268,12 +281,21 @@ jobs:
cache: pip
cache-dependency-path: test-requirements.txt
allow-prereleases: true
+ - name: Setup minimum Python version
+ if: matrix.check_formatting == '1'
+ uses: actions/setup-python@v6
+ with:
+ python-version: '3.10'
+ cache: pip
+ cache-dependency-path: test-requirements.txt
+ allow-prereleases: true
- name: Check Formatting
if: matrix.check_formatting == '1'
run:
python -m pip install tox &&
tox -m check
- name: Install python3-apport
+ if: matrix.check_formatting == '0'
run: |
sudo apt update
sudo apt install -q python3-apport
@@ -456,6 +478,7 @@ jobs:
if: always()
needs:
+ - smoke-tests
- Windows
- Ubuntu
- macOS
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 46cad85615..2cfce7979c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -20,11 +20,11 @@ repos:
- id: sort-simple-yaml
files: .pre-commit-config.yaml
- repo: https://github.com/psf/black-pre-commit-mirror
- rev: 25.9.0
+ rev: 26.1.0
hooks:
- id: black
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.14.2
+ rev: v0.15.1
hooks:
- id: ruff-check
types: [file]
@@ -38,15 +38,15 @@ repos:
# tomli needed on 3.10. tomllib is available in stdlib on 3.11+
- tomli
- repo: https://github.com/adhtruong/mirrors-typos
- rev: v1.38.1
+ rev: v1.43.4
hooks:
- id: typos
- repo: https://github.com/sphinx-contrib/sphinx-lint
- rev: v1.0.0
+ rev: v1.0.2
hooks:
- id: sphinx-lint
- repo: https://github.com/woodruffw/zizmor-pre-commit
- rev: v1.16.0
+ rev: v1.22.0
hooks:
- id: zizmor
- repo: local
@@ -73,7 +73,7 @@ repos:
additional_dependencies: ["pyyaml"]
files: ^(test-requirements\.txt)|(\.pre-commit-config\.yaml)$
- repo: https://github.com/astral-sh/uv-pre-commit
- rev: 0.9.5
+ rev: 0.10.2
hooks:
# Compile requirements
- id: pip-compile
diff --git a/ci.sh b/ci.sh
index 07797937e9..9362dbc727 100755
--- a/ci.sh
+++ b/ci.sh
@@ -54,7 +54,7 @@ if [ "${NO_TEST_REQUIREMENTS-0}" == 1 ]; then
python -m uv pip install pytest coverage -c test-requirements.txt
flags="--skip-optional-imports"
else
- python -m uv pip install -r test-requirements.txt
+ python -m uv pip install -r test-requirements.txt --no-deps
flags=""
fi
diff --git a/docs-requirements.txt b/docs-requirements.txt
index 4fcf0cd18b..e74c483b50 100644
--- a/docs-requirements.txt
+++ b/docs-requirements.txt
@@ -6,11 +6,11 @@ attrs==25.4.0
# via
# -r docs-requirements.in
# outcome
-babel==2.17.0
+babel==2.18.0
# via sphinx
-beautifulsoup4==4.14.2
+beautifulsoup4==4.14.3
# via sphinx-codeautolink
-certifi==2025.10.5
+certifi==2026.1.4
# via requests
cffi==2.0.0 ; os_name == 'nt' or platform_python_implementation != 'PyPy'
# via
@@ -18,19 +18,19 @@ cffi==2.0.0 ; os_name == 'nt' or platform_python_implementation != 'PyPy'
# cryptography
charset-normalizer==3.4.4
# via requests
-click==8.3.0
+click==8.3.1
# via towncrier
colorama==0.4.6 ; sys_platform == 'win32'
# via
# click
# sphinx
-cryptography==46.0.3
+cryptography==46.0.5
# via pyopenssl
-docutils==0.21.2
+docutils==0.22.4
# via
# sphinx
# sphinx-rtd-theme
-exceptiongroup==1.3.0
+exceptiongroup==1.3.1
# via -r docs-requirements.in
idna==3.11
# via
@@ -49,9 +49,9 @@ markupsafe==3.0.3
# via jinja2
outcome==1.3.0.post0
# via -r docs-requirements.in
-packaging==25.0
+packaging==26.0
# via sphinx
-pycparser==2.23 ; (implementation_name != 'PyPy' and os_name == 'nt') or (implementation_name != 'PyPy' and platform_python_implementation != 'PyPy')
+pycparser==3.0 ; (implementation_name != 'PyPy' and os_name == 'nt') or (implementation_name != 'PyPy' and platform_python_implementation != 'PyPy')
# via cffi
pygments==2.19.2
# via sphinx
@@ -59,7 +59,7 @@ pyopenssl==25.3.0
# via -r docs-requirements.in
requests==2.32.5
# via sphinx
-roman-numerals-py==3.1.0
+roman-numerals==4.1.0
# via sphinx
sniffio==1.3.1
# via -r docs-requirements.in
@@ -67,9 +67,16 @@ snowballstemmer==3.0.1
# via sphinx
sortedcontainers==2.4.0
# via -r docs-requirements.in
-soupsieve==2.8
+soupsieve==2.8.3
# via beautifulsoup4
-sphinx==8.2.3
+sphinx==9.0.4 ; python_full_version < '3.12'
+ # via
+ # -r docs-requirements.in
+ # sphinx-codeautolink
+ # sphinx-rtd-theme
+ # sphinxcontrib-jquery
+ # sphinxcontrib-trio
+sphinx==9.1.0 ; python_full_version >= '3.12'
# via
# -r docs-requirements.in
# sphinx-codeautolink
@@ -78,7 +85,7 @@ sphinx==8.2.3
# sphinxcontrib-trio
sphinx-codeautolink==0.17.5
# via -r docs-requirements.in
-sphinx-rtd-theme==3.0.2
+sphinx-rtd-theme==3.1.0
# via -r docs-requirements.in
sphinxcontrib-applehelp==2.0.0
# via sphinx
@@ -96,7 +103,7 @@ sphinxcontrib-qthelp==2.0.0
# via sphinx
sphinxcontrib-serializinghtml==2.0.0
# via sphinx
-sphinxcontrib-trio==1.1.2
+sphinxcontrib-trio==1.2.0
# via -r docs-requirements.in
towncrier==25.8.0
# via -r docs-requirements.in
@@ -105,5 +112,5 @@ typing-extensions==4.15.0
# beautifulsoup4
# exceptiongroup
# pyopenssl
-urllib3==2.5.0
+urllib3==2.6.3
# via requests
diff --git a/docs/source/_templates/genindex.html b/docs/source/_templates/genindex.html
new file mode 100644
index 0000000000..7db72e44d1
--- /dev/null
+++ b/docs/source/_templates/genindex.html
@@ -0,0 +1,38 @@
+{% extends "!genindex.html" %}
+
+{# check sphinx/themes/basic/genindex if this snippet has become outdated #}
+
+{% block body %}
+
+
{{ _('Index') }}
+
+
+ {% for key, dummy in genindexentries -%}
+
{{ key }}
+ {% if not loop.last %}| {% endif %}
+ {%- endfor %}
+
+
+{%- for key, entries in genindexentries %}
+{{ key }}
+
+ {%- for column in entries|slice_index(2) if column %}
+
+ {%- for entryname, (links, subitems, _) in column %}
+ {% set name = links[0][1].rsplit('#', 1)[1] if links else '' %}
+ - {{ indexentries(name if name and '-' not in name else entryname, links) }}
+ {%- if subitems %}
+
+ {%- for subentryname, subentrylinks in subitems %}
+ {% set sname = subentrylinks[0][1].rsplit('#', 1)[1] if subentrylinks else '' %}
+ - {{ indexentries(sname if sname and '-' not in sname else subentryname, subentrylinks) }}
+ {%- endfor %}
+
+ {%- endif -%}
+ {%- endfor %}
+ |
+ {%- endfor %}
+
+{% endfor %}
+
+{% endblock %}
diff --git a/docs/source/awesome-trio-libraries.rst b/docs/source/awesome-trio-libraries.rst
index 875471a21f..4a603960d8 100644
--- a/docs/source/awesome-trio-libraries.rst
+++ b/docs/source/awesome-trio-libraries.rst
@@ -50,6 +50,7 @@ Database
* `asyncakumuli `__ - Client for the `Akumuli `__ time series database.
* `aio-databases `_ - Async Support for various databases (triopg, trio-mysql)
* `peewee-aio `_ - Peewee Async ORM with trio support (triopg, trio-mysql).
+* `coredis `_ - Fast, async, fully-typed Redis client with support for cluster and sentinel
IOT
@@ -83,6 +84,11 @@ Stream Processing
* `Slurry `__ - Slurry is a microframework for building reactive, data processing applications with Trio.
+Distributed Task Queue
+----------------------
+* `streaQ `_ - Fast, async, fully-typed distributed task queue via Redis streams
+
+
RPC
---
* `purepc `__ - Native, async Python gRPC client and server implementation using anyio.
diff --git a/docs/source/conf.py b/docs/source/conf.py
index ab0d1e5059..5017f3895e 100755
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -25,7 +25,9 @@
from pathlib import Path
from typing import TYPE_CHECKING, cast
-from sphinx.util.inventory import _InventoryItem
+if sys.version_info >= (3, 11):
+ from sphinx.util.inventory import _InventoryItem
+
from sphinx.util.logging import getLogger
if TYPE_CHECKING:
@@ -280,12 +282,20 @@ def add_mapping(
assert isinstance(inventory, dict)
inventory = cast("Inventory", inventory)
- inventory[f"py:{reftype}"][f"{target}"] = _InventoryItem(
- project_name="Python",
- project_version=version,
- uri=f"https://docs.python.org/{url_version}/library/{library}.html/{obj}",
- display_name="-",
- )
+ if sys.version_info >= (3, 11):
+ inventory[f"py:{reftype}"][f"{target}"] = _InventoryItem(
+ project_name="Python",
+ project_version=version,
+ uri=f"https://docs.python.org/{url_version}/library/{library}.html/{obj}",
+ display_name="-",
+ )
+ else:
+ inventory[f"py:{reftype}"][f"{target}"] = (
+ "Python",
+ version,
+ f"https://docs.python.org/{url_version}/library/{library}.html/{obj}",
+ "-",
+ )
# This has been removed in Py3.12, so add a link to the 3.11 version with deprecation warnings.
add_mapping("method", "pathlib", "Path.link_to", "3.11")
diff --git a/docs/source/history.rst b/docs/source/history.rst
index d66ac70ff0..2ca86c6980 100644
--- a/docs/source/history.rst
+++ b/docs/source/history.rst
@@ -5,6 +5,21 @@ Release history
.. towncrier release notes start
+trio 0.33.0 (2026-02-14)
+------------------------
+
+Bugfixes
+~~~~~~~~
+
+- Start supporting Android's new ``"android"`` `sys.platform`. (`#3357 `__)
+
+
+Deprecations and removals
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- Both :class:`trio.testing.RaisesGroup` and :class:`trio.testing.Matcher` have been deprecated. Pytest alternatives ``pytest.RaisesGroup`` and ``pytest.RaisesExc`` (respectively) are considered correct replacement. (`#3326 `__)
+
+
trio 0.32.0 (2025-10-31)
------------------------
diff --git a/docs/source/index.rst b/docs/source/index.rst
index f8fc6add8f..d64d28041a 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -86,6 +86,7 @@ Vital statistics:
contributing.rst
releasing.rst
code-of-conduct.rst
+ genindex
====================
Indices and tables
diff --git a/pyproject.toml b/pyproject.toml
index aef3c258fc..4e50a217ec 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -155,6 +155,7 @@ extend-ignore = [
"PERF203", # try-except-in-loop (not always possible to refactor)
"PT012", # multiple statements in pytest.raises block
"SIM117", # multiple-with-statements (messes up lots of context-based stuff and looks bad)
+ "RUF067", # non-empty-init-module, since we need lots of logic in our __init__
# conflicts with formatter (ruff recommends these be disabled)
"COM812",
@@ -170,12 +171,16 @@ extend-ignore = [
'src/trio/lowlevel.py' = ['F401']
'src/trio/socket.py' = ['F401', 'A005']
'src/trio/testing/__init__.py' = ['F401']
+
# RUF029 is ignoring tests that are marked as async functions but
# do not use an await in their function bodies. There are several
# places where internal trio synchronous code relies on being
# called from an async function, where current task is set up.
-'src/trio/_tests/*.py' = ['RUF029']
-'src/trio/_core/_tests/*.py' = ['RUF029']
+#
+# RUF069 is ignoring exact float comparison, because we use that
+# in combination with autojump clocks
+'src/trio/_tests/*.py' = ['RUF029', 'RUF069']
+'src/trio/_core/_tests/*.py' = ['RUF029', 'RUF069']
# A005 is ignoring modules that shadow stdlib modules.
'src/trio/_abc.py' = ['A005']
'src/trio/_socket.py' = ['A005']
@@ -293,6 +298,10 @@ skip_covered = false
[tool.coverage.paths]
source = ["src", "**/site-packages"]
+attrs-plugin = [
+ "tests/_trio_check_attrs_aliases.py",
+ "_trio_check_attrs_aliases.py",
+]
[tool.coverage.run]
branch = true
@@ -311,7 +320,8 @@ omit = [
parallel = true
plugins = []
relative_files = true
-source = ["trio"]
+source = ["."]
+source_pkgs = ["trio", "_trio_check_attrs_aliases"]
[tool.coverage.report]
precision = 1
diff --git a/src/trio/_core/__init__.py b/src/trio/_core/__init__.py
index f9d8068f0c..c53d9d59a0 100644
--- a/src/trio/_core/__init__.py
+++ b/src/trio/_core/__init__.py
@@ -86,9 +86,9 @@
write_overlapped,
)
# Kqueue imports
-if (sys.platform != "linux" and sys.platform != "win32") or (
- not _t.TYPE_CHECKING and "sphinx.ext.autodoc" in sys.modules
-):
+if (
+ sys.platform != "linux" and sys.platform != "win32" and sys.platform != "android"
+) or (not _t.TYPE_CHECKING and "sphinx.ext.autodoc" in sys.modules):
from ._run import current_kqueue, monitor_kevent, wait_kevent
del sys # It would be better to import sys as _sys, but mypy does not understand it
diff --git a/src/trio/_core/_generated_run.py b/src/trio/_core/_generated_run.py
index db1454e6c7..4da520109b 100644
--- a/src/trio/_core/_generated_run.py
+++ b/src/trio/_core/_generated_run.py
@@ -12,7 +12,7 @@
import contextvars
from collections.abc import Awaitable, Callable
- from outcome import Outcome
+ import outcome
from typing_extensions import Unpack
from .._abc import Clock
@@ -102,7 +102,7 @@ def current_root_task() -> Task | None:
@enable_ki_protection
-def reschedule(task: Task, next_send: Outcome[object] = _NO_SEND) -> None:
+def reschedule(task: Task, next_send: outcome.Outcome[object] = _NO_SEND) -> None:
"""Reschedule the given task with the given
:class:`outcome.Outcome`.
diff --git a/src/trio/_core/_run.py b/src/trio/_core/_run.py
index 4689dca104..9ecdada1e0 100644
--- a/src/trio/_core/_run.py
+++ b/src/trio/_core/_run.py
@@ -25,6 +25,7 @@
)
import attrs
+import outcome
from outcome import Error, Outcome, Value, capture
from sniffio import thread_local as sniffio_library
from sortedcontainers import SortedDict
@@ -1903,7 +1904,9 @@ def current_root_task(self) -> Task | None:
################
@_public
- def reschedule(self, task: Task, next_send: Outcome[object] = _NO_SEND) -> None:
+ def reschedule(
+ self, task: Task, next_send: outcome.Outcome[object] = _NO_SEND
+ ) -> None:
"""Reschedule the given task with the given
:class:`outcome.Outcome`.
@@ -3113,7 +3116,11 @@ def in_trio_task() -> bool:
WindowsIOManager as TheIOManager,
_WindowsStatistics as IOStatistics,
)
-elif sys.platform == "linux" or (not TYPE_CHECKING and hasattr(select, "epoll")):
+elif (
+ sys.platform == "linux"
+ or sys.platform == "android"
+ or (not TYPE_CHECKING and hasattr(select, "epoll"))
+):
from ._generated_io_epoll import *
from ._io_epoll import (
EpollIOManager as TheIOManager,
diff --git a/src/trio/_core/_tests/test_asyncgen.py b/src/trio/_core/_tests/test_asyncgen.py
index 8147a0e57b..91ca0250aa 100644
--- a/src/trio/_core/_tests/test_asyncgen.py
+++ b/src/trio/_core/_tests/test_asyncgen.py
@@ -221,13 +221,15 @@ async def async_main() -> None:
saved.append(agen())
await saved[-1].asend(None)
+ ATTEMPT_AMOUNT = 50
+
# Actually running into the edge case requires that the run_sync_soon task
# execute in between the system nursery's closure and the strong-ification
# of runner.asyncgens. There's about a 25% chance that it doesn't
# (if the run_sync_soon task runs before init on one tick and after init
# on the next tick); if we try enough times, we can make the chance of
# failure as small as we want.
- for _attempt in range(50):
+ for _ in range(ATTEMPT_AMOUNT):
needs_retry = False
record.clear()
saved.clear()
@@ -240,7 +242,7 @@ async def async_main() -> None:
else: # pragma: no cover
pytest.fail(
"Didn't manage to hit the trailing_finalizer_asyncgens case "
- f"despite trying {_attempt} times",
+ f"despite trying {ATTEMPT_AMOUNT} times",
)
diff --git a/src/trio/_core/_tests/test_cancelled.py b/src/trio/_core/_tests/test_cancelled.py
index 0c144c37f0..0280b6068b 100644
--- a/src/trio/_core/_tests/test_cancelled.py
+++ b/src/trio/_core/_tests/test_cancelled.py
@@ -7,7 +7,7 @@
import trio
from trio import Cancelled
from trio.lowlevel import current_task
-from trio.testing import RaisesGroup, wait_all_tasks_blocked
+from trio.testing import wait_all_tasks_blocked
from .test_ki import ki_self
@@ -108,7 +108,7 @@ async def failing_task(task_status: trio.TaskStatus[trio.lowlevel.Task]) -> None
task_status.started(current_task())
raise ValueError
- with RaisesGroup(ValueError, TypeError):
+ with pytest.RaisesGroup(ValueError, TypeError):
async with trio.open_nursery() as nursery:
fail_task = await nursery.start(failing_task)
with pytest.raises(Cancelled, match=match_str.format(fail_task)):
@@ -123,7 +123,7 @@ async def failing_task(task_status: trio.TaskStatus[trio.lowlevel.Task]) -> None
await wait_all_tasks_blocked()
raise ValueError
- with RaisesGroup(ValueError, TypeError):
+ with pytest.RaisesGroup(ValueError, TypeError):
async with trio.open_nursery() as nursery:
fail_task = await nursery.start(failing_task)
await nursery.start(cancelled_task, fail_task)
@@ -147,7 +147,7 @@ async def cancelled_task() -> None:
):
await trio.sleep_forever()
- with RaisesGroup(ValueError):
+ with pytest.RaisesGroup(ValueError):
async with trio.open_nursery() as nursery:
nursery.start_soon(cancelled_task)
await wait_all_tasks_blocked()
@@ -192,7 +192,7 @@ async def child() -> None:
):
await trio.sleep_forever()
- with RaisesGroup(ValueError):
+ with pytest.RaisesGroup(ValueError):
async with trio.open_nursery() as nursery:
nursery.start_soon(child)
await ev.wait()
@@ -214,7 +214,7 @@ async def sleeper(name: str) -> None:
async def raiser(name: str) -> None:
ki_self()
- with RaisesGroup(KeyboardInterrupt):
+ with pytest.RaisesGroup(KeyboardInterrupt):
async with trio.open_nursery() as nursery:
nursery.start_soon(sleeper, "s1")
nursery.start_soon(sleeper, "s2")
diff --git a/src/trio/_core/_tests/test_ki.py b/src/trio/_core/_tests/test_ki.py
index a8d81ca020..ea45edaef6 100644
--- a/src/trio/_core/_tests/test_ki.py
+++ b/src/trio/_core/_tests/test_ki.py
@@ -12,8 +12,6 @@
import outcome
import pytest
-from trio.testing import RaisesGroup
-
from .tutil import gc_collect_harder
try:
@@ -164,7 +162,7 @@ def protected_manager() -> Iterator[None]:
@pytest.mark.skipif(async_generator is None, reason="async_generator not installed")
async def test_async_generator_agen_protection() -> None:
@_core.enable_ki_protection
- @async_generator # type: ignore[misc] # untyped generator
+ @async_generator # type: ignore[untyped-decorator]
async def agen_protected1() -> None: # type: ignore[misc] # untyped generator
assert _core.currently_ki_protected()
try:
@@ -173,7 +171,7 @@ async def agen_protected1() -> None: # type: ignore[misc] # untyped generator
assert _core.currently_ki_protected()
@_core.disable_ki_protection
- @async_generator # type: ignore[misc] # untyped generator
+ @async_generator # type: ignore[untyped-decorator]
async def agen_unprotected1() -> None: # type: ignore[misc] # untyped generator
assert not _core.currently_ki_protected()
try:
@@ -182,7 +180,7 @@ async def agen_unprotected1() -> None: # type: ignore[misc] # untyped generator
assert not _core.currently_ki_protected()
# Swap the order of the decorators:
- @async_generator # type: ignore[misc] # untyped generator
+ @async_generator # type: ignore[untyped-decorator]
@_core.enable_ki_protection
async def agen_protected2() -> None: # type: ignore[misc] # untyped generator
assert _core.currently_ki_protected()
@@ -191,7 +189,7 @@ async def agen_protected2() -> None: # type: ignore[misc] # untyped generator
finally:
assert _core.currently_ki_protected()
- @async_generator # type: ignore[misc] # untyped generator
+ @async_generator # type: ignore[untyped-decorator]
@_core.disable_ki_protection
async def agen_unprotected2() -> None: # type: ignore[misc] # untyped generator
assert not _core.currently_ki_protected()
@@ -309,7 +307,7 @@ async def check_unprotected_kill() -> None:
nursery.start_soon(raiser, "r1", record_set)
# raises inside a nursery, so the KeyboardInterrupt is wrapped in an ExceptionGroup
- with RaisesGroup(KeyboardInterrupt):
+ with pytest.RaisesGroup(KeyboardInterrupt):
_core.run(check_unprotected_kill)
assert record_set == {"s1 ok", "s2 ok", "r1 raise ok"}
@@ -326,7 +324,7 @@ async def check_protected_kill() -> None:
# __aexit__ blocks, and then receives the KI
# raises inside a nursery, so the KeyboardInterrupt is wrapped in an ExceptionGroup
- with RaisesGroup(KeyboardInterrupt):
+ with pytest.RaisesGroup(KeyboardInterrupt):
_core.run(check_protected_kill)
assert record_set == {"s1 ok", "s2 ok", "r1 cancel ok"}
diff --git a/src/trio/_core/_tests/test_parking_lot.py b/src/trio/_core/_tests/test_parking_lot.py
index 3348e47cbf..55c5144f91 100644
--- a/src/trio/_core/_tests/test_parking_lot.py
+++ b/src/trio/_core/_tests/test_parking_lot.py
@@ -11,7 +11,6 @@
current_task,
remove_parking_lot_breaker,
)
-from trio.testing import Matcher, RaisesGroup
from ... import _core
from ...testing import wait_all_tasks_blocked
@@ -267,8 +266,8 @@ async def bad_parker(lot: ParkingLot, scope: _core.CancelScope) -> None:
cs = _core.CancelScope()
# check that parked task errors
- with RaisesGroup(
- Matcher(_core.BrokenResourceError, match="^Parking lot broken by"),
+ with pytest.RaisesGroup(
+ pytest.RaisesExc(_core.BrokenResourceError, match="^Parking lot broken by"),
):
async with _core.open_nursery() as nursery:
nursery.start_soon(bad_parker, lot, cs)
@@ -382,8 +381,8 @@ async def return_me_and_park(
await lot.park()
lot = ParkingLot()
- with RaisesGroup(
- Matcher(_core.BrokenResourceError, match="^Parking lot broken by"),
+ with pytest.RaisesGroup(
+ pytest.RaisesExc(_core.BrokenResourceError, match="^Parking lot broken by"),
):
async with _core.open_nursery() as nursery:
child_task = await nursery.start(return_me_and_park, lot)
diff --git a/src/trio/_core/_tests/test_run.py b/src/trio/_core/_tests/test_run.py
index 9b059a4366..94e448448d 100644
--- a/src/trio/_core/_tests/test_run.py
+++ b/src/trio/_core/_tests/test_run.py
@@ -27,8 +27,6 @@
from ..._threads import to_thread_run_sync
from ..._timeouts import fail_after, sleep
from ...testing import (
- Matcher,
- RaisesGroup,
Sequencer,
assert_checkpoints,
wait_all_tasks_blocked,
@@ -134,7 +132,7 @@ async def test_nursery_warn_use_async_with() -> None:
async def test_nursery_main_block_error_basic() -> None:
exc = ValueError("whoops")
- with RaisesGroup(Matcher(check=lambda e: e is exc)):
+ with pytest.RaisesGroup(pytest.RaisesExc(check=lambda e: e is exc)):
async with _core.open_nursery():
raise exc
@@ -145,7 +143,7 @@ async def test_child_crash_basic() -> None:
async def erroring() -> NoReturn:
raise my_exc
- with RaisesGroup(Matcher(check=lambda e: e is my_exc)):
+ with pytest.RaisesGroup(pytest.RaisesExc(check=lambda e: e is my_exc)):
# nursery.__aexit__ propagates exception from child back to parent
async with _core.open_nursery() as nursery:
nursery.start_soon(erroring)
@@ -187,7 +185,7 @@ async def main() -> None:
nursery.start_soon(looper)
nursery.start_soon(crasher)
- with RaisesGroup(Matcher(ValueError, "^argh$")):
+ with pytest.RaisesGroup(pytest.RaisesExc(ValueError, match="^argh$")):
_core.run(main)
assert looper_record == ["cancelled"]
@@ -204,7 +202,7 @@ async def main() -> NoReturn:
nursery.start_soon(crasher)
raise KeyError
- with RaisesGroup(ValueError, KeyError):
+ with pytest.RaisesGroup(ValueError, KeyError):
_core.run(main)
@@ -217,7 +215,7 @@ async def main() -> None:
nursery.start_soon(crasher, KeyError)
nursery.start_soon(crasher, ValueError)
- with RaisesGroup(ValueError, KeyError):
+ with pytest.RaisesGroup(ValueError, KeyError):
_core.run(main)
@@ -225,7 +223,7 @@ async def test_child_crash_wakes_parent() -> None:
async def crasher() -> NoReturn:
raise ValueError("this is a crash")
- with RaisesGroup(Matcher(ValueError, "^this is a crash$")):
+ with pytest.RaisesGroup(pytest.RaisesExc(ValueError, match="^this is a crash$")):
async with _core.open_nursery() as nursery:
nursery.start_soon(crasher)
await sleep_forever()
@@ -465,13 +463,13 @@ async def crasher() -> NoReturn:
# This is outside the outer scope, so all the Cancelled
# exceptions should have been absorbed, leaving just a regular
# KeyError from crasher(), wrapped in an ExceptionGroup
- with RaisesGroup(KeyError):
+ with pytest.RaisesGroup(KeyError):
with _core.CancelScope() as outer:
# Since the outer scope became cancelled before the
# nursery block exited, all cancellations inside the
# nursery block continue propagating to reach the
# outer scope.
- with RaisesGroup(
+ with pytest.RaisesGroup(
_core.Cancelled,
_core.Cancelled,
_core.Cancelled,
@@ -490,7 +488,7 @@ async def crasher() -> NoReturn:
# And one that raises a different error
nursery.start_soon(crasher) # t4
# and then our __aexit__ also receives an outer Cancelled
- # reraise the exception caught by RaisesGroup for the
+ # reraise the exception caught by pytest.RaisesGroup for the
# CancelScope to handle
raise excinfo.value
@@ -824,11 +822,11 @@ def no_context(exc: RuntimeError) -> bool:
return exc.__context__ is None
msg = "closed before the task exited"
- group = RaisesGroup(
- Matcher(RuntimeError, match=msg, check=no_context),
- Matcher(RuntimeError, match=msg, check=no_context),
+ group = pytest.RaisesGroup(
+ pytest.RaisesExc(RuntimeError, match=msg, check=no_context),
+ pytest.RaisesExc(RuntimeError, match=msg, check=no_context),
# sleep_forever
- Matcher(
+ pytest.RaisesExc(
RuntimeError,
match=msg,
check=lambda x: isinstance(x.__context__, _core.Cancelled),
@@ -1062,7 +1060,7 @@ async def main() -> None:
# the upstream issue is resolved.
@restore_unraisablehook()
@pytest.mark.skipif(
- sys.version_info[:3] == (3, 14, 0),
+ sys.version_info[:2] == (3, 14),
reason="https://github.com/python/cpython/issues/133932",
)
def test_error_in_run_loop() -> None:
@@ -1126,7 +1124,9 @@ async def main() -> None:
# the first exceptiongroup is from the first nursery opened in Runner.init()
# the second exceptiongroup is from the second nursery opened in Runner.init()
# the third exceptongroup is from the nursery defined in `system_task` above
- assert RaisesGroup(RaisesGroup(RaisesGroup(KeyError, ValueError))).matches(
+ assert pytest.RaisesGroup(
+ pytest.RaisesGroup(pytest.RaisesGroup(KeyError, ValueError))
+ ).matches(
excinfo.value.__cause__,
)
@@ -1156,7 +1156,9 @@ async def main() -> None:
_core.run(main)
# See explanation for triple-wrap in test_system_task_crash_ExceptionGroup
- assert RaisesGroup(RaisesGroup(RaisesGroup(ValueError))).matches(
+ assert pytest.RaisesGroup(
+ pytest.RaisesGroup(pytest.RaisesGroup(ValueError))
+ ).matches(
excinfo.value.__cause__,
)
@@ -1172,7 +1174,9 @@ async def main() -> None:
with pytest.raises(_core.TrioInternalError) as excinfo:
_core.run(main)
# "Only" double-wrapped since ki() doesn't create an exceptiongroup
- assert RaisesGroup(RaisesGroup(KeyboardInterrupt)).matches(excinfo.value.__cause__)
+ assert pytest.RaisesGroup(pytest.RaisesGroup(KeyboardInterrupt)).matches(
+ excinfo.value.__cause__
+ )
# This used to fail because checkpoint was a yield followed by an immediate
@@ -1280,7 +1284,9 @@ async def child() -> None:
await sleep_forever()
raise
- with RaisesGroup(Matcher(KeyError, check=lambda e: e.__context__ is None)):
+ with pytest.RaisesGroup(
+ pytest.RaisesExc(KeyError, check=lambda e: e.__context__ is None)
+ ):
async with _core.open_nursery() as nursery:
nursery.start_soon(child)
await wait_all_tasks_blocked()
@@ -1306,11 +1312,11 @@ async def child() -> None:
except KeyError:
await sleep_forever()
- with RaisesGroup(
- Matcher(
+ with pytest.RaisesGroup(
+ pytest.RaisesExc(
ValueError,
- "error text",
- lambda e: isinstance(e.__context__, KeyError),
+ match="error text",
+ check=lambda e: isinstance(e.__context__, KeyError),
),
):
async with _core.open_nursery() as nursery:
@@ -1345,7 +1351,9 @@ async def inner(exc: BaseException) -> None:
await sleep_forever()
assert not_none(sys.exc_info()[1]) is exc
- with RaisesGroup(Matcher(KeyError, check=lambda e: e.__context__ is None)):
+ with pytest.RaisesGroup(
+ pytest.RaisesExc(KeyError, check=lambda e: e.__context__ is None)
+ ):
async with _core.open_nursery() as nursery:
nursery.start_soon(child)
await wait_all_tasks_blocked()
@@ -1375,11 +1383,11 @@ async def inner() -> None:
except IndexError:
await sleep_forever()
- with RaisesGroup(
- Matcher(
+ with pytest.RaisesGroup(
+ pytest.RaisesExc(
ValueError,
- "^Unique Text$",
- lambda e: isinstance(e.__context__, IndexError)
+ match="^Unique Text$",
+ check=lambda e: isinstance(e.__context__, IndexError)
and isinstance(e.__context__.__context__, KeyError),
),
):
@@ -1397,7 +1405,9 @@ async def crasher() -> NoReturn:
raise KeyError
# the ExceptionGroup should not have the KeyError or ValueError as context
- with RaisesGroup(ValueError, KeyError, check=lambda x: x.__context__ is None):
+ with pytest.RaisesGroup(
+ ValueError, KeyError, check=lambda x: x.__context__ is None
+ ):
async with _core.open_nursery() as nursery:
nursery.start_soon(crasher)
raise ValueError
@@ -1527,7 +1537,9 @@ async def main() -> None:
_core.run(main)
# the first exceptiongroup is from the first nursery opened in Runner.init()
# the second exceptiongroup is from the second nursery opened in Runner.init()
- assert RaisesGroup(RaisesGroup(KeyError)).matches(excinfo.value.__cause__)
+ assert pytest.RaisesGroup(pytest.RaisesGroup(KeyError)).matches(
+ excinfo.value.__cause__
+ )
assert record == {"2nd run_sync_soon ran", "cancelled!"}
@@ -1641,7 +1653,7 @@ async def main() -> None:
with pytest.raises(_core.TrioInternalError) as excinfo:
_core.run(main)
- assert RaisesGroup(KeyError).matches(excinfo.value.__cause__)
+ assert pytest.RaisesGroup(KeyError).matches(excinfo.value.__cause__)
assert record == ["main exiting", "2nd ran"]
@@ -1876,11 +1888,15 @@ async def async_gen(arg: T) -> AsyncGenerator[T, None]: # pragma: no cover
# bad_call_spawn calls the function inside a nursery, so the exception will be
# wrapped in an exceptiongroup
- with RaisesGroup(Matcher(TypeError, "expecting an async function")):
+ with pytest.RaisesGroup(
+ pytest.RaisesExc(TypeError, match="expecting an async function")
+ ):
bad_call_spawn(f()) # type: ignore[arg-type]
- with RaisesGroup(
- Matcher(TypeError, "expected an async function but got an async generator"),
+ with pytest.RaisesGroup(
+ pytest.RaisesExc(
+ TypeError, match="expected an async function but got an async generator"
+ ),
):
bad_call_spawn(async_gen, 0) # type: ignore
@@ -1903,7 +1919,7 @@ async def misguided() -> None:
async def test_asyncio_function_inside_nursery_does_not_explode() -> None:
# Regression test for https://github.com/python-trio/trio/issues/552
- with RaisesGroup(Matcher(TypeError, "asyncio")):
+ with pytest.RaisesGroup(pytest.RaisesExc(TypeError, match="asyncio")):
async with _core.open_nursery() as nursery:
nursery.start_soon(sleep_forever)
await create_asyncio_future_in_new_loop()
@@ -1943,7 +1959,7 @@ async def noop_with_no_checkpoint() -> None:
with _core.CancelScope() as cancel_scope:
cancel_scope.cancel()
- with RaisesGroup(KeyError):
+ with pytest.RaisesGroup(KeyError):
async with _core.open_nursery():
raise KeyError
@@ -2081,7 +2097,7 @@ async def test_task_nursery_stack() -> None:
assert task._child_nurseries == []
async with _core.open_nursery() as nursery1:
assert task._child_nurseries == [nursery1]
- with RaisesGroup(KeyError):
+ with pytest.RaisesGroup(KeyError):
async with _core.open_nursery() as nursery2:
assert task._child_nurseries == [nursery1, nursery2]
raise KeyError
@@ -2179,7 +2195,7 @@ async def start_sleep_then_crash(nursery: _core.Nursery) -> None:
async def test_nursery_explicit_exception() -> None:
- with RaisesGroup(KeyError):
+ with pytest.RaisesGroup(KeyError):
async with _core.open_nursery():
raise KeyError()
@@ -2188,7 +2204,7 @@ async def test_nursery_stop_iteration() -> None:
async def fail() -> NoReturn:
raise ValueError
- with RaisesGroup(StopIteration, ValueError):
+ with pytest.RaisesGroup(StopIteration, ValueError):
async with _core.open_nursery() as nursery:
nursery.start_soon(fail)
raise StopIteration
@@ -2235,7 +2251,7 @@ async def __anext__(self) -> list[int]:
# With strict_exception_groups enabled, users now need to unwrap
# StopAsyncIteration and re-raise it.
# This would be relatively clean on python3.11+ with except*.
- # We could also use RaisesGroup, but that's primarily meant as
+ # We could also use pytest.RaisesGroup, but that's primarily meant as
# test infra, not as a runtime tool.
if len(e.exceptions) == 1 and isinstance(
e.exceptions[0],
@@ -2264,7 +2280,7 @@ def check_traceback(exc: KeyError) -> bool:
assert tb is not None
return tb.tb_frame.f_code is my_child_task.__code__
- with RaisesGroup(Matcher(KeyError, check=check_traceback)):
+ with pytest.RaisesGroup(pytest.RaisesExc(KeyError, check=check_traceback)):
# For now cancel/nursery scopes still leave a bunch of tb gunk behind.
# But if there's an Exceptiongroup, they leave it on the group,
# which lets us get a clean look at the KeyError itself.
@@ -2443,7 +2459,7 @@ async def detachable_coroutine(
# Check the exception paths too
task = None
pdco_outcome = None
- with RaisesGroup(KeyError):
+ with pytest.RaisesGroup(KeyError):
async with _core.open_nursery() as nursery:
nursery.start_soon(detachable_coroutine, outcome.Error(KeyError()), "uh oh")
throw_in = ValueError()
@@ -2619,7 +2635,9 @@ async def crasher() -> NoReturn:
# (See https://github.com/python-trio/trio/pull/1864)
await do_a_cancel()
- with RaisesGroup(Matcher(ValueError, "^this is a crash$")):
+ with pytest.RaisesGroup(
+ pytest.RaisesExc(ValueError, match="^this is a crash$")
+ ):
async with _core.open_nursery() as nursery:
# cover NurseryManager.__aexit__
nursery.start_soon(crasher)
@@ -2645,8 +2663,8 @@ async def crasher() -> NoReturn:
old_flags = gc.get_debug()
try:
with (
- RaisesGroup(
- Matcher(ValueError, "^this is a crash$"),
+ pytest.RaisesGroup(
+ pytest.RaisesExc(ValueError, match="^this is a crash$"),
),
_core.CancelScope() as outer,
):
@@ -2769,15 +2787,15 @@ def run_main() -> None:
# mypy doesn't like kwarg magic
_core.run(main, **_create_kwargs(run_strict)) # type: ignore[arg-type]
- matcher = Matcher(RuntimeError, r"^test error$")
+ matcher = pytest.RaisesExc(RuntimeError, match=r"^test error$")
if multiple_exceptions:
- with RaisesGroup(matcher, matcher):
+ with pytest.RaisesGroup(matcher, matcher):
run_main()
elif open_nursery_strict or (
open_nursery_strict is None and run_strict is not False
):
- with RaisesGroup(matcher):
+ with pytest.RaisesGroup(matcher):
run_main()
else:
with pytest.raises(RuntimeError, match=r"^test error$"):
@@ -2798,11 +2816,11 @@ async def raise_error() -> NoReturn:
raise RuntimeError("test error")
# mypy requires explicit type for conditional expression
- maybe_wrapped_runtime_error: type[RuntimeError] | RaisesGroup[RuntimeError] = (
- RuntimeError if strict is False else RaisesGroup(RuntimeError)
- )
+ maybe_wrapped_runtime_error: (
+ type[RuntimeError] | pytest.RaisesGroup[RuntimeError]
+ ) = (RuntimeError if strict is False else pytest.RaisesGroup(RuntimeError))
- with RaisesGroup(RuntimeError, maybe_wrapped_runtime_error):
+ with pytest.RaisesGroup(RuntimeError, maybe_wrapped_runtime_error):
async with _core.open_nursery() as nursery:
nursery.start_soon(sleep_forever)
nursery.start_soon(raise_error)
@@ -2818,7 +2836,7 @@ async def test_cancel_scope_no_cancellederror() -> None:
a Cancelled exception, it will NOT set the ``cancelled_caught`` flag.
"""
- with RaisesGroup(RuntimeError, RuntimeError, match="test"):
+ with pytest.RaisesGroup(RuntimeError, RuntimeError, match="test"):
with _core.CancelScope() as scope:
scope.cancel()
raise ExceptionGroup("test", [RuntimeError(), RuntimeError()])
@@ -2915,7 +2933,7 @@ async def spawn_tasks_in_old_nursery(task_status: _core.TaskStatus[None]) -> Non
async with _core.open_nursery() as nursery:
with pytest.raises(_core.TrioInternalError) as excinfo:
await nursery.start(spawn_tasks_in_old_nursery)
- assert RaisesGroup(ValueError, ValueError).matches(excinfo.value.__cause__)
+ assert pytest.RaisesGroup(ValueError, ValueError).matches(excinfo.value.__cause__)
if sys.version_info >= (3, 11):
diff --git a/src/trio/_core/_tests/test_thread_cache.py b/src/trio/_core/_tests/test_thread_cache.py
index a308befb67..3abdd59e43 100644
--- a/src/trio/_core/_tests/test_thread_cache.py
+++ b/src/trio/_core/_tests/test_thread_cache.py
@@ -220,23 +220,8 @@ def foo() -> None:
if child_pid != 0:
# if this test fails, this will hang, triggering a timeout.
os.waitpid(child_pid, 0)
- else:
- # this is necessary because os._exit doesn't unwind the stack,
- # so coverage doesn't get to automatically stop and save
- # coverage information.
- try:
- import coverage
-
- cov = coverage.Coverage.current()
- # the following pragmas are necessary because if coverage:
- # - isn't running, then it can't record the branch not
- # taken
- # - isn't installed, then it can't record the ImportError
-
- if cov: # pragma: no branch
- cov.stop()
- cov.save()
- except ImportError: # pragma: no cover
- pass
-
- os._exit(0) # pragma: no cover # coverage was stopped above.
+ else: # pragma: no cover # coverage is shut down by os._exit(0)
+ # we would *want* to allow coverage to take a snapshot here. check
+ # git blame for how to do that. however, that times out for some
+ # reason when having `tests/` in `source` for coverage.py.
+ os._exit(0)
diff --git a/src/trio/_core/_thread_cache.py b/src/trio/_core/_thread_cache.py
index 44820e7711..c2b2315bd3 100644
--- a/src/trio/_core/_thread_cache.py
+++ b/src/trio/_core/_thread_cache.py
@@ -303,7 +303,9 @@ def start_thread_soon(
THREAD_CACHE.start_thread_soon(fn, deliver, name)
-def clear_worker_threads() -> None:
+def clear_worker_threads() -> (
+ None
+): # pragma: no cover # see test_clear_thread_cache_after_fork
# This is OK because the child process does not actually have any
# worker threads. Additionally, while WorkerThread keeps a strong
# reference and so would get affected, the only place those are
diff --git a/src/trio/_core/_traps.py b/src/trio/_core/_traps.py
index 652cc0b879..14f66af927 100644
--- a/src/trio/_core/_traps.py
+++ b/src/trio/_core/_traps.py
@@ -66,7 +66,9 @@ class PermanentlyDetachCoroutineObject:
def _real_async_yield(
obj: MessageType,
) -> Generator[MessageType, None, None]:
- return (yield obj)
+ # "Using `yield` and `return {value}` in a generator function can
+ # lead to confusing behavior"
+ return (yield obj) # noqa: B901
# Real yield value is from trio's main loop, but type checkers can't
diff --git a/src/trio/_deprecate.py b/src/trio/_deprecate.py
index 5c827b4fda..2f1f9f43a4 100644
--- a/src/trio/_deprecate.py
+++ b/src/trio/_deprecate.py
@@ -7,6 +7,8 @@
import attrs
+from ._util import final
+
if TYPE_CHECKING:
from collections.abc import Callable
@@ -139,6 +141,7 @@ def wrapper(*args: ArgsT.args, **kwargs: ArgsT.kwargs) -> RetT:
return wrapper
+@final
@attrs.frozen(slots=False)
class DeprecatedAttribute:
_not_set: ClassVar[object] = object()
diff --git a/src/trio/_highlevel_open_tcp_stream.py b/src/trio/_highlevel_open_tcp_stream.py
index 1787f4a97e..33f1e86025 100644
--- a/src/trio/_highlevel_open_tcp_stream.py
+++ b/src/trio/_highlevel_open_tcp_stream.py
@@ -162,7 +162,7 @@ def format_host_port(host: str | bytes, port: int | str) -> str:
return f"{host}:{port}"
-# Twisted's HostnameEndpoint has a good set of configurables:
+# Twisted's HostnameEndpoint has a good set of configurations:
# https://twistedmatrix.com/documents/current/api/twisted.internet.endpoints.HostnameEndpoint.html
#
# - per-connection timeout
diff --git a/src/trio/_tests/_check_type_completeness.json b/src/trio/_tests/_check_type_completeness.json
index 72d981f89c..6e8c54ea8b 100644
--- a/src/trio/_tests/_check_type_completeness.json
+++ b/src/trio/_tests/_check_type_completeness.json
@@ -45,10 +45,6 @@
"No docstring found for class \"trio.socket.herror\"",
"No docstring found for function \"trio._core._mock_clock.MockClock.start_clock\"",
"No docstring found for function \"trio._core._mock_clock.MockClock.current_time\"",
- "No docstring found for function \"trio._core._mock_clock.MockClock.deadline_to_sleep_time\"",
- "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.exconly\"",
- "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.errisinstance\"",
- "No docstring found for function \"trio.testing._raises_group._ExceptionInfo.getrepr\"",
- "No docstring found for function \"trio.testing._raises_group.RaisesGroup.expected_type\""
+ "No docstring found for function \"trio._core._mock_clock.MockClock.deadline_to_sleep_time\""
]
}
diff --git a/src/trio/_tests/check_type_completeness.py b/src/trio/_tests/check_type_completeness.py
index ef5bdaafa4..a0f23a66e9 100755
--- a/src/trio/_tests/check_type_completeness.py
+++ b/src/trio/_tests/check_type_completeness.py
@@ -7,6 +7,7 @@
If this check is giving you false alarms, you can ignore them by adding logic to `has_docstring_at_runtime`, in the main loop in `check_type`, or by updating the json file.
"""
+
from __future__ import annotations
# this file is not run as part of the tests, instead it's run standalone from check.sh
diff --git a/src/trio/_tests/test_channel.py b/src/trio/_tests/test_channel.py
index 986207b309..c8b2efddc6 100644
--- a/src/trio/_tests/test_channel.py
+++ b/src/trio/_tests/test_channel.py
@@ -8,7 +8,7 @@
import trio
from trio import EndOfChannel, as_safe_channel, open_memory_channel
-from ..testing import Matcher, RaisesGroup, assert_checkpoints, wait_all_tasks_blocked
+from ..testing import assert_checkpoints, wait_all_tasks_blocked
if sys.version_info < (3, 11):
from exceptiongroup import ExceptionGroup
@@ -518,9 +518,9 @@ async def agen(events: list[str]) -> AsyncGenerator[int]:
raise ValueError("agen")
events: list[str] = []
- with RaisesGroup(
- Matcher(ValueError, match="^agen$"),
- Matcher(TypeError, match="^iterator$"),
+ with pytest.RaisesGroup(
+ pytest.RaisesExc(ValueError, match="^agen$"),
+ pytest.RaisesExc(TypeError, match="^iterator$"),
) as g:
async with agen(events) as recv_chan:
async for i in recv_chan: # pragma: no branch
@@ -574,7 +574,7 @@ async def agen() -> AsyncGenerator[None]:
raise NotImplementedError("not entered")
yield # pragma: no cover
- with RaisesGroup(Matcher(ValueError, match="bar"), match="foo"):
+ with pytest.RaisesGroup(pytest.RaisesExc(ValueError, match="bar"), match="foo"):
async with agen() as _:
raise ExceptionGroup("foo", [ValueError("bar")])
diff --git a/src/trio/_tests/test_deprecate.py b/src/trio/_tests/test_deprecate.py
index 4786b06c96..b7614d0e52 100644
--- a/src/trio/_tests/test_deprecate.py
+++ b/src/trio/_tests/test_deprecate.py
@@ -200,45 +200,33 @@ def docstring_test4() -> None: # pragma: no cover
def test_deprecated_docstring_munging() -> None:
- assert (
- docstring_test1.__doc__
- == """Hello!
+ assert docstring_test1.__doc__ == """Hello!
.. deprecated:: 2.1
Use hi instead.
For details, see `issue #1 `__.
"""
- )
- assert (
- docstring_test2.__doc__
- == """Hello!
+ assert docstring_test2.__doc__ == """Hello!
.. deprecated:: 2.1
Use hi instead.
"""
- )
- assert (
- docstring_test3.__doc__
- == """Hello!
+ assert docstring_test3.__doc__ == """Hello!
.. deprecated:: 2.1
For details, see `issue #1 `__.
"""
- )
- assert (
- docstring_test4.__doc__
- == """Hello!
+ assert docstring_test4.__doc__ == """Hello!
.. deprecated:: 2.1
"""
- )
def test_module_with_deprecations(recwarn_always: pytest.WarningsRecorder) -> None:
diff --git a/src/trio/_tests/test_exports.py b/src/trio/_tests/test_exports.py
index 0f8158f377..0414d43423 100644
--- a/src/trio/_tests/test_exports.py
+++ b/src/trio/_tests/test_exports.py
@@ -175,6 +175,10 @@ def no_underscores(symbols: Iterable[str]) -> set[str]:
completions = script.complete()
static_names = no_underscores(c.name for c in completions)
elif tool == "mypy":
+ if sys.implementation.name != "cpython":
+ # https://github.com/python/mypy/issues/20329
+ pytest.skip("mypy does not support pypy")
+
if not RUN_SLOW: # pragma: no cover
pytest.skip("use --run-slow to check against mypy")
@@ -272,6 +276,10 @@ def no_hidden(symbols: Iterable[str]) -> set[str]:
if tool == "jedi" and sys.implementation.name != "cpython":
pytest.skip("jedi does not support pypy")
+ if tool == "mypy" and sys.implementation.name != "cpython":
+ # https://github.com/python/mypy/issues/20329
+ pytest.skip("mypy does not support pypy")
+
if tool == "mypy":
cache = Path.cwd() / ".mypy_cache"
@@ -325,7 +333,7 @@ def lookup_symbol(symbol: str) -> dict[str, Any]: # type: ignore[misc, explicit
# __init__ is called (and reason they don't use attrs is because they're going
# to be reimplemented in pytest).
# Not 100% that's the case, and it works locally, so whatever /shrug
- if class_ is trio.testing.RaisesGroup or class_ is trio.testing.Matcher:
+ if module_name == "trio.testing" and class_name in ("_RaisesGroup", "_Matcher"):
continue
# dir() and inspect.getmembers doesn't display properties from the metaclass
@@ -357,17 +365,6 @@ def lookup_symbol(symbol: str) -> dict[str, Any]: # type: ignore[misc, explicit
ignore_names.add("__firstlineno__")
ignore_names.add("__static_attributes__")
- # pypy seems to have some additional dunders that differ
- if sys.implementation.name == "pypy":
- ignore_names |= {
- "__basicsize__",
- "__dictoffset__",
- "__itemsize__",
- "__sizeof__",
- "__weakrefoffset__",
- "__unicode__",
- }
-
# inspect.getmembers sees `name` and `value` in Enums, otherwise
# it behaves the same way as `dir`
# runtime_names = no_underscores(dir(class_))
diff --git a/src/trio/_tests/test_fakenet.py b/src/trio/_tests/test_fakenet.py
index c7d21ad25b..12dc6a9088 100644
--- a/src/trio/_tests/test_fakenet.py
+++ b/src/trio/_tests/test_fakenet.py
@@ -98,9 +98,9 @@ async def test_recv_methods() -> None:
buf = bytearray(10)
with pytest.raises(NotImplementedError, match=r"^partial recvfrom_into$"):
- (nbytes, addr) = await s2.recvfrom_into(buf, nbytes=2)
+ nbytes, addr = await s2.recvfrom_into(buf, nbytes=2)
- (nbytes, addr) = await s2.recvfrom_into(buf)
+ nbytes, addr = await s2.recvfrom_into(buf)
assert nbytes == 3
assert buf == b"ghi" + b"\x00" * 7
assert addr == s1.getsockname()
@@ -154,7 +154,7 @@ async def test_nonwindows_functionality() -> None:
assert exc.value.errno == errno.ENOTCONN
assert await s1.sendmsg([b"jkl"], (), 0, s2.getsockname()) == 3
- (data, ancdata, msg_flags, addr) = await s2.recvmsg(10)
+ data, ancdata, msg_flags, addr = await s2.recvmsg(10)
assert data == b"jkl"
assert ancdata == []
assert msg_flags == 0
@@ -167,7 +167,7 @@ async def test_nonwindows_functionality() -> None:
buf1 = bytearray(2)
buf2 = bytearray(3)
ret = await s2.recvmsg_into([buf1, buf2])
- (nbytes, ancdata, msg_flags, addr) = ret
+ nbytes, ancdata, msg_flags, addr = ret
assert nbytes == 4
assert buf1 == b"xy"
assert buf2 == b"zw" + b"\x00"
@@ -179,7 +179,7 @@ async def test_nonwindows_functionality() -> None:
assert await s1.sendto(b"xyzwv", s2.getsockname()) == 5
buf1 = bytearray(2)
ret = await s2.recvmsg_into([buf1])
- (nbytes, ancdata, msg_flags, addr) = ret
+ nbytes, ancdata, msg_flags, addr = ret
assert nbytes == 2
assert buf1 == b"xy"
assert ancdata == []
diff --git a/src/trio/_tests/test_highlevel_open_tcp_stream.py b/src/trio/_tests/test_highlevel_open_tcp_stream.py
index eaec52ad4e..193bf77e75 100644
--- a/src/trio/_tests/test_highlevel_open_tcp_stream.py
+++ b/src/trio/_tests/test_highlevel_open_tcp_stream.py
@@ -16,7 +16,6 @@
reorder_for_rfc_6555_section_5_4,
)
from trio.socket import AF_INET, AF_INET6, IPPROTO_TCP, SOCK_STREAM, SocketType
-from trio.testing import Matcher, RaisesGroup
if TYPE_CHECKING:
from collections.abc import Sequence
@@ -550,8 +549,8 @@ async def test_all_fail(autojump_clock: MockClock) -> None:
)
assert isinstance(exc, OSError)
- subexceptions = (Matcher(OSError, match="^sorry$"),) * 4
- assert RaisesGroup(
+ subexceptions = (pytest.RaisesExc(OSError, match="^sorry$"),) * 4
+ assert pytest.RaisesGroup(
*subexceptions,
match="all attempts to connect to test.example.com:80 failed",
).matches(exc.__cause__)
diff --git a/src/trio/_tests/test_highlevel_serve_listeners.py b/src/trio/_tests/test_highlevel_serve_listeners.py
index 9268555b32..1a6ac94690 100644
--- a/src/trio/_tests/test_highlevel_serve_listeners.py
+++ b/src/trio/_tests/test_highlevel_serve_listeners.py
@@ -5,15 +5,14 @@
from typing import TYPE_CHECKING, NoReturn, cast
import attrs
+import pytest
import trio
from trio import Nursery, StapledStream, TaskStatus
from trio.testing import (
- Matcher,
MemoryReceiveStream,
MemorySendStream,
MockClock,
- RaisesGroup,
memory_stream_pair,
wait_all_tasks_blocked,
)
@@ -21,8 +20,6 @@
if TYPE_CHECKING:
from collections.abc import Awaitable, Callable
- import pytest
-
from trio._channel import MemoryReceiveChannel, MemorySendChannel
from trio.abc import Stream
@@ -124,7 +121,7 @@ def check_error(e: BaseException) -> bool:
listener.accept_hook = raise_error
- with RaisesGroup(Matcher(check=check_error)):
+ with pytest.RaisesGroup(pytest.RaisesExc(check=check_error)):
await trio.serve_listeners(None, [listener]) # type: ignore[arg-type]
@@ -172,7 +169,7 @@ async def connection_watcher(
raise Done
# the exception is wrapped twice because we open two nested nurseries
- with RaisesGroup(RaisesGroup(Done)):
+ with pytest.RaisesGroup(pytest.RaisesGroup(Done)):
async with trio.open_nursery() as nursery:
value = await nursery.start(connection_watcher)
assert isinstance(value, trio.Nursery)
diff --git a/src/trio/_tests/test_path.py b/src/trio/_tests/test_path.py
index 533ff94c5a..9fe3920df3 100644
--- a/src/trio/_tests/test_path.py
+++ b/src/trio/_tests/test_path.py
@@ -210,16 +210,16 @@ async def test_globmethods(path: trio.Path) -> None:
await (path / "bar.dat").write_bytes(b"")
# Path.glob
- for _pattern, _results in {
+ for pattern, results in {
"*.txt": {"bar.txt"},
"**/*.txt": {"_bar.txt", "bar.txt"},
}.items():
entries = set()
- for entry in await path.glob(_pattern):
+ for entry in await path.glob(pattern):
assert isinstance(entry, trio.Path)
entries.add(entry.name)
- assert entries == _results
+ assert entries == results
# Path.rglob
entries = set()
diff --git a/src/trio/_tests/test_signals.py b/src/trio/_tests/test_signals.py
index 9c86741742..501ae1e80a 100644
--- a/src/trio/_tests/test_signals.py
+++ b/src/trio/_tests/test_signals.py
@@ -6,7 +6,6 @@
import pytest
import trio
-from trio.testing import RaisesGroup
from .. import _core
from .._signals import _signal_handler, get_pending_signal_count, open_signal_receiver
@@ -75,7 +74,7 @@ async def naughty() -> None:
async def test_open_signal_receiver_conflict() -> None:
- with RaisesGroup(trio.BusyResourceError):
+ with pytest.RaisesGroup(trio.BusyResourceError):
with open_signal_receiver(signal.SIGILL) as receiver:
async with trio.open_nursery() as nursery:
nursery.start_soon(receiver.__anext__)
diff --git a/src/trio/_tests/test_socket.py b/src/trio/_tests/test_socket.py
index 2b8a3a5ee1..03918b0e1f 100644
--- a/src/trio/_tests/test_socket.py
+++ b/src/trio/_tests/test_socket.py
@@ -975,7 +975,7 @@ async def test_send_recv_variants() -> None:
# recvfrom + sendto, with and without names
for target in targets:
assert await a.sendto(b"xxx", target) == 3
- (data, addr) = await b.recvfrom(10)
+ data, addr = await b.recvfrom(10)
assert data == b"xxx"
assert addr == a.getsockname()
@@ -991,21 +991,21 @@ async def test_send_recv_variants() -> None:
await a.sendto(b"xxx", tsocket.MSG_MORE, b.getsockname())
await a.sendto(b"yyy", tsocket.MSG_MORE, b.getsockname())
await a.sendto(b"zzz", b.getsockname())
- (data, addr) = await b.recvfrom(10)
+ data, addr = await b.recvfrom(10)
assert data == b"xxxyyyzzz"
assert addr == a.getsockname()
# recvfrom_into
assert await a.sendto(b"xxx", b.getsockname()) == 3
buf = bytearray(10)
- (nbytes, addr) = await b.recvfrom_into(buf)
+ nbytes, addr = await b.recvfrom_into(buf)
assert nbytes == 3
assert buf == b"xxx" + b"\x00" * 7
assert addr == a.getsockname()
if hasattr(b, "recvmsg"):
assert await a.sendto(b"xxx", b.getsockname()) == 3
- (data, ancdata, msg_flags, addr) = await b.recvmsg(10)
+ data, ancdata, msg_flags, addr = await b.recvmsg(10)
assert data == b"xxx"
assert ancdata == []
assert msg_flags == 0
@@ -1016,7 +1016,7 @@ async def test_send_recv_variants() -> None:
buf1 = bytearray(2)
buf2 = bytearray(3)
ret = await b.recvmsg_into([buf1, buf2])
- (nbytes, ancdata, msg_flags, addr) = ret
+ nbytes, ancdata, msg_flags, addr = ret
assert nbytes == 4
assert buf1 == b"xy"
assert buf2 == b"zw" + b"\x00"
diff --git a/src/trio/_tests/test_ssl.py b/src/trio/_tests/test_ssl.py
index b365f7dd35..085ce8f1dc 100644
--- a/src/trio/_tests/test_ssl.py
+++ b/src/trio/_tests/test_ssl.py
@@ -15,12 +15,7 @@
from trio import StapledStream
from trio._tests.pytest_plugin import skip_if_optional_else_raise
from trio.abc import ReceiveStream, SendStream
-from trio.testing import (
- Matcher,
- MemoryReceiveStream,
- MemorySendStream,
- RaisesGroup,
-)
+from trio.testing import MemoryReceiveStream, MemorySendStream
try:
import trustme
@@ -350,7 +345,9 @@ async def do_test(
args2: tuple[object, ...],
) -> None:
s = PyOpenSSLEchoStream()
- with RaisesGroup(Matcher(_core.BusyResourceError, "simultaneous")):
+ with pytest.RaisesGroup(
+ pytest.RaisesExc(_core.BusyResourceError, match="simultaneous")
+ ):
async with _core.open_nursery() as nursery:
nursery.start_soon(getattr(s, func1), *args1)
nursery.start_soon(getattr(s, func2), *args2)
@@ -774,7 +771,9 @@ async def do_test(
func2: Callable[[S], Awaitable[None]],
) -> None:
s, _ = ssl_lockstep_stream_pair(client_ctx)
- with RaisesGroup(Matcher(_core.BusyResourceError, "another task")):
+ with pytest.RaisesGroup(
+ pytest.RaisesExc(_core.BusyResourceError, match="another task")
+ ):
async with _core.open_nursery() as nursery:
nursery.start_soon(func1, s)
nursery.start_soon(func2, s)
diff --git a/src/trio/_tests/test_subprocess.py b/src/trio/_tests/test_subprocess.py
index 71b143c38c..05ac69d3f2 100644
--- a/src/trio/_tests/test_subprocess.py
+++ b/src/trio/_tests/test_subprocess.py
@@ -22,7 +22,6 @@
import pytest
import trio
-from trio.testing import Matcher, RaisesGroup
from .. import (
Event,
@@ -661,7 +660,9 @@ async def do_stuff() -> None:
nursery.cancel_scope.cancel()
# double wrap from our nursery + the internal nursery
- with RaisesGroup(RaisesGroup(Matcher(ValueError, "^foo$"))):
+ with pytest.RaisesGroup(
+ pytest.RaisesGroup(pytest.RaisesExc(ValueError, match="^foo$"))
+ ):
_core.run(do_stuff, strict_exception_groups=True)
@@ -698,7 +699,7 @@ async def test_warn_on_cancel_SIGKILL_escalation(
# the background_process_param exercises a lot of run_process cases, but it uses
# check=False, so lets have a test that uses check=True as well
async def test_run_process_background_fail() -> None:
- with RaisesGroup(subprocess.CalledProcessError):
+ with pytest.RaisesGroup(subprocess.CalledProcessError):
async with _core.open_nursery() as nursery:
value = await nursery.start(run_process, EXIT_FALSE)
assert isinstance(value, Process)
@@ -733,7 +734,7 @@ async def very_broken_open(*args: object, **kwargs: object) -> str:
return "oops"
monkeypatch.setattr(trio._subprocess, "_open_process", very_broken_open)
- with RaisesGroup(AttributeError, AttributeError):
+ with pytest.RaisesGroup(AttributeError, AttributeError):
await run_process(EXIT_TRUE, capture_stdout=True)
diff --git a/src/trio/_tests/test_sync.py b/src/trio/_tests/test_sync.py
index 0646898983..e4177307fe 100644
--- a/src/trio/_tests/test_sync.py
+++ b/src/trio/_tests/test_sync.py
@@ -7,8 +7,6 @@
import pytest
-from trio.testing import Matcher, RaisesGroup
-
from .. import _core
from .._core._parking_lot import GLOBAL_PARKING_LOT_BREAKER
from .._sync import *
@@ -696,8 +694,8 @@ async def test_lock_multiple_acquire() -> None:
see https://github.com/python-trio/trio/issues/3035"""
assert not GLOBAL_PARKING_LOT_BREAKER
lock = trio.Lock()
- with RaisesGroup(
- Matcher(
+ with pytest.RaisesGroup(
+ pytest.RaisesExc(
trio.BrokenResourceError,
match="^Owner of this lock exited without releasing: ",
),
diff --git a/src/trio/_tests/test_testing.py b/src/trio/_tests/test_testing.py
index e7c9a1a1a7..ca67b8bef7 100644
--- a/src/trio/_tests/test_testing.py
+++ b/src/trio/_tests/test_testing.py
@@ -6,8 +6,6 @@
import pytest
-from trio.testing import RaisesGroup
-
from .. import _core, sleep, socket as tsocket
from .._core._tests.tutil import can_bind_ipv6
from .._highlevel_generic import StapledStream, aclose_forcefully
@@ -292,7 +290,7 @@ async def getter(expect: bytes) -> None:
nursery.start_soon(putter, b"xyz")
# Two gets at the same time -> BusyResourceError
- with RaisesGroup(_core.BusyResourceError):
+ with pytest.RaisesGroup(_core.BusyResourceError):
async with _core.open_nursery() as nursery:
nursery.start_soon(getter, b"asdf")
nursery.start_soon(getter, b"asdf")
@@ -428,7 +426,7 @@ async def do_receive_some(max_bytes: int | None) -> bytes:
mrs.put_data(b"abc")
assert await do_receive_some(None) == b"abc"
- with RaisesGroup(_core.BusyResourceError):
+ with pytest.RaisesGroup(_core.BusyResourceError):
async with _core.open_nursery() as nursery:
nursery.start_soon(do_receive_some, 10)
nursery.start_soon(do_receive_some, 10)
diff --git a/src/trio/_tests/test_testing_raisesgroup.py b/src/trio/_tests/test_testing_raisesgroup.py
index 9cc9299382..ba453edbe8 100644
--- a/src/trio/_tests/test_testing_raisesgroup.py
+++ b/src/trio/_tests/test_testing_raisesgroup.py
@@ -7,7 +7,7 @@
import pytest
import trio
-from trio.testing import Matcher, RaisesGroup
+from trio.testing import _Matcher as Matcher, _RaisesGroup as RaisesGroup
from trio.testing._raises_group import repr_callable
if sys.version_info < (3, 11):
@@ -1107,8 +1107,22 @@ def test__ExceptionInfo(monkeypatch: pytest.MonkeyPatch) -> None:
"ExceptionInfo",
trio.testing._raises_group._ExceptionInfo,
)
- with trio.testing.RaisesGroup(ValueError) as excinfo:
+ with RaisesGroup(ValueError) as excinfo:
raise ExceptionGroup("", (ValueError("hello"),))
assert excinfo.type is ExceptionGroup
assert excinfo.value.exceptions[0].args == ("hello",)
assert isinstance(excinfo.tb, TracebackType)
+
+
+def test_raisesgroup_matcher_deprecation() -> None:
+ with pytest.deprecated_call():
+ trio.testing.Matcher # type: ignore # noqa: B018
+
+ with pytest.deprecated_call():
+ trio.testing.RaisesGroup # type: ignore # noqa: B018
+
+ with pytest.deprecated_call():
+ from trio.testing import Matcher # type: ignore # noqa: F401
+
+ with pytest.deprecated_call():
+ from trio.testing import RaisesGroup # type: ignore # noqa: F401
diff --git a/src/trio/_tests/test_unix_pipes.py b/src/trio/_tests/test_unix_pipes.py
index 4d1b08c06e..f81004049f 100644
--- a/src/trio/_tests/test_unix_pipes.py
+++ b/src/trio/_tests/test_unix_pipes.py
@@ -26,7 +26,7 @@
async def make_pipe() -> tuple[FdStream, FdStream]:
"""Makes a new pair of pipes."""
- (r, w) = os.pipe()
+ r, w = os.pipe()
return FdStream(w), FdStream(r)
diff --git a/src/trio/_tests/test_util.py b/src/trio/_tests/test_util.py
index 69310d64e7..c8beefa1ca 100644
--- a/src/trio/_tests/test_util.py
+++ b/src/trio/_tests/test_util.py
@@ -9,7 +9,7 @@
import pytest
import trio
-from trio.testing import Matcher, RaisesGroup
+from trio.testing import _Matcher as Matcher, _RaisesGroup as RaisesGroup
from .. import _core
from .._core._tests.tutil import (
diff --git a/src/trio/_tests/test_windows_pipes.py b/src/trio/_tests/test_windows_pipes.py
index e42736d65d..2aee568156 100644
--- a/src/trio/_tests/test_windows_pipes.py
+++ b/src/trio/_tests/test_windows_pipes.py
@@ -24,7 +24,7 @@
async def make_pipe() -> tuple[PipeSendStream, PipeReceiveStream]:
"""Makes a new pair of pipes."""
- (r, w) = pipe()
+ r, w = pipe()
return PipeSendStream(w), PipeReceiveStream(r)
diff --git a/src/trio/_tests/tools/test_sync_requirements.py b/src/trio/_tests/tools/test_sync_requirements.py
index db64d36eaa..b2acfdceb5 100644
--- a/src/trio/_tests/tools/test_sync_requirements.py
+++ b/src/trio/_tests/tools/test_sync_requirements.py
@@ -52,16 +52,13 @@ def test_update_requirements(
encoding="utf-8",
)
assert update_requirements(requirements_file, {"black": "3.1.5", "ruff": "1.2.7"})
- assert (
- requirements_file.read_text(encoding="utf-8")
- == """# comment
+ assert requirements_file.read_text(encoding="utf-8") == """# comment
# also comment but spaces line start
waffles are delicious no equals
black==3.1.5 ; specific version thingy
mypy==1.15.0
ruff==1.2.7
# required by soupy cat"""
- )
def test_update_requirements_no_changes(
diff --git a/src/trio/_tests/type_tests/raisesgroup.py b/src/trio/_tests/type_tests/raisesgroup.py
deleted file mode 100644
index 012c42b4d8..0000000000
--- a/src/trio/_tests/type_tests/raisesgroup.py
+++ /dev/null
@@ -1,223 +0,0 @@
-from __future__ import annotations
-
-import sys
-from collections.abc import Callable
-
-from trio.testing import Matcher, RaisesGroup
-from typing_extensions import assert_type
-
-if sys.version_info < (3, 11):
- from exceptiongroup import BaseExceptionGroup, ExceptionGroup
-
-# split into functions to isolate the different scopes
-
-
-def check_matcher_typevar_default(e: Matcher) -> None:
- assert e.exception_type is not None
- _exc: type[BaseException] = e.exception_type
- # this would previously pass, as the type would be `Any`
- e.exception_type().blah() # type: ignore
-
-
-def check_basic_contextmanager() -> None:
- with RaisesGroup(ValueError) as e:
- raise ExceptionGroup("foo", (ValueError(),))
- assert_type(e.value, ExceptionGroup[ValueError])
-
-
-def check_basic_matches() -> None:
- # check that matches gets rid of the naked ValueError in the union
- exc: ExceptionGroup[ValueError] | ValueError = ExceptionGroup("", (ValueError(),))
- if RaisesGroup(ValueError).matches(exc):
- assert_type(exc, ExceptionGroup[ValueError])
-
- # also check that BaseExceptionGroup shows up for BaseExceptions
- if RaisesGroup(KeyboardInterrupt).matches(exc):
- assert_type(exc, BaseExceptionGroup[KeyboardInterrupt])
-
-
-def check_matches_with_different_exception_type() -> None:
- e: BaseExceptionGroup[KeyboardInterrupt] = BaseExceptionGroup(
- "",
- (KeyboardInterrupt(),),
- )
-
- # note: it might be tempting to have this warn.
- # however, that isn't possible with current typing
- if RaisesGroup(ValueError).matches(e):
- assert_type(e, ExceptionGroup[ValueError])
-
-
-def check_matcher_init() -> None:
- def check_exc(exc: BaseException) -> bool:
- return isinstance(exc, ValueError)
-
- # Check various combinations of constructor signatures.
- # At least 1 arg must be provided.
- Matcher() # type: ignore
- Matcher(ValueError)
- Matcher(ValueError, "regex")
- Matcher(ValueError, "regex", check_exc)
- Matcher(exception_type=ValueError)
- Matcher(match="regex")
- Matcher(check=check_exc)
- Matcher(ValueError, match="regex")
- Matcher(match="regex", check=check_exc)
-
- def check_filenotfound(exc: FileNotFoundError) -> bool:
- return not exc.filename.endswith(".tmp")
-
- # If exception_type is provided, that narrows the `check` method's argument.
- Matcher(FileNotFoundError, check=check_filenotfound)
- Matcher(ValueError, check=check_filenotfound) # type: ignore
- Matcher(check=check_filenotfound) # type: ignore
- Matcher(FileNotFoundError, match="regex", check=check_filenotfound)
-
-
-def raisesgroup_check_type_narrowing() -> None:
- """Check type narrowing on the `check` argument to `RaisesGroup`.
- All `type: ignore`s are correctly pointing out type errors.
- """
-
- def handle_exc(e: BaseExceptionGroup[BaseException]) -> bool:
- return True
-
- def handle_kbi(e: BaseExceptionGroup[KeyboardInterrupt]) -> bool:
- return True
-
- def handle_value(e: BaseExceptionGroup[ValueError]) -> bool:
- return True
-
- RaisesGroup(BaseException, check=handle_exc)
- RaisesGroup(BaseException, check=handle_kbi) # type: ignore
-
- RaisesGroup(Exception, check=handle_exc)
- RaisesGroup(Exception, check=handle_value) # type: ignore
-
- RaisesGroup(KeyboardInterrupt, check=handle_exc)
- RaisesGroup(KeyboardInterrupt, check=handle_kbi)
- RaisesGroup(KeyboardInterrupt, check=handle_value) # type: ignore
-
- RaisesGroup(ValueError, check=handle_exc)
- RaisesGroup(ValueError, check=handle_kbi) # type: ignore
- RaisesGroup(ValueError, check=handle_value)
-
- RaisesGroup(ValueError, KeyboardInterrupt, check=handle_exc)
- RaisesGroup(ValueError, KeyboardInterrupt, check=handle_kbi) # type: ignore
- RaisesGroup(ValueError, KeyboardInterrupt, check=handle_value) # type: ignore
-
-
-def raisesgroup_narrow_baseexceptiongroup() -> None:
- """Check type narrowing specifically for the container exceptiongroup."""
-
- def handle_group(e: ExceptionGroup[Exception]) -> bool:
- return True
-
- def handle_group_value(e: ExceptionGroup[ValueError]) -> bool:
- return True
-
- RaisesGroup(ValueError, check=handle_group_value)
-
- RaisesGroup(Exception, check=handle_group)
-
-
-def check_matcher_transparent() -> None:
- with RaisesGroup(Matcher(ValueError)) as e:
- ...
- _: BaseExceptionGroup[ValueError] = e.value
- assert_type(e.value, ExceptionGroup[ValueError])
-
-
-def check_nested_raisesgroups_contextmanager() -> None:
- with RaisesGroup(RaisesGroup(ValueError)) as excinfo:
- raise ExceptionGroup("foo", (ValueError(),))
-
- _: BaseExceptionGroup[BaseExceptionGroup[ValueError]] = excinfo.value
-
- assert_type(
- excinfo.value,
- ExceptionGroup[ExceptionGroup[ValueError]],
- )
-
- assert_type(
- excinfo.value.exceptions[0],
- # this union is because of how typeshed defines .exceptions
- ExceptionGroup[ValueError] | ExceptionGroup[ExceptionGroup[ValueError]],
- )
-
-
-def check_nested_raisesgroups_matches() -> None:
- """Check nested RaisesGroups with .matches"""
- exc: ExceptionGroup[ExceptionGroup[ValueError]] = ExceptionGroup(
- "",
- (ExceptionGroup("", (ValueError(),)),),
- )
-
- if RaisesGroup(RaisesGroup(ValueError)).matches(exc):
- assert_type(exc, ExceptionGroup[ExceptionGroup[ValueError]])
-
-
-def check_multiple_exceptions_1() -> None:
- a = RaisesGroup(ValueError, ValueError)
- b = RaisesGroup(Matcher(ValueError), Matcher(ValueError))
- c = RaisesGroup(ValueError, Matcher(ValueError))
-
- d: RaisesGroup[ValueError]
- d = a
- d = b
- d = c
- assert isinstance(d, RaisesGroup)
-
-
-def check_multiple_exceptions_2() -> None:
- # This previously failed due to lack of covariance in the TypeVar
- a = RaisesGroup(Matcher(ValueError), Matcher(TypeError))
- b = RaisesGroup(Matcher(ValueError), TypeError)
- c = RaisesGroup(ValueError, TypeError)
-
- d: RaisesGroup[Exception]
- d = a
- d = b
- d = c
- assert isinstance(d, RaisesGroup)
-
-
-def check_raisesgroup_overloads() -> None:
- # allow_unwrapped=True does not allow:
- # multiple exceptions
- RaisesGroup(ValueError, TypeError, allow_unwrapped=True) # type: ignore
- # nested RaisesGroup
- RaisesGroup(RaisesGroup(ValueError), allow_unwrapped=True) # type: ignore
- # specifying match
- RaisesGroup(ValueError, match="foo", allow_unwrapped=True) # type: ignore
- # specifying check
- RaisesGroup(ValueError, check=bool, allow_unwrapped=True) # type: ignore
- # allowed variants
- RaisesGroup(ValueError, allow_unwrapped=True)
- RaisesGroup(ValueError, allow_unwrapped=True, flatten_subgroups=True)
- RaisesGroup(Matcher(ValueError), allow_unwrapped=True)
-
- # flatten_subgroups=True does not allow nested RaisesGroup
- RaisesGroup(RaisesGroup(ValueError), flatten_subgroups=True) # type: ignore
- # but rest is plenty fine
- RaisesGroup(ValueError, TypeError, flatten_subgroups=True)
- RaisesGroup(ValueError, match="foo", flatten_subgroups=True)
- RaisesGroup(ValueError, check=bool, flatten_subgroups=True)
- RaisesGroup(ValueError, flatten_subgroups=True)
- RaisesGroup(Matcher(ValueError), flatten_subgroups=True)
-
- # if they're both false we can of course specify nested raisesgroup
- RaisesGroup(RaisesGroup(ValueError))
-
-
-def check_triple_nested_raisesgroup() -> None:
- with RaisesGroup(RaisesGroup(RaisesGroup(ValueError))) as e:
- assert_type(e.value, ExceptionGroup[ExceptionGroup[ExceptionGroup[ValueError]]])
-
-
-def check_check_typing() -> None:
- # `BaseExceptiongroup` should perhaps be `ExceptionGroup`, but close enough
- assert_type(
- RaisesGroup(ValueError).check,
- Callable[[BaseExceptionGroup[ValueError]], bool] | None,
- )
diff --git a/src/trio/_tools/gen_exports.py b/src/trio/_tools/gen_exports.py
index e3d1659c02..d1b5deb844 100755
--- a/src/trio/_tools/gen_exports.py
+++ b/src/trio/_tools/gen_exports.py
@@ -3,6 +3,7 @@
Code generation script for class methods
to be exported as public API
"""
+
from __future__ import annotations
import argparse
@@ -347,7 +348,7 @@ def main() -> None: # pragma: no cover
from collections.abc import Awaitable, Callable
from typing import Any, TYPE_CHECKING
-from outcome import Outcome
+import outcome
import contextvars
from ._run import _NO_SEND, RunStatistics, Task
diff --git a/src/trio/_version.py b/src/trio/_version.py
index 9f941c270b..7a1cad22bd 100644
--- a/src/trio/_version.py
+++ b/src/trio/_version.py
@@ -1,3 +1,3 @@
# This file is imported from __init__.py and parsed by setuptools
-__version__ = "0.32.0"
+__version__ = "0.33.0"
diff --git a/src/trio/socket.py b/src/trio/socket.py
index cfcb9943c8..5375a1a675 100644
--- a/src/trio/socket.py
+++ b/src/trio/socket.py
@@ -26,9 +26,9 @@
# have:
globals().update(
{
- _name: getattr(_stdlib_socket, _name)
- for _name in _stdlib_socket.__all__
- if _name.isupper() and _name not in _bad_symbols
+ name: getattr(_stdlib_socket, name)
+ for name in _stdlib_socket.__all__
+ if name.isupper() and name not in _bad_symbols
},
)
diff --git a/src/trio/testing/__init__.py b/src/trio/testing/__init__.py
index d93d33aab7..a8f323447e 100644
--- a/src/trio/testing/__init__.py
+++ b/src/trio/testing/__init__.py
@@ -1,5 +1,6 @@
# Uses `from x import y as y` for compatibility with `pyright --verifytypes` (#2625)
+from .. import _deprecate as _deprecate
from .._core import (
MockClock as MockClock,
wait_all_tasks_blocked as wait_all_tasks_blocked,
@@ -28,12 +29,30 @@
memory_stream_pump as memory_stream_pump,
)
from ._network import open_stream_to_socket_listener as open_stream_to_socket_listener
-from ._raises_group import Matcher as Matcher, RaisesGroup as RaisesGroup
+from ._raises_group import Matcher as _Matcher, RaisesGroup as _RaisesGroup
from ._sequencer import Sequencer as Sequencer
from ._trio_test import trio_test as trio_test
################################################################
+_deprecate.deprecate_attributes(
+ __name__,
+ {
+ "RaisesGroup": _deprecate.DeprecatedAttribute(
+ _RaisesGroup,
+ version="0.33.0",
+ issue=3326,
+ instead="See https://docs.pytest.org/en/stable/reference/reference.html#pytest.RaisesGroup",
+ ),
+ "Matcher": _deprecate.DeprecatedAttribute(
+ _Matcher,
+ version="0.33.0",
+ issue=3326,
+ instead="See https://docs.pytest.org/en/stable/reference/reference.html#pytest.RaisesExc",
+ ),
+ },
+)
+
fixup_module_metadata(__name__, globals())
del fixup_module_metadata
diff --git a/test-requirements.in b/test-requirements.in
index b16a2b5d6e..272da6a34c 100644
--- a/test-requirements.in
+++ b/test-requirements.in
@@ -11,7 +11,7 @@ cryptography>=41.0.0 # cryptography<41 segfaults on pypy3.10
# Tools
black; implementation_name == "cpython"
-mypy
+mypy; implementation_name == "cpython"
ruff >= 0.8.0
astor # code generation
uv >= 0.2.24
diff --git a/test-requirements.txt b/test-requirements.txt
index 9717b1646c..6888340058 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -4,7 +4,7 @@ alabaster==1.0.0
# via sphinx
astor==0.8.1
# via -r test-requirements.in
-astroid==4.0.1
+astroid==4.0.4
# via pylint
async-generator==1.10
# via -r test-requirements.in
@@ -12,21 +12,21 @@ attrs==25.4.0
# via
# -r test-requirements.in
# outcome
-babel==2.17.0
+babel==2.18.0
# via sphinx
-black==25.9.0 ; implementation_name == 'cpython'
+black==26.1.0 ; implementation_name == 'cpython'
# via -r test-requirements.in
-certifi==2025.10.5
+certifi==2026.1.4
# via requests
cffi==2.0.0 ; os_name == 'nt' or platform_python_implementation != 'PyPy'
# via
# -r test-requirements.in
# cryptography
-cfgv==3.4.0
+cfgv==3.5.0
# via pre-commit
charset-normalizer==3.4.4
# via requests
-click==8.3.0 ; implementation_name == 'cpython'
+click==8.3.1 ; implementation_name == 'cpython'
# via black
codespell==2.4.1
# via -r test-requirements.in
@@ -36,27 +36,29 @@ colorama==0.4.6 ; sys_platform == 'win32'
# pylint
# pytest
# sphinx
-coverage==7.11.0
+coverage==7.13.4
# via -r test-requirements.in
-cryptography==46.0.3
+cryptography==46.0.5
# via
# -r test-requirements.in
# pyopenssl
# trustme
# types-pyopenssl
-dill==0.4.0
+dill==0.4.1
# via pylint
distlib==0.4.0
# via virtualenv
-docutils==0.21.2
+docutils==0.21.2 ; python_full_version < '3.11'
# via sphinx
-exceptiongroup==1.3.0 ; python_full_version < '3.11'
+docutils==0.22.4 ; python_full_version >= '3.11'
+ # via sphinx
+exceptiongroup==1.3.1 ; python_full_version < '3.11'
# via
# -r test-requirements.in
# pytest
-filelock==3.20.0
+filelock==3.24.0
# via virtualenv
-identify==2.6.15
+identify==2.6.16
# via pre-commit
idna==3.11
# via
@@ -73,66 +75,68 @@ jedi==0.19.2 ; implementation_name == 'cpython'
# via -r test-requirements.in
jinja2==3.1.6
# via sphinx
+librt==0.8.0 ; implementation_name == 'cpython' and platform_python_implementation != 'PyPy'
+ # via mypy
markupsafe==3.0.3
# via jinja2
mccabe==0.7.0
# via pylint
-mypy==1.18.2
+mypy==1.19.1 ; implementation_name == 'cpython'
# via -r test-requirements.in
mypy-extensions==1.1.0
# via
# -r test-requirements.in
# black
# mypy
-nodeenv==1.9.1
+nodeenv==1.10.0
# via
# pre-commit
# pyright
outcome==1.3.0.post0
# via -r test-requirements.in
-packaging==25.0
+packaging==26.0
# via
# black
# pytest
# sphinx
-parso==0.8.5 ; implementation_name == 'cpython'
+parso==0.8.6 ; implementation_name == 'cpython'
# via jedi
-pathspec==0.12.1
+pathspec==1.0.4 ; implementation_name == 'cpython'
# via
# black
# mypy
-platformdirs==4.5.0
+platformdirs==4.9.0
# via
# black
# pylint
# virtualenv
pluggy==1.6.0
# via pytest
-pre-commit==4.3.0
+pre-commit==4.5.1
# via -r test-requirements.in
-pycparser==2.23 ; (implementation_name != 'PyPy' and os_name == 'nt') or (implementation_name != 'PyPy' and platform_python_implementation != 'PyPy')
+pycparser==3.0 ; (implementation_name != 'PyPy' and os_name == 'nt') or (implementation_name != 'PyPy' and platform_python_implementation != 'PyPy')
# via cffi
pygments==2.19.2
# via
# pytest
# sphinx
-pylint==4.0.2
+pylint==4.0.4
# via -r test-requirements.in
pyopenssl==25.3.0
# via -r test-requirements.in
-pyright==1.1.406
+pyright==1.1.408
# via -r test-requirements.in
-pytest==8.4.2
+pytest==9.0.2
# via -r test-requirements.in
-pytokens==0.2.0 ; implementation_name == 'cpython'
+pytokens==0.4.1 ; implementation_name == 'cpython'
# via black
pyyaml==6.0.3
# via pre-commit
requests==2.32.5
# via sphinx
-roman-numerals-py==3.1.0 ; python_full_version >= '3.11'
+roman-numerals==4.1.0 ; python_full_version >= '3.11'
# via sphinx
-ruff==0.14.2
+ruff==0.15.1
# via -r test-requirements.in
sniffio==1.3.1
# via -r test-requirements.in
@@ -142,7 +146,9 @@ sortedcontainers==2.4.0
# via -r test-requirements.in
sphinx==8.1.3 ; python_full_version < '3.11'
# via -r test-requirements.in
-sphinx==8.2.3 ; python_full_version >= '3.11'
+sphinx==9.0.4 ; python_full_version == '3.11.*'
+ # via -r test-requirements.in
+sphinx==9.1.0 ; python_full_version >= '3.12'
# via -r test-requirements.in
sphinxcontrib-applehelp==2.0.0
# via sphinx
@@ -156,14 +162,14 @@ sphinxcontrib-qthelp==2.0.0
# via sphinx
sphinxcontrib-serializinghtml==2.0.0
# via sphinx
-tomli==2.3.0 ; python_full_version < '3.11'
+tomli==2.4.0 ; python_full_version < '3.11'
# via
# black
# mypy
# pylint
# pytest
# sphinx
-tomlkit==0.13.3
+tomlkit==0.14.0
# via pylint
trustme==1.2.1
# via -r test-requirements.in
@@ -171,13 +177,13 @@ types-cffi==1.17.0.20250915
# via
# -r test-requirements.in
# types-pyopenssl
-types-docutils==0.22.2.20251006
+types-docutils==0.22.3.20251115
# via -r test-requirements.in
types-pyopenssl==24.1.0.20240722
# via -r test-requirements.in
types-pyyaml==6.0.12.20250915
# via -r test-requirements.in
-types-setuptools==80.9.0.20250822
+types-setuptools==82.0.0.20260210
# via types-cffi
typing-extensions==4.15.0
# via
@@ -190,9 +196,9 @@ typing-extensions==4.15.0
# pyopenssl
# pyright
# virtualenv
-urllib3==2.5.0
+urllib3==2.6.3
# via requests
-uv==0.9.5
+uv==0.10.2
# via -r test-requirements.in
-virtualenv==20.35.3
+virtualenv==20.36.1
# via pre-commit
diff --git a/tox.ini b/tox.ini
index 6b1851f4de..11bd09d0ca 100644
--- a/tox.ini
+++ b/tox.ini
@@ -97,13 +97,12 @@ base_python = 3.13
commands =
pre-commit run pip-compile --all-files
-# TODO: allow specifying e.g. typing-3.11 to run with --python[-]version=3.11
[testenv:typing]
description = "Run type checks: mypy on all platforms, and pyright on `src/trio[/_core]/_tests/type_tests/`."
deps =
-r test-requirements.txt
exceptiongroup
-base_python = 3.13
+base_python = 3.10
set_env =
PYRIGHT_PYTHON_IGNORE_WARNINGS=1
commands =