From bef8a1f477a430d5f097c9e48386152d81bb1511 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Thu, 27 Mar 2025 23:24:33 -0500 Subject: [PATCH 001/111] Add tool to keep test-requirements in sync with pre-commit --- .pre-commit-config.yaml | 7 ++ src/trio/_tools/sync_requirements.py | 102 +++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100755 src/trio/_tools/sync_requirements.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 127130cc05..57c050cf25 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -58,6 +58,13 @@ repos: pass_filenames: false additional_dependencies: ["astor", "attrs", "black", "ruff"] files: ^src\/trio\/_core\/(_run|(_i(o_(common|epoll|kqueue|windows)|nstrumentation)))\.py$ + - id: sync-test-requirements + name: synchronize test requirements + language: python + entry: python src/trio/_tools/sync_requirements.py + pass_filenames: false + additional_dependencies: ["pyyaml"] + files: ^(test-requirements\.txt)|(\.pre-commit-config\.yaml)$ - repo: https://github.com/astral-sh/uv-pre-commit rev: 0.6.6 hooks: diff --git a/src/trio/_tools/sync_requirements.py b/src/trio/_tools/sync_requirements.py new file mode 100755 index 0000000000..db409bd007 --- /dev/null +++ b/src/trio/_tools/sync_requirements.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 + +"""Sync Requirements - Automatically upgrade test requirements pinned +versions from pre-commit config file.""" + +from __future__ import annotations + +import sys +from pathlib import Path +from typing import TYPE_CHECKING + +from yaml import load as load_yaml + +if TYPE_CHECKING: + from collections.abc import Generator + + from yaml import CLoader as _CLoader, Loader as _Loader + + Loader: type[_CLoader | _Loader] + +try: + from yaml import CLoader as Loader +except ImportError: + from yaml import Loader + + +def yield_pre_commit_version_data( + pre_commit: Path, +) -> Generator[tuple[str, str], None, None]: + """Yield (name, rev) tuples from pre-commit config file.""" + pre_commit_config = load_yaml(pre_commit.read_text(encoding="utf-8"), Loader) + for repo in pre_commit_config["repos"]: + if "repo" not in repo or "rev" not in repo: + continue + url = repo["repo"] + name = url.rsplit("/", 1)[-1] + rev = repo["rev"].removeprefix("v") + yield name, rev + + +def update_requirements( + requirements: Path, + version_data: dict[str, str], +) -> bool: + """Return if updated requirements file. + + Update requirements file to match versions in version_data.""" + changed = False + old_lines = requirements.read_text(encoding="utf-8").splitlines(True) + + with requirements.open("w", encoding="utf-8") as file: + for line in old_lines: + # If comment or not version mark line, ignore. + if line.startswith("#") or "==" not in line: + file.write(line) + continue + name, rest = line.split("==", 1) + # Maintain extra markers if they exist + old_version = rest.strip() + extra = "\n" + if " " in rest: + old_version, extra = rest.split(" ", 1) + extra = " " + extra + version = version_data.get(name) + # If does not exist, skip + if version is None: + file.write(line) + continue + # Otherwise might have changed + new_line = f"{name}=={version}{extra}" + if new_line != line: + if not changed: + changed = True + print("Changed test requirements version to match pre-commit") + print(f"{name}=={old_version} -> {name}=={version}") + file.write(new_line) + return changed + + +def main() -> int: + """Run program.""" + + source_root = Path.cwd().absolute() + while not (source_root / "LICENSE").exists(): + source_root = source_root.parent + # Double-check we found the right directory + assert (source_root / "LICENSE").exists() + pre_commit = source_root / ".pre-commit-config.yaml" + test_requirements = source_root / "test-requirements.txt" + + # Get tool versions from pre-commit + # Get correct names + pre_commit_versions = { + name.removesuffix("-mirror").removesuffix("-pre-commit"): version + for name, version in yield_pre_commit_version_data(pre_commit) + } + changed = update_requirements(test_requirements, pre_commit_versions) + return int(changed) + + +if __name__ == "__main__": + sys.exit(main()) From 76b8dbebb4ad85c9b06d645dcd572e6a46579b2a Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Fri, 28 Mar 2025 00:10:17 -0500 Subject: [PATCH 002/111] Add tests for sync tool --- .../_tests/tools/test_sync_requirements.py | 82 +++++++++++++++++++ src/trio/_tools/sync_requirements.py | 11 +-- 2 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 src/trio/_tests/tools/test_sync_requirements.py diff --git a/src/trio/_tests/tools/test_sync_requirements.py b/src/trio/_tests/tools/test_sync_requirements.py new file mode 100644 index 0000000000..83ae21be65 --- /dev/null +++ b/src/trio/_tests/tools/test_sync_requirements.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING + +from trio._tests.pytest_plugin import skip_if_optional_else_raise + +# imports in gen_exports that are not in `install_requires` in requirements +try: + import yaml # noqa: F401 +except ImportError as error: + skip_if_optional_else_raise(error) + +from trio._tools.sync_requirements import ( + update_requirements, + yield_pre_commit_version_data, +) + +if TYPE_CHECKING: + from pathlib import Path + + +def test_yield_pre_commit_version_data() -> None: + text = """ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.0 + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 25.1.0 +""" + results = tuple(yield_pre_commit_version_data(text)) + assert results == ( + ("ruff-pre-commit", "0.11.0"), + ("black-pre-commit-mirror", "25.1.0"), + ) + + +def test_update_requirements( + tmp_path: Path, +) -> None: + requirements_file = tmp_path / "requirements.txt" + assert not requirements_file.exists() + requirements_file.write_text( + """# comment + # also comment but spaces line start +waffles are delicious no equals +black==3.1.4 ; specific version thingy +mypy==1.15.0 +ruff==1.2.5 +# required by soupy cat""", + 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 + # 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( + tmp_path: Path, +) -> None: + requirements_file = tmp_path / "requirements.txt" + assert not requirements_file.exists() + original = """# comment + # also comment but spaces line start +waffles are delicious no equals +black==3.1.4 ; specific version thingy +mypy==1.15.0 +ruff==1.2.5 +# required by soupy cat""" + requirements_file.write_text(original, encoding="utf-8") + assert not update_requirements( + requirements_file, {"black": "3.1.4", "ruff": "1.2.5"} + ) + assert requirements_file.read_text(encoding="utf-8") == original diff --git a/src/trio/_tools/sync_requirements.py b/src/trio/_tools/sync_requirements.py index db409bd007..3ade5b941c 100755 --- a/src/trio/_tools/sync_requirements.py +++ b/src/trio/_tools/sync_requirements.py @@ -25,10 +25,10 @@ def yield_pre_commit_version_data( - pre_commit: Path, + pre_commit_text: str, ) -> Generator[tuple[str, str], None, None]: """Yield (name, rev) tuples from pre-commit config file.""" - pre_commit_config = load_yaml(pre_commit.read_text(encoding="utf-8"), Loader) + pre_commit_config = load_yaml(pre_commit_text, Loader) for repo in pre_commit_config["repos"]: if "repo" not in repo or "rev" not in repo: continue @@ -81,18 +81,19 @@ def main() -> int: """Run program.""" source_root = Path.cwd().absolute() - while not (source_root / "LICENSE").exists(): - source_root = source_root.parent + # Double-check we found the right directory assert (source_root / "LICENSE").exists() pre_commit = source_root / ".pre-commit-config.yaml" test_requirements = source_root / "test-requirements.txt" + pre_commit_text = pre_commit.read_text(encoding="utf-8") + # Get tool versions from pre-commit # Get correct names pre_commit_versions = { name.removesuffix("-mirror").removesuffix("-pre-commit"): version - for name, version in yield_pre_commit_version_data(pre_commit) + for name, version in yield_pre_commit_version_data(pre_commit_text) } changed = update_requirements(test_requirements, pre_commit_versions) return int(changed) From 3d685fab924fcc30b35b68b58709ce4e07a7e708 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Fri, 28 Mar 2025 00:17:02 -0500 Subject: [PATCH 003/111] Fix mypy types not installed issue --- test-requirements.in | 2 ++ test-requirements.txt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/test-requirements.in b/test-requirements.in index bf64c568da..fd16b2d3bc 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -27,6 +27,8 @@ types-pyOpenSSL # annotations in doc files types-docutils sphinx +# sync-requirements +types-PyYAML # Trio's own dependencies cffi; os_name == "nt" diff --git a/test-requirements.txt b/test-requirements.txt index db84cccc90..04eb41fcf5 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -177,6 +177,8 @@ types-docutils==0.21.0.20241128 # via -r test-requirements.in types-pyopenssl==24.1.0.20240722 # via -r test-requirements.in +types-pyyaml==6.0.12.20250326 + # via -r test-requirements.in types-setuptools==75.8.0.20250225 # via types-cffi typing-extensions==4.12.2 From 7cdc087f06c656e234a3b147ac55691f72650263 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Tue, 1 Apr 2025 11:06:21 -0500 Subject: [PATCH 004/111] Split on `;` --- src/trio/_tools/sync_requirements.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/trio/_tools/sync_requirements.py b/src/trio/_tools/sync_requirements.py index 3ade5b941c..f4ca481faf 100755 --- a/src/trio/_tools/sync_requirements.py +++ b/src/trio/_tools/sync_requirements.py @@ -58,9 +58,10 @@ def update_requirements( # Maintain extra markers if they exist old_version = rest.strip() extra = "\n" - if " " in rest: - old_version, extra = rest.split(" ", 1) - extra = " " + extra + if ";" in rest: + old_version, extra = rest.split(";", 1) + old_version = old_version.strip() + extra = " ;" + extra version = version_data.get(name) # If does not exist, skip if version is None: From ebd1102b86596c481301f1408efe5768f6daca07 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Mon, 21 Apr 2025 09:48:33 +0900 Subject: [PATCH 005/111] Start new cycle --- src/trio/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/trio/_version.py b/src/trio/_version.py index 868a54eef5..87bdae021d 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.30.0" +__version__ = "0.30.0+dev" From d65d3f61ca83b0481e8d105d182c91e335e6ae29 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Mon, 21 Apr 2025 08:14:08 -0400 Subject: [PATCH 006/111] Allow pickling `Cancelled` (#3250) * Allow pickling `Cancelled` * Add newsfragment * Use `collections.abc.Callable` not `typing.Callable` * Fix coverage * Generalize __reduce__ * Address PR review Co-authored-by: jakkdl * Add a test for coverage reasons * Revert generalization It's too much complexity to do it correctly. * Ensure that we get any extra cases that need pickling * Remove now-unnecessary new `__new__` --------- Co-authored-by: jakkdl --- newsfragments/3248.bugfix.rst | 1 + src/trio/_core/_exceptions.py | 10 ++++++++++ src/trio/_core/_tests/test_run.py | 8 ++++++++ 3 files changed, 19 insertions(+) create mode 100644 newsfragments/3248.bugfix.rst diff --git a/newsfragments/3248.bugfix.rst b/newsfragments/3248.bugfix.rst new file mode 100644 index 0000000000..69d7a9859a --- /dev/null +++ b/newsfragments/3248.bugfix.rst @@ -0,0 +1 @@ +Allow pickling `trio.Cancelled`, as they can show up when you want to pickle something else. This does not rule out pickling other ``NoPublicConstructor`` objects -- create an issue if necessary. diff --git a/src/trio/_core/_exceptions.py b/src/trio/_core/_exceptions.py index 4996c18f15..4a76a674ac 100644 --- a/src/trio/_core/_exceptions.py +++ b/src/trio/_core/_exceptions.py @@ -1,5 +1,12 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + from trio._util import NoPublicConstructor, final +if TYPE_CHECKING: + from collections.abc import Callable + class TrioInternalError(Exception): """Raised by :func:`run` if we encounter a bug in Trio, or (possibly) a @@ -63,6 +70,9 @@ class Cancelled(BaseException, metaclass=NoPublicConstructor): def __str__(self) -> str: return "Cancelled" + def __reduce__(self) -> tuple[Callable[[], Cancelled], tuple[()]]: + return (Cancelled._create, ()) + class BusyResourceError(Exception): """Raised when a task attempts to use a resource that some other task is diff --git a/src/trio/_core/_tests/test_run.py b/src/trio/_core/_tests/test_run.py index 7728a6f3d4..c02d185d45 100644 --- a/src/trio/_core/_tests/test_run.py +++ b/src/trio/_core/_tests/test_run.py @@ -3,6 +3,7 @@ import contextvars import functools import gc +import pickle import sys import threading import time @@ -2235,6 +2236,13 @@ def test_Cancelled_subclass() -> None: type("Subclass", (_core.Cancelled,), {}) +# https://github.com/python-trio/trio/issues/3248 +def test_Cancelled_pickle() -> None: + cancelled = _core.Cancelled._create() + cancelled = pickle.loads(pickle.dumps(cancelled)) + assert isinstance(cancelled, _core.Cancelled) + + def test_CancelScope_subclass() -> None: with pytest.raises(TypeError): type("Subclass", (_core.CancelScope,), {}) From b633fdadb7e4795d3491b5e6f98e1eb538bb5ce4 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 21 Apr 2025 22:26:24 -0500 Subject: [PATCH 007/111] Switch to https://github.com/adhtruong/mirrors-typos See https://github.com/crate-ci/typos/issues/390 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0d1a3781a..2478ca80d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,7 +37,7 @@ repos: additional_dependencies: # tomli needed on 3.10. tomllib is available in stdlib on 3.11+ - tomli - - repo: https://github.com/crate-ci/typos + - repo: https://github.com/adhtruong/mirrors-typos rev: v1.31.1 hooks: - id: typos From a853e72cc4f8be39e83d30f41064fd62e353a2ae Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 20:29:01 +0000 Subject: [PATCH 008/111] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.4 → v0.11.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.4...v0.11.7) - [github.com/woodruffw/zizmor-pre-commit: v1.5.2 → v1.6.0](https://github.com/woodruffw/zizmor-pre-commit/compare/v1.5.2...v1.6.0) - [github.com/astral-sh/uv-pre-commit: 0.6.13 → 0.6.17](https://github.com/astral-sh/uv-pre-commit/compare/0.6.13...0.6.17) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2478ca80d8..b8d0270281 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.4 + rev: v0.11.7 hooks: - id: ruff types: [file] @@ -46,7 +46,7 @@ repos: hooks: - id: sphinx-lint - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.5.2 + rev: v1.6.0 hooks: - id: zizmor - repo: local @@ -59,7 +59,7 @@ repos: additional_dependencies: ["astor", "attrs", "black", "ruff"] files: ^src\/trio\/_core\/(_run|(_i(o_(common|epoll|kqueue|windows)|nstrumentation)))\.py$ - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.6.13 + rev: 0.6.17 hooks: # Compile requirements - id: pip-compile From 2319e091611fde0e242665b89f9b70c86e58e48f Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 29 Apr 2025 09:14:51 -0400 Subject: [PATCH 009/111] Pin deps and bypass zizmor --- test-requirements.txt | 4 ++-- zizmor.yml | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 zizmor.yml diff --git a/test-requirements.txt b/test-requirements.txt index 37dc7f278b..2cbe7d4b8c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -132,7 +132,7 @@ requests==2.32.3 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.11.4 +ruff==0.11.7 # via -r test-requirements.in setuptools==78.1.0 # via types-setuptools @@ -192,7 +192,7 @@ typing-extensions==4.13.0 # pyright urllib3==2.3.0 # via requests -uv==0.6.13 +uv==0.6.17 # via -r test-requirements.in virtualenv==20.30.0 # via pre-commit diff --git a/zizmor.yml b/zizmor.yml new file mode 100644 index 0000000000..49673bd825 --- /dev/null +++ b/zizmor.yml @@ -0,0 +1,6 @@ +rules: + unpinned-uses: + config: + policies: + # TODO: use the default policies + "*": any \ No newline at end of file From 1671eab733d9702913580b149809de2fee9d1eaf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 13:15:05 +0000 Subject: [PATCH 010/111] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- zizmor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zizmor.yml b/zizmor.yml index 49673bd825..b1431987af 100644 --- a/zizmor.yml +++ b/zizmor.yml @@ -3,4 +3,4 @@ rules: config: policies: # TODO: use the default policies - "*": any \ No newline at end of file + "*": any From ebae2b629e313ecbf3bafa7eb71e6713426a56e8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 00:55:09 +0000 Subject: [PATCH 011/111] Dependency updates --- .pre-commit-config.yaml | 4 ++-- docs-requirements.txt | 12 ++++++------ test-requirements.txt | 26 +++++++++++++------------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b8d0270281..276fa3539e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: # tomli needed on 3.10. tomllib is available in stdlib on 3.11+ - tomli - repo: https://github.com/adhtruong/mirrors-typos - rev: v1.31.1 + rev: v1.31.2 hooks: - id: typos - repo: https://github.com/sphinx-contrib/sphinx-lint @@ -59,7 +59,7 @@ repos: additional_dependencies: ["astor", "attrs", "black", "ruff"] files: ^src\/trio\/_core\/(_run|(_i(o_(common|epoll|kqueue|windows)|nstrumentation)))\.py$ - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.6.17 + rev: 0.7.2 hooks: # Compile requirements - id: pip-compile diff --git a/docs-requirements.txt b/docs-requirements.txt index 1897984332..b385b91616 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -8,9 +8,9 @@ attrs==25.3.0 # outcome babel==2.17.0 # via sphinx -beautifulsoup4==4.13.3 +beautifulsoup4==4.13.4 # via sphinx-codeautolink -certifi==2025.1.31 +certifi==2025.4.26 # via requests cffi==1.17.1 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via @@ -49,7 +49,7 @@ markupsafe==3.0.2 # via jinja2 outcome==1.3.0.post0 # via -r docs-requirements.in -packaging==24.2 +packaging==25.0 # via sphinx pycparser==2.22 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via cffi @@ -67,7 +67,7 @@ snowballstemmer==2.2.0 # via sphinx sortedcontainers==2.4.0 # via -r docs-requirements.in -soupsieve==2.6 +soupsieve==2.7 # via beautifulsoup4 sphinx==8.2.3 # via @@ -100,9 +100,9 @@ sphinxcontrib-trio==1.1.2 # via -r docs-requirements.in towncrier==24.8.0 # via -r docs-requirements.in -typing-extensions==4.13.0 +typing-extensions==4.13.2 # via # beautifulsoup4 # pyopenssl -urllib3==2.3.0 +urllib3==2.4.0 # via requests diff --git a/test-requirements.txt b/test-requirements.txt index 2cbe7d4b8c..110f078f2b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -18,7 +18,7 @@ babel==2.17.0 # via sphinx black==25.1.0 ; implementation_name == 'cpython' # via -r test-requirements.in -certifi==2025.1.31 +certifi==2025.4.26 # via requests cffi==1.17.1 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via @@ -46,7 +46,7 @@ cryptography==44.0.2 # pyopenssl # trustme # types-pyopenssl -dill==0.3.9 +dill==0.4.0 # via pylint distlib==0.3.9 # via virtualenv @@ -58,7 +58,7 @@ exceptiongroup==1.2.2 ; python_full_version < '3.11' # pytest filelock==3.18.0 # via virtualenv -identify==2.6.9 +identify==2.6.10 # via pre-commit idna==3.10 # via @@ -67,7 +67,7 @@ idna==3.10 # trustme imagesize==1.4.1 # via sphinx -importlib-metadata==8.6.1 ; python_full_version < '3.10' +importlib-metadata==8.7.0 ; python_full_version < '3.10' # via sphinx iniconfig==2.1.0 # via pytest @@ -83,7 +83,7 @@ mccabe==0.7.0 # via pylint mypy==1.15.0 # via -r test-requirements.in -mypy-extensions==1.0.0 +mypy-extensions==1.1.0 # via # -r test-requirements.in # black @@ -92,11 +92,11 @@ nodeenv==1.9.1 # via # pre-commit # pyright -orjson==3.10.16 ; python_full_version < '3.14' and implementation_name == 'cpython' +orjson==3.10.18 ; python_full_version < '3.14' and implementation_name == 'cpython' # via -r test-requirements.in outcome==1.3.0.post0 # via -r test-requirements.in -packaging==24.2 +packaging==25.0 # via # black # pytest @@ -122,7 +122,7 @@ pylint==3.3.6 # via -r test-requirements.in pyopenssl==25.0.0 # via -r test-requirements.in -pyright==1.1.398 +pyright==1.1.400 # via -r test-requirements.in pytest==8.3.5 # via -r test-requirements.in @@ -134,7 +134,7 @@ roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx ruff==0.11.7 # via -r test-requirements.in -setuptools==78.1.0 +setuptools==80.1.0 # via types-setuptools sniffio==1.3.1 # via -r test-requirements.in @@ -179,9 +179,9 @@ types-docutils==0.21.0.20241128 # via -r test-requirements.in types-pyopenssl==24.1.0.20240722 # via -r test-requirements.in -types-setuptools==78.1.0.20250329 +types-setuptools==80.0.0.20250429 # via types-cffi -typing-extensions==4.13.0 +typing-extensions==4.13.2 # via # -r test-requirements.in # astroid @@ -190,9 +190,9 @@ typing-extensions==4.13.0 # pylint # pyopenssl # pyright -urllib3==2.3.0 +urllib3==2.4.0 # via requests -uv==0.6.17 +uv==0.7.2 # via -r test-requirements.in virtualenv==20.30.0 # via pre-commit From 8533ea7dacbf072760af49e80a4d9e3460cce949 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 20:46:25 +0000 Subject: [PATCH 012/111] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.7 → v0.11.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.7...v0.11.8) - [github.com/adhtruong/mirrors-typos: v1.31.2 → v1.32.0](https://github.com/adhtruong/mirrors-typos/compare/v1.31.2...v1.32.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 276fa3539e..91b607f76c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.7 + rev: v0.11.8 hooks: - id: ruff types: [file] @@ -38,7 +38,7 @@ repos: # tomli needed on 3.10. tomllib is available in stdlib on 3.11+ - tomli - repo: https://github.com/adhtruong/mirrors-typos - rev: v1.31.2 + rev: v1.32.0 hooks: - id: typos - repo: https://github.com/sphinx-contrib/sphinx-lint From 585f2ad1860a1c02203c765c85a6d140d747624c Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 6 May 2025 23:46:38 -0400 Subject: [PATCH 013/111] Update pins --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 110f078f2b..6ab3933d0b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -132,7 +132,7 @@ requests==2.32.3 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.11.7 +ruff==0.11.8 # via -r test-requirements.in setuptools==80.1.0 # via types-setuptools From bb63c53681cd27569ecef99201ba38517d8527b0 Mon Sep 17 00:00:00 2001 From: John Litborn <11260241+jakkdl@users.noreply.github.com> Date: Thu, 15 May 2025 01:01:37 +0200 Subject: [PATCH 014/111] Unbreak CI (3.14 + cython) (#3264) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * skip static introspection on 3.14.0b1 * also skip sees_all_symbols, pin cython to <3.1.0 * name 3.14 with -dev to pass if failing. Also restrict cython version in ci * continue-on-error for windows3.14, add non-strict xfail for test_error_in_run_loop, add overeager strict xfail on export tests * switch back to skipif ... maybe unbreak CI? * unbreak CI * summer is finally here ☀ --- .github/workflows/ci.yml | 9 +++++++-- src/trio/_core/_tests/test_run.py | 6 ++++++ src/trio/_tests/test_exports.py | 10 ++++++++++ tox.ini | 3 ++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15cfcbaa64..562495a4a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -181,11 +181,14 @@ jobs: # lsp: 'http://download.pctools.com/mirror/updates/9.0.0.2308-SDavfree-lite_en.exe' # lsp_extract_file: '' # extra_name: ', with non-IFS LSP' + + # ***REMEMBER*** to remove the 3.14 line once windows+cffi works again continue-on-error: >- ${{ ( endsWith(matrix.python, '-dev') || endsWith(matrix.python, '-nightly') + || matrix.python == '3.14' ) && true || false @@ -387,11 +390,13 @@ jobs: - python: '3.9' # We support running on cython 2 and 3 for 3.9 cython: '<3' # cython 2 - python: '3.9' - cython: '>=3' # cython 3 (or greater) + # cython 3.1.0 broke stuff https://github.com/cython/cython/issues/6865 + cython: '>=3,<3.1' # cython 3 (or greater) - python: '3.11' # 3.11 is the last version Cy2 supports cython: '<3' # cython 2 - python: '3.13' # We support running cython3 on 3.13 - cython: '>=3' # cython 3 (or greater) + # cython 3.1.0 broke stuff https://github.com/cython/cython/issues/6865 + cython: '>=3,<3.1' # cython 3 (or greater) steps: - name: Retrieve the project source from an sdist inside the GHA artifact uses: re-actors/checkout-python-sdist@release/v2 diff --git a/src/trio/_core/_tests/test_run.py b/src/trio/_core/_tests/test_run.py index c02d185d45..f3e53ace38 100644 --- a/src/trio/_core/_tests/test_run.py +++ b/src/trio/_core/_tests/test_run.py @@ -922,7 +922,13 @@ async def main() -> None: gc_collect_harder() +# This segfaults, so we need to skipif. Remember to remove the skipif once +# the upstream issue is resolved. @restore_unraisablehook() +@pytest.mark.skipif( + sys.version_info[:3] == (3, 14, 0), + reason="https://github.com/python/cpython/issues/133932", +) def test_error_in_run_loop() -> None: # Blow stuff up real good to check we at least get a TrioInternalError async def main() -> None: diff --git a/src/trio/_tests/test_exports.py b/src/trio/_tests/test_exports.py index 0bf619e3b8..1af78513d7 100644 --- a/src/trio/_tests/test_exports.py +++ b/src/trio/_tests/test_exports.py @@ -117,6 +117,11 @@ def iter_modules( # won't be reflected in trio.socket, and this shouldn't cause downstream test # runs to start failing. @pytest.mark.redistributors_should_skip +@pytest.mark.skipif( + sys.version_info[:4] == (3, 14, 0, "beta"), + # 12 pass, 16 fail + reason="several tools don't support 3.14", +) # Static analysis tools often have trouble with alpha releases, where Python's # internals are in flux, grammar may not have settled down, etc. @pytest.mark.skipif( @@ -243,6 +248,11 @@ def no_underscores(symbols: Iterable[str]) -> set[str]: @slow # see comment on test_static_tool_sees_all_symbols @pytest.mark.redistributors_should_skip +@pytest.mark.skipif( + sys.version_info[:4] == (3, 14, 0, "beta"), + # 2 passes, 12 fails + reason="several tools don't support 3.14.0", +) # Static analysis tools often have trouble with alpha releases, where Python's # internals are in flux, grammar may not have settled down, etc. @pytest.mark.skipif( diff --git a/tox.ini b/tox.ini index 6eecf7b78a..39d4dece5d 100644 --- a/tox.ini +++ b/tox.ini @@ -52,7 +52,8 @@ commands = [testenv:py39-cython2,py39-cython,py311-cython2,py313-cython] description = "Run cython tests." deps = - cython + # cython 3.1.0 broke stuff https://github.com/cython/cython/issues/6865 + cython: cython<3.1.0 cython2: cython<3 setuptools ; python_version >= '3.12' commands_pre = From 23b2d318622a1fb3513de574bd3a259cf4b0fdec Mon Sep 17 00:00:00 2001 From: John Litborn <11260241+jakkdl@users.noreply.github.com> Date: Thu, 15 May 2025 12:58:26 +0200 Subject: [PATCH 015/111] Add message with debugging info to Cancelled (#3256) --------- Co-authored-by: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Co-authored-by: A5Rocks --- newsfragments/3232.feature.rst | 1 + src/trio/_channel.py | 4 +- src/trio/_core/_asyncgens.py | 4 +- src/trio/_core/_exceptions.py | 52 +++++- src/trio/_core/_run.py | 143 +++++++++++++-- src/trio/_core/_tests/test_cancelled.py | 222 ++++++++++++++++++++++++ src/trio/_core/_tests/test_run.py | 34 +--- src/trio/_highlevel_generic.py | 2 +- src/trio/_highlevel_open_tcp_stream.py | 2 +- src/trio/_subprocess.py | 2 +- src/trio/_tests/test_threads.py | 113 +++++++----- src/trio/_tests/test_util.py | 8 +- src/trio/_threads.py | 2 + src/trio/_util.py | 3 +- 14 files changed, 489 insertions(+), 103 deletions(-) create mode 100644 newsfragments/3232.feature.rst create mode 100644 src/trio/_core/_tests/test_cancelled.py diff --git a/newsfragments/3232.feature.rst b/newsfragments/3232.feature.rst new file mode 100644 index 0000000000..9da76cb370 --- /dev/null +++ b/newsfragments/3232.feature.rst @@ -0,0 +1 @@ +:exc:`Cancelled` strings can now display the source and reason for a cancellation. Trio-internal sources of cancellation will set this string, and :meth:`CancelScope.cancel` now has a ``reason`` string parameter that can be used to attach info to any :exc:`Cancelled` to help in debugging. diff --git a/src/trio/_channel.py b/src/trio/_channel.py index 1ed5945798..54b5ea4bea 100644 --- a/src/trio/_channel.py +++ b/src/trio/_channel.py @@ -547,7 +547,9 @@ async def context_manager( yield wrapped_recv_chan # User has exited context manager, cancel to immediately close the # abandoned generator if it's still alive. - nursery.cancel_scope.cancel() + nursery.cancel_scope.cancel( + "exited trio.as_safe_channel context manager" + ) except BaseExceptionGroup as eg: try: raise_single_exception_from_group(eg) diff --git a/src/trio/_core/_asyncgens.py b/src/trio/_core/_asyncgens.py index b3b6895753..fea41e0e4d 100644 --- a/src/trio/_core/_asyncgens.py +++ b/src/trio/_core/_asyncgens.py @@ -230,7 +230,9 @@ async def _finalize_one( # with an exception, not even a Cancelled. The inside # is cancelled so there's no deadlock risk. with _core.CancelScope(shield=True) as cancel_scope: - cancel_scope.cancel() + cancel_scope.cancel( + reason="disallow async work when closing async generators during trio shutdown" + ) await agen.aclose() except BaseException: ASYNCGEN_LOGGER.exception( diff --git a/src/trio/_core/_exceptions.py b/src/trio/_core/_exceptions.py index 4a76a674ac..0974d8eb5e 100644 --- a/src/trio/_core/_exceptions.py +++ b/src/trio/_core/_exceptions.py @@ -1,12 +1,26 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from functools import partial +from typing import TYPE_CHECKING, Literal + +import attrs from trio._util import NoPublicConstructor, final if TYPE_CHECKING: from collections.abc import Callable + from typing_extensions import Self, TypeAlias + +CancelReasonLiteral: TypeAlias = Literal[ + "KeyboardInterrupt", + "deadline", + "explicit", + "nursery", + "shutdown", + "unknown", +] + class TrioInternalError(Exception): """Raised by :func:`run` if we encounter a bug in Trio, or (possibly) a @@ -34,6 +48,7 @@ class WouldBlock(Exception): @final +@attrs.define(eq=False, kw_only=True) class Cancelled(BaseException, metaclass=NoPublicConstructor): """Raised by blocking calls if the surrounding scope has been cancelled. @@ -67,11 +82,42 @@ class Cancelled(BaseException, metaclass=NoPublicConstructor): """ + source: CancelReasonLiteral + # repr(Task), so as to avoid gc troubles from holding a reference + source_task: str | None = None + reason: str | None = None + def __str__(self) -> str: - return "Cancelled" + return ( + f"cancelled due to {self.source}" + + ("" if self.reason is None else f" with reason {self.reason!r}") + + ("" if self.source_task is None else f" from task {self.source_task}") + ) def __reduce__(self) -> tuple[Callable[[], Cancelled], tuple[()]]: - return (Cancelled._create, ()) + # The `__reduce__` tuple does not support directly passing kwargs, and the + # kwargs are required so we can't use the third item for adding to __dict__, + # so we use partial. + return ( + partial( + Cancelled._create, + source=self.source, + source_task=self.source_task, + reason=self.reason, + ), + (), + ) + + if TYPE_CHECKING: + # for type checking on internal code + @classmethod + def _create( + cls, + *, + source: CancelReasonLiteral, + source_task: str | None = None, + reason: str | None = None, + ) -> Self: ... class BusyResourceError(Exception): diff --git a/src/trio/_core/_run.py b/src/trio/_core/_run.py index c2410150e1..5fff0b772f 100644 --- a/src/trio/_core/_run.py +++ b/src/trio/_core/_run.py @@ -36,7 +36,12 @@ from ._asyncgens import AsyncGenerators from ._concat_tb import concat_tb from ._entry_queue import EntryQueue, TrioToken -from ._exceptions import Cancelled, RunFinishedError, TrioInternalError +from ._exceptions import ( + Cancelled, + CancelReasonLiteral, + RunFinishedError, + TrioInternalError, +) from ._instrumentation import Instruments from ._ki import KIManager, enable_ki_protection from ._parking_lot import GLOBAL_PARKING_LOT_BREAKER @@ -305,7 +310,7 @@ def expire(self, now: float) -> bool: did_something = True # This implicitly calls self.remove(), so we don't need to # decrement _active here - cancel_scope.cancel() + cancel_scope._cancel(CancelReason(source="deadline")) # If we've accumulated too many stale entries, then prune the heap to # keep it under control. (We only do this occasionally in a batch, to # keep the amortized cost down) @@ -314,6 +319,20 @@ def expire(self, now: float) -> bool: return did_something +@attrs.define +class CancelReason: + """Attached to a :class:`CancelScope` upon cancellation with details of the source of the + cancellation, which is then used to construct the string in a :exc:`Cancelled`. + Users can pass a ``reason`` str to :meth:`CancelScope.cancel` to set it. + + Not publicly exported or documented. + """ + + source: CancelReasonLiteral + source_task: str | None = None + reason: str | None = None + + @attrs.define(eq=False) class CancelStatus: """Tracks the cancellation status for a contiguous extent @@ -468,6 +487,14 @@ def recalculate(self) -> None: or current.parent_cancellation_is_visible_to_us ) if new_state != current.effectively_cancelled: + if ( + current._scope._cancel_reason is None + and current.parent_cancellation_is_visible_to_us + ): + assert current._parent is not None + current._scope._cancel_reason = ( + current._parent._scope._cancel_reason + ) current.effectively_cancelled = new_state if new_state: for task in current._tasks: @@ -558,6 +585,8 @@ class CancelScope: _cancel_called: bool = attrs.field(default=False, init=False) cancelled_caught: bool = attrs.field(default=False, init=False) + _cancel_reason: CancelReason | None = attrs.field(default=None, init=False) + # Constructor arguments: _relative_deadline: float = attrs.field( default=inf, @@ -594,7 +623,7 @@ def __enter__(self) -> Self: self._relative_deadline = inf if current_time() >= self._deadline: - self.cancel() + self._cancel(CancelReason(source="deadline")) with self._might_change_registered_deadline(): self._cancel_status = CancelStatus(scope=self, parent=task._cancel_status) task._activate_cancel_status(self._cancel_status) @@ -883,19 +912,42 @@ def shield(self, new_value: bool) -> None: self._cancel_status.recalculate() @enable_ki_protection - def cancel(self) -> None: - """Cancels this scope immediately. - - This method is idempotent, i.e., if the scope was already - cancelled then this method silently does nothing. + def _cancel(self, cancel_reason: CancelReason | None) -> None: + """Internal sources of cancellation should use this instead of :meth:`cancel` + in order to set a more detailed :class:`CancelReason` + Helper or high-level functions can use `cancel`. """ if self._cancel_called: return + + if self._cancel_reason is None: + self._cancel_reason = cancel_reason + with self._might_change_registered_deadline(): self._cancel_called = True + if self._cancel_status is not None: self._cancel_status.recalculate() + @enable_ki_protection + def cancel(self, reason: str | None = None) -> None: + """Cancels this scope immediately. + + The optional ``reason`` argument accepts a string, which will be attached to + any resulting :exc:`Cancelled` exception to help you understand where that + cancellation is coming from and why it happened. + + This method is idempotent, i.e., if the scope was already + cancelled then this method silently does nothing. + """ + try: + current_task = repr(_core.current_task()) + except RuntimeError: + current_task = None + self._cancel( + CancelReason(reason=reason, source="explicit", source_task=current_task) + ) + @property def cancel_called(self) -> bool: """Readonly :class:`bool`. Records whether cancellation has been @@ -924,7 +976,7 @@ def cancel_called(self) -> bool: # but it makes the value returned by cancel_called more # closely match expectations. if not self._cancel_called and current_time() >= self._deadline: - self.cancel() + self._cancel(CancelReason(source="deadline")) return self._cancel_called @@ -1192,9 +1244,9 @@ def parent_task(self) -> Task: "(`~trio.lowlevel.Task`): The Task that opened this nursery." return self._parent_task - def _add_exc(self, exc: BaseException) -> None: + def _add_exc(self, exc: BaseException, reason: CancelReason | None) -> None: self._pending_excs.append(exc) - self.cancel_scope.cancel() + self.cancel_scope._cancel(reason) def _check_nursery_closed(self) -> None: if not any([self._nested_child_running, self._children, self._pending_starts]): @@ -1210,7 +1262,14 @@ def _child_finished( ) -> None: self._children.remove(task) if isinstance(outcome, Error): - self._add_exc(outcome.error) + self._add_exc( + outcome.error, + CancelReason( + source="nursery", + source_task=repr(task), + reason=f"child task raised exception {outcome.error!r}", + ), + ) self._check_nursery_closed() async def _nested_child_finished( @@ -1220,7 +1279,14 @@ async def _nested_child_finished( # Returns ExceptionGroup instance (or any exception if the nursery is in loose mode # and there is just one contained exception) if there are pending exceptions if nested_child_exc is not None: - self._add_exc(nested_child_exc) + self._add_exc( + nested_child_exc, + reason=CancelReason( + source="nursery", + source_task=repr(self._parent_task), + reason=f"Code block inside nursery contextmanager raised exception {nested_child_exc!r}", + ), + ) self._nested_child_running = False self._check_nursery_closed() @@ -1231,7 +1297,13 @@ async def _nested_child_finished( def aborted(raise_cancel: _core.RaiseCancelT) -> Abort: exn = capture(raise_cancel).error if not isinstance(exn, Cancelled): - self._add_exc(exn) + self._add_exc( + exn, + CancelReason( + source="KeyboardInterrupt", + source_task=repr(self._parent_task), + ), + ) # see test_cancel_scope_exit_doesnt_create_cyclic_garbage del exn # prevent cyclic garbage creation return Abort.FAILED @@ -1245,7 +1317,8 @@ def aborted(raise_cancel: _core.RaiseCancelT) -> Abort: try: await cancel_shielded_checkpoint() except BaseException as exc: - self._add_exc(exc) + # there's no children to cancel, so don't need to supply cancel reason + self._add_exc(exc, reason=None) popped = self._parent_task._child_nurseries.pop() assert popped is self @@ -1575,8 +1648,17 @@ def _attempt_delivery_of_any_pending_cancel(self) -> None: if not self._cancel_status.effectively_cancelled: return + reason = self._cancel_status._scope._cancel_reason + def raise_cancel() -> NoReturn: - raise Cancelled._create() + if reason is None: + raise Cancelled._create(source="unknown", reason="misnesting") + else: + raise Cancelled._create( + source=reason.source, + reason=reason.reason, + source_task=reason.source_task, + ) self._attempt_abort(raise_cancel) @@ -2075,7 +2157,13 @@ async def init( ) # Main task is done; start shutting down system tasks - self.system_nursery.cancel_scope.cancel() + self.system_nursery.cancel_scope._cancel( + CancelReason( + source="shutdown", + reason="main task done, shutting down system tasks", + source_task=repr(self.init_task), + ) + ) # System nursery is closed; finalize remaining async generators await self.asyncgens.finalize_remaining(self) @@ -2083,7 +2171,13 @@ async def init( # There are no more asyncgens, which means no more user-provided # code except possibly run_sync_soon callbacks. It's finally safe # to stop the run_sync_soon task and exit run(). - run_sync_soon_nursery.cancel_scope.cancel() + run_sync_soon_nursery.cancel_scope._cancel( + CancelReason( + source="shutdown", + reason="main task done, shutting down run_sync_soon callbacks", + source_task=repr(self.init_task), + ) + ) ################ # Outside context problems @@ -2926,7 +3020,18 @@ async def checkpoint() -> None: if task._cancel_status.effectively_cancelled or ( task is task._runner.main_task and task._runner.ki_pending ): - with CancelScope(deadline=-inf): + cs = CancelScope(deadline=-inf) + if ( + task._cancel_status._scope._cancel_reason is None + and task is task._runner.main_task + and task._runner.ki_pending + ): + task._cancel_status._scope._cancel_reason = CancelReason( + source="KeyboardInterrupt" + ) + assert task._cancel_status._scope._cancel_reason is not None + cs._cancel_reason = task._cancel_status._scope._cancel_reason + with cs: await _core.wait_task_rescheduled(lambda _: _core.Abort.SUCCEEDED) diff --git a/src/trio/_core/_tests/test_cancelled.py b/src/trio/_core/_tests/test_cancelled.py new file mode 100644 index 0000000000..18af4a367e --- /dev/null +++ b/src/trio/_core/_tests/test_cancelled.py @@ -0,0 +1,222 @@ +import pickle +import re +from math import inf + +import pytest + +import trio +from trio import Cancelled +from trio.lowlevel import current_task +from trio.testing import RaisesGroup, wait_all_tasks_blocked + +from .test_ki import ki_self + + +def test_Cancelled_init() -> None: + with pytest.raises(TypeError, match=r"^trio.Cancelled has no public constructor$"): + raise Cancelled # type: ignore[call-arg] + + with pytest.raises(TypeError, match=r"^trio.Cancelled has no public constructor$"): + Cancelled(source="explicit") + + # private constructor should not raise + Cancelled._create(source="explicit") + + +async def test_Cancelled_str() -> None: + cancelled = Cancelled._create(source="explicit") + assert str(cancelled) == "cancelled due to explicit" + # note: repr(current_task()) is often fairly verbose + assert re.fullmatch( + r"cancelled due to deadline from task " + r"", + str( + Cancelled._create( + source="deadline", + source_task=repr(current_task()), + ) + ), + ) + + assert re.fullmatch( + rf"cancelled due to nursery with reason 'pigs flying' from task {current_task()!r}", + str( + Cancelled._create( + source="nursery", + source_task=repr(current_task()), + reason="pigs flying", + ) + ), + ) + + +def test_Cancelled_subclass() -> None: + with pytest.raises(TypeError): + type("Subclass", (Cancelled,), {}) + + +# https://github.com/python-trio/trio/issues/3248 +def test_Cancelled_pickle() -> None: + cancelled = Cancelled._create(source="KeyboardInterrupt") + pickled_cancelled = pickle.loads(pickle.dumps(cancelled)) + assert isinstance(pickled_cancelled, Cancelled) + assert cancelled.source == pickled_cancelled.source + assert cancelled.source_task == pickled_cancelled.source_task + assert cancelled.reason == pickled_cancelled.reason + + +async def test_cancel_reason() -> None: + with trio.CancelScope() as cs: + cs.cancel(reason="hello") + with pytest.raises( + Cancelled, + match=rf"^cancelled due to explicit with reason 'hello' from task {current_task()!r}$", + ) as excinfo: + await trio.lowlevel.checkpoint() + assert excinfo.value.source == "explicit" + assert excinfo.value.reason == "hello" + assert excinfo.value.source_task == repr(current_task()) + + with trio.CancelScope(deadline=-inf) as cs: + with pytest.raises(Cancelled, match=r"^cancelled due to deadline"): + await trio.lowlevel.checkpoint() + + with trio.CancelScope() as cs: + cs.deadline = -inf + with pytest.raises( + Cancelled, + match=r"^cancelled due to deadline", + ): + await trio.lowlevel.checkpoint() + + +match_str = r"^cancelled due to nursery with reason 'child task raised exception ValueError\(\)' from task {0!r}$" + + +async def cancelled_task( + fail_task: trio.lowlevel.Task, task_status: trio.TaskStatus +) -> None: + task_status.started() + with pytest.raises(Cancelled, match=match_str.format(fail_task)): + await trio.sleep_forever() + raise TypeError + + +# failing_task raises before cancelled_task is started +async def test_cancel_reason_nursery() -> None: + async def failing_task(task_status: trio.TaskStatus[trio.lowlevel.Task]) -> None: + task_status.started(current_task()) + raise ValueError + + with 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)): + await wait_all_tasks_blocked() + await nursery.start(cancelled_task, fail_task) + + +# wait until both tasks are running before failing_task raises +async def test_cancel_reason_nursery2() -> None: + async def failing_task(task_status: trio.TaskStatus[trio.lowlevel.Task]) -> None: + task_status.started(current_task()) + await wait_all_tasks_blocked() + raise ValueError + + with RaisesGroup(ValueError, TypeError): + async with trio.open_nursery() as nursery: + fail_task = await nursery.start(failing_task) + await nursery.start(cancelled_task, fail_task) + + +# failing_task raises before calling task_status.started() +async def test_cancel_reason_nursery3() -> None: + async def failing_task(task_status: trio.TaskStatus[None]) -> None: + raise ValueError + + parent_task = current_task() + + async def cancelled_task() -> None: + # We don't have a way of distinguishing that the nursery code block failed + # because it failed to `start()` a task. + with pytest.raises( + Cancelled, + match=re.escape( + rf"cancelled due to nursery with reason 'Code block inside nursery contextmanager raised exception ValueError()' from task {parent_task!r}" + ), + ): + await trio.sleep_forever() + + with RaisesGroup(ValueError): + async with trio.open_nursery() as nursery: + nursery.start_soon(cancelled_task) + await wait_all_tasks_blocked() + await nursery.start(failing_task) + + +async def test_cancel_reason_not_overwritten() -> None: + with trio.CancelScope() as cs: + cs.cancel() + with pytest.raises( + Cancelled, + match=rf"^cancelled due to explicit from task {current_task()!r}$", + ): + await trio.lowlevel.checkpoint() + cs.deadline = -inf + with pytest.raises( + Cancelled, + match=rf"^cancelled due to explicit from task {current_task()!r}$", + ): + await trio.lowlevel.checkpoint() + + +async def test_cancel_reason_not_overwritten_2() -> None: + with trio.CancelScope() as cs: + cs.deadline = -inf + with pytest.raises(Cancelled, match=r"^cancelled due to deadline$"): + await trio.lowlevel.checkpoint() + cs.cancel() + with pytest.raises(Cancelled, match=r"^cancelled due to deadline$"): + await trio.lowlevel.checkpoint() + + +async def test_nested_child_source() -> None: + ev = trio.Event() + parent_task = current_task() + + async def child() -> None: + ev.set() + with pytest.raises( + Cancelled, + match=rf"^cancelled due to nursery with reason 'Code block inside nursery contextmanager raised exception ValueError\(\)' from task {parent_task!r}$", + ): + await trio.sleep_forever() + + with RaisesGroup(ValueError): + async with trio.open_nursery() as nursery: + nursery.start_soon(child) + await ev.wait() + raise ValueError + + +async def test_reason_delayed_ki() -> None: + # simplified version of test_ki.test_ki_protection_works check #2 + parent_task = current_task() + + async def sleeper(name: str) -> None: + with pytest.raises( + Cancelled, + match=rf"^cancelled due to KeyboardInterrupt from task {parent_task!r}$", + ): + while True: + await trio.lowlevel.checkpoint() + + async def raiser(name: str) -> None: + ki_self() + + with RaisesGroup(KeyboardInterrupt): + async with trio.open_nursery() as nursery: + nursery.start_soon(sleeper, "s1") + nursery.start_soon(sleeper, "s2") + nursery.start_soon(trio.lowlevel.enable_ki_protection(raiser), "r1") + # __aexit__ blocks, and then receives the KI diff --git a/src/trio/_core/_tests/test_run.py b/src/trio/_core/_tests/test_run.py index f3e53ace38..5026c139c6 100644 --- a/src/trio/_core/_tests/test_run.py +++ b/src/trio/_core/_tests/test_run.py @@ -3,7 +3,6 @@ import contextvars import functools import gc -import pickle import sys import threading import time @@ -781,7 +780,10 @@ async def task1() -> None: # Even if inside another cancel scope async def task2() -> None: with _core.CancelScope(): - with pytest.raises(_core.Cancelled): + with pytest.raises( + _core.Cancelled, + match=r"^cancelled due to unknown with reason 'misnesting'$", + ): await sleep_forever() with ExitStack() as stack: @@ -2221,34 +2223,6 @@ def test_Nursery_subclass() -> None: type("Subclass", (_core._run.Nursery,), {}) -def test_Cancelled_init() -> None: - with pytest.raises(TypeError): - raise _core.Cancelled - - with pytest.raises(TypeError): - _core.Cancelled() - - # private constructor should not raise - _core.Cancelled._create() - - -def test_Cancelled_str() -> None: - cancelled = _core.Cancelled._create() - assert str(cancelled) == "Cancelled" - - -def test_Cancelled_subclass() -> None: - with pytest.raises(TypeError): - type("Subclass", (_core.Cancelled,), {}) - - -# https://github.com/python-trio/trio/issues/3248 -def test_Cancelled_pickle() -> None: - cancelled = _core.Cancelled._create() - cancelled = pickle.loads(pickle.dumps(cancelled)) - assert isinstance(cancelled, _core.Cancelled) - - def test_CancelScope_subclass() -> None: with pytest.raises(TypeError): type("Subclass", (_core.CancelScope,), {}) diff --git a/src/trio/_highlevel_generic.py b/src/trio/_highlevel_generic.py index 041a684c62..9bd8822c9e 100644 --- a/src/trio/_highlevel_generic.py +++ b/src/trio/_highlevel_generic.py @@ -43,7 +43,7 @@ async def aclose_forcefully(resource: AsyncResource) -> None: """ with trio.CancelScope() as cs: - cs.cancel() + cs.cancel(reason="cancelled during aclose_forcefully") await resource.aclose() diff --git a/src/trio/_highlevel_open_tcp_stream.py b/src/trio/_highlevel_open_tcp_stream.py index 5723180e46..11460689b4 100644 --- a/src/trio/_highlevel_open_tcp_stream.py +++ b/src/trio/_highlevel_open_tcp_stream.py @@ -357,7 +357,7 @@ async def attempt_connect( # Success! Save the winning socket and cancel all outstanding # connection attempts. winning_socket = sock - nursery.cancel_scope.cancel() + nursery.cancel_scope.cancel(reason="successfully found a socket") except OSError as exc: # This connection attempt failed, but the next one might # succeed. Save the error for later so we can report it if diff --git a/src/trio/_subprocess.py b/src/trio/_subprocess.py index 823c50ea63..c1b162e308 100644 --- a/src/trio/_subprocess.py +++ b/src/trio/_subprocess.py @@ -766,7 +766,7 @@ async def killer() -> None: nursery.start_soon(killer) await proc.wait() - killer_cscope.cancel() + killer_cscope.cancel(reason="trio internal implementation detail") raise stdout = b"".join(stdout_chunks) if capture_stdout else None diff --git a/src/trio/_tests/test_threads.py b/src/trio/_tests/test_threads.py index 380da3833b..ba2c366faa 100644 --- a/src/trio/_tests/test_threads.py +++ b/src/trio/_tests/test_threads.py @@ -993,82 +993,113 @@ async def async_time_bomb() -> None: assert cancel_scope.cancelled_caught -async def test_from_thread_check_cancelled() -> None: - q: stdlib_queue.Queue[str] = stdlib_queue.Queue() +async def child( + abandon_on_cancel: bool, + scope: CancelScope, + record: list[str], + f: Callable[[], None], +) -> None: + with scope: + record.append("start") + try: + return await to_thread_run_sync(f, abandon_on_cancel=abandon_on_cancel) + except _core.Cancelled as e: + record.append(str(e)) + raise + finally: + record.append("exit") - async def child(abandon_on_cancel: bool, scope: CancelScope) -> None: - with scope: - record.append("start") - try: - return await to_thread_run_sync(f, abandon_on_cancel=abandon_on_cancel) - except _core.Cancelled: - record.append("cancel") - raise - finally: - record.append("exit") + +@pytest.mark.parametrize("cancel_the_scope", [False, True]) +async def test_from_thread_check_cancelled_no_abandon(cancel_the_scope: bool) -> None: + q: stdlib_queue.Queue[str | BaseException] = stdlib_queue.Queue() def f() -> None: try: from_thread_check_cancelled() - except _core.Cancelled: # pragma: no cover, test failure path - q.put("Cancelled") + except _core.Cancelled as e: # pragma: no cover, test failure path + q.put(str(e)) else: q.put("Not Cancelled") ev.wait() return from_thread_check_cancelled() - # Base case: nothing cancelled so we shouldn't see cancels anywhere record: list[str] = [] ev = threading.Event() - async with _core.open_nursery() as nursery: - nursery.start_soon(child, False, _core.CancelScope()) - await wait_all_tasks_blocked() - assert record[0] == "start" - assert q.get(timeout=1) == "Not Cancelled" - ev.set() - # implicit assertion, Cancelled not raised via nursery - assert record[1] == "exit" - - # abandon_on_cancel=False case: a cancel will pop out but be handled by - # the appropriate cancel scope - record = [] - ev = threading.Event() scope = _core.CancelScope() # Nursery cancel scope gives false positives + async with _core.open_nursery() as nursery: - nursery.start_soon(child, False, scope) + nursery.start_soon(child, False, scope, record, f) await wait_all_tasks_blocked() assert record[0] == "start" assert q.get(timeout=1) == "Not Cancelled" - scope.cancel() + if cancel_the_scope: + scope.cancel() ev.set() - assert scope.cancelled_caught - assert "cancel" in record - assert record[-1] == "exit" + # Base case: nothing cancelled so we shouldn't see cancels anywhere + if not cancel_the_scope: + # implicit assertion, Cancelled not raised via nursery + assert record[1] == "exit" + else: + # abandon_on_cancel=False case: a cancel will pop out but be handled by + # the appropriate cancel scope + + assert scope.cancelled_caught + assert re.fullmatch( + r"cancelled due to explicit from task " + r"", + record[1], + ), record[1] + assert record[2] == "exit" + assert len(record) == 3 + +async def test_from_thread_check_cancelled_abandon_on_cancel() -> None: + q: stdlib_queue.Queue[str | BaseException] = stdlib_queue.Queue() # abandon_on_cancel=True case: slightly different thread behavior needed # check thread is cancelled "soon" after abandonment - def f() -> None: # type: ignore[no-redef] # noqa: F811 + + def f() -> None: ev.wait() try: from_thread_check_cancelled() - except _core.Cancelled: - q.put("Cancelled") + except _core.Cancelled as e: + q.put(str(e)) + except BaseException as e: # pragma: no cover, test failure path + # abandon_on_cancel=True will eat exceptions, so we pass it + # through the queue in order to be able to debug any exceptions + q.put(e) else: # pragma: no cover, test failure path q.put("Not Cancelled") - record = [] + record: list[str] = [] ev = threading.Event() scope = _core.CancelScope() async with _core.open_nursery() as nursery: - nursery.start_soon(child, True, scope) + nursery.start_soon(child, True, scope, record, f) await wait_all_tasks_blocked() assert record[0] == "start" scope.cancel() - ev.set() + # In the worst case the nursery fully exits before the threaded function + # checks for cancellation. + ev.set() + assert scope.cancelled_caught - assert "cancel" in record + assert re.fullmatch( + r"cancelled due to explicit from task " + r"", + record[1], + ), record[1] assert record[-1] == "exit" - assert q.get(timeout=1) == "Cancelled" + res = q.get(timeout=1) + if isinstance(res, BaseException): # pragma: no cover # for debugging + raise res + else: + assert re.fullmatch( + r"cancelled due to explicit from task " + r"", + res, + ), res def test_from_thread_check_cancelled_raises_in_foreign_threads() -> None: diff --git a/src/trio/_tests/test_util.py b/src/trio/_tests/test_util.py index c0b0a3108e..4182ba5db4 100644 --- a/src/trio/_tests/test_util.py +++ b/src/trio/_tests/test_util.py @@ -282,7 +282,7 @@ async def test_raise_single_exception_from_group() -> None: context = TypeError("context") exc.__cause__ = cause exc.__context__ = context - cancelled = trio.Cancelled._create() + cancelled = trio.Cancelled._create(source="deadline") with pytest.raises(ValueError, match="foo") as excinfo: raise_single_exception_from_group(ExceptionGroup("", [exc])) @@ -346,9 +346,11 @@ async def test_raise_single_exception_from_group() -> None: assert excinfo.value.__context__ is None # if we only got cancelled, first one is reraised - with pytest.raises(trio.Cancelled, match=r"^Cancelled$") as excinfo: + with pytest.raises(trio.Cancelled, match=r"^cancelled due to deadline$") as excinfo: raise_single_exception_from_group( - BaseExceptionGroup("", [cancelled, trio.Cancelled._create()]) + BaseExceptionGroup( + "", [cancelled, trio.Cancelled._create(source="explicit")] + ) ) assert excinfo.value is cancelled assert excinfo.value.__cause__ is None diff --git a/src/trio/_threads.py b/src/trio/_threads.py index 394e5b06ac..4b1e54f540 100644 --- a/src/trio/_threads.py +++ b/src/trio/_threads.py @@ -430,6 +430,8 @@ def deliver_worker_fn_result(result: outcome.Outcome[RetT]) -> None: def abort(raise_cancel: RaiseCancelT) -> trio.lowlevel.Abort: # fill so from_thread_check_cancelled can raise + # 'raise_cancel' will immediately delete its reason object, so we make + # a copy in each thread cancel_register[0] = raise_cancel if abandon_bool: # empty so report_back_in_trio_thread_fn cannot reschedule diff --git a/src/trio/_util.py b/src/trio/_util.py index 6656749111..106423e2aa 100644 --- a/src/trio/_util.py +++ b/src/trio/_util.py @@ -29,12 +29,11 @@ import sys from types import AsyncGeneratorType, TracebackType - from typing_extensions import ParamSpec, Self, TypeVarTuple, Unpack + from typing_extensions import Self, TypeVarTuple, Unpack if sys.version_info < (3, 11): from exceptiongroup import BaseExceptionGroup - ArgsT = ParamSpec("ArgsT") PosArgsT = TypeVarTuple("PosArgsT") From 0209a186a8afdc65997caa61317df443b3ec28ad Mon Sep 17 00:00:00 2001 From: A5rocks Date: Thu, 15 May 2025 10:09:09 -0400 Subject: [PATCH 016/111] Get coverage back to 100% (#3268) * Add a missing test case and make coverage ignore scripts * Remove now unnecessary pragmas --- pyproject.toml | 3 ++- src/trio/_tests/tools/test_sync_requirements.py | 1 + src/trio/_tools/gen_exports.py | 2 +- src/trio/_tools/mypy_annotate.py | 2 +- src/trio/_tools/sync_requirements.py | 10 ++-------- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ba2c4387d6..d7076de7bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -332,7 +332,8 @@ exclude_also = [ 'class .*\bProtocol\b.*\):', "raise NotImplementedError", '.*if "sphinx" in sys.modules:', - 'TODO: test this line' + 'TODO: test this line', + 'if __name__ == "__main__":', ] partial_branches = [ "pragma: no branch", diff --git a/src/trio/_tests/tools/test_sync_requirements.py b/src/trio/_tests/tools/test_sync_requirements.py index 83ae21be65..db64d36eaa 100644 --- a/src/trio/_tests/tools/test_sync_requirements.py +++ b/src/trio/_tests/tools/test_sync_requirements.py @@ -27,6 +27,7 @@ def test_yield_pre_commit_version_data() -> None: rev: v0.11.0 - repo: https://github.com/psf/black-pre-commit-mirror rev: 25.1.0 + - bad: data """ results = tuple(yield_pre_commit_version_data(text)) assert results == ( diff --git a/src/trio/_tools/gen_exports.py b/src/trio/_tools/gen_exports.py index 5b1affe24a..101e0e4912 100755 --- a/src/trio/_tools/gen_exports.py +++ b/src/trio/_tools/gen_exports.py @@ -399,5 +399,5 @@ def main() -> None: # pragma: no cover """ -if __name__ == "__main__": # pragma: no cover +if __name__ == "__main__": main() diff --git a/src/trio/_tools/mypy_annotate.py b/src/trio/_tools/mypy_annotate.py index 5acb9b993c..1d625ad7ae 100644 --- a/src/trio/_tools/mypy_annotate.py +++ b/src/trio/_tools/mypy_annotate.py @@ -122,5 +122,5 @@ def main(argv: list[str]) -> None: pickle.dump(results, f) -if __name__ == "__main__": # pragma: no cover +if __name__ == "__main__": main(sys.argv[1:]) diff --git a/src/trio/_tools/sync_requirements.py b/src/trio/_tools/sync_requirements.py index f4ca481faf..43337e29dc 100755 --- a/src/trio/_tools/sync_requirements.py +++ b/src/trio/_tools/sync_requirements.py @@ -78,9 +78,7 @@ def update_requirements( return changed -def main() -> int: - """Run program.""" - +if __name__ == "__main__": source_root = Path.cwd().absolute() # Double-check we found the right directory @@ -97,8 +95,4 @@ def main() -> int: for name, version in yield_pre_commit_version_data(pre_commit_text) } changed = update_requirements(test_requirements, pre_commit_versions) - return int(changed) - - -if __name__ == "__main__": - sys.exit(main()) + sys.exit(int(changed)) From 56275c10c3ee00589000d6a0a230845302b23540 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 15 May 2025 16:54:31 +0000 Subject: [PATCH 017/111] [pre-commit.ci] pre-commit autoupdate (#3265) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.8 → v0.11.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.8...v0.11.9) - [github.com/woodruffw/zizmor-pre-commit: v1.6.0 → v1.7.0](https://github.com/woodruffw/zizmor-pre-commit/compare/v1.6.0...v1.7.0) - [github.com/astral-sh/uv-pre-commit: 0.7.2 → 0.7.3](https://github.com/astral-sh/uv-pre-commit/compare/0.7.2...0.7.3) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- test-requirements.txt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b545b1130b..d33754c09b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.8 + rev: v0.11.9 hooks: - id: ruff types: [file] @@ -46,7 +46,7 @@ repos: hooks: - id: sphinx-lint - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.6.0 + rev: v1.7.0 hooks: - id: zizmor - repo: local @@ -66,7 +66,7 @@ repos: additional_dependencies: ["pyyaml"] files: ^(test-requirements\.txt)|(\.pre-commit-config\.yaml)$ - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.7.2 + rev: 0.7.3 hooks: # Compile requirements - id: pip-compile diff --git a/test-requirements.txt b/test-requirements.txt index a61dea107d..0d99258197 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -132,7 +132,7 @@ requests==2.32.3 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.11.8 +ruff==0.11.9 # via -r test-requirements.in setuptools==80.1.0 # via types-setuptools @@ -194,7 +194,7 @@ typing-extensions==4.13.2 # pyright urllib3==2.4.0 # via requests -uv==0.7.2 +uv==0.7.3 # via -r test-requirements.in virtualenv==20.30.0 # via pre-commit From 7d5ce6bb0a29b9cb028d8419cf6737538739ded3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 20:35:27 +0000 Subject: [PATCH 018/111] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.9 → v0.11.10](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.9...v0.11.10) - [github.com/astral-sh/uv-pre-commit: 0.7.3 → 0.7.5](https://github.com/astral-sh/uv-pre-commit/compare/0.7.3...0.7.5) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d33754c09b..eb4c664763 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.9 + rev: v0.11.10 hooks: - id: ruff types: [file] @@ -66,7 +66,7 @@ repos: additional_dependencies: ["pyyaml"] files: ^(test-requirements\.txt)|(\.pre-commit-config\.yaml)$ - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.7.3 + rev: 0.7.5 hooks: # Compile requirements - id: pip-compile From d4b25ba1bab2defd831af9f150ff8947a3042d44 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 20:36:11 +0000 Subject: [PATCH 019/111] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 0d99258197..d926d5351f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -132,7 +132,7 @@ requests==2.32.3 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.11.9 +ruff==0.11.10 # via -r test-requirements.in setuptools==80.1.0 # via types-setuptools @@ -194,7 +194,7 @@ typing-extensions==4.13.2 # pyright urllib3==2.4.0 # via requests -uv==0.7.3 +uv==0.7.5 # via -r test-requirements.in virtualenv==20.30.0 # via pre-commit From 874f893e7b0726f787b977f172c3a492720ba9e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Tue, 20 May 2025 19:10:35 +0300 Subject: [PATCH 020/111] Added default value for the "source" parameter in the trio.Cancelled initializer This restores backwards compatibility with AnyIO. --- src/trio/_core/_exceptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/trio/_core/_exceptions.py b/src/trio/_core/_exceptions.py index 0974d8eb5e..f70d5e0e95 100644 --- a/src/trio/_core/_exceptions.py +++ b/src/trio/_core/_exceptions.py @@ -82,7 +82,7 @@ class Cancelled(BaseException, metaclass=NoPublicConstructor): """ - source: CancelReasonLiteral + source: CancelReasonLiteral = "unknown" # repr(Task), so as to avoid gc troubles from holding a reference source_task: str | None = None reason: str | None = None @@ -114,7 +114,7 @@ def __reduce__(self) -> tuple[Callable[[], Cancelled], tuple[()]]: def _create( cls, *, - source: CancelReasonLiteral, + source: CancelReasonLiteral = "unknown", source_task: str | None = None, reason: str | None = None, ) -> Self: ... From d7125ac188087d29bd39adacfe1e8416985a1708 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Tue, 20 May 2025 11:29:07 -0700 Subject: [PATCH 021/111] remove type-ignore --- src/trio/_core/_tests/test_cancelled.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/trio/_core/_tests/test_cancelled.py b/src/trio/_core/_tests/test_cancelled.py index 18af4a367e..0c144c37f0 100644 --- a/src/trio/_core/_tests/test_cancelled.py +++ b/src/trio/_core/_tests/test_cancelled.py @@ -14,7 +14,7 @@ def test_Cancelled_init() -> None: with pytest.raises(TypeError, match=r"^trio.Cancelled has no public constructor$"): - raise Cancelled # type: ignore[call-arg] + raise Cancelled with pytest.raises(TypeError, match=r"^trio.Cancelled has no public constructor$"): Cancelled(source="explicit") From ca218b23182b1f89017e72a750d85a960147cd7d Mon Sep 17 00:00:00 2001 From: John Litborn <11260241+jakkdl@users.noreply.github.com> Date: Wed, 21 May 2025 15:18:10 +0200 Subject: [PATCH 022/111] unpin cython (#3269) follow up to #3264 --- .github/workflows/ci.yml | 6 ++---- tox.ini | 22 +++++++++++++++++++++- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 562495a4a2..bacdccbd50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -390,13 +390,11 @@ jobs: - python: '3.9' # We support running on cython 2 and 3 for 3.9 cython: '<3' # cython 2 - python: '3.9' - # cython 3.1.0 broke stuff https://github.com/cython/cython/issues/6865 - cython: '>=3,<3.1' # cython 3 (or greater) + cython: '>=3' # cython 3 (or greater) - python: '3.11' # 3.11 is the last version Cy2 supports cython: '<3' # cython 2 - python: '3.13' # We support running cython3 on 3.13 - # cython 3.1.0 broke stuff https://github.com/cython/cython/issues/6865 - cython: '>=3,<3.1' # cython 3 (or greater) + cython: '>=3' # cython 3 (or greater) steps: - name: Retrieve the project source from an sdist inside the GHA artifact uses: re-actors/checkout-python-sdist@release/v2 diff --git a/tox.ini b/tox.ini index 39d4dece5d..37bb42b7bb 100644 --- a/tox.ini +++ b/tox.ini @@ -53,7 +53,7 @@ commands = description = "Run cython tests." deps = # cython 3.1.0 broke stuff https://github.com/cython/cython/issues/6865 - cython: cython<3.1.0 + cython: cython cython2: cython<3 setuptools ; python_version >= '3.12' commands_pre = @@ -63,6 +63,26 @@ commands_pre = commands = python -m tests.cython.run_test_cython +[testenv:cov-cython] +deps = + setuptools + cython +set_env = + CFLAGS= -DCYTHON_TRACE_NOGIL=1A +allowlist_externals = + sed + cp +commands_pre = + python --version + cython --version + cp pyproject.toml {temp_dir}/ + sed -i "s/plugins\ =\ \\[\\]/plugins = [\"Cython.Coverage\"]/" {temp_dir}/pyproject.toml + cythonize --inplace -X linetrace=True tests/cython/test_cython.pyx +commands = + coverage run -m tests.cython.run_test_cython --rcfile={temp_dir}/pyproject.toml + coverage combine + coverage report + [testenv:gen_exports] description = "Run gen_exports.py, regenerating code for public API wrappers." deps = From 302911940e7bad083195c8162203fb56c409c188 Mon Sep 17 00:00:00 2001 From: John Litborn <11260241+jakkdl@users.noreply.github.com> Date: Fri, 23 May 2025 11:50:01 +0200 Subject: [PATCH 023/111] fix typo, clean up test_traceback_frame_removal (#3272) --- src/trio/_core/_tests/test_run.py | 13 ++++--------- tox.ini | 2 +- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/trio/_core/_tests/test_run.py b/src/trio/_core/_tests/test_run.py index 5026c139c6..b317c8d6c9 100644 --- a/src/trio/_core/_tests/test_run.py +++ b/src/trio/_core/_tests/test_run.py @@ -2130,17 +2130,12 @@ def check_traceback(exc: KeyError) -> bool: assert tb is not None return tb.tb_frame.f_code is my_child_task.__code__ - expected_exception = Matcher(KeyError, check=check_traceback) - - with RaisesGroup(expected_exception, expected_exception): - # Trick: 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. Someday I - # guess this will always be an ExceptionGroup (#611), but for now we can - # force it by raising two exceptions. + with RaisesGroup(Matcher(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. async with _core.open_nursery() as nursery: nursery.start_soon(my_child_task) - nursery.start_soon(my_child_task) def test_contextvar_support() -> None: diff --git a/tox.ini b/tox.ini index 37bb42b7bb..c8e848cb1b 100644 --- a/tox.ini +++ b/tox.ini @@ -68,7 +68,7 @@ deps = setuptools cython set_env = - CFLAGS= -DCYTHON_TRACE_NOGIL=1A + CFLAGS= -DCYTHON_TRACE_NOGIL=1 allowlist_externals = sed cp From 37c407daf6ec9773dfdba4ea5c3e10ccb6bfed88 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 20:34:41 +0000 Subject: [PATCH 024/111] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.10 → v0.11.11](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.10...v0.11.11) - [github.com/woodruffw/zizmor-pre-commit: v1.7.0 → v1.8.0](https://github.com/woodruffw/zizmor-pre-commit/compare/v1.7.0...v1.8.0) - [github.com/astral-sh/uv-pre-commit: 0.7.5 → 0.7.8](https://github.com/astral-sh/uv-pre-commit/compare/0.7.5...0.7.8) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eb4c664763..3112804e28 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.10 + rev: v0.11.11 hooks: - id: ruff types: [file] @@ -46,7 +46,7 @@ repos: hooks: - id: sphinx-lint - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.7.0 + rev: v1.8.0 hooks: - id: zizmor - repo: local @@ -66,7 +66,7 @@ repos: additional_dependencies: ["pyyaml"] files: ^(test-requirements\.txt)|(\.pre-commit-config\.yaml)$ - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.7.5 + rev: 0.7.8 hooks: # Compile requirements - id: pip-compile From 6d8600b3549fbb1bac007626b65ae76fb89a70e8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 May 2025 20:35:05 +0000 Subject: [PATCH 025/111] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index d926d5351f..87036e952d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -132,7 +132,7 @@ requests==2.32.3 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.11.10 +ruff==0.11.11 # via -r test-requirements.in setuptools==80.1.0 # via types-setuptools @@ -194,7 +194,7 @@ typing-extensions==4.13.2 # pyright urllib3==2.4.0 # via requests -uv==0.7.5 +uv==0.7.8 # via -r test-requirements.in virtualenv==20.30.0 # via pre-commit From ca2e9a11a80e487b8308770c66c56ede6a7f6d97 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Fri, 30 May 2025 07:10:03 +0900 Subject: [PATCH 026/111] Push some local cleanups --- .github/workflows/ci.yml | 42 +++++++++++++++++------------- src/trio/_subprocess.py | 7 +++-- src/trio/_tests/test_subprocess.py | 2 +- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bacdccbd50..72bfb7efd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -182,13 +182,11 @@ jobs: # lsp_extract_file: '' # extra_name: ', with non-IFS LSP' - # ***REMEMBER*** to remove the 3.14 line once windows+cffi works again continue-on-error: >- ${{ ( endsWith(matrix.python, '-dev') || endsWith(matrix.python, '-nightly') - || matrix.python == '3.14' ) && true || false @@ -202,18 +200,11 @@ jobs: - name: Setup python uses: actions/setup-python@v5 with: - # This allows the matrix to specify just the major.minor version while still - # expanding it to get the latest patch version including alpha releases. - # This avoids the need to update for each new alpha, beta, release candidate, - # and then finally an actual release version. actions/setup-python doesn't - # support this for PyPy presently so we get no help there. - # - # 'CPython' -> '3.9.0-alpha - 3.9.X' - # 'PyPy' -> 'pypy-3.9' - python-version: ${{ fromJSON(format('["{0}", "{1}"]', format('{0}.0-alpha - {0}.X', matrix.python), matrix.python))[startsWith(matrix.python, 'pypy')] }} + python-version: '${{ matrix.python }}' architecture: '${{ matrix.arch }}' cache: pip cache-dependency-path: test-requirements.txt + allow-prereleases: true - name: Run tests run: ./ci.sh shell: bash @@ -276,9 +267,10 @@ jobs: - name: Setup python uses: actions/setup-python@v5 with: - python-version: ${{ fromJSON(format('["{0}", "{1}"]', format('{0}.0-alpha - {0}.X', matrix.python), matrix.python))[startsWith(matrix.python, 'pypy')] }} + python-version: '${{ matrix.python }}' cache: pip cache-dependency-path: test-requirements.txt + allow-prereleases: true - name: Check Formatting if: matrix.check_formatting == '1' run: @@ -328,9 +320,10 @@ jobs: - name: Setup python uses: actions/setup-python@v5 with: - python-version: ${{ fromJSON(format('["{0}", "{1}"]', format('{0}.0-alpha - {0}.X', matrix.python), matrix.python))[startsWith(matrix.python, 'pypy')] }} + python-version: '${{ matrix.python }}' cache: pip cache-dependency-path: test-requirements.txt + allow-prereleases: true - name: Run tests run: ./ci.sh - if: always() @@ -356,19 +349,29 @@ jobs: # `nodejs` for pyright (`node-env` pulls in nodejs but that takes a while and can time out the test). # `perl` for a platform independent `sed -i` alternative run: apk update && apk add python3-dev bash nodejs perl + - name: Retrieve the project source from an sdist inside the GHA artifact # must be after `apk add` because it relies on `bash` existing uses: re-actors/checkout-python-sdist@release/v2 with: source-tarball-name: ${{ needs.build.outputs.sdist-artifact-name }} workflow-artifact-name: ${{ env.dists-artifact-name }} + - name: Enter virtual environment run: python -m venv .venv + - name: Run tests run: source .venv/bin/activate && ./ci.sh + - name: Get Python version for codecov flag id: get-version - run: echo "version=$(python -V | cut -d' ' -f2 | cut -d'.' -f1,2)" >> "${GITHUB_OUTPUT}" + shell: python + run: | + import sys, os + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write("version=" + ".".join(map(str, sys.version_info[:2]))) + f.write("\n") + - if: always() uses: codecov/codecov-action@v3 with: @@ -425,11 +428,14 @@ jobs: - name: import & run module run: coverage run -m tests.cython.run_test_cython - - name: get Python version for codecov flag + - name: Get Python version for codecov flag id: get-version - run: >- - echo "version=$(python -V | cut -d' ' -f2 | cut -d'.' -f1,2)" - >> "${GITHUB_OUTPUT}" + shell: python + run: | + import sys, os + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write("version=" + ".".join(map(str, sys.version_info[:2]))) + f.write("\n") - run: | coverage combine diff --git a/src/trio/_subprocess.py b/src/trio/_subprocess.py index c1b162e308..d4faf317f8 100644 --- a/src/trio/_subprocess.py +++ b/src/trio/_subprocess.py @@ -736,8 +736,7 @@ async def read_output( # Opening the process does not need to be inside the nursery, so we put it outside # so any exceptions get directly seen by users. - # options needs a complex TypedDict. The overload error only occurs on Unix. - proc = await open_process(command, **options) # type: ignore[arg-type, call-overload, unused-ignore] + proc = await _open_process(command, **options) # type: ignore[arg-type] async with trio.open_nursery() as nursery: try: if input_ is not None: @@ -1164,7 +1163,7 @@ async def open_process( async def run_process( command: StrOrBytesPath, *, - stdin: bytes | bytearray | memoryview | int | HasFileno | None = None, + stdin: bytes | bytearray | memoryview | int | HasFileno | None = b"", shell: Literal[True], **kwargs: Unpack[UnixRunProcessArgs], ) -> subprocess.CompletedProcess[bytes]: ... @@ -1173,7 +1172,7 @@ async def run_process( async def run_process( command: Sequence[StrOrBytesPath], *, - stdin: bytes | bytearray | memoryview | int | HasFileno | None = None, + stdin: bytes | bytearray | memoryview | int | HasFileno | None = b"", shell: bool = False, **kwargs: Unpack[UnixRunProcessArgs], ) -> subprocess.CompletedProcess[bytes]: ... diff --git a/src/trio/_tests/test_subprocess.py b/src/trio/_tests/test_subprocess.py index 15c88be25e..7bb40d2b3d 100644 --- a/src/trio/_tests/test_subprocess.py +++ b/src/trio/_tests/test_subprocess.py @@ -733,7 +733,7 @@ async def test_run_process_internal_error(monkeypatch: pytest.MonkeyPatch) -> No async def very_broken_open(*args: object, **kwargs: object) -> str: return "oops" - monkeypatch.setattr(trio._subprocess, "open_process", very_broken_open) + monkeypatch.setattr(trio._subprocess, "_open_process", very_broken_open) with RaisesGroup(AttributeError, AttributeError): await run_process(EXIT_TRUE, capture_stdout=True) From be7faad27de163c625b4282459c0de1cbdcbfcbb Mon Sep 17 00:00:00 2001 From: Ievgeniia Radetska <656043+CheViana@users.noreply.github.com> Date: Sun, 8 Jun 2025 20:44:00 -0400 Subject: [PATCH 027/111] Fixes #3275; handle reraise of KI or SystemExit without losing err code (#3280) --- newsfragments/3275.bugfix.rst | 1 + src/trio/_tests/test_util.py | 17 ++++++++++++----- src/trio/_util.py | 2 +- 3 files changed, 14 insertions(+), 6 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..b418b26d76 --- /dev/null +++ b/newsfragments/3275.bugfix.rst @@ -0,0 +1 @@ +Handle unwrapping SystemExit/KeyboardInterrupt exception gracefully in utility function ``raise_single_exception_from_group`` that reraises last exception from group. diff --git a/src/trio/_tests/test_util.py b/src/trio/_tests/test_util.py index 4182ba5db4..3a57b9cf21 100644 --- a/src/trio/_tests/test_util.py +++ b/src/trio/_tests/test_util.py @@ -323,25 +323,32 @@ async def test_raise_single_exception_from_group() -> None: [ ValueError("foo"), ValueError("bar"), - KeyboardInterrupt("this exc doesn't get reraised"), + KeyboardInterrupt("preserve error msg"), ], ) - with pytest.raises(KeyboardInterrupt, match=r"^$") as excinfo: + with pytest.raises( + KeyboardInterrupt, + match=r"^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 systemexit_ki = BaseExceptionGroup( "", [ ValueError("foo"), ValueError("bar"), - SystemExit("this exc doesn't get reraised"), + SystemExit(2), ], ) - with pytest.raises(SystemExit, match=r"^$") 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/src/trio/_util.py b/src/trio/_util.py index 106423e2aa..54d324cab3 100644 --- a/src/trio/_util.py +++ b/src/trio/_util.py @@ -398,7 +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)): - raise type(e) from eg + raise type(e)(*e.args) from eg cancelled_exception: trio.Cancelled | None = None noncancelled_exception: BaseException | None = None From 8771618c56cab079f7acd80aafe89255e8164408 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 09:45:54 +0900 Subject: [PATCH 028/111] Bump dependencies from commit 79bdcf (#3277) --- .pre-commit-config.yaml | 8 +- docs-requirements.txt | 13 ++- docs/source/typevars.py | 2 +- newsfragments/3277.bugfix.rst | 1 + src/trio/_core/_io_windows.py | 2 +- src/trio/_core/_run.py | 3 + src/trio/_dtls.py | 24 ++-- src/trio/_file_io.py | 2 +- src/trio/_path.py | 2 +- src/trio/_tests/test_dtls.py | 194 ++++++++++++++++++++------------ src/trio/_tests/test_exports.py | 4 +- src/trio/_tests/test_util.py | 8 +- test-requirements.txt | 53 +++++---- 13 files changed, 188 insertions(+), 128 deletions(-) create mode 100644 newsfragments/3277.bugfix.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3112804e28..f00affc6c6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,9 +24,9 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.11 + rev: v0.11.12 hooks: - - id: ruff + - id: ruff-check types: [file] types_or: [python, pyi, toml] args: ["--show-fixes"] @@ -46,7 +46,7 @@ repos: hooks: - id: sphinx-lint - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.8.0 + rev: v1.9.0 hooks: - id: zizmor - repo: local @@ -66,7 +66,7 @@ repos: additional_dependencies: ["pyyaml"] files: ^(test-requirements\.txt)|(\.pre-commit-config\.yaml)$ - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.7.8 + rev: 0.7.9 hooks: # Compile requirements - id: pip-compile diff --git a/docs-requirements.txt b/docs-requirements.txt index b385b91616..4d79238139 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -16,21 +16,21 @@ cffi==1.17.1 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via # -r docs-requirements.in # cryptography -charset-normalizer==3.4.1 +charset-normalizer==3.4.2 # via requests -click==8.1.8 +click==8.2.1 # via towncrier colorama==0.4.6 ; sys_platform == 'win32' # via # click # sphinx -cryptography==44.0.2 +cryptography==45.0.3 # via pyopenssl docutils==0.21.2 # via # sphinx # sphinx-rtd-theme -exceptiongroup==1.2.2 +exceptiongroup==1.3.0 # via -r docs-requirements.in idna==3.10 # via @@ -55,7 +55,7 @@ pycparser==2.22 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via cffi pygments==2.19.1 # via sphinx -pyopenssl==25.0.0 +pyopenssl==25.1.0 # via -r docs-requirements.in requests==2.32.3 # via sphinx @@ -63,7 +63,7 @@ roman-numerals-py==3.1.0 # via sphinx sniffio==1.3.1 # via -r docs-requirements.in -snowballstemmer==2.2.0 +snowballstemmer==3.0.1 # via sphinx sortedcontainers==2.4.0 # via -r docs-requirements.in @@ -103,6 +103,7 @@ towncrier==24.8.0 typing-extensions==4.13.2 # via # beautifulsoup4 + # exceptiongroup # pyopenssl urllib3==2.4.0 # via requests diff --git a/docs/source/typevars.py b/docs/source/typevars.py index 17115c9298..5cd79aa0fc 100644 --- a/docs/source/typevars.py +++ b/docs/source/typevars.py @@ -98,7 +98,7 @@ def lookup_reference( new_node["reftitle"] = f"{paren}{typevar_type}, {reftitle.lstrip('(')}" # Add a CSS class, for restyling. new_node["classes"].append("typevarref") - return new_node + return new_node # type: ignore[no-any-return] def setup(app: Sphinx) -> None: diff --git a/newsfragments/3277.bugfix.rst b/newsfragments/3277.bugfix.rst new file mode 100644 index 0000000000..321130815a --- /dev/null +++ b/newsfragments/3277.bugfix.rst @@ -0,0 +1 @@ +Ensure that the DTLS server does not mutate SSL context. diff --git a/src/trio/_core/_io_windows.py b/src/trio/_core/_io_windows.py index 148253ab88..a3bcc3c682 100644 --- a/src/trio/_core/_io_windows.py +++ b/src/trio/_core/_io_windows.py @@ -427,7 +427,7 @@ def _afd_helper_handle() -> Handle: @attrs.frozen(slots=False) class CompletionKeyEventInfo: - lpOverlapped: CData + lpOverlapped: CData | int dwNumberOfBytesTransferred: int diff --git a/src/trio/_core/_run.py b/src/trio/_core/_run.py index 5fff0b772f..5644099637 100644 --- a/src/trio/_core/_run.py +++ b/src/trio/_core/_run.py @@ -2836,6 +2836,9 @@ def unrolled_run( next_send = task._next_send task._next_send_fn = task._next_send = None final_outcome: Outcome[object] | None = None + + assert next_send_fn is not None + try: # We used to unwrap the Outcome object here and send/throw # its contents in directly, but it turns out that .throw() diff --git a/src/trio/_dtls.py b/src/trio/_dtls.py index a7709632a4..a7dff634d9 100644 --- a/src/trio/_dtls.py +++ b/src/trio/_dtls.py @@ -786,8 +786,7 @@ async def dtls_receive_loop( await stream._resend_final_volley() else: try: - # mypy for some reason cannot determine type of _q - stream._q.s.send_nowait(packet) # type:ignore[has-type] + stream._q.s.send_nowait(packet) except trio.WouldBlock: stream._packets_dropped_in_trio += 1 else: @@ -851,14 +850,6 @@ def __init__( self._packets_dropped_in_trio = 0 self._client_hello = None self._did_handshake = False - # These are mandatory for all DTLS connections. OP_NO_QUERY_MTU is required to - # stop openssl from trying to query the memory BIO's MTU and then breaking, and - # OP_NO_RENEGOTIATION disables renegotiation, which is too complex for us to - # support and isn't useful anyway -- especially for DTLS where it's equivalent - # to just performing a new handshake. - ctx.set_options( - SSL.OP_NO_QUERY_MTU | SSL.OP_NO_RENEGOTIATION, # type: ignore[attr-defined] - ) self._ssl = SSL.Connection(ctx) self._handshake_mtu = 0 # This calls self._ssl.set_ciphertext_mtu, which is important, because if you @@ -1334,6 +1325,7 @@ async def handler(dtls_channel): # We do cookie verification ourselves, so tell OpenSSL not to worry about it. # (See also _inject_client_hello_untrusted.) ssl_context.set_cookie_verify_callback(lambda *_: True) + set_ssl_context_options(ssl_context) try: self._listening_context = ssl_context task_status.started() @@ -1374,6 +1366,7 @@ def connect( # loopback connection), because that can't work # but I don't see how to do it reliably self._check_closed() + set_ssl_context_options(ssl_context) channel = DTLSChannel._create(self, address, ssl_context) channel._ssl.set_connect_state() old_channel = self._streams.get(address) @@ -1381,3 +1374,14 @@ def connect( old_channel._set_replaced() self._streams[address] = channel return channel + + +def set_ssl_context_options(ctx: SSL.Context) -> None: + # These are mandatory for all DTLS connections. OP_NO_QUERY_MTU is required to + # stop openssl from trying to query the memory BIO's MTU and then breaking, and + # OP_NO_RENEGOTIATION disables renegotiation, which is too complex for us to + # support and isn't useful anyway -- especially for DTLS where it's equivalent + # to just performing a new handshake. + ctx.set_options( + SSL.OP_NO_QUERY_MTU | SSL.OP_NO_RENEGOTIATION, # type: ignore[attr-defined] + ) diff --git a/src/trio/_file_io.py b/src/trio/_file_io.py index 443dcdfb40..3df9b3e443 100644 --- a/src/trio/_file_io.py +++ b/src/trio/_file_io.py @@ -428,7 +428,7 @@ async def open_file( @overload -async def open_file( # type: ignore[explicit-any, misc] # Any usage matches builtins.open(). +async def open_file( # type: ignore[explicit-any] # Any usage matches builtins.open(). file: _OpenFile, mode: str, buffering: int = -1, diff --git a/src/trio/_path.py b/src/trio/_path.py index 75e83a2bef..97642e2078 100644 --- a/src/trio/_path.py +++ b/src/trio/_path.py @@ -183,7 +183,7 @@ async def open( ) -> AsyncIOWrapper[BinaryIO]: ... @overload - async def open( # type: ignore[misc, explicit-any] # Any usage matches builtins.open(). + async def open( # type: ignore[explicit-any] # Any usage matches builtins.open(). self, mode: str, buffering: int = -1, diff --git a/src/trio/_tests/test_dtls.py b/src/trio/_tests/test_dtls.py index f88e87fa58..6363be2e99 100644 --- a/src/trio/_tests/test_dtls.py +++ b/src/trio/_tests/test_dtls.py @@ -30,11 +30,23 @@ ca = trustme.CA() server_cert = ca.issue_cert("example.com") -server_ctx = SSL.Context(SSL.DTLS_METHOD) -server_cert.configure_cert(server_ctx) -client_ctx = SSL.Context(SSL.DTLS_METHOD) -ca.configure_trust(client_ctx) +@pytest.fixture +def server_ctx() -> SSL.Context: + ctx = SSL.Context(SSL.DTLS_METHOD) + server_cert.configure_cert(ctx) + return ctx + + +def client_ctx_fn() -> SSL.Context: + ctx = SSL.Context(SSL.DTLS_METHOD) + ca.configure_trust(ctx) + return ctx + + +@pytest.fixture +def client_ctx() -> SSL.Context: + return client_ctx_fn() parametrize_ipv6 = pytest.mark.parametrize( @@ -54,6 +66,7 @@ def endpoint(**kwargs: int | bool) -> DTLSEndpoint: @asynccontextmanager async def dtls_echo_server( *, + server_ctx: SSL.Context, autocancel: bool = True, mtu: int | None = None, ipv6: bool = False, @@ -92,8 +105,13 @@ async def echo_handler(dtls_channel: DTLSChannel) -> None: @parametrize_ipv6 -async def test_smoke(ipv6: bool) -> None: - async with dtls_echo_server(ipv6=ipv6) as (_server_endpoint, address): +async def test_smoke( + ipv6: bool, server_ctx: SSL.Context, client_ctx: SSL.Context +) -> None: + async with dtls_echo_server(ipv6=ipv6, server_ctx=server_ctx) as ( + _server_endpoint, + address, + ): with endpoint(ipv6=ipv6) as client_endpoint: client_channel = client_endpoint.connect(address, client_ctx) with pytest.raises(trio.NeedHandshakeError): @@ -122,6 +140,7 @@ async def test_smoke(ipv6: bool) -> None: @slow async def test_handshake_over_terrible_network( autojump_clock: trio.testing.MockClock, + server_ctx: SSL.Context, ) -> None: HANDSHAKES = 100 r = random.Random(0) @@ -130,7 +149,7 @@ async def test_handshake_over_terrible_network( # avoid spurious timeouts on slow machines autojump_clock.autojump_threshold = 0.001 - async with dtls_echo_server() as (_, address): + async with dtls_echo_server(server_ctx=server_ctx) as (_, address): async with trio.open_nursery() as nursery: async def route_packet(packet: UDPPacket) -> None: @@ -193,7 +212,7 @@ def route_packet_wrapper(packet: UDPPacket) -> None: print("#" * 80) print("#" * 80) with endpoint() as client_endpoint: - client = client_endpoint.connect(address, client_ctx) + client = client_endpoint.connect(address, client_ctx_fn()) print("client starting do_handshake") await client.do_handshake() print("client finished do_handshake") @@ -208,8 +227,10 @@ def route_packet_wrapper(packet: UDPPacket) -> None: break -async def test_implicit_handshake() -> None: - async with dtls_echo_server() as (_, address): +async def test_implicit_handshake( + server_ctx: SSL.Context, client_ctx: SSL.Context +) -> None: + async with dtls_echo_server(server_ctx=server_ctx) as (_, address): with endpoint() as client_endpoint: client = client_endpoint.connect(address, client_ctx) @@ -218,7 +239,7 @@ async def test_implicit_handshake() -> None: assert await client.receive() == b"xyz" -async def test_full_duplex() -> None: +async def test_full_duplex(server_ctx: SSL.Context, client_ctx: SSL.Context) -> None: # Tests simultaneous send/receive, and also multiple methods implicitly invoking # do_handshake simultaneously. with endpoint() as server_endpoint, endpoint() as client_endpoint: @@ -243,8 +264,10 @@ async def handler(channel: DTLSChannel) -> None: server_nursery.cancel_scope.cancel() -async def test_channel_closing() -> None: - async with dtls_echo_server() as (_, address): +async def test_channel_closing( + server_ctx: SSL.Context, client_ctx: SSL.Context +) -> None: + async with dtls_echo_server(server_ctx=server_ctx) as (_, address): with endpoint() as client_endpoint: client = client_endpoint.connect(address, client_ctx) await client.do_handshake() @@ -261,19 +284,25 @@ async def test_channel_closing() -> None: await client.aclose() -async def test_serve_exits_cleanly_on_close() -> None: - async with dtls_echo_server(autocancel=False) as (server_endpoint, _address): +async def test_serve_exits_cleanly_on_close(server_ctx: SSL.Context) -> None: + async with dtls_echo_server(autocancel=False, server_ctx=server_ctx) as ( + server_endpoint, + _address, + ): server_endpoint.close() # Testing that the nursery exits even without being cancelled # close is idempotent server_endpoint.close() -async def test_client_multiplex() -> None: - async with dtls_echo_server() as (_, address1), dtls_echo_server() as (_, address2): +async def test_client_multiplex(server_ctx: SSL.Context) -> None: + async with ( + dtls_echo_server(server_ctx=server_ctx) as (_, address1), + dtls_echo_server(server_ctx=server_ctx) as (_, address2), + ): with endpoint() as client_endpoint: - client1 = client_endpoint.connect(address1, client_ctx) - client2 = client_endpoint.connect(address2, client_ctx) + client1 = client_endpoint.connect(address1, client_ctx_fn()) + client2 = client_endpoint.connect(address2, client_ctx_fn()) await client1.send(b"abc") await client2.send(b"xyz") @@ -287,7 +316,7 @@ async def test_client_multiplex() -> None: with pytest.raises(trio.ClosedResourceError): await client2.receive() with pytest.raises(trio.ClosedResourceError): - client_endpoint.connect(address1, client_ctx) + client_endpoint.connect(address1, client_ctx_fn()) async def null_handler(_: object) -> None: # pragma: no cover pass @@ -303,7 +332,7 @@ async def test_dtls_over_dgram_only() -> None: DTLSEndpoint(s) -async def test_double_serve() -> None: +async def test_double_serve(server_ctx: SSL.Context) -> None: async def null_handler(_: object) -> None: # pragma: no cover pass @@ -321,7 +350,9 @@ async def null_handler(_: object) -> None: # pragma: no cover nursery.cancel_scope.cancel() -async def test_connect_to_non_server(autojump_clock: trio.abc.Clock) -> None: +async def test_connect_to_non_server( + autojump_clock: trio.abc.Clock, client_ctx: SSL.Context +) -> None: fn = FakeNet() fn.enable() with endpoint() as client1, endpoint() as client2: @@ -333,27 +364,32 @@ async def test_connect_to_non_server(autojump_clock: trio.abc.Clock) -> None: assert cscope.cancelled_caught -async def test_incoming_buffer_overflow(autojump_clock: trio.abc.Clock) -> None: +@pytest.mark.parametrize("buffer_size", [10, 20]) +async def test_incoming_buffer_overflow( + autojump_clock: trio.abc.Clock, + server_ctx: SSL.Context, + client_ctx: SSL.Context, + buffer_size: int, +) -> None: fn = FakeNet() fn.enable() - for buffer_size in [10, 20]: - async with dtls_echo_server() as (_, address): - with endpoint(incoming_packets_buffer=buffer_size) as client_endpoint: - assert client_endpoint.incoming_packets_buffer == buffer_size - client = client_endpoint.connect(address, client_ctx) - for i in range(buffer_size + 15): - await client.send(str(i).encode()) - await trio.sleep(1) - stats = client.statistics() - assert stats.incoming_packets_dropped_in_trio == 15 - for i in range(buffer_size): - assert await client.receive() == str(i).encode() - await client.send(b"buffer clear now") - assert await client.receive() == b"buffer clear now" + async with dtls_echo_server(server_ctx=server_ctx) as (_, address): + with endpoint(incoming_packets_buffer=buffer_size) as client_endpoint: + assert client_endpoint.incoming_packets_buffer == buffer_size + client = client_endpoint.connect(address, client_ctx) + for i in range(buffer_size + 15): + await client.send(str(i).encode()) + await trio.sleep(1) + stats = client.statistics() + assert stats.incoming_packets_dropped_in_trio == 15 + for i in range(buffer_size): + assert await client.receive() == str(i).encode() + await client.send(b"buffer clear now") + assert await client.receive() == b"buffer clear now" async def test_server_socket_doesnt_crash_on_garbage( - autojump_clock: trio.abc.Clock, + autojump_clock: trio.abc.Clock, server_ctx: SSL.Context ) -> None: fn = FakeNet() fn.enable() @@ -448,7 +484,7 @@ async def test_server_socket_doesnt_crash_on_garbage( ), ) - async with dtls_echo_server() as (_, address): + async with dtls_echo_server(server_ctx=server_ctx) as (_, address): with trio.socket.socket(type=trio.socket.SOCK_DGRAM) as sock: for bad_packet in [ b"", @@ -466,7 +502,9 @@ async def test_server_socket_doesnt_crash_on_garbage( await trio.sleep(1) -async def test_invalid_cookie_rejected(autojump_clock: trio.abc.Clock) -> None: +async def test_invalid_cookie_rejected( + autojump_clock: trio.abc.Clock, server_ctx: SSL.Context, client_ctx: SSL.Context +) -> None: fn = FakeNet() fn.enable() @@ -500,7 +538,7 @@ def route_packet(packet: UDPPacket) -> None: fn.route_packet = route_packet # type: ignore[assignment] # TODO: Fix FakeNet typing - async with dtls_echo_server() as (_, address): + async with dtls_echo_server(server_ctx=server_ctx) as (_, address): while True: with endpoint() as client: channel = client.connect(address, client_ctx) @@ -509,7 +547,7 @@ def route_packet(packet: UDPPacket) -> None: async def test_client_cancels_handshake_and_starts_new_one( - autojump_clock: trio.abc.Clock, + autojump_clock: trio.abc.Clock, server_ctx: SSL.Context ) -> None: # if a client disappears during the handshake, and then starts a new handshake from # scratch, then the first handler's channel should fail, and a new handler get @@ -540,12 +578,12 @@ async def handler(channel: DTLSChannel) -> None: print("client: starting first connect") with trio.CancelScope() as connect_cscope: - channel = client.connect(server.socket.getsockname(), client_ctx) + channel = client.connect(server.socket.getsockname(), client_ctx_fn()) await channel.do_handshake() assert connect_cscope.cancelled_caught print("client: starting second connect") - channel = client.connect(server.socket.getsockname(), client_ctx) + channel = client.connect(server.socket.getsockname(), client_ctx_fn()) assert await channel.receive() == b"hello" # Give handlers a chance to finish @@ -553,7 +591,7 @@ async def handler(channel: DTLSChannel) -> None: nursery.cancel_scope.cancel() -async def test_swap_client_server() -> None: +async def test_swap_client_server(server_ctx: SSL.Context) -> None: with endpoint() as a, endpoint() as b: await a.socket.bind(("127.0.0.1", 0)) await b.socket.bind(("127.0.0.1", 0)) @@ -570,11 +608,11 @@ async def crashing_echo_handler(channel: DTLSChannel) -> None: await nursery.start(a.serve, server_ctx, crashing_echo_handler) await nursery.start(b.serve, server_ctx, echo_handler) - b_to_a = b.connect(a.socket.getsockname(), client_ctx) + b_to_a = b.connect(a.socket.getsockname(), client_ctx_fn()) await b_to_a.send(b"b as client") assert await b_to_a.receive() == b"b as client" - a_to_b = a.connect(b.socket.getsockname(), client_ctx) + a_to_b = a.connect(b.socket.getsockname(), client_ctx_fn()) await a_to_b.do_handshake() with pytest.raises(trio.BrokenResourceError): await b_to_a.send(b"association broken") @@ -585,7 +623,9 @@ async def crashing_echo_handler(channel: DTLSChannel) -> None: @slow -async def test_openssl_retransmit_doesnt_break_stuff() -> None: +async def test_openssl_retransmit_doesnt_break_stuff( + server_ctx: SSL.Context, client_ctx: SSL.Context +) -> None: # can't use autojump_clock here, because the point of the test is to wait for # openssl's built-in retransmit timer to expire, which is hard-coded to use # wall-clock time. @@ -610,7 +650,7 @@ def route_packet(packet: UDPPacket) -> None: fn.route_packet = route_packet # type: ignore[assignment] # TODO add type annotations for FakeNet - async with dtls_echo_server() as (server_endpoint, address): + async with dtls_echo_server(server_ctx=server_ctx) as (server_endpoint, address): with endpoint() as client_endpoint: async with trio.open_nursery() as nursery: @@ -638,7 +678,7 @@ async def connecter() -> None: async def test_initial_retransmit_timeout_configuration( - autojump_clock: trio.abc.Clock, + autojump_clock: trio.abc.Clock, server_ctx: SSL.Context ) -> None: fn = FakeNet() fn.enable() @@ -652,20 +692,22 @@ def route_packet(packet: UDPPacket) -> None: else: fn.deliver_packet(packet) - fn.route_packet = route_packet # type: ignore[assignment] # TODO add type annotations for FakeNet + fn.route_packet = route_packet # type: ignore[assignment] # TODO: add type annotations for FakeNet - async with dtls_echo_server() as (_, address): + async with dtls_echo_server(server_ctx=server_ctx) as (_, address): for t in [1, 2, 4]: with endpoint() as client: before = trio.current_time() blackholed = True - channel = client.connect(address, client_ctx) + channel = client.connect(address, client_ctx_fn()) await channel.do_handshake(initial_retransmit_timeout=t) after = trio.current_time() assert after - before == t -async def test_explicit_tiny_mtu_is_respected() -> None: +async def test_explicit_tiny_mtu_is_respected( + server_ctx: SSL.Context, client_ctx: SSL.Context +) -> None: # ClientHello is ~240 bytes, and it can't be fragmented, so our mtu has to # be larger than that. (300 is still smaller than any real network though.) MTU = 300 @@ -681,7 +723,7 @@ def route_packet(packet: UDPPacket) -> None: fn.route_packet = route_packet # type: ignore[assignment] # TODO add type annotations for FakeNet - async with dtls_echo_server(mtu=MTU) as (_server, address): + async with dtls_echo_server(mtu=MTU, server_ctx=server_ctx) as (_server, address): with endpoint() as client: channel = client.connect(address, client_ctx) channel.set_ciphertext_mtu(MTU) @@ -694,6 +736,8 @@ def route_packet(packet: UDPPacket) -> None: async def test_handshake_handles_minimum_network_mtu( ipv6: bool, autojump_clock: trio.abc.Clock, + server_ctx: SSL.Context, + client_ctx: SSL.Context, ) -> None: # Fake network that has the minimum allowable MTU for whatever protocol we're using. fn = FakeNet() @@ -708,12 +752,12 @@ def route_packet(packet: UDPPacket) -> None: print(f"delivering {packet}") fn.deliver_packet(packet) - fn.route_packet = route_packet # type: ignore[assignment] # TODO add type annotations for FakeNet + fn.route_packet = route_packet # type: ignore[assignment] # TODO: add type annotations for FakeNet # See if we can successfully do a handshake -- some of the volleys will get dropped, # and the retransmit logic should detect this and back off the MTU to something # smaller until it succeeds. - async with dtls_echo_server(ipv6=ipv6) as (_, address): + async with dtls_echo_server(ipv6=ipv6, server_ctx=server_ctx) as (_, address): with endpoint(ipv6=ipv6) as client_endpoint: client = client_endpoint.connect(address, client_ctx) # the handshake mtu backoff shouldn't affect the return value from @@ -726,7 +770,7 @@ def route_packet(packet: UDPPacket) -> None: @pytest.mark.filterwarnings("always:unclosed DTLS:ResourceWarning") -async def test_system_task_cleaned_up_on_gc() -> None: +async def test_system_task_cleaned_up_on_gc(client_ctx: SSL.Context) -> None: before_tasks = trio.lowlevel.current_statistics().tasks_living # We put this into a sub-function so that everything automatically becomes garbage @@ -820,14 +864,14 @@ async def test_already_closed_socket_doesnt_crash() -> None: async def test_socket_closed_while_processing_clienthello( - autojump_clock: trio.abc.Clock, + autojump_clock: trio.abc.Clock, server_ctx: SSL.Context, client_ctx: SSL.Context ) -> None: fn = FakeNet() fn.enable() # Check what happens if the socket is discovered to be closed when sending a # HelloVerifyRequest, since that has its own sending logic - async with dtls_echo_server() as (server, address): + async with dtls_echo_server(server_ctx=server_ctx) as (server, address): def route_packet(packet: UDPPacket) -> None: fn.deliver_packet(packet) @@ -842,7 +886,7 @@ def route_packet(packet: UDPPacket) -> None: async def test_association_replaced_while_handshake_running( - autojump_clock: trio.abc.Clock, + autojump_clock: trio.abc.Clock, server_ctx: SSL.Context ) -> None: fn = FakeNet() fn.enable() @@ -850,11 +894,12 @@ async def test_association_replaced_while_handshake_running( def route_packet(packet: UDPPacket) -> None: pass - fn.route_packet = route_packet # type: ignore[assignment] # TODO add type annotations for FakeNet + fn.route_packet = route_packet # type: ignore[assignment] # TODO: add type annotations for FakeNet - async with dtls_echo_server() as (_, address): + async with dtls_echo_server(server_ctx=server_ctx) as (_, address): with endpoint() as client_endpoint: - c1 = client_endpoint.connect(address, client_ctx) + # TODO: should this have the same exact client_ctx? + c1 = client_endpoint.connect(address, client_ctx_fn()) async with trio.open_nursery() as nursery: async def doomed_handshake() -> None: @@ -865,10 +910,12 @@ async def doomed_handshake() -> None: await trio.sleep(10) - client_endpoint.connect(address, client_ctx) + client_endpoint.connect(address, client_ctx_fn()) -async def test_association_replaced_before_handshake_starts() -> None: +async def test_association_replaced_before_handshake_starts( + server_ctx: SSL.Context, +) -> None: fn = FakeNet() fn.enable() @@ -878,25 +925,26 @@ def route_packet(packet: UDPPacket) -> NoReturn: # pragma: no cover fn.route_packet = route_packet # type: ignore[assignment] # TODO add type annotations for FakeNet - async with dtls_echo_server() as (_, address): + async with dtls_echo_server(server_ctx=server_ctx) as (_, address): with endpoint() as client_endpoint: - c1 = client_endpoint.connect(address, client_ctx) - client_endpoint.connect(address, client_ctx) + # TODO: should this use the same client_ctx? + c1 = client_endpoint.connect(address, client_ctx_fn()) + client_endpoint.connect(address, client_ctx_fn()) with pytest.raises(trio.BrokenResourceError): await c1.do_handshake() -async def test_send_to_closed_local_port() -> None: +async def test_send_to_closed_local_port(server_ctx: SSL.Context) -> None: # On Windows, sending a UDP packet to a closed local port can cause a weird # ECONNRESET error later, inside the receive task. Make sure we're handling it # properly. - async with dtls_echo_server() as (_, address): + async with dtls_echo_server(server_ctx=server_ctx) as (_, address): with endpoint() as client_endpoint: async with trio.open_nursery() as nursery: for i in range(1, 10): - channel = client_endpoint.connect(("127.0.0.1", i), client_ctx) + channel = client_endpoint.connect(("127.0.0.1", i), client_ctx_fn()) nursery.start_soon(channel.do_handshake) - channel = client_endpoint.connect(address, client_ctx) + channel = client_endpoint.connect(address, client_ctx_fn()) await channel.send(b"xxx") assert await channel.receive() == b"xxx" nursery.cancel_scope.cancel() diff --git a/src/trio/_tests/test_exports.py b/src/trio/_tests/test_exports.py index 1af78513d7..68f9cbcc31 100644 --- a/src/trio/_tests/test_exports.py +++ b/src/trio/_tests/test_exports.py @@ -12,7 +12,7 @@ import types from pathlib import Path, PurePath from types import ModuleType -from typing import TYPE_CHECKING, Protocol +from typing import TYPE_CHECKING, Any, Protocol import attrs import pytest @@ -301,7 +301,7 @@ def no_hidden(symbols: Iterable[str]) -> set[str]: # skip a bunch of file-system activity (probably can un-memoize?) @functools.lru_cache - def lookup_symbol(symbol: str) -> dict[str, str]: + def lookup_symbol(symbol: str) -> dict[str, Any]: # type: ignore[misc, explicit-any] topname, *modname, name = symbol.split(".") version = next(cache.glob("3.*/")) mod_cache = version / topname diff --git a/src/trio/_tests/test_util.py b/src/trio/_tests/test_util.py index 3a57b9cf21..5aaad267f6 100644 --- a/src/trio/_tests/test_util.py +++ b/src/trio/_tests/test_util.py @@ -248,7 +248,7 @@ def test_fixup_module_metadata() -> None: }, ) # Reference loop is fine. - mod.SomeClass.recursion = mod.SomeClass # type: ignore[attr-defined] + mod.SomeClass.recursion = mod.SomeClass fixup_module_metadata("trio.somemodule", vars(mod)) assert mod.some_func.__name__ == "some_func" @@ -264,9 +264,9 @@ def test_fixup_module_metadata() -> None: assert mod.only_has_name.__module__ == "trio.somemodule" assert not hasattr(mod.only_has_name, "__qualname__") - assert mod.SomeClass.method.__name__ == "method" # type: ignore[attr-defined] - assert mod.SomeClass.method.__module__ == "trio.somemodule" # type: ignore[attr-defined] - assert mod.SomeClass.method.__qualname__ == "SomeClass.method" # type: ignore[attr-defined] + assert mod.SomeClass.method.__name__ == "method" + assert mod.SomeClass.method.__module__ == "trio.somemodule" + assert mod.SomeClass.method.__qualname__ == "SomeClass.method" # Make coverage happy. non_trio_module.some_func() mod.some_func() diff --git a/test-requirements.txt b/test-requirements.txt index 87036e952d..c62afe66b8 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,7 +6,7 @@ alabaster==1.0.0 ; python_full_version >= '3.10' # via sphinx astor==0.8.1 # via -r test-requirements.in -astroid==3.3.9 +astroid==3.3.10 # via pylint async-generator==1.10 # via -r test-requirements.in @@ -26,9 +26,11 @@ cffi==1.17.1 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # cryptography cfgv==3.4.0 # via pre-commit -charset-normalizer==3.4.1 +charset-normalizer==3.4.2 # via requests -click==8.1.8 ; implementation_name == 'cpython' +click==8.1.8 ; python_full_version < '3.10' and implementation_name == 'cpython' + # via black +click==8.2.1 ; python_full_version >= '3.10' and implementation_name == 'cpython' # via black codespell==2.4.1 # via -r test-requirements.in @@ -38,9 +40,9 @@ colorama==0.4.6 ; sys_platform == 'win32' # pylint # pytest # sphinx -coverage==7.8.0 +coverage==7.8.2 # via -r test-requirements.in -cryptography==44.0.2 +cryptography==45.0.3 # via # -r test-requirements.in # pyopenssl @@ -52,13 +54,13 @@ distlib==0.3.9 # via virtualenv docutils==0.21.2 # via sphinx -exceptiongroup==1.2.2 ; python_full_version < '3.11' +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 +identify==2.6.12 # via pre-commit idna==3.10 # via @@ -81,7 +83,7 @@ markupsafe==3.0.2 # via jinja2 mccabe==0.7.0 # via pylint -mypy==1.15.0 +mypy==1.16.0 # via -r test-requirements.in mypy-extensions==1.1.0 # via @@ -103,14 +105,16 @@ packaging==25.0 # sphinx parso==0.8.4 ; implementation_name == 'cpython' # via jedi -pathspec==0.12.1 ; implementation_name == 'cpython' - # via black -platformdirs==4.3.7 +pathspec==0.12.1 + # via + # black + # mypy +platformdirs==4.3.8 # via # black # pylint # virtualenv -pluggy==1.5.0 +pluggy==1.6.0 # via pytest pre-commit==4.2.0 # via -r test-requirements.in @@ -118,9 +122,9 @@ pycparser==2.22 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via cffi pygments==2.19.1 # via sphinx -pylint==3.3.6 +pylint==3.3.7 # via -r test-requirements.in -pyopenssl==25.0.0 +pyopenssl==25.1.0 # via -r test-requirements.in pyright==1.1.400 # via -r test-requirements.in @@ -132,13 +136,11 @@ requests==2.32.3 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.11.11 +ruff==0.11.12 # via -r test-requirements.in -setuptools==80.1.0 - # via types-setuptools sniffio==1.3.1 # via -r test-requirements.in -snowballstemmer==2.2.0 +snowballstemmer==3.0.1 # via sphinx sortedcontainers==2.4.0 # via -r test-requirements.in @@ -171,32 +173,33 @@ tomlkit==0.13.2 # via pylint trustme==1.2.1 # via -r test-requirements.in -types-cffi==1.17.0.20250326 +types-cffi==1.17.0.20250523 # via # -r test-requirements.in # types-pyopenssl -types-docutils==0.21.0.20241128 +types-docutils==0.21.0.20250526 # via -r test-requirements.in types-pyopenssl==24.1.0.20240722 # via -r test-requirements.in -types-pyyaml==6.0.12.20250326 +types-pyyaml==6.0.12.20250516 # via -r test-requirements.in -types-setuptools==80.0.0.20250429 +types-setuptools==80.9.0.20250529 # via types-cffi typing-extensions==4.13.2 # via # -r test-requirements.in # astroid # black + # exceptiongroup # mypy # pylint # pyopenssl # pyright urllib3==2.4.0 # via requests -uv==0.7.8 +uv==0.7.9 # via -r test-requirements.in -virtualenv==20.30.0 +virtualenv==20.31.2 # via pre-commit -zipp==3.21.0 ; python_full_version < '3.10' +zipp==3.22.0 ; python_full_version < '3.10' # via importlib-metadata From d64c4294adc38ef8e781bf5bb24335ac004d11b5 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Mon, 9 Jun 2025 10:30:28 +0900 Subject: [PATCH 029/111] Remove unnecessary `start_soon` workaround --- src/trio/_highlevel_open_tcp_stream.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/trio/_highlevel_open_tcp_stream.py b/src/trio/_highlevel_open_tcp_stream.py index 11460689b4..1787f4a97e 100644 --- a/src/trio/_highlevel_open_tcp_stream.py +++ b/src/trio/_highlevel_open_tcp_stream.py @@ -375,14 +375,6 @@ async def attempt_connect( # allowing the next target to be tried early attempt_failed = trio.Event() - # workaround to check types until typing of nursery.start_soon improved - if TYPE_CHECKING: - await attempt_connect( - (address_family, socket_type, proto), - addr, - attempt_failed, - ) - nursery.start_soon( attempt_connect, (address_family, socket_type, proto), From ed54f83a75a71c3aa1a84c931938a222d529e966 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Mon, 9 Jun 2025 10:42:29 +0900 Subject: [PATCH 030/111] Start running PyPy 3.11 in CI --- .github/workflows/ci.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72bfb7efd6..1eb28dc516 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -176,6 +176,11 @@ jobs: lsp: '' lsp_extract_file: '' extra_name: '' + - python: 'pypy-3.11' + arch: 'x64' + lsp: '' + lsp_extract_file: '' + extra_name: '' #- python: '3.9' # arch: 'x64' # lsp: 'http://download.pctools.com/mirror/updates/9.0.0.2308-SDavfree-lite_en.exe' @@ -231,7 +236,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['pypy-3.10', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + python: ['pypy-3.10', 'pypy-3.11', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] check_formatting: ['0'] no_test_requirements: ['0'] extra_name: [''] @@ -301,7 +306,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['pypy-3.10', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + python: ['pypy-3.10', 'pypy-3.11', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] continue-on-error: >- ${{ ( From a8577f9961531d2e15be049ec2ba882afa6388d8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 20:33:10 +0000 Subject: [PATCH 031/111] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.11.12 → v0.11.13](https://github.com/astral-sh/ruff-pre-commit/compare/v0.11.12...v0.11.13) - [github.com/adhtruong/mirrors-typos: v1.32.0 → v1.33.1](https://github.com/adhtruong/mirrors-typos/compare/v1.32.0...v1.33.1) - [github.com/astral-sh/uv-pre-commit: 0.7.9 → 0.7.12](https://github.com/astral-sh/uv-pre-commit/compare/0.7.9...0.7.12) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f00affc6c6..64ba91d593 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.12 + rev: v0.11.13 hooks: - id: ruff-check types: [file] @@ -38,7 +38,7 @@ repos: # tomli needed on 3.10. tomllib is available in stdlib on 3.11+ - tomli - repo: https://github.com/adhtruong/mirrors-typos - rev: v1.32.0 + rev: v1.33.1 hooks: - id: typos - repo: https://github.com/sphinx-contrib/sphinx-lint @@ -66,7 +66,7 @@ repos: additional_dependencies: ["pyyaml"] files: ^(test-requirements\.txt)|(\.pre-commit-config\.yaml)$ - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.7.9 + rev: 0.7.12 hooks: # Compile requirements - id: pip-compile From 4c54ac9c2cdfe782a62082bb5eb3ecaccd05f1ab Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 20:33:23 +0000 Subject: [PATCH 032/111] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index c62afe66b8..285f7cd44b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -136,7 +136,7 @@ requests==2.32.3 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.11.12 +ruff==0.11.13 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -197,7 +197,7 @@ typing-extensions==4.13.2 # pyright urllib3==2.4.0 # via requests -uv==0.7.9 +uv==0.7.12 # via -r test-requirements.in virtualenv==20.31.2 # via pre-commit From ea0305104d2c24672b5e2cc47242f82bc40923e2 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 10 Jun 2025 22:37:14 +0900 Subject: [PATCH 033/111] Use CFFI's out-of-line mode for import time reductions on Windows (#3263) --- .gitattributes | 2 +- .pre-commit-config.yaml | 7 + newsfragments/3263.bugfix.rst | 1 + pyproject.toml | 4 + src/trio/_core/_generated_windows_ffi.py | 10 + src/trio/_core/_io_windows.py | 4 +- src/trio/_core/_tests/test_windows.py | 8 +- src/trio/_core/_windows_cffi.py | 226 +---------------------- src/trio/_tools/windows_ffi_build.py | 220 ++++++++++++++++++++++ 9 files changed, 262 insertions(+), 220 deletions(-) create mode 100644 newsfragments/3263.bugfix.rst create mode 100644 src/trio/_core/_generated_windows_ffi.py create mode 100644 src/trio/_tools/windows_ffi_build.py diff --git a/.gitattributes b/.gitattributes index 991065e069..f53506170f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ -# For files generated by trio/_tools/gen_exports.py +# For files generated by trio/_tools/gen_exports.py or trio/_tools/windows_ffi_build.py trio/_core/_generated* linguist-generated=true # Treat generated files as binary in git diff trio/_core/_generated* -diff diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 64ba91d593..a356f0129d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -58,6 +58,13 @@ repos: pass_filenames: false additional_dependencies: ["astor", "attrs", "black", "ruff"] files: ^src\/trio\/_core\/(_run|(_i(o_(common|epoll|kqueue|windows)|nstrumentation)))\.py$ + - id: regenerate-windows-cffi + name: regenerate windows CFFI + language: python + entry: python src/trio/_tools/windows_ffi_build.py + pass_filenames: false + additional_dependencies: ["cffi"] + files: ^src\/trio\/_tools\/windows_ffi_build\.py$ - id: sync-test-requirements name: synchronize test requirements language: python diff --git a/newsfragments/3263.bugfix.rst b/newsfragments/3263.bugfix.rst new file mode 100644 index 0000000000..db9e8f770c --- /dev/null +++ b/newsfragments/3263.bugfix.rst @@ -0,0 +1 @@ +Decrease import time on Windows by around 10%. diff --git a/pyproject.toml b/pyproject.toml index d7076de7bf..2e88ed3e4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,6 +78,7 @@ force-exclude = ''' ( ^/docs/source/reference-.* | ^/docs/source/tutorial + | ^/src/trio/_core/_generated_windows_ffi.py ) ''' @@ -110,6 +111,7 @@ include = ["*.py", "*.pyi", "**/pyproject.toml"] extend-exclude = [ "docs/source/reference-*", "docs/source/tutorial/*", + "src/trio/_core/_generated_windows_ffi.py" ] [tool.ruff.lint] @@ -309,6 +311,8 @@ omit = [ "*/type_tests/*", # Script used to check type completeness that isn't run in tests "*/trio/_tests/check_type_completeness.py", + # Script to generate a CFFI interface for the Windows kernel + "*/trio/_tools/windows_ffi_build.py", ] # The test suite spawns subprocesses to test some stuff, so make sure # this doesn't corrupt the coverage files diff --git a/src/trio/_core/_generated_windows_ffi.py b/src/trio/_core/_generated_windows_ffi.py new file mode 100644 index 0000000000..0178993e37 --- /dev/null +++ b/src/trio/_core/_generated_windows_ffi.py @@ -0,0 +1,10 @@ +# auto-generated file +import _cffi_backend + +ffi = _cffi_backend.FFI('trio._core._generated_windows_ffi', + _version = 0x2601, + _types = b'\x00\x00\x39\x0D\x00\x00\x1A\x01\x00\x00\x0A\x01\x00\x00\x72\x03\x00\x00\x0A\x01\x00\x00\x03\x11\x00\x00\x0A\x01\x00\x00\x02\x03\x00\x00\x6D\x03\x00\x00\x03\x11\x00\x00\x00\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x00\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x04\x01\x00\x00\x00\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x0A\x01\x00\x00\x03\x11\x00\x00\x0A\x01\x00\x00\x03\x11\x00\x00\x0A\x01\x00\x00\x07\x11\x00\x00\x08\x11\x00\x00\x00\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x03\x11\x00\x00\x0A\x01\x00\x00\x07\x11\x00\x00\x08\x11\x00\x00\x00\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x72\x03\x00\x00\x0A\x01\x00\x00\x07\x11\x00\x00\x08\x11\x00\x00\x00\x0F\x00\x00\x39\x0D\x00\x00\x00\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x02\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x08\x11\x00\x00\x02\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x6E\x03\x00\x00\x0A\x01\x00\x00\x07\x11\x00\x00\x0A\x01\x00\x00\x07\x01\x00\x00\x02\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x07\x01\x00\x00\x02\x0F\x00\x00\x39\x0D\x00\x00\x03\x11\x00\x00\x0A\x01\x00\x00\x1A\x01\x00\x00\x08\x11\x00\x00\x02\x0F\x00\x00\x02\x0D\x00\x00\x08\x01\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x0A\x01\x00\x00\x03\x03\x00\x00\x07\x01\x00\x00\x0A\x01\x00\x00\x00\x0F\x00\x00\x02\x0D\x00\x00\x03\x11\x00\x00\x0A\x01\x00\x00\x00\x0F\x00\x00\x03\x0D\x00\x00\x03\x11\x00\x00\x07\x01\x00\x00\x07\x01\x00\x00\x03\x11\x00\x00\x00\x0F\x00\x00\x03\x0D\x00\x00\x73\x03\x00\x00\x0A\x01\x00\x00\x0A\x01\x00\x00\x03\x11\x00\x00\x0A\x01\x00\x00\x0A\x01\x00\x00\x03\x11\x00\x00\x00\x0F\x00\x00\x03\x0D\x00\x00\x03\x11\x00\x00\x03\x11\x00\x00\x1A\x01\x00\x00\x0A\x01\x00\x00\x02\x0F\x00\x00\x68\x03\x00\x00\x02\x09\x00\x00\x68\x05\x00\x00\x00\x01\x00\x00\x6C\x03\x00\x00\x03\x09\x00\x00\x04\x09\x00\x00\x05\x09\x00\x00\x17\x01\x00\x00\x01\x09\x00\x00\x00\x09\x00\x00\x00\x01\x00\x00\x10\x01', + _globals = (b'\x00\x00\x2F\x23CancelIoEx',0,b'\x00\x00\x2C\x23CloseHandle',0,b'\x00\x00\x52\x23CreateEventA',0,b'\x00\x00\x58\x23CreateFileW',0,b'\x00\x00\x61\x23CreateIoCompletionPort',0,b'\x00\x00\x12\x23DeviceIoControl',0,b'\x00\x00\x33\x23GetQueuedCompletionStatusEx',0,b'\x00\x00\x3F\x23PostQueuedCompletionStatus',0,b'\x00\x00\x1C\x23ReadFile',0,b'\x00\x00\x0B\x23ResetEvent',0,b'\x00\x00\x45\x23RtlNtStatusToDosError',0,b'\x00\x00\x3B\x23SetConsoleCtrlHandler',0,b'\x00\x00\x0B\x23SetEvent',0,b'\x00\x00\x0E\x23SetFileCompletionNotificationModes',0,b'\x00\x00\x2A\x23WSAGetLastError',0,b'\x00\x00\x00\x23WSAIoctl',0,b'\x00\x00\x48\x23WaitForMultipleObjects',0,b'\x00\x00\x4E\x23WaitForSingleObject',0,b'\x00\x00\x23\x23WriteFile',0), + _struct_unions = ((b'\x00\x00\x00\x71\x00\x00\x00\x03$1',b'\x00\x00\x70\x11DUMMYSTRUCTNAME',b'\x00\x00\x03\x11Pointer'),(b'\x00\x00\x00\x70\x00\x00\x00\x02$2',b'\x00\x00\x02\x11Offset',b'\x00\x00\x02\x11OffsetHigh'),(b'\x00\x00\x00\x68\x00\x00\x00\x02_AFD_POLL_HANDLE_INFO',b'\x00\x00\x03\x11Handle',b'\x00\x00\x02\x11Events',b'\x00\x00\x46\x11Status'),(b'\x00\x00\x00\x6C\x00\x00\x00\x02_AFD_POLL_INFO',b'\x00\x00\x6F\x11Timeout',b'\x00\x00\x02\x11NumberOfHandles',b'\x00\x00\x02\x11Exclusive',b'\x00\x00\x69\x11Handles'),(b'\x00\x00\x00\x6D\x00\x00\x00\x02_OVERLAPPED',b'\x00\x00\x01\x11Internal',b'\x00\x00\x01\x11InternalHigh',b'\x00\x00\x71\x11DUMMYUNIONNAME',b'\x00\x00\x03\x11hEvent'),(b'\x00\x00\x00\x6E\x00\x00\x00\x02_OVERLAPPED_ENTRY',b'\x00\x00\x01\x11lpCompletionKey',b'\x00\x00\x08\x11lpOverlapped',b'\x00\x00\x01\x11Internal',b'\x00\x00\x02\x11dwNumberOfBytesTransferred')), + _typenames = (b'\x00\x00\x00\x68AFD_POLL_HANDLE_INFO',b'\x00\x00\x00\x6CAFD_POLL_INFO',b'\x00\x00\x00\x39BOOL',b'\x00\x00\x00\x10BOOLEAN',b'\x00\x00\x00\x10BYTE',b'\x00\x00\x00\x02DWORD',b'\x00\x00\x00\x03HANDLE',b'\x00\x00\x00\x6FLARGE_INTEGER',b'\x00\x00\x00\x03LPCSTR',b'\x00\x00\x00\x25LPCVOID',b'\x00\x00\x00\x59LPCWSTR',b'\x00\x00\x00\x07LPDWORD',b'\x00\x00\x00\x08LPOVERLAPPED',b'\x00\x00\x00\x35LPOVERLAPPED_ENTRY',b'\x00\x00\x00\x03LPSECURITY_ATTRIBUTES',b'\x00\x00\x00\x03LPVOID',b'\x00\x00\x00\x08LPWSAOVERLAPPED',b'\x00\x00\x00\x46NTSTATUS',b'\x00\x00\x00\x6DOVERLAPPED',b'\x00\x00\x00\x6EOVERLAPPED_ENTRY',b'\x00\x00\x00\x67PAFD_POLL_HANDLE_INFO',b'\x00\x00\x00\x6BPAFD_POLL_INFO',b'\x00\x00\x00\x07PULONG',b'\x00\x00\x00\x03PVOID',b'\x00\x00\x00\x01SOCKET',b'\x00\x00\x00\x10UCHAR',b'\x00\x00\x00\x01UINT_PTR',b'\x00\x00\x00\x02ULONG',b'\x00\x00\x00\x01ULONG_PTR',b'\x00\x00\x00\x6DWSAOVERLAPPED',b'\x00\x00\x00\x02u_long'), +) diff --git a/src/trio/_core/_io_windows.py b/src/trio/_core/_io_windows.py index a3bcc3c682..9a9d6b9cc4 100644 --- a/src/trio/_core/_io_windows.py +++ b/src/trio/_core/_io_windows.py @@ -714,9 +714,9 @@ def _refresh_afd(self, base_handle: Handle) -> None: kernel32.DeviceIoControl( afd_group.handle, IoControlCodes.IOCTL_AFD_POLL, - cast("CType", poll_info), + cast("CType", poll_info), # type: ignore[arg-type] ffi.sizeof("AFD_POLL_INFO"), - cast("CType", poll_info), + cast("CType", poll_info), # type: ignore[arg-type] ffi.sizeof("AFD_POLL_INFO"), ffi.NULL, lpOverlapped, diff --git a/src/trio/_core/_tests/test_windows.py b/src/trio/_core/_tests/test_windows.py index e4a1bab615..df24dd3614 100644 --- a/src/trio/_core/_tests/test_windows.py +++ b/src/trio/_core/_tests/test_windows.py @@ -37,8 +37,14 @@ def test_winerror(monkeypatch: pytest.MonkeyPatch) -> None: + # this is unfortunately needed as the generated ffi is read-only + class FFIWrapper: + def getwinerror(self) -> None: + raise NotImplementedError("this is a fake implementation") + mock = create_autospec(ffi.getwinerror) - monkeypatch.setattr(ffi, "getwinerror", mock) + monkeypatch.setattr("trio._core._windows_cffi.ffi", FFIWrapper) + monkeypatch.setattr("trio._core._windows_cffi.ffi.getwinerror", mock) # Returning none = no error, should not happen. mock.return_value = None diff --git a/src/trio/_core/_windows_cffi.py b/src/trio/_core/_windows_cffi.py index 4aef8ddc83..0e3c0b10b3 100644 --- a/src/trio/_core/_windows_cffi.py +++ b/src/trio/_core/_windows_cffi.py @@ -1,232 +1,26 @@ from __future__ import annotations import enum -import re from typing import TYPE_CHECKING, NewType, NoReturn, Protocol, cast if TYPE_CHECKING: + import cffi from typing_extensions import TypeAlias -import cffi + CData: TypeAlias = cffi.api.FFI.CData + CType: TypeAlias = cffi.api.FFI.CType + +from ._generated_windows_ffi import ffi ################################################################ # Functions and types ################################################################ -LIB = """ -// https://msdn.microsoft.com/en-us/library/windows/desktop/aa383751(v=vs.85).aspx -typedef int BOOL; -typedef unsigned char BYTE; -typedef BYTE BOOLEAN; -typedef void* PVOID; -typedef PVOID HANDLE; -typedef unsigned long DWORD; -typedef unsigned long ULONG; -typedef unsigned int NTSTATUS; -typedef unsigned long u_long; -typedef ULONG *PULONG; -typedef const void *LPCVOID; -typedef void *LPVOID; -typedef const wchar_t *LPCWSTR; - -typedef uintptr_t ULONG_PTR; -typedef uintptr_t UINT_PTR; - -typedef UINT_PTR SOCKET; - -typedef struct _OVERLAPPED { - ULONG_PTR Internal; - ULONG_PTR InternalHigh; - union { - struct { - DWORD Offset; - DWORD OffsetHigh; - } DUMMYSTRUCTNAME; - PVOID Pointer; - } DUMMYUNIONNAME; - - HANDLE hEvent; -} OVERLAPPED, *LPOVERLAPPED; - -typedef OVERLAPPED WSAOVERLAPPED; -typedef LPOVERLAPPED LPWSAOVERLAPPED; -typedef PVOID LPSECURITY_ATTRIBUTES; -typedef PVOID LPCSTR; - -typedef struct _OVERLAPPED_ENTRY { - ULONG_PTR lpCompletionKey; - LPOVERLAPPED lpOverlapped; - ULONG_PTR Internal; - DWORD dwNumberOfBytesTransferred; -} OVERLAPPED_ENTRY, *LPOVERLAPPED_ENTRY; - -// kernel32.dll -HANDLE WINAPI CreateIoCompletionPort( - _In_ HANDLE FileHandle, - _In_opt_ HANDLE ExistingCompletionPort, - _In_ ULONG_PTR CompletionKey, - _In_ DWORD NumberOfConcurrentThreads -); - -BOOL SetFileCompletionNotificationModes( - HANDLE FileHandle, - UCHAR Flags -); - -HANDLE CreateFileW( - LPCWSTR lpFileName, - DWORD dwDesiredAccess, - DWORD dwShareMode, - LPSECURITY_ATTRIBUTES lpSecurityAttributes, - DWORD dwCreationDisposition, - DWORD dwFlagsAndAttributes, - HANDLE hTemplateFile -); - -BOOL WINAPI CloseHandle( - _In_ HANDLE hObject -); - -BOOL WINAPI PostQueuedCompletionStatus( - _In_ HANDLE CompletionPort, - _In_ DWORD dwNumberOfBytesTransferred, - _In_ ULONG_PTR dwCompletionKey, - _In_opt_ LPOVERLAPPED lpOverlapped -); - -BOOL WINAPI GetQueuedCompletionStatusEx( - _In_ HANDLE CompletionPort, - _Out_ LPOVERLAPPED_ENTRY lpCompletionPortEntries, - _In_ ULONG ulCount, - _Out_ PULONG ulNumEntriesRemoved, - _In_ DWORD dwMilliseconds, - _In_ BOOL fAlertable -); - -BOOL WINAPI CancelIoEx( - _In_ HANDLE hFile, - _In_opt_ LPOVERLAPPED lpOverlapped -); - -BOOL WriteFile( - HANDLE hFile, - LPCVOID lpBuffer, - DWORD nNumberOfBytesToWrite, - LPDWORD lpNumberOfBytesWritten, - LPOVERLAPPED lpOverlapped -); - -BOOL ReadFile( - HANDLE hFile, - LPVOID lpBuffer, - DWORD nNumberOfBytesToRead, - LPDWORD lpNumberOfBytesRead, - LPOVERLAPPED lpOverlapped -); - -BOOL WINAPI SetConsoleCtrlHandler( - _In_opt_ void* HandlerRoutine, - _In_ BOOL Add -); - -HANDLE CreateEventA( - LPSECURITY_ATTRIBUTES lpEventAttributes, - BOOL bManualReset, - BOOL bInitialState, - LPCSTR lpName -); - -BOOL SetEvent( - HANDLE hEvent -); - -BOOL ResetEvent( - HANDLE hEvent -); - -DWORD WaitForSingleObject( - HANDLE hHandle, - DWORD dwMilliseconds -); - -DWORD WaitForMultipleObjects( - DWORD nCount, - HANDLE *lpHandles, - BOOL bWaitAll, - DWORD dwMilliseconds -); - -ULONG RtlNtStatusToDosError( - NTSTATUS Status -); - -int WSAIoctl( - SOCKET s, - DWORD dwIoControlCode, - LPVOID lpvInBuffer, - DWORD cbInBuffer, - LPVOID lpvOutBuffer, - DWORD cbOutBuffer, - LPDWORD lpcbBytesReturned, - LPWSAOVERLAPPED lpOverlapped, - // actually LPWSAOVERLAPPED_COMPLETION_ROUTINE - void* lpCompletionRoutine -); - -int WSAGetLastError(); - -BOOL DeviceIoControl( - HANDLE hDevice, - DWORD dwIoControlCode, - LPVOID lpInBuffer, - DWORD nInBufferSize, - LPVOID lpOutBuffer, - DWORD nOutBufferSize, - LPDWORD lpBytesReturned, - LPOVERLAPPED lpOverlapped -); - -// From https://github.com/piscisaureus/wepoll/blob/master/src/afd.h -typedef struct _AFD_POLL_HANDLE_INFO { - HANDLE Handle; - ULONG Events; - NTSTATUS Status; -} AFD_POLL_HANDLE_INFO, *PAFD_POLL_HANDLE_INFO; - -// This is really defined as a messy union to allow stuff like -// i.DUMMYSTRUCTNAME.LowPart, but we don't need those complications. -// Under all that it's just an int64. -typedef int64_t LARGE_INTEGER; - -typedef struct _AFD_POLL_INFO { - LARGE_INTEGER Timeout; - ULONG NumberOfHandles; - ULONG Exclusive; - AFD_POLL_HANDLE_INFO Handles[1]; -} AFD_POLL_INFO, *PAFD_POLL_INFO; - -""" - -# cribbed from pywincffi -# programmatically strips out those annotations MSDN likes, like _In_ -REGEX_SAL_ANNOTATION = re.compile( - r"\b(_In_|_Inout_|_Out_|_Outptr_|_Reserved_)(opt_)?\b", -) -LIB = REGEX_SAL_ANNOTATION.sub(" ", LIB) - -# Other fixups: -# - get rid of FAR, cffi doesn't like it -LIB = re.sub(r"\bFAR\b", " ", LIB) -# - PASCAL is apparently an alias for __stdcall (on modern compilers - modern -# being _MSC_VER >= 800) -LIB = re.sub(r"\bPASCAL\b", "__stdcall", LIB) - -ffi = cffi.api.FFI() -ffi.cdef(LIB) - -CData: TypeAlias = cffi.api.FFI.CData -CType: TypeAlias = cffi.api.FFI.CType -AlwaysNull: TypeAlias = CType # We currently always pass ffi.NULL here. +if not TYPE_CHECKING: + CData: TypeAlias = ffi.CData + CType: TypeAlias = ffi.CType + +AlwaysNull: TypeAlias = CData # We currently always pass ffi.NULL here. Handle = NewType("Handle", CData) HandleArray = NewType("HandleArray", CData) diff --git a/src/trio/_tools/windows_ffi_build.py b/src/trio/_tools/windows_ffi_build.py new file mode 100644 index 0000000000..a9a3941087 --- /dev/null +++ b/src/trio/_tools/windows_ffi_build.py @@ -0,0 +1,220 @@ +# builder for CFFI out-of-line mode, for reduced import time. +# run this to generate `trio._core._generated_windows_ffi`. +import re + +import cffi + +LIB = """ +// https://msdn.microsoft.com/en-us/library/windows/desktop/aa383751(v=vs.85).aspx +typedef int BOOL; +typedef unsigned char BYTE; +typedef unsigned char UCHAR; +typedef BYTE BOOLEAN; +typedef void* PVOID; +typedef PVOID HANDLE; +typedef unsigned long DWORD; +typedef unsigned long ULONG; +typedef unsigned int NTSTATUS; +typedef unsigned long u_long; +typedef ULONG *PULONG; +typedef const void *LPCVOID; +typedef void *LPVOID; +typedef const wchar_t *LPCWSTR; +typedef DWORD* LPDWORD; + +typedef uintptr_t ULONG_PTR; +typedef uintptr_t UINT_PTR; + +typedef UINT_PTR SOCKET; + +typedef struct _OVERLAPPED { + ULONG_PTR Internal; + ULONG_PTR InternalHigh; + union { + struct { + DWORD Offset; + DWORD OffsetHigh; + } DUMMYSTRUCTNAME; + PVOID Pointer; + } DUMMYUNIONNAME; + + HANDLE hEvent; +} OVERLAPPED, *LPOVERLAPPED; + +typedef OVERLAPPED WSAOVERLAPPED; +typedef LPOVERLAPPED LPWSAOVERLAPPED; +typedef PVOID LPSECURITY_ATTRIBUTES; +typedef PVOID LPCSTR; + +typedef struct _OVERLAPPED_ENTRY { + ULONG_PTR lpCompletionKey; + LPOVERLAPPED lpOverlapped; + ULONG_PTR Internal; + DWORD dwNumberOfBytesTransferred; +} OVERLAPPED_ENTRY, *LPOVERLAPPED_ENTRY; + +// kernel32.dll +HANDLE WINAPI CreateIoCompletionPort( + _In_ HANDLE FileHandle, + _In_opt_ HANDLE ExistingCompletionPort, + _In_ ULONG_PTR CompletionKey, + _In_ DWORD NumberOfConcurrentThreads +); + +BOOL SetFileCompletionNotificationModes( + HANDLE FileHandle, + UCHAR Flags +); + +HANDLE CreateFileW( + LPCWSTR lpFileName, + DWORD dwDesiredAccess, + DWORD dwShareMode, + LPSECURITY_ATTRIBUTES lpSecurityAttributes, + DWORD dwCreationDisposition, + DWORD dwFlagsAndAttributes, + HANDLE hTemplateFile +); + +BOOL WINAPI CloseHandle( + _In_ HANDLE hObject +); + +BOOL WINAPI PostQueuedCompletionStatus( + _In_ HANDLE CompletionPort, + _In_ DWORD dwNumberOfBytesTransferred, + _In_ ULONG_PTR dwCompletionKey, + _In_opt_ LPOVERLAPPED lpOverlapped +); + +BOOL WINAPI GetQueuedCompletionStatusEx( + _In_ HANDLE CompletionPort, + _Out_ LPOVERLAPPED_ENTRY lpCompletionPortEntries, + _In_ ULONG ulCount, + _Out_ PULONG ulNumEntriesRemoved, + _In_ DWORD dwMilliseconds, + _In_ BOOL fAlertable +); + +BOOL WINAPI CancelIoEx( + _In_ HANDLE hFile, + _In_opt_ LPOVERLAPPED lpOverlapped +); + +BOOL WriteFile( + HANDLE hFile, + LPCVOID lpBuffer, + DWORD nNumberOfBytesToWrite, + LPDWORD lpNumberOfBytesWritten, + LPOVERLAPPED lpOverlapped +); + +BOOL ReadFile( + HANDLE hFile, + LPVOID lpBuffer, + DWORD nNumberOfBytesToRead, + LPDWORD lpNumberOfBytesRead, + LPOVERLAPPED lpOverlapped +); + +BOOL WINAPI SetConsoleCtrlHandler( + _In_opt_ void* HandlerRoutine, + _In_ BOOL Add +); + +HANDLE CreateEventA( + LPSECURITY_ATTRIBUTES lpEventAttributes, + BOOL bManualReset, + BOOL bInitialState, + LPCSTR lpName +); + +BOOL SetEvent( + HANDLE hEvent +); + +BOOL ResetEvent( + HANDLE hEvent +); + +DWORD WaitForSingleObject( + HANDLE hHandle, + DWORD dwMilliseconds +); + +DWORD WaitForMultipleObjects( + DWORD nCount, + HANDLE *lpHandles, + BOOL bWaitAll, + DWORD dwMilliseconds +); + +ULONG RtlNtStatusToDosError( + NTSTATUS Status +); + +int WSAIoctl( + SOCKET s, + DWORD dwIoControlCode, + LPVOID lpvInBuffer, + DWORD cbInBuffer, + LPVOID lpvOutBuffer, + DWORD cbOutBuffer, + LPDWORD lpcbBytesReturned, + LPWSAOVERLAPPED lpOverlapped, + // actually LPWSAOVERLAPPED_COMPLETION_ROUTINE + void* lpCompletionRoutine +); + +int WSAGetLastError(); + +BOOL DeviceIoControl( + HANDLE hDevice, + DWORD dwIoControlCode, + LPVOID lpInBuffer, + DWORD nInBufferSize, + LPVOID lpOutBuffer, + DWORD nOutBufferSize, + LPDWORD lpBytesReturned, + LPOVERLAPPED lpOverlapped +); + +// From https://github.com/piscisaureus/wepoll/blob/master/src/afd.h +typedef struct _AFD_POLL_HANDLE_INFO { + HANDLE Handle; + ULONG Events; + NTSTATUS Status; +} AFD_POLL_HANDLE_INFO, *PAFD_POLL_HANDLE_INFO; + +// This is really defined as a messy union to allow stuff like +// i.DUMMYSTRUCTNAME.LowPart, but we don't need those complications. +// Under all that it's just an int64. +typedef int64_t LARGE_INTEGER; + +typedef struct _AFD_POLL_INFO { + LARGE_INTEGER Timeout; + ULONG NumberOfHandles; + ULONG Exclusive; + AFD_POLL_HANDLE_INFO Handles[1]; +} AFD_POLL_INFO, *PAFD_POLL_INFO; + +""" + +# cribbed from pywincffi +# programmatically strips out those annotations MSDN likes, like _In_ +LIB = re.sub(r"\b(_In_|_Inout_|_Out_|_Outptr_|_Reserved_)(opt_)?\b", " ", LIB) + +# Other fixups: +# - get rid of FAR, cffi doesn't like it +LIB = re.sub(r"\bFAR\b", " ", LIB) +# - PASCAL is apparently an alias for __stdcall (on modern compilers - modern +# being _MSC_VER >= 800) +LIB = re.sub(r"\bPASCAL\b", "__stdcall", LIB) + +ffibuilder = cffi.FFI() +# a bit hacky but, it works +ffibuilder.set_source("trio._core._generated_windows_ffi", None) +ffibuilder.cdef(LIB) + +if __name__ == "__main__": + ffibuilder.compile("src") From 154deeb02cbaa9cdd362cf7f7dc1abcca9f93c71 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 10 Jun 2025 22:38:59 +0900 Subject: [PATCH 034/111] Update tox.ini to add PyPy 3.11 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index c8e848cb1b..6b1851f4de 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{39,310,311,312,313,314,py310} +envlist = py{39,310,311,312,313,314,py310,py311} labels = check = typing, gen_exports, type_completeness, pip_compile cython = py39-cython2,py39-cython,py311-cython2,py313-cython From aa95d4c702ec2fa000a18be8c8e973cb6ba0d6fe Mon Sep 17 00:00:00 2001 From: A5rocks Date: Wed, 11 Jun 2025 11:20:54 -0400 Subject: [PATCH 035/111] Switch from installing in `ci.sh` to GHA --- .github/workflows/ci.yml | 4 ++++ ci.sh | 5 ----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1eb28dc516..2baf0d38d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -281,6 +281,10 @@ jobs: run: python -m pip install tox && tox -m check + - name: Install python3-apport + run: + sudo apt update + sudo apt install -q python3-apport - name: Run tests if: matrix.check_formatting == '0' run: ./ci.sh diff --git a/ci.sh b/ci.sh index f414d94a2c..8b23ad279f 100755 --- a/ci.sh +++ b/ci.sh @@ -58,11 +58,6 @@ else flags="" fi -# So we can run the test for our apport/excepthook interaction working -if [ -e /etc/lsb-release ] && grep -q Ubuntu /etc/lsb-release; then - sudo apt install -q python3-apport -fi - # If we're testing with a LSP installed, then it might break network # stuff, so wait until after we've finished setting everything else # up. From 1cc8f523bc0dfdd604771efc1baeb08d08696dae Mon Sep 17 00:00:00 2001 From: A5rocks Date: Wed, 11 Jun 2025 11:25:46 -0400 Subject: [PATCH 036/111] Actually use a multiline string --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2baf0d38d5..f47e7ab07e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -282,7 +282,7 @@ jobs: python -m pip install tox && tox -m check - name: Install python3-apport - run: + run: | sudo apt update sudo apt install -q python3-apport - name: Run tests From dd87366abfc8def8b0a57a28cb073490effa64a4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 20:08:14 +0000 Subject: [PATCH 037/111] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/uv-pre-commit: 0.7.12 → 0.7.13](https://github.com/astral-sh/uv-pre-commit/compare/0.7.12...0.7.13) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a356f0129d..f67e0f8bc8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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.7.12 + rev: 0.7.13 hooks: # Compile requirements - id: pip-compile From e72c95967df9dba09be0a9e592967c1b81f02ed7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 16 Jun 2025 20:08:43 +0000 Subject: [PATCH 038/111] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 285f7cd44b..b2a761ebde 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -197,7 +197,7 @@ typing-extensions==4.13.2 # pyright urllib3==2.4.0 # via requests -uv==0.7.12 +uv==0.7.13 # via -r test-requirements.in virtualenv==20.31.2 # via pre-commit From 4f54774716a65ed75d584c561133c31663525c6b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:27:40 +0000 Subject: [PATCH 039/111] Dependency updates (#3292) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- docs-requirements.txt | 10 +++---- src/trio/_tests/test_testing_raisesgroup.py | 6 +---- test-requirements.txt | 30 +++++++++++---------- 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f67e0f8bc8..b888acc6b2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.13 + rev: v0.12.0 hooks: - id: ruff-check types: [file] diff --git a/docs-requirements.txt b/docs-requirements.txt index 4d79238139..df5c1e236a 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -10,7 +10,7 @@ babel==2.17.0 # via sphinx beautifulsoup4==4.13.4 # via sphinx-codeautolink -certifi==2025.4.26 +certifi==2025.6.15 # via requests cffi==1.17.1 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via @@ -24,7 +24,7 @@ colorama==0.4.6 ; sys_platform == 'win32' # via # click # sphinx -cryptography==45.0.3 +cryptography==45.0.4 # via pyopenssl docutils==0.21.2 # via @@ -57,7 +57,7 @@ pygments==2.19.1 # via sphinx pyopenssl==25.1.0 # via -r docs-requirements.in -requests==2.32.3 +requests==2.32.4 # via sphinx roman-numerals-py==3.1.0 # via sphinx @@ -100,10 +100,10 @@ sphinxcontrib-trio==1.1.2 # via -r docs-requirements.in towncrier==24.8.0 # via -r docs-requirements.in -typing-extensions==4.13.2 +typing-extensions==4.14.0 # via # beautifulsoup4 # exceptiongroup # pyopenssl -urllib3==2.4.0 +urllib3==2.5.0 # via requests diff --git a/src/trio/_tests/test_testing_raisesgroup.py b/src/trio/_tests/test_testing_raisesgroup.py index b3fe2ae755..9cc9299382 100644 --- a/src/trio/_tests/test_testing_raisesgroup.py +++ b/src/trio/_tests/test_testing_raisesgroup.py @@ -3,7 +3,6 @@ import re import sys from types import TracebackType -from typing import TYPE_CHECKING import pytest @@ -14,9 +13,6 @@ if sys.version_info < (3, 11): from exceptiongroup import BaseExceptionGroup, ExceptionGroup -if TYPE_CHECKING: - from _pytest.python_api import RaisesContext - def wrap_escape(s: str) -> str: return "^" + re.escape(s) + "$" @@ -24,7 +20,7 @@ def wrap_escape(s: str) -> str: def fails_raises_group( msg: str, add_prefix: bool = True -) -> RaisesContext[AssertionError]: +) -> pytest.RaisesExc[AssertionError]: assert ( msg[-1] != "\n" ), "developer error, expected string should not end with newline" diff --git a/test-requirements.txt b/test-requirements.txt index b2a761ebde..fbc9b2eab3 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -18,7 +18,7 @@ babel==2.17.0 # via sphinx black==25.1.0 ; implementation_name == 'cpython' # via -r test-requirements.in -certifi==2025.4.26 +certifi==2025.6.15 # via requests cffi==1.17.1 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via @@ -40,9 +40,9 @@ colorama==0.4.6 ; sys_platform == 'win32' # pylint # pytest # sphinx -coverage==7.8.2 +coverage==7.9.1 # via -r test-requirements.in -cryptography==45.0.3 +cryptography==45.0.4 # via # -r test-requirements.in # pyopenssl @@ -83,7 +83,7 @@ markupsafe==3.0.2 # via jinja2 mccabe==0.7.0 # via pylint -mypy==1.16.0 +mypy==1.16.1 # via -r test-requirements.in mypy-extensions==1.1.0 # via @@ -121,22 +121,24 @@ 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.7 # via -r test-requirements.in pyopenssl==25.1.0 # via -r test-requirements.in -pyright==1.1.400 +pyright==1.1.402 # via -r test-requirements.in -pytest==8.3.5 +pytest==8.4.1 # via -r test-requirements.in pyyaml==6.0.2 # via pre-commit -requests==2.32.3 +requests==2.32.4 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.11.13 +ruff==0.12.0 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -169,7 +171,7 @@ tomli==2.2.1 ; python_full_version < '3.11' # pylint # pytest # sphinx -tomlkit==0.13.2 +tomlkit==0.13.3 # via pylint trustme==1.2.1 # via -r test-requirements.in @@ -177,7 +179,7 @@ types-cffi==1.17.0.20250523 # via # -r test-requirements.in # types-pyopenssl -types-docutils==0.21.0.20250526 +types-docutils==0.21.0.20250604 # via -r test-requirements.in types-pyopenssl==24.1.0.20240722 # via -r test-requirements.in @@ -185,7 +187,7 @@ types-pyyaml==6.0.12.20250516 # via -r test-requirements.in types-setuptools==80.9.0.20250529 # via types-cffi -typing-extensions==4.13.2 +typing-extensions==4.14.0 # via # -r test-requirements.in # astroid @@ -195,11 +197,11 @@ typing-extensions==4.13.2 # pylint # pyopenssl # pyright -urllib3==2.4.0 +urllib3==2.5.0 # via requests uv==0.7.13 # via -r test-requirements.in virtualenv==20.31.2 # via pre-commit -zipp==3.22.0 ; python_full_version < '3.10' +zipp==3.23.0 ; python_full_version < '3.10' # via importlib-metadata From 79f685f14e8048ecc78bd50d3d0c4e3f839d6b9a Mon Sep 17 00:00:00 2001 From: A5rocks Date: Sun, 29 Jun 2025 14:33:06 +0900 Subject: [PATCH 040/111] Allow documentation builds on Windows (#3227) * Stop raising in `_unix_pipes` * Successfully build docs on Windows on CPython 3.13 * Be a bit stricter about what is allowed for Unix-seeing-Windows * Make use of these new capabilities * Appease type checker * Appease codecov * More codecov appeasement * Migrate to a single excluded line --- docs/source/reference-lowlevel.rst | 21 ++++++++++++--------- pyproject.toml | 2 +- src/trio/_channel.py | 2 +- src/trio/_core/__init__.py | 9 +++++++-- src/trio/_core/_run.py | 8 +++++++- src/trio/_highlevel_socket.py | 13 +++++++++++-- src/trio/_path.py | 16 +++++++++++++++- src/trio/_tests/test_unix_pipes.py | 3 --- src/trio/_timeouts.py | 2 +- src/trio/_unix_pipes.py | 5 ----- src/trio/lowlevel.py | 20 ++++++++++++++------ 11 files changed, 69 insertions(+), 32 deletions(-) diff --git a/docs/source/reference-lowlevel.rst b/docs/source/reference-lowlevel.rst index 82bd8537d9..7a39180e02 100644 --- a/docs/source/reference-lowlevel.rst +++ b/docs/source/reference-lowlevel.rst @@ -274,18 +274,21 @@ TODO: these are implemented, but are currently more of a sketch than anything real. See `#26 `__. -.. function:: current_kqueue() +.. autofunction:: current_kqueue() -.. function:: wait_kevent(ident, filter, abort_func) +.. autofunction:: wait_kevent(ident, filter, abort_func) :async: -.. function:: monitor_kevent(ident, filter) +.. autofunction:: monitor_kevent(ident, filter) :with: queue Windows-specific API -------------------- +.. note: this is a function and not `autofunction` since it relies on cffi + compiling some things. + .. function:: WaitForSingleObject(handle) :async: @@ -304,20 +307,20 @@ anything real. See `#26 `__ and `#52 `__. -.. function:: register_with_iocp(handle) +.. autofunction:: register_with_iocp(handle) -.. function:: wait_overlapped(handle, lpOverlapped) +.. autofunction:: wait_overlapped(handle, lpOverlapped) :async: -.. function:: write_overlapped(handle, data) +.. autofunction:: write_overlapped(handle, data) :async: -.. function:: readinto_overlapped(handle, data) +.. autofunction:: readinto_overlapped(handle, data) :async: -.. function:: current_iocp() +.. autofunction:: current_iocp() -.. function:: monitor_completion_key() +.. autofunction:: monitor_completion_key() :with: queue diff --git a/pyproject.toml b/pyproject.toml index 2e88ed3e4d..f0ba3610c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -335,7 +335,7 @@ exclude_also = [ "@overload", 'class .*\bProtocol\b.*\):', "raise NotImplementedError", - '.*if "sphinx" in sys.modules:', + '.*if "sphinx.ext.autodoc" in sys.modules:', 'TODO: test this line', 'if __name__ == "__main__":', ] diff --git a/src/trio/_channel.py b/src/trio/_channel.py index 54b5ea4bea..9caa7f7699 100644 --- a/src/trio/_channel.py +++ b/src/trio/_channel.py @@ -35,7 +35,7 @@ from typing_extensions import ParamSpec, Self P = ParamSpec("P") -elif "sphinx" in sys.modules: +elif "sphinx.ext.autodoc" in sys.modules: # P needs to exist for Sphinx to parse the type hints successfully. try: from typing_extensions import ParamSpec diff --git a/src/trio/_core/__init__.py b/src/trio/_core/__init__.py index d21aefb3e2..f9d8068f0c 100644 --- a/src/trio/_core/__init__.py +++ b/src/trio/_core/__init__.py @@ -5,6 +5,7 @@ """ import sys +import typing as _t from ._entry_queue import TrioToken from ._exceptions import ( @@ -73,7 +74,9 @@ from ._unbounded_queue import UnboundedQueue, UnboundedQueueStatistics # Windows imports -if sys.platform == "win32": +if sys.platform == "win32" or ( + not _t.TYPE_CHECKING and "sphinx.ext.autodoc" in sys.modules +): from ._run import ( current_iocp, monitor_completion_key, @@ -83,7 +86,9 @@ write_overlapped, ) # Kqueue imports -elif sys.platform != "linux" and sys.platform != "win32": +if (sys.platform != "linux" and sys.platform != "win32") 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/_run.py b/src/trio/_core/_run.py index 5644099637..aee025cb7a 100644 --- a/src/trio/_core/_run.py +++ b/src/trio/_core/_run.py @@ -3080,6 +3080,12 @@ def in_trio_task() -> bool: return hasattr(GLOBAL_RUN_CONTEXT, "task") +# export everything for the documentation +if "sphinx.ext.autodoc" in sys.modules: + from ._generated_io_epoll import * + from ._generated_io_kqueue import * + from ._generated_io_windows import * + if sys.platform == "win32": from ._generated_io_windows import * from ._io_windows import ( @@ -3105,7 +3111,7 @@ def in_trio_task() -> bool: _patchers = sorted({"eventlet", "gevent"}.intersection(sys.modules)) if _patchers: raise NotImplementedError( - "unsupported platform or primitives trio depends on are monkey-patched out by " + "unsupported platform or primitives Trio depends on are monkey-patched out by " + ", ".join(_patchers), ) diff --git a/src/trio/_highlevel_socket.py b/src/trio/_highlevel_socket.py index c04e66e1bf..142ab11e07 100644 --- a/src/trio/_highlevel_socket.py +++ b/src/trio/_highlevel_socket.py @@ -14,10 +14,18 @@ if TYPE_CHECKING: from collections.abc import Generator - from typing_extensions import Buffer - from ._socket import SocketType +import sys + +if sys.version_info >= (3, 12): + # NOTE: this isn't in the `TYPE_CHECKING` since for some reason + # sphinx doesn't autoreload this module for SocketStream + # (hypothesis: it's our module renaming magic) + from collections.abc import Buffer +elif TYPE_CHECKING: + from typing_extensions import Buffer + # XX TODO: this number was picked arbitrarily. We should do experiments to # tune it. (Or make it dynamic -- one idea is to start small and increase it # if we observe single reads filling up the whole buffer, at least within some @@ -152,6 +160,7 @@ def setsockopt(self, level: int, option: int, value: int | Buffer) -> None: ... @overload def setsockopt(self, level: int, option: int, value: None, length: int) -> None: ... + # TODO: rename `length` to `optlen` def setsockopt( self, level: int, diff --git a/src/trio/_path.py b/src/trio/_path.py index 97642e2078..af6fbe0059 100644 --- a/src/trio/_path.py +++ b/src/trio/_path.py @@ -39,8 +39,18 @@ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: update_wrapper(wrapper, wrapped) if wrapped.__doc__: + module = wrapped.__module__ + # these are exported specially from CPython's intersphinx inventory + module = module.replace("pathlib._local", "pathlib") + module = module.replace("pathlib._abc", "pathlib") + + name = wrapped.__qualname__ + name = name.replace( + "PathBase", "Path" + ) # I'm not sure why this is necessary + wrapper.__doc__ = ( - f"Like :meth:`~{wrapped.__module__}.{wrapped.__qualname__}`, but async.\n" + f"Like :meth:`~{module}.{name}`, but async.\n" f"\n" f"{cleandoc(wrapped.__doc__)}\n" ) @@ -248,6 +258,10 @@ def as_uri(self) -> str: return pathlib.Path.as_uri(self) +if Path.relative_to.__doc__: # pragma: no branch + Path.relative_to.__doc__ = Path.relative_to.__doc__.replace(" `..` ", " ``..`` ") + + @final class PosixPath(Path, pathlib.PurePosixPath): """An async :class:`pathlib.PosixPath` that executes blocking methods in :meth:`trio.to_thread.run_sync`.""" diff --git a/src/trio/_tests/test_unix_pipes.py b/src/trio/_tests/test_unix_pipes.py index e0c2d4e4f2..4d1b08c06e 100644 --- a/src/trio/_tests/test_unix_pipes.py +++ b/src/trio/_tests/test_unix_pipes.py @@ -22,9 +22,6 @@ if posix: from .._unix_pipes import FdStream -else: - with pytest.raises(ImportError): - from .._unix_pipes import FdStream async def make_pipe() -> tuple[FdStream, FdStream]: diff --git a/src/trio/_timeouts.py b/src/trio/_timeouts.py index 7ce123c7c5..d95cbe4cfc 100644 --- a/src/trio/_timeouts.py +++ b/src/trio/_timeouts.py @@ -190,7 +190,7 @@ def fail_after( # Users don't need to know that fail_at & fail_after wraps move_on_at and move_on_after # and there is no functional difference. So we replace the return value when generating # documentation. -if "sphinx" in sys.modules: # pragma: no cover +if "sphinx.ext.autodoc" in sys.modules: import inspect for c in (fail_at, fail_after): diff --git a/src/trio/_unix_pipes.py b/src/trio/_unix_pipes.py index a95f761bcc..65a4fa889b 100644 --- a/src/trio/_unix_pipes.py +++ b/src/trio/_unix_pipes.py @@ -15,11 +15,6 @@ assert not TYPE_CHECKING or sys.platform != "win32" -if os.name != "posix": - # We raise an error here rather than gating the import in lowlevel.py - # in order to keep jedi static analysis happy. - raise ImportError - # XX TODO: is this a good number? who knows... it does match the default Linux # pipe capacity though. DEFAULT_RECEIVE_SIZE: FinalType = 65536 diff --git a/src/trio/lowlevel.py b/src/trio/lowlevel.py index bbeab6af17..b6621d47ed 100644 --- a/src/trio/lowlevel.py +++ b/src/trio/lowlevel.py @@ -4,7 +4,6 @@ """ # imports are renamed with leading underscores to indicate they are not part of the public API - import select as _select # static checkers don't understand if importing this as _sys, so it's deleted later @@ -60,8 +59,9 @@ # Uses `from x import y as y` for compatibility with `pyright --verifytypes` (#2625) - -if sys.platform == "win32": +if sys.platform == "win32" or ( + not _t.TYPE_CHECKING and "sphinx.ext.autodoc" in sys.modules +): # Windows symbols from ._core import ( current_iocp as current_iocp, @@ -71,13 +71,21 @@ wait_overlapped as wait_overlapped, write_overlapped as write_overlapped, ) - from ._wait_for_object import WaitForSingleObject as WaitForSingleObject -else: + + # don't let documentation import the actual implementation + if sys.platform == "win32": # pragma: no branch + from ._wait_for_object import WaitForSingleObject as WaitForSingleObject + +if sys.platform != "win32" or ( + not _t.TYPE_CHECKING and "sphinx.ext.autodoc" in sys.modules +): # Unix symbols from ._unix_pipes import FdStream as FdStream # Kqueue-specific symbols - if sys.platform != "linux" and (_t.TYPE_CHECKING or not hasattr(_select, "epoll")): + if ( + sys.platform != "linux" and (_t.TYPE_CHECKING or not hasattr(_select, "epoll")) + ) or (not _t.TYPE_CHECKING and "sphinx.ext.autodoc" in sys.modules): from ._core import ( current_kqueue as current_kqueue, monitor_kevent as monitor_kevent, From 4f07b688bc334f1fabcae563b6bdb06e6a3687b2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 20:29:13 +0000 Subject: [PATCH 041/111] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.0 → v0.12.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.0...v0.12.1) - [github.com/woodruffw/zizmor-pre-commit: v1.9.0 → v1.10.0](https://github.com/woodruffw/zizmor-pre-commit/compare/v1.9.0...v1.10.0) - [github.com/astral-sh/uv-pre-commit: 0.7.13 → 0.7.17](https://github.com/astral-sh/uv-pre-commit/compare/0.7.13...0.7.17) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b888acc6b2..856c63ff29 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.0 + rev: v0.12.1 hooks: - id: ruff-check types: [file] @@ -46,7 +46,7 @@ repos: hooks: - id: sphinx-lint - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.9.0 + rev: v1.10.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.7.13 + rev: 0.7.17 hooks: # Compile requirements - id: pip-compile From d96f07bca3b5078cbdf593f07500d2d19204fb4b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 20:29:28 +0000 Subject: [PATCH 042/111] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index fbc9b2eab3..5461d3e969 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -138,7 +138,7 @@ requests==2.32.4 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.12.0 +ruff==0.12.1 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -199,7 +199,7 @@ typing-extensions==4.14.0 # pyright urllib3==2.5.0 # via requests -uv==0.7.13 +uv==0.7.17 # via -r test-requirements.in virtualenv==20.31.2 # via pre-commit From 3d3de3bfbb9afc8789336a9104c0fef556bfa08a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 04:39:32 +0000 Subject: [PATCH 043/111] Dependency updates (#3294) --- .pre-commit-config.yaml | 2 +- docs-requirements.txt | 2 +- test-requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 856c63ff29..fe54cc1e33 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,7 +46,7 @@ repos: hooks: - id: sphinx-lint - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.10.0 + rev: v1.11.0 hooks: - id: zizmor - repo: local diff --git a/docs-requirements.txt b/docs-requirements.txt index df5c1e236a..e26ad79733 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -53,7 +53,7 @@ packaging==25.0 # via sphinx pycparser==2.22 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via cffi -pygments==2.19.1 +pygments==2.19.2 # via sphinx pyopenssl==25.1.0 # via -r docs-requirements.in diff --git a/test-requirements.txt b/test-requirements.txt index 5461d3e969..1fc23234a6 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -120,7 +120,7 @@ pre-commit==4.2.0 # via -r test-requirements.in pycparser==2.22 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via cffi -pygments==2.19.1 +pygments==2.19.2 # via # pytest # sphinx From 5471a37e82b36f556e0d26b36cb95a6b05afbef1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 07:10:08 +0900 Subject: [PATCH 044/111] [pre-commit.ci] pre-commit autoupdate (#3297) --- .pre-commit-config.yaml | 6 +++--- test-requirements.txt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fe54cc1e33..5af96b0cff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.1 + rev: v0.12.2 hooks: - id: ruff-check types: [file] @@ -38,7 +38,7 @@ repos: # tomli needed on 3.10. tomllib is available in stdlib on 3.11+ - tomli - repo: https://github.com/adhtruong/mirrors-typos - rev: v1.33.1 + rev: v1.34.0 hooks: - id: typos - repo: https://github.com/sphinx-contrib/sphinx-lint @@ -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.7.17 + rev: 0.7.19 hooks: # Compile requirements - id: pip-compile diff --git a/test-requirements.txt b/test-requirements.txt index 1fc23234a6..a24af7a07d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -138,7 +138,7 @@ requests==2.32.4 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.12.1 +ruff==0.12.2 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -199,7 +199,7 @@ typing-extensions==4.14.0 # pyright urllib3==2.5.0 # via requests -uv==0.7.17 +uv==0.7.19 # via -r test-requirements.in virtualenv==20.31.2 # via pre-commit From 55f7f2b0ff2db068c875b093f8a8f9464689a9ff Mon Sep 17 00:00:00 2001 From: Kannappan Date: Wed, 9 Jul 2025 12:06:10 +0530 Subject: [PATCH 045/111] Updated docs to reference :pep: format (#3295) --- docs/source/history.rst | 4 ++-- docs/source/reference-lowlevel.rst | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/source/history.rst b/docs/source/history.rst index d039ce6a8e..762f9eecae 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -125,7 +125,7 @@ Removals without deprecations Miscellaneous internal changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- Switch to using PEP570 for positional-only arguments for `~trio.socket.SocketType`'s methods. (`#3094 `__) +- Switch to using :pep:`570` for positional-only arguments for `~trio.socket.SocketType`'s methods. (`#3094 `__) - Improve type annotations in several places by removing `Any` usage. (`#3121 `__) - Get and enforce 100% coverage (`#3159 `__) @@ -1070,7 +1070,7 @@ Features to make the task scheduler reproducible and avoid flaky tests. (`#890 `__) - :class:`~trio.abc.SendChannel`, :class:`~trio.abc.ReceiveChannel`, :class:`~trio.abc.Listener`, and :func:`~trio.open_memory_channel` can now be referenced using a generic type parameter - (the type of object sent over the channel or produced by the listener) using PEP 484 syntax: + (the type of object sent over the channel or produced by the listener) using :pep:`484` syntax: ``trio.abc.SendChannel[bytes]``, ``trio.abc.Listener[trio.SocketStream]``, ``trio.open_memory_channel[MyMessage](5)``, etc. The added type information does not change the runtime semantics, but permits better integration with external static type checkers. (`#908 `__) diff --git a/docs/source/reference-lowlevel.rst b/docs/source/reference-lowlevel.rst index 7a39180e02..077f21cd27 100644 --- a/docs/source/reference-lowlevel.rst +++ b/docs/source/reference-lowlevel.rst @@ -1082,8 +1082,7 @@ issue #649 `__. For more details on how coroutines work, we recommend André Caron's `A tale of event loops `__, or -going straight to `PEP 492 -`__ for the full details. +going straight to :pep:`492` for the full details. .. autofunction:: permanently_detach_coroutine_object From 49cc76a70281ae649b60047116eafa7d45f10c07 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 20:39:49 +0000 Subject: [PATCH 046/111] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.2 → v0.12.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.2...v0.12.3) - [github.com/astral-sh/uv-pre-commit: 0.7.19 → 0.7.20](https://github.com/astral-sh/uv-pre-commit/compare/0.7.19...0.7.20) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5af96b0cff..59c74a466f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.2 + rev: v0.12.3 hooks: - id: ruff-check types: [file] @@ -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.7.19 + rev: 0.7.20 hooks: # Compile requirements - id: pip-compile From afcf02803cca6381fd8f28bfe544b077bde540ad Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Jul 2025 20:40:08 +0000 Subject: [PATCH 047/111] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index a24af7a07d..58bbb1f8fe 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -138,7 +138,7 @@ requests==2.32.4 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.12.2 +ruff==0.12.3 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -199,7 +199,7 @@ typing-extensions==4.14.0 # pyright urllib3==2.5.0 # via requests -uv==0.7.19 +uv==0.7.20 # via -r test-requirements.in virtualenv==20.31.2 # via pre-commit From e590d39ffa04f751bf10e5b35864686cb7cd6bbb Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 08:09:08 +0900 Subject: [PATCH 048/111] [pre-commit.ci] pre-commit autoupdate (#3305) --- .pre-commit-config.yaml | 4 ++-- test-requirements.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 59c74a466f..4dfc20afc1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.3 + rev: v0.12.4 hooks: - id: ruff-check types: [file] @@ -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.7.20 + rev: 0.8.0 hooks: # Compile requirements - id: pip-compile diff --git a/test-requirements.txt b/test-requirements.txt index 58bbb1f8fe..bf2d1d9f68 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -138,7 +138,7 @@ requests==2.32.4 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.12.3 +ruff==0.12.4 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -199,7 +199,7 @@ typing-extensions==4.14.0 # pyright urllib3==2.5.0 # via requests -uv==0.7.20 +uv==0.8.0 # via -r test-requirements.in virtualenv==20.31.2 # via pre-commit From 921d36357f2cda7553c031b56729d0b005bf09c6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 06:54:07 +0000 Subject: [PATCH 049/111] Bump dependencies from commit e590d3 (#3309) * Dependency updates * Trigger CI * Add new socket attributes * Use unreleased bleading edge parso in 3.14 * Update export tests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Try to understand __annotate_func__ * make tests pass locally --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Co-authored-by: A5rocks Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- docs-requirements.txt | 8 ++++---- src/trio/_repl.py | 3 +-- src/trio/_tests/test_exports.py | 13 ++++++++---- src/trio/socket.py | 12 +++++++++++ test-requirements.in | 2 ++ test-requirements.txt | 36 +++++++++++++++++++-------------- 7 files changed, 51 insertions(+), 27 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4dfc20afc1..63324b3df7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.4 + rev: v0.12.7 hooks: - id: ruff-check types: [file] @@ -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.8.0 + rev: 0.8.4 hooks: # Compile requirements - id: pip-compile diff --git a/docs-requirements.txt b/docs-requirements.txt index e26ad79733..919267501c 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -10,7 +10,7 @@ babel==2.17.0 # via sphinx beautifulsoup4==4.13.4 # via sphinx-codeautolink -certifi==2025.6.15 +certifi==2025.7.14 # via requests cffi==1.17.1 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via @@ -24,7 +24,7 @@ colorama==0.4.6 ; sys_platform == 'win32' # via # click # sphinx -cryptography==45.0.4 +cryptography==45.0.5 # via pyopenssl docutils==0.21.2 # via @@ -76,7 +76,7 @@ sphinx==8.2.3 # sphinx-rtd-theme # sphinxcontrib-jquery # sphinxcontrib-trio -sphinx-codeautolink==0.17.4 +sphinx-codeautolink==0.17.5 # via -r docs-requirements.in sphinx-rtd-theme==3.0.2 # via -r docs-requirements.in @@ -100,7 +100,7 @@ sphinxcontrib-trio==1.1.2 # via -r docs-requirements.in towncrier==24.8.0 # via -r docs-requirements.in -typing-extensions==4.14.0 +typing-extensions==4.14.1 # via # beautifulsoup4 # exceptiongroup diff --git a/src/trio/_repl.py b/src/trio/_repl.py index 8be5af8fb8..c8863989d2 100644 --- a/src/trio/_repl.py +++ b/src/trio/_repl.py @@ -22,8 +22,7 @@ def __init__(self, repl_locals: dict[str, object] | None = None) -> None: self.compile.compiler.flags |= ast.PyCF_ALLOW_TOP_LEVEL_AWAIT def runcode(self, code: types.CodeType) -> None: - # https://github.com/python/typeshed/issues/13768 - func = types.FunctionType(code, self.locals) # type: ignore[arg-type] + func = types.FunctionType(code, self.locals) if inspect.iscoroutinefunction(func): result = trio.from_thread.run(outcome.acapture, func) else: diff --git a/src/trio/_tests/test_exports.py b/src/trio/_tests/test_exports.py index 68f9cbcc31..c29d9b609a 100644 --- a/src/trio/_tests/test_exports.py +++ b/src/trio/_tests/test_exports.py @@ -443,13 +443,13 @@ def lookup_symbol(symbol: str) -> dict[str, Any]: # type: ignore[misc, explicit extra = {e for e in extra if not e.endswith("AttrsAttributes__")} assert len(extra) == before - 1 - # mypy does not see these attributes in Enum subclasses + # dir does not see `__signature__` on enums until 3.14 if ( tool == "mypy" and enum.Enum in class_.__mro__ and sys.version_info >= (3, 12) + and sys.version_info < (3, 14) ): - # Another attribute, in 3.12+ only. extra.remove("__signature__") # TODO: this *should* be visible via `dir`!! @@ -511,10 +511,9 @@ def lookup_symbol(symbol: str) -> dict[str, Any]: # type: ignore[misc, explicit extra -= {"owner", "is_mount", "group"} # not sure why jedi in particular ignores this (static?) method in 3.13 - # (especially given the method is from 3.12....) if ( tool == "jedi" - and sys.version_info >= (3, 13) + and sys.version_info[:2] == (3, 13) and class_ in (trio.Path, trio.WindowsPath, trio.PosixPath) ): missing.remove("with_segments") @@ -522,6 +521,12 @@ def lookup_symbol(symbol: str) -> dict[str, Any]: # type: ignore[misc, explicit if sys.version_info >= (3, 13) and attrs.has(class_): missing.remove("__replace__") + if sys.version_info >= (3, 14): + # these depend on whether a class has processed deferred annotations. + # (which might or might not happen and we don't know) + missing.discard("__annotate_func__") + missing.discard("__annotations_cache__") + if missing or extra: # pragma: no cover errors[f"{module_name}.{class_name}"] = { "missing": missing, diff --git a/src/trio/socket.py b/src/trio/socket.py index 718db1ac6f..cfcb9943c8 100644 --- a/src/trio/socket.py +++ b/src/trio/socket.py @@ -277,6 +277,7 @@ IP_DEFAULT_MULTICAST_TTL as IP_DEFAULT_MULTICAST_TTL, IP_DROP_MEMBERSHIP as IP_DROP_MEMBERSHIP, IP_DROP_SOURCE_MEMBERSHIP as IP_DROP_SOURCE_MEMBERSHIP, + IP_FREEBIND as IP_FREEBIND, IP_HDRINCL as IP_HDRINCL, IP_MAX_MEMBERSHIPS as IP_MAX_MEMBERSHIPS, IP_MULTICAST_IF as IP_MULTICAST_IF, @@ -285,9 +286,12 @@ IP_OPTIONS as IP_OPTIONS, IP_PKTINFO as IP_PKTINFO, IP_RECVDSTADDR as IP_RECVDSTADDR, + IP_RECVERR as IP_RECVERR, IP_RECVOPTS as IP_RECVOPTS, + IP_RECVORIGDSTADDR as IP_RECVORIGDSTADDR, IP_RECVRETOPTS as IP_RECVRETOPTS, IP_RECVTOS as IP_RECVTOS, + IP_RECVTTL as IP_RECVTTL, IP_RETOPTS as IP_RETOPTS, IP_TOS as IP_TOS, IP_TRANSPARENT as IP_TRANSPARENT, @@ -351,6 +355,7 @@ IPV6_PATHMTU as IPV6_PATHMTU, IPV6_PKTINFO as IPV6_PKTINFO, IPV6_RECVDSTOPTS as IPV6_RECVDSTOPTS, + IPV6_RECVERR as IPV6_RECVERR, IPV6_RECVHOPLIMIT as IPV6_RECVHOPLIMIT, IPV6_RECVHOPOPTS as IPV6_RECVHOPOPTS, IPV6_RECVPATHMTU as IPV6_RECVPATHMTU, @@ -458,6 +463,10 @@ SO_BINDTODEVICE as SO_BINDTODEVICE, SO_BINDTOIFINDEX as SO_BINDTOIFINDEX, SO_BROADCAST as SO_BROADCAST, + SO_BTH_ENCRYPT as SO_BTH_ENCRYPT, + SO_BTH_MTU as SO_BTH_MTU, + SO_BTH_MTU_MAX as SO_BTH_MTU_MAX, + SO_BTH_MTU_MIN as SO_BTH_MTU_MIN, SO_DEBUG as SO_DEBUG, SO_DOMAIN as SO_DOMAIN, SO_DONTROUTE as SO_DONTROUTE, @@ -472,6 +481,7 @@ SO_LINGER as SO_LINGER, SO_MARK as SO_MARK, SO_OOBINLINE as SO_OOBINLINE, + SO_ORIGINAL_DST as SO_ORIGINAL_DST, SO_PASSCRED as SO_PASSCRED, SO_PASSSEC as SO_PASSSEC, SO_PEERCRED as SO_PEERCRED, @@ -505,6 +515,7 @@ SOL_HCI as SOL_HCI, SOL_IP as SOL_IP, SOL_RDS as SOL_RDS, + SOL_RFCOMM as SOL_RFCOMM, SOL_SOCKET as SOL_SOCKET, SOL_TCP as SOL_TCP, SOL_TIPC as SOL_TIPC, @@ -577,6 +588,7 @@ VM_SOCKETS_INVALID_VERSION as VM_SOCKETS_INVALID_VERSION, VMADDR_CID_ANY as VMADDR_CID_ANY, VMADDR_CID_HOST as VMADDR_CID_HOST, + VMADDR_CID_LOCAL as VMADDR_CID_LOCAL, VMADDR_PORT_ANY as VMADDR_PORT_ANY, WSA_FLAG_OVERLAPPED as WSA_FLAG_OVERLAPPED, WSA_INVALID_HANDLE as WSA_INVALID_HANDLE, diff --git a/test-requirements.in b/test-requirements.in index fd16b2d3bc..331e87ca34 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -8,6 +8,8 @@ trustme # for the ssl + DTLS tests pylint # for pylint finding all symbols tests jedi; implementation_name == "cpython" # for jedi code completion tests cryptography>=41.0.0 # cryptography<41 segfaults on pypy3.10 +# Temp workaround until parso makes a new release +git+https://github.com/davidhalter/parso.git; python_version >= "3.14" # Tools black; implementation_name == "cpython" diff --git a/test-requirements.txt b/test-requirements.txt index bf2d1d9f68..ba75607afd 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,7 +6,7 @@ alabaster==1.0.0 ; python_full_version >= '3.10' # via sphinx astor==0.8.1 # via -r test-requirements.in -astroid==3.3.10 +astroid==3.3.11 # via pylint async-generator==1.10 # via -r test-requirements.in @@ -18,7 +18,7 @@ babel==2.17.0 # via sphinx black==25.1.0 ; implementation_name == 'cpython' # via -r test-requirements.in -certifi==2025.6.15 +certifi==2025.7.14 # via requests cffi==1.17.1 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via @@ -40,9 +40,9 @@ colorama==0.4.6 ; sys_platform == 'win32' # pylint # pytest # sphinx -coverage==7.9.1 +coverage==7.10.1 # via -r test-requirements.in -cryptography==45.0.4 +cryptography==45.0.5 # via # -r test-requirements.in # pyopenssl @@ -50,7 +50,7 @@ cryptography==45.0.4 # types-pyopenssl dill==0.4.0 # via pylint -distlib==0.3.9 +distlib==0.4.0 # via virtualenv docutils==0.21.2 # via sphinx @@ -83,7 +83,7 @@ markupsafe==3.0.2 # via jinja2 mccabe==0.7.0 # via pylint -mypy==1.16.1 +mypy==1.17.1 # via -r test-requirements.in mypy-extensions==1.1.0 # via @@ -94,7 +94,7 @@ nodeenv==1.9.1 # via # pre-commit # pyright -orjson==3.10.18 ; python_full_version < '3.14' and implementation_name == 'cpython' +orjson==3.11.1 ; python_full_version < '3.14' and implementation_name == 'cpython' # via -r test-requirements.in outcome==1.3.0.post0 # via -r test-requirements.in @@ -103,8 +103,14 @@ packaging==25.0 # black # pytest # sphinx -parso==0.8.4 ; implementation_name == 'cpython' - # via jedi +parso==0.8.4 ; python_full_version < '3.14' and implementation_name == 'cpython' + # via + # -r test-requirements.in + # jedi +parso @ git+https://github.com/davidhalter/parso.git@a73af5c709a292cbb789bf6cab38b20559f166c0 ; python_full_version >= '3.14' + # via + # -r test-requirements.in + # jedi pathspec==0.12.1 # via # black @@ -128,7 +134,7 @@ pylint==3.3.7 # via -r test-requirements.in pyopenssl==25.1.0 # via -r test-requirements.in -pyright==1.1.402 +pyright==1.1.403 # via -r test-requirements.in pytest==8.4.1 # via -r test-requirements.in @@ -138,7 +144,7 @@ requests==2.32.4 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.12.4 +ruff==0.12.7 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -179,7 +185,7 @@ types-cffi==1.17.0.20250523 # via # -r test-requirements.in # types-pyopenssl -types-docutils==0.21.0.20250604 +types-docutils==0.21.0.20250728 # via -r test-requirements.in types-pyopenssl==24.1.0.20240722 # via -r test-requirements.in @@ -187,7 +193,7 @@ types-pyyaml==6.0.12.20250516 # via -r test-requirements.in types-setuptools==80.9.0.20250529 # via types-cffi -typing-extensions==4.14.0 +typing-extensions==4.14.1 # via # -r test-requirements.in # astroid @@ -199,9 +205,9 @@ typing-extensions==4.14.0 # pyright urllib3==2.5.0 # via requests -uv==0.8.0 +uv==0.8.4 # via -r test-requirements.in -virtualenv==20.31.2 +virtualenv==20.32.0 # via pre-commit zipp==3.23.0 ; python_full_version < '3.10' # via importlib-metadata From 7f7bf5e19cb6faf9c963f5d424074457c96fd065 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 5 Aug 2025 19:42:01 +0900 Subject: [PATCH 050/111] Remove pip caching for Windows (#3313) --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f47e7ab07e..93fbf6ab05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -207,8 +207,6 @@ jobs: with: python-version: '${{ matrix.python }}' architecture: '${{ matrix.arch }}' - cache: pip - cache-dependency-path: test-requirements.txt allow-prereleases: true - name: Run tests run: ./ci.sh From f8a51b681f5d771dc94f1cf2b0fe05f3c632bd7a Mon Sep 17 00:00:00 2001 From: A5rocks Date: Thu, 7 Aug 2025 13:47:26 +0900 Subject: [PATCH 051/111] Codecov v5 (#3312) * Bump Codecov action to v5 It's more stable and requires a token that is already in the config. Alpine needs curl, gpg, git and jq because of a bug in codecov-action [[1]]. [1]: https://github.com/codecov/codecov-action/issues/1320 * Make codecov v5 work * Remove forgotten lines too * Add an extra thing I missed --------- Co-authored-by: Sviatoslav Sydorenko --- .github/workflows/ci.yml | 25 +++++++++++-------------- ci.sh | 6 ++++-- pyproject.toml | 12 ++---------- 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93fbf6ab05..ed09025c3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -215,14 +215,13 @@ jobs: LSP: '${{ matrix.lsp }}' LSP_EXTRACT_FILE: '${{ matrix.lsp_extract_file }}' - if: always() - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: - directory: empty name: Windows (${{ matrix.python }}, ${{ matrix.arch }}${{ matrix.extra_name }}) # multiple flags is marked as an error in codecov UI, but is actually fine # https://github.com/codecov/feedback/issues/567 flags: Windows,${{ matrix.python }} - fail_ci_if_error: false # change to true when using codecov action v5 + fail_ci_if_error: true Ubuntu: name: 'Ubuntu (${{ matrix.python }}${{ matrix.extra_name }})' @@ -291,12 +290,11 @@ jobs: - if: >- always() && matrix.check_formatting != '1' - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: - directory: empty name: Ubuntu (${{ matrix.python }}${{ matrix.extra_name }}) flags: Ubuntu,${{ matrix.python }} - fail_ci_if_error: false + fail_ci_if_error: true macOS: name: 'macOS (${{ matrix.python }})' @@ -334,12 +332,11 @@ jobs: - name: Run tests run: ./ci.sh - if: always() - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: - directory: empty name: macOS (${{ matrix.python }}) flags: macOS,${{ matrix.python }} - fail_ci_if_error: false + fail_ci_if_error: true # run CI on a musl linux Alpine: @@ -353,9 +350,10 @@ jobs: - name: Install necessary packages # can't use setup-python because that python doesn't seem to work; # `python3-dev` (rather than `python:alpine`) for some ctypes reason, + # `curl`, `gpg`, `git` for codecov-action v4/v5 to work (https://github.com/codecov/codecov-action/issues/1320). # `nodejs` for pyright (`node-env` pulls in nodejs but that takes a while and can time out the test). # `perl` for a platform independent `sed -i` alternative - run: apk update && apk add python3-dev bash nodejs perl + run: apk update && apk add python3-dev bash curl gpg git nodejs perl - name: Retrieve the project source from an sdist inside the GHA artifact # must be after `apk add` because it relies on `bash` existing @@ -380,12 +378,11 @@ jobs: f.write("\n") - if: always() - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: - directory: empty name: Alpine flags: Alpine,${{ steps.get-version.outputs.version }} - fail_ci_if_error: false + fail_ci_if_error: true Cython: name: "Cython" @@ -453,7 +450,7 @@ jobs: with: name: Cython flags: Cython,${{ steps.get-version.outputs.version }} - fail_ci_if_error: false + fail_ci_if_error: true # https://github.com/marketplace/actions/alls-green#why check: # This job does nothing and is only used for the branch protection diff --git a/ci.sh b/ci.sh index 8b23ad279f..07797937e9 100755 --- a/ci.sh +++ b/ci.sh @@ -137,8 +137,10 @@ echo "::endgroup::" echo "::group::Coverage" coverage combine --rcfile ../pyproject.toml -coverage report -m --rcfile ../pyproject.toml -coverage xml --rcfile ../pyproject.toml +cd .. # coverage needs to be in the folder containing src/trio +cp empty/.coverage . +coverage report -m --rcfile ./pyproject.toml +coverage xml --rcfile ./pyproject.toml # Remove the LSP again; again we want to do this ASAP to avoid # accidentally breaking other stuff. diff --git a/pyproject.toml b/pyproject.toml index f0ba3610c8..bec87cfef0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -292,18 +292,10 @@ show_contexts = true skip_covered = false [tool.coverage.paths] -_site-packages-to-src-mapping = [ - "src", - "*/src", - '*\src', - "*/lib/pypy*/site-packages", - "*/lib/python*/site-packages", - '*\Lib\site-packages', -] +source = ["src", "**/site-packages"] [tool.coverage.run] branch = true -source_pkgs = ["trio"] omit = [ # Omit the generated files in trio/_core starting with _generated_ "*/trio/_core/_generated_*", @@ -319,7 +311,7 @@ omit = [ parallel = true plugins = [] relative_files = true -source = ["."] +source = ["trio"] [tool.coverage.report] precision = 1 From 7168bfcd45ab8efea876132a73a81e2c66073116 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 07:21:04 +0900 Subject: [PATCH 052/111] [pre-commit.ci] pre-commit autoupdate (#3316) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0) - [github.com/astral-sh/ruff-pre-commit: v0.12.7 → v0.12.8](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.7...v0.12.8) - [github.com/adhtruong/mirrors-typos: v1.34.0 → v1.35.3](https://github.com/adhtruong/mirrors-typos/compare/v1.34.0...v1.35.3) - [github.com/astral-sh/uv-pre-commit: 0.8.4 → 0.8.8](https://github.com/astral-sh/uv-pre-commit/compare/0.8.4...0.8.8) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 8 ++++---- test-requirements.txt | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 63324b3df7..344cbad3fd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ ci: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.7 + rev: v0.12.8 hooks: - id: ruff-check types: [file] @@ -38,7 +38,7 @@ repos: # tomli needed on 3.10. tomllib is available in stdlib on 3.11+ - tomli - repo: https://github.com/adhtruong/mirrors-typos - rev: v1.34.0 + rev: v1.35.3 hooks: - id: typos - repo: https://github.com/sphinx-contrib/sphinx-lint @@ -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.8.4 + rev: 0.8.8 hooks: # Compile requirements - id: pip-compile diff --git a/test-requirements.txt b/test-requirements.txt index ba75607afd..6b6c23c71b 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -144,7 +144,7 @@ requests==2.32.4 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.12.7 +ruff==0.12.8 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -205,7 +205,7 @@ typing-extensions==4.14.1 # pyright urllib3==2.5.0 # via requests -uv==0.8.4 +uv==0.8.8 # via -r test-requirements.in virtualenv==20.32.0 # via pre-commit From 393d0ccaedd8adf4d3a7aca52fd6610d7f00b762 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Wed, 13 Aug 2025 01:35:03 +0900 Subject: [PATCH 053/111] Drop PyPy 3.10 (#3317) --- .github/workflows/ci.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed09025c3e..9b1db86d03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -171,11 +171,6 @@ jobs: lsp: 'https://www.proxifier.com/download/legacy/ProxifierSetup342.exe' lsp_extract_file: '' extra_name: ', with IFS LSP' - - python: 'pypy-3.10' - arch: 'x64' - lsp: '' - lsp_extract_file: '' - extra_name: '' - python: 'pypy-3.11' arch: 'x64' lsp: '' @@ -233,7 +228,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['pypy-3.10', 'pypy-3.11', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + python: ['pypy-3.11', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] check_formatting: ['0'] no_test_requirements: ['0'] extra_name: [''] @@ -306,7 +301,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['pypy-3.10', 'pypy-3.11', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + python: ['pypy-3.11', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] continue-on-error: >- ${{ ( From 732f15744752ccfb8603cdae3f3f49eca7f39df6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 22:06:51 +0000 Subject: [PATCH 054/111] [pre-commit.ci] pre-commit autoupdate (#3318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.8 → v0.12.9](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.8...v0.12.9) - [github.com/adhtruong/mirrors-typos: v1.35.3 → v1.35.4](https://github.com/adhtruong/mirrors-typos/compare/v1.35.3...v1.35.4) - [github.com/woodruffw/zizmor-pre-commit: v1.11.0 → v1.12.1](https://github.com/woodruffw/zizmor-pre-commit/compare/v1.11.0...v1.12.1) - [github.com/astral-sh/uv-pre-commit: 0.8.8 → 0.8.11](https://github.com/astral-sh/uv-pre-commit/compare/0.8.8...0.8.11) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Bypass codecov * Oops, I misremembered the flag * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: A5rocks --- .pre-commit-config.yaml | 8 ++++---- src/trio/_tests/test_socket.py | 4 +++- test-requirements.txt | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 344cbad3fd..436d3a9bf2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.8 + rev: v0.12.9 hooks: - id: ruff-check types: [file] @@ -38,7 +38,7 @@ repos: # tomli needed on 3.10. tomllib is available in stdlib on 3.11+ - tomli - repo: https://github.com/adhtruong/mirrors-typos - rev: v1.35.3 + rev: v1.35.4 hooks: - id: typos - repo: https://github.com/sphinx-contrib/sphinx-lint @@ -46,7 +46,7 @@ repos: hooks: - id: sphinx-lint - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.11.0 + rev: v1.12.1 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.8.8 + rev: 0.8.11 hooks: # Compile requirements - id: pip-compile diff --git a/src/trio/_tests/test_socket.py b/src/trio/_tests/test_socket.py index 07fceb3112..850e0b4a3b 100644 --- a/src/trio/_tests/test_socket.py +++ b/src/trio/_tests/test_socket.py @@ -469,7 +469,9 @@ def setsockopt_tests(sock: SocketType | SocketStream) -> None: if hasattr(tsocket, "SO_BINDTODEVICE"): try: sock.setsockopt(tsocket.SOL_SOCKET, tsocket.SO_BINDTODEVICE, None, 0) - except OSError as e: + except ( + OSError + ) as e: # pragma: no cover # all CI runners support SO_BINDTODEVICE assert e.errno in [ # noqa: PT017 # some versions of Python have the attribute yet can run on # platforms that do not support it. For instance, MacOS 15 diff --git a/test-requirements.txt b/test-requirements.txt index 6b6c23c71b..f9cf112c5f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -144,7 +144,7 @@ requests==2.32.4 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.12.8 +ruff==0.12.9 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -205,7 +205,7 @@ typing-extensions==4.14.1 # pyright urllib3==2.5.0 # via requests -uv==0.8.8 +uv==0.8.11 # via -r test-requirements.in virtualenv==20.32.0 # via pre-commit From d0c85e4c01e081473dc3545f6bd9b7cd9c5165d2 Mon Sep 17 00:00:00 2001 From: Daniel Colascione Date: Thu, 21 Aug 2025 02:58:11 -0400 Subject: [PATCH 055/111] Add warning about non-blocking mode and TTYs (#3315) * Add warning about non-blocking mode and TTYs * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * streamline warning message * Make it shorter * Update _unix_pipes.py --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: A5rocks --- src/trio/_unix_pipes.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/trio/_unix_pipes.py b/src/trio/_unix_pipes.py index 65a4fa889b..dbe4358b4c 100644 --- a/src/trio/_unix_pipes.py +++ b/src/trio/_unix_pipes.py @@ -81,8 +81,7 @@ def close(self) -> None: @final class FdStream(Stream): - """ - Represents a stream given the file descriptor to a pipe, TTY, etc. + """Represents a stream given the file descriptor to a pipe, TTY, etc. *fd* must refer to a file that is open for reading and/or writing and supports non-blocking I/O (pipes and TTYs will work, on-disk files probably @@ -106,6 +105,15 @@ class FdStream(Stream): `__ for a discussion of the challenges involved in relaxing this restriction. + .. warning:: one specific consequence of non-blocking mode + applying to the entire open file description is that when + your program is run with multiple standard streams connected to + a TTY (as in a terminal emulator), all of the streams become + non-blocking when you construct an `FdStream` for any of them. + For example, if you construct an `FdStream` for standard input, + you might observe Python loggers begin to fail with + `BlockingIOError`. + Args: fd (int): The fd to be wrapped. From 77dd921d98a75dbb5a1cf14334a39b175994530c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 23 Aug 2025 20:10:18 +0000 Subject: [PATCH 056/111] Bump dependencies from commit d0c85e (#3320) * Dependency updates * Parso made a new release * Update some missed versions --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: A5rocks --- .pre-commit-config.yaml | 6 +++--- docs-requirements.txt | 8 +++---- test-requirements.in | 4 +--- test-requirements.txt | 47 ++++++++++++++++++----------------------- 4 files changed, 29 insertions(+), 36 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 436d3a9bf2..97a059b92c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.9 + rev: v0.12.10 hooks: - id: ruff-check types: [file] @@ -38,7 +38,7 @@ repos: # tomli needed on 3.10. tomllib is available in stdlib on 3.11+ - tomli - repo: https://github.com/adhtruong/mirrors-typos - rev: v1.35.4 + rev: v1.35.5 hooks: - id: typos - repo: https://github.com/sphinx-contrib/sphinx-lint @@ -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.8.11 + rev: 0.8.13 hooks: # Compile requirements - id: pip-compile diff --git a/docs-requirements.txt b/docs-requirements.txt index 919267501c..8cd7031fc4 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -10,13 +10,13 @@ babel==2.17.0 # via sphinx beautifulsoup4==4.13.4 # via sphinx-codeautolink -certifi==2025.7.14 +certifi==2025.8.3 # via requests cffi==1.17.1 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via # -r docs-requirements.in # cryptography -charset-normalizer==3.4.2 +charset-normalizer==3.4.3 # via requests click==8.2.1 # via towncrier @@ -24,7 +24,7 @@ colorama==0.4.6 ; sys_platform == 'win32' # via # click # sphinx -cryptography==45.0.5 +cryptography==45.0.6 # via pyopenssl docutils==0.21.2 # via @@ -57,7 +57,7 @@ pygments==2.19.2 # via sphinx pyopenssl==25.1.0 # via -r docs-requirements.in -requests==2.32.4 +requests==2.32.5 # via sphinx roman-numerals-py==3.1.0 # via sphinx diff --git a/test-requirements.in b/test-requirements.in index 331e87ca34..90776153c4 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -8,13 +8,11 @@ trustme # for the ssl + DTLS tests pylint # for pylint finding all symbols tests jedi; implementation_name == "cpython" # for jedi code completion tests cryptography>=41.0.0 # cryptography<41 segfaults on pypy3.10 -# Temp workaround until parso makes a new release -git+https://github.com/davidhalter/parso.git; python_version >= "3.14" # Tools black; implementation_name == "cpython" mypy # Would use mypy[faster-cache], but orjson has build issues on pypy -orjson; implementation_name == "cpython" and python_version < "3.14" # orjson does not yet install on 3.14 +orjson; implementation_name == "cpython" # orjson does not yet install on 3.14 ruff >= 0.8.0 astor # code generation uv >= 0.2.24 diff --git a/test-requirements.txt b/test-requirements.txt index f9cf112c5f..3d12554ce6 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -18,7 +18,7 @@ babel==2.17.0 # via sphinx black==25.1.0 ; implementation_name == 'cpython' # via -r test-requirements.in -certifi==2025.7.14 +certifi==2025.8.3 # via requests cffi==1.17.1 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via @@ -26,7 +26,7 @@ cffi==1.17.1 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # cryptography cfgv==3.4.0 # via pre-commit -charset-normalizer==3.4.2 +charset-normalizer==3.4.3 # via requests click==8.1.8 ; python_full_version < '3.10' and implementation_name == 'cpython' # via black @@ -40,9 +40,9 @@ colorama==0.4.6 ; sys_platform == 'win32' # pylint # pytest # sphinx -coverage==7.10.1 +coverage==7.10.5 # via -r test-requirements.in -cryptography==45.0.5 +cryptography==45.0.6 # via # -r test-requirements.in # pyopenssl @@ -58,9 +58,9 @@ exceptiongroup==1.3.0 ; python_full_version < '3.11' # via # -r test-requirements.in # pytest -filelock==3.18.0 +filelock==3.19.1 # via virtualenv -identify==2.6.12 +identify==2.6.13 # via pre-commit idna==3.10 # via @@ -94,7 +94,7 @@ nodeenv==1.9.1 # via # pre-commit # pyright -orjson==3.11.1 ; python_full_version < '3.14' and implementation_name == 'cpython' +orjson==3.11.2 ; implementation_name == 'cpython' # via -r test-requirements.in outcome==1.3.0.post0 # via -r test-requirements.in @@ -103,14 +103,8 @@ packaging==25.0 # black # pytest # sphinx -parso==0.8.4 ; python_full_version < '3.14' and implementation_name == 'cpython' - # via - # -r test-requirements.in - # jedi -parso @ git+https://github.com/davidhalter/parso.git@a73af5c709a292cbb789bf6cab38b20559f166c0 ; python_full_version >= '3.14' - # via - # -r test-requirements.in - # jedi +parso==0.8.5 ; implementation_name == 'cpython' + # via jedi pathspec==0.12.1 # via # black @@ -122,7 +116,7 @@ platformdirs==4.3.8 # virtualenv pluggy==1.6.0 # via pytest -pre-commit==4.2.0 +pre-commit==4.3.0 # via -r test-requirements.in pycparser==2.22 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via cffi @@ -130,21 +124,21 @@ pygments==2.19.2 # via # pytest # sphinx -pylint==3.3.7 +pylint==3.3.8 # via -r test-requirements.in pyopenssl==25.1.0 # via -r test-requirements.in -pyright==1.1.403 +pyright==1.1.404 # via -r test-requirements.in pytest==8.4.1 # via -r test-requirements.in pyyaml==6.0.2 # via pre-commit -requests==2.32.4 +requests==2.32.5 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.12.9 +ruff==0.12.10 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -181,17 +175,17 @@ tomlkit==0.13.3 # via pylint trustme==1.2.1 # via -r test-requirements.in -types-cffi==1.17.0.20250523 +types-cffi==1.17.0.20250822 # via # -r test-requirements.in # types-pyopenssl -types-docutils==0.21.0.20250728 +types-docutils==0.22.0.20250822 # via -r test-requirements.in types-pyopenssl==24.1.0.20240722 # via -r test-requirements.in -types-pyyaml==6.0.12.20250516 +types-pyyaml==6.0.12.20250822 # via -r test-requirements.in -types-setuptools==80.9.0.20250529 +types-setuptools==80.9.0.20250822 # via types-cffi typing-extensions==4.14.1 # via @@ -203,11 +197,12 @@ typing-extensions==4.14.1 # pylint # pyopenssl # pyright + # virtualenv urllib3==2.5.0 # via requests -uv==0.8.11 +uv==0.8.13 # via -r test-requirements.in -virtualenv==20.32.0 +virtualenv==20.34.0 # via pre-commit zipp==3.23.0 ; python_full_version < '3.10' # via importlib-metadata From 2f4c529b60793b675066711465b6fb62d72897a0 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Sun, 24 Aug 2025 16:11:30 +0500 Subject: [PATCH 057/111] Allow `CapacityLimiter` to have zero total_tokens --- src/trio/_sync.py | 4 ++-- src/trio/_tests/test_sync.py | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/trio/_sync.py b/src/trio/_sync.py index ca373922b0..236a7028b1 100644 --- a/src/trio/_sync.py +++ b/src/trio/_sync.py @@ -251,8 +251,8 @@ def total_tokens(self) -> int | float: def total_tokens(self, new_total_tokens: int | float) -> None: # noqa: PYI041 if not isinstance(new_total_tokens, int) and new_total_tokens != math.inf: raise TypeError("total_tokens must be an int or math.inf") - if new_total_tokens < 1: - raise ValueError("total_tokens must be >= 1") + if new_total_tokens < 0: + raise ValueError("total_tokens must be >= 0") self._total_tokens = new_total_tokens self._wake_waiters() diff --git a/src/trio/_tests/test_sync.py b/src/trio/_tests/test_sync.py index 39f8d21f38..2815b489f2 100644 --- a/src/trio/_tests/test_sync.py +++ b/src/trio/_tests/test_sync.py @@ -49,9 +49,10 @@ async def child() -> None: async def test_CapacityLimiter() -> None: + assert CapacityLimiter(0).total_tokens == 0 with pytest.raises(TypeError): CapacityLimiter(1.0) - with pytest.raises(ValueError, match=r"^total_tokens must be >= 1$"): + with pytest.raises(ValueError, match=r"^total_tokens must be >= 0$"): CapacityLimiter(-1) c = CapacityLimiter(2) repr(c) # smoke test @@ -139,10 +140,10 @@ async def test_CapacityLimiter_change_total_tokens() -> None: with pytest.raises(TypeError): c.total_tokens = 1.0 - with pytest.raises(ValueError, match=r"^total_tokens must be >= 1$"): - c.total_tokens = 0 + with pytest.raises(ValueError, match=r"^total_tokens must be >= 0$"): + c.total_tokens = -1 - with pytest.raises(ValueError, match=r"^total_tokens must be >= 1$"): + with pytest.raises(ValueError, match=r"^total_tokens must be >= 0$"): c.total_tokens = -10 assert c.total_tokens == 2 From a97f905edd2f0d36c715313d12b879f036e3b554 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sun, 24 Aug 2025 19:38:11 -0500 Subject: [PATCH 058/111] Define `trio.Event.__bool__()` to reduce bugs Closes #3238 --- src/trio/_sync.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/trio/_sync.py b/src/trio/_sync.py index ca373922b0..6f46245e31 100644 --- a/src/trio/_sync.py +++ b/src/trio/_sync.py @@ -1,7 +1,7 @@ from __future__ import annotations import math -from typing import TYPE_CHECKING, Protocol +from typing import TYPE_CHECKING, NoReturn, Protocol import attrs @@ -112,6 +112,14 @@ def statistics(self) -> EventStatistics: """ return EventStatistics(tasks_waiting=len(self._tasks)) + if not TYPE_CHECKING: + + def __bool__(self) -> NoReturn: + """Raise NotImplementedError.""" + raise NotImplementedError( + "Trio events cannot be treated as bools; consider using 'event.is_set()'" + ) + class _HasAcquireRelease(Protocol): """Only classes with acquire() and release() can use the mixin's implementations.""" From 24a8f08bf8eb3245b0d2611054bb9c61d06c1da3 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sun, 24 Aug 2025 20:46:05 -0500 Subject: [PATCH 059/111] Don't do if type checking block, jedi doesn't like that --- src/trio/_sync.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/trio/_sync.py b/src/trio/_sync.py index 6f46245e31..e5c5725c4d 100644 --- a/src/trio/_sync.py +++ b/src/trio/_sync.py @@ -112,13 +112,11 @@ def statistics(self) -> EventStatistics: """ return EventStatistics(tasks_waiting=len(self._tasks)) - if not TYPE_CHECKING: - - def __bool__(self) -> NoReturn: - """Raise NotImplementedError.""" - raise NotImplementedError( - "Trio events cannot be treated as bools; consider using 'event.is_set()'" - ) + def __bool__(self) -> NoReturn: + """Raise NotImplementedError.""" + raise NotImplementedError( + "Trio events cannot be treated as bools; consider using 'event.is_set()'" + ) class _HasAcquireRelease(Protocol): From 1ea6615510616b4f0b447b93dedd670c526f7e94 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Sun, 24 Aug 2025 22:10:19 -0500 Subject: [PATCH 060/111] Switch to deprecated warning --- src/trio/_sync.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/trio/_sync.py b/src/trio/_sync.py index e5c5725c4d..e95ae05374 100644 --- a/src/trio/_sync.py +++ b/src/trio/_sync.py @@ -1,7 +1,7 @@ from __future__ import annotations import math -from typing import TYPE_CHECKING, NoReturn, Protocol +from typing import TYPE_CHECKING, Protocol import attrs @@ -16,6 +16,7 @@ enable_ki_protection, remove_parking_lot_breaker, ) +from ._depreciate import warn_deprecated from ._util import final if TYPE_CHECKING: @@ -112,11 +113,16 @@ def statistics(self) -> EventStatistics: """ return EventStatistics(tasks_waiting=len(self._tasks)) - def __bool__(self) -> NoReturn: - """Raise NotImplementedError.""" - raise NotImplementedError( - "Trio events cannot be treated as bools; consider using 'event.is_set()'" + def __bool__(self) -> True: + """Return True and raise warning.""" + warn_deprecated( + self.__bool__, + "0.30.1", + issue=3238, + instead=self.is_set, + use_triodeprecationwarning=True, ) + return True class _HasAcquireRelease(Protocol): From 68a4fad041414dc89dea264cea044a45d04d597f Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 25 Aug 2025 09:25:15 -0500 Subject: [PATCH 061/111] Don't use trio's deprecated warning, add test, add newsfragment --- newsfragments/3322.deprecated.txt | 1 + src/trio/_sync.py | 3 +-- src/trio/_tests/test_sync.py | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 newsfragments/3322.deprecated.txt diff --git a/newsfragments/3322.deprecated.txt b/newsfragments/3322.deprecated.txt new file mode 100644 index 0000000000..ca920129e2 --- /dev/null +++ b/newsfragments/3322.deprecated.txt @@ -0,0 +1 @@ +Implement `trio.Event.__bool__` and have it raise a `DeprecationWarning` and tell users to use `trio.Event.is_set` instead. Would be caught earlier with `mypy --enable-error-code=truthy-bool`, this is for users who don't use mypy. diff --git a/src/trio/_sync.py b/src/trio/_sync.py index e95ae05374..8aac8528b1 100644 --- a/src/trio/_sync.py +++ b/src/trio/_sync.py @@ -16,7 +16,7 @@ enable_ki_protection, remove_parking_lot_breaker, ) -from ._depreciate import warn_deprecated +from ._deprecate import warn_deprecated from ._util import final if TYPE_CHECKING: @@ -120,7 +120,6 @@ def __bool__(self) -> True: "0.30.1", issue=3238, instead=self.is_set, - use_triodeprecationwarning=True, ) return True diff --git a/src/trio/_tests/test_sync.py b/src/trio/_tests/test_sync.py index 39f8d21f38..92655bbbc1 100644 --- a/src/trio/_tests/test_sync.py +++ b/src/trio/_tests/test_sync.py @@ -23,6 +23,12 @@ async def test_Event() -> None: assert not e.is_set() assert e.statistics().tasks_waiting == 0 + with pytest.warns( + DeprecationWarning, + match=r"trio\.Event\.__bool__ is deprecated since Trio 0\.30\.1; use trio\.Event\.is_set instead \(https://github.com/python-trio/trio/issues/3238\)", + ): + e.__bool__() + e.set() assert e.is_set() with assert_checkpoints(): From 93950f21531fb98766726529bcba6fb9bce9514d Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 26 Aug 2025 02:04:13 +0900 Subject: [PATCH 062/111] Apply suggestions from code review --- newsfragments/3322.deprecated.txt | 2 +- src/trio/_sync.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/newsfragments/3322.deprecated.txt b/newsfragments/3322.deprecated.txt index ca920129e2..0e1b69f32b 100644 --- a/newsfragments/3322.deprecated.txt +++ b/newsfragments/3322.deprecated.txt @@ -1 +1 @@ -Implement `trio.Event.__bool__` and have it raise a `DeprecationWarning` and tell users to use `trio.Event.is_set` instead. Would be caught earlier with `mypy --enable-error-code=truthy-bool`, this is for users who don't use mypy. +Implement ``bool(trio.Event)`` and have it raise a `DeprecationWarning` and tell users to use `trio.Event.is_set` instead. This is an alternative to ``mypy --enable-error-code=truthy-bool`` for users who don't use type checking. diff --git a/src/trio/_sync.py b/src/trio/_sync.py index 8aac8528b1..2463188588 100644 --- a/src/trio/_sync.py +++ b/src/trio/_sync.py @@ -113,7 +113,7 @@ def statistics(self) -> EventStatistics: """ return EventStatistics(tasks_waiting=len(self._tasks)) - def __bool__(self) -> True: + def __bool__(self) -> Literal[True]: """Return True and raise warning.""" warn_deprecated( self.__bool__, From 68e7242b9da090b3077edf6db4a4837fdde317d5 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 26 Aug 2025 02:08:23 +0900 Subject: [PATCH 063/111] Add necessary imports --- src/trio/_sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/trio/_sync.py b/src/trio/_sync.py index 2463188588..4058349167 100644 --- a/src/trio/_sync.py +++ b/src/trio/_sync.py @@ -1,7 +1,7 @@ from __future__ import annotations import math -from typing import TYPE_CHECKING, Protocol +from typing import TYPE_CHECKING, Protocol, Literal import attrs From 14e16fea4b9e47f69fec48ca2081bbe1c7d5bd32 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:08:39 +0000 Subject: [PATCH 064/111] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/trio/_sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/trio/_sync.py b/src/trio/_sync.py index 4058349167..89e49d6187 100644 --- a/src/trio/_sync.py +++ b/src/trio/_sync.py @@ -1,7 +1,7 @@ from __future__ import annotations import math -from typing import TYPE_CHECKING, Protocol, Literal +from typing import TYPE_CHECKING, Literal, Protocol import attrs From 3b14df291c3cfb3ab6c8b32b221da92cd4491492 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:05:28 -0500 Subject: [PATCH 065/111] Add `if not TYPE_CHECKING` --- src/trio/_sync.py | 20 +++++++++++--------- src/trio/_tests/test_sync.py | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/trio/_sync.py b/src/trio/_sync.py index 89e49d6187..eddf0094a1 100644 --- a/src/trio/_sync.py +++ b/src/trio/_sync.py @@ -113,15 +113,17 @@ def statistics(self) -> EventStatistics: """ return EventStatistics(tasks_waiting=len(self._tasks)) - def __bool__(self) -> Literal[True]: - """Return True and raise warning.""" - warn_deprecated( - self.__bool__, - "0.30.1", - issue=3238, - instead=self.is_set, - ) - return True + if not TYPE_CHECKING: + + def __bool__(self) -> Literal[True]: + """Return True and raise warning.""" + warn_deprecated( + self.__bool__, + "0.31.0", + issue=3238, + instead=self.is_set, + ) + return True class _HasAcquireRelease(Protocol): diff --git a/src/trio/_tests/test_sync.py b/src/trio/_tests/test_sync.py index 92655bbbc1..6096510d3f 100644 --- a/src/trio/_tests/test_sync.py +++ b/src/trio/_tests/test_sync.py @@ -25,7 +25,7 @@ async def test_Event() -> None: with pytest.warns( DeprecationWarning, - match=r"trio\.Event\.__bool__ is deprecated since Trio 0\.30\.1; use trio\.Event\.is_set instead \(https://github.com/python-trio/trio/issues/3238\)", + match=r"trio\.Event\.__bool__ is deprecated since Trio 0\.31\.0; use trio\.Event\.is_set instead \(https://github.com/python-trio/trio/issues/3238\)", ): e.__bool__() From 4347db2244465558857d7d085560b6bf6b971c79 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:09:24 -0500 Subject: [PATCH 066/111] Revert and use `typing_extensions.deprecated` instead --- pyproject.toml | 2 ++ src/trio/_sync.py | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bec87cfef0..8241677bcd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,8 @@ dependencies = [ # cffi is required on Windows, except on PyPy where it is built-in "cffi>=1.14; os_name == 'nt' and implementation_name != 'pypy'", "exceptiongroup; python_version < '3.11'", + # using typing_extensions.depreciated in _sync.py + "typing_extensions >= 4.15.0", ] dynamic = ["version"] diff --git a/src/trio/_sync.py b/src/trio/_sync.py index eddf0094a1..ec698992b3 100644 --- a/src/trio/_sync.py +++ b/src/trio/_sync.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Literal, Protocol import attrs +from typing_extensions import deprecated import trio @@ -113,17 +114,16 @@ def statistics(self) -> EventStatistics: """ return EventStatistics(tasks_waiting=len(self._tasks)) - if not TYPE_CHECKING: - - def __bool__(self) -> Literal[True]: - """Return True and raise warning.""" - warn_deprecated( - self.__bool__, - "0.31.0", - issue=3238, - instead=self.is_set, - ) - return True + @deprecated + def __bool__(self) -> Literal[True]: + """Return True and raise warning.""" + warn_deprecated( + self.__bool__, + "0.31.0", + issue=3238, + instead=self.is_set, + ) + return True class _HasAcquireRelease(Protocol): From 2a723ffacdb67c5f699064d446e65c57d82a8ae7 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:23:55 -0500 Subject: [PATCH 067/111] Add `typing_extensions` to requirements --- test-requirements.in | 1 + test-requirements.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test-requirements.in b/test-requirements.in index 90776153c4..1c33b6522c 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -8,6 +8,7 @@ trustme # for the ssl + DTLS tests pylint # for pylint finding all symbols tests jedi; implementation_name == "cpython" # for jedi code completion tests cryptography>=41.0.0 # cryptography<41 segfaults on pypy3.10 +typing_extensions >= 4.15.0 # Tools black; implementation_name == "cpython" diff --git a/test-requirements.txt b/test-requirements.txt index 3d12554ce6..c11c47e7af 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -187,7 +187,7 @@ types-pyyaml==6.0.12.20250822 # via -r test-requirements.in types-setuptools==80.9.0.20250822 # via types-cffi -typing-extensions==4.14.1 +typing-extensions==4.15.0 # via # -r test-requirements.in # astroid From e34ce248d6d1713a1986d13c93f2f49dda457228 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:29:56 -0500 Subject: [PATCH 068/111] Add message properly --- src/trio/_sync.py | 5 ++++- test-requirements.in | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/trio/_sync.py b/src/trio/_sync.py index ec698992b3..0744f2cea7 100644 --- a/src/trio/_sync.py +++ b/src/trio/_sync.py @@ -114,7 +114,10 @@ def statistics(self) -> EventStatistics: """ return EventStatistics(tasks_waiting=len(self._tasks)) - @deprecated + @deprecated( + "trio.Event.__bool__ is deprecated since Trio 0.31.0, use trio.Event.is_set instead (https://github.com/python-trio/trio/issues/3238)", + stacklevel=2, + ) def __bool__(self) -> Literal[True]: """Return True and raise warning.""" warn_deprecated( diff --git a/test-requirements.in b/test-requirements.in index 1c33b6522c..21ae962140 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -8,7 +8,6 @@ trustme # for the ssl + DTLS tests pylint # for pylint finding all symbols tests jedi; implementation_name == "cpython" # for jedi code completion tests cryptography>=41.0.0 # cryptography<41 segfaults on pypy3.10 -typing_extensions >= 4.15.0 # Tools black; implementation_name == "cpython" @@ -40,3 +39,4 @@ outcome sniffio # 1.2.1 fixes types exceptiongroup >= 1.2.1; python_version < "3.11" +typing_extensions >= 4.15.0 From c3a0a762c343e5e4876725da18714bc79f38314f Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 25 Aug 2025 17:40:22 -0500 Subject: [PATCH 069/111] `,` -> `;` --- src/trio/_sync.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/trio/_sync.py b/src/trio/_sync.py index 0744f2cea7..03d2c0cc4a 100644 --- a/src/trio/_sync.py +++ b/src/trio/_sync.py @@ -115,7 +115,7 @@ def statistics(self) -> EventStatistics: return EventStatistics(tasks_waiting=len(self._tasks)) @deprecated( - "trio.Event.__bool__ is deprecated since Trio 0.31.0, use trio.Event.is_set instead (https://github.com/python-trio/trio/issues/3238)", + "trio.Event.__bool__ is deprecated since Trio 0.31.0; use trio.Event.is_set instead (https://github.com/python-trio/trio/issues/3238)", stacklevel=2, ) def __bool__(self) -> Literal[True]: From 02c3794913a04a030f1f2ea76fb67b69abd90c3c Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 25 Aug 2025 20:29:00 -0500 Subject: [PATCH 070/111] Remove `typing_extensions` requirement --- pyproject.toml | 2 -- src/trio/_sync.py | 20 ++++++++++++++++++-- test-requirements.in | 1 - 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8241677bcd..bec87cfef0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,8 +50,6 @@ dependencies = [ # cffi is required on Windows, except on PyPy where it is built-in "cffi>=1.14; os_name == 'nt' and implementation_name != 'pypy'", "exceptiongroup; python_version < '3.11'", - # using typing_extensions.depreciated in _sync.py - "typing_extensions >= 4.15.0", ] dynamic = ["version"] diff --git a/src/trio/_sync.py b/src/trio/_sync.py index 03d2c0cc4a..d026f4bc37 100644 --- a/src/trio/_sync.py +++ b/src/trio/_sync.py @@ -1,10 +1,9 @@ from __future__ import annotations import math -from typing import TYPE_CHECKING, Literal, Protocol +from typing import TYPE_CHECKING, Literal, Protocol, TypeVar import attrs -from typing_extensions import deprecated import trio @@ -21,10 +20,27 @@ from ._util import final if TYPE_CHECKING: + from collections.abc import Callable from types import TracebackType + from typing_extensions import deprecated + from ._core import Task from ._core._parking_lot import ParkingLotStatistics +else: + T = TypeVar("T") + + def deprecated( + message: str, + /, + *, + category: type[Warning] | None = DeprecationWarning, + stacklevel: int = 1, + ) -> Callable[[T], T]: + def wrapper(f: T) -> T: + return f + + return wrapper @attrs.frozen diff --git a/test-requirements.in b/test-requirements.in index 21ae962140..90776153c4 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -39,4 +39,3 @@ outcome sniffio # 1.2.1 fixes types exceptiongroup >= 1.2.1; python_version < "3.11" -typing_extensions >= 4.15.0 From 8dd7d8ea38346b5effdfcc0669b356380eb670c9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 02:03:12 +0000 Subject: [PATCH 071/111] Dependency updates (#3323) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- docs-requirements.txt | 8 ++++---- test-requirements.txt | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 97a059b92c..791745821e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.10 + rev: v0.12.11 hooks: - id: ruff-check types: [file] @@ -38,7 +38,7 @@ repos: # tomli needed on 3.10. tomllib is available in stdlib on 3.11+ - tomli - repo: https://github.com/adhtruong/mirrors-typos - rev: v1.35.5 + rev: v1.35.6 hooks: - id: typos - repo: https://github.com/sphinx-contrib/sphinx-lint @@ -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.8.13 + rev: 0.8.14 hooks: # Compile requirements - id: pip-compile diff --git a/docs-requirements.txt b/docs-requirements.txt index 8cd7031fc4..88209712f6 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -8,7 +8,7 @@ attrs==25.3.0 # outcome babel==2.17.0 # via sphinx -beautifulsoup4==4.13.4 +beautifulsoup4==4.13.5 # via sphinx-codeautolink certifi==2025.8.3 # via requests @@ -67,7 +67,7 @@ snowballstemmer==3.0.1 # via sphinx sortedcontainers==2.4.0 # via -r docs-requirements.in -soupsieve==2.7 +soupsieve==2.8 # via beautifulsoup4 sphinx==8.2.3 # via @@ -98,9 +98,9 @@ sphinxcontrib-serializinghtml==2.0.0 # via sphinx sphinxcontrib-trio==1.1.2 # via -r docs-requirements.in -towncrier==24.8.0 +towncrier==25.8.0 # via -r docs-requirements.in -typing-extensions==4.14.1 +typing-extensions==4.15.0 # via # beautifulsoup4 # exceptiongroup diff --git a/test-requirements.txt b/test-requirements.txt index 3d12554ce6..265ad2365c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -40,7 +40,7 @@ colorama==0.4.6 ; sys_platform == 'win32' # pylint # pytest # sphinx -coverage==7.10.5 +coverage==7.10.6 # via -r test-requirements.in cryptography==45.0.6 # via @@ -94,7 +94,7 @@ nodeenv==1.9.1 # via # pre-commit # pyright -orjson==3.11.2 ; implementation_name == 'cpython' +orjson==3.11.3 ; implementation_name == 'cpython' # via -r test-requirements.in outcome==1.3.0.post0 # via -r test-requirements.in @@ -109,7 +109,7 @@ pathspec==0.12.1 # via # black # mypy -platformdirs==4.3.8 +platformdirs==4.4.0 # via # black # pylint @@ -138,7 +138,7 @@ requests==2.32.5 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.12.10 +ruff==0.12.11 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -187,7 +187,7 @@ types-pyyaml==6.0.12.20250822 # via -r test-requirements.in types-setuptools==80.9.0.20250822 # via types-cffi -typing-extensions==4.14.1 +typing-extensions==4.15.0 # via # -r test-requirements.in # astroid @@ -200,7 +200,7 @@ typing-extensions==4.14.1 # virtualenv urllib3==2.5.0 # via requests -uv==0.8.13 +uv==0.8.14 # via -r test-requirements.in virtualenv==20.34.0 # via pre-commit From 5bfba27b23f9a1f1bac86880b341bcfab54e43f2 Mon Sep 17 00:00:00 2001 From: richardsheridan Date: Tue, 2 Sep 2025 03:35:55 -0400 Subject: [PATCH 072/111] Improve REPL KI (#3030) * improve repl KI * apply suggestion from review, ensure calls in handler are safe * grab token and install handler in one go * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Don't add extra newlines on KI * Add some tests * First pass at CI failures * The CI runners have `dev.tty.legacy_tiocsti` set to `0` This means that we cannot test our usage of `TIOCSTI`. This ctrl+c support was dead on arrival! * Hacky fixes for Windows * Try to avoid flakiness * Address PR review and first pass at codecov * Start checking sysctls for `test_ki_newline_injection` * Try enabling the sysctl * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix pre-commit * Give up on TIOCSTI in CI * Actually skip newline injection on Windows * Actually skip newline injection tests on MacOS * Codecov annoyances --------- Co-authored-by: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: A5rocks --- newsfragments/3007.bugfix.rst | 1 + src/trio/_repl.py | 75 ++++++++++++- src/trio/_tests/test_repl.py | 194 ++++++++++++++++++++++++++++++++++ 3 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 newsfragments/3007.bugfix.rst diff --git a/newsfragments/3007.bugfix.rst b/newsfragments/3007.bugfix.rst new file mode 100644 index 0000000000..da6732395a --- /dev/null +++ b/newsfragments/3007.bugfix.rst @@ -0,0 +1 @@ +Make ctrl+c work in more situations in the Trio REPL (``python -m trio``). diff --git a/src/trio/_repl.py b/src/trio/_repl.py index c8863989d2..5a96e68789 100644 --- a/src/trio/_repl.py +++ b/src/trio/_repl.py @@ -4,9 +4,10 @@ import contextlib import inspect import sys -import types import warnings from code import InteractiveConsole +from types import CodeType, FrameType, FunctionType +from typing import Callable import outcome @@ -15,14 +16,33 @@ from trio._util import final +class SuppressDecorator(contextlib.ContextDecorator, contextlib.suppress): + pass + + +@SuppressDecorator(KeyboardInterrupt) +@trio.lowlevel.disable_ki_protection +def terminal_newline() -> None: # TODO: test this line + import fcntl + import termios + + # Fake up a newline char as if user had typed it at the terminal + try: + fcntl.ioctl(sys.stdin, termios.TIOCSTI, b"\n") # type: ignore[attr-defined, unused-ignore] + except OSError as e: + print(f"\nPress enter! Newline injection failed: {e}", end="", flush=True) + + @final class TrioInteractiveConsole(InteractiveConsole): def __init__(self, repl_locals: dict[str, object] | None = None) -> None: super().__init__(locals=repl_locals) + self.token: trio.lowlevel.TrioToken | None = None self.compile.compiler.flags |= ast.PyCF_ALLOW_TOP_LEVEL_AWAIT + self.interrupted = False - def runcode(self, code: types.CodeType) -> None: - func = types.FunctionType(code, self.locals) + def runcode(self, code: CodeType) -> None: + func = FunctionType(code, self.locals) if inspect.iscoroutinefunction(func): result = trio.from_thread.run(outcome.acapture, func) else: @@ -48,6 +68,55 @@ def runcode(self, code: types.CodeType) -> None: # We always use sys.excepthook, unlike other implementations. # This means that overriding self.write also does nothing to tbs. sys.excepthook(sys.last_type, sys.last_value, sys.last_traceback) + # clear any residual KI + trio.from_thread.run(trio.lowlevel.checkpoint_if_cancelled) + # trio.from_thread.check_cancelled() has too long of a memory + + if sys.platform == "win32": # TODO: test this line + + def raw_input(self, prompt: str = "") -> str: + try: + return input(prompt) + except EOFError: + # check if trio has a pending KI + trio.from_thread.run(trio.lowlevel.checkpoint_if_cancelled) + raise + + else: + + def raw_input(self, prompt: str = "") -> str: + from signal import SIGINT, signal + + assert not self.interrupted + + def install_handler() -> ( + Callable[[int, FrameType | None], None] | int | None + ): + def handler( + sig: int, frame: FrameType | None + ) -> None: # TODO: test this line + self.interrupted = True + token.run_sync_soon(terminal_newline, idempotent=True) + + token = trio.lowlevel.current_trio_token() + + return signal(SIGINT, handler) + + prev_handler = trio.from_thread.run_sync(install_handler) + try: + return input(prompt) + finally: + trio.from_thread.run_sync(signal, SIGINT, prev_handler) + if self.interrupted: # TODO: test this line + raise KeyboardInterrupt + + def write(self, output: str) -> None: + if self.interrupted: # TODO: test this line + assert output == "\nKeyboardInterrupt\n" + sys.stderr.write(output[1:]) + self.interrupted = False + else: + sys.stderr.write(output) async def run_repl(console: TrioInteractiveConsole) -> None: diff --git a/src/trio/_tests/test_repl.py b/src/trio/_tests/test_repl.py index be9338ce4c..ae125d9ab0 100644 --- a/src/trio/_tests/test_repl.py +++ b/src/trio/_tests/test_repl.py @@ -1,7 +1,11 @@ from __future__ import annotations +import os +import pathlib +import signal import subprocess import sys +from functools import partial from typing import Protocol import pytest @@ -239,3 +243,193 @@ def test_main_entrypoint() -> None: """ repl = subprocess.run([sys.executable, "-m", "trio"], input=b"exit()") assert repl.returncode == 0 + + +def should_try_newline_injection() -> bool: + if sys.platform != "linux": + return False + + sysctl = pathlib.Path("/proc/sys/dev/tty/legacy_tiocsti") + if not sysctl.exists(): # pragma: no cover + return True + + else: + return sysctl.read_text() == "1" + + +@pytest.mark.skipif( + not should_try_newline_injection(), + reason="the ioctl we use is disabled in CI", +) +def test_ki_newline_injection() -> None: # TODO: test this line + # TODO: we want to remove this functionality, eg by using vendored + # pyrepls. + assert sys.platform != "win32" + + import pty + + # NOTE: this cannot be subprocess.Popen because pty.fork + # does some magic to set the controlling terminal. + # (which I don't know how to replicate... so I copied this + # structure from pty.spawn...) + pid, pty_fd = pty.fork() # type: ignore[attr-defined,unused-ignore] + if pid == 0: + os.execlp(sys.executable, *[sys.executable, "-u", "-m", "trio"]) + + # setup: + buffer = b"" + while not buffer.endswith(b"import trio\r\n>>> "): + buffer += os.read(pty_fd, 4096) + + # sanity check: + print(buffer.decode()) + buffer = b"" + os.write(pty_fd, b'print("hello!")\n') + while not buffer.endswith(b">>> "): + buffer += os.read(pty_fd, 4096) + + assert buffer.count(b"hello!") == 2 + + # press ctrl+c + print(buffer.decode()) + buffer = b"" + os.kill(pid, signal.SIGINT) + while not buffer.endswith(b">>> "): + buffer += os.read(pty_fd, 4096) + + assert b"KeyboardInterrupt" in buffer + + # press ctrl+c later + print(buffer.decode()) + buffer = b"" + os.write(pty_fd, b'print("hello!")') + os.kill(pid, signal.SIGINT) + while not buffer.endswith(b">>> "): + buffer += os.read(pty_fd, 4096) + + assert b"KeyboardInterrupt" in buffer + print(buffer.decode()) + os.close(pty_fd) + os.waitpid(pid, 0)[1] + + +async def test_ki_in_repl() -> None: + async with trio.open_nursery() as nursery: + proc = await nursery.start( + partial( + trio.run_process, + [sys.executable, "-u", "-m", "trio"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + stdin=subprocess.PIPE, + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if sys.platform == "win32" else 0, # type: ignore[attr-defined,unused-ignore] + ) + ) + + async with proc.stdout: + # setup + buffer = b"" + async for part in proc.stdout: # pragma: no branch + buffer += part + # TODO: consider making run_process stdout have some universal newlines thing + if buffer.replace(b"\r\n", b"\n").endswith(b"import trio\n>>> "): + break + + # ensure things work + print(buffer.decode()) + buffer = b"" + await proc.stdin.send_all(b'print("hello!")\n') + async for part in proc.stdout: # pragma: no branch + buffer += part + if buffer.endswith(b">>> "): + break + + assert b"hello!" in buffer + print(buffer.decode()) + + # this seems to be necessary on Windows for reasons + # (the parents of process groups ignore ctrl+c by default...) + if sys.platform == "win32": + buffer = b"" + await proc.stdin.send_all( + b"import ctypes; ctypes.windll.kernel32.SetConsoleCtrlHandler(None, False)\n" + ) + async for part in proc.stdout: # pragma: no branch + buffer += part + if buffer.endswith(b">>> "): + break + + print(buffer.decode()) + + # try to decrease flakiness... + buffer = b"" + await proc.stdin.send_all( + b"import coverage; trio.lowlevel.enable_ki_protection(coverage.pytracer.PyTracer._trace)\n" + ) + async for part in proc.stdout: # pragma: no branch + buffer += part + if buffer.endswith(b">>> "): + break + + print(buffer.decode()) + + # ensure that ctrl+c on a prompt works + # NOTE: for some reason, signal.SIGINT doesn't work for this test. + # Using CTRL_C_EVENT is also why we need subprocess.CREATE_NEW_PROCESS_GROUP + signal_sent = signal.CTRL_C_EVENT if sys.platform == "win32" else signal.SIGINT # type: ignore[attr-defined,unused-ignore] + os.kill(proc.pid, signal_sent) + if sys.platform == "win32": + # we rely on EOFError which... doesn't happen with pipes. + # I'm not sure how to fix it... + await proc.stdin.send_all(b"\n") + else: + # we test injection separately + await proc.stdin.send_all(b"\n") + + buffer = b"" + async for part in proc.stdout: # pragma: no branch + buffer += part + if buffer.endswith(b">>> "): + break + + assert b"KeyboardInterrupt" in buffer + + # ensure ctrl+c while a command runs works + print(buffer.decode()) + await proc.stdin.send_all(b'print("READY"); await trio.sleep_forever()\n') + killed = False + buffer = b"" + async for part in proc.stdout: # pragma: no branch + buffer += part + if buffer.replace(b"\r\n", b"\n").endswith(b"READY\n") and not killed: + os.kill(proc.pid, signal_sent) + killed = True + if buffer.endswith(b">>> "): + break + + assert b"trio" in buffer + assert b"KeyboardInterrupt" in buffer + + # make sure it works for sync commands too + # (though this would be hard to break) + print(buffer.decode()) + await proc.stdin.send_all( + b'import time; print("READY"); time.sleep(99999)\n' + ) + killed = False + buffer = b"" + async for part in proc.stdout: # pragma: no branch + buffer += part + if buffer.replace(b"\r\n", b"\n").endswith(b"READY\n") and not killed: + os.kill(proc.pid, signal_sent) + killed = True + if buffer.endswith(b">>> "): + break + + assert b"Traceback" in buffer + assert b"KeyboardInterrupt" in buffer + + print(buffer.decode()) + + # kill the process + nursery.cancel_scope.cancel() From fc352b2b8ee84071273142375d8babb44333d115 Mon Sep 17 00:00:00 2001 From: John Litborn <11260241+jakkdl@users.noreply.github.com> Date: Wed, 3 Sep 2025 06:38:36 +0200 Subject: [PATCH 073/111] check for nursery misnesting on task exit (#3307) * check for nursery misnesting on task exit ... not working * forcefully abort children of unclosed nurseries to avoid internalerror * call Runner.task_exited on aborted tasks to get errors for aborted tasks and handle nested scenarios (needs test) * new approach * suppress Cancelled if a CancelScope is abandoned by misnesting * add example of losing exception from abandoned task * add newsfragment --- newsfragments/3307.misc.rst | 1 + src/trio/_core/_run.py | 24 +++++- src/trio/_core/_tests/test_run.py | 138 +++++++++++++++++++++++++++++- test-requirements.in | 2 +- 4 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 newsfragments/3307.misc.rst diff --git a/newsfragments/3307.misc.rst b/newsfragments/3307.misc.rst new file mode 100644 index 0000000000..ab59183729 --- /dev/null +++ b/newsfragments/3307.misc.rst @@ -0,0 +1 @@ +When misnesting nurseries you now get a helpful :exc:`RuntimeError` instead of a catastrophic :exc:`TrioInternalError`. diff --git a/src/trio/_core/_run.py b/src/trio/_core/_run.py index aee025cb7a..5303dfe75d 100644 --- a/src/trio/_core/_run.py +++ b/src/trio/_core/_run.py @@ -678,6 +678,9 @@ def _close(self, exc: BaseException | None) -> BaseException | None: exc is not None and self._cancel_status.effectively_cancelled and not self._cancel_status.parent_cancellation_is_visible_to_us + ) or ( + scope_task._cancel_status is not self._cancel_status + and self._cancel_status.abandoned_by_misnesting ): if isinstance(exc, Cancelled): self.cancelled_caught = True @@ -1261,6 +1264,9 @@ def _child_finished( outcome: Outcome[object], ) -> None: self._children.remove(task) + if self._closed and not hasattr(self, "_pending_excs"): + # We're abandoned by misnested nurseries, the result of the task is lost. + return if isinstance(outcome, Error): self._add_exc( outcome.error, @@ -1321,7 +1327,7 @@ def aborted(raise_cancel: _core.RaiseCancelT) -> Abort: self._add_exc(exc, reason=None) popped = self._parent_task._child_nurseries.pop() - assert popped is self + assert popped is self, "Nursery misnesting detected!" if self._pending_excs: try: if not self._strict_exception_groups and len(self._pending_excs) == 1: @@ -2007,6 +2013,17 @@ async def python_wrapper(orig_coro: Awaitable[RetT]) -> RetT: return task def task_exited(self, task: Task, outcome: Outcome[object]) -> None: + if task._child_nurseries: + for nursery in task._child_nurseries: + nursery.cancel_scope._cancel( + CancelReason( + source="nursery", + reason="Parent Task exited prematurely, abandoning this nursery without exiting it properly.", + source_task=repr(task), + ) + ) + nursery._closed = True + # break parking lots associated with the exiting task if task in GLOBAL_PARKING_LOT_BREAKER: for lot in GLOBAL_PARKING_LOT_BREAKER[task]: @@ -2017,7 +2034,8 @@ def task_exited(self, task: Task, outcome: Outcome[object]) -> None: task._cancel_status is not None and task._cancel_status.abandoned_by_misnesting and task._cancel_status.parent is None - ): + ) or task._child_nurseries: + reason = "Nursery" if task._child_nurseries else "Cancel scope" # The cancel scope surrounding this task's nursery was closed # before the task exited. Force the task to exit with an error, # since the error might not have been caught elsewhere. See the @@ -2026,7 +2044,7 @@ def task_exited(self, task: Task, outcome: Outcome[object]) -> None: # Raise this, rather than just constructing it, to get a # traceback frame included raise RuntimeError( - "Cancel scope stack corrupted: cancel scope surrounding " + f"{reason} stack corrupted: {reason} surrounding " f"{task!r} was closed before the task exited\n{MISNESTING_ADVICE}", ) except RuntimeError as new_exc: diff --git a/src/trio/_core/_tests/test_run.py b/src/trio/_core/_tests/test_run.py index b317c8d6c9..111ba9e5ec 100644 --- a/src/trio/_core/_tests/test_run.py +++ b/src/trio/_core/_tests/test_run.py @@ -8,7 +8,13 @@ import time import types import weakref -from contextlib import ExitStack, contextmanager, suppress +from contextlib import ( + AsyncExitStack, + ExitStack, + asynccontextmanager, + contextmanager, + suppress, +) from math import inf, nan from typing import TYPE_CHECKING, NoReturn, TypeVar from unittest import mock @@ -761,7 +767,7 @@ async def enter_scope() -> None: assert scope.cancel_called # never become un-cancelled -async def test_cancel_scope_misnesting() -> None: +async def test_cancel_scope_misnesting_1() -> None: outer = _core.CancelScope() inner = _core.CancelScope() with ExitStack() as stack: @@ -771,6 +777,8 @@ async def test_cancel_scope_misnesting() -> None: stack.close() # No further error is raised when exiting the inner context + +async def test_cancel_scope_misnesting_2() -> None: # If there are other tasks inside the abandoned part of the cancel tree, # they get cancelled when the misnesting is detected async def task1() -> None: @@ -828,6 +836,8 @@ def no_context(exc: RuntimeError) -> bool: ) assert group.matches(exc_info.value.__context__) + +async def test_cancel_scope_misnesting_3() -> None: # Trying to exit a cancel scope from an unrelated task raises an error # without affecting any state async def task3(task_status: _core.TaskStatus[_core.CancelScope]) -> None: @@ -844,6 +854,130 @@ async def task3(task_status: _core.TaskStatus[_core.CancelScope]) -> None: scope.cancel() +# helper to check we're not outputting overly verbose tracebacks +def no_cause_or_context(e: BaseException) -> bool: + return e.__cause__ is None and e.__context__ is None + + +async def test_nursery_misnest() -> None: + # See https://github.com/python-trio/trio/issues/3298 + async def inner_func() -> None: + inner_nursery = await inner_cm.__aenter__() + inner_nursery.start_soon(sleep, 1) + + with pytest.RaisesGroup( + pytest.RaisesExc( + RuntimeError, match="Nursery stack corrupted", check=no_cause_or_context + ), + check=no_cause_or_context, + ): + async with _core.open_nursery() as outer_nursery: + inner_cm = _core.open_nursery() + outer_nursery.start_soon(inner_func) + + +def test_nursery_nested_child_misnest() -> None: + # Note that this example does *not* raise an exception group. + async def main() -> None: + async with _core.open_nursery(): + inner_cm = _core.open_nursery() + await inner_cm.__aenter__() + + with pytest.raises(RuntimeError, match="Nursery stack corrupted") as excinfo: + _core.run(main) + assert excinfo.value.__cause__ is None + # This AssertionError is kind of redundant, but I don't think we want to remove + # the assertion and don't think we care enough to suppress it in this specific case. + assert pytest.RaisesExc( + AssertionError, match="^Nursery misnesting detected!$" + ).matches(excinfo.value.__context__) + assert excinfo.value.__context__.__cause__ is None + assert excinfo.value.__context__.__context__ is None + + +async def test_asyncexitstack_nursery_misnest() -> None: + # This example is trickier than the above ones, and is the one that requires + # special logic of abandoned nurseries to avoid nasty internal errors that masks + # the RuntimeError. + @asynccontextmanager + async def asynccontextmanager_that_creates_a_nursery_internally() -> ( + AsyncGenerator[None] + ): + async with _core.open_nursery() as nursery: + await nursery.start(started_sleeper) + nursery.start_soon(unstarted_task) + yield + + async def started_sleeper(task_status: _core.TaskStatus[None]) -> None: + task_status.started() + await sleep_forever() + + async def unstarted_task() -> None: + await _core.checkpoint() + + with pytest.RaisesGroup( + pytest.RaisesGroup( + pytest.RaisesExc( + RuntimeError, match="Nursery stack corrupted", check=no_cause_or_context + ), + check=no_cause_or_context, + ), + check=no_cause_or_context, + ): + async with AsyncExitStack() as stack, _core.open_nursery() as nursery: + # The asynccontextmanager is going to create a nursery that outlives this nursery! + nursery.start_soon( + stack.enter_async_context, + asynccontextmanager_that_creates_a_nursery_internally(), + ) + + +def test_asyncexitstack_nursery_misnest_cleanup() -> None: + # We guarantee that abandoned tasks get to do cleanup *eventually*, but exceptions + # are lost. With more effort it's possible we could reschedule child tasks to exit + # promptly. + finally_entered = [] + + async def main() -> None: + async def unstarted_task() -> None: + try: + await _core.checkpoint() + finally: + finally_entered.append(True) + raise ValueError("this exception is lost") + + # rest of main() is ~identical to the above test + @asynccontextmanager + async def asynccontextmanager_that_creates_a_nursery_internally() -> ( + AsyncGenerator[None] + ): + async with _core.open_nursery() as nursery: + nursery.start_soon(unstarted_task) + yield + + with pytest.RaisesGroup( + pytest.RaisesGroup( + pytest.RaisesExc( + RuntimeError, + match="Nursery stack corrupted", + check=no_cause_or_context, + ), + check=no_cause_or_context, + ), + check=no_cause_or_context, + ): + async with AsyncExitStack() as stack, _core.open_nursery() as nursery: + # The asynccontextmanager is going to create a nursery that outlives this nursery! + nursery.start_soon( + stack.enter_async_context, + asynccontextmanager_that_creates_a_nursery_internally(), + ) + assert not finally_entered # abandoned task still hasn't been cleaned up + + _core.run(main) + assert finally_entered # now it has + + @slow async def test_timekeeping() -> None: # probably a good idea to use a real clock for *one* test anyway... diff --git a/test-requirements.in b/test-requirements.in index 90776153c4..e49dbd1782 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 # for pytest.RaisesGroup coverage >= 7.2.5 async_generator >= 1.9 pyright From cc148cfc0f23934aa603012b4f152c990e375f62 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 9 Sep 2025 01:35:53 +0900 Subject: [PATCH 074/111] Suppress GeneratorExit from `.aclose()` for `as_safe_channel` (#3325) --- newsfragments/3324.bugfix.rst | 2 + src/trio/_channel.py | 31 ++++++++++++++- src/trio/_tests/test_channel.py | 70 +++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 newsfragments/3324.bugfix.rst diff --git a/newsfragments/3324.bugfix.rst b/newsfragments/3324.bugfix.rst new file mode 100644 index 0000000000..0fd5a05399 --- /dev/null +++ b/newsfragments/3324.bugfix.rst @@ -0,0 +1,2 @@ +Avoid having `trio.as_safe_channel` raise if closing the generator wrapped +`GeneratorExit` in a `BaseExceptionGroup`. diff --git a/src/trio/_channel.py b/src/trio/_channel.py index 9caa7f7699..2afca9d7cd 100644 --- a/src/trio/_channel.py +++ b/src/trio/_channel.py @@ -570,6 +570,8 @@ async def _move_elems_to_channel( # `async with send_chan` will eat exceptions, # see https://github.com/python-trio/trio/issues/1559 with send_chan: + # replace try-finally with contextlib.aclosing once python39 is + # dropped: try: task_status.started() while True: @@ -582,7 +584,32 @@ async def _move_elems_to_channel( # Send the value to the channel await send_chan.send(value) finally: - # replace try-finally with contextlib.aclosing once python39 is dropped - await agen.aclose() + # work around `.aclose()` not suppressing GeneratorExit in an + # ExceptionGroup: + # TODO: make an issue on CPython about this + try: + await agen.aclose() + except BaseExceptionGroup as exceptions: + removed, narrowed_exceptions = exceptions.split(GeneratorExit) + + # TODO: extract a helper to flatten exception groups + removed_exceptions: list[BaseException | None] = [removed] + genexits_seen = 0 + for e in removed_exceptions: + if isinstance(e, BaseExceptionGroup): + removed_exceptions.extend(e.exceptions) # noqa: B909 + else: + genexits_seen += 1 + + if genexits_seen > 1: + exc = AssertionError("More than one GeneratorExit found.") + if narrowed_exceptions is None: + narrowed_exceptions = exceptions.derive([exc]) + else: + narrowed_exceptions = narrowed_exceptions.derive( + [*narrowed_exceptions.exceptions, exc] + ) + if narrowed_exceptions is not None: + raise narrowed_exceptions from None return context_manager diff --git a/src/trio/_tests/test_channel.py b/src/trio/_tests/test_channel.py index f1556a153c..85cd982a41 100644 --- a/src/trio/_tests/test_channel.py +++ b/src/trio/_tests/test_channel.py @@ -625,3 +625,73 @@ async def agen(events: list[str]) -> AsyncGenerator[None]: events.append("body cancel") raise assert events == ["body cancel", "agen cancel"] + + +async def test_as_safe_channel_genexit_exception_group() -> None: + @as_safe_channel + async def agen() -> AsyncGenerator[None]: + try: + async with trio.open_nursery(): + yield + except BaseException as e: + assert pytest.RaisesGroup(GeneratorExit).matches(e) # noqa: PT017 + raise + + async with agen() as g: + async for _ in g: + break + + +async def test_as_safe_channel_does_not_suppress_nested_genexit() -> None: + @as_safe_channel + async def agen() -> AsyncGenerator[None]: + yield + + with pytest.RaisesGroup(GeneratorExit): + async with agen() as g, trio.open_nursery(): + await g.receive() # this is for coverage reasons + raise GeneratorExit + + +async def test_as_safe_channel_genexit_filter() -> None: + async def wait_then_raise() -> None: + try: + await trio.sleep_forever() + except trio.Cancelled: + raise ValueError from None + + @as_safe_channel + async def agen() -> AsyncGenerator[None]: + async with trio.open_nursery() as nursery: + nursery.start_soon(wait_then_raise) + yield + + with pytest.RaisesGroup(ValueError): + async with agen() as g: + async for _ in g: + break + + +async def test_as_safe_channel_swallowing_extra_exceptions() -> None: + async def wait_then_raise(ex: type[BaseException]) -> None: + try: + await trio.sleep_forever() + except trio.Cancelled: + raise ex from None + + @as_safe_channel + async def agen(ex: type[BaseException]) -> AsyncGenerator[None]: + async with trio.open_nursery() as nursery: + nursery.start_soon(wait_then_raise, ex) + nursery.start_soon(wait_then_raise, GeneratorExit) + yield + + with pytest.RaisesGroup(AssertionError): + async with agen(GeneratorExit) as g: + async for _ in g: + break + + with pytest.RaisesGroup(ValueError, AssertionError): + async with agen(ValueError) as g: + async for _ in g: + break From b8dd570326316c1a1b896bf4fa5c2fdbf4082a36 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Mon, 8 Sep 2025 22:53:00 +0500 Subject: [PATCH 075/111] test: add extra test cases for 0 limit `CapacityLimiter` --- src/trio/_tests/test_sync.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/trio/_tests/test_sync.py b/src/trio/_tests/test_sync.py index 2815b489f2..fc33b2ec01 100644 --- a/src/trio/_tests/test_sync.py +++ b/src/trio/_tests/test_sync.py @@ -185,6 +185,41 @@ async def test_CapacityLimiter_memleak_548() -> None: assert len(limiter._pending_borrowers) == 0 +async def test_CapacityLimiter_zero_limit_tokens() -> None: + c = CapacityLimiter(5) + + assert c.total_tokens == 5 + + async with _core.open_nursery() as nursery: + c.total_tokens = 0 + + for i in range(6): + nursery.start_soon(c.acquire_on_behalf_of, i) + await wait_all_tasks_blocked() + + assert set(c.statistics().borrowers) == set() + + c.total_tokens = 5 + + assert set(c.statistics().borrowers) == {0, 1, 2, 3, 4} + + for i in range(6): + c.release_on_behalf_of(i) + + # making sure that zero limit capacity limiter doesn't let any tasks through + + c.total_tokens = 0 + + with pytest.raises(_core.WouldBlock): + c.acquire_nowait() + + nursery.cancel_scope.cancel() + + assert c.total_tokens == 0 + assert c.statistics().borrowers == [] + assert c._pending_borrowers == {} + + async def test_Semaphore() -> None: with pytest.raises(TypeError): Semaphore(1.0) # type: ignore[arg-type] From 8062c8f103390bddb50e0c5eb9f1e02bdba57a85 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Mon, 8 Sep 2025 22:57:18 +0500 Subject: [PATCH 076/111] chore: add newsfragment --- newsfragments/3321.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/3321.feature.rst diff --git a/newsfragments/3321.feature.rst b/newsfragments/3321.feature.rst new file mode 100644 index 0000000000..d7014999b3 --- /dev/null +++ b/newsfragments/3321.feature.rst @@ -0,0 +1 @@ +Allow ``CapacityLimiter`` to have zero total_tokens. \ No newline at end of file From ac393c3bcdecaeb47c50a8c6fbd06db544920536 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Mon, 8 Sep 2025 23:11:52 +0500 Subject: [PATCH 077/111] fix: pre-commit new line issue --- newsfragments/3321.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsfragments/3321.feature.rst b/newsfragments/3321.feature.rst index d7014999b3..ac13695406 100644 --- a/newsfragments/3321.feature.rst +++ b/newsfragments/3321.feature.rst @@ -1 +1 @@ -Allow ``CapacityLimiter`` to have zero total_tokens. \ No newline at end of file +Allow ``CapacityLimiter`` to have zero total_tokens. From 439de50f5e3c9a718a2b4ed5790f27198de33e13 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 21:32:25 +0000 Subject: [PATCH 078/111] [pre-commit.ci] pre-commit autoupdate (#3327) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.11 → v0.12.12](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.11...v0.12.12) - [github.com/adhtruong/mirrors-typos: v1.35.6 → v1.36.2](https://github.com/adhtruong/mirrors-typos/compare/v1.35.6...v1.36.2) - [github.com/astral-sh/uv-pre-commit: 0.8.14 → 0.8.15](https://github.com/astral-sh/uv-pre-commit/compare/0.8.14...0.8.15) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- test-requirements.txt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 791745821e..79e003a49a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.11 + rev: v0.12.12 hooks: - id: ruff-check types: [file] @@ -38,7 +38,7 @@ repos: # tomli needed on 3.10. tomllib is available in stdlib on 3.11+ - tomli - repo: https://github.com/adhtruong/mirrors-typos - rev: v1.35.6 + rev: v1.36.2 hooks: - id: typos - repo: https://github.com/sphinx-contrib/sphinx-lint @@ -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.8.14 + rev: 0.8.15 hooks: # Compile requirements - id: pip-compile diff --git a/test-requirements.txt b/test-requirements.txt index 265ad2365c..66dad38bb8 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -138,7 +138,7 @@ requests==2.32.5 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.12.11 +ruff==0.12.12 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -200,7 +200,7 @@ typing-extensions==4.15.0 # virtualenv urllib3==2.5.0 # via requests -uv==0.8.14 +uv==0.8.15 # via -r test-requirements.in virtualenv==20.34.0 # via pre-commit From e26c18a49730b577ab1b384c06b2680b75439bdc Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Tue, 9 Sep 2025 06:17:23 +0500 Subject: [PATCH 079/111] fix: in newsfragment, refer to as a link --- newsfragments/3321.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsfragments/3321.feature.rst b/newsfragments/3321.feature.rst index ac13695406..e62649bbd2 100644 --- a/newsfragments/3321.feature.rst +++ b/newsfragments/3321.feature.rst @@ -1 +1 @@ -Allow ``CapacityLimiter`` to have zero total_tokens. +Allow `trio.CapacityLimiter` to have zero total_tokens. From de008aac87c3152d9ae7bbc359b980cb50c878a3 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Tue, 9 Sep 2025 06:39:25 +0500 Subject: [PATCH 080/111] test: extra checks for waiting tasks --- src/trio/_tests/test_sync.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/trio/_tests/test_sync.py b/src/trio/_tests/test_sync.py index 3554e611a2..72c9359912 100644 --- a/src/trio/_tests/test_sync.py +++ b/src/trio/_tests/test_sync.py @@ -199,19 +199,28 @@ async def test_CapacityLimiter_zero_limit_tokens() -> None: async with _core.open_nursery() as nursery: c.total_tokens = 0 - for i in range(6): + for i in range(5): nursery.start_soon(c.acquire_on_behalf_of, i) await wait_all_tasks_blocked() assert set(c.statistics().borrowers) == set() + assert c.statistics().tasks_waiting == 5 c.total_tokens = 5 assert set(c.statistics().borrowers) == {0, 1, 2, 3, 4} - for i in range(6): + nursery.start_soon(c.acquire_on_behalf_of, 5) + await wait_all_tasks_blocked() + + assert c.statistics().tasks_waiting == 1 + + for i in range(5): c.release_on_behalf_of(i) + assert c.statistics().tasks_waiting == 0 + c.release_on_behalf_of(5) + # making sure that zero limit capacity limiter doesn't let any tasks through c.total_tokens = 0 @@ -219,6 +228,19 @@ async def test_CapacityLimiter_zero_limit_tokens() -> None: with pytest.raises(_core.WouldBlock): c.acquire_nowait() + nursery.start_soon(c.acquire_on_behalf_of, 6) + await wait_all_tasks_blocked() + + assert c.statistics().tasks_waiting == 1 + assert c.statistics().borrowers == [] + + c.total_tokens = 1 + assert c.statistics().tasks_waiting == 0 + assert c.statistics().borrowers == [6] + + c.release_on_behalf_of(6) + c.total_tokens = 0 + nursery.cancel_scope.cancel() assert c.total_tokens == 0 From 94b7244505212061e21507c84725ef28be2705dd Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 9 Sep 2025 23:54:02 +0900 Subject: [PATCH 081/111] Bump version to 0.31.0 --- docs/source/history.rst | 33 +++++++++++++++++++++++++++++++ newsfragments/3007.bugfix.rst | 1 - newsfragments/3232.feature.rst | 1 - newsfragments/3248.bugfix.rst | 1 - newsfragments/3263.bugfix.rst | 1 - newsfragments/3275.bugfix.rst | 1 - newsfragments/3277.bugfix.rst | 1 - newsfragments/3307.misc.rst | 1 - newsfragments/3322.deprecated.txt | 1 - newsfragments/3324.bugfix.rst | 2 -- src/trio/_version.py | 2 +- 11 files changed, 34 insertions(+), 11 deletions(-) delete mode 100644 newsfragments/3007.bugfix.rst delete mode 100644 newsfragments/3232.feature.rst delete mode 100644 newsfragments/3248.bugfix.rst delete mode 100644 newsfragments/3263.bugfix.rst delete mode 100644 newsfragments/3275.bugfix.rst delete mode 100644 newsfragments/3277.bugfix.rst delete mode 100644 newsfragments/3307.misc.rst delete mode 100644 newsfragments/3322.deprecated.txt delete mode 100644 newsfragments/3324.bugfix.rst diff --git a/docs/source/history.rst b/docs/source/history.rst index 762f9eecae..da41b29d16 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -5,6 +5,39 @@ Release history .. towncrier release notes start +Trio 0.31.0 (2025-09-09) +------------------------ + +Features +~~~~~~~~ + +- :exc:`Cancelled` strings can now display the source and reason for a cancellation. Trio-internal sources of cancellation will set this string, and :meth:`CancelScope.cancel` now has a ``reason`` string parameter that can be used to attach info to any :exc:`Cancelled` to help in debugging. (`#3232 `__) + + +Bugfixes +~~~~~~~~ + +- Make ctrl+c work in more situations in the Trio REPL (``python -m trio``). (`#3007 `__) +- Allow pickling `trio.Cancelled`, as they can show up when you want to pickle something else. This does not rule out pickling other ``NoPublicConstructor`` objects -- create an issue if necessary. (`#3248 `__) +- Decrease import time on Windows by around 10%. (`#3263 `__) +- Handle unwrapping SystemExit/KeyboardInterrupt exception gracefully in utility function ``raise_single_exception_from_group`` that reraises last exception from group. (`#3275 `__) +- Ensure that the DTLS server does not mutate SSL context. (`#3277 `__) +- Avoid having `trio.as_safe_channel` raise if closing the generator wrapped + `GeneratorExit` in a `BaseExceptionGroup`. (`#3324 `__) + + +Deprecations and removals +~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Implement ``bool(trio.Event)`` and have it raise a `DeprecationWarning` and tell users to use `trio.Event.is_set` instead. This is an alternative to ``mypy --enable-error-code=truthy-bool`` for users who don't use type checking. (`#3322 `__) + + +Miscellaneous internal changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- When misnesting nurseries you now get a helpful :exc:`RuntimeError` instead of a catastrophic :exc:`TrioInternalError`. (`#3307 `__) + + Trio 0.30.0 (2025-04-20) ------------------------ diff --git a/newsfragments/3007.bugfix.rst b/newsfragments/3007.bugfix.rst deleted file mode 100644 index da6732395a..0000000000 --- a/newsfragments/3007.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Make ctrl+c work in more situations in the Trio REPL (``python -m trio``). diff --git a/newsfragments/3232.feature.rst b/newsfragments/3232.feature.rst deleted file mode 100644 index 9da76cb370..0000000000 --- a/newsfragments/3232.feature.rst +++ /dev/null @@ -1 +0,0 @@ -:exc:`Cancelled` strings can now display the source and reason for a cancellation. Trio-internal sources of cancellation will set this string, and :meth:`CancelScope.cancel` now has a ``reason`` string parameter that can be used to attach info to any :exc:`Cancelled` to help in debugging. diff --git a/newsfragments/3248.bugfix.rst b/newsfragments/3248.bugfix.rst deleted file mode 100644 index 69d7a9859a..0000000000 --- a/newsfragments/3248.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Allow pickling `trio.Cancelled`, as they can show up when you want to pickle something else. This does not rule out pickling other ``NoPublicConstructor`` objects -- create an issue if necessary. diff --git a/newsfragments/3263.bugfix.rst b/newsfragments/3263.bugfix.rst deleted file mode 100644 index db9e8f770c..0000000000 --- a/newsfragments/3263.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Decrease import time on Windows by around 10%. diff --git a/newsfragments/3275.bugfix.rst b/newsfragments/3275.bugfix.rst deleted file mode 100644 index b418b26d76..0000000000 --- a/newsfragments/3275.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Handle unwrapping SystemExit/KeyboardInterrupt exception gracefully in utility function ``raise_single_exception_from_group`` that reraises last exception from group. diff --git a/newsfragments/3277.bugfix.rst b/newsfragments/3277.bugfix.rst deleted file mode 100644 index 321130815a..0000000000 --- a/newsfragments/3277.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Ensure that the DTLS server does not mutate SSL context. diff --git a/newsfragments/3307.misc.rst b/newsfragments/3307.misc.rst deleted file mode 100644 index ab59183729..0000000000 --- a/newsfragments/3307.misc.rst +++ /dev/null @@ -1 +0,0 @@ -When misnesting nurseries you now get a helpful :exc:`RuntimeError` instead of a catastrophic :exc:`TrioInternalError`. diff --git a/newsfragments/3322.deprecated.txt b/newsfragments/3322.deprecated.txt deleted file mode 100644 index 0e1b69f32b..0000000000 --- a/newsfragments/3322.deprecated.txt +++ /dev/null @@ -1 +0,0 @@ -Implement ``bool(trio.Event)`` and have it raise a `DeprecationWarning` and tell users to use `trio.Event.is_set` instead. This is an alternative to ``mypy --enable-error-code=truthy-bool`` for users who don't use type checking. diff --git a/newsfragments/3324.bugfix.rst b/newsfragments/3324.bugfix.rst deleted file mode 100644 index 0fd5a05399..0000000000 --- a/newsfragments/3324.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Avoid having `trio.as_safe_channel` raise if closing the generator wrapped -`GeneratorExit` in a `BaseExceptionGroup`. diff --git a/src/trio/_version.py b/src/trio/_version.py index 87bdae021d..03116ed1c9 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.30.0+dev" +__version__ = "0.31.0" From 8f7f51c97462eaa2c076ba9f07bf9723e245780e Mon Sep 17 00:00:00 2001 From: A5rocks Date: Wed, 10 Sep 2025 00:18:04 +0900 Subject: [PATCH 082/111] Start new cycle --- src/trio/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/trio/_version.py b/src/trio/_version.py index 03116ed1c9..a089e44d49 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.31.0" +__version__ = "0.31.0+dev" From 2c6c3e78e862c068038603e6a5571f3925b13559 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Wed, 10 Sep 2025 07:34:07 +0500 Subject: [PATCH 083/111] test: add extra test case --- src/trio/_tests/test_sync.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/trio/_tests/test_sync.py b/src/trio/_tests/test_sync.py index 72c9359912..9f1962f4d7 100644 --- a/src/trio/_tests/test_sync.py +++ b/src/trio/_tests/test_sync.py @@ -237,8 +237,25 @@ async def test_CapacityLimiter_zero_limit_tokens() -> None: c.total_tokens = 1 assert c.statistics().tasks_waiting == 0 assert c.statistics().borrowers == [6] - c.release_on_behalf_of(6) + + await c.acquire_on_behalf_of(0) # total_tokens is 1 + + nursery.start_soon(c.acquire_on_behalf_of, 1) + c.total_tokens = 0 + + assert c.statistics().borrowers == [0] + + c.release_on_behalf_of(0) + assert c.statistics().borrowers == [] + assert c.statistics().tasks_waiting == 0 + + c.total_tokens = 1 + await wait_all_tasks_blocked() + assert c.statistics().borrowers == [1] + + c.release_on_behalf_of(1) + c.total_tokens = 0 nursery.cancel_scope.cancel() From 4ef6ba8f578971cf549aa7c1280e0b95bdce5170 Mon Sep 17 00:00:00 2001 From: Abduaziz Ziyodov Date: Wed, 10 Sep 2025 09:43:21 +0500 Subject: [PATCH 084/111] fix: add "wait"s before changing token length & releasing limiter --- src/trio/_tests/test_sync.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/trio/_tests/test_sync.py b/src/trio/_tests/test_sync.py index 9f1962f4d7..836e8597e8 100644 --- a/src/trio/_tests/test_sync.py +++ b/src/trio/_tests/test_sync.py @@ -242,17 +242,20 @@ async def test_CapacityLimiter_zero_limit_tokens() -> None: await c.acquire_on_behalf_of(0) # total_tokens is 1 nursery.start_soon(c.acquire_on_behalf_of, 1) + await wait_all_tasks_blocked() c.total_tokens = 0 assert c.statistics().borrowers == [0] c.release_on_behalf_of(0) + await wait_all_tasks_blocked() assert c.statistics().borrowers == [] - assert c.statistics().tasks_waiting == 0 + assert c.statistics().tasks_waiting == 1 c.total_tokens = 1 await wait_all_tasks_blocked() assert c.statistics().borrowers == [1] + assert c.statistics().tasks_waiting == 0 c.release_on_behalf_of(1) From 0aa5ee37d967e039eeaee9b2628ea9aa778e74ad Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 16 Sep 2025 06:35:33 +0900 Subject: [PATCH 085/111] [pre-commit.ci] pre-commit autoupdate (#3330) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.12.12 → v0.13.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.12.12...v0.13.0) - [github.com/woodruffw/zizmor-pre-commit: v1.12.1 → v1.13.0](https://github.com/woodruffw/zizmor-pre-commit/compare/v1.12.1...v1.13.0) - [github.com/astral-sh/uv-pre-commit: 0.8.15 → 0.8.17](https://github.com/astral-sh/uv-pre-commit/compare/0.8.15...0.8.17) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- test-requirements.txt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 79e003a49a..afd3b269bd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.12 + rev: v0.13.0 hooks: - id: ruff-check types: [file] @@ -46,7 +46,7 @@ repos: hooks: - id: sphinx-lint - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.12.1 + rev: v1.13.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.8.15 + rev: 0.8.17 hooks: # Compile requirements - id: pip-compile diff --git a/test-requirements.txt b/test-requirements.txt index 66dad38bb8..1c14d9f66f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -138,7 +138,7 @@ requests==2.32.5 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.12.12 +ruff==0.13.0 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -200,7 +200,7 @@ typing-extensions==4.15.0 # virtualenv urllib3==2.5.0 # via requests -uv==0.8.15 +uv==0.8.17 # via -r test-requirements.in virtualenv==20.34.0 # via pre-commit From 4407451942c5d09e0862058ee3349bd2fd536cf8 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Tue, 16 Sep 2025 21:30:52 -0700 Subject: [PATCH 086/111] fix broken-channel bug --- newsfragments/3331.bugfix.rst | 3 +++ src/trio/_channel.py | 11 ++++++-- src/trio/_tests/test_channel.py | 46 ++++++++++++++++++++++++++++++++- 3 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 newsfragments/3331.bugfix.rst diff --git a/newsfragments/3331.bugfix.rst b/newsfragments/3331.bugfix.rst new file mode 100644 index 0000000000..da30bc9d5c --- /dev/null +++ b/newsfragments/3331.bugfix.rst @@ -0,0 +1,3 @@ +Fixed a bug where iterating over an ``@as_safe_channel``-derived ``ReceiveChannel`` +would raise `~trio.BrokenResourceError` if the channel was closed by another task. +It now shuts down cleanly. diff --git a/src/trio/_channel.py b/src/trio/_channel.py index 2afca9d7cd..1398e766e4 100644 --- a/src/trio/_channel.py +++ b/src/trio/_channel.py @@ -17,7 +17,7 @@ import trio from ._abc import ReceiveChannel, ReceiveType, SendChannel, SendType, T -from ._core import Abort, RaiseCancelT, Task, enable_ki_protection +from ._core import Abort, BrokenResourceError, RaiseCancelT, Task, enable_ki_protection from ._util import ( MultipleExceptionError, NoPublicConstructor, @@ -577,12 +577,19 @@ async def _move_elems_to_channel( while True: # wait for receiver to call next on the aiter await send_semaphore.acquire() + if not send_chan._state.open_receive_channels: + # skip the possibly-expensive computation in the generator, + # if we know it will be impossible to send the result. + break try: value = await agen.__anext__() except StopAsyncIteration: return # Send the value to the channel - await send_chan.send(value) + try: + await send_chan.send(value) + except BrokenResourceError: + break # closed since we checked above finally: # work around `.aclose()` not suppressing GeneratorExit in an # ExceptionGroup: diff --git a/src/trio/_tests/test_channel.py b/src/trio/_tests/test_channel.py index 85cd982a41..cfdf904fdd 100644 --- a/src/trio/_tests/test_channel.py +++ b/src/trio/_tests/test_channel.py @@ -434,7 +434,7 @@ async def test_as_safe_channel_broken_resource() -> None: @as_safe_channel async def agen() -> AsyncGenerator[int]: yield 1 - yield 2 + yield 2 # pragma: no cover async with agen() as recv_chan: assert await recv_chan.__anext__() == 1 @@ -695,3 +695,47 @@ async def agen(ex: type[BaseException]) -> AsyncGenerator[None]: async with agen(ValueError) as g: async for _ in g: break + + +async def test_as_safe_channel_close_between_iteration() -> None: + @as_safe_channel + async def agen() -> AsyncGenerator[None]: + while True: + yield + + async with agen() as chan, trio.open_nursery() as nursery: + + async def close_channel() -> None: + await trio.lowlevel.checkpoint() + await chan.aclose() + + nursery.start_soon(close_channel) + with pytest.raises(trio.ClosedResourceError): + async for _ in chan: + pass + + +async def test_as_safe_channel_close_before_iteration() -> None: + @as_safe_channel + async def agen() -> AsyncGenerator[None]: + raise AssertionError("should be unreachable") # pragma: no cover + yield # pragma: no cover + + async with agen() as chan: + await chan.aclose() + with pytest.raises(trio.ClosedResourceError): + await chan.receive() + + +async def test_as_safe_channel_close_during_iteration() -> None: + @as_safe_channel + async def agen() -> AsyncGenerator[None]: + await chan.aclose() + while True: + yield + + for _ in range(10): # 20% missed-alarm rate, so run ten times + async with agen() as chan: + with pytest.raises(trio.ClosedResourceError): + async for _ in chan: + pass From d09e942911e3cc41a2084d186f98b45c25dfaa93 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Sun, 21 Sep 2025 11:25:58 +0900 Subject: [PATCH 087/111] Wrap exception groups less when raising in `as_safe_channel` While this loses a little bit of information on pre-3.11 versions, I think it's worth the readability improvements. I could be convinced that conditionally raising the exception group with the special note would work too, though. This commit also simplifies a test by accounting for cancellation. Fixes #3332 --- newsfragments/3332.misc.rst | 1 + src/trio/_channel.py | 11 +++++++---- src/trio/_tests/test_channel.py | 29 ++++++++++++++++++----------- 3 files changed, 26 insertions(+), 15 deletions(-) create mode 100644 newsfragments/3332.misc.rst diff --git a/newsfragments/3332.misc.rst b/newsfragments/3332.misc.rst new file mode 100644 index 0000000000..f4b3a9b3a3 --- /dev/null +++ b/newsfragments/3332.misc.rst @@ -0,0 +1 @@ +Decrease indentation for exception groups raised in `trio.as_safe_channel`. diff --git a/src/trio/_channel.py b/src/trio/_channel.py index 1398e766e4..947d6b0888 100644 --- a/src/trio/_channel.py +++ b/src/trio/_channel.py @@ -556,10 +556,13 @@ async def context_manager( except MultipleExceptionError: # In case user has except* we make it possible for them to handle the # exceptions. - raise BaseExceptionGroup( - "Encountered exception during cleanup of generator object, as well as exception in the contextmanager body - unable to unwrap.", - [eg], - ) from None + if sys.version_info >= (3, 11): + eg.add_note( + "Encountered exception during cleanup of generator object, as " + "well as exception in the contextmanager body - unable to unwrap." + ) + + raise eg from None async def _move_elems_to_channel( agen: AsyncGenerator[T, None], diff --git a/src/trio/_tests/test_channel.py b/src/trio/_tests/test_channel.py index cfdf904fdd..d2664b95c7 100644 --- a/src/trio/_tests/test_channel.py +++ b/src/trio/_tests/test_channel.py @@ -519,17 +519,20 @@ async def agen(events: list[str]) -> AsyncGenerator[int]: events: list[str] = [] with RaisesGroup( - RaisesGroup( - Matcher(ValueError, match="^agen$"), - Matcher(TypeError, match="^iterator$"), - ), - match=r"^Encountered exception during cleanup of generator object, as well as exception in the contextmanager body - unable to unwrap.$", - ): + Matcher(ValueError, match="^agen$"), + Matcher(TypeError, match="^iterator$"), + ) as g: async with agen(events) as recv_chan: async for i in recv_chan: # pragma: no branch assert i == 1 raise TypeError("iterator") + if sys.version_info >= (3, 11): + assert g.value.__notes__ == [ + "Encountered exception during cleanup of generator object, as " + "well as exception in the contextmanager body - unable to unwrap." + ] + assert events == ["GeneratorExit()", "finally"] @@ -734,8 +737,12 @@ async def agen() -> AsyncGenerator[None]: while True: yield - for _ in range(10): # 20% missed-alarm rate, so run ten times - async with agen() as chan: - with pytest.raises(trio.ClosedResourceError): - async for _ in chan: - pass + async with agen() as chan: + with pytest.raises(trio.ClosedResourceError): + async for _ in chan: + pass + + # This is necessary to ensure that `chan` has been sent + # to. Otherwise, this test sometimes passes on a broken + # version of trio. + await trio.testing.wait_all_tasks_blocked() From b596e38b76920389acfbba46597eab75fb239e41 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 21:46:50 +0000 Subject: [PATCH 088/111] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black-pre-commit-mirror: 25.1.0 → 25.9.0](https://github.com/psf/black-pre-commit-mirror/compare/25.1.0...25.9.0) - [github.com/astral-sh/ruff-pre-commit: v0.13.0 → v0.13.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.0...v0.13.1) - [github.com/astral-sh/uv-pre-commit: 0.8.17 → 0.8.19](https://github.com/astral-sh/uv-pre-commit/compare/0.8.17...0.8.19) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index afd3b269bd..eb86a84248 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.1.0 + rev: 25.9.0 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.0 + rev: v0.13.1 hooks: - id: ruff-check types: [file] @@ -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.8.17 + rev: 0.8.19 hooks: # Compile requirements - id: pip-compile From 48e00425a435e4b68561e9ed742263a2adec9a97 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 21:47:06 +0000 Subject: [PATCH 089/111] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test-requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 1c14d9f66f..f814c2065c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -16,7 +16,7 @@ attrs==25.3.0 # outcome babel==2.17.0 # via sphinx -black==25.1.0 ; implementation_name == 'cpython' +black==25.9.0 ; implementation_name == 'cpython' # via -r test-requirements.in certifi==2025.8.3 # via requests @@ -138,7 +138,7 @@ requests==2.32.5 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.13.0 +ruff==0.13.1 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -200,7 +200,7 @@ typing-extensions==4.15.0 # virtualenv urllib3==2.5.0 # via requests -uv==0.8.17 +uv==0.8.19 # via -r test-requirements.in virtualenv==20.34.0 # via pre-commit From 2165828b500bfb63c17149b7c8b7cf52a2cb2c7a Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 22 Sep 2025 17:25:18 -0500 Subject: [PATCH 090/111] Re-run pre-commit hooks --- test-requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test-requirements.txt b/test-requirements.txt index f814c2065c..d3f660fbdd 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -132,6 +132,8 @@ pyright==1.1.404 # via -r test-requirements.in pytest==8.4.1 # via -r test-requirements.in +pytokens==0.1.10 ; implementation_name == 'cpython' + # via black pyyaml==6.0.2 # via pre-commit requests==2.32.5 From 26f55f67d676fe485d94879616ee782b4db81714 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Tue, 23 Sep 2025 18:30:27 -0700 Subject: [PATCH 091/111] avoid error when coro.cr_frame is None --- newsfragments/3337.bugfix.rst | 3 +++ src/trio/_core/_run.py | 6 ++++-- src/trio/_tests/test_tracing.py | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 newsfragments/3337.bugfix.rst diff --git a/newsfragments/3337.bugfix.rst b/newsfragments/3337.bugfix.rst new file mode 100644 index 0000000000..fe2124e976 --- /dev/null +++ b/newsfragments/3337.bugfix.rst @@ -0,0 +1,3 @@ +`trio.lowlevel.Task.iter_await_frames` now works on completed tasks, by +returning an empty list of frames if the underlying coroutine has been closed. +Previously, it raised an internal error. diff --git a/src/trio/_core/_run.py b/src/trio/_core/_run.py index 5303dfe75d..4689dca104 100644 --- a/src/trio/_core/_run.py +++ b/src/trio/_core/_run.py @@ -1588,11 +1588,13 @@ def print_stack_for_task(task): while coro is not None: if hasattr(coro, "cr_frame"): # A real coroutine - yield coro.cr_frame, coro.cr_frame.f_lineno + if cr_frame := coro.cr_frame: # None if the task has finished + yield cr_frame, cr_frame.f_lineno coro = coro.cr_await elif hasattr(coro, "gi_frame"): # A generator decorated with @types.coroutine - yield coro.gi_frame, coro.gi_frame.f_lineno + if gi_frame := coro.gi_frame: # pragma: no branch + yield gi_frame, gi_frame.f_lineno # pragma: no cover coro = coro.gi_yieldfrom elif coro.__class__.__name__ in [ "async_generator_athrow", diff --git a/src/trio/_tests/test_tracing.py b/src/trio/_tests/test_tracing.py index 52ea9bfa40..473317c17c 100644 --- a/src/trio/_tests/test_tracing.py +++ b/src/trio/_tests/test_tracing.py @@ -69,3 +69,20 @@ async def test_task_iter_await_frames_async_gen() -> None: ] nursery.cancel_scope.cancel() + + +async def test_closed_task_iter_await_frames() -> None: + async with trio.open_nursery() as nursery: + task = object() + + async def capture_task() -> None: + nonlocal task + task = trio.lowlevel.current_task() + await trio.lowlevel.checkpoint() + + nursery.start_soon(capture_task) + + # Task has completed, so coro.cr_frame should be None, thus no frames + assert isinstance(task, trio.lowlevel.Task) # Ran `capture_task` + assert task.coro.cr_frame is None # and the task was over, but + assert list(task.iter_await_frames()) == [] # look, no crash! From cc99574951578ebaa3958601c8b9560166b832fd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 21:10:43 +0000 Subject: [PATCH 092/111] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.13.1 → v0.13.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.1...v0.13.2) - [github.com/adhtruong/mirrors-typos: v1.36.2 → v1.36.3](https://github.com/adhtruong/mirrors-typos/compare/v1.36.2...v1.36.3) - [github.com/woodruffw/zizmor-pre-commit: v1.13.0 → v1.14.2](https://github.com/woodruffw/zizmor-pre-commit/compare/v1.13.0...v1.14.2) - [github.com/astral-sh/uv-pre-commit: 0.8.19 → 0.8.22](https://github.com/astral-sh/uv-pre-commit/compare/0.8.19...0.8.22) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eb86a84248..b8ab4a42a5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.1 + rev: v0.13.2 hooks: - id: ruff-check types: [file] @@ -38,7 +38,7 @@ repos: # tomli needed on 3.10. tomllib is available in stdlib on 3.11+ - tomli - repo: https://github.com/adhtruong/mirrors-typos - rev: v1.36.2 + rev: v1.36.3 hooks: - id: typos - repo: https://github.com/sphinx-contrib/sphinx-lint @@ -46,7 +46,7 @@ repos: hooks: - id: sphinx-lint - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.13.0 + rev: v1.14.2 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.8.19 + rev: 0.8.22 hooks: # Compile requirements - id: pip-compile From ba2bd232c23a0950f49dbf7dae02af22b3eb2bdc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 21:11:09 +0000 Subject: [PATCH 093/111] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index d3f660fbdd..464312b59c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -140,7 +140,7 @@ requests==2.32.5 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.13.1 +ruff==0.13.2 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -202,7 +202,7 @@ typing-extensions==4.15.0 # virtualenv urllib3==2.5.0 # via requests -uv==0.8.19 +uv==0.8.22 # via -r test-requirements.in virtualenv==20.34.0 # via pre-commit From bc6a72e66b63c522c418b4844911fe7e294dfdf9 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Mon, 29 Sep 2025 17:07:38 -0500 Subject: [PATCH 094/111] Add noqas for unit tests --- src/trio/_tests/test_path.py | 2 +- src/trio/_tests/test_subprocess.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/trio/_tests/test_path.py b/src/trio/_tests/test_path.py index 9c1e43c756..b347ae2c76 100644 --- a/src/trio/_tests/test_path.py +++ b/src/trio/_tests/test_path.py @@ -254,7 +254,7 @@ async def test_classmethods() -> None: assert isinstance(await trio.Path.home(), trio.Path) # pathlib.Path has only two classmethods - assert str(await trio.Path.home()) == os.path.expanduser("~") + assert str(await trio.Path.home()) == os.path.expanduser("~") # noqa: ASYNC240 assert str(await trio.Path.cwd()) == os.getcwd() # Wrapped method has docstring diff --git a/src/trio/_tests/test_subprocess.py b/src/trio/_tests/test_subprocess.py index 7bb40d2b3d..f1bfa4ed06 100644 --- a/src/trio/_tests/test_subprocess.py +++ b/src/trio/_tests/test_subprocess.py @@ -714,17 +714,17 @@ async def test_run_process_background_fail() -> None: async def test_for_leaking_fds() -> None: gc.collect() # address possible flakiness on PyPy - starting_fds = set(SyncPath("/dev/fd").iterdir()) + starting_fds = set(SyncPath("/dev/fd").iterdir()) # noqa: ASYNC240 await run_process(EXIT_TRUE) - assert set(SyncPath("/dev/fd").iterdir()) == starting_fds + assert set(SyncPath("/dev/fd").iterdir()) == starting_fds # noqa: ASYNC240 with pytest.raises(subprocess.CalledProcessError): await run_process(EXIT_FALSE) - assert set(SyncPath("/dev/fd").iterdir()) == starting_fds + assert set(SyncPath("/dev/fd").iterdir()) == starting_fds # noqa: ASYNC240 with pytest.raises(PermissionError): await run_process(["/dev/fd/0"]) - assert set(SyncPath("/dev/fd").iterdir()) == starting_fds + assert set(SyncPath("/dev/fd").iterdir()) == starting_fds # noqa: ASYNC240 async def test_run_process_internal_error(monkeypatch: pytest.MonkeyPatch) -> None: From 57f725c3c012776054cc7b971a1c04094b714c3b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 07:56:13 +0000 Subject: [PATCH 095/111] Bump dependencies from commit 2748f5 (#3341) * Dependency updates * Trigger CI * Remove unnecessary type ignore * Typing changes --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Co-authored-by: A5rocks --- docs-requirements.txt | 14 +++++------ src/trio/_subprocess_platform/kqueue.py | 2 +- src/trio/_tests/test_exports.py | 2 +- test-requirements.txt | 31 +++++++++++++------------ 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/docs-requirements.txt b/docs-requirements.txt index 88209712f6..2ec1465b91 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -8,23 +8,23 @@ attrs==25.3.0 # outcome babel==2.17.0 # via sphinx -beautifulsoup4==4.13.5 +beautifulsoup4==4.14.2 # via sphinx-codeautolink certifi==2025.8.3 # via requests -cffi==1.17.1 ; os_name == 'nt' or platform_python_implementation != 'PyPy' +cffi==2.0.0 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via # -r docs-requirements.in # cryptography charset-normalizer==3.4.3 # via requests -click==8.2.1 +click==8.3.0 # via towncrier colorama==0.4.6 ; sys_platform == 'win32' # via # click # sphinx -cryptography==45.0.6 +cryptography==46.0.1 # via pyopenssl docutils==0.21.2 # via @@ -45,17 +45,17 @@ jinja2==3.1.6 # -r docs-requirements.in # sphinx # towncrier -markupsafe==3.0.2 +markupsafe==3.0.3 # via jinja2 outcome==1.3.0.post0 # via -r docs-requirements.in packaging==25.0 # via sphinx -pycparser==2.22 ; os_name == 'nt' or platform_python_implementation != 'PyPy' +pycparser==2.23 ; (implementation_name != 'PyPy' and os_name == 'nt') or (implementation_name != 'PyPy' and platform_python_implementation != 'PyPy') # via cffi pygments==2.19.2 # via sphinx -pyopenssl==25.1.0 +pyopenssl==25.3.0 # via -r docs-requirements.in requests==2.32.5 # via sphinx diff --git a/src/trio/_subprocess_platform/kqueue.py b/src/trio/_subprocess_platform/kqueue.py index 2283bb5360..0bff66ddb0 100644 --- a/src/trio/_subprocess_platform/kqueue.py +++ b/src/trio/_subprocess_platform/kqueue.py @@ -17,7 +17,7 @@ async def wait_child_exiting(process: _subprocess.Process) -> None: # pypy doesn't define KQ_NOTE_EXIT: # https://bitbucket.org/pypy/pypy/issues/2921/ # I verified this value against both Darwin and FreeBSD - KQ_NOTE_EXIT = 0x80000000 + KQ_NOTE_EXIT = 0x80000000 # type: ignore[misc] def make_event(flags: int) -> select.kevent: return select.kevent( diff --git a/src/trio/_tests/test_exports.py b/src/trio/_tests/test_exports.py index c29d9b609a..ab2b3377e7 100644 --- a/src/trio/_tests/test_exports.py +++ b/src/trio/_tests/test_exports.py @@ -33,7 +33,7 @@ try: # If installed, check both versions of this class. from typing_extensions import Protocol as Protocol_ext except ImportError: # pragma: no cover - Protocol_ext = Protocol # type: ignore[assignment] + Protocol_ext = Protocol def _ensure_mypy_cache_updated() -> None: diff --git a/test-requirements.txt b/test-requirements.txt index 464312b59c..d181c66abf 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -20,7 +20,7 @@ black==25.9.0 ; implementation_name == 'cpython' # via -r test-requirements.in certifi==2025.8.3 # via requests -cffi==1.17.1 ; os_name == 'nt' or platform_python_implementation != 'PyPy' +cffi==2.0.0 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via # -r test-requirements.in # cryptography @@ -30,7 +30,7 @@ charset-normalizer==3.4.3 # via requests click==8.1.8 ; python_full_version < '3.10' and implementation_name == 'cpython' # via black -click==8.2.1 ; python_full_version >= '3.10' and implementation_name == 'cpython' +click==8.3.0 ; python_full_version >= '3.10' and implementation_name == 'cpython' # via black codespell==2.4.1 # via -r test-requirements.in @@ -40,9 +40,9 @@ colorama==0.4.6 ; sys_platform == 'win32' # pylint # pytest # sphinx -coverage==7.10.6 +coverage==7.10.7 # via -r test-requirements.in -cryptography==45.0.6 +cryptography==46.0.1 # via # -r test-requirements.in # pyopenssl @@ -60,7 +60,7 @@ exceptiongroup==1.3.0 ; python_full_version < '3.11' # pytest filelock==3.19.1 # via virtualenv -identify==2.6.13 +identify==2.6.14 # via pre-commit idna==3.10 # via @@ -79,11 +79,11 @@ jedi==0.19.2 ; implementation_name == 'cpython' # via -r test-requirements.in jinja2==3.1.6 # via sphinx -markupsafe==3.0.2 +markupsafe==3.0.3 # via jinja2 mccabe==0.7.0 # via pylint -mypy==1.17.1 +mypy==1.18.2 # via -r test-requirements.in mypy-extensions==1.1.0 # via @@ -118,7 +118,7 @@ pluggy==1.6.0 # via pytest pre-commit==4.3.0 # via -r test-requirements.in -pycparser==2.22 ; os_name == 'nt' or platform_python_implementation != 'PyPy' +pycparser==2.23 ; (implementation_name != 'PyPy' and os_name == 'nt') or (implementation_name != 'PyPy' and platform_python_implementation != 'PyPy') # via cffi pygments==2.19.2 # via @@ -126,15 +126,15 @@ pygments==2.19.2 # sphinx pylint==3.3.8 # via -r test-requirements.in -pyopenssl==25.1.0 +pyopenssl==25.3.0 # via -r test-requirements.in -pyright==1.1.404 +pyright==1.1.405 # via -r test-requirements.in -pytest==8.4.1 +pytest==8.4.2 # via -r test-requirements.in pytokens==0.1.10 ; implementation_name == 'cpython' # via black -pyyaml==6.0.2 +pyyaml==6.0.3 # via pre-commit requests==2.32.5 # via sphinx @@ -177,15 +177,15 @@ tomlkit==0.13.3 # via pylint trustme==1.2.1 # via -r test-requirements.in -types-cffi==1.17.0.20250822 +types-cffi==1.17.0.20250915 # via # -r test-requirements.in # types-pyopenssl -types-docutils==0.22.0.20250822 +types-docutils==0.22.2.20250924 # via -r test-requirements.in types-pyopenssl==24.1.0.20240722 # via -r test-requirements.in -types-pyyaml==6.0.12.20250822 +types-pyyaml==6.0.12.20250915 # via -r test-requirements.in types-setuptools==80.9.0.20250822 # via types-cffi @@ -194,6 +194,7 @@ typing-extensions==4.15.0 # -r test-requirements.in # astroid # black + # cryptography # exceptiongroup # mypy # pylint From 3ee08fe46826296e660722a8c57afa56eb242b11 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 22:39:17 +0000 Subject: [PATCH 096/111] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.13.2 → v0.13.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.13.2...v0.13.3) - [github.com/adhtruong/mirrors-typos: v1.36.3 → v1.37.2](https://github.com/adhtruong/mirrors-typos/compare/v1.36.3...v1.37.2) - [github.com/astral-sh/uv-pre-commit: 0.8.22 → 0.8.23](https://github.com/astral-sh/uv-pre-commit/compare/0.8.22...0.8.23) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b8ab4a42a5..9d812d6172 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.2 + rev: v0.13.3 hooks: - id: ruff-check types: [file] @@ -38,7 +38,7 @@ repos: # tomli needed on 3.10. tomllib is available in stdlib on 3.11+ - tomli - repo: https://github.com/adhtruong/mirrors-typos - rev: v1.36.3 + rev: v1.37.2 hooks: - id: typos - repo: https://github.com/sphinx-contrib/sphinx-lint @@ -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.8.22 + rev: 0.8.23 hooks: # Compile requirements - id: pip-compile From 236a2094f239d42cb45db9975469e9eb9a5a77c7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 22:43:03 +0000 Subject: [PATCH 097/111] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/trio/_tests/test_highlevel_ssl_helpers.py | 1 - test-requirements.txt | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/trio/_tests/test_highlevel_ssl_helpers.py b/src/trio/_tests/test_highlevel_ssl_helpers.py index 8c60c25f2c..2d7f39372d 100644 --- a/src/trio/_tests/test_highlevel_ssl_helpers.py +++ b/src/trio/_tests/test_highlevel_ssl_helpers.py @@ -7,7 +7,6 @@ import pytest import trio -import trio.testing from trio.socket import AF_INET, IPPROTO_TCP, SOCK_STREAM from .._highlevel_ssl_helpers import ( diff --git a/test-requirements.txt b/test-requirements.txt index d181c66abf..847e4d6614 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -140,7 +140,7 @@ requests==2.32.5 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.13.2 +ruff==0.13.3 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -203,7 +203,7 @@ typing-extensions==4.15.0 # virtualenv urllib3==2.5.0 # via requests -uv==0.8.22 +uv==0.8.23 # via -r test-requirements.in virtualenv==20.34.0 # via pre-commit From 1425e7e5ea65e200a2bfe76756a38388a1981b83 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 21:56:57 +0000 Subject: [PATCH 098/111] [pre-commit.ci] pre-commit autoupdate (#3344) --- .pre-commit-config.yaml | 6 +++--- test-requirements.txt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9d812d6172..fd9e698a8a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.3 + rev: v0.14.0 hooks: - id: ruff-check types: [file] @@ -38,7 +38,7 @@ repos: # tomli needed on 3.10. tomllib is available in stdlib on 3.11+ - tomli - repo: https://github.com/adhtruong/mirrors-typos - rev: v1.37.2 + rev: v1.38.1 hooks: - id: typos - repo: https://github.com/sphinx-contrib/sphinx-lint @@ -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.8.23 + rev: 0.9.2 hooks: # Compile requirements - id: pip-compile diff --git a/test-requirements.txt b/test-requirements.txt index 847e4d6614..b156681b5e 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -140,7 +140,7 @@ requests==2.32.5 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.13.3 +ruff==0.14.0 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -203,7 +203,7 @@ typing-extensions==4.15.0 # virtualenv urllib3==2.5.0 # via requests -uv==0.8.23 +uv==0.9.2 # via -r test-requirements.in virtualenv==20.34.0 # via pre-commit From 43261ce646e3efda0620fc6cc0a38c850aeaf267 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:42:02 +0000 Subject: [PATCH 099/111] [pre-commit.ci] pre-commit autoupdate (#3346) --- .pre-commit-config.yaml | 6 +++--- test-requirements.txt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fd9e698a8a..7efee44473 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.0 + rev: v0.14.1 hooks: - id: ruff-check types: [file] @@ -46,7 +46,7 @@ repos: hooks: - id: sphinx-lint - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.14.2 + rev: v1.15.2 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.2 + rev: 0.9.4 hooks: # Compile requirements - id: pip-compile diff --git a/test-requirements.txt b/test-requirements.txt index b156681b5e..dbc4692f5c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -140,7 +140,7 @@ requests==2.32.5 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.14.0 +ruff==0.14.1 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -203,7 +203,7 @@ typing-extensions==4.15.0 # virtualenv urllib3==2.5.0 # via requests -uv==0.9.2 +uv==0.9.4 # via -r test-requirements.in virtualenv==20.34.0 # via pre-commit From bd3787fc766babe9a49ff2ca19c4627a642aee12 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 13:13:15 +0000 Subject: [PATCH 100/111] Dependency updates (#3347) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 ++-- docs-requirements.txt | 10 +++--- src/trio/_tests/test_exports.py | 4 +++ test-requirements.txt | 61 +++++++++++++++++++++------------ 4 files changed, 52 insertions(+), 29 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7efee44473..60b0294a95 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.1 + rev: v0.14.2 hooks: - id: ruff-check types: [file] @@ -46,7 +46,7 @@ repos: hooks: - id: sphinx-lint - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.15.2 + rev: v1.16.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.4 + rev: 0.9.5 hooks: # Compile requirements - id: pip-compile diff --git a/docs-requirements.txt b/docs-requirements.txt index 2ec1465b91..4fcf0cd18b 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -2,7 +2,7 @@ # uv pip compile --universal --python-version=3.11 docs-requirements.in -o docs-requirements.txt alabaster==1.0.0 # via sphinx -attrs==25.3.0 +attrs==25.4.0 # via # -r docs-requirements.in # outcome @@ -10,13 +10,13 @@ babel==2.17.0 # via sphinx beautifulsoup4==4.14.2 # via sphinx-codeautolink -certifi==2025.8.3 +certifi==2025.10.5 # via requests cffi==2.0.0 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via # -r docs-requirements.in # cryptography -charset-normalizer==3.4.3 +charset-normalizer==3.4.4 # via requests click==8.3.0 # via towncrier @@ -24,7 +24,7 @@ colorama==0.4.6 ; sys_platform == 'win32' # via # click # sphinx -cryptography==46.0.1 +cryptography==46.0.3 # via pyopenssl docutils==0.21.2 # via @@ -32,7 +32,7 @@ docutils==0.21.2 # sphinx-rtd-theme exceptiongroup==1.3.0 # via -r docs-requirements.in -idna==3.10 +idna==3.11 # via # -r docs-requirements.in # requests diff --git a/src/trio/_tests/test_exports.py b/src/trio/_tests/test_exports.py index ab2b3377e7..bc719ceb0c 100644 --- a/src/trio/_tests/test_exports.py +++ b/src/trio/_tests/test_exports.py @@ -443,6 +443,10 @@ def lookup_symbol(symbol: str) -> dict[str, Any]: # type: ignore[misc, explicit extra = {e for e in extra if not e.endswith("AttrsAttributes__")} assert len(extra) == before - 1 + if attrs.has(class_): + # dynamically created attribute by attrs? + missing.remove("__attrs_props__") + # dir does not see `__signature__` on enums until 3.14 if ( tool == "mypy" diff --git a/test-requirements.txt b/test-requirements.txt index dbc4692f5c..d0f99d71a3 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -6,11 +6,13 @@ alabaster==1.0.0 ; python_full_version >= '3.10' # via sphinx astor==0.8.1 # via -r test-requirements.in -astroid==3.3.11 +astroid==3.3.11 ; python_full_version < '3.10' + # via pylint +astroid==4.0.1 ; python_full_version >= '3.10' # via pylint async-generator==1.10 # via -r test-requirements.in -attrs==25.3.0 +attrs==25.4.0 # via # -r test-requirements.in # outcome @@ -18,7 +20,7 @@ babel==2.17.0 # via sphinx black==25.9.0 ; implementation_name == 'cpython' # via -r test-requirements.in -certifi==2025.8.3 +certifi==2025.10.5 # via requests cffi==2.0.0 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # via @@ -26,7 +28,7 @@ cffi==2.0.0 ; os_name == 'nt' or platform_python_implementation != 'PyPy' # cryptography cfgv==3.4.0 # via pre-commit -charset-normalizer==3.4.3 +charset-normalizer==3.4.4 # via requests click==8.1.8 ; python_full_version < '3.10' and implementation_name == 'cpython' # via black @@ -40,9 +42,11 @@ colorama==0.4.6 ; sys_platform == 'win32' # pylint # pytest # sphinx -coverage==7.10.7 +coverage==7.10.7 ; python_full_version < '3.10' + # via -r test-requirements.in +coverage==7.11.0 ; python_full_version >= '3.10' # via -r test-requirements.in -cryptography==46.0.1 +cryptography==46.0.3 # via # -r test-requirements.in # pyopenssl @@ -58,11 +62,13 @@ exceptiongroup==1.3.0 ; python_full_version < '3.11' # via # -r test-requirements.in # pytest -filelock==3.19.1 +filelock==3.19.1 ; python_full_version < '3.10' # via virtualenv -identify==2.6.14 +filelock==3.20.0 ; python_full_version >= '3.10' + # via virtualenv +identify==2.6.15 # via pre-commit -idna==3.10 +idna==3.11 # via # -r test-requirements.in # requests @@ -70,10 +76,16 @@ idna==3.10 imagesize==1.4.1 # via sphinx importlib-metadata==8.7.0 ; python_full_version < '3.10' - # via sphinx -iniconfig==2.1.0 + # via + # isort + # sphinx +iniconfig==2.1.0 ; python_full_version < '3.10' + # via pytest +iniconfig==2.3.0 ; python_full_version >= '3.10' # via pytest -isort==6.0.1 +isort==6.1.0 ; python_full_version < '3.10' + # via pylint +isort==7.0.0 ; python_full_version >= '3.10' # via pylint jedi==0.19.2 ; implementation_name == 'cpython' # via -r test-requirements.in @@ -109,7 +121,12 @@ pathspec==0.12.1 # via # black # mypy -platformdirs==4.4.0 +platformdirs==4.4.0 ; python_full_version < '3.10' + # via + # black + # pylint + # virtualenv +platformdirs==4.5.0 ; python_full_version >= '3.10' # via # black # pylint @@ -124,15 +141,17 @@ pygments==2.19.2 # via # pytest # sphinx -pylint==3.3.8 +pylint==3.3.9 ; python_full_version < '3.10' + # via -r test-requirements.in +pylint==4.0.2 ; python_full_version >= '3.10' # via -r test-requirements.in pyopenssl==25.3.0 # via -r test-requirements.in -pyright==1.1.405 +pyright==1.1.406 # via -r test-requirements.in pytest==8.4.2 # via -r test-requirements.in -pytokens==0.1.10 ; implementation_name == 'cpython' +pytokens==0.2.0 ; implementation_name == 'cpython' # via black pyyaml==6.0.3 # via pre-commit @@ -140,7 +159,7 @@ requests==2.32.5 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.14.1 +ruff==0.14.2 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -166,7 +185,7 @@ sphinxcontrib-qthelp==2.0.0 # via sphinx sphinxcontrib-serializinghtml==2.0.0 # via sphinx -tomli==2.2.1 ; python_full_version < '3.11' +tomli==2.3.0 ; python_full_version < '3.11' # via # black # mypy @@ -181,7 +200,7 @@ types-cffi==1.17.0.20250915 # via # -r test-requirements.in # types-pyopenssl -types-docutils==0.22.2.20250924 +types-docutils==0.22.2.20251006 # via -r test-requirements.in types-pyopenssl==24.1.0.20240722 # via -r test-requirements.in @@ -203,9 +222,9 @@ typing-extensions==4.15.0 # virtualenv urllib3==2.5.0 # via requests -uv==0.9.4 +uv==0.9.5 # via -r test-requirements.in -virtualenv==20.34.0 +virtualenv==20.35.3 # via pre-commit zipp==3.23.0 ; python_full_version < '3.10' # via importlib-metadata From 19196a83424dcce23f85bb7fc5dad73a4d943002 Mon Sep 17 00:00:00 2001 From: A5rocks Date: Tue, 28 Oct 2025 00:56:34 -0400 Subject: [PATCH 101/111] Drop 3.9 support (#3345) * Drop 3.9 * Fixes for CI * Address PR feedback * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Better pass through - Fix a few typos - Remove unnecessary generic function infrastructure - Deduplicate `from typing import ...` imports - Fix codecov complains * Undo ruff autofix * A few more codecov changes I'm not so sure about the unix pipes change. I suppose we'll get comments if it doesn't work? I can't tell why the code was there to begin with, which is a bad sign for being able to tell whether it's necessary anymore. * Fix pip-compile files --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/autodeps.yml | 14 ++--- .github/workflows/ci.yml | 33 ++++++----- .github/workflows/release.yml | 2 +- .pre-commit-config.yaml | 2 +- README.rst | 2 +- docs/source/index.rst | 2 +- docs/source/tutorial.rst | 2 +- newsfragments/3345.removal.rst | 1 + pyproject.toml | 7 ++- src/trio/_channel.py | 55 +++++++------------ src/trio/_core/_exceptions.py | 4 +- src/trio/_core/_io_epoll.py | 4 +- src/trio/_core/_io_kqueue.py | 4 +- src/trio/_core/_io_windows.py | 10 +--- src/trio/_core/_ki.py | 5 +- .../_core/_tests/test_exceptiongroup_gc.py | 3 +- src/trio/_core/_tests/test_guest_mode.py | 7 +-- src/trio/_core/_tests/test_ki.py | 4 +- src/trio/_core/_tests/test_run.py | 15 +++-- src/trio/_core/_traps.py | 26 +++++---- src/trio/_core/_windows_cffi.py | 3 +- src/trio/_dtls.py | 17 ++---- src/trio/_file_io.py | 2 +- src/trio/_highlevel_generic.py | 6 +- src/trio/_path.py | 17 ++++-- src/trio/_repl.py | 5 +- src/trio/_socket.py | 9 +-- src/trio/_subprocess.py | 20 ++----- src/trio/_tests/check_type_completeness.py | 2 +- src/trio/_tests/test_channel.py | 10 ++-- src/trio/_tests/test_exports.py | 12 ++-- src/trio/_tests/test_path.py | 6 +- src/trio/_tests/test_repl.py | 23 +++----- src/trio/_tests/test_socket.py | 12 ++-- src/trio/_tests/test_ssl.py | 8 +-- src/trio/_tests/test_subprocess.py | 3 +- src/trio/_tests/test_sync.py | 24 ++++---- src/trio/_tests/test_threads.py | 3 +- src/trio/_tests/test_timeouts.py | 14 ++--- src/trio/_tests/test_util.py | 15 ----- src/trio/_tests/type_tests/path.py | 3 +- src/trio/_tests/type_tests/raisesgroup.py | 12 +--- src/trio/_tools/gen_exports.py | 7 +-- src/trio/_unix_pipes.py | 12 +--- src/trio/_util.py | 38 +------------ src/trio/testing/_check_streams.py | 3 +- src/trio/testing/_fake_net.py | 12 +--- src/trio/testing/_memory_streams.py | 6 +- src/trio/testing/_raises_group.py | 10 +--- test-requirements.txt | 52 ++++-------------- 50 files changed, 203 insertions(+), 365 deletions(-) create mode 100644 newsfragments/3345.removal.rst diff --git a/.github/workflows/autodeps.yml b/.github/workflows/autodeps.yml index 4d2cfd4bbb..8b58f8904f 100644 --- a/.github/workflows/autodeps.yml +++ b/.github/workflows/autodeps.yml @@ -26,27 +26,21 @@ jobs: - name: Setup python uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "3.10" - name: Bump dependencies run: | python -m pip install -U pip pre-commit python -m pip install -r test-requirements.txt - uv pip compile --universal --python-version=3.9 --upgrade test-requirements.in -o test-requirements.txt + uv pip compile --universal --python-version=3.10 --upgrade test-requirements.in -o test-requirements.txt uv pip compile --universal --python-version=3.11 --upgrade docs-requirements.in -o docs-requirements.txt pre-commit autoupdate --jobs 0 - name: Install new requirements run: python -m pip install -r test-requirements.txt - # apply newer versions' formatting - - name: Black - run: black src/trio - - - name: uv - run: | - uv pip compile --universal --python-version=3.9 test-requirements.in -o test-requirements.txt - uv pip compile --universal --python-version=3.11 docs-requirements.in -o docs-requirements.txt + - name: Pre-commit fixes + run: pre-commit run -a - name: Commit changes and create automerge PR env: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b1db86d03..47527d0e39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -155,18 +155,18 @@ jobs: strategy: fail-fast: false matrix: - python: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + python: ['3.10', '3.11', '3.12', '3.13', '3.14'] arch: ['x86', 'x64'] lsp: [''] lsp_extract_file: [''] extra_name: [''] include: - - python: '3.9' + - python: '3.10' arch: 'x64' lsp: 'https://raw.githubusercontent.com/python-trio/trio-ci-assets/master/komodia-based-vpn-setup.zip' lsp_extract_file: 'komodia-based-vpn-setup.exe' extra_name: ', with Komodia LSP' - - python: '3.9' + - python: '3.10' arch: 'x64' lsp: 'https://www.proxifier.com/download/legacy/ProxifierSetup342.exe' lsp_extract_file: '' @@ -176,7 +176,7 @@ jobs: lsp: '' lsp_extract_file: '' extra_name: '' - #- python: '3.9' + #- python: '3.10' # arch: 'x64' # lsp: 'http://download.pctools.com/mirror/updates/9.0.0.2308-SDavfree-lite_en.exe' # lsp_extract_file: '' @@ -228,7 +228,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['pypy-3.11', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + python: ['pypy-3.11', '3.10', '3.11', '3.12', '3.13', '3.14'] check_formatting: ['0'] no_test_requirements: ['0'] extra_name: [''] @@ -237,7 +237,7 @@ jobs: check_formatting: '1' extra_name: ', check formatting' # separate test run that doesn't install test-requirements.txt - - python: '3.9' + - python: '3.10' no_test_requirements: '1' extra_name: ', no test-requirements' continue-on-error: >- @@ -301,7 +301,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['pypy-3.11', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + python: ['pypy-3.11', '3.10', '3.11', '3.12', '3.13', '3.14'] continue-on-error: >- ${{ ( @@ -389,14 +389,17 @@ jobs: fail-fast: false matrix: include: - - python: '3.9' # We support running on cython 2 and 3 for 3.9 - cython: '<3' # cython 2 - - python: '3.9' - cython: '>=3' # cython 3 (or greater) - - python: '3.11' # 3.11 is the last version Cy2 supports - cython: '<3' # cython 2 - - python: '3.13' # We support running cython3 on 3.13 - cython: '>=3' # cython 3 (or greater) + # Cython 2 supports 3.10 and 3.11 and Cython 3 supports all versions we do, + # so test both the lowest and higher version for both + - python: '3.10' + cython: '<3' + - python: '3.11' + cython: '<3' + # TODO: technically we should pin cython versions + - python: '3.10' + cython: '>=3' + - python: '3.14' + cython: '>=3' steps: - name: Retrieve the project source from an sdist inside the GHA artifact uses: re-actors/checkout-python-sdist@release/v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2efc31d9e9..17c426489a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: persist-credentials: false - uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: "3.10" - run: python -m pip install build - run: python -m build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 60b0294a95..46cad85615 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -80,7 +80,7 @@ repos: name: uv pip-compile test-requirements.in args: [ "--universal", - "--python-version=3.9", + "--python-version=3.10", "test-requirements.in", "-o", "test-requirements.txt"] diff --git a/README.rst b/README.rst index f62c2a37af..e29f9035f7 100644 --- a/README.rst +++ b/README.rst @@ -92,7 +92,7 @@ demonstration of implementing the "Happy Eyeballs" algorithm in an older library versus Trio. **Cool, but will it work on my system?** Probably! As long as you have -some kind of Python 3.9-or-better (CPython or `currently maintained versions of +some kind of Python 3.10-or-better (CPython or `currently maintained versions of PyPy3 `__ are both fine), and are using Linux, macOS, Windows, or FreeBSD, then Trio will work. Other environments might work too, but those diff --git a/docs/source/index.rst b/docs/source/index.rst index fea472424e..f8fc6add8f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -45,7 +45,7 @@ Vital statistics: * Supported environments: We test on - - Python: 3.9+ (CPython and PyPy) + - Python: 3.10+ (CPython and PyPy) - Windows, macOS, Linux (glibc and musl), FreeBSD Other environments might also work; give it a try and see. diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index 3ae9bb4597..acaa8750d8 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -88,7 +88,7 @@ Okay, ready? Let's get started. Before you begin ---------------- -1. Make sure you're using Python 3.9 or newer. +1. Make sure you're using Python 3.10 or newer. 2. ``python3 -m pip install --upgrade trio`` (or on Windows, maybe ``py -3 -m pip install --upgrade trio`` – `details diff --git a/newsfragments/3345.removal.rst b/newsfragments/3345.removal.rst new file mode 100644 index 0000000000..8279a1c7f9 --- /dev/null +++ b/newsfragments/3345.removal.rst @@ -0,0 +1 @@ +Drop support for Python 3.9. diff --git a/pyproject.toml b/pyproject.toml index bec87cfef0..aef3c258fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,15 +26,15 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: System :: Networking", "Typing :: Typed", ] -requires-python = ">=3.9" +requires-python = ">=3.10" dependencies = [ # attrs 19.2.0 adds `eq` option to decorators # attrs 20.1.0 adds @frozen @@ -215,7 +215,7 @@ disallow_untyped_defs = true check_untyped_defs = true [tool.pyright] -pythonVersion = "3.9" +pythonVersion = "3.10" reportUnnecessaryTypeIgnoreComment = true typeCheckingMode = "strict" @@ -330,6 +330,7 @@ exclude_also = [ '.*if "sphinx.ext.autodoc" in sys.modules:', 'TODO: test this line', 'if __name__ == "__main__":', + 'pass', ] partial_branches = [ "pragma: no branch", diff --git a/src/trio/_channel.py b/src/trio/_channel.py index 947d6b0888..05037d8131 100644 --- a/src/trio/_channel.py +++ b/src/trio/_channel.py @@ -22,7 +22,6 @@ MultipleExceptionError, NoPublicConstructor, final, - generic_function, raise_single_exception_from_group, ) @@ -45,9 +44,9 @@ P = ParamSpec("P") -def _open_memory_channel( - max_buffer_size: int | float, # noqa: PYI041 -) -> tuple[MemorySendChannel[T], MemoryReceiveChannel[T]]: +# written as a class so you can say open_memory_channel[int](5) +@final +class open_memory_channel(tuple["MemorySendChannel[T]", "MemoryReceiveChannel[T]"]): """Open a channel for passing objects between tasks within a process. Memory channels are lightweight, cheap to allocate, and entirely @@ -97,38 +96,24 @@ def _open_memory_channel( channel (summing over all clones). * ``tasks_waiting_receive``: The number of tasks blocked in ``receive`` on this channel (summing over all clones). - """ - if max_buffer_size != inf and not isinstance(max_buffer_size, int): - raise TypeError("max_buffer_size must be an integer or math.inf") - if max_buffer_size < 0: - raise ValueError("max_buffer_size must be >= 0") - state: MemoryChannelState[T] = MemoryChannelState(max_buffer_size) - return ( - MemorySendChannel[T]._create(state), - MemoryReceiveChannel[T]._create(state), - ) - - -# This workaround requires python3.9+, once older python versions are not supported -# or there's a better way of achieving type-checking on a generic factory function, -# it could replace the normal function header -if TYPE_CHECKING: - # written as a class so you can say open_memory_channel[int](5) - class open_memory_channel(tuple["MemorySendChannel[T]", "MemoryReceiveChannel[T]"]): - def __new__( # type: ignore[misc] # "must return a subtype" - cls, - max_buffer_size: int | float, # noqa: PYI041 - ) -> tuple[MemorySendChannel[T], MemoryReceiveChannel[T]]: - return _open_memory_channel(max_buffer_size) - - def __init__(self, max_buffer_size: int | float) -> None: # noqa: PYI041 - ... - -else: - # apply the generic_function decorator to make open_memory_channel indexable - # so it's valid to say e.g. ``open_memory_channel[bytes](5)`` at runtime - open_memory_channel = generic_function(_open_memory_channel) + + def __new__( # type: ignore[misc] # "must return a subtype" + cls, + max_buffer_size: int | float, # noqa: PYI041 + ) -> tuple[MemorySendChannel[T], MemoryReceiveChannel[T]]: + if max_buffer_size != inf and not isinstance(max_buffer_size, int): + raise TypeError("max_buffer_size must be an integer or math.inf") + if max_buffer_size < 0: + raise ValueError("max_buffer_size must be >= 0") + state: MemoryChannelState[T] = MemoryChannelState(max_buffer_size) + return ( + MemorySendChannel[T]._create(state), + MemoryReceiveChannel[T]._create(state), + ) + + def __init__(self, max_buffer_size: int | float) -> None: # noqa: PYI041 + ... @attrs.frozen diff --git a/src/trio/_core/_exceptions.py b/src/trio/_core/_exceptions.py index f70d5e0e95..f24e56b5e6 100644 --- a/src/trio/_core/_exceptions.py +++ b/src/trio/_core/_exceptions.py @@ -1,7 +1,7 @@ from __future__ import annotations from functools import partial -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Literal, TypeAlias import attrs @@ -10,7 +10,7 @@ if TYPE_CHECKING: from collections.abc import Callable - from typing_extensions import Self, TypeAlias + from typing_extensions import Self CancelReasonLiteral: TypeAlias = Literal[ "KeyboardInterrupt", diff --git a/src/trio/_core/_io_epoll.py b/src/trio/_core/_io_epoll.py index 5e05f0813f..d7064dc718 100644 --- a/src/trio/_core/_io_epoll.py +++ b/src/trio/_core/_io_epoll.py @@ -4,7 +4,7 @@ import select import sys from collections import defaultdict -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Literal, TypeAlias import attrs @@ -14,8 +14,6 @@ from ._wakeup_socketpair import WakeupSocketpair if TYPE_CHECKING: - from typing_extensions import TypeAlias - from .._core import Abort, RaiseCancelT from .._file_io import _HasFileNo diff --git a/src/trio/_core/_io_kqueue.py b/src/trio/_core/_io_kqueue.py index 9718c4df80..464ca457e1 100644 --- a/src/trio/_core/_io_kqueue.py +++ b/src/trio/_core/_io_kqueue.py @@ -4,7 +4,7 @@ import select import sys from contextlib import contextmanager -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Literal, TypeAlias import attrs import outcome @@ -16,8 +16,6 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterator - from typing_extensions import TypeAlias - from .._core import Abort, RaiseCancelT, Task, UnboundedQueue from .._file_io import _HasFileNo diff --git a/src/trio/_core/_io_windows.py b/src/trio/_core/_io_windows.py index 9a9d6b9cc4..7b789c3dec 100644 --- a/src/trio/_core/_io_windows.py +++ b/src/trio/_core/_io_windows.py @@ -5,13 +5,7 @@ import socket import sys from contextlib import contextmanager -from typing import ( - TYPE_CHECKING, - Literal, - Protocol, - TypeVar, - cast, -) +from typing import TYPE_CHECKING, Literal, Protocol, TypeAlias, TypeVar, cast import attrs from outcome import Value @@ -42,7 +36,7 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterator - from typing_extensions import Buffer, TypeAlias + from typing_extensions import Buffer from .._file_io import _HasFileNo from ._traps import Abort, RaiseCancelT diff --git a/src/trio/_core/_ki.py b/src/trio/_core/_ki.py index 9fa849229a..fa3369d56d 100644 --- a/src/trio/_core/_ki.py +++ b/src/trio/_core/_ki.py @@ -2,9 +2,8 @@ import signal import sys -import types import weakref -from typing import TYPE_CHECKING, Generic, Protocol, TypeVar +from typing import TYPE_CHECKING, Generic, Protocol, TypeGuard, TypeVar import attrs @@ -15,7 +14,7 @@ import types from collections.abc import Callable - from typing_extensions import Self, TypeGuard + from typing_extensions import Self # In ordinary single-threaded Python code, when you hit control-C, it raises # an exception and automatically does all the regular unwinding stuff. # diff --git a/src/trio/_core/_tests/test_exceptiongroup_gc.py b/src/trio/_core/_tests/test_exceptiongroup_gc.py index 885ef68624..e8ffa62945 100644 --- a/src/trio/_core/_tests/test_exceptiongroup_gc.py +++ b/src/trio/_core/_tests/test_exceptiongroup_gc.py @@ -3,13 +3,14 @@ import gc import sys from traceback import extract_tb -from typing import TYPE_CHECKING, Callable, NoReturn +from typing import TYPE_CHECKING, NoReturn import pytest from .._concat_tb import concat_tb if TYPE_CHECKING: + from collections.abc import Callable from types import TracebackType if sys.version_info < (3, 11): diff --git a/src/trio/_core/_tests/test_guest_mode.py b/src/trio/_core/_tests/test_guest_mode.py index 81b7a07d87..743eddc846 100644 --- a/src/trio/_core/_tests/test_guest_mode.py +++ b/src/trio/_core/_tests/test_guest_mode.py @@ -17,6 +17,7 @@ from typing import ( TYPE_CHECKING, NoReturn, + TypeAlias, TypeVar, cast, ) @@ -32,8 +33,6 @@ from .tutil import gc_collect_harder, restore_unraisablehook if TYPE_CHECKING: - from typing_extensions import TypeAlias - from trio._channel import MemorySendChannel T = TypeVar("T") @@ -507,8 +506,8 @@ def trio_done_callback(main_outcome: Outcome[T]) -> None: return (await trio_done_fut).unwrap() try: - # can't use asyncio.run because that fails on Windows (3.8, x64, with - # Komodia LSP) and segfaults on Windows (3.9, x64, with Komodia LSP) + # can't use asyncio.run because that fails on Windows (3.10, x64, with + # Komodia LSP) return loop.run_until_complete(aio_main()) finally: loop.close() diff --git a/src/trio/_core/_tests/test_ki.py b/src/trio/_core/_tests/test_ki.py index 07a7558720..a8d81ca020 100644 --- a/src/trio/_core/_tests/test_ki.py +++ b/src/trio/_core/_tests/test_ki.py @@ -6,8 +6,8 @@ import sys import threading import weakref -from collections.abc import AsyncIterator, Iterator -from typing import TYPE_CHECKING, Callable, TypeVar +from collections.abc import AsyncIterator, Callable, Iterator +from typing import TYPE_CHECKING, TypeVar import outcome import pytest diff --git a/src/trio/_core/_tests/test_run.py b/src/trio/_core/_tests/test_run.py index 111ba9e5ec..9b059a4366 100644 --- a/src/trio/_core/_tests/test_run.py +++ b/src/trio/_core/_tests/test_run.py @@ -1293,7 +1293,7 @@ async def child() -> None: # https://bugs.python.org/issue25612 (Example 2) # https://bugs.python.org/issue25683 # https://bugs.python.org/issue29587 (Example 1) -# This is fixed in CPython >= 3.9. +# This is fixed in CPython >= 3.9, but kept as a regression test. async def test_exception_chaining_after_throw() -> None: child_task: _core.Task | None = None @@ -2644,13 +2644,12 @@ async def crasher() -> NoReturn: old_flags = gc.get_debug() try: - # fmt: off - # Remove after 3.9 unsupported, black formats in a way that breaks if - # you do `-X oldparser` - with RaisesGroup( - Matcher(ValueError, "^this is a crash$"), - ), _core.CancelScope() as outer: - # fmt: on + with ( + RaisesGroup( + Matcher(ValueError, "^this is a crash$"), + ), + _core.CancelScope() as outer, + ): async with _core.open_nursery() as nursery: gc.collect() gc.set_debug(gc.DEBUG_SAVEALL) diff --git a/src/trio/_core/_traps.py b/src/trio/_core/_traps.py index 60f72d1295..652cc0b879 100644 --- a/src/trio/_core/_traps.py +++ b/src/trio/_core/_traps.py @@ -5,8 +5,16 @@ import enum import types -# Jedi gets mad in test_static_tool_sees_class_members if we use collections Callable -from typing import TYPE_CHECKING, Any, Callable, NoReturn, Union, cast +# typing.Callable is necessary because collections.abc.Callable breaks +# test_static_tool_sees_all_symbols in 3.10. +from typing import ( # noqa: UP035 + TYPE_CHECKING, + Any, + Callable, + NoReturn, + TypeAlias, + cast, +) import attrs import outcome @@ -16,8 +24,6 @@ if TYPE_CHECKING: from collections.abc import Awaitable, Generator - from typing_extensions import TypeAlias - from ._run import Task RaiseCancelT: TypeAlias = Callable[[], NoReturn] @@ -41,12 +47,12 @@ class PermanentlyDetachCoroutineObject: final_outcome: outcome.Outcome[object] -MessageType: TypeAlias = Union[ - type[CancelShieldedCheckpoint], - WaitTaskRescheduled, - PermanentlyDetachCoroutineObject, - object, -] +MessageType: TypeAlias = ( + type[CancelShieldedCheckpoint] + | WaitTaskRescheduled + | PermanentlyDetachCoroutineObject + | object +) # Helper for the bottommost 'yield'. You can't use 'yield' inside an async diff --git a/src/trio/_core/_windows_cffi.py b/src/trio/_core/_windows_cffi.py index 0e3c0b10b3..cafae17fa2 100644 --- a/src/trio/_core/_windows_cffi.py +++ b/src/trio/_core/_windows_cffi.py @@ -1,11 +1,10 @@ from __future__ import annotations import enum -from typing import TYPE_CHECKING, NewType, NoReturn, Protocol, cast +from typing import TYPE_CHECKING, NewType, NoReturn, Protocol, TypeAlias, cast if TYPE_CHECKING: import cffi - from typing_extensions import TypeAlias CData: TypeAlias = cffi.api.FFI.CData CType: TypeAlias = cffi.api.FFI.CType diff --git a/src/trio/_dtls.py b/src/trio/_dtls.py index a7dff634d9..f6ffc31c65 100644 --- a/src/trio/_dtls.py +++ b/src/trio/_dtls.py @@ -17,12 +17,7 @@ import warnings import weakref from itertools import count -from typing import ( - TYPE_CHECKING, - Generic, - TypeVar, - Union, -) +from typing import TYPE_CHECKING, Generic, TypeAlias, TypeVar from weakref import ReferenceType, WeakValueDictionary import attrs @@ -37,7 +32,7 @@ # See DTLSEndpoint.__init__ for why this is imported here from OpenSSL import SSL # noqa: TC004 - from typing_extensions import Self, TypeAlias, TypeVarTuple, Unpack + from typing_extensions import Self, TypeVarTuple, Unpack from trio._socket import AddressFormat from trio.socket import SocketType @@ -349,11 +344,9 @@ class OpaqueHandshakeMessage: record: Record -_AnyHandshakeMessage: TypeAlias = Union[ - HandshakeMessage, - PseudoHandshakeMessage, - OpaqueHandshakeMessage, -] +_AnyHandshakeMessage: TypeAlias = ( + HandshakeMessage | PseudoHandshakeMessage | OpaqueHandshakeMessage +) # This takes a raw outgoing handshake volley that openssl generated, and diff --git a/src/trio/_file_io.py b/src/trio/_file_io.py index 3df9b3e443..d9305ef4ff 100644 --- a/src/trio/_file_io.py +++ b/src/trio/_file_io.py @@ -10,6 +10,7 @@ AnyStr, BinaryIO, Generic, + Literal, TypeVar, Union, overload, @@ -29,7 +30,6 @@ OpenTextMode, StrOrBytesPath, ) - from typing_extensions import Literal from ._sync import CapacityLimiter diff --git a/src/trio/_highlevel_generic.py b/src/trio/_highlevel_generic.py index 9bd8822c9e..b2f8723388 100644 --- a/src/trio/_highlevel_generic.py +++ b/src/trio/_highlevel_generic.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Generic, TypeVar +from typing import Generic, TypeGuard, TypeVar import attrs @@ -9,10 +9,6 @@ from .abc import AsyncResource, HalfCloseableStream, ReceiveStream, SendStream -if TYPE_CHECKING: - from typing_extensions import TypeGuard - - SendStreamT = TypeVar("SendStreamT", bound=SendStream) ReceiveStreamT = TypeVar("ReceiveStreamT", bound=ReceiveStream) diff --git a/src/trio/_path.py b/src/trio/_path.py index af6fbe0059..21e33eaae4 100644 --- a/src/trio/_path.py +++ b/src/trio/_path.py @@ -5,7 +5,17 @@ import sys from functools import partial, update_wrapper from inspect import cleandoc -from typing import IO, TYPE_CHECKING, Any, BinaryIO, ClassVar, TypeVar, overload +from typing import ( + IO, + TYPE_CHECKING, + Any, + BinaryIO, + ClassVar, + Concatenate, + Literal, + TypeVar, + overload, +) from trio._file_io import AsyncIOWrapper, wrap_file from trio._util import final @@ -22,7 +32,7 @@ OpenBinaryModeWriting, OpenTextMode, ) - from typing_extensions import Concatenate, Literal, ParamSpec, Self + from typing_extensions import ParamSpec, Self P = ParamSpec("P") @@ -238,8 +248,7 @@ def __repr__(self) -> str: resolve = _wrap_method_path(pathlib.Path.resolve) rmdir = _wrap_method(pathlib.Path.rmdir) symlink_to = _wrap_method(pathlib.Path.symlink_to) - if sys.version_info >= (3, 10): - hardlink_to = _wrap_method(pathlib.Path.hardlink_to) + hardlink_to = _wrap_method(pathlib.Path.hardlink_to) touch = _wrap_method(pathlib.Path.touch) unlink = _wrap_method(pathlib.Path.unlink) absolute = _wrap_method_path(pathlib.Path.absolute) diff --git a/src/trio/_repl.py b/src/trio/_repl.py index 5a96e68789..6b5612e28f 100644 --- a/src/trio/_repl.py +++ b/src/trio/_repl.py @@ -7,7 +7,7 @@ import warnings from code import InteractiveConsole from types import CodeType, FrameType, FunctionType -from typing import Callable +from typing import TYPE_CHECKING import outcome @@ -15,6 +15,9 @@ import trio.lowlevel from trio._util import final +if TYPE_CHECKING: + from collections.abc import Callable + class SuppressDecorator(contextlib.ContextDecorator, contextlib.suppress): pass diff --git a/src/trio/_socket.py b/src/trio/_socket.py index 8454970693..68bbd4b6b8 100644 --- a/src/trio/_socket.py +++ b/src/trio/_socket.py @@ -9,9 +9,10 @@ from typing import ( TYPE_CHECKING, Any, + Concatenate, SupportsIndex, + TypeAlias, TypeVar, - Union, overload, ) @@ -26,7 +27,7 @@ from collections.abc import Awaitable, Callable, Iterable from types import TracebackType - from typing_extensions import Buffer, Concatenate, ParamSpec, Self, TypeAlias + from typing_extensions import Buffer, ParamSpec, Self from ._abc import HostnameResolver, SocketFactory @@ -336,8 +337,8 @@ def fromshare(info: bytes) -> SocketType: FamilyDefault = _stdlib_socket.AF_INET else: FamilyDefault: None = None - FamilyT: TypeAlias = Union[int, AddressFamily, None] - TypeT: TypeAlias = Union[_stdlib_socket.socket, int] + FamilyT: TypeAlias = int | AddressFamily | None + TypeT: TypeAlias = _stdlib_socket.socket | int @_wraps(_stdlib_socket.socketpair, assigned=(), updated=()) diff --git a/src/trio/_subprocess.py b/src/trio/_subprocess.py index d4faf317f8..145c2de9b0 100644 --- a/src/trio/_subprocess.py +++ b/src/trio/_subprocess.py @@ -12,8 +12,8 @@ Final, Literal, Protocol, + TypeAlias, TypedDict, - Union, overload, ) @@ -34,13 +34,13 @@ from collections.abc import Awaitable, Callable, Iterable, Mapping, Sequence from io import TextIOWrapper - from typing_extensions import TypeAlias, Unpack + from typing_extensions import Unpack from ._abc import ReceiveStream, SendStream # Sphinx cannot parse the stringified version -StrOrBytesPath: TypeAlias = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] +StrOrBytesPath: TypeAlias = str | bytes | os.PathLike[str] | os.PathLike[bytes] # Linux-specific, but has complex lifetime management stuff so we hard-code it @@ -1088,7 +1088,7 @@ async def my_deliver_cancel(process): # overloads. But might still be a problem for other static analyzers / docstring # readers (?) - class UnixProcessArgs3_9(GeneralProcessArgs, total=False): + class UnixProcessArgs3_10(GeneralProcessArgs, total=False): """Arguments shared between all Unix runs.""" preexec_fn: Callable[[], object] | None @@ -1102,9 +1102,7 @@ class UnixProcessArgs3_9(GeneralProcessArgs, total=False): user: str | int | None umask: int - class UnixProcessArgs3_10(UnixProcessArgs3_9, total=False): - """Arguments shared between all Unix runs on 3.10+.""" - + # 3.10+ pipesize: int class UnixProcessArgs3_11(UnixProcessArgs3_10, total=False): @@ -1129,18 +1127,12 @@ class UnixRunProcessMixin(TypedDict, total=False): class UnixRunProcessArgs(UnixProcessArgs3_11, UnixRunProcessMixin): """Arguments for run_process on Unix with 3.11+""" - elif sys.version_info >= (3, 10): + else: UnixProcessArgs = UnixProcessArgs3_10 class UnixRunProcessArgs(UnixProcessArgs3_10, UnixRunProcessMixin): """Arguments for run_process on Unix with 3.10+""" - else: - UnixProcessArgs = UnixProcessArgs3_9 - - class UnixRunProcessArgs(UnixProcessArgs3_9, UnixRunProcessMixin): - """Arguments for run_process on Unix with 3.9+""" - @overload # type: ignore[no-overload-impl] async def open_process( command: StrOrBytesPath, diff --git a/src/trio/_tests/check_type_completeness.py b/src/trio/_tests/check_type_completeness.py index 156f6987ca..ef5bdaafa4 100755 --- a/src/trio/_tests/check_type_completeness.py +++ b/src/trio/_tests/check_type_completeness.py @@ -32,7 +32,7 @@ def run_pyright(platform: str) -> subprocess.CompletedProcess[bytes]: "pyright", # Specify a platform and version to keep imported modules consistent. f"--pythonplatform={platform}", - "--pythonversion=3.9", + "--pythonversion=3.10", "--verifytypes=trio", "--outputjson", "--ignoreexternal", diff --git a/src/trio/_tests/test_channel.py b/src/trio/_tests/test_channel.py index d2664b95c7..986207b309 100644 --- a/src/trio/_tests/test_channel.py +++ b/src/trio/_tests/test_channel.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING import pytest @@ -23,7 +23,7 @@ async def test_channel() -> None: with pytest.raises(ValueError, match=r"^max_buffer_size must be >= 0$"): open_memory_channel(-1) - s, r = open_memory_channel[Union[int, str, None]](2) + s, r = open_memory_channel[int | str | None](2) repr(s) # smoke test repr(r) # smoke test @@ -362,7 +362,7 @@ async def test_statistics() -> None: async def test_channel_fairness() -> None: # We can remove an item we just sent, and send an item back in after, if # no-one else is waiting. - s, r = open_memory_channel[Union[int, None]](1) + s, r = open_memory_channel[int | None](1) s.send_nowait(1) assert r.receive_nowait() == 1 s.send_nowait(2) @@ -388,7 +388,7 @@ async def do_receive(r: trio.MemoryReceiveChannel[int | None]) -> None: # And the analogous situation for send: if we free up a space, we can't # immediately send something in it if someone is already waiting to do # that - s, r = open_memory_channel[Union[int, None]](1) + s, r = open_memory_channel[int | None](1) s.send_nowait(1) with pytest.raises(trio.WouldBlock): s.send_nowait(None) @@ -556,6 +556,7 @@ async def agen() -> AsyncGenerator[int]: async def test_as_safe_channel_doesnt_leak_cancellation() -> None: @as_safe_channel async def agen() -> AsyncGenerator[None]: + yield with trio.CancelScope() as cscope: cscope.cancel() yield @@ -733,6 +734,7 @@ async def agen() -> AsyncGenerator[None]: async def test_as_safe_channel_close_during_iteration() -> None: @as_safe_channel async def agen() -> AsyncGenerator[None]: + yield await chan.aclose() while True: yield diff --git a/src/trio/_tests/test_exports.py b/src/trio/_tests/test_exports.py index bc719ceb0c..0f8158f377 100644 --- a/src/trio/_tests/test_exports.py +++ b/src/trio/_tests/test_exports.py @@ -243,16 +243,9 @@ def no_underscores(symbols: Iterable[str]) -> set[str]: raise AssertionError() -# this could be sped up by only invoking mypy once per module, or even once for all -# modules, instead of once per class. @slow # see comment on test_static_tool_sees_all_symbols @pytest.mark.redistributors_should_skip -@pytest.mark.skipif( - sys.version_info[:4] == (3, 14, 0, "beta"), - # 2 passes, 12 fails - reason="several tools don't support 3.14.0", -) # Static analysis tools often have trouble with alpha releases, where Python's # internals are in flux, grammar may not have settled down, etc. @pytest.mark.skipif( @@ -522,6 +515,11 @@ def lookup_symbol(symbol: str) -> dict[str, Any]: # type: ignore[misc, explicit ): missing.remove("with_segments") + # tuple subclasses are weird + if issubclass(class_, tuple): + extra.remove("__reversed__") + missing.remove("__getnewargs__") + if sys.version_info >= (3, 13) and attrs.has(class_): missing.remove("__replace__") diff --git a/src/trio/_tests/test_path.py b/src/trio/_tests/test_path.py index b347ae2c76..533ff94c5a 100644 --- a/src/trio/_tests/test_path.py +++ b/src/trio/_tests/test_path.py @@ -2,7 +2,7 @@ import os import pathlib -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING import pytest @@ -51,8 +51,8 @@ def test_magic() -> None: assert bytes(path) == b"test" -EitherPathType = Union[type[trio.Path], type[pathlib.Path]] -PathOrStrType = Union[EitherPathType, type[str]] +EitherPathType = type[trio.Path] | type[pathlib.Path] +PathOrStrType = EitherPathType | type[str] cls_pairs: list[tuple[EitherPathType, EitherPathType]] = [ (trio.Path, pathlib.Path), (pathlib.Path, trio.Path), diff --git a/src/trio/_tests/test_repl.py b/src/trio/_tests/test_repl.py index ae125d9ab0..28075d551c 100644 --- a/src/trio/_tests/test_repl.py +++ b/src/trio/_tests/test_repl.py @@ -44,13 +44,6 @@ def test_build_raw_input() -> None: raw_input() -# In 3.10 or later, types.FunctionType (used internally) will automatically -# attach __builtins__ to the function objects. However we need to explicitly -# include it for 3.9 support -def build_locals() -> dict[str, object]: - return {"__builtins__": __builtins__} - - async def test_basic_interaction( capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch, @@ -59,7 +52,7 @@ async def test_basic_interaction( Run some basic commands through the interpreter while capturing stdout. Ensure that the interpreted prints the expected results. """ - console = trio._repl.TrioInteractiveConsole(repl_locals=build_locals()) + console = trio._repl.TrioInteractiveConsole() raw_input = build_raw_input( [ # evaluate simple expression and recall the value @@ -89,7 +82,7 @@ async def test_basic_interaction( async def test_system_exits_quit_interpreter(monkeypatch: pytest.MonkeyPatch) -> None: - console = trio._repl.TrioInteractiveConsole(repl_locals=build_locals()) + console = trio._repl.TrioInteractiveConsole() raw_input = build_raw_input( [ "raise SystemExit", @@ -104,7 +97,7 @@ async def test_KI_interrupts( capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch, ) -> None: - console = trio._repl.TrioInteractiveConsole(repl_locals=build_locals()) + console = trio._repl.TrioInteractiveConsole() raw_input = build_raw_input( [ "import signal, trio, trio.lowlevel", @@ -132,7 +125,7 @@ async def test_system_exits_in_exc_group( capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch, ) -> None: - console = trio._repl.TrioInteractiveConsole(repl_locals=build_locals()) + console = trio._repl.TrioInteractiveConsole() raw_input = build_raw_input( [ "import sys", @@ -155,7 +148,7 @@ async def test_system_exits_in_nested_exc_group( capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch, ) -> None: - console = trio._repl.TrioInteractiveConsole(repl_locals=build_locals()) + console = trio._repl.TrioInteractiveConsole() raw_input = build_raw_input( [ "import sys", @@ -179,7 +172,7 @@ async def test_base_exception_captured( capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch, ) -> None: - console = trio._repl.TrioInteractiveConsole(repl_locals=build_locals()) + console = trio._repl.TrioInteractiveConsole() raw_input = build_raw_input( [ # The statement after raise should still get executed @@ -199,7 +192,7 @@ async def test_exc_group_captured( capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch, ) -> None: - console = trio._repl.TrioInteractiveConsole(repl_locals=build_locals()) + console = trio._repl.TrioInteractiveConsole() raw_input = build_raw_input( [ # The statement after raise should still get executed @@ -217,7 +210,7 @@ async def test_base_exception_capture_from_coroutine( capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch, ) -> None: - console = trio._repl.TrioInteractiveConsole(repl_locals=build_locals()) + console = trio._repl.TrioInteractiveConsole() raw_input = build_raw_input( [ "async def async_func_raises_base_exception():", diff --git a/src/trio/_tests/test_socket.py b/src/trio/_tests/test_socket.py index 850e0b4a3b..2b8a3a5ee1 100644 --- a/src/trio/_tests/test_socket.py +++ b/src/trio/_tests/test_socket.py @@ -8,7 +8,7 @@ import tempfile from pathlib import Path from socket import AddressFamily, SocketKind -from typing import TYPE_CHECKING, Union, cast +from typing import TYPE_CHECKING, TypeAlias, cast import attrs import pytest @@ -21,8 +21,6 @@ if TYPE_CHECKING: from collections.abc import Callable - from typing_extensions import TypeAlias - from .._highlevel_socket import SocketStream GaiTuple: TypeAlias = tuple[ @@ -30,12 +28,12 @@ SocketKind, int, str, - Union[tuple[str, int], tuple[str, int, int, int], tuple[int, bytes]], + tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes], ] GetAddrInfoResponse: TypeAlias = list[GaiTuple] GetAddrInfoArgs: TypeAlias = tuple[ - Union[str, bytes, None], - Union[str, bytes, int, None], + str | bytes | None, + str | bytes | int | None, int, int, int, @@ -669,7 +667,7 @@ async def res( local=local, # noqa: B023 # local is not bound in function definition ) assert isinstance(value, tuple) - return cast("tuple[Union[str, int], ...]", value) + return cast("tuple[str | int, ...]", value) assert_eq(await res((addrs.arbitrary, "http")), (addrs.arbitrary, 80)) if v6: diff --git a/src/trio/_tests/test_ssl.py b/src/trio/_tests/test_ssl.py index 5427ee65ae..b365f7dd35 100644 --- a/src/trio/_tests/test_ssl.py +++ b/src/trio/_tests/test_ssl.py @@ -8,11 +8,7 @@ from contextlib import asynccontextmanager, contextmanager, suppress from functools import partial from ssl import SSLContext -from typing import ( - TYPE_CHECKING, - Any, - NoReturn, -) +from typing import TYPE_CHECKING, Any, NoReturn, TypeAlias import pytest @@ -54,8 +50,6 @@ if TYPE_CHECKING: from collections.abc import AsyncIterator, Awaitable, Callable, Iterator - from typing_extensions import TypeAlias - from trio._core import MockClock from trio._ssl import T_Stream diff --git a/src/trio/_tests/test_subprocess.py b/src/trio/_tests/test_subprocess.py index f1bfa4ed06..71b143c38c 100644 --- a/src/trio/_tests/test_subprocess.py +++ b/src/trio/_tests/test_subprocess.py @@ -15,6 +15,7 @@ TYPE_CHECKING, Any, NoReturn, + TypeAlias, ) from unittest import mock @@ -40,8 +41,6 @@ if TYPE_CHECKING: from types import FrameType - from typing_extensions import TypeAlias - from .._abc import ReceiveStream if sys.platform == "win32": diff --git a/src/trio/_tests/test_sync.py b/src/trio/_tests/test_sync.py index 836e8597e8..0646898983 100644 --- a/src/trio/_tests/test_sync.py +++ b/src/trio/_tests/test_sync.py @@ -2,7 +2,8 @@ import re import weakref -from typing import TYPE_CHECKING, Callable, Union +from collections.abc import Callable +from typing import TypeAlias import pytest @@ -14,9 +15,6 @@ from .._timeouts import sleep_forever from ..testing import assert_checkpoints, wait_all_tasks_blocked -if TYPE_CHECKING: - from typing_extensions import TypeAlias - async def test_Event() -> None: e = Event() @@ -587,15 +585,15 @@ def release(self) -> None: ids=lock_factory_names, ) -LockLike: TypeAlias = Union[ - CapacityLimiter, - Semaphore, - Lock, - StrictFIFOLock, - ChannelLock1, - ChannelLock2, - ChannelLock3, -] +LockLike: TypeAlias = ( + CapacityLimiter + | Semaphore + | Lock + | StrictFIFOLock + | ChannelLock1 + | ChannelLock2 + | ChannelLock3 +) LockFactory: TypeAlias = Callable[[], LockLike] diff --git a/src/trio/_tests/test_threads.py b/src/trio/_tests/test_threads.py index ba2c366faa..0ad1163011 100644 --- a/src/trio/_tests/test_threads.py +++ b/src/trio/_tests/test_threads.py @@ -12,7 +12,6 @@ TYPE_CHECKING, NoReturn, TypeVar, - Union, ) import pytest @@ -48,7 +47,7 @@ from ..lowlevel import Task -RecordType = list[tuple[str, Union[threading.Thread, type[BaseException]]]] +RecordType = list[tuple[str, threading.Thread | type[BaseException]]] T = TypeVar("T") diff --git a/src/trio/_tests/test_timeouts.py b/src/trio/_tests/test_timeouts.py index d1f423c848..ff62a0708f 100644 --- a/src/trio/_tests/test_timeouts.py +++ b/src/trio/_tests/test_timeouts.py @@ -137,14 +137,14 @@ async def task() -> None: @slow async def test_fail_after_fails_even_if_shielded() -> None: async def task() -> None: - # fmt: off - # Remove after 3.9 unsupported, black formats in a way that breaks if - # you do `-X oldparser` - with pytest.raises(TooSlowError), _core.CancelScope() as outer, fail_after( - TARGET, - shield=True, + with ( + pytest.raises(TooSlowError), + _core.CancelScope() as outer, + fail_after( + TARGET, + shield=True, + ), ): - # fmt: on outer.cancel() # The outer scope is cancelled, but this task is protected by the # shield, so it manages to get to sleep until deadline is met diff --git a/src/trio/_tests/test_util.py b/src/trio/_tests/test_util.py index 5aaad267f6..69310d64e7 100644 --- a/src/trio/_tests/test_util.py +++ b/src/trio/_tests/test_util.py @@ -23,7 +23,6 @@ coroutine_or_error, final, fixup_module_metadata, - generic_function, is_main_thread, raise_single_exception_from_group, ) @@ -159,20 +158,6 @@ async def async_gen( del excinfo -def test_generic_function() -> None: - @generic_function # Decorated function contains "Any". - def test_func(arg: T) -> T: # type: ignore[misc] - """Look, a docstring!""" - return arg - - assert test_func is test_func[int] is test_func[int, str] - assert test_func(42) == test_func[int](42) == 42 - assert test_func.__doc__ == "Look, a docstring!" - assert test_func.__qualname__ == "test_generic_function..test_func" # type: ignore[attr-defined] - assert test_func.__name__ == "test_func" # type: ignore[attr-defined] - assert test_func.__module__ == __name__ - - def test_final_decorator() -> None: """Test that subclassing a @final-annotated class is not allowed. diff --git a/src/trio/_tests/type_tests/path.py b/src/trio/_tests/type_tests/path.py index 6749d06276..2b956c9315 100644 --- a/src/trio/_tests/type_tests/path.py +++ b/src/trio/_tests/type_tests/path.py @@ -104,8 +104,7 @@ async def async_attrs(path: trio.Path) -> None: assert_type(await path.rmdir(), None) assert_type(await path.samefile("something_else"), bool) assert_type(await path.symlink_to("somewhere"), None) - if sys.version_info >= (3, 10): - assert_type(await path.hardlink_to("elsewhere"), None) + assert_type(await path.hardlink_to("elsewhere"), None) assert_type(await path.touch(), None) assert_type(await path.unlink(missing_ok=True), None) assert_type(await path.write_bytes(b"123"), int) diff --git a/src/trio/_tests/type_tests/raisesgroup.py b/src/trio/_tests/type_tests/raisesgroup.py index c8a781d786..012c42b4d8 100644 --- a/src/trio/_tests/type_tests/raisesgroup.py +++ b/src/trio/_tests/type_tests/raisesgroup.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from typing import Callable, Union +from collections.abc import Callable from trio.testing import Matcher, RaisesGroup from typing_extensions import assert_type @@ -142,10 +142,7 @@ def check_nested_raisesgroups_contextmanager() -> None: assert_type( excinfo.value.exceptions[0], # this union is because of how typeshed defines .exceptions - Union[ - ExceptionGroup[ValueError], - ExceptionGroup[ExceptionGroup[ValueError]], - ], + ExceptionGroup[ValueError] | ExceptionGroup[ExceptionGroup[ValueError]], ) @@ -222,8 +219,5 @@ def check_check_typing() -> None: # `BaseExceptiongroup` should perhaps be `ExceptionGroup`, but close enough assert_type( RaisesGroup(ValueError).check, - Union[ - Callable[[BaseExceptionGroup[ValueError]], bool], - None, - ], + Callable[[BaseExceptionGroup[ValueError]], bool] | None, ) diff --git a/src/trio/_tools/gen_exports.py b/src/trio/_tools/gen_exports.py index 101e0e4912..e3d1659c02 100755 --- a/src/trio/_tools/gen_exports.py +++ b/src/trio/_tools/gen_exports.py @@ -12,15 +12,13 @@ import sys from pathlib import Path from textwrap import indent -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, TypeGuard import attrs if TYPE_CHECKING: from collections.abc import Iterable, Iterator - from typing_extensions import TypeGuard - # keep these imports up to date with conditional imports in test_gen_exports # isort: split import astor @@ -290,8 +288,7 @@ def process(files: Iterable[File], *, do_test: bool) -> None: print("Generated sources are up to date.") else: for new_path, new_source in new_files.items(): - with open(new_path, "w", encoding="utf-8", newline="\n") as fp: - fp.write(new_source) + Path(new_path).write_text(new_source, encoding="utf-8", newline="\n") print("Regenerated sources successfully.") if not matches_disk: # TODO: test this branch # With pre-commit integration, show that we edited files. diff --git a/src/trio/_unix_pipes.py b/src/trio/_unix_pipes.py index dbe4358b4c..9bfd5dc18a 100644 --- a/src/trio/_unix_pipes.py +++ b/src/trio/_unix_pipes.py @@ -3,16 +3,13 @@ import errno import os import sys -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Final as FinalType import trio from ._abc import Stream from ._util import ConflictDetector, final -if TYPE_CHECKING: - from typing import Final as FinalType - assert not TYPE_CHECKING or sys.platform != "win32" # XX TODO: is this a good number? who knows... it does match the default Linux @@ -159,12 +156,7 @@ async def wait_send_all_might_not_block(self) -> None: with self._send_conflict_detector: if self._fd_holder.closed: raise trio.ClosedResourceError("file was already closed") - try: - await trio.lowlevel.wait_writable(self._fd_holder.fd) - except BrokenPipeError as e: - # kqueue: raises EPIPE on wait_writable instead - # of sending, which is annoying - raise trio.BrokenResourceError from e + await trio.lowlevel.wait_writable(self._fd_holder.fd) async def receive_some(self, max_bytes: int | None = None) -> bytes: with self._receive_conflict_detector: diff --git a/src/trio/_util.py b/src/trio/_util.py index 54d324cab3..8c10362f9d 100644 --- a/src/trio/_util.py +++ b/src/trio/_util.py @@ -6,11 +6,9 @@ import signal from abc import ABCMeta from collections.abc import Awaitable, Callable, Sequence -from functools import update_wrapper from typing import ( TYPE_CHECKING, Any, - Generic, NoReturn, TypeVar, final as std_final, @@ -29,7 +27,7 @@ import sys from types import AsyncGeneratorType, TracebackType - from typing_extensions import Self, TypeVarTuple, Unpack + from typing_extensions import TypeVarTuple, Unpack if sys.version_info < (3, 11): from exceptiongroup import BaseExceptionGroup @@ -230,40 +228,6 @@ def fix_one(qualname: str, name: str, obj: object) -> None: fix_one(objname, objname, obj) -# We need ParamSpec to type this "properly", but that requires a runtime typing_extensions import -# to use as a class base. This is only used at runtime and isn't correct for type checkers anyway, -# so don't bother. -class generic_function(Generic[RetT]): - """Decorator that makes a function indexable, to communicate - non-inferable generic type parameters to a static type checker. - - If you write:: - - @generic_function - def open_memory_channel(max_buffer_size: int) -> Tuple[ - SendChannel[T], ReceiveChannel[T] - ]: ... - - it is valid at runtime to say ``open_memory_channel[bytes](5)``. - This behaves identically to ``open_memory_channel(5)`` at runtime, - and currently won't type-check without a mypy plugin or clever stubs, - but at least it becomes possible to write those. - """ - - def __init__( # type: ignore[explicit-any] - self, - fn: Callable[..., RetT], - ) -> None: - update_wrapper(self, fn) - self._fn = fn - - def __call__(self, *args: object, **kwargs: object) -> RetT: - return self._fn(*args, **kwargs) - - def __getitem__(self, subscript: object) -> Self: - return self - - def _init_final_cls(cls: type[object]) -> NoReturn: """Raises an exception when a final class is subclassed.""" raise TypeError(f"{cls.__module__}.{cls.__qualname__} does not support subclassing") diff --git a/src/trio/testing/_check_streams.py b/src/trio/testing/_check_streams.py index e58e2ddfed..a53a45416a 100644 --- a/src/trio/testing/_check_streams.py +++ b/src/trio/testing/_check_streams.py @@ -8,6 +8,7 @@ from typing import ( TYPE_CHECKING, Generic, + TypeAlias, TypeVar, ) @@ -19,7 +20,7 @@ if TYPE_CHECKING: from types import TracebackType - from typing_extensions import ParamSpec, TypeAlias + from typing_extensions import ParamSpec ArgsT = ParamSpec("ArgsT") diff --git a/src/trio/testing/_fake_net.py b/src/trio/testing/_fake_net.py index 5d63112e17..4fed1b0bda 100644 --- a/src/trio/testing/_fake_net.py +++ b/src/trio/testing/_fake_net.py @@ -14,13 +14,7 @@ import os import socket import sys -from typing import ( - TYPE_CHECKING, - Any, - NoReturn, - Union, - overload, -) +from typing import TYPE_CHECKING, Any, NoReturn, TypeAlias, overload import attrs @@ -33,11 +27,11 @@ from socket import AddressFamily, SocketKind from types import TracebackType - from typing_extensions import Buffer, Self, TypeAlias + from typing_extensions import Buffer, Self from trio._socket import AddressFormat -IPAddress: TypeAlias = Union[ipaddress.IPv4Address, ipaddress.IPv6Address] +IPAddress: TypeAlias = ipaddress.IPv4Address | ipaddress.IPv6Address def _family_for(ip: IPAddress) -> int: diff --git a/src/trio/testing/_memory_streams.py b/src/trio/testing/_memory_streams.py index 547d8afbe9..6eca1a542e 100644 --- a/src/trio/testing/_memory_streams.py +++ b/src/trio/testing/_memory_streams.py @@ -2,16 +2,12 @@ import operator from collections.abc import Awaitable, Callable -from typing import TYPE_CHECKING, TypeVar +from typing import TypeAlias, TypeVar from .. import _core, _util from .._highlevel_generic import StapledStream from ..abc import ReceiveStream, SendStream -if TYPE_CHECKING: - from typing_extensions import TypeAlias - - AsyncHook: TypeAlias = Callable[[], Awaitable[object]] # Would be nice to exclude awaitable here, but currently not possible. SyncHook: TypeAlias = Callable[[], object] diff --git a/src/trio/testing/_raises_group.py b/src/trio/testing/_raises_group.py index 6001e4dab1..2c483a0f29 100644 --- a/src/trio/testing/_raises_group.py +++ b/src/trio/testing/_raises_group.py @@ -5,13 +5,7 @@ from abc import ABC, abstractmethod from re import Pattern from textwrap import indent -from typing import ( - TYPE_CHECKING, - Generic, - Literal, - cast, - overload, -) +from typing import TYPE_CHECKING, Generic, Literal, TypeGuard, cast, overload from trio._util import final @@ -24,7 +18,7 @@ from collections.abc import Callable, Sequence from _pytest._code.code import ExceptionChainRepr, ReprExceptionInfo, Traceback - from typing_extensions import TypeGuard, TypeVar + from typing_extensions import TypeVar # this conditional definition is because we want to allow a TypeVar default MatchE = TypeVar( diff --git a/test-requirements.txt b/test-requirements.txt index d0f99d71a3..22951703b9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,14 +1,10 @@ # 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.10 test-requirements.in -o test-requirements.txt +alabaster==1.0.0 # via sphinx astor==0.8.1 # via -r test-requirements.in -astroid==3.3.11 ; python_full_version < '3.10' - # via pylint -astroid==4.0.1 ; python_full_version >= '3.10' +astroid==4.0.1 # via pylint async-generator==1.10 # via -r test-requirements.in @@ -30,9 +26,7 @@ cfgv==3.4.0 # via pre-commit charset-normalizer==3.4.4 # via requests -click==8.1.8 ; python_full_version < '3.10' and implementation_name == 'cpython' - # via black -click==8.3.0 ; python_full_version >= '3.10' and implementation_name == 'cpython' +click==8.3.0 ; implementation_name == 'cpython' # via black codespell==2.4.1 # via -r test-requirements.in @@ -42,9 +36,7 @@ colorama==0.4.6 ; sys_platform == 'win32' # pylint # pytest # sphinx -coverage==7.10.7 ; python_full_version < '3.10' - # via -r test-requirements.in -coverage==7.11.0 ; python_full_version >= '3.10' +coverage==7.11.0 # via -r test-requirements.in cryptography==46.0.3 # via @@ -62,9 +54,7 @@ exceptiongroup==1.3.0 ; python_full_version < '3.11' # via # -r test-requirements.in # pytest -filelock==3.19.1 ; python_full_version < '3.10' - # via virtualenv -filelock==3.20.0 ; python_full_version >= '3.10' +filelock==3.20.0 # via virtualenv identify==2.6.15 # via pre-commit @@ -75,17 +65,9 @@ idna==3.11 # trustme imagesize==1.4.1 # via sphinx -importlib-metadata==8.7.0 ; python_full_version < '3.10' - # via - # isort - # sphinx -iniconfig==2.1.0 ; python_full_version < '3.10' - # via pytest -iniconfig==2.3.0 ; python_full_version >= '3.10' +iniconfig==2.3.0 # via pytest -isort==6.1.0 ; python_full_version < '3.10' - # via pylint -isort==7.0.0 ; python_full_version >= '3.10' +isort==7.0.0 # via pylint jedi==0.19.2 ; implementation_name == 'cpython' # via -r test-requirements.in @@ -121,12 +103,7 @@ pathspec==0.12.1 # via # black # mypy -platformdirs==4.4.0 ; python_full_version < '3.10' - # via - # black - # pylint - # virtualenv -platformdirs==4.5.0 ; python_full_version >= '3.10' +platformdirs==4.5.0 # via # black # pylint @@ -141,9 +118,7 @@ pygments==2.19.2 # via # pytest # sphinx -pylint==3.3.9 ; python_full_version < '3.10' - # via -r test-requirements.in -pylint==4.0.2 ; python_full_version >= '3.10' +pylint==4.0.2 # via -r test-requirements.in pyopenssl==25.3.0 # via -r test-requirements.in @@ -167,9 +142,7 @@ snowballstemmer==3.0.1 # 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.*' +sphinx==8.1.3 ; python_full_version < '3.11' # via -r test-requirements.in sphinx==8.2.3 ; python_full_version >= '3.11' # via -r test-requirements.in @@ -216,7 +189,6 @@ typing-extensions==4.15.0 # cryptography # exceptiongroup # mypy - # pylint # pyopenssl # pyright # virtualenv @@ -226,5 +198,3 @@ uv==0.9.5 # via -r test-requirements.in virtualenv==20.35.3 # via pre-commit -zipp==3.23.0 ; python_full_version < '3.10' - # via importlib-metadata From f05398f17521e246dff608c6843f6970bcabc220 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 28 Oct 2025 21:41:25 +0100 Subject: [PATCH 102/111] Add free-threaded Python 3.14t to the testing https://hugovk.dev/blog/2025/free-threaded-python-on-github-actions --- .github/workflows/ci.yml | 30 +++++++++++++++--------------- test-requirements.in | 3 +-- test-requirements.txt | 2 -- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 47527d0e39..9daca186a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,12 +33,12 @@ jobs: steps: - name: Switch to using Python 3.11 - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: 3.11 - name: Grab the source from Git - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: persist-credentials: false @@ -74,7 +74,7 @@ jobs: SDIST: ${{ steps.artifact-name.outputs.sdist }} WHEEL: ${{ steps.artifact-name.outputs.wheel }} - name: Store the distribution packages - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: ${{ env.dists-artifact-name }} # NOTE: Exact expected file names are specified here @@ -104,7 +104,7 @@ jobs: - name: >- Smoke-test: grab the source from Git into git-src - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: path: git-src persist-credentials: false @@ -155,7 +155,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['3.10', '3.11', '3.12', '3.13', '3.14'] + python: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.14t'] arch: ['x86', 'x64'] lsp: [''] lsp_extract_file: [''] @@ -197,8 +197,8 @@ jobs: with: source-tarball-name: ${{ needs.build.outputs.sdist-artifact-name }} workflow-artifact-name: ${{ env.dists-artifact-name }} - - name: Setup python - uses: actions/setup-python@v5 + - name: Setup Python + uses: actions/setup-python@v6 with: python-version: '${{ matrix.python }}' architecture: '${{ matrix.arch }}' @@ -228,7 +228,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['pypy-3.11', '3.10', '3.11', '3.12', '3.13', '3.14'] + python: ['pypy-3.11', '3.10', '3.11', '3.12', '3.13', '3.14', '3.14t'] check_formatting: ['0'] no_test_requirements: ['0'] extra_name: [''] @@ -261,8 +261,8 @@ jobs: uses: actions/checkout@v4 with: persist-credentials: false - - name: Setup python - uses: actions/setup-python@v5 + - name: Setup Python + uses: actions/setup-python@v6 with: python-version: '${{ matrix.python }}' cache: pip @@ -301,7 +301,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['pypy-3.11', '3.10', '3.11', '3.12', '3.13', '3.14'] + python: ['pypy-3.11', '3.10', '3.11', '3.12', '3.13', '3.14', '3.14t'] continue-on-error: >- ${{ ( @@ -317,8 +317,8 @@ jobs: with: source-tarball-name: ${{ needs.build.outputs.sdist-artifact-name }} workflow-artifact-name: ${{ env.dists-artifact-name }} - - name: Setup python - uses: actions/setup-python@v5 + - name: Setup Python + uses: actions/setup-python@v6 with: python-version: '${{ matrix.python }}' cache: pip @@ -406,8 +406,8 @@ jobs: with: source-tarball-name: ${{ needs.build.outputs.sdist-artifact-name }} workflow-artifact-name: ${{ env.dists-artifact-name }} - - name: Setup python - uses: actions/setup-python@v5 + - name: Setup Python + uses: actions/setup-python@v6 with: python-version: '${{ matrix.python }}' cache: pip diff --git a/test-requirements.in b/test-requirements.in index e49dbd1782..b16a2b5d6e 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -11,8 +11,7 @@ cryptography>=41.0.0 # cryptography<41 segfaults on pypy3.10 # Tools black; implementation_name == "cpython" -mypy # Would use mypy[faster-cache], but orjson has build issues on pypy -orjson; implementation_name == "cpython" # orjson does not yet install on 3.14 +mypy ruff >= 0.8.0 astor # code generation uv >= 0.2.24 diff --git a/test-requirements.txt b/test-requirements.txt index 22951703b9..9717b1646c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -88,8 +88,6 @@ nodeenv==1.9.1 # via # pre-commit # pyright -orjson==3.11.3 ; implementation_name == 'cpython' - # via -r test-requirements.in outcome==1.3.0.post0 # via -r test-requirements.in packaging==25.0 From c0b394a55594dc39f353bae87e745083acff172e Mon Sep 17 00:00:00 2001 From: A5rocks Date: Fri, 31 Oct 2025 16:02:47 +0900 Subject: [PATCH 103/111] Bump version to 0.32.0 --- docs/source/history.rst | 32 ++++++++++++++++++++++++++++++++ newsfragments/3321.feature.rst | 1 - newsfragments/3331.bugfix.rst | 3 --- newsfragments/3332.misc.rst | 1 - newsfragments/3337.bugfix.rst | 3 --- newsfragments/3345.removal.rst | 1 - src/trio/_version.py | 2 +- 7 files changed, 33 insertions(+), 10 deletions(-) delete mode 100644 newsfragments/3321.feature.rst delete mode 100644 newsfragments/3331.bugfix.rst delete mode 100644 newsfragments/3332.misc.rst delete mode 100644 newsfragments/3337.bugfix.rst delete mode 100644 newsfragments/3345.removal.rst diff --git a/docs/source/history.rst b/docs/source/history.rst index da41b29d16..d66ac70ff0 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -5,6 +5,38 @@ Release history .. towncrier release notes start +trio 0.32.0 (2025-10-31) +------------------------ + +Features +~~~~~~~~ + +- Allow `trio.CapacityLimiter` to have zero total_tokens. (`#3321 `__) + + +Bugfixes +~~~~~~~~ + +- Fixed a bug where iterating over an ``@as_safe_channel``-derived ``ReceiveChannel`` + would raise `~trio.BrokenResourceError` if the channel was closed by another task. + It now shuts down cleanly. (`#3331 `__) +- `trio.lowlevel.Task.iter_await_frames` now works on completed tasks, by + returning an empty list of frames if the underlying coroutine has been closed. + Previously, it raised an internal error. (`#3337 `__) + + +Removals without deprecations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Drop support for Python 3.9. (`#3345 `__) + + +Miscellaneous internal changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Decrease indentation for exception groups raised in `trio.as_safe_channel`. (`#3332 `__) + + Trio 0.31.0 (2025-09-09) ------------------------ diff --git a/newsfragments/3321.feature.rst b/newsfragments/3321.feature.rst deleted file mode 100644 index e62649bbd2..0000000000 --- a/newsfragments/3321.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Allow `trio.CapacityLimiter` to have zero total_tokens. diff --git a/newsfragments/3331.bugfix.rst b/newsfragments/3331.bugfix.rst deleted file mode 100644 index da30bc9d5c..0000000000 --- a/newsfragments/3331.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fixed a bug where iterating over an ``@as_safe_channel``-derived ``ReceiveChannel`` -would raise `~trio.BrokenResourceError` if the channel was closed by another task. -It now shuts down cleanly. diff --git a/newsfragments/3332.misc.rst b/newsfragments/3332.misc.rst deleted file mode 100644 index f4b3a9b3a3..0000000000 --- a/newsfragments/3332.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Decrease indentation for exception groups raised in `trio.as_safe_channel`. diff --git a/newsfragments/3337.bugfix.rst b/newsfragments/3337.bugfix.rst deleted file mode 100644 index fe2124e976..0000000000 --- a/newsfragments/3337.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -`trio.lowlevel.Task.iter_await_frames` now works on completed tasks, by -returning an empty list of frames if the underlying coroutine has been closed. -Previously, it raised an internal error. diff --git a/newsfragments/3345.removal.rst b/newsfragments/3345.removal.rst deleted file mode 100644 index 8279a1c7f9..0000000000 --- a/newsfragments/3345.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Drop support for Python 3.9. diff --git a/src/trio/_version.py b/src/trio/_version.py index a089e44d49..9f941c270b 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.31.0+dev" +__version__ = "0.32.0" From 57ac04ed8c0bfb1a16b9ba4a46374c465e579a2a Mon Sep 17 00:00:00 2001 From: A5rocks Date: Fri, 31 Oct 2025 16:18:13 +0900 Subject: [PATCH 104/111] Start new cycle --- src/trio/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/trio/_version.py b/src/trio/_version.py index 9f941c270b..659188b5e0 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.32.0+dev" From 5926cd8e302ed5fdbf905c5b9eb0062c5e1082cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 1 Nov 2025 01:15:58 +0000 Subject: [PATCH 105/111] Dependency updates (#3350) --- .pre-commit-config.yaml | 8 ++++---- test-requirements.txt | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 46cad85615..d7a89bb87a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.2 + rev: v0.14.3 hooks: - id: ruff-check types: [file] @@ -42,11 +42,11 @@ repos: hooks: - id: typos - repo: https://github.com/sphinx-contrib/sphinx-lint - rev: v1.0.0 + rev: v1.0.1 hooks: - id: sphinx-lint - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.16.0 + rev: v1.16.1 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.9.7 hooks: # Compile requirements - id: pip-compile diff --git a/test-requirements.txt b/test-requirements.txt index 9717b1646c..952a219608 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -120,7 +120,7 @@ pylint==4.0.2 # via -r test-requirements.in pyopenssl==25.3.0 # via -r test-requirements.in -pyright==1.1.406 +pyright==1.1.407 # via -r test-requirements.in pytest==8.4.2 # via -r test-requirements.in @@ -132,7 +132,7 @@ requests==2.32.5 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.14.2 +ruff==0.14.3 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -192,7 +192,7 @@ typing-extensions==4.15.0 # virtualenv urllib3==2.5.0 # via requests -uv==0.9.5 +uv==0.9.7 # via -r test-requirements.in -virtualenv==20.35.3 +virtualenv==20.35.4 # via pre-commit From 300adeec95e2cd92d2665d74456e360d12e32a81 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 18:11:39 -0500 Subject: [PATCH 106/111] [pre-commit.ci] pre-commit autoupdate (#3351) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d7a89bb87a..d3f4e44306 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ 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.39.0 hooks: - id: typos - repo: https://github.com/sphinx-contrib/sphinx-lint @@ -46,7 +46,7 @@ repos: hooks: - id: sphinx-lint - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.16.1 + rev: v1.16.2 hooks: - id: zizmor - repo: local From 107048dc592434c608bf77dd40eecc9aeac24b7f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 03:51:30 +0000 Subject: [PATCH 107/111] Dependency updates (#3354) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 8 ++++---- test-requirements.txt | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d3f4e44306..b1fa725056 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: 25.11.0 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.3 + rev: v0.14.4 hooks: - id: ruff-check types: [file] @@ -46,7 +46,7 @@ repos: hooks: - id: sphinx-lint - repo: https://github.com/woodruffw/zizmor-pre-commit - rev: v1.16.2 + rev: v1.16.3 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.7 + rev: 0.9.8 hooks: # Compile requirements - id: pip-compile diff --git a/test-requirements.txt b/test-requirements.txt index 952a219608..9c00ee385c 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.2 # via pylint async-generator==1.10 # via -r test-requirements.in @@ -14,7 +14,7 @@ attrs==25.4.0 # outcome babel==2.17.0 # via sphinx -black==25.9.0 ; implementation_name == 'cpython' +black==25.11.0 ; implementation_name == 'cpython' # via -r test-requirements.in certifi==2025.10.5 # via requests @@ -36,7 +36,7 @@ colorama==0.4.6 ; sys_platform == 'win32' # pylint # pytest # sphinx -coverage==7.11.0 +coverage==7.11.3 # via -r test-requirements.in cryptography==46.0.3 # via @@ -108,7 +108,7 @@ platformdirs==4.5.0 # virtualenv pluggy==1.6.0 # via pytest -pre-commit==4.3.0 +pre-commit==4.4.0 # via -r test-requirements.in pycparser==2.23 ; (implementation_name != 'PyPy' and os_name == 'nt') or (implementation_name != 'PyPy' and platform_python_implementation != 'PyPy') # via cffi @@ -122,9 +122,9 @@ pyopenssl==25.3.0 # via -r test-requirements.in pyright==1.1.407 # via -r test-requirements.in -pytest==8.4.2 +pytest==9.0.0 # via -r test-requirements.in -pytokens==0.2.0 ; implementation_name == 'cpython' +pytokens==0.3.0 ; implementation_name == 'cpython' # via black pyyaml==6.0.3 # via pre-commit @@ -132,7 +132,7 @@ requests==2.32.5 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.14.3 +ruff==0.14.4 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -192,7 +192,7 @@ typing-extensions==4.15.0 # virtualenv urllib3==2.5.0 # via requests -uv==0.9.7 +uv==0.9.8 # via -r test-requirements.in virtualenv==20.35.4 # via pre-commit From 63ce96808bca270c8e58601c21c4d194d6249b56 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:56:00 -0500 Subject: [PATCH 108/111] [pre-commit.ci] pre-commit autoupdate (#3356) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.4 → v0.14.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.4...v0.14.5) - [github.com/adhtruong/mirrors-typos: v1.39.0 → v1.39.2](https://github.com/adhtruong/mirrors-typos/compare/v1.39.0...v1.39.2) - [github.com/astral-sh/uv-pre-commit: 0.9.8 → 0.9.10](https://github.com/astral-sh/uv-pre-commit/compare/0.9.8...0.9.10) * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +++--- test-requirements.txt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b1fa725056..c042990630 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.4 + rev: v0.14.5 hooks: - id: ruff-check types: [file] @@ -38,7 +38,7 @@ repos: # tomli needed on 3.10. tomllib is available in stdlib on 3.11+ - tomli - repo: https://github.com/adhtruong/mirrors-typos - rev: v1.39.0 + rev: v1.39.2 hooks: - id: typos - repo: https://github.com/sphinx-contrib/sphinx-lint @@ -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.8 + rev: 0.9.10 hooks: # Compile requirements - id: pip-compile diff --git a/test-requirements.txt b/test-requirements.txt index 9c00ee385c..8a2bbe2015 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -132,7 +132,7 @@ requests==2.32.5 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.14.4 +ruff==0.14.5 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -192,7 +192,7 @@ typing-extensions==4.15.0 # virtualenv urllib3==2.5.0 # via requests -uv==0.9.8 +uv==0.9.10 # via -r test-requirements.in virtualenv==20.35.4 # via pre-commit From c4017e4cd590acee1d835257ce5cab8cae6723ee Mon Sep 17 00:00:00 2001 From: Ripan Roy <88824889+Ripan-Roy@users.noreply.github.com> Date: Mon, 24 Nov 2025 16:23:29 +0530 Subject: [PATCH 109/111] Add smoke tests to CI workflow for verifying build artifacts (#3352) --- .github/workflows/ci.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9daca186a9..1e84e47c07 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: >- @@ -456,6 +469,7 @@ jobs: if: always() needs: + - smoke-tests - Windows - Ubuntu - macOS From fe49a53454161e1e1bfaf15de9b324f58bd8d5ac Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:55:51 +0000 Subject: [PATCH 110/111] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.14.5 → v0.14.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.14.5...v0.14.6) - [github.com/sphinx-contrib/sphinx-lint: v1.0.1 → v1.0.2](https://github.com/sphinx-contrib/sphinx-lint/compare/v1.0.1...v1.0.2) - [github.com/astral-sh/uv-pre-commit: 0.9.10 → 0.9.11](https://github.com/astral-sh/uv-pre-commit/compare/0.9.10...0.9.11) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c042990630..6887e0cca4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,7 +24,7 @@ repos: hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.5 + rev: v0.14.6 hooks: - id: ruff-check types: [file] @@ -42,7 +42,7 @@ repos: hooks: - id: typos - repo: https://github.com/sphinx-contrib/sphinx-lint - rev: v1.0.1 + rev: v1.0.2 hooks: - id: sphinx-lint - repo: https://github.com/woodruffw/zizmor-pre-commit @@ -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.10 + rev: 0.9.11 hooks: # Compile requirements - id: pip-compile From 5c07f6f7e7b40308e54dbdc6c03ce5bb1f211226 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:56:19 +0000 Subject: [PATCH 111/111] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- test-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 8a2bbe2015..4c56a29c71 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -132,7 +132,7 @@ requests==2.32.5 # via sphinx roman-numerals-py==3.1.0 ; python_full_version >= '3.11' # via sphinx -ruff==0.14.5 +ruff==0.14.6 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -192,7 +192,7 @@ typing-extensions==4.15.0 # virtualenv urllib3==2.5.0 # via requests -uv==0.9.10 +uv==0.9.11 # via -r test-requirements.in virtualenv==20.35.4 # via pre-commit