diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 1d3079ad5a..001d83c4a1 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,4 @@ # sorting all imports with isort 933f77b96f0092e1baab4474a9208fc2e379aa32 +# enabling ruff's flake8-commas rule +b25c02a94e2defcb0fad32976b02218be1133bdf diff --git a/.github/workflows/autodeps.yml b/.github/workflows/autodeps.yml index 0e0655c5aa..0e28de2687 100644 --- a/.github/workflows/autodeps.yml +++ b/.github/workflows/autodeps.yml @@ -17,6 +17,7 @@ jobs: issues: write repository-projects: write contents: write + steps: - name: Checkout uses: actions/checkout@v4 @@ -24,19 +25,27 @@ jobs: uses: actions/setup-python@v5 with: python-version: "3.8" + - 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.8 --upgrade test-requirements.in -o test-requirements.txt - uv pip compile --universal --python-version=3.8 --upgrade docs-requirements.in -o docs-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: | - # The new dependencies may contain a new black version. - # Commit any changes immediately. - python -m pip install -r test-requirements.txt - black src/trio + uv pip compile --universal --python-version=3.8 test-requirements.in -o test-requirements.txt + uv pip compile --universal --python-version=3.11 docs-requirements.in -o docs-requirements.txt + - name: Commit changes and create automerge PR env: GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/check-newsfragment.yml b/.github/workflows/check-newsfragment.yml new file mode 100644 index 0000000000..0aa78fcd3c --- /dev/null +++ b/.github/workflows/check-newsfragment.yml @@ -0,0 +1,23 @@ +name: Check newsfragment + +on: + pull_request: + types: [labeled, unlabeled, opened, synchronize] + branches: + - main + +jobs: + check-newsfragment: + if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip newsfragment') }} + runs-on: 'ubuntu-latest' + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check newsfragments + run: | + if git diff --name-only origin/main | grep -v '/_tests/' | grep 'src/trio/'; then + git diff --name-only origin/main | grep 'newsfragments/' || exit 1 + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f457740bdf..074943ec63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python: ['pypy-3.10', '3.8', '3.9', '3.10', '3.11', '3.12'] arch: ['x86', 'x64'] lsp: [''] lsp_extract_file: [''] @@ -87,7 +87,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['pypy-3.9', 'pypy-3.10', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + python: ['pypy-3.10', '3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] check_formatting: ['0'] no_test_requirements: ['0'] extra_name: [''] @@ -143,7 +143,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python: ['pypy-3.10', '3.8', '3.9', '3.10', '3.11', '3.12'] continue-on-error: >- ${{ ( diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d195f130a5..209d5e26a2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,10 +4,11 @@ ci: autoupdate_commit_msg: "[pre-commit.ci] pre-commit autoupdate" autoupdate_schedule: weekly submodules: false + skip: [regenerate-files] repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -19,11 +20,11 @@ repos: - id: sort-simple-yaml files: .pre-commit-config.yaml - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.4.2 + rev: 24.10.0 hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.6 + rev: v0.6.9 hooks: - id: ruff types: [file] @@ -33,3 +34,15 @@ repos: rev: v2.3.0 hooks: - id: codespell + - repo: https://github.com/sphinx-contrib/sphinx-lint + rev: v1.0.0 + hooks: + - id: sphinx-lint + - repo: local + hooks: + - id: regenerate-files + name: regenerate generated files + language: system + entry: python src/trio/_tools/gen_exports.py + pass_filenames: false + files: ^src\/trio\/_core\/(_run|(_i(o_(common|epoll|kqueue|windows)|nstrumentation)))\.py$ diff --git a/check.sh b/check.sh index d6efb8749a..a4fb2e4e2a 100755 --- a/check.sh +++ b/check.sh @@ -81,7 +81,7 @@ echo "::group::Pip Compile - Tests" uv pip compile --universal --python-version=3.8 test-requirements.in -o test-requirements.txt echo "::endgroup::" echo "::group::Pip Compile - Docs" -uv pip compile --universal --python-version=3.8 docs-requirements.in -o docs-requirements.txt +uv pip compile --universal --python-version=3.11 docs-requirements.in -o docs-requirements.txt echo "::endgroup::" if git status --porcelain | grep -q "requirements.txt"; then @@ -112,7 +112,7 @@ if [ $EXIT_STATUS -ne 0 ]; then Problems were found by static analysis (listed above). To fix formatting and see remaining errors, run - pip install -r test-requirements.txt + uv pip install -r test-requirements.txt black src/trio ruff check src/trio ./check.sh diff --git a/ci.sh b/ci.sh index e62946a4de..ef3dee55ca 100755 --- a/ci.sh +++ b/ci.sh @@ -37,25 +37,29 @@ python -c "import sys, struct, ssl; print('python:', sys.version); print('versio echo "::endgroup::" echo "::group::Install dependencies" -python -m pip install -U pip build +python -m pip install -U pip uv -c test-requirements.txt python -m pip --version +python -m uv --version + +python -m uv pip install build python -m build -python -m pip install dist/*.whl +wheel_package=$(ls dist/*.whl) +python -m uv pip install "trio @ $wheel_package" -c test-requirements.txt if [ "$CHECK_FORMATTING" = "1" ]; then - python -m pip install -r test-requirements.txt exceptiongroup + python -m uv pip install -r test-requirements.txt exceptiongroup echo "::endgroup::" source check.sh else # Actual tests # expands to 0 != 1 if NO_TEST_REQUIREMENTS is not set, if set the `-0` has no effect # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_06_02 - if [ ${NO_TEST_REQUIREMENTS-0} == 1 ]; then - python -m pip install pytest coverage + if [ "${NO_TEST_REQUIREMENTS-0}" == 1 ]; then + python -m uv pip install pytest coverage -c test-requirements.txt flags="--skip-optional-imports" else - python -m pip install -r test-requirements.txt + python -m uv pip install -r test-requirements.txt flags="" fi @@ -118,7 +122,7 @@ else cd empty INSTALLDIR=$(python -c "import os, trio; print(os.path.dirname(trio.__file__))") - cp ../pyproject.toml $INSTALLDIR + cp ../pyproject.toml "$INSTALLDIR" # get mypy tests a nice cache MYPYPATH=".." mypy --config-file= --cache-dir=./.mypy_cache -c "import trio" >/dev/null 2>/dev/null || true @@ -128,7 +132,7 @@ else echo "::endgroup::" echo "::group:: Run Tests" - if COVERAGE_PROCESS_START=$(pwd)/../pyproject.toml coverage run --rcfile=../pyproject.toml -m pytest -ra --junitxml=../test-results.xml --run-slow ${INSTALLDIR} --verbose --durations=10 $flags; then + if COVERAGE_PROCESS_START=$(pwd)/../pyproject.toml coverage run --rcfile=../pyproject.toml -m pytest -ra --junitxml=../test-results.xml --run-slow "${INSTALLDIR}" --verbose --durations=10 $flags; then PASSED=true else PASSED=false diff --git a/docs-requirements.in b/docs-requirements.in index c4695fc688..ce7c139b85 100644 --- a/docs-requirements.in +++ b/docs-requirements.in @@ -2,7 +2,8 @@ # sphinx 5.3 doesn't work with our _NoValue workaround sphinx >= 6.0 jinja2 -sphinx_rtd_theme +# >= is necessary to prevent `uv` from selecting a `Sphinx` version this does not support +sphinx_rtd_theme >= 2 sphinxcontrib-jquery sphinxcontrib-trio towncrier diff --git a/docs-requirements.txt b/docs-requirements.txt index 461d6e3d93..fa9163713a 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -1,18 +1,18 @@ # This file was autogenerated by uv via the following command: -# uv pip compile --universal --python-version=3.8 docs-requirements.in -o docs-requirements.txt -alabaster==0.7.13 +# uv pip compile --universal --python-version=3.11 docs-requirements.in -o docs-requirements.txt +alabaster==0.7.16 # via sphinx -attrs==23.2.0 +attrs==24.2.0 # via # -r docs-requirements.in # outcome -babel==2.15.0 +babel==2.16.0 # via sphinx beautifulsoup4==4.12.3 # via sphinx-codeautolink -certifi==2024.7.4 +certifi==2024.8.30 # via requests -cffi==1.16.0 ; os_name == 'nt' or platform_python_implementation != 'PyPy' +cffi==1.17.1 ; platform_python_implementation != 'PyPy' or os_name == 'nt' # via # -r docs-requirements.in # cryptography @@ -20,19 +20,19 @@ charset-normalizer==3.3.2 # via requests click==8.1.7 # via towncrier -colorama==0.4.6 ; platform_system == 'Windows' or sys_platform == 'win32' +colorama==0.4.6 ; sys_platform == 'win32' or platform_system == 'Windows' # via # click # sphinx -cryptography==42.0.8 +cryptography==43.0.1 # via pyopenssl docutils==0.20.1 # via # sphinx # sphinx-rtd-theme -exceptiongroup==1.2.1 +exceptiongroup==1.2.2 # via -r docs-requirements.in -idna==3.7 +idna==3.10 # via # -r docs-requirements.in # requests @@ -40,12 +40,6 @@ imagesize==1.4.1 # via sphinx immutables==0.20 # via -r docs-requirements.in -importlib-metadata==8.0.0 ; python_version < '3.10' - # via sphinx -importlib-resources==6.4.0 ; python_version < '3.10' - # via towncrier -incremental==22.10.0 - # via towncrier jinja2==3.1.4 # via # -r docs-requirements.in @@ -57,14 +51,12 @@ outcome==1.3.0.post0 # via -r docs-requirements.in packaging==24.1 # via sphinx -pycparser==2.22 ; os_name == 'nt' or platform_python_implementation != 'PyPy' +pycparser==2.22 ; platform_python_implementation != 'PyPy' or os_name == 'nt' # via cffi pygments==2.18.0 # via sphinx -pyopenssl==24.1.0 +pyopenssl==24.2.1 # via -r docs-requirements.in -pytz==2024.1 ; python_version < '3.9' - # via babel requests==2.32.3 # via sphinx sniffio==1.3.1 @@ -73,9 +65,9 @@ snowballstemmer==2.2.0 # via sphinx sortedcontainers==2.4.0 # via -r docs-requirements.in -soupsieve==2.5 +soupsieve==2.6 # via beautifulsoup4 -sphinx==7.1.2 +sphinx==7.4.7 # via # -r docs-requirements.in # sphinx-codeautolink @@ -85,15 +77,15 @@ sphinx==7.1.2 # sphinxcontrib-trio sphinx-codeautolink==0.15.2 # via -r docs-requirements.in -sphinx-hoverxref==1.4.0 +sphinx-hoverxref==1.4.1 # via -r docs-requirements.in sphinx-rtd-theme==2.0.0 # via -r docs-requirements.in -sphinxcontrib-applehelp==1.0.4 +sphinxcontrib-applehelp==2.0.0 # via sphinx -sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-devhelp==2.0.0 # via sphinx -sphinxcontrib-htmlhelp==2.0.1 +sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jquery==4.1 # via @@ -102,19 +94,13 @@ sphinxcontrib-jquery==4.1 # sphinx-rtd-theme sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-qthelp==2.0.0 # via sphinx -sphinxcontrib-serializinghtml==1.1.5 +sphinxcontrib-serializinghtml==2.0.0 # via sphinx sphinxcontrib-trio==1.1.2 # via -r docs-requirements.in -tomli==2.0.1 ; python_version < '3.11' - # via towncrier -towncrier==23.11.0 +towncrier==24.8.0 # via -r docs-requirements.in -urllib3==2.2.2 +urllib3==2.2.3 # via requests -zipp==3.19.2 ; python_version < '3.10' - # via - # importlib-metadata - # importlib-resources diff --git a/docs/source/awesome-trio-libraries.rst b/docs/source/awesome-trio-libraries.rst index 823bf0779a..c5c415583a 100644 --- a/docs/source/awesome-trio-libraries.rst +++ b/docs/source/awesome-trio-libraries.rst @@ -29,8 +29,8 @@ Web and HTML ------------ * `httpx `__ - HTTPX is a fully featured HTTP client for Python 3, which provides sync and async APIs, and support for both HTTP/1.1 and HTTP/2. * `trio-websocket `__ - A WebSocket client and server implementation striving for safety, correctness, and ergonomics. -* `quart-trio `__ - Like Flask, but for Trio. A simple and powerful framework for building async web applications and REST APIs. Tip: this is an ASGI-based framework, so you'll also need an HTTP server with ASGI support. -* `hypercorn `__ - An HTTP server for hosting your ASGI apps. Supports HTTP/1.1, HTTP/2, HTTP/3, and Websockets. Can be run as a standalone server, or embedded in a larger Trio app. Use it with ``quart-trio``, or any other Trio-compatible ASGI framework. +* `quart-trio `__ - Like Flask, but for Trio. A simple and powerful framework for building async web applications and REST APIs. Tip: this is an ASGI-based framework, so you'll also need an HTTP server with ASGI support. +* `hypercorn `__ - An HTTP server for hosting your ASGI apps. Supports HTTP/1.1, HTTP/2, HTTP/3, and Websockets. Can be run as a standalone server, or embedded in a larger Trio app. Use it with ``quart-trio``, or any other Trio-compatible ASGI framework. * `DeFramed `__ - DeFramed is a Web non-framework that supports a 99%-server-centric approach to Web coding, including support for the `Remi `__ GUI library. * `pura `__ - A simple web framework for embedding realtime graphical visualization into Trio apps, enabling inspection and manipulation of program state during development. * `pyscalpel `__ - A fast and powerful webscraping library. diff --git a/docs/source/conf.py b/docs/source/conf.py index 7ea27de24b..2e84d91532 100755 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,9 +19,11 @@ from __future__ import annotations import collections.abc +import glob import os import sys import types +from pathlib import Path from typing import TYPE_CHECKING, cast if TYPE_CHECKING: @@ -36,19 +38,62 @@ # Enable reloading with `typing.TYPE_CHECKING` being True os.environ["SPHINX_AUTODOC_RELOAD_MODULES"] = "1" -# https://docs.readthedocs.io/en/stable/builds.html#build-environment -if "READTHEDOCS" in os.environ: - import glob - - if glob.glob("../../newsfragments/*.*.rst"): - print("-- Found newsfragments; running towncrier --", flush=True) - import subprocess - +# Handle writing newsfragments into the history file. +# We want to keep files unchanged when testing locally. +# So immediately revert the contents after running towncrier, +# then substitute when Sphinx wants to read it in. +history_file = Path("history.rst") + +history_new: str | None +if glob.glob("../../newsfragments/*.*.rst"): + print("-- Found newsfragments; running towncrier --", flush=True) + history_orig = history_file.read_bytes() + import subprocess + + # In case changes were staged, preserve indexed version. + # This grabs the hash of the current staged version. + history_staged = subprocess.run( + ["git", "rev-parse", "--verify", ":docs/source/history.rst"], + check=True, + cwd="../..", + stdout=subprocess.PIPE, + encoding="ascii", + ).stdout.strip() + try: subprocess.run( - ["towncrier", "--yes", "--date", "not released yet"], + ["towncrier", "--keep", "--date", "not released yet"], cwd="../..", check=True, ) + history_new = history_file.read_text("utf8") + finally: + # Make sure this reverts even if a failure occurred. + # Restore whatever was staged. + print(f"Restoring history.rst = {history_staged}") + subprocess.run( + [ + "git", + "update-index", + "--cacheinfo", + f"100644,{history_staged},docs/source/history.rst", + ], + cwd="../..", + check=False, + ) + # And restore the working copy. + history_file.write_bytes(history_orig) + del history_orig # We don't need this any more. +else: + # Leave it as is. + history_new = None + + +def on_read_source(app: Sphinx, docname: str, content: list[str]) -> None: + """Substitute the modified history file.""" + if docname == "history" and history_new is not None: + # This is a 1-item list with the file contents. + content[0] = history_new + # Sphinx is very finicky, and somewhat buggy, so we have several different # methods to help it resolve links. @@ -132,7 +177,8 @@ def autodoc_process_signature( # and insert the fully-qualified name. signature = signature.replace("+E", "~trio.testing._raises_group.E") signature = signature.replace( - "+MatchE", "~trio.testing._raises_group.MatchE" + "+MatchE", + "~trio.testing._raises_group.MatchE", ) if "DTLS" in name: signature = signature.replace("SSL.Context", "OpenSSL.SSL.Context") @@ -152,6 +198,7 @@ def setup(app: Sphinx) -> None: app.connect("autodoc-process-signature", autodoc_process_signature) # After Intersphinx runs, add additional mappings. app.connect("builder-inited", add_intersphinx, priority=1000) + app.connect("source-read", on_read_source) # -- General configuration ------------------------------------------------ @@ -182,6 +229,7 @@ def setup(app: Sphinx) -> None: "pyopenssl": ("https://www.pyopenssl.org/en/stable/", None), "sniffio": ("https://sniffio.readthedocs.io/en/latest/", None), "trio-util": ("https://trio-util.readthedocs.io/en/latest/", None), + "flake8-async": ("https://flake8-async.readthedocs.io/en/latest/", None), } # See https://sphinx-hoverxref.readthedocs.io/en/latest/configuration.html @@ -243,12 +291,14 @@ def add_mapping( # This has been removed in Py3.12, so add a link to the 3.11 version with deprecation warnings. add_mapping("method", "pathlib", "Path.link_to", "3.11") + # defined in py:data in objects.inv, but sphinx looks for a py:class + # see https://github.com/sphinx-doc/sphinx/issues/10974 + # to dump the objects.inv for the stdlib, you can run + # python -m sphinx.ext.intersphinx http://docs.python.org/3/objects.inv add_mapping("class", "math", "inf") - # `types.FrameType.__module__` is "builtins", so sphinx looks for - # builtins.FrameType. - # See https://github.com/sphinx-doc/sphinx/issues/11802 add_mapping("class", "types", "FrameType") + # new in py3.12, and need target because sphinx is unable to look up # the module of the object if compiling on <3.12 if not hasattr(collections.abc, "Buffer"): diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst index f37f57d5dd..cace2943d2 100644 --- a/docs/source/contributing.rst +++ b/docs/source/contributing.rst @@ -176,6 +176,24 @@ This keeps us closer to the desired state where each open issue reflects some work that still needs to be done. +Environment +~~~~~~~~~~~ +We strongly suggest using a virtual environment for managing dependencies, +for example with `venv `__. So to +set up your environment and install dependencies, you should run something like: + +.. code-block:: shell + + cd path/to/trio/checkout/ + python -m venv .venv # create virtual env in .venv + source .venv/bin/activate # activate it + pip install -e . # install trio, needed for pytest plugin + pip install -r test-requirements.txt # install test requirements + +you rarely need to recreate the virtual environment, but you need to re-activate it +in future terminals. You might also need to re-install from test-requirements.txt if +the versions in it get updated. + .. _pull-request-tests: Tests @@ -186,12 +204,11 @@ locally, you should run: .. code-block:: shell - cd path/to/trio/checkout/ - pip install -r test-requirements.txt # possibly using a virtualenv - pytest trio + source .venv/bin/activate # if not already activated + pytest src This doesn't try to be completely exhaustive – it only checks that -things work on your machine, and it may skip some slow tests. But it's +things work on your machine, and it will skip some slow tests. But it's a good way to quickly check that things seem to be working, and we'll automatically run the full test suite when your PR is submitted, so you'll have a chance to see and fix any remaining issues then. @@ -211,8 +228,14 @@ it being merely hard to fix). For example: We use Codecov to track coverage, because it makes it easy to combine coverage from running in different configurations. Running coverage locally can be useful -(``pytest --cov=PACKAGENAME --cov-report=html``), but don't be -surprised if you get lower coverage than when looking at Codecov + +.. code-block:: shell + + coverage run -m pytest + coverage combine + coverage report + +but don't be surprised if you get lower coverage than when looking at Codecov reports, because there are some lines that are only executed on Windows, or macOS, or PyPy, or CPython, or... you get the idea. After you create a PR, Codecov will automatically report back with the diff --git a/docs/source/history.rst b/docs/source/history.rst index 151c02af6d..34e2fe9772 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -5,6 +5,39 @@ Release history .. towncrier release notes start +Trio 0.27.0 (2024-10-17) +------------------------ + +Breaking changes +~~~~~~~~~~~~~~~~ + +- :func:`trio.move_on_after` and :func:`trio.fail_after` previously set the deadline relative to initialization time, instead of more intuitively upon entering the context manager. This might change timeouts if a program relied on this behavior. If you want to restore previous behavior you should instead use ``trio.move_on_at(trio.current_time() + ...)``. + flake8-async has a new rule to catch this, in case you're supporting older trio versions. See :ref:`ASYNC122`. (`#2512 `__) + + +Features +~~~~~~~~ + +- :meth:`CancelScope.relative_deadline` and :meth:`CancelScope.is_relative` added, as well as a ``relative_deadline`` parameter to ``__init__``. This allows initializing scopes ahead of time, but where the specified relative deadline doesn't count down until the scope is entered. (`#2512 `__) +- :class:`trio.Lock` and :class:`trio.StrictFIFOLock` will now raise :exc:`trio.BrokenResourceError` when :meth:`trio.Lock.acquire` would previously stall due to the owner of the lock exiting without releasing the lock. (`#3035 `__) +- `trio.move_on_at`, `trio.move_on_after`, `trio.fail_at` and `trio.fail_after` now accept *shield* as a keyword argument. If specified, it provides an initial value for the `~trio.CancelScope.shield` attribute of the `trio.CancelScope` object created by the context manager. (`#3052 `__) +- Added :func:`trio.lowlevel.add_parking_lot_breaker` and :func:`trio.lowlevel.remove_parking_lot_breaker` to allow creating custom lock/semaphore implementations that will break their underlying parking lot if a task exits unexpectedly. :meth:`trio.lowlevel.ParkingLot.break_lot` is also added, to allow breaking a parking lot intentionally. (`#3081 `__) + + +Bugfixes +~~~~~~~~ + +- Allow sockets to bind any ``os.PathLike`` object. (`#3041 `__) +- Update ``trio.lowlevel.open_process``'s documentation to allow bytes. (`#3076 `__) +- Update :func:`trio.sleep_forever` to be `NoReturn`. (`#3095 `__) + + +Improved documentation +~~~~~~~~~~~~~~~~~~~~~~ + +- Add docstrings for memory channels' ``statistics()`` and ``aclose`` methods. (`#3101 `__) + + Trio 0.26.2 (2024-08-08) ------------------------ @@ -141,7 +174,7 @@ Trio 0.23.2 (2023-12-14) Features ~~~~~~~~ -- `TypeVarTuple `_ is now used to fully type :meth:`nursery.start_soon() `, :func:`trio.run()`, :func:`trio.to_thread.run_sync()`, and other similar functions accepting ``(func, *args)``. This means type checkers will be able to verify types are used correctly. :meth:`nursery.start() ` is not fully typed yet however. (`#2881 `__) +- `TypeVarTuple `_ is now used to fully type :meth:`nursery.start_soon() `, :func:`trio.run`, :func:`trio.to_thread.run_sync`, and other similar functions accepting ``(func, *args)``. This means type checkers will be able to verify types are used correctly. :meth:`nursery.start() ` is not fully typed yet however. (`#2881 `__) Bugfixes @@ -1000,7 +1033,7 @@ Bugfixes - Fixed a race condition on macOS, where Trio's TCP listener would crash if an incoming TCP connection was closed before the listener had a chance to accept it. (`#609 `__) -- :func:`trio.open_tcp_stream()` has been refactored to clean up unsuccessful +- :func:`trio.open_tcp_stream` has been refactored to clean up unsuccessful connection attempts more reliably. (`#809 `__) diff --git a/docs/source/reference-core.rst b/docs/source/reference-core.rst index 6808f930c6..a9bb3909d9 100644 --- a/docs/source/reference-core.rst +++ b/docs/source/reference-core.rst @@ -449,8 +449,7 @@ attribute to :data:`True`: try: await conn.send_hello_msg() finally: - with trio.move_on_after(CLEANUP_TIMEOUT) as cleanup_scope: - cleanup_scope.shield = True + with trio.move_on_after(CLEANUP_TIMEOUT, shield=True) as cleanup_scope: await conn.send_goodbye_msg() So long as you're inside a scope with ``shield = True`` set, then @@ -528,8 +527,12 @@ objects. .. autoattribute:: deadline + .. autoattribute:: relative_deadline + .. autoattribute:: shield + .. automethod:: is_relative() + .. automethod:: cancel() .. attribute:: cancelled_caught @@ -562,7 +565,8 @@ situation of just wanting to impose a timeout on some code: .. autofunction:: fail_at :with: cancel_scope -Cheat sheet: +Cheat sheet ++++++++++++ * If you want to impose a timeout on a function, but you don't care whether it timed out or not: @@ -598,7 +602,6 @@ which is sometimes useful: .. autofunction:: current_effective_deadline - .. _tasks: Tasks let you do multiple things at once @@ -1238,6 +1241,8 @@ more features beyond the core channel interface: .. autoclass:: MemoryReceiveChannel :members: +.. autoclass:: MemoryChannelStatistics + :members: A simple channel example ++++++++++++++++++++++++ diff --git a/docs/source/reference-lowlevel.rst b/docs/source/reference-lowlevel.rst index 70133b9839..10c1ddfdc0 100644 --- a/docs/source/reference-lowlevel.rst +++ b/docs/source/reference-lowlevel.rst @@ -393,6 +393,10 @@ Wait queue abstraction .. autoclass:: ParkingLotStatistics :members: +.. autofunction:: add_parking_lot_breaker + +.. autofunction:: remove_parking_lot_breaker + Low-level checkpoint functions ------------------------------ diff --git a/notes-to-self/afd-lab.py b/notes-to-self/afd-lab.py index 600975482c..a95eb8bfd8 100644 --- a/notes-to-self/afd-lab.py +++ b/notes-to-self/afd-lab.py @@ -125,7 +125,7 @@ async def afd_poll(self, sock, flags, *, exclusive=0): ffi.sizeof("AFD_POLL_INFO"), ffi.NULL, lpOverlapped, - ) + ), ) except OSError as exc: if exc.winerror != ErrorCodes.ERROR_IO_PENDING: # pragma: no cover diff --git a/notes-to-self/blocking-read-hack.py b/notes-to-self/blocking-read-hack.py index 56bcd03df9..688f103057 100644 --- a/notes-to-self/blocking-read-hack.py +++ b/notes-to-self/blocking-read-hack.py @@ -12,7 +12,9 @@ class BlockingReadTimeoutError(Exception): async def blocking_read_with_timeout( - fd, count, timeout # noqa: ASYNC109 # manual timeout + fd, + count, + timeout, # noqa: ASYNC109 # manual timeout ): print("reading from fd", fd) cancel_requested = False diff --git a/notes-to-self/file-read-latency.py b/notes-to-self/file-read-latency.py index e82b027a03..c01cd41bf2 100644 --- a/notes-to-self/file-read-latency.py +++ b/notes-to-self/file-read-latency.py @@ -23,5 +23,5 @@ seek = (end - between) / COUNT * 1e9 read = both - seek print( - f"{both:.2f} ns/(seek+read), {seek:.2f} ns/seek, estimate ~{read:.2f} ns/read" + f"{both:.2f} ns/(seek+read), {seek:.2f} ns/seek, estimate ~{read:.2f} ns/read", ) diff --git a/notes-to-self/how-does-windows-so-reuseaddr-work.py b/notes-to-self/how-does-windows-so-reuseaddr-work.py index 70dd75e39f..f87e957adf 100644 --- a/notes-to-self/how-does-windows-so-reuseaddr-work.py +++ b/notes-to-self/how-does-windows-so-reuseaddr-work.py @@ -49,7 +49,7 @@ def table_entry(mode1, bind_type1, mode2, bind_type2): """ second bind | """ - + " | ".join(["%-19s" % mode for mode in modes]) + + " | ".join(["%-19s" % mode for mode in modes]), ) print(""" """, end="") @@ -58,7 +58,7 @@ def table_entry(mode1, bind_type1, mode2, bind_type2): print( """ -first bind -----------------------------------------------------------------""" +first bind -----------------------------------------------------------------""", # default | wildcard | INUSE | Success | ACCESS | Success | INUSE | Success ) @@ -72,5 +72,5 @@ def table_entry(mode1, bind_type1, mode2, bind_type2): # print(mode1, bind_type1, mode2, bind_type2, entry) print( f"{mode1:>19} | {bind_type1:>8} | " - + " | ".join(["%8s" % entry for entry in row]) + + " | ".join(["%8s" % entry for entry in row]), ) diff --git a/notes-to-self/manual-signal-handler.py b/notes-to-self/manual-signal-handler.py index d865fa89ee..ead7f84d7d 100644 --- a/notes-to-self/manual-signal-handler.py +++ b/notes-to-self/manual-signal-handler.py @@ -11,11 +11,12 @@ """ void* WINAPI GetProcAddress(void* hModule, char* lpProcName); typedef void (*PyOS_sighandler_t)(int); - """ + """, ) kernel32 = ffi.dlopen("kernel32.dll") PyOS_getsig_ptr = kernel32.GetProcAddress( - ffi.cast("void*", sys.dllhandle), b"PyOS_getsig" + ffi.cast("void*", sys.dllhandle), + b"PyOS_getsig", ) PyOS_getsig = ffi.cast("PyOS_sighandler_t (*)(int)", PyOS_getsig_ptr) diff --git a/notes-to-self/proxy-benchmarks.py b/notes-to-self/proxy-benchmarks.py index d28909a347..830327cf48 100644 --- a/notes-to-self/proxy-benchmarks.py +++ b/notes-to-self/proxy-benchmarks.py @@ -54,7 +54,7 @@ def add_wrapper(cls, method): f""" def wrapper(self, *args, **kwargs): return self._wrapped.{method}(*args, **kwargs) - """ + """, ) ns = {} exec(code, ns) @@ -113,7 +113,7 @@ def setter(self, newval): def deleter(self): del self._wrapped.{attr} - """ + """, ) ns = {} exec(code, ns) diff --git a/notes-to-self/ssl-handshake/ssl-handshake.py b/notes-to-self/ssl-handshake/ssl-handshake.py index e906bc2a87..1665ce3331 100644 --- a/notes-to-self/ssl-handshake/ssl-handshake.py +++ b/notes-to-self/ssl-handshake/ssl-handshake.py @@ -27,7 +27,9 @@ def echo_server_connection(): client_sock, server_sock = socket.socketpair() with client_sock, server_sock: t = threading.Thread( - target=_ssl_echo_serve_sync, args=(server_sock,), daemon=True + target=_ssl_echo_serve_sync, + args=(server_sock,), + daemon=True, ) t.start() @@ -101,7 +103,9 @@ def wrap_socket_via_wrap_bio(ctx, sock, **kwargs): with echo_server_connection() as client_sock: client_ctx = ssl.create_default_context(cafile="trio-test-CA.pem") wrapped = wrap_socket( - client_ctx, client_sock, server_hostname="trio-test-1.example.org" + client_ctx, + client_sock, + server_hostname="trio-test-1.example.org", ) wrapped.do_handshake() wrapped.sendall(b"x") @@ -113,7 +117,9 @@ def wrap_socket_via_wrap_bio(ctx, sock, **kwargs): with echo_server_connection() as client_sock: client_ctx = ssl.create_default_context(cafile="trio-test-CA.pem") wrapped = wrap_socket( - client_ctx, client_sock, server_hostname="trio-test-2.example.org" + client_ctx, + client_sock, + server_hostname="trio-test-2.example.org", ) try: wrapped.do_handshake() @@ -126,7 +132,9 @@ def wrap_socket_via_wrap_bio(ctx, sock, **kwargs): with echo_server_connection() as client_sock: client_ctx = ssl.create_default_context(cafile="trio-test-CA.pem") wrapped = wrap_socket( - client_ctx, client_sock, server_hostname="trio-test-2.example.org" + client_ctx, + client_sock, + server_hostname="trio-test-2.example.org", ) # We forgot to call do_handshake # But the hostname is wrong so something had better error out... diff --git a/notes-to-self/wakeup-fd-racer.py b/notes-to-self/wakeup-fd-racer.py index b56cbdc91c..97faa0dfdf 100644 --- a/notes-to-self/wakeup-fd-racer.py +++ b/notes-to-self/wakeup-fd-racer.py @@ -91,7 +91,7 @@ def main(): if duration < 2: print( f"Attempt {attempt}: OK, trying again " - f"(select_calls = {select_calls}, drained = {drained})" + f"(select_calls = {select_calls}, drained = {drained})", ) else: print(f"Attempt {attempt}: FAILED, took {duration} seconds") diff --git a/notes-to-self/win-waitable-timer.py b/notes-to-self/win-waitable-timer.py index 43e6acd34a..b8d9af6cad 100644 --- a/notes-to-self/win-waitable-timer.py +++ b/notes-to-self/win-waitable-timer.py @@ -49,7 +49,7 @@ WORD wSecond; WORD wMilliseconds; } SYSTEMTIME, *PSYSTEMTIME, *LPSYSTEMTIME; -""" +""", ) ffi.cdef( diff --git a/pyproject.toml b/pyproject.toml index 9bb0919d54..ff4aa34600 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,13 +109,20 @@ select = [ "ASYNC", # flake8-async "B", # flake8-bugbear "C4", # flake8-comprehensions + "COM", # flake8-commas "E", # Error + "EXE", # flake8-executable "F", # pyflakes "FA", # flake8-future-annotations + "FLY", # flynt + "FURB", # refurb "I", # isort + "ICN", # flake8-import-conventions "PERF", # Perflint + "PIE", # flake8-pie "PT", # flake8-pytest-style "PYI", # flake8-pyi + "Q", # flake8-quotes "RUF", # Ruff-specific rules "SIM", # flake8-simplify "TCH", # flake8-type-checking diff --git a/src/trio/__init__.py b/src/trio/__init__.py index d2151677b1..34fda84525 100644 --- a/src/trio/__init__.py +++ b/src/trio/__init__.py @@ -25,6 +25,7 @@ # Submodules imported by default from . import abc, from_thread, lowlevel, socket, to_thread from ._channel import ( + MemoryChannelStatistics as MemoryChannelStatistics, MemoryReceiveChannel as MemoryReceiveChannel, MemorySendChannel as MemorySendChannel, open_memory_channel as open_memory_channel, diff --git a/src/trio/_abc.py b/src/trio/_abc.py index 20f1614cc6..306ee227fc 100644 --- a/src/trio/_abc.py +++ b/src/trio/_abc.py @@ -198,7 +198,9 @@ async def getaddrinfo( @abstractmethod async def getnameinfo( - self, sockaddr: tuple[str, int] | tuple[str, int, int, int], flags: int + self, + sockaddr: tuple[str, int] | tuple[str, int, int, int], + flags: int, ) -> tuple[str, str]: """A custom implementation of :func:`~trio.socket.getnameinfo`. @@ -691,6 +693,14 @@ async def __anext__(self) -> ReceiveType: raise StopAsyncIteration from None +# these are necessary for Sphinx's :show-inheritance: with type args. +# (this should be removed if possible) +# see: https://github.com/python/cpython/issues/123250 +SendChannel.__module__ = SendChannel.__module__.replace("_abc", "abc") +ReceiveChannel.__module__ = ReceiveChannel.__module__.replace("_abc", "abc") +Listener.__module__ = Listener.__module__.replace("_abc", "abc") + + class Channel(SendChannel[T], ReceiveChannel[T]): """A standard interface for interacting with bidirectional channels. @@ -700,3 +710,7 @@ class Channel(SendChannel[T], ReceiveChannel[T]): """ __slots__ = () + + +# see above +Channel.__module__ = Channel.__module__.replace("_abc", "abc") diff --git a/src/trio/_channel.py b/src/trio/_channel.py index f5ed4004d7..41a7fc6824 100644 --- a/src/trio/_channel.py +++ b/src/trio/_channel.py @@ -96,7 +96,8 @@ def _open_memory_channel( # Need to use Tuple instead of tuple due to CI check running on 3.8 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 + cls, + max_buffer_size: int | float, # noqa: PYI041 ) -> tuple[MemorySendChannel[T], MemoryReceiveChannel[T]]: return _open_memory_channel(max_buffer_size) @@ -110,7 +111,7 @@ def __init__(self, max_buffer_size: int | float): # noqa: PYI041 @attrs.frozen -class MemoryChannelStats: +class MemoryChannelStatistics: current_buffer_used: int max_buffer_size: int | float open_send_channels: int @@ -131,8 +132,8 @@ class MemoryChannelState(Generic[T]): # {task: None} receive_tasks: OrderedDict[Task, None] = attrs.Factory(OrderedDict) - def statistics(self) -> MemoryChannelStats: - return MemoryChannelStats( + def statistics(self) -> MemoryChannelStatistics: + return MemoryChannelStatistics( current_buffer_used=len(self.data), max_buffer_size=self.max_buffer_size, open_send_channels=self.open_send_channels, @@ -158,7 +159,9 @@ def __attrs_post_init__(self) -> None: def __repr__(self) -> str: return f"" - def statistics(self) -> MemoryChannelStats: + def statistics(self) -> MemoryChannelStatistics: + """Returns a `MemoryChannelStatistics` for the memory channel this is + associated with.""" # XX should we also report statistics specific to this object? return self._state.statistics() @@ -281,6 +284,9 @@ def close(self) -> None: @enable_ki_protection async def aclose(self) -> None: + """Close this send channel object asynchronously. + + See `MemorySendChannel.close`.""" self.close() await trio.lowlevel.checkpoint() @@ -295,7 +301,9 @@ class MemoryReceiveChannel(ReceiveChannel[ReceiveType], metaclass=NoPublicConstr def __attrs_post_init__(self) -> None: self._state.open_receive_channels += 1 - def statistics(self) -> MemoryChannelStats: + def statistics(self) -> MemoryChannelStatistics: + """Returns a `MemoryChannelStatistics` for the memory channel this is + associated with.""" return self._state.statistics() def __repr__(self) -> str: @@ -429,5 +437,8 @@ def close(self) -> None: @enable_ki_protection async def aclose(self) -> None: + """Close this receive channel object asynchronously. + + See `MemoryReceiveChannel.close`.""" self.close() await trio.lowlevel.checkpoint() diff --git a/src/trio/_core/__init__.py b/src/trio/_core/__init__.py index 71f5f17eb2..fdef90292d 100644 --- a/src/trio/_core/__init__.py +++ b/src/trio/_core/__init__.py @@ -20,7 +20,12 @@ from ._ki import currently_ki_protected, disable_ki_protection, enable_ki_protection from ._local import RunVar, RunVarToken from ._mock_clock import MockClock -from ._parking_lot import ParkingLot, ParkingLotStatistics +from ._parking_lot import ( + ParkingLot, + ParkingLotStatistics, + add_parking_lot_breaker, + remove_parking_lot_breaker, +) # Imports that always exist from ._run import ( diff --git a/src/trio/_core/_asyncgens.py b/src/trio/_core/_asyncgens.py index 1a622dadfc..77f1c7eced 100644 --- a/src/trio/_core/_asyncgens.py +++ b/src/trio/_core/_asyncgens.py @@ -60,7 +60,8 @@ def firstiter(agen: AsyncGeneratorType[object, NoReturn]) -> None: self.prev_hooks.firstiter(agen) def finalize_in_trio_context( - agen: AsyncGeneratorType[object, NoReturn], agen_name: str + agen: AsyncGeneratorType[object, NoReturn], + agen_name: str, ) -> None: try: runner.spawn_system_task( @@ -85,7 +86,9 @@ def finalizer(agen: AsyncGeneratorType[object, NoReturn]) -> None: if is_ours: runner.entry_queue.run_sync_soon( - finalize_in_trio_context, agen, agen_name + finalize_in_trio_context, + agen, + agen_name, ) # Do this last, because it might raise an exception @@ -123,7 +126,7 @@ def finalizer(agen: AsyncGeneratorType[object, NoReturn]) -> None: raise RuntimeError( f"Non-Trio async generator {agen_name!r} awaited something " "during finalization; install a finalization hook to " - "support this, or wrap it in 'async with aclosing(...):'" + "support this, or wrap it in 'async with aclosing(...):'", ) self.prev_hooks = sys.get_asyncgen_hooks() @@ -146,7 +149,7 @@ async def finalize_remaining(self, runner: _run.Runner) -> None: # them was an asyncgen finalizer that snuck in under the wire. runner.entry_queue.run_sync_soon(runner.reschedule, runner.init_task) await _core.wait_task_rescheduled( - lambda _: _core.Abort.FAILED # pragma: no cover + lambda _: _core.Abort.FAILED, # pragma: no cover ) self.alive.update(self.trailing_needs_finalize) self.trailing_needs_finalize.clear() @@ -193,7 +196,9 @@ def close(self) -> None: sys.set_asyncgen_hooks(*self.prev_hooks) async def _finalize_one( - self, agen: AsyncGeneratorType[object, NoReturn], name: object + self, + agen: AsyncGeneratorType[object, NoReturn], + name: object, ) -> None: try: # This shield ensures that finalize_asyncgen never exits diff --git a/src/trio/_core/_concat_tb.py b/src/trio/_core/_concat_tb.py index 497d37f8ad..2ddaf2e8e6 100644 --- a/src/trio/_core/_concat_tb.py +++ b/src/trio/_core/_concat_tb.py @@ -104,7 +104,8 @@ def controller(operation: tputil.ProxyOperation) -> Any | None: # type: ignore[ return operation.delegate() # Delegate is reverting to original behaviour return cast( - TracebackType, tputil.make_proxy(controller, type(base_tb), base_tb) + TracebackType, + tputil.make_proxy(controller, type(base_tb), base_tb), ) # Returns proxy to traceback @@ -112,7 +113,8 @@ def controller(operation: tputil.ProxyOperation) -> Any | None: # type: ignore[ # `strict_exception_groups=False`. Once that is retired this function and its helper can # be removed as well. def concat_tb( - head: TracebackType | None, tail: TracebackType | None + head: TracebackType | None, + tail: TracebackType | None, ) -> TracebackType | None: # We have to use an iterative algorithm here, because in the worst case # this might be a RecursionError stack that is by definition too deep to diff --git a/src/trio/_core/_entry_queue.py b/src/trio/_core/_entry_queue.py index 8305cd1629..8534bc07dc 100644 --- a/src/trio/_core/_entry_queue.py +++ b/src/trio/_core/_entry_queue.py @@ -77,7 +77,7 @@ async def kill_everything(exc: BaseException) -> NoReturn: parent_nursery = _core.current_task().parent_nursery if parent_nursery is None: raise AssertionError( - "Internal error: `parent_nursery` should never be `None`" + "Internal error: `parent_nursery` should never be `None`", ) from exc # pragma: no cover parent_nursery.start_soon(kill_everything, exc) diff --git a/src/trio/_core/_generated_io_kqueue.py b/src/trio/_core/_generated_io_kqueue.py index e150fc21f9..eb230597b3 100644 --- a/src/trio/_core/_generated_io_kqueue.py +++ b/src/trio/_core/_generated_io_kqueue.py @@ -42,7 +42,8 @@ def current_kqueue() -> select.kqueue: def monitor_kevent( - ident: int, filter: int + ident: int, + filter: int, ) -> ContextManager[_core.UnboundedQueue[select.kevent]]: """TODO: these are implemented, but are currently more of a sketch than anything real. See `#26 @@ -56,7 +57,9 @@ def monitor_kevent( async def wait_kevent( - ident: int, filter: int, abort_func: Callable[[RaiseCancelT], Abort] + ident: int, + filter: int, + abort_func: Callable[[RaiseCancelT], Abort], ) -> Abort: """TODO: these are implemented, but are currently more of a sketch than anything real. See `#26 @@ -65,7 +68,9 @@ async def wait_kevent( sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return await GLOBAL_RUN_CONTEXT.runner.io_manager.wait_kevent( - ident, filter, abort_func + ident, + filter, + abort_func, ) except AttributeError: raise RuntimeError("must be called from async context") from None diff --git a/src/trio/_core/_generated_io_windows.py b/src/trio/_core/_generated_io_windows.py index 72264f599f..1cad305a03 100644 --- a/src/trio/_core/_generated_io_windows.py +++ b/src/trio/_core/_generated_io_windows.py @@ -134,14 +134,17 @@ async def wait_overlapped(handle_: int | CData, lpOverlapped: CData | int) -> ob sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return await GLOBAL_RUN_CONTEXT.runner.io_manager.wait_overlapped( - handle_, lpOverlapped + handle_, + lpOverlapped, ) except AttributeError: raise RuntimeError("must be called from async context") from None async def write_overlapped( - handle: int | CData, data: Buffer, file_offset: int = 0 + handle: int | CData, + data: Buffer, + file_offset: int = 0, ) -> int: """TODO: these are implemented, but are currently more of a sketch than anything real. See `#26 @@ -151,14 +154,18 @@ async def write_overlapped( sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return await GLOBAL_RUN_CONTEXT.runner.io_manager.write_overlapped( - handle, data, file_offset + handle, + data, + file_offset, ) except AttributeError: raise RuntimeError("must be called from async context") from None async def readinto_overlapped( - handle: int | CData, buffer: Buffer, file_offset: int = 0 + handle: int | CData, + buffer: Buffer, + file_offset: int = 0, ) -> int: """TODO: these are implemented, but are currently more of a sketch than anything real. See `#26 @@ -168,7 +175,9 @@ async def readinto_overlapped( sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return await GLOBAL_RUN_CONTEXT.runner.io_manager.readinto_overlapped( - handle, buffer, file_offset + handle, + buffer, + file_offset, ) except AttributeError: raise RuntimeError("must be called from async context") from None diff --git a/src/trio/_core/_generated_run.py b/src/trio/_core/_generated_run.py index ac3e0f39d6..b5957a134e 100644 --- a/src/trio/_core/_generated_run.py +++ b/src/trio/_core/_generated_run.py @@ -187,7 +187,10 @@ def spawn_system_task( sys._getframe().f_locals[LOCALS_KEY_KI_PROTECTION_ENABLED] = True try: return GLOBAL_RUN_CONTEXT.runner.spawn_system_task( - async_fn, *args, name=name, context=context + async_fn, + *args, + name=name, + context=context, ) except AttributeError: raise RuntimeError("must be called from async context") from None diff --git a/src/trio/_core/_io_epoll.py b/src/trio/_core/_io_epoll.py index 29b378a014..5e05f0813f 100644 --- a/src/trio/_core/_io_epoll.py +++ b/src/trio/_core/_io_epoll.py @@ -205,7 +205,7 @@ class EpollIOManager: _epoll: select.epoll = attrs.Factory(lambda: select.epoll()) # {fd: EpollWaiters} _registered: defaultdict[int, EpollWaiters] = attrs.Factory( - lambda: defaultdict(EpollWaiters) + lambda: defaultdict(EpollWaiters), ) _force_wakeup: WakeupSocketpair = attrs.Factory(WakeupSocketpair) _force_wakeup_fd: int | None = None @@ -298,7 +298,7 @@ async def _epoll_wait(self, fd: int | _HasFileNo, attr_name: str) -> None: waiters = self._registered[fd] if getattr(waiters, attr_name) is not None: raise _core.BusyResourceError( - "another task is already reading / writing this fd" + "another task is already reading / writing this fd", ) setattr(waiters, attr_name, _core.current_task()) self._update_registrations(fd) diff --git a/src/trio/_core/_io_kqueue.py b/src/trio/_core/_io_kqueue.py index 3d0aed7d35..13f95b22b2 100644 --- a/src/trio/_core/_io_kqueue.py +++ b/src/trio/_core/_io_kqueue.py @@ -43,7 +43,9 @@ class KqueueIOManager: def __attrs_post_init__(self) -> None: force_wakeup_event = select.kevent( - self._force_wakeup.wakeup_sock, select.KQ_FILTER_READ, select.KQ_EV_ADD + self._force_wakeup.wakeup_sock, + select.KQ_FILTER_READ, + select.KQ_EV_ADD, ) self._kqueue.control([force_wakeup_event], 0) self._force_wakeup_fd = self._force_wakeup.wakeup_sock.fileno() @@ -129,7 +131,7 @@ def monitor_kevent( key = (ident, filter) if key in self._registered: raise _core.BusyResourceError( - "attempt to register multiple listeners for same ident/filter pair" + "attempt to register multiple listeners for same ident/filter pair", ) q = _core.UnboundedQueue[select.kevent]() self._registered[key] = q @@ -152,7 +154,7 @@ async def wait_kevent( key = (ident, filter) if key in self._registered: raise _core.BusyResourceError( - "attempt to register multiple listeners for same ident/filter pair" + "attempt to register multiple listeners for same ident/filter pair", ) self._registered[key] = _core.current_task() @@ -286,5 +288,5 @@ def notify_closing(self, fd: int | _HasFileNo) -> None: # XX this is an interesting example of a case where being able # to close a queue would be useful... raise NotImplementedError( - "can't close an fd that monitor_kevent is using" + "can't close an fd that monitor_kevent is using", ) diff --git a/src/trio/_core/_io_windows.py b/src/trio/_core/_io_windows.py index 99cb7c76be..0bc3b33378 100644 --- a/src/trio/_core/_io_windows.py +++ b/src/trio/_core/_io_windows.py @@ -303,7 +303,9 @@ def _check(success: T) -> T: def _get_underlying_socket( - sock: _HasFileNo | int | Handle, *, which: WSAIoctls = WSAIoctls.SIO_BASE_HANDLE + sock: _HasFileNo | int | Handle, + *, + which: WSAIoctls = WSAIoctls.SIO_BASE_HANDLE, ) -> Handle: if hasattr(sock, "fileno"): sock = sock.fileno() @@ -354,7 +356,8 @@ def _get_base_socket(sock: _HasFileNo | int | Handle) -> Handle: sock = sock.fileno() sock = _handle(sock) next_sock = _get_underlying_socket( - sock, which=WSAIoctls.SIO_BSP_HANDLE_POLL + sock, + which=WSAIoctls.SIO_BSP_HANDLE_POLL, ) if next_sock == sock: # If BSP_HANDLE_POLL returns the same socket we already had, @@ -366,7 +369,7 @@ def _get_base_socket(sock: _HasFileNo | int | Handle) -> Handle: "return a different socket. Please file a bug at " "https://github.com/python-trio/trio/issues/new, " "and include the output of running: " - "netsh winsock show catalog" + "netsh winsock show catalog", ) from ex # Otherwise we've gotten at least one layer deeper, so # loop back around to keep digging. @@ -421,7 +424,7 @@ def __init__(self) -> None: self._all_afd_handles: list[Handle] = [] self._iocp = _check( - kernel32.CreateIoCompletionPort(INVALID_HANDLE_VALUE, ffi.NULL, 0, 0) + kernel32.CreateIoCompletionPort(INVALID_HANDLE_VALUE, ffi.NULL, 0, 0), ) self._events = ffi.new("OVERLAPPED_ENTRY[]", MAX_EVENTS) @@ -454,7 +457,8 @@ def __init__(self) -> None: # LSPs can in theory override this, but we believe that it never # actually happens in the wild (except Komodia) select_handle = _get_underlying_socket( - s, which=WSAIoctls.SIO_BSP_HANDLE_SELECT + s, + which=WSAIoctls.SIO_BSP_HANDLE_SELECT, ) try: # LSPs shouldn't override this... @@ -472,7 +476,7 @@ def __init__(self) -> None: "Please file a bug at " "https://github.com/python-trio/trio/issues/new, " "and include the output of running: " - "netsh winsock show catalog" + "netsh winsock show catalog", ) def close(self) -> None: @@ -508,8 +512,11 @@ def force_wakeup(self) -> None: assert self._iocp is not None _check( kernel32.PostQueuedCompletionStatus( - self._iocp, 0, CKeys.FORCE_WAKEUP, ffi.NULL - ) + self._iocp, + 0, + CKeys.FORCE_WAKEUP, + ffi.NULL, + ), ) def get_events(self, timeout: float) -> EventResult: @@ -521,8 +528,13 @@ def get_events(self, timeout: float) -> EventResult: assert self._iocp is not None _check( kernel32.GetQueuedCompletionStatusEx( - self._iocp, self._events, MAX_EVENTS, received, milliseconds, 0 - ) + self._iocp, + self._events, + MAX_EVENTS, + received, + milliseconds, + 0, + ), ) except OSError as exc: if exc.winerror != ErrorCodes.WAIT_TIMEOUT: # pragma: no cover @@ -563,7 +575,8 @@ def process_events(self, received: EventResult) -> None: overlapped = entry.lpOverlapped transferred = entry.dwNumberOfBytesTransferred info = CompletionKeyEventInfo( - lpOverlapped=overlapped, dwNumberOfBytesTransferred=transferred + lpOverlapped=overlapped, + dwNumberOfBytesTransferred=transferred, ) _core.reschedule(waiter, Value(info)) elif entry.lpCompletionKey == CKeys.LATE_CANCEL: @@ -586,7 +599,7 @@ def process_events(self, received: EventResult) -> None: exc = _core.TrioInternalError( f"Failed to cancel overlapped I/O in {waiter.name} and didn't " "receive the completion either. Did you forget to " - "call register_with_iocp()?" + "call register_with_iocp()?", ) # Raising this out of handle_io ensures that # the user will see our message even if some @@ -608,7 +621,8 @@ def process_events(self, received: EventResult) -> None: overlapped = int(ffi.cast("uintptr_t", entry.lpOverlapped)) transferred = entry.dwNumberOfBytesTransferred info = CompletionKeyEventInfo( - lpOverlapped=overlapped, dwNumberOfBytesTransferred=transferred + lpOverlapped=overlapped, + dwNumberOfBytesTransferred=transferred, ) queue.put_nowait(info) @@ -622,8 +636,9 @@ def _register_with_iocp(self, handle_: int | CData, completion_key: int) -> None # Ref: http://www.lenholgate.com/blog/2009/09/interesting-blog-posts-on-high-performance-servers.html _check( kernel32.SetFileCompletionNotificationModes( - handle, CompletionModes.FILE_SKIP_SET_EVENT_ON_HANDLE - ) + handle, + CompletionModes.FILE_SKIP_SET_EVENT_ON_HANDLE, + ), ) ################################################################ @@ -637,8 +652,9 @@ def _refresh_afd(self, base_handle: Handle) -> None: try: _check( kernel32.CancelIoEx( - afd_group.handle, waiters.current_op.lpOverlapped - ) + afd_group.handle, + waiters.current_op.lpOverlapped, + ), ) except OSError as exc: if exc.winerror != ErrorCodes.ERROR_NOT_FOUND: @@ -687,7 +703,7 @@ def _refresh_afd(self, base_handle: Handle) -> None: ffi.sizeof("AFD_POLL_INFO"), ffi.NULL, lpOverlapped, - ) + ), ) except OSError as exc: if exc.winerror != ErrorCodes.ERROR_IO_PENDING: @@ -813,7 +829,9 @@ def register_with_iocp(self, handle: int | CData) -> None: @_public async def wait_overlapped( - self, handle_: int | CData, lpOverlapped: CData | int + self, + handle_: int | CData, + lpOverlapped: CData | int, ) -> object: """TODO: these are implemented, but are currently more of a sketch than anything real. See `#26 @@ -825,7 +843,7 @@ async def wait_overlapped( lpOverlapped = ffi.cast("LPOVERLAPPED", lpOverlapped) if lpOverlapped in self._overlapped_waiters: raise _core.BusyResourceError( - "another task is already waiting on that lpOverlapped" + "another task is already waiting on that lpOverlapped", ) task = _core.current_task() self._overlapped_waiters[lpOverlapped] = task @@ -852,8 +870,11 @@ def abort(raise_cancel_: RaiseCancelT) -> Abort: # does, we'll assume the handle wasn't registered. _check( kernel32.PostQueuedCompletionStatus( - self._iocp, 0, CKeys.LATE_CANCEL, lpOverlapped - ) + self._iocp, + 0, + CKeys.LATE_CANCEL, + lpOverlapped, + ), ) # Keep the lpOverlapped referenced so its address # doesn't get reused until our posted completion @@ -863,7 +884,7 @@ def abort(raise_cancel_: RaiseCancelT) -> Abort: self._posted_too_late_to_cancel.add(lpOverlapped) else: # pragma: no cover raise _core.TrioInternalError( - "CancelIoEx failed with unexpected error" + "CancelIoEx failed with unexpected error", ) from exc return _core.Abort.FAILED @@ -888,7 +909,9 @@ def abort(raise_cancel_: RaiseCancelT) -> Abort: return info async def _perform_overlapped( - self, handle: int | CData, submit_fn: Callable[[_Overlapped], None] + self, + handle: int | CData, + submit_fn: Callable[[_Overlapped], None], ) -> _Overlapped: # submit_fn(lpOverlapped) submits some I/O # it may raise an OSError with ERROR_IO_PENDING @@ -909,7 +932,10 @@ async def _perform_overlapped( @_public async def write_overlapped( - self, handle: int | CData, data: Buffer, file_offset: int = 0 + self, + handle: int | CData, + data: Buffer, + file_offset: int = 0, ) -> int: """TODO: these are implemented, but are currently more of a sketch than anything real. See `#26 @@ -930,7 +956,7 @@ def submit_write(lpOverlapped: _Overlapped) -> None: len(cbuf), ffi.NULL, lpOverlapped, - ) + ), ) lpOverlapped = await self._perform_overlapped(handle, submit_write) @@ -939,7 +965,10 @@ def submit_write(lpOverlapped: _Overlapped) -> None: @_public async def readinto_overlapped( - self, handle: int | CData, buffer: Buffer, file_offset: int = 0 + self, + handle: int | CData, + buffer: Buffer, + file_offset: int = 0, ) -> int: """TODO: these are implemented, but are currently more of a sketch than anything real. See `#26 @@ -959,7 +988,7 @@ def submit_read(lpOverlapped: _Overlapped) -> None: len(cbuf), ffi.NULL, lpOverlapped, - ) + ), ) lpOverlapped = await self._perform_overlapped(handle, submit_read) diff --git a/src/trio/_core/_parking_lot.py b/src/trio/_core/_parking_lot.py index c01fc2a9d8..af6ff610ee 100644 --- a/src/trio/_core/_parking_lot.py +++ b/src/trio/_core/_parking_lot.py @@ -71,11 +71,13 @@ # See: https://github.com/python-trio/trio/issues/53 from __future__ import annotations +import inspect import math from collections import OrderedDict from typing import TYPE_CHECKING import attrs +import outcome from .. import _core from .._util import final @@ -86,6 +88,37 @@ from ._run import Task +GLOBAL_PARKING_LOT_BREAKER: dict[Task, list[ParkingLot]] = {} + + +def add_parking_lot_breaker(task: Task, lot: ParkingLot) -> None: + """Register a task as a breaker for a lot. See :meth:`ParkingLot.break_lot`. + + raises: + trio.BrokenResourceError: if the task has already exited. + """ + if inspect.getcoroutinestate(task.coro) == inspect.CORO_CLOSED: + raise _core._exceptions.BrokenResourceError( + "Attempted to add already exited task as lot breaker.", + ) + if task not in GLOBAL_PARKING_LOT_BREAKER: + GLOBAL_PARKING_LOT_BREAKER[task] = [lot] + else: + GLOBAL_PARKING_LOT_BREAKER[task].append(lot) + + +def remove_parking_lot_breaker(task: Task, lot: ParkingLot) -> None: + """Deregister a task as a breaker for a lot. See :meth:`ParkingLot.break_lot`""" + try: + GLOBAL_PARKING_LOT_BREAKER[task].remove(lot) + except (KeyError, ValueError): + raise RuntimeError( + "Attempted to remove task as breaker for a lot it is not registered for", + ) from None + if not GLOBAL_PARKING_LOT_BREAKER[task]: + del GLOBAL_PARKING_LOT_BREAKER[task] + + @attrs.frozen class ParkingLotStatistics: """An object containing debugging information for a ParkingLot. @@ -118,6 +151,7 @@ class ParkingLot: # {task: None}, we just want a deque where we can quickly delete random # items _parked: OrderedDict[Task, None] = attrs.field(factory=OrderedDict, init=False) + broken_by: list[Task] = attrs.field(factory=list, init=False) def __len__(self) -> int: """Returns the number of parked tasks.""" @@ -136,7 +170,15 @@ async def park(self) -> None: """Park the current task until woken by a call to :meth:`unpark` or :meth:`unpark_all`. + Raises: + BrokenResourceError: if attempting to park in a broken lot, or the lot + breaks before we get to unpark. + """ + if self.broken_by: + raise _core.BrokenResourceError( + f"Attempted to park in parking lot broken by {self.broken_by}", + ) task = _core.current_task() self._parked[task] = None task.custom_sleep_data = self @@ -182,7 +224,10 @@ def unpark_all(self) -> list[Task]: @_core.enable_ki_protection def repark( - self, new_lot: ParkingLot, *, count: int | float = 1 # noqa: PYI041 + self, + new_lot: ParkingLot, + *, + count: int | float = 1, # noqa: PYI041 ) -> None: """Move parked tasks from one :class:`ParkingLot` object to another. @@ -231,6 +276,35 @@ def repark_all(self, new_lot: ParkingLot) -> None: """ return self.repark(new_lot, count=len(self)) + def break_lot(self, task: Task | None = None) -> None: + """Break this lot, with ``task`` noted as the task that broke it. + + This causes all parked tasks to raise an error, and any + future tasks attempting to park to error. Unpark & repark become no-ops as the + parking lot is empty. + + The error raised contains a reference to the task sent as a parameter. The task + is also saved in the parking lot in the ``broken_by`` attribute. + """ + if task is None: + task = _core.current_task() + + # if lot is already broken, just mark this as another breaker and return + if self.broken_by: + self.broken_by.append(task) + return + + self.broken_by.append(task) + + for parked_task in self._parked: + _core.reschedule( + parked_task, + outcome.Error( + _core.BrokenResourceError(f"Parking lot broken by {task}"), + ), + ) + self._parked.clear() + def statistics(self) -> ParkingLotStatistics: """Return an object containing debugging information. diff --git a/src/trio/_core/_run.py b/src/trio/_core/_run.py index 093d3a202a..cba7a8dec0 100644 --- a/src/trio/_core/_run.py +++ b/src/trio/_core/_run.py @@ -13,7 +13,7 @@ from contextlib import AbstractAsyncContextManager, contextmanager, suppress from contextvars import copy_context from heapq import heapify, heappop, heappush -from math import inf +from math import inf, isnan from time import perf_counter from typing import ( TYPE_CHECKING, @@ -40,6 +40,7 @@ from ._exceptions import Cancelled, RunFinishedError, TrioInternalError from ._instrumentation import Instruments from ._ki import LOCALS_KEY_KI_PROTECTION_ENABLED, KIManager, enable_ki_protection +from ._parking_lot import GLOBAL_PARKING_LOT_BREAKER from ._thread_cache import start_thread_soon from ._traps import ( Abort, @@ -142,7 +143,7 @@ def function_with_unique_name_xyzzy() -> NoReturn: raise else: # pragma: no cover raise TrioInternalError( - "A ZeroDivisionError should have been raised, but it wasn't." + "A ZeroDivisionError should have been raised, but it wasn't.", ) ctx = copy_context() @@ -160,7 +161,7 @@ def function_with_unique_name_xyzzy() -> NoReturn: else: # pragma: no cover raise TrioInternalError( f"The purpose of {function_with_unique_name_xyzzy.__name__} is " - "to raise a ZeroDivisionError, but it didn't." + "to raise a ZeroDivisionError, but it didn't.", ) @@ -219,7 +220,8 @@ def collapse_exception_group( and NONSTRICT_EXCEPTIONGROUP_NOTE in getattr(excgroup, "__notes__", ()) ): exceptions[0].__traceback__ = concat_tb( - excgroup.__traceback__, exceptions[0].__traceback__ + excgroup.__traceback__, + exceptions[0].__traceback__, ) return exceptions[0] elif modified: @@ -542,17 +544,36 @@ class CancelScope: cancelled_caught: bool = attrs.field(default=False, init=False) # Constructor arguments: - _deadline: float = attrs.field(default=inf, kw_only=True, alias="deadline") - _shield: bool = attrs.field(default=False, kw_only=True, alias="shield") + _relative_deadline: float = attrs.field(default=inf, kw_only=True) + _deadline: float = attrs.field(default=inf, kw_only=True) + _shield: bool = attrs.field(default=False, kw_only=True) + + def __attrs_post_init__(self) -> None: + if isnan(self._deadline): + raise ValueError("deadline must not be NaN") + if isnan(self._relative_deadline): + raise ValueError("relative deadline must not be NaN") + if self._relative_deadline < 0: + raise ValueError("timeout must be non-negative") + if self._relative_deadline != inf and self._deadline != inf: + raise ValueError( + "Cannot specify both a deadline and a relative deadline", + ) @enable_ki_protection def __enter__(self) -> Self: task = _core.current_task() if self._has_been_entered: raise RuntimeError( - "Each CancelScope may only be used for a single 'with' block" + "Each CancelScope may only be used for a single 'with' block", ) self._has_been_entered = True + + if self._relative_deadline != inf: + assert self._deadline == inf + self._deadline = current_time() + self._relative_deadline + self._relative_deadline = inf + if current_time() >= self._deadline: self.cancel() with self._might_change_registered_deadline(): @@ -564,7 +585,7 @@ def _close(self, exc: BaseException | None) -> BaseException | None: if self._cancel_status is None: new_exc = RuntimeError( f"Cancel scope stack corrupted: attempted to exit {self!r} " - "which had already been exited" + "which had already been exited", ) new_exc.__context__ = exc return new_exc @@ -586,7 +607,7 @@ def _close(self, exc: BaseException | None) -> BaseException | None: # without changing any state. new_exc = RuntimeError( f"Cancel scope stack corrupted: attempted to exit {self!r} " - f"from unrelated {scope_task!r}\n{MISNESTING_ADVICE}" + f"from unrelated {scope_task!r}\n{MISNESTING_ADVICE}", ) new_exc.__context__ = exc return new_exc @@ -598,7 +619,7 @@ def _close(self, exc: BaseException | None) -> BaseException | None: # pass silently. new_exc = RuntimeError( f"Cancel scope stack corrupted: attempted to exit {self!r} " - f"in {scope_task!r} that's still within its child {scope_task._cancel_status._scope!r}\n{MISNESTING_ADVICE}" + f"in {scope_task!r} that's still within its child {scope_task._cancel_status._scope!r}\n{MISNESTING_ADVICE}", ) new_exc.__context__ = exc exc = new_exc @@ -733,13 +754,70 @@ def deadline(self) -> float: this can be overridden by the ``deadline=`` argument to the :class:`~trio.CancelScope` constructor. """ + if self._relative_deadline != inf: + assert self._deadline == inf + warnings.warn( + DeprecationWarning( + "unentered relative cancel scope does not have an absolute deadline. Use `.relative_deadline`", + ), + stacklevel=2, + ) + return current_time() + self._relative_deadline return self._deadline @deadline.setter def deadline(self, new_deadline: float) -> None: + if isnan(new_deadline): + raise ValueError("deadline must not be NaN") + if self._relative_deadline != inf: + assert self._deadline == inf + warnings.warn( + DeprecationWarning( + "unentered relative cancel scope does not have an absolute deadline. Transforming into an absolute cancel scope. First set `.relative_deadline = math.inf` if you do want an absolute cancel scope.", + ), + stacklevel=2, + ) + self._relative_deadline = inf with self._might_change_registered_deadline(): self._deadline = float(new_deadline) + @property + def relative_deadline(self) -> float: + if self._has_been_entered: + return self._deadline - current_time() + elif self._deadline != inf: + assert self._relative_deadline == inf + raise RuntimeError( + "unentered non-relative cancel scope does not have a relative deadline", + ) + return self._relative_deadline + + @relative_deadline.setter + def relative_deadline(self, new_relative_deadline: float) -> None: + if isnan(new_relative_deadline): + raise ValueError("relative deadline must not be NaN") + if new_relative_deadline < 0: + raise ValueError("relative deadline must be non-negative") + if self._has_been_entered: + with self._might_change_registered_deadline(): + self._deadline = current_time() + float(new_relative_deadline) + elif self._deadline != inf: + assert self._relative_deadline == inf + raise RuntimeError( + "unentered non-relative cancel scope does not have a relative deadline", + ) + else: + self._relative_deadline = new_relative_deadline + + @property + def is_relative(self) -> bool | None: + """Returns None after entering. Returns False if both deadline and + relative_deadline are inf.""" + assert not (self._deadline != inf and self._relative_deadline != inf) + if self._has_been_entered: + return None + return self._relative_deadline != inf + @property def shield(self) -> bool: """Read-write, :class:`bool`, default :data:`False`. So long as @@ -932,7 +1010,9 @@ async def __aenter__(self) -> Nursery: self._scope = CancelScope() self._scope.__enter__() self._nursery = Nursery._create( - current_task(), self._scope, self.strict_exception_groups + current_task(), + self._scope, + self.strict_exception_groups, ) return self._nursery @@ -970,7 +1050,7 @@ async def __aexit__( def __enter__(self) -> NoReturn: raise RuntimeError( - "use 'async with open_nursery(...)', not 'with open_nursery(...)'" + "use 'async with open_nursery(...)', not 'with open_nursery(...)'", ) def __exit__( @@ -1099,7 +1179,8 @@ def _child_finished(self, task: Task, outcome: Outcome[Any]) -> None: self._check_nursery_closed() async def _nested_child_finished( - self, nested_child_exc: BaseException | None + self, + nested_child_exc: BaseException | None, ) -> BaseException | None: # 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 @@ -1138,7 +1219,8 @@ def aborted(raise_cancel: _core.RaiseCancelT) -> Abort: if not self._strict_exception_groups and len(self._pending_excs) == 1: return self._pending_excs[0] exception = BaseExceptionGroup( - "Exceptions from Trio nursery", self._pending_excs + "Exceptions from Trio nursery", + self._pending_excs, ) if not self._strict_exception_groups: exception.add_note(NONSTRICT_EXCEPTIONGROUP_NOTE) @@ -1255,7 +1337,10 @@ async def async_fn(arg1, arg2, *, task_status=trio.TASK_STATUS_IGNORED): task_status: _TaskStatus[Any] = _TaskStatus(old_nursery, self) thunk = functools.partial(async_fn, task_status=task_status) task = GLOBAL_RUN_CONTEXT.runner.spawn_impl( - thunk, args, old_nursery, name + thunk, + args, + old_nursery, + name, ) task._eventual_parent_nursery = self # Wait for either TaskStatus.started or an exception to @@ -1266,7 +1351,7 @@ async def async_fn(arg1, arg2, *, task_status=trio.TASK_STATUS_IGNORED): raise TrioInternalError( "Internal nursery should not have multiple tasks. This can be " 'caused by the user managing to access the "old" nursery in ' - "`task_status` and spawning tasks in it." + "`task_status` and spawning tasks in it.", ) from exc # If we get here, then the child either got reparented or exited @@ -1557,7 +1642,8 @@ def guest_tick(self) -> None: # Optimization: try to skip going into the thread if we can avoid it events_outcome: Value[EventResult] | Error = capture( - self.runner.io_manager.get_events, 0 + self.runner.io_manager.get_events, + 0, ) if timeout <= 0 or isinstance(events_outcome, Error) or events_outcome.value: # No need to go into the thread @@ -1696,7 +1782,9 @@ def current_root_task(self) -> Task | None: @_public # Type-ignore due to use of Any here. def reschedule( # type: ignore[misc] - self, task: Task, next_send: Outcome[Any] = _NO_SEND + self, + task: Task, + next_send: Outcome[Any] = _NO_SEND, ) -> None: """Reschedule the given task with the given :class:`outcome.Outcome`. @@ -1789,7 +1877,11 @@ async def python_wrapper(orig_coro: Awaitable[RetT]) -> RetT: # Set up the Task object ###### task = Task._create( - coro=coro, parent_nursery=nursery, runner=self, name=name, context=context + coro=coro, + parent_nursery=nursery, + runner=self, + name=name, + context=context, ) self.tasks.add(task) @@ -1805,6 +1897,12 @@ async def python_wrapper(orig_coro: Awaitable[RetT]) -> RetT: return task def task_exited(self, task: Task, outcome: Outcome[Any]) -> None: + # break parking lots associated with the exiting task + if task in GLOBAL_PARKING_LOT_BREAKER: + for lot in GLOBAL_PARKING_LOT_BREAKER[task]: + lot.break_lot(task) + del GLOBAL_PARKING_LOT_BREAKER[task] + if ( task._cancel_status is not None and task._cancel_status.abandoned_by_misnesting @@ -1819,7 +1917,7 @@ def task_exited(self, task: Task, outcome: Outcome[Any]) -> None: # traceback frame included raise RuntimeError( "Cancel scope stack corrupted: cancel scope surrounding " - f"{task!r} was closed before the task exited\n{MISNESTING_ADVICE}" + f"{task!r} was closed before the task exited\n{MISNESTING_ADVICE}", ) except RuntimeError as new_exc: if isinstance(outcome, Error): @@ -1932,7 +2030,10 @@ async def init( async with open_nursery() as main_task_nursery: try: self.main_task = self.spawn_impl( - async_fn, args, main_task_nursery, None + async_fn, + args, + main_task_nursery, + None, ) except BaseException as exc: self.main_task_outcome = Error(exc) @@ -2424,7 +2525,8 @@ def my_done_callback(run_outcome): # this time, so it shouldn't be possible to get an exception here, # except for a TrioInternalError. next_send = cast( - EventResult, None + EventResult, + None, ) # First iteration must be `None`, every iteration after that is EventResult for _tick in range(5): # expected need is 2 iterations + leave some wiggle room if runner.system_nursery is not None: @@ -2434,13 +2536,13 @@ def my_done_callback(run_outcome): timeout = guest_state.unrolled_run_gen.send(next_send) except StopIteration: # pragma: no cover raise TrioInternalError( - "Guest runner exited before system nursery was initialized" + "Guest runner exited before system nursery was initialized", ) from None if timeout != 0: # pragma: no cover guest_state.unrolled_run_gen.throw( TrioInternalError( - "Guest runner blocked before system nursery was initialized" - ) + "Guest runner blocked before system nursery was initialized", + ), ) # next_send should be the return value of # IOManager.get_events() if no I/O was waiting, which is @@ -2454,8 +2556,8 @@ def my_done_callback(run_outcome): guest_state.unrolled_run_gen.throw( TrioInternalError( "Guest runner yielded too many times before " - "system nursery was initialized" - ) + "system nursery was initialized", + ), ) guest_state.unrolled_run_next_send = Value(next_send) @@ -2488,7 +2590,11 @@ def unrolled_run( runner.instruments.call("before_run") runner.clock.start_clock() runner.init_task = runner.spawn_impl( - runner.init, (async_fn, args), None, "", system_task=True + runner.init, + (async_fn, args), + None, + "", + system_task=True, ) # You know how people talk about "event loops"? This 'while' loop right @@ -2666,7 +2772,7 @@ def unrolled_run( f"trio.run received unrecognized yield message {msg!r}. " "Are you trying to use a library written for some " "other framework like asyncio? That won't work " - "without some kind of compatibility shim." + "without some kind of compatibility shim.", ) # The foreign library probably doesn't adhere to our # protocol of unwrapping whatever outcome gets sent in. @@ -2690,7 +2796,7 @@ def unrolled_run( warnings.warn( RuntimeWarning( "Trio guest run got abandoned without properly finishing... " - "weird stuff might happen" + "weird stuff might happen", ), stacklevel=1, ) diff --git a/src/trio/_core/_tests/test_asyncgen.py b/src/trio/_core/_tests/test_asyncgen.py index aa64a49d70..7f029771b3 100644 --- a/src/trio/_core/_tests/test_asyncgen.py +++ b/src/trio/_core/_tests/test_asyncgen.py @@ -43,7 +43,8 @@ async def example(cause: str) -> AsyncGenerator[int, None]: async def async_main() -> None: # GC'ed before exhausted with pytest.warns( - ResourceWarning, match="Async generator.*collected before.*exhausted" + ResourceWarning, + match="Async generator.*collected before.*exhausted", ): assert await example("abandoned").asend(None) == 42 gc_collect_harder() @@ -153,7 +154,8 @@ async def innermost() -> AsyncGenerator[int, None]: record.append("innermost") async def agen( - label: int, inner: AsyncGenerator[int, None] + label: int, + inner: AsyncGenerator[int, None], ) -> AsyncGenerator[int, None]: try: yield await inner.asend(None) @@ -197,7 +199,8 @@ def collect_at_opportune_moment(token: _core._entry_queue.TrioToken) -> None: runner = _core._run.GLOBAL_RUN_CONTEXT.runner assert runner.system_nursery is not None if runner.system_nursery._closed and isinstance( - runner.asyncgens.alive, weakref.WeakSet + runner.asyncgens.alive, + weakref.WeakSet, ): saved.clear() record.append("final collection") @@ -235,7 +238,7 @@ async def async_main() -> None: else: # pragma: no cover pytest.fail( "Didn't manage to hit the trailing_finalizer_asyncgens case " - f"despite trying {_attempt} times" + f"despite trying {_attempt} times", ) diff --git a/src/trio/_core/_tests/test_exceptiongroup_gc.py b/src/trio/_core/_tests/test_exceptiongroup_gc.py index 8957a581a5..885ef68624 100644 --- a/src/trio/_core/_tests/test_exceptiongroup_gc.py +++ b/src/trio/_core/_tests/test_exceptiongroup_gc.py @@ -76,7 +76,8 @@ def test_concat_tb() -> None: # Unclear if this can still fail, removing the `del` from _concat_tb.copy_tb does not seem # to trigger it (on a platform where the `del` is executed) @pytest.mark.skipif( - sys.implementation.name != "cpython", reason="Only makes sense with refcounting GC" + sys.implementation.name != "cpython", + reason="Only makes sense with refcounting GC", ) def test_ExceptionGroup_catch_doesnt_create_cyclic_garbage() -> None: # https://github.com/python-trio/trio/pull/2063 diff --git a/src/trio/_core/_tests/test_guest_mode.py b/src/trio/_core/_tests/test_guest_mode.py index 8972ec735a..1a8b230e78 100644 --- a/src/trio/_core/_tests/test_guest_mode.py +++ b/src/trio/_core/_tests/test_guest_mode.py @@ -62,7 +62,8 @@ def run_sync_soon_threadsafe(fn: Callable[[], object]) -> None: nonlocal todo if host_thread is threading.current_thread(): # pragma: no cover crash = partial( - pytest.fail, "run_sync_soon_threadsafe called from host thread" + pytest.fail, + "run_sync_soon_threadsafe called from host thread", ) todo.put(("run", crash)) todo.put(("run", fn)) @@ -71,7 +72,8 @@ def run_sync_soon_not_threadsafe(fn: Callable[[], object]) -> None: nonlocal todo if host_thread is not threading.current_thread(): # pragma: no cover crash = partial( - pytest.fail, "run_sync_soon_not_threadsafe called from worker thread" + pytest.fail, + "run_sync_soon_not_threadsafe called from worker thread", ) todo.put(("run", crash)) todo.put(("run", fn)) @@ -181,7 +183,9 @@ def after_start_never_runs() -> None: # pragma: no cover # are raised out of start_guest_run, not out of the done_callback with pytest.raises(trio.TrioInternalError): trivial_guest_run( - trio_main, clock=BadClock(), in_host_after_start=after_start_never_runs + trio_main, + clock=BadClock(), + in_host_after_start=after_start_never_runs, ) @@ -331,7 +335,7 @@ async def sit_in_wait_all_tasks_blocked(watb_cscope: trio.CancelScope) -> None: await trio.testing.wait_all_tasks_blocked(cushion=9999) raise AssertionError( # pragma: no cover "wait_all_tasks_blocked should *not* return normally, " - "only by cancellation." + "only by cancellation.", ) assert watb_cscope.cancelled_caught @@ -486,7 +490,8 @@ async def trio_main() -> str: raise AssertionError("should never be reached") # pragma: no cover async def aio_pingpong( - from_trio: asyncio.Queue[int], to_trio: MemorySendChannel[int] + from_trio: asyncio.Queue[int], + to_trio: MemorySendChannel[int], ) -> None: print("aio_pingpong!") @@ -525,7 +530,8 @@ async def aio_pingpong( def test_guest_mode_internal_errors( - monkeypatch: pytest.MonkeyPatch, recwarn: pytest.WarningsRecorder + monkeypatch: pytest.MonkeyPatch, + recwarn: pytest.WarningsRecorder, ) -> None: with monkeypatch.context() as m: diff --git a/src/trio/_core/_tests/test_io.py b/src/trio/_core/_tests/test_io.py index acecc9d6c6..7060262edf 100644 --- a/src/trio/_core/_tests/test_io.py +++ b/src/trio/_core/_tests/test_io.py @@ -91,7 +91,9 @@ def fileno_wrapper(fileobj: stdlib_socket.socket) -> RetT: @read_socket_test @write_socket_test async def test_wait_basic( - socketpair: SocketPair, wait_readable: WaitSocket, wait_writable: WaitSocket + socketpair: SocketPair, + wait_readable: WaitSocket, + wait_writable: WaitSocket, ) -> None: a, b = socketpair @@ -215,7 +217,9 @@ async def writer() -> None: @read_socket_test @write_socket_test async def test_socket_simultaneous_read_write( - socketpair: SocketPair, wait_readable: WaitSocket, wait_writable: WaitSocket + socketpair: SocketPair, + wait_readable: WaitSocket, + wait_writable: WaitSocket, ) -> None: record: list[str] = [] @@ -245,7 +249,9 @@ async def w_task(sock: stdlib_socket.socket) -> None: @read_socket_test @write_socket_test async def test_socket_actual_streaming( - socketpair: SocketPair, wait_readable: WaitSocket, wait_writable: WaitSocket + socketpair: SocketPair, + wait_readable: WaitSocket, + wait_writable: WaitSocket, ) -> None: a, b = socketpair diff --git a/src/trio/_core/_tests/test_parking_lot.py b/src/trio/_core/_tests/test_parking_lot.py index 353c1ba45d..d9afee83d4 100644 --- a/src/trio/_core/_tests/test_parking_lot.py +++ b/src/trio/_core/_tests/test_parking_lot.py @@ -1,9 +1,18 @@ from __future__ import annotations +import re from typing import TypeVar import pytest +import trio +from trio.lowlevel import ( + add_parking_lot_breaker, + current_task, + remove_parking_lot_breaker, +) +from trio.testing import Matcher, RaisesGroup + from ... import _core from ...testing import wait_all_tasks_blocked from .._parking_lot import ParkingLot @@ -38,7 +47,8 @@ async def waiter(i: int, lot: ParkingLot) -> None: assert len(record) == 6 check_sequence_matches( - record, [{"sleep 0", "sleep 1", "sleep 2"}, {"wake 0", "wake 1", "wake 2"}] + record, + [{"sleep 0", "sleep 1", "sleep 2"}, {"wake 0", "wake 1", "wake 2"}], ) async with _core.open_nursery() as nursery: @@ -74,18 +84,23 @@ async def waiter(i: int, lot: ParkingLot) -> None: lot.unpark(count=2) await wait_all_tasks_blocked() check_sequence_matches( - record, ["sleep 0", "sleep 1", "sleep 2", {"wake 0", "wake 1"}] + record, + ["sleep 0", "sleep 1", "sleep 2", {"wake 0", "wake 1"}], ) lot.unpark_all() with pytest.raises( - ValueError, match=r"^Cannot pop a non-integer number of tasks\.$" + ValueError, + match=r"^Cannot pop a non-integer number of tasks\.$", ): lot.unpark(count=1.5) async def cancellable_waiter( - name: T, lot: ParkingLot, scopes: dict[T, _core.CancelScope], record: list[str] + name: T, + lot: ParkingLot, + scopes: dict[T, _core.CancelScope], + record: list[str], ) -> None: with _core.CancelScope() as scope: scopes[name] = scope @@ -120,7 +135,8 @@ async def test_parking_lot_cancel() -> None: assert len(record) == 6 check_sequence_matches( - record, ["sleep 1", "sleep 2", "sleep 3", "cancelled 2", {"wake 1", "wake 3"}] + record, + ["sleep 1", "sleep 2", "sleep 3", "cancelled 2", {"wake 1", "wake 3"}], ) @@ -208,3 +224,161 @@ async def test_parking_lot_repark_with_count() -> None: "wake 2", ] lot1.unpark_all() + + +async def dummy_task( + task_status: _core.TaskStatus[_core.Task] = trio.TASK_STATUS_IGNORED, +) -> None: + task_status.started(_core.current_task()) + await trio.sleep_forever() + + +async def test_parking_lot_breaker_basic() -> None: + """Test basic functionality for breaking lots.""" + lot = ParkingLot() + task = current_task() + + # defaults to current task + lot.break_lot() + assert lot.broken_by == [task] + + # breaking the lot again with the same task appends another copy in `broken_by` + lot.break_lot() + assert lot.broken_by == [task, task] + + # trying to park in broken lot errors + broken_by_str = re.escape(str([task, task])) + with pytest.raises( + _core.BrokenResourceError, + match=f"^Attempted to park in parking lot broken by {broken_by_str}$", + ): + await lot.park() + + +async def test_parking_lot_break_parking_tasks() -> None: + """Checks that tasks currently waiting to park raise an error when the breaker exits.""" + + async def bad_parker(lot: ParkingLot, scope: _core.CancelScope) -> None: + add_parking_lot_breaker(current_task(), lot) + with scope: + await trio.sleep_forever() + + lot = ParkingLot() + cs = _core.CancelScope() + + # check that parked task errors + with RaisesGroup( + Matcher(_core.BrokenResourceError, match="^Parking lot broken by"), + ): + async with _core.open_nursery() as nursery: + nursery.start_soon(bad_parker, lot, cs) + await wait_all_tasks_blocked() + + nursery.start_soon(lot.park) + await wait_all_tasks_blocked() + + cs.cancel() + + +async def test_parking_lot_breaker_registration() -> None: + lot = ParkingLot() + task = current_task() + + with pytest.raises( + RuntimeError, + match="Attempted to remove task as breaker for a lot it is not registered for", + ): + remove_parking_lot_breaker(task, lot) + + # check that a task can be registered as breaker for the same lot multiple times + add_parking_lot_breaker(task, lot) + add_parking_lot_breaker(task, lot) + remove_parking_lot_breaker(task, lot) + remove_parking_lot_breaker(task, lot) + + with pytest.raises( + RuntimeError, + match="Attempted to remove task as breaker for a lot it is not registered for", + ): + remove_parking_lot_breaker(task, lot) + + # registering a task as breaker on an already broken lot is fine + lot.break_lot() + child_task = None + async with trio.open_nursery() as nursery: + child_task = await nursery.start(dummy_task) + add_parking_lot_breaker(child_task, lot) + nursery.cancel_scope.cancel() + assert lot.broken_by == [task, child_task] + + # manually breaking a lot with an already exited task is fine + lot = ParkingLot() + lot.break_lot(child_task) + assert lot.broken_by == [child_task] + + +async def test_parking_lot_breaker_rebreak() -> None: + lot = ParkingLot() + task = current_task() + lot.break_lot() + + # breaking an already broken lot with a different task is allowed + # The nursery is only to create a task we can pass to lot.break_lot + async with trio.open_nursery() as nursery: + child_task = await nursery.start(dummy_task) + lot.break_lot(child_task) + nursery.cancel_scope.cancel() + + assert lot.broken_by == [task, child_task] + + +async def test_parking_lot_multiple_breakers_exit() -> None: + # register multiple tasks as lot breakers, then have them all exit + lot = ParkingLot() + async with trio.open_nursery() as nursery: + child_task1 = await nursery.start(dummy_task) + child_task2 = await nursery.start(dummy_task) + child_task3 = await nursery.start(dummy_task) + add_parking_lot_breaker(child_task1, lot) + add_parking_lot_breaker(child_task2, lot) + add_parking_lot_breaker(child_task3, lot) + nursery.cancel_scope.cancel() + + # I think the order is guaranteed currently, but doesn't hurt to be safe. + assert set(lot.broken_by) == {child_task1, child_task2, child_task3} + + +async def test_parking_lot_breaker_register_exited_task() -> None: + lot = ParkingLot() + child_task = None + async with trio.open_nursery() as nursery: + child_task = await nursery.start(dummy_task) + nursery.cancel_scope.cancel() + # trying to register an exited task as lot breaker errors + with pytest.raises( + trio.BrokenResourceError, + match="^Attempted to add already exited task as lot breaker.$", + ): + add_parking_lot_breaker(child_task, lot) + + +async def test_parking_lot_break_itself() -> None: + """Break a parking lot, where the breakee is parked. + Doing this is weird, but should probably be supported. + """ + + async def return_me_and_park( + lot: ParkingLot, + *, + task_status: _core.TaskStatus[_core.Task] = trio.TASK_STATUS_IGNORED, + ) -> None: + task_status.started(_core.current_task()) + await lot.park() + + lot = ParkingLot() + with RaisesGroup( + Matcher(_core.BrokenResourceError, match="^Parking lot broken by"), + ): + async with _core.open_nursery() as nursery: + child_task = await nursery.start(return_me_and_park, lot) + lot.break_lot(child_task) diff --git a/src/trio/_core/_tests/test_run.py b/src/trio/_core/_tests/test_run.py index ee823cb81a..9d2eab787d 100644 --- a/src/trio/_core/_tests/test_run.py +++ b/src/trio/_core/_tests/test_run.py @@ -9,7 +9,7 @@ import types import weakref from contextlib import ExitStack, contextmanager, suppress -from math import inf +from math import inf, nan from typing import TYPE_CHECKING, Any, NoReturn, TypeVar, cast import outcome @@ -116,7 +116,7 @@ async def test_nursery_warn_use_async_with() -> None: with on: # type: ignore pass # pragma: no cover excinfo.match( - r"use 'async with open_nursery\(...\)', not 'with open_nursery\(...\)'" + r"use 'async with open_nursery\(...\)', not 'with open_nursery\(...\)'", ) # avoid unawaited coro. @@ -156,7 +156,8 @@ async def looper(whoami: str, record: list[tuple[str, int]]) -> None: nursery.start_soon(looper, "b", record) check_sequence_matches( - record, [{("a", 0), ("b", 0)}, {("a", 1), ("b", 1)}, {("a", 2), ("b", 2)}] + record, + [{("a", 0), ("b", 0)}, {("a", 1), ("b", 1)}, {("a", 2), ("b", 2)}], ) @@ -364,6 +365,27 @@ async def test_cancel_scope_repr(mock_clock: _core.MockClock) -> None: assert "exited" in repr(scope) +async def test_cancel_scope_validation() -> None: + with pytest.raises( + ValueError, + match="^Cannot specify both a deadline and a relative deadline$", + ): + _core.CancelScope(deadline=7, relative_deadline=3) + scope = _core.CancelScope() + + with pytest.raises(ValueError, match="^deadline must not be NaN$"): + scope.deadline = nan + with pytest.raises(ValueError, match="^relative deadline must not be NaN$"): + scope.relative_deadline = nan + + with pytest.raises(ValueError, match="^relative deadline must be non-negative$"): + scope.relative_deadline = -3 + scope.relative_deadline = 5 + assert scope.relative_deadline == 5 + + # several related tests of CancelScope are implicitly handled by test_timeouts.py + + def test_cancel_points() -> None: async def main1() -> None: with _core.CancelScope() as scope: @@ -434,7 +456,10 @@ async def crasher() -> NoReturn: # nursery block continue propagating to reach the # outer scope. with RaisesGroup( - _core.Cancelled, _core.Cancelled, _core.Cancelled, KeyError + _core.Cancelled, + _core.Cancelled, + _core.Cancelled, + KeyError, ) as excinfo: async with _core.open_nursery() as nursery: # Two children that get cancelled by the nursery scope @@ -769,7 +794,8 @@ async def task2() -> None: nursery.cancel_scope.__exit__(None, None, None) finally: with pytest.raises( - RuntimeError, match="which had already been exited" + RuntimeError, + match="which had already been exited", ) as exc_info: await nursery_mgr.__aexit__(*sys.exc_info()) @@ -946,7 +972,7 @@ async def main() -> None: # the second exceptiongroup is from the second nursery opened in Runner.init() # the third exceptongroup is from the nursery defined in `system_task` above assert RaisesGroup(RaisesGroup(RaisesGroup(KeyError, ValueError))).matches( - excinfo.value.__cause__ + excinfo.value.__cause__, ) @@ -976,7 +1002,7 @@ async def main() -> None: # See explanation for triple-wrap in test_system_task_crash_ExceptionGroup assert RaisesGroup(RaisesGroup(RaisesGroup(ValueError))).matches( - excinfo.value.__cause__ + excinfo.value.__cause__, ) @@ -1126,13 +1152,18 @@ async def child() -> None: await sleep_forever() with RaisesGroup( - Matcher(ValueError, "error text", lambda e: isinstance(e.__context__, KeyError)) + Matcher( + ValueError, + "error text", + lambda e: isinstance(e.__context__, KeyError), + ), ): async with _core.open_nursery() as nursery: nursery.start_soon(child) await wait_all_tasks_blocked() _core.reschedule( - not_none(child_task), outcome.Error(ValueError("error text")) + not_none(child_task), + outcome.Error(ValueError("error text")), ) @@ -1195,13 +1226,14 @@ async def inner() -> None: "^Unique Text$", lambda e: isinstance(e.__context__, IndexError) and isinstance(e.__context__.__context__, KeyError), - ) + ), ): async with _core.open_nursery() as nursery: nursery.start_soon(child) await wait_all_tasks_blocked() _core.reschedule( - not_none(child_task), outcome.Error(ValueError("Unique Text")) + not_none(child_task), + outcome.Error(ValueError("Unique Text")), ) @@ -1680,7 +1712,8 @@ async def async_gen(arg: T) -> AsyncGenerator[T, None]: # pragma: no cover ): bad_call_run(f()) # type: ignore[arg-type] with pytest.raises( - TypeError, match="expected an async function but got an async generator" + TypeError, + match="expected an async function but got an async generator", ): bad_call_run(async_gen, 0) # type: ignore @@ -1690,7 +1723,7 @@ async def async_gen(arg: T) -> AsyncGenerator[T, None]: # pragma: no cover bad_call_spawn(f()) # type: ignore[arg-type] with RaisesGroup( - Matcher(TypeError, "expected an async function but got an async generator") + Matcher(TypeError, "expected an async function but got an async generator"), ): bad_call_spawn(async_gen, 0) # type: ignore @@ -1768,7 +1801,9 @@ async def no_args() -> None: # pragma: no cover await nursery.start(no_args) async def sleep_then_start( - seconds: int, *, task_status: _core.TaskStatus[int] = _core.TASK_STATUS_IGNORED + seconds: int, + *, + task_status: _core.TaskStatus[int] = _core.TASK_STATUS_IGNORED, ) -> None: repr(task_status) # smoke test await sleep(seconds) @@ -1847,7 +1882,8 @@ async def just_started( # but if the task does not execute any checkpoints, and exits, then start() # doesn't raise Cancelled, since the task completed successfully. async def started_with_no_checkpoint( - *, task_status: _core.TaskStatus[None] = _core.TASK_STATUS_IGNORED + *, + task_status: _core.TaskStatus[None] = _core.TASK_STATUS_IGNORED, ) -> None: task_status.started(None) @@ -1861,7 +1897,8 @@ async def started_with_no_checkpoint( # the child crashes after calling started(), the error can *still* come # out of start() async def raise_keyerror_after_started( - *, task_status: _core.TaskStatus[None] = _core.TASK_STATUS_IGNORED + *, + task_status: _core.TaskStatus[None] = _core.TASK_STATUS_IGNORED, ) -> None: task_status.started() raise KeyError("whoopsiedaisy") @@ -1924,7 +1961,8 @@ async def sleeping_children( async with _core.open_nursery() as nursery: target_nursery: _core.Nursery = await nursery.start(setup_nursery) await target_nursery.start( - sleeping_children, target_nursery.cancel_scope.cancel + sleeping_children, + target_nursery.cancel_scope.cancel, ) # Cancelling the setup_nursery just *after* calling started() @@ -2014,7 +2052,10 @@ def __init__(self, *largs: it) -> None: self.nexts = [obj.__anext__ for obj in largs] async def _accumulate( - self, f: Callable[[], Awaitable[int]], items: list[int], i: int + self, + f: Callable[[], Awaitable[int]], + items: list[int], + i: int, ) -> None: items[i] = await f() @@ -2036,7 +2077,8 @@ async def __anext__(self) -> list[int]: # We could also use RaisesGroup, but that's primarily meant as # test infra, not as a runtime tool. if len(e.exceptions) == 1 and isinstance( - e.exceptions[0], StopAsyncIteration + e.exceptions[0], + StopAsyncIteration, ): raise e.exceptions[0] from None else: # pragma: no cover @@ -2244,7 +2286,8 @@ async def detachable_coroutine( nonlocal task, pdco_outcome task = _core.current_task() pdco_outcome = await outcome.acapture( - _core.permanently_detach_coroutine_object, task_outcome + _core.permanently_detach_coroutine_object, + task_outcome, ) await async_yield(yield_value) @@ -2308,7 +2351,8 @@ def abort_fn(_: _core.RaiseCancelT) -> _core.Abort: # pragma: no cover with pytest.raises(RuntimeError) as excinfo: await _core.reattach_detached_coroutine_object( - not_none(unrelated_task), None + not_none(unrelated_task), + None, ) assert "does not match" in str(excinfo.value) @@ -2409,7 +2453,8 @@ async def test_cancel_scope_deadline_duplicates() -> None: # refer to this only seems to break test_cancel_scope_exit_doesnt_create_cyclic_garbage # We're keeping it for now to cover Outcome and potential future refactoring @pytest.mark.skipif( - sys.implementation.name != "cpython", reason="Only makes sense with refcounting GC" + sys.implementation.name != "cpython", + reason="Only makes sense with refcounting GC", ) async def test_simple_cancel_scope_usage_doesnt_create_cyclic_garbage() -> None: # https://github.com/python-trio/trio/issues/1770 @@ -2448,7 +2493,8 @@ async def crasher() -> NoReturn: @pytest.mark.skipif( - sys.implementation.name != "cpython", reason="Only makes sense with refcounting GC" + sys.implementation.name != "cpython", + reason="Only makes sense with refcounting GC", ) async def test_cancel_scope_exit_doesnt_create_cyclic_garbage() -> None: # https://github.com/python-trio/trio/pull/2063 @@ -2460,7 +2506,7 @@ async def crasher() -> NoReturn: old_flags = gc.get_debug() try: with RaisesGroup( - Matcher(ValueError, "^this is a crash$") + Matcher(ValueError, "^this is a crash$"), ), _core.CancelScope() as outer: async with _core.open_nursery() as nursery: gc.collect() @@ -2482,7 +2528,8 @@ async def crasher() -> NoReturn: @pytest.mark.skipif( - sys.implementation.name != "cpython", reason="Only makes sense with refcounting GC" + sys.implementation.name != "cpython", + reason="Only makes sense with refcounting GC", ) async def test_nursery_cancel_doesnt_create_cyclic_garbage() -> None: collected = False @@ -2518,7 +2565,8 @@ def toggle_collected() -> None: @pytest.mark.skipif( - sys.implementation.name != "cpython", reason="Only makes sense with refcounting GC" + sys.implementation.name != "cpython", + reason="Only makes sense with refcounting GC", ) async def test_locals_destroyed_promptly_on_cancel() -> None: destroyed = False @@ -2550,13 +2598,15 @@ def _create_kwargs(strictness: bool | None) -> dict[str, bool]: @pytest.mark.filterwarnings( - "ignore:.*strict_exception_groups=False:trio.TrioDeprecationWarning" + "ignore:.*strict_exception_groups=False:trio.TrioDeprecationWarning", ) @pytest.mark.parametrize("run_strict", [True, False, None]) @pytest.mark.parametrize("open_nursery_strict", [True, False, None]) @pytest.mark.parametrize("multiple_exceptions", [True, False]) def test_setting_strict_exception_groups( - run_strict: bool | None, open_nursery_strict: bool | None, multiple_exceptions: bool + run_strict: bool | None, + open_nursery_strict: bool | None, + multiple_exceptions: bool, ) -> None: """ Test default values and that nurseries can both inherit and override the global context @@ -2593,7 +2643,7 @@ def run_main() -> None: @pytest.mark.filterwarnings( - "ignore:.*strict_exception_groups=False:trio.TrioDeprecationWarning" + "ignore:.*strict_exception_groups=False:trio.TrioDeprecationWarning", ) @pytest.mark.parametrize("strict", [True, False, None]) async def test_nursery_collapse(strict: bool | None) -> None: @@ -2635,7 +2685,7 @@ async def test_cancel_scope_no_cancellederror() -> None: @pytest.mark.filterwarnings( - "ignore:.*strict_exception_groups=False:trio.TrioDeprecationWarning" + "ignore:.*strict_exception_groups=False:trio.TrioDeprecationWarning", ) @pytest.mark.parametrize("run_strict", [False, True]) @pytest.mark.parametrize("start_raiser_strict", [False, True, None]) @@ -2670,7 +2720,7 @@ async def raiser(*, task_status: _core.TaskStatus[None]) -> None: async def start_raiser() -> None: try: async with _core.open_nursery( - strict_exception_groups=start_raiser_strict + strict_exception_groups=start_raiser_strict, ) as nursery: await nursery.start(raiser) except BaseExceptionGroup as exc_group: @@ -2680,7 +2730,8 @@ async def start_raiser() -> None: # exception group raised by trio with a more specific one (subtype, # different message, etc.). raise BaseExceptionGroup( - "start_raiser nursery custom message", exc_group.exceptions + "start_raiser nursery custom message", + exc_group.exceptions, ) from None raise diff --git a/src/trio/_core/_tests/test_unbounded_queue.py b/src/trio/_core/_tests/test_unbounded_queue.py index 33eb41a5b3..e483ecd30a 100644 --- a/src/trio/_core/_tests/test_unbounded_queue.py +++ b/src/trio/_core/_tests/test_unbounded_queue.py @@ -8,7 +8,7 @@ from ...testing import assert_checkpoints, wait_all_tasks_blocked pytestmark = pytest.mark.filterwarnings( - "ignore:.*UnboundedQueue:trio.TrioDeprecationWarning" + "ignore:.*UnboundedQueue:trio.TrioDeprecationWarning", ) diff --git a/src/trio/_core/_tests/test_windows.py b/src/trio/_core/_tests/test_windows.py index 486a405590..e4a1bab615 100644 --- a/src/trio/_core/_tests/test_windows.py +++ b/src/trio/_core/_tests/test_windows.py @@ -54,7 +54,8 @@ def test_winerror(monkeypatch: pytest.MonkeyPatch) -> None: mock.return_value = (12, "test error") with pytest.raises( - OSError, match=r"^\[WinError 12\] test error: 'file_1' -> 'file_2'$" + OSError, + match=r"^\[WinError 12\] test error: 'file_1' -> 'file_2'$", ) as exc: raise_winerror(filename="file_1", filename2="file_2") mock.assert_called_once_with() @@ -66,7 +67,8 @@ def test_winerror(monkeypatch: pytest.MonkeyPatch) -> None: # With an explicit number passed in, it overrides what getwinerror() returns. with pytest.raises( - OSError, match=r"^\[WinError 18\] test error: 'a/file' -> 'b/file'$" + OSError, + match=r"^\[WinError 18\] test error: 'a/file' -> 'b/file'$", ) as exc: raise_winerror(18, filename="a/file", filename2="b/file") mock.assert_called_once_with(18) @@ -117,7 +119,8 @@ async def test_readinto_overlapped() -> None: with tempfile.TemporaryDirectory() as tdir: tfile = os.path.join(tdir, "numbers.txt") with open( # noqa: ASYNC230 # This is a test, synchronous is ok - tfile, "wb" + tfile, + "wb", ) as fp: fp.write(data) fp.flush() @@ -141,7 +144,9 @@ async def test_readinto_overlapped() -> None: async def read_region(start: int, end: int) -> None: await _core.readinto_overlapped( - handle, buffer_view[start:end], start + handle, + buffer_view[start:end], + start, ) _core.register_with_iocp(handle) @@ -184,7 +189,10 @@ async def main() -> None: try: async with _core.open_nursery() as nursery: nursery.start_soon( - _core.readinto_overlapped, read_handle, target, name="xyz" + _core.readinto_overlapped, + read_handle, + target, + name="xyz", ) await wait_all_tasks_blocked() nursery.cancel_scope.cancel() @@ -241,7 +249,9 @@ def test_lsp_that_hooks_select_gives_good_error( from .._windows_cffi import CData, WSAIoctls, _handle def patched_get_underlying( - sock: int | CData, *, which: int = WSAIoctls.SIO_BASE_HANDLE + sock: int | CData, + *, + which: int = WSAIoctls.SIO_BASE_HANDLE, ) -> CData: if hasattr(sock, "fileno"): # pragma: no branch sock = sock.fileno() @@ -252,7 +262,8 @@ def patched_get_underlying( monkeypatch.setattr(_io_windows, "_get_underlying_socket", patched_get_underlying) with pytest.raises( - RuntimeError, match="SIO_BASE_HANDLE and SIO_BSP_HANDLE_SELECT differ" + RuntimeError, + match="SIO_BASE_HANDLE and SIO_BSP_HANDLE_SELECT differ", ): _core.run(sleep, 0) @@ -269,7 +280,9 @@ def test_lsp_that_completely_hides_base_socket_gives_good_error( from .._windows_cffi import CData, WSAIoctls, _handle def patched_get_underlying( - sock: int | CData, *, which: int = WSAIoctls.SIO_BASE_HANDLE + sock: int | CData, + *, + which: int = WSAIoctls.SIO_BASE_HANDLE, ) -> CData: if hasattr(sock, "fileno"): # pragma: no branch sock = sock.fileno() diff --git a/src/trio/_core/_tests/type_tests/nursery_start.py b/src/trio/_core/_tests/type_tests/nursery_start.py index 77667590b9..b286b76ff2 100644 --- a/src/trio/_core/_tests/type_tests/nursery_start.py +++ b/src/trio/_core/_tests/type_tests/nursery_start.py @@ -47,7 +47,6 @@ async def task_requires_start(*, task_status: TaskStatus[str]) -> None: async def task_pos_or_kw(value: str, task_status: TaskStatus[int]) -> None: """Check a function which doesn't use the *-syntax works.""" - ... def check_start_soon(nursery: Nursery) -> None: diff --git a/src/trio/_core/_tests/type_tests/run.py b/src/trio/_core/_tests/type_tests/run.py index c121ce6c7a..6d2124102b 100644 --- a/src/trio/_core/_tests/type_tests/run.py +++ b/src/trio/_core/_tests/type_tests/run.py @@ -29,7 +29,9 @@ async def foo_overloaded(arg: int | str) -> int | str: v = trio.run( - sleep_sort, (1, 3, 5, 2, 4), clock=trio.testing.MockClock(autojump_threshold=0) + sleep_sort, + (1, 3, 5, 2, 4), + clock=trio.testing.MockClock(autojump_threshold=0), ) assert_type(v, "list[float]") trio.run(sleep_sort, ["hi", "there"]) # type: ignore[arg-type] diff --git a/src/trio/_core/_thread_cache.py b/src/trio/_core/_thread_cache.py index d338ec1ee7..c612222697 100644 --- a/src/trio/_core/_thread_cache.py +++ b/src/trio/_core/_thread_cache.py @@ -23,7 +23,9 @@ def _to_os_thread_name(name: str) -> bytes: # called once on import def get_os_thread_name_func() -> Callable[[int | None, str], None] | None: def namefunc( - setname: Callable[[int, bytes], int], ident: int | None, name: str + setname: Callable[[int, bytes], int], + ident: int | None, + name: str, ) -> None: # Thread.ident is None "if it has not been started". Unclear if that can happen # with current usage. @@ -33,7 +35,9 @@ def namefunc( # namefunc on Mac also takes an ident, even if pthread_setname_np doesn't/can't use it # so the caller don't need to care about platform. def darwin_namefunc( - setname: Callable[[bytes], int], ident: int | None, name: str + setname: Callable[[bytes], int], + ident: int | None, + name: str, ) -> None: # I don't know if Mac can rename threads that hasn't been started, but default # to no to be on the safe side. diff --git a/src/trio/_core/_traps.py b/src/trio/_core/_traps.py index 85e6b57306..27518406cb 100644 --- a/src/trio/_core/_traps.py +++ b/src/trio/_core/_traps.py @@ -213,13 +213,13 @@ async def permanently_detach_coroutine_object( """ if _run.current_task().child_nurseries: raise RuntimeError( - "can't permanently detach a coroutine object with open nurseries" + "can't permanently detach a coroutine object with open nurseries", ) return await _async_yield(PermanentlyDetachCoroutineObject(final_outcome)) async def temporarily_detach_coroutine_object( - abort_func: Callable[[RaiseCancelT], Abort] + abort_func: Callable[[RaiseCancelT], Abort], ) -> Any: """Temporarily detach the current coroutine object from the Trio scheduler. diff --git a/src/trio/_core/_unbounded_queue.py b/src/trio/_core/_unbounded_queue.py index b9ebe484d7..b9e7974841 100644 --- a/src/trio/_core/_unbounded_queue.py +++ b/src/trio/_core/_unbounded_queue.py @@ -152,7 +152,8 @@ async def get_batch(self) -> list[T]: def statistics(self) -> UnboundedQueueStatistics: """Return an :class:`UnboundedQueueStatistics` object containing debugging information.""" return UnboundedQueueStatistics( - qsize=len(self._data), tasks_waiting=self._lot.statistics().tasks_waiting + qsize=len(self._data), + tasks_waiting=self._lot.statistics().tasks_waiting, ) def __aiter__(self) -> Self: diff --git a/src/trio/_core/_wakeup_socketpair.py b/src/trio/_core/_wakeup_socketpair.py index fb821a23e7..ea4567017f 100644 --- a/src/trio/_core/_wakeup_socketpair.py +++ b/src/trio/_core/_wakeup_socketpair.py @@ -63,7 +63,7 @@ def wakeup_on_signals(self) -> None: "running Trio in guest mode, then this might mean you " "should set host_uses_signal_set_wakeup_fd=True. " "Otherwise, file a bug on Trio and we'll help you figure " - "out what's going on." + "out what's going on.", ), stacklevel=1, ) diff --git a/src/trio/_core/_windows_cffi.py b/src/trio/_core/_windows_cffi.py index 244ea773c5..453b4beda3 100644 --- a/src/trio/_core/_windows_cffi.py +++ b/src/trio/_core/_windows_cffi.py @@ -210,7 +210,7 @@ # 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" + r"\b(_In_|_Inout_|_Out_|_Outptr_|_Reserved_)(opt_)?\b", ) LIB = REGEX_SAL_ANNOTATION.sub(" ", LIB) @@ -253,7 +253,10 @@ def CreateEventA( ) -> Handle: ... def SetFileCompletionNotificationModes( - self, handle: Handle, flags: CompletionModes, / + self, + handle: Handle, + flags: CompletionModes, + /, ) -> int: ... def PostQueuedCompletionStatus( diff --git a/src/trio/_dtls.py b/src/trio/_dtls.py index 31f7817e1c..62b01292a8 100644 --- a/src/trio/_dtls.py +++ b/src/trio/_dtls.py @@ -353,7 +353,9 @@ class OpaqueHandshakeMessage: _AnyHandshakeMessage: TypeAlias = Union[ - HandshakeMessage, PseudoHandshakeMessage, OpaqueHandshakeMessage + HandshakeMessage, + PseudoHandshakeMessage, + OpaqueHandshakeMessage, ] @@ -378,8 +380,10 @@ def decode_volley_trusted( elif record.content_type in (ContentType.change_cipher_spec, ContentType.alert): messages.append( PseudoHandshakeMessage( - record.version, record.content_type, record.payload - ) + record.version, + record.content_type, + record.payload, + ), ) else: assert record.content_type == ContentType.handshake @@ -565,7 +569,11 @@ def _signable(*fields: bytes) -> bytes: def _make_cookie( - key: bytes, salt: bytes, tick: int, address: Any, client_hello_bits: bytes + key: bytes, + salt: bytes, + tick: int, + address: Any, + client_hello_bits: bytes, ) -> bytes: assert len(salt) == SALT_BYTES assert len(key) == KEY_BYTES @@ -583,7 +591,10 @@ def _make_cookie( def valid_cookie( - key: bytes, cookie: bytes, address: Any, client_hello_bits: bytes + key: bytes, + cookie: bytes, + address: Any, + client_hello_bits: bytes, ) -> bool: if len(cookie) > SALT_BYTES: salt = cookie[:SALT_BYTES] @@ -592,20 +603,28 @@ def valid_cookie( cur_cookie = _make_cookie(key, salt, tick, address, client_hello_bits) old_cookie = _make_cookie( - key, salt, max(tick - 1, 0), address, client_hello_bits + key, + salt, + max(tick - 1, 0), + address, + client_hello_bits, ) # I doubt using a short-circuiting 'or' here would leak any meaningful # information, but why risk it when '|' is just as easy. return hmac.compare_digest(cookie, cur_cookie) | hmac.compare_digest( - cookie, old_cookie + cookie, + old_cookie, ) else: return False def challenge_for( - key: bytes, address: Any, epoch_seqno: int, client_hello_bits: bytes + key: bytes, + address: Any, + epoch_seqno: int, + client_hello_bits: bytes, ) -> bytes: salt = os.urandom(SALT_BYTES) tick = _current_cookie_tick() @@ -641,7 +660,7 @@ def challenge_for( payload = encode_handshake_fragment(hs) packet = encode_record( - Record(ContentType.handshake, ProtocolVersion.DTLS10, epoch_seqno, payload) + Record(ContentType.handshake, ProtocolVersion.DTLS10, epoch_seqno, payload), ) return packet @@ -666,7 +685,9 @@ def _read_loop(read_fn: Callable[[int], bytes]) -> bytes: async def handle_client_hello_untrusted( - endpoint: DTLSEndpoint, address: Any, packet: bytes + endpoint: DTLSEndpoint, + address: Any, + packet: bytes, ) -> None: # it's trivial to write a simple function that directly calls this to # get code coverage, but it should maybe: @@ -686,7 +707,10 @@ async def handle_client_hello_untrusted( if not valid_cookie(endpoint._listening_key, cookie, address, bits): challenge_packet = challenge_for( - endpoint._listening_key, address, epoch_seqno, bits + endpoint._listening_key, + address, + epoch_seqno, + bits, ) try: async with endpoint._send_lock: @@ -746,7 +770,8 @@ async def handle_client_hello_untrusted( async def dtls_receive_loop( - endpoint_ref: ReferenceType[DTLSEndpoint], sock: SocketType + endpoint_ref: ReferenceType[DTLSEndpoint], + sock: SocketType, ) -> None: try: while True: @@ -853,7 +878,7 @@ def __init__( # 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] + SSL.OP_NO_QUERY_MTU | SSL.OP_NO_RENEGOTIATION, # type: ignore[attr-defined] ) self._ssl = SSL.Connection(ctx) self._handshake_mtu = 0 @@ -877,7 +902,7 @@ def _set_replaced(self) -> None: def _check_replaced(self) -> None: if self._replaced: raise trio.BrokenResourceError( - "peer tore down this connection to start a new one" + "peer tore down this connection to start a new one", ) # XX on systems where we can (maybe just Linux?) take advantage of the kernel's PMTU @@ -931,7 +956,8 @@ async def aclose(self) -> None: async def _send_volley(self, volley_messages: list[_AnyHandshakeMessage]) -> None: packets = self._record_encoder.encode_volley( - volley_messages, self._handshake_mtu + volley_messages, + self._handshake_mtu, ) for packet in packets: async with self.endpoint._send_lock: @@ -1066,7 +1092,8 @@ def read_volley() -> list[_AnyHandshakeMessage]: # PMTU estimate is wrong? Let's try dropping it to the minimum # and hope that helps. self._handshake_mtu = min( - self._handshake_mtu, worst_case_mtu(self.endpoint.socket) + self._handshake_mtu, + worst_case_mtu(self.endpoint.socket), ) async def send(self, data: bytes) -> None: @@ -1082,7 +1109,8 @@ async def send(self, data: bytes) -> None: self._ssl.write(data) async with self.endpoint._send_lock: await self.endpoint.socket.sendto( - _read_loop(self._ssl.bio_read), self.peer_address + _read_loop(self._ssl.bio_read), + self.peer_address, ) async def receive(self) -> bytes: @@ -1226,7 +1254,9 @@ def _ensure_receive_loop(self) -> None: # after we send our first packet. if not self._receive_loop_spawned: trio.lowlevel.spawn_system_task( - dtls_receive_loop, weakref.ref(self), self.socket + dtls_receive_loop, + weakref.ref(self), + self.socket, ) self._receive_loop_spawned = True @@ -1318,7 +1348,7 @@ async def handler(dtls_channel): except OSError: # TODO: Write test that triggers this raise RuntimeError( # pragma: no cover - "DTLS socket must be bound before it can serve" + "DTLS socket must be bound before it can serve", ) from None self._ensure_receive_loop() # We do cookie verification ourselves, so tell OpenSSL not to worry about it. diff --git a/src/trio/_file_io.py b/src/trio/_file_io.py index ef867243f0..8038ff6234 100644 --- a/src/trio/_file_io.py +++ b/src/trio/_file_io.py @@ -465,8 +465,16 @@ async def open_file( """ _file = wrap_file( await trio.to_thread.run_sync( - io.open, file, mode, buffering, encoding, errors, newline, closefd, opener - ) + io.open, + file, + mode, + buffering, + encoding, + errors, + newline, + closefd, + opener, + ), ) return _file @@ -495,7 +503,7 @@ def has(attr: str) -> bool: if not (has("close") and (has("read") or has("write"))): raise TypeError( f"{file} does not implement required duck-file methods: " - "close and (read or write)" + "close and (read or write)", ) return AsyncIOWrapper(file) diff --git a/src/trio/_highlevel_open_tcp_listeners.py b/src/trio/_highlevel_open_tcp_listeners.py index 80555be33e..2e71ca5543 100644 --- a/src/trio/_highlevel_open_tcp_listeners.py +++ b/src/trio/_highlevel_open_tcp_listeners.py @@ -112,7 +112,10 @@ async def open_tcp_listeners( computed_backlog = _compute_backlog(backlog) addresses = await tsocket.getaddrinfo( - host, port, type=tsocket.SOCK_STREAM, flags=tsocket.AI_PASSIVE + host, + port, + type=tsocket.SOCK_STREAM, + flags=tsocket.AI_PASSIVE, ) listeners = [] @@ -159,7 +162,8 @@ async def open_tcp_listeners( "socket that that address could use" ) raise OSError(errno.EAFNOSUPPORT, msg) from ExceptionGroup( - msg, unsupported_address_families + msg, + unsupported_address_families, ) return listeners @@ -240,5 +244,8 @@ async def serve_tcp( """ listeners = await trio.open_tcp_listeners(port, host=host, backlog=backlog) await trio.serve_listeners( - handler, listeners, handler_nursery=handler_nursery, task_status=task_status + handler, + listeners, + handler_nursery=handler_nursery, + task_status=task_status, ) diff --git a/src/trio/_highlevel_open_tcp_stream.py b/src/trio/_highlevel_open_tcp_stream.py index d5c83da7c0..1e7f4332d3 100644 --- a/src/trio/_highlevel_open_tcp_stream.py +++ b/src/trio/_highlevel_open_tcp_stream.py @@ -141,7 +141,7 @@ def reorder_for_rfc_6555_section_5_4( str, Any, ] - ] + ], ) -> None: # RFC 6555 section 5.4 says that if getaddrinfo returns multiple address # families (e.g. IPv4 and IPv6), then you should make sure that your first @@ -346,14 +346,16 @@ async def attempt_connect( # better job of it because it knows the remote IP/port. with suppress(OSError, AttributeError): sock.setsockopt( - trio.socket.IPPROTO_IP, trio.socket.IP_BIND_ADDRESS_NO_PORT, 1 + trio.socket.IPPROTO_IP, + trio.socket.IP_BIND_ADDRESS_NO_PORT, + 1, ) try: await sock.bind((local_address, 0)) except OSError: raise OSError( f"local_address={local_address!r} is incompatible " - f"with remote address {sockaddr!r}" + f"with remote address {sockaddr!r}", ) from None await sock.connect(sockaddr) @@ -382,7 +384,9 @@ async def attempt_connect( # 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 + (address_family, socket_type, proto), + addr, + attempt_failed, ) nursery.start_soon( diff --git a/src/trio/_highlevel_serve_listeners.py b/src/trio/_highlevel_serve_listeners.py index ec5a0efb3c..71521546d3 100644 --- a/src/trio/_highlevel_serve_listeners.py +++ b/src/trio/_highlevel_serve_listeners.py @@ -143,5 +143,5 @@ async def serve_listeners( task_status.started(listeners) raise AssertionError( - "_serve_one_listener should never complete" + "_serve_one_listener should never complete", ) # pragma: no cover diff --git a/src/trio/_highlevel_socket.py b/src/trio/_highlevel_socket.py index 901e22f345..8d9e768074 100644 --- a/src/trio/_highlevel_socket.py +++ b/src/trio/_highlevel_socket.py @@ -76,7 +76,7 @@ def __init__(self, socket: SocketType): self.socket = socket self._send_conflict_detector = ConflictDetector( - "another task is currently sending data on this SocketStream" + "another task is currently sending data on this SocketStream", ) # Socket defaults: @@ -167,12 +167,12 @@ def setsockopt( if length is None: if value is None: raise TypeError( - "invalid value for argument 'value', must not be None when specifying length" + "invalid value for argument 'value', must not be None when specifying length", ) return self.socket.setsockopt(level, option, value) if value is not None: raise TypeError( - f"invalid value for argument 'value': {value!r}, must be None when specifying optlen" + f"invalid value for argument 'value': {value!r}, must be None when specifying optlen", ) return self.socket.setsockopt(level, option, value, length) diff --git a/src/trio/_highlevel_ssl_helpers.py b/src/trio/_highlevel_ssl_helpers.py index 03562c9edb..1239491a43 100644 --- a/src/trio/_highlevel_ssl_helpers.py +++ b/src/trio/_highlevel_ssl_helpers.py @@ -59,7 +59,9 @@ async def open_ssl_over_tcp_stream( """ tcp_stream = await trio.open_tcp_stream( - host, port, happy_eyeballs_delay=happy_eyeballs_delay + host, + port, + happy_eyeballs_delay=happy_eyeballs_delay, ) if ssl_context is None: ssl_context = ssl.create_default_context() @@ -68,7 +70,10 @@ async def open_ssl_over_tcp_stream( ssl_context.options &= ~ssl.OP_IGNORE_UNEXPECTED_EOF return trio.SSLStream( - tcp_stream, ssl_context, server_hostname=host, https_compatible=https_compatible + tcp_stream, + ssl_context, + server_hostname=host, + https_compatible=https_compatible, ) @@ -168,5 +173,8 @@ async def serve_ssl_over_tcp( backlog=backlog, ) await trio.serve_listeners( - handler, listeners, handler_nursery=handler_nursery, task_status=task_status + handler, + listeners, + handler_nursery=handler_nursery, + task_status=task_status, ) diff --git a/src/trio/_path.py b/src/trio/_path.py index b9b5749c35..3663831a23 100644 --- a/src/trio/_path.py +++ b/src/trio/_path.py @@ -31,7 +31,7 @@ def _wraps_async( - wrapped: Callable[..., Any] + wrapped: Callable[..., Any], ) -> Callable[[Callable[P, T]], Callable[P, Awaitable[T]]]: def decorator(fn: Callable[P, T]) -> Callable[P, Awaitable[T]]: async def wrapper(*args: P.args, **kwargs: P.kwargs) -> T: diff --git a/src/trio/_signals.py b/src/trio/_signals.py index f4d912808f..63ee176c1b 100644 --- a/src/trio/_signals.py +++ b/src/trio/_signals.py @@ -72,7 +72,7 @@ def __init__(self) -> None: self._pending: OrderedDict[int, None] = OrderedDict() self._lot = trio.lowlevel.ParkingLot() self._conflict_detector = ConflictDetector( - "only one task can iterate on a signal receiver at a time" + "only one task can iterate on a signal receiver at a time", ) self._closed = False @@ -170,7 +170,7 @@ def open_signal_receiver( if not is_main_thread(): raise RuntimeError( "Sorry, open_signal_receiver is only possible when running in " - "Python interpreter's main thread" + "Python interpreter's main thread", ) token = trio.lowlevel.current_trio_token() queue = SignalReceiver() diff --git a/src/trio/_socket.py b/src/trio/_socket.py index 0a3bd1cba1..6dbe9c0c4b 100644 --- a/src/trio/_socket.py +++ b/src/trio/_socket.py @@ -62,7 +62,8 @@ # class _try_sync: def __init__( - self, blocking_exc_override: Callable[[BaseException], bool] | None = None + self, + blocking_exc_override: Callable[[BaseException], bool] | None = None, ): self._blocking_exc_override = blocking_exc_override @@ -179,7 +180,11 @@ async def getaddrinfo( flags: int = 0, ) -> list[ tuple[ - AddressFamily, SocketKind, int, str, tuple[str, int] | tuple[str, int, int, int] + AddressFamily, + SocketKind, + int, + str, + tuple[str, int] | tuple[str, int, int, int], ] ]: """Look up a numeric address given a name. @@ -210,7 +215,12 @@ def numeric_only_failure(exc: BaseException) -> bool: async with _try_sync(numeric_only_failure): return _stdlib_socket.getaddrinfo( - host, port, family, type, proto, flags | _NUMERIC_ONLY + host, + port, + family, + type, + proto, + flags | _NUMERIC_ONLY, ) # That failed; it's a real hostname. We better use a thread. # @@ -245,7 +255,8 @@ def numeric_only_failure(exc: BaseException) -> bool: async def getnameinfo( - sockaddr: tuple[str, int] | tuple[str, int, int, int], flags: int + sockaddr: tuple[str, int] | tuple[str, int, int, int], + flags: int, ) -> tuple[str, str]: """Look up a name given a numeric address. @@ -261,7 +272,10 @@ async def getnameinfo( return await hr.getnameinfo(sockaddr, flags) else: return await trio.to_thread.run_sync( - _stdlib_socket.getnameinfo, sockaddr, flags, abandon_on_cancel=True + _stdlib_socket.getnameinfo, + sockaddr, + flags, + abandon_on_cancel=True, ) @@ -272,7 +286,9 @@ async def getprotobyname(name: str) -> int: """ return await trio.to_thread.run_sync( - _stdlib_socket.getprotobyname, name, abandon_on_cancel=True + _stdlib_socket.getprotobyname, + name, + abandon_on_cancel=True, ) @@ -357,7 +373,10 @@ def socket( return sf.socket(family, type, proto) else: family, type, proto = _sniff_sockopts_for_fileno( # noqa: A001 - family, type, proto, fileno + family, + type, + proto, + fileno, ) stdlib_socket = _stdlib_socket.socket(family, type, proto, fileno) return from_stdlib_socket(stdlib_socket) @@ -465,11 +484,11 @@ async def _resolve_address_nocp( elif family == _stdlib_socket.AF_INET6: if not isinstance(address, tuple) or not 2 <= len(address) <= 4: raise ValueError( - "address should be a (host, port, [flowinfo, [scopeid]]) tuple" + "address should be a (host, port, [flowinfo, [scopeid]]) tuple", ) elif hasattr(_stdlib_socket, "AF_UNIX") and family == _stdlib_socket.AF_UNIX: # unwrap path-likes - assert isinstance(address, (str, bytes)) + assert isinstance(address, (str, bytes, os.PathLike)) return os.fspath(address) else: return address @@ -531,7 +550,7 @@ def __init__(self) -> None: if type(self) is SocketType: raise TypeError( "SocketType is an abstract class; use trio.socket.socket if you " - "want to construct a socket object" + "want to construct a socket object", ) def detach(self) -> int: @@ -553,7 +572,11 @@ def getsockopt(self, /, level: int, optname: int) -> int: ... def getsockopt(self, /, level: int, optname: int, buflen: int) -> bytes: ... def getsockopt( - self, /, level: int, optname: int, buflen: int | None = None + self, + /, + level: int, + optname: int, + buflen: int | None = None, ) -> int | bytes: raise NotImplementedError @@ -562,7 +585,12 @@ def setsockopt(self, /, level: int, optname: int, value: int | Buffer) -> None: @overload def setsockopt( - self, /, level: int, optname: int, value: None, optlen: int + self, + /, + level: int, + optname: int, + value: None, + optlen: int, ) -> None: ... def setsockopt( @@ -653,19 +681,27 @@ def recv(__self, __buflen: int, __flags: int = 0) -> Awaitable[bytes]: raise NotImplementedError def recv_into( - __self, buffer: Buffer, nbytes: int = 0, flags: int = 0 + __self, + buffer: Buffer, + nbytes: int = 0, + flags: int = 0, ) -> Awaitable[int]: raise NotImplementedError # return type of socket.socket.recvfrom in typeshed is tuple[bytes, Any] def recvfrom( - __self, __bufsize: int, __flags: int = 0 + __self, + __bufsize: int, + __flags: int = 0, ) -> Awaitable[tuple[bytes, AddressFormat]]: raise NotImplementedError # return type of socket.socket.recvfrom_into in typeshed is tuple[bytes, Any] def recvfrom_into( - __self, buffer: Buffer, nbytes: int = 0, flags: int = 0 + __self, + buffer: Buffer, + nbytes: int = 0, + flags: int = 0, ) -> Awaitable[tuple[int, AddressFormat]]: raise NotImplementedError @@ -698,7 +734,9 @@ def send(__self, __bytes: Buffer, __flags: int = 0) -> Awaitable[int]: @overload async def sendto( - self, __data: Buffer, __address: tuple[object, ...] | str | Buffer + self, + __data: Buffer, + __address: tuple[object, ...] | str | Buffer, ) -> int: ... @overload @@ -748,7 +786,7 @@ def __init__(self, sock: _stdlib_socket.socket): # For example, ssl.SSLSocket subclasses socket.socket, but we # certainly don't want to blindly wrap one of those. raise TypeError( - f"expected object of type 'socket.socket', not '{type(sock).__name__}'" + f"expected object of type 'socket.socket', not '{type(sock).__name__}'", ) self._sock = sock self._sock.setblocking(False) @@ -778,7 +816,11 @@ def getsockopt(self, /, level: int, optname: int) -> int: ... def getsockopt(self, /, level: int, optname: int, buflen: int) -> bytes: ... def getsockopt( - self, /, level: int, optname: int, buflen: int | None = None + self, + /, + level: int, + optname: int, + buflen: int | None = None, ) -> int | bytes: if buflen is None: return self._sock.getsockopt(level, optname) @@ -789,7 +831,12 @@ def setsockopt(self, /, level: int, optname: int, value: int | Buffer) -> None: @overload def setsockopt( - self, /, level: int, optname: int, value: None, optlen: int + self, + /, + level: int, + optname: int, + value: None, + optlen: int, ) -> None: ... def setsockopt( @@ -803,12 +850,12 @@ def setsockopt( if optlen is None: if value is None: raise TypeError( - "invalid value for argument 'value', must not be None when specifying optlen" + "invalid value for argument 'value', must not be None when specifying optlen", ) return self._sock.setsockopt(level, optname, value) if value is not None: raise TypeError( - f"invalid value for argument 'value': {value!r}, must be None when specifying optlen" + f"invalid value for argument 'value': {value!r}, must be None when specifying optlen", ) # Note: PyPy may crash here due to setsockopt only supporting @@ -915,7 +962,8 @@ async def _resolve_address_nocp( ) -> AddressFormat: if self.family == _stdlib_socket.AF_INET6: ipv6_v6only = self._sock.getsockopt( - _stdlib_socket.IPPROTO_IPV6, _stdlib_socket.IPV6_V6ONLY + _stdlib_socket.IPPROTO_IPV6, + _stdlib_socket.IPV6_V6ONLY, ) else: ipv6_v6only = False @@ -977,7 +1025,8 @@ async def _nonblocking_helper( ################################################################ _accept = _make_simple_sock_method_wrapper( - _stdlib_socket.socket.accept, _core.wait_readable + _stdlib_socket.socket.accept, + _core.wait_readable, ) async def accept(self) -> tuple[SocketType, AddressFormat]: @@ -1075,7 +1124,8 @@ def recv(__self, __buflen: int, __flags: int = 0) -> Awaitable[bytes]: ... # this requires that we refrain from using `/` to specify pos-only # args, or mypy thinks the signature differs from typeshed. recv = _make_simple_sock_method_wrapper( - _stdlib_socket.socket.recv, _core.wait_readable + _stdlib_socket.socket.recv, + _core.wait_readable, ) ################################################################ @@ -1085,11 +1135,15 @@ def recv(__self, __buflen: int, __flags: int = 0) -> Awaitable[bytes]: ... if TYPE_CHECKING: def recv_into( - __self, buffer: Buffer, nbytes: int = 0, flags: int = 0 + __self, + buffer: Buffer, + nbytes: int = 0, + flags: int = 0, ) -> Awaitable[int]: ... recv_into = _make_simple_sock_method_wrapper( - _stdlib_socket.socket.recv_into, _core.wait_readable + _stdlib_socket.socket.recv_into, + _core.wait_readable, ) ################################################################ @@ -1099,11 +1153,14 @@ def recv_into( if TYPE_CHECKING: # return type of socket.socket.recvfrom in typeshed is tuple[bytes, Any] def recvfrom( - __self, __bufsize: int, __flags: int = 0 + __self, + __bufsize: int, + __flags: int = 0, ) -> Awaitable[tuple[bytes, AddressFormat]]: ... recvfrom = _make_simple_sock_method_wrapper( - _stdlib_socket.socket.recvfrom, _core.wait_readable + _stdlib_socket.socket.recvfrom, + _core.wait_readable, ) ################################################################ @@ -1113,11 +1170,15 @@ def recvfrom( if TYPE_CHECKING: # return type of socket.socket.recvfrom_into in typeshed is tuple[bytes, Any] def recvfrom_into( - __self, buffer: Buffer, nbytes: int = 0, flags: int = 0 + __self, + buffer: Buffer, + nbytes: int = 0, + flags: int = 0, ) -> Awaitable[tuple[int, AddressFormat]]: ... recvfrom_into = _make_simple_sock_method_wrapper( - _stdlib_socket.socket.recvfrom_into, _core.wait_readable + _stdlib_socket.socket.recvfrom_into, + _core.wait_readable, ) ################################################################ @@ -1130,11 +1191,16 @@ def recvfrom_into( if TYPE_CHECKING: def recvmsg( - __self, __bufsize: int, __ancbufsize: int = 0, __flags: int = 0 + __self, + __bufsize: int, + __ancbufsize: int = 0, + __flags: int = 0, ) -> Awaitable[tuple[bytes, list[tuple[int, int, bytes]], int, Any]]: ... recvmsg = _make_simple_sock_method_wrapper( - _stdlib_socket.socket.recvmsg, _core.wait_readable, maybe_avail=True + _stdlib_socket.socket.recvmsg, + _core.wait_readable, + maybe_avail=True, ) ################################################################ @@ -1154,7 +1220,9 @@ def recvmsg_into( ) -> Awaitable[tuple[int, list[tuple[int, int, bytes]], int, Any]]: ... recvmsg_into = _make_simple_sock_method_wrapper( - _stdlib_socket.socket.recvmsg_into, _core.wait_readable, maybe_avail=True + _stdlib_socket.socket.recvmsg_into, + _core.wait_readable, + maybe_avail=True, ) ################################################################ @@ -1166,7 +1234,8 @@ def recvmsg_into( def send(__self, __bytes: Buffer, __flags: int = 0) -> Awaitable[int]: ... send = _make_simple_sock_method_wrapper( - _stdlib_socket.socket.send, _core.wait_writable + _stdlib_socket.socket.send, + _core.wait_writable, ) ################################################################ @@ -1175,12 +1244,17 @@ def send(__self, __bytes: Buffer, __flags: int = 0) -> Awaitable[int]: ... @overload async def sendto( - self, __data: Buffer, __address: tuple[object, ...] | str | Buffer + self, + __data: Buffer, + __address: tuple[object, ...] | str | Buffer, ) -> int: ... @overload async def sendto( - self, __data: Buffer, __flags: int, __address: tuple[object, ...] | str | Buffer + self, + __data: Buffer, + __flags: int, + __address: tuple[object, ...] | str | Buffer, ) -> int: ... @_wraps(_stdlib_socket.socket.sendto, assigned=(), updated=()) # type: ignore[misc] diff --git a/src/trio/_ssl.py b/src/trio/_ssl.py index 5bc37cf7dc..df1cbc37bc 100644 --- a/src/trio/_ssl.py +++ b/src/trio/_ssl.py @@ -376,10 +376,10 @@ def __init__( # multiple concurrent calls to send_all/wait_send_all_might_not_block # or to receive_some. self._outer_send_conflict_detector = ConflictDetector( - "another task is currently sending data on this SSLStream" + "another task is currently sending data on this SSLStream", ) self._outer_recv_conflict_detector = ConflictDetector( - "another task is currently receiving data on this SSLStream" + "another task is currently receiving data on this SSLStream", ) self._estimated_receive_size = STARTING_RECEIVE_SIZE @@ -615,7 +615,8 @@ async def _retry( self._incoming.write_eof() else: self._estimated_receive_size = max( - self._estimated_receive_size, len(data) + self._estimated_receive_size, + len(data), ) self._incoming.write(data) self._inner_recv_count += 1 @@ -893,6 +894,10 @@ async def wait_send_all_might_not_block(self) -> None: await self.transport_stream.wait_send_all_might_not_block() +# this is necessary for Sphinx, see also `_abc.py` +SSLStream.__module__ = SSLStream.__module__.replace("._ssl", "") + + @final class SSLListener(Listener[SSLStream[T_Stream]]): """A :class:`~trio.abc.Listener` for SSL/TLS-encrypted servers. diff --git a/src/trio/_subprocess.py b/src/trio/_subprocess.py index 553e3d4885..263225ffca 100644 --- a/src/trio/_subprocess.py +++ b/src/trio/_subprocess.py @@ -36,7 +36,10 @@ StrOrBytesPath: TypeAlias = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] else: StrOrBytesPath: TypeAlias = Union[ - str, bytes, "os.PathLike[str]", "os.PathLike[bytes]" + str, + bytes, + "os.PathLike[str]", + "os.PathLike[bytes]", ] @@ -238,7 +241,7 @@ async def wait(self) -> int: if self.poll() is None: if self._pidfd is not None: with contextlib.suppress( - ClosedResourceError + ClosedResourceError, ): # something else (probably a call to poll) already closed the pidfd await trio.lowlevel.wait_readable(self._pidfd.fileno()) else: @@ -301,7 +304,7 @@ def kill(self) -> None: async def _open_process( - command: list[str] | str, + command: StrOrBytesPath | Sequence[StrOrBytesPath], *, stdin: int | HasFileno | None = None, stdout: int | HasFileno | None = None, @@ -326,13 +329,14 @@ async def _open_process( want. Args: - command (list or str): The command to run. Typically this is a - sequence of strings such as ``['ls', '-l', 'directory with spaces']``, - where the first element names the executable to invoke and the other - elements specify its arguments. With ``shell=True`` in the - ``**options``, or on Windows, ``command`` may alternatively - be a string, which will be parsed following platform-dependent - :ref:`quoting rules `. + command: The command to run. Typically this is a sequence of strings or + bytes such as ``['ls', '-l', 'directory with spaces']``, where the + first element names the executable to invoke and the other elements + specify its arguments. With ``shell=True`` in the ``**options``, or on + Windows, ``command`` can be a string or bytes, which will be parsed + following platform-dependent :ref:`quoting rules + `. In all cases ``command`` can be a path or a + sequence of paths. stdin: Specifies what the child process's standard input stream should connect to: output written by the parent (``subprocess.PIPE``), nothing (``subprocess.DEVNULL``), @@ -362,19 +366,20 @@ async def _open_process( if options.get(key): raise TypeError( "trio.Process only supports communicating over " - f"unbuffered byte streams; the '{key}' option is not supported" + f"unbuffered byte streams; the '{key}' option is not supported", ) if os.name == "posix": - if isinstance(command, str) and not options.get("shell"): + # TODO: how do paths and sequences thereof play with `shell=True`? + if isinstance(command, (str, bytes)) and not options.get("shell"): raise TypeError( - "command must be a sequence (not a string) if shell=False " - "on UNIX systems" + "command must be a sequence (not a string or bytes) if " + "shell=False on UNIX systems", ) - if not isinstance(command, str) and options.get("shell"): + if not isinstance(command, (str, bytes)) and options.get("shell"): raise TypeError( - "command must be a string (not a sequence) if shell=True " - "on UNIX systems" + "command must be a string or bytes (not a sequence) if " + "shell=True on UNIX systems", ) trio_stdin: ClosableSendStream | None = None @@ -417,7 +422,7 @@ async def _open_process( stdout=stdout, stderr=stderr, **options, - ) + ), ) # We did not fail, so dismiss the stack for the trio ends cleanup_on_fail.pop_all() @@ -443,7 +448,7 @@ async def _posix_deliver_cancel(p: Process) -> None: RuntimeWarning( f"process {p!r} ignored SIGTERM for 5 seconds. " "(Maybe you should pass a custom deliver_cancel?) " - "Trying SIGKILL." + "Trying SIGKILL.", ), stacklevel=1, ) @@ -671,17 +676,17 @@ async def my_deliver_cancel(process): raise ValueError( "stdout=subprocess.PIPE is only valid with nursery.start, " "since that's the only way to access the pipe; use nursery.start " - "or pass the data you want to write directly" + "or pass the data you want to write directly", ) if options.get("stdout") is subprocess.PIPE: raise ValueError( "stdout=subprocess.PIPE is only valid with nursery.start, " - "since that's the only way to access the pipe" + "since that's the only way to access the pipe", ) if options.get("stderr") is subprocess.PIPE: raise ValueError( "stderr=subprocess.PIPE is only valid with nursery.start, " - "since that's the only way to access the pipe" + "since that's the only way to access the pipe", ) if isinstance(stdin, (bytes, bytearray, memoryview)): input_ = stdin @@ -768,7 +773,10 @@ async def killer() -> None: if proc.returncode and check: raise subprocess.CalledProcessError( - proc.returncode, proc.args, output=stdout, stderr=stderr + proc.returncode, + proc.args, + output=stdout, + stderr=stderr, ) else: assert proc.returncode is not None diff --git a/src/trio/_subprocess_platform/kqueue.py b/src/trio/_subprocess_platform/kqueue.py index fcf72650ee..2283bb5360 100644 --- a/src/trio/_subprocess_platform/kqueue.py +++ b/src/trio/_subprocess_platform/kqueue.py @@ -21,7 +21,10 @@ async def wait_child_exiting(process: _subprocess.Process) -> None: def make_event(flags: int) -> select.kevent: return select.kevent( - process.pid, filter=select.KQ_FILTER_PROC, flags=flags, fflags=KQ_NOTE_EXIT + process.pid, + filter=select.KQ_FILTER_PROC, + flags=flags, + fflags=KQ_NOTE_EXIT, ) try: diff --git a/src/trio/_subprocess_platform/waitid.py b/src/trio/_subprocess_platform/waitid.py index 44c8261074..ebf83b4802 100644 --- a/src/trio/_subprocess_platform/waitid.py +++ b/src/trio/_subprocess_platform/waitid.py @@ -40,7 +40,7 @@ def sync_wait_reapable(pid: int) -> None: int pad[26]; } siginfo_t; int waitid(int idtype, int id, siginfo_t* result, int options); -""" +""", ) waitid_cffi = waitid_ffi.dlopen(None).waitid # type: ignore[attr-defined] @@ -79,7 +79,10 @@ async def _waitid_system_task(pid: int, event: Event) -> None: try: await to_thread_run_sync( - sync_wait_reapable, pid, abandon_on_cancel=True, limiter=waitid_limiter + sync_wait_reapable, + pid, + abandon_on_cancel=True, + limiter=waitid_limiter, ) except OSError: # If waitid fails, waitpid will fail too, so it still makes diff --git a/src/trio/_sync.py b/src/trio/_sync.py index 9f498e82a3..03d518aab9 100644 --- a/src/trio/_sync.py +++ b/src/trio/_sync.py @@ -8,7 +8,14 @@ import trio from . import _core -from ._core import Abort, ParkingLot, RaiseCancelT, enable_ki_protection +from ._core import ( + Abort, + ParkingLot, + RaiseCancelT, + add_parking_lot_breaker, + enable_ki_protection, + remove_parking_lot_breaker, +) from ._util import final if TYPE_CHECKING: @@ -297,7 +304,7 @@ def acquire_on_behalf_of_nowait(self, borrower: Task | object) -> None: """ if borrower in self._borrowers: raise RuntimeError( - "this borrower is already holding one of this CapacityLimiter's tokens" + "this borrower is already holding one of this CapacityLimiter's tokens", ) if len(self._borrowers) < self._total_tokens and not self._lot: self._borrowers.add(borrower) @@ -366,7 +373,7 @@ def release_on_behalf_of(self, borrower: Task | object) -> None: """ if borrower not in self._borrowers: raise RuntimeError( - "this borrower isn't holding any of this CapacityLimiter's tokens" + "this borrower isn't holding any of this CapacityLimiter's tokens", ) self._borrowers.remove(borrower) self._wake_waiters() @@ -576,20 +583,30 @@ def acquire_nowait(self) -> None: elif self._owner is None and not self._lot: # No-one owns it self._owner = task + add_parking_lot_breaker(task, self._lot) else: raise trio.WouldBlock @enable_ki_protection async def acquire(self) -> None: - """Acquire the lock, blocking if necessary.""" + """Acquire the lock, blocking if necessary. + + Raises: + BrokenResourceError: if the owner of the lock exits without releasing. + """ await trio.lowlevel.checkpoint_if_cancelled() try: self.acquire_nowait() except trio.WouldBlock: - # NOTE: it's important that the contended acquire path is just - # "_lot.park()", because that's how Condition.wait() acquires the - # lock as well. - await self._lot.park() + try: + # NOTE: it's important that the contended acquire path is just + # "_lot.park()", because that's how Condition.wait() acquires the + # lock as well. + await self._lot.park() + except trio.BrokenResourceError: + raise trio.BrokenResourceError( + f"Owner of this lock exited without releasing: {self._owner}", + ) from None else: await trio.lowlevel.cancel_shielded_checkpoint() @@ -604,8 +621,10 @@ def release(self) -> None: task = trio.lowlevel.current_task() if task is not self._owner: raise RuntimeError("can't release a Lock you don't own") + remove_parking_lot_breaker(self._owner, self._lot) if self._lot: (self._owner,) = self._lot.unpark(count=1) + add_parking_lot_breaker(self._owner, self._lot) else: self._owner = None @@ -622,7 +641,9 @@ def statistics(self) -> LockStatistics: """ return LockStatistics( - locked=self.locked(), owner=self._owner, tasks_waiting=len(self._lot) + locked=self.locked(), + owner=self._owner, + tasks_waiting=len(self._lot), ) @@ -765,7 +786,11 @@ def acquire_nowait(self) -> None: return self._lock.acquire_nowait() async def acquire(self) -> None: - """Acquire the underlying lock, blocking if necessary.""" + """Acquire the underlying lock, blocking if necessary. + + Raises: + BrokenResourceError: if the owner of the underlying lock exits without releasing. + """ await self._lock.acquire() def release(self) -> None: @@ -794,6 +819,7 @@ async def wait(self) -> None: Raises: RuntimeError: if the calling task does not hold the lock. + BrokenResourceError: if the owner of the lock exits without releasing, when attempting to re-acquire. """ if trio.lowlevel.current_task() is not self._lock._owner: @@ -845,5 +871,6 @@ def statistics(self) -> ConditionStatistics: """ return ConditionStatistics( - tasks_waiting=len(self._lot), lock_statistics=self._lock.statistics() + tasks_waiting=len(self._lot), + lock_statistics=self._lock.statistics(), ) diff --git a/src/trio/_tests/_check_type_completeness.json b/src/trio/_tests/_check_type_completeness.json index 0bbd47fada..badb7cba17 100644 --- a/src/trio/_tests/_check_type_completeness.json +++ b/src/trio/_tests/_check_type_completeness.json @@ -20,13 +20,10 @@ "all": [ "No docstring found for class \"trio.MemoryReceiveChannel\"", "No docstring found for class \"trio._channel.MemoryReceiveChannel\"", - "No docstring found for function \"trio._channel.MemoryReceiveChannel.statistics\"", - "No docstring found for class \"trio._channel.MemoryChannelStats\"", - "No docstring found for function \"trio._channel.MemoryReceiveChannel.aclose\"", + "No docstring found for class \"trio.MemoryChannelStatistics\"", + "No docstring found for class \"trio._channel.MemoryChannelStatistics\"", "No docstring found for class \"trio.MemorySendChannel\"", "No docstring found for class \"trio._channel.MemorySendChannel\"", - "No docstring found for function \"trio._channel.MemorySendChannel.statistics\"", - "No docstring found for function \"trio._channel.MemorySendChannel.aclose\"", "No docstring found for class \"trio._core._run.Task\"", "No docstring found for class \"trio._socket.SocketType\"", "No docstring found for function \"trio._highlevel_socket.SocketStream.send_all\"", diff --git a/src/trio/_tests/check_type_completeness.py b/src/trio/_tests/check_type_completeness.py index fa6ace074f..48e4df0bd9 100755 --- a/src/trio/_tests/check_type_completeness.py +++ b/src/trio/_tests/check_type_completeness.py @@ -111,7 +111,9 @@ def has_docstring_at_runtime(name: str) -> bool: def check_type( - platform: str, full_diagnostics_file: Path | None, expected_errors: list[object] + platform: str, + full_diagnostics_file: Path | None, + expected_errors: list[object], ) -> list[object]: # convince isort we use the trio import assert trio @@ -144,7 +146,7 @@ def check_type( if message.startswith("No docstring found for"): continue if message.startswith( - "Type is missing type annotation and could be inferred differently by type checkers" + "Type is missing type annotation and could be inferred differently by type checkers", ): continue diff --git a/src/trio/_tests/module_with_deprecations.py b/src/trio/_tests/module_with_deprecations.py index 73184d11e8..afe4187191 100644 --- a/src/trio/_tests/module_with_deprecations.py +++ b/src/trio/_tests/module_with_deprecations.py @@ -16,6 +16,9 @@ __deprecated_attributes__ = { "dep1": _deprecate.DeprecatedAttribute("value1", "1.1", issue=1), "dep2": _deprecate.DeprecatedAttribute( - "value2", "1.2", issue=1, instead="instead-string" + "value2", + "1.2", + issue=1, + instead="instead-string", ), } diff --git a/src/trio/_tests/test_channel.py b/src/trio/_tests/test_channel.py index 1271f6b765..07cc84a559 100644 --- a/src/trio/_tests/test_channel.py +++ b/src/trio/_tests/test_channel.py @@ -107,7 +107,8 @@ async def consumer(receive_channel: trio.MemoryReceiveChannel[int], i: int) -> N async def test_close_basics() -> None: async def send_block( - s: trio.MemorySendChannel[None], expect: type[BaseException] + s: trio.MemorySendChannel[None], + expect: type[BaseException], ) -> None: with pytest.raises(expect): await s.send(None) @@ -164,7 +165,8 @@ async def receive_block(r: trio.MemoryReceiveChannel[int]) -> None: async def test_close_sync() -> None: async def send_block( - s: trio.MemorySendChannel[None], expect: type[BaseException] + s: trio.MemorySendChannel[None], + expect: type[BaseException], ) -> None: with pytest.raises(expect): await s.send(None) diff --git a/src/trio/_tests/test_contextvars.py b/src/trio/_tests/test_contextvars.py index ae0c25f876..63965e1e12 100644 --- a/src/trio/_tests/test_contextvars.py +++ b/src/trio/_tests/test_contextvars.py @@ -5,7 +5,7 @@ from .. import _core trio_testing_contextvar: contextvars.ContextVar[str] = contextvars.ContextVar( - "trio_testing_contextvar" + "trio_testing_contextvar", ) diff --git a/src/trio/_tests/test_deprecate.py b/src/trio/_tests/test_deprecate.py index fa5d7cbfef..1da1549d38 100644 --- a/src/trio/_tests/test_deprecate.py +++ b/src/trio/_tests/test_deprecate.py @@ -161,7 +161,10 @@ def new_hotness_method(self) -> str: return "new hotness method" old_hotness_method = deprecated_alias( - "Alias.old_hotness_method", new_hotness_method, "3.21", issue=1 + "Alias.old_hotness_method", + new_hotness_method, + "3.21", + issue=1, ) @@ -272,5 +275,9 @@ def test_warning_class() -> None: with pytest.warns(TrioDeprecationWarning): warn_deprecated( - "foo", "bar", issue=None, instead=None, use_triodeprecationwarning=True + "foo", + "bar", + issue=None, + instead=None, + use_triodeprecationwarning=True, ) diff --git a/src/trio/_tests/test_deprecate_strict_exception_groups_false.py b/src/trio/_tests/test_deprecate_strict_exception_groups_false.py index 317672bf23..ccf34d72ed 100644 --- a/src/trio/_tests/test_deprecate_strict_exception_groups_false.py +++ b/src/trio/_tests/test_deprecate_strict_exception_groups_false.py @@ -7,7 +7,8 @@ async def test_deprecation_warning_open_nursery() -> None: with pytest.warns( - trio.TrioDeprecationWarning, match="strict_exception_groups=False" + trio.TrioDeprecationWarning, + match="strict_exception_groups=False", ) as record: async with trio.open_nursery(strict_exception_groups=False): ... @@ -33,7 +34,8 @@ async def foo_loose_nursery() -> None: def helper(fun: Callable[..., Awaitable[None]], num: int) -> None: with pytest.warns( - trio.TrioDeprecationWarning, match="strict_exception_groups=False" + trio.TrioDeprecationWarning, + match="strict_exception_groups=False", ) as record: trio.run(fun, strict_exception_groups=False) assert len(record) == num @@ -52,7 +54,8 @@ async def trio_return(in_host: object) -> str: return "ok" with pytest.warns( - trio.TrioDeprecationWarning, match="strict_exception_groups=False" + trio.TrioDeprecationWarning, + match="strict_exception_groups=False", ) as record: trivial_guest_run( trio_return, diff --git a/src/trio/_tests/test_dtls.py b/src/trio/_tests/test_dtls.py index d14edae25c..43ad273e57 100644 --- a/src/trio/_tests/test_dtls.py +++ b/src/trio/_tests/test_dtls.py @@ -38,7 +38,9 @@ parametrize_ipv6 = pytest.mark.parametrize( - "ipv6", [False, pytest.param(True, marks=binds_ipv6)], ids=["ipv4", "ipv6"] + "ipv6", + [False, pytest.param(True, marks=binds_ipv6)], + ids=["ipv4", "ipv6"], ) @@ -51,7 +53,10 @@ def endpoint(**kwargs: int | bool) -> DTLSEndpoint: @asynccontextmanager async def dtls_echo_server( - *, autocancel: bool = True, mtu: int | None = None, ipv6: bool = False + *, + autocancel: bool = True, + mtu: int | None = None, + ipv6: bool = False, ) -> AsyncGenerator[tuple[DTLSEndpoint, tuple[str, int]], None]: with endpoint(ipv6=ipv6) as server: localhost = "::1" if ipv6 else "127.0.0.1" @@ -62,7 +67,7 @@ async def echo_handler(dtls_channel: DTLSChannel) -> None: print( "echo handler started: " f"server {dtls_channel.endpoint.socket.getsockname()!r} " - f"client {dtls_channel.peer_address!r}" + f"client {dtls_channel.peer_address!r}", ) if mtu is not None: dtls_channel.set_ciphertext_mtu(mtu) @@ -99,7 +104,8 @@ async def test_smoke(ipv6: bool) -> None: assert await client_channel.receive() == b"goodbye" with pytest.raises( - ValueError, match="^openssl doesn't support sending empty DTLS packets$" + ValueError, + match="^openssl doesn't support sending empty DTLS packets$", ): await client_channel.send(b"") @@ -165,7 +171,7 @@ async def route_packet(packet: UDPPacket) -> None: assert op == "deliver" print( f"{packet.source} -> {packet.destination}: delivered" - f" {packet.payload.hex()}" + f" {packet.payload.hex()}", ) fn.deliver_packet(packet) break @@ -225,7 +231,8 @@ async def handler(channel: DTLSChannel) -> None: await server_nursery.start(server_endpoint.serve, server_ctx, handler) client = client_endpoint.connect( - server_endpoint.socket.getsockname(), client_ctx + server_endpoint.socket.getsockname(), + client_ctx, ) async with trio.open_nursery() as nursery: nursery.start_soon(client.send, b"from client") @@ -372,9 +379,9 @@ async def test_server_socket_doesnt_crash_on_garbage( frag_offset=0, frag_len=10, frag=bytes(10), - ) + ), ), - ) + ), ) client_hello_extended = client_hello + b"\x00" @@ -397,9 +404,9 @@ async def test_server_socket_doesnt_crash_on_garbage( frag_offset=0, frag_len=10, frag=bytes(10), - ) + ), ), - ) + ), ) client_hello_trailing_data_in_record = encode_record( @@ -415,10 +422,10 @@ async def test_server_socket_doesnt_crash_on_garbage( frag_offset=0, frag_len=10, frag=bytes(10), - ) + ), ) + b"\x00", - ) + ), ) handshake_empty = encode_record( @@ -427,7 +434,7 @@ async def test_server_socket_doesnt_crash_on_garbage( version=ProtocolVersion.DTLS10, epoch_seqno=0, payload=b"", - ) + ), ) client_hello_truncated_in_cookie = encode_record( @@ -436,7 +443,7 @@ async def test_server_socket_doesnt_crash_on_garbage( version=ProtocolVersion.DTLS10, epoch_seqno=0, payload=bytes(2 + 32 + 1) + b"\xff", - ) + ), ) async with dtls_echo_server() as (_, address): @@ -620,7 +627,8 @@ async def connecter() -> None: # notices the timeout has expired blackholed = False await server_endpoint.socket.sendto( - b"xxx", client_endpoint.socket.getsockname() + b"xxx", + client_endpoint.socket.getsockname(), ) # now the client task should finish connecting and exit cleanly @@ -682,7 +690,8 @@ def route_packet(packet: UDPPacket) -> None: @parametrize_ipv6 async def test_handshake_handles_minimum_network_mtu( - ipv6: bool, autojump_clock: trio.abc.Clock + ipv6: bool, + autojump_clock: trio.abc.Clock, ) -> None: # Fake network that has the minimum allowable MTU for whatever protocol we're using. fn = FakeNet() diff --git a/src/trio/_tests/test_exports.py b/src/trio/_tests/test_exports.py index 32a2666e48..a463b778f9 100644 --- a/src/trio/_tests/test_exports.py +++ b/src/trio/_tests/test_exports.py @@ -55,7 +55,7 @@ def _ensure_mypy_cache_updated() -> None: "--no-error-summary", "-c", "import trio", - ] + ], ) assert not result[1] # stderr assert not result[0] # stdout @@ -72,7 +72,8 @@ def test_core_is_properly_reexported() -> None: found = 0 for source in sources: if symbol in dir(source) and getattr(source, symbol) is getattr( - _core, symbol + _core, + symbol, ): found += 1 print(symbol, found) @@ -172,8 +173,6 @@ def no_underscores(symbols: Iterable[str]) -> set[str]: elif tool == "mypy": if not RUN_SLOW: # pragma: no cover pytest.skip("use --run-slow to check against mypy") - if sys.implementation.name != "cpython": - pytest.skip("mypy not installed in tests on pypy") cache = Path.cwd() / ".mypy_cache" @@ -254,7 +253,9 @@ def no_underscores(symbols: Iterable[str]) -> set[str]: @pytest.mark.parametrize("module_name", PUBLIC_MODULE_NAMES) @pytest.mark.parametrize("tool", ["jedi", "mypy"]) def test_static_tool_sees_class_members( - tool: str, module_name: str, tmp_path: Path + tool: str, + module_name: str, + tmp_path: Path, ) -> None: module = PUBLIC_MODULES[PUBLIC_MODULE_NAMES.index(module_name)] @@ -266,10 +267,10 @@ def no_hidden(symbols: Iterable[str]) -> set[str]: if (not symbol.startswith("_")) or symbol.startswith("__") } - if tool == "mypy": - if sys.implementation.name != "cpython": - pytest.skip("mypy not installed in tests on pypy") + if tool == "jedi" and sys.implementation.name != "cpython": + pytest.skip("jedi does not support pypy") + if tool == "mypy": cache = Path.cwd() / ".mypy_cache" _ensure_mypy_cache_updated() @@ -376,7 +377,7 @@ def lookup_symbol(symbol: str) -> dict[str, str]: skip_if_optional_else_raise(error) script = jedi.Script( - f"from {module_name} import {class_name}; {class_name}." + f"from {module_name} import {class_name}; {class_name}.", ) completions = script.complete() static_names = no_hidden(c.name for c in completions) - ignore_names diff --git a/src/trio/_tests/test_fakenet.py b/src/trio/_tests/test_fakenet.py index bde6db0191..487b86514e 100644 --- a/src/trio/_tests/test_fakenet.py +++ b/src/trio/_tests/test_fakenet.py @@ -34,14 +34,16 @@ async def test_basic_udp() -> None: assert port != 0 with pytest.raises( - OSError, match=r"^\[\w+ \d+\] Invalid argument$" + OSError, + match=r"^\[\w+ \d+\] Invalid argument$", ) as exc: # Cannot rebind. await s1.bind(("192.0.2.1", 0)) assert exc.value.errno == errno.EINVAL # Cannot bind multiple sockets to the same address with pytest.raises( - OSError, match=r"^\[\w+ \d+\] (Address (already )?in use|Unknown error)$" + OSError, + match=r"^\[\w+ \d+\] (Address (already )?in use|Unknown error)$", ) as exc: await s2.bind(("127.0.0.1", port)) assert exc.value.errno == errno.EADDRINUSE @@ -131,7 +133,8 @@ async def test_recv_methods() -> None: @pytest.mark.skipif( - sys.platform == "win32", reason="functions not in socket on windows" + sys.platform == "win32", + reason="functions not in socket on windows", ) async def test_nonwindows_functionality() -> None: # mypy doesn't support a good way of aborting typechecking on different platforms @@ -180,13 +183,15 @@ async def test_nonwindows_functionality() -> None: assert addr == s1.getsockname() with pytest.raises( - AttributeError, match="^'FakeSocket' object has no attribute 'share'$" + AttributeError, + match="^'FakeSocket' object has no attribute 'share'$", ): await s1.share(0) # type: ignore[attr-defined] @pytest.mark.skipif( - sys.platform != "win32", reason="windows-specific fakesocket testing" + sys.platform != "win32", + reason="windows-specific fakesocket testing", ) async def test_windows_functionality() -> None: # mypy doesn't support a good way of aborting typechecking on different platforms @@ -196,11 +201,13 @@ async def test_windows_functionality() -> None: s2 = trio.socket.socket(type=trio.socket.SOCK_DGRAM) await s1.bind(("127.0.0.1", 0)) with pytest.raises( - AttributeError, match="^'FakeSocket' object has no attribute 'sendmsg'$" + AttributeError, + match="^'FakeSocket' object has no attribute 'sendmsg'$", ): await s1.sendmsg([b"jkl"], (), 0, s2.getsockname()) # type: ignore[attr-defined] with pytest.raises( - AttributeError, match="^'FakeSocket' object has no attribute 'recvmsg'$" + AttributeError, + match="^'FakeSocket' object has no attribute 'recvmsg'$", ): s2.recvmsg(0) # type: ignore[attr-defined] with pytest.raises( @@ -224,28 +231,33 @@ async def test_not_implemented_functions() -> None: # getsockopt with pytest.raises( - OSError, match=r"^FakeNet doesn't implement getsockopt\(\d, \d\)$" + OSError, + match=r"^FakeNet doesn't implement getsockopt\(\d, \d\)$", ): s1.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY) # setsockopt with pytest.raises( - NotImplementedError, match="^FakeNet always has IPV6_V6ONLY=True$" + NotImplementedError, + match="^FakeNet always has IPV6_V6ONLY=True$", ): s1.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, False) with pytest.raises( - OSError, match=r"^FakeNet doesn't implement setsockopt\(\d+, \d+, \.\.\.\)$" + OSError, + match=r"^FakeNet doesn't implement setsockopt\(\d+, \d+, \.\.\.\)$", ): s1.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, True) with pytest.raises( - OSError, match=r"^FakeNet doesn't implement setsockopt\(\d+, \d+, \.\.\.\)$" + OSError, + match=r"^FakeNet doesn't implement setsockopt\(\d+, \d+, \.\.\.\)$", ): s1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # set_inheritable s1.set_inheritable(False) with pytest.raises( - NotImplementedError, match="^FakeNet can't make inheritable sockets$" + NotImplementedError, + match="^FakeNet can't make inheritable sockets$", ): s1.set_inheritable(True) @@ -274,7 +286,7 @@ async def test_init() -> None: with pytest.raises( NotImplementedError, match=re.escape( - f"FakeNet doesn't (yet) support type={trio.socket.SOCK_STREAM}" + f"FakeNet doesn't (yet) support type={trio.socket.SOCK_STREAM}", ), ): s1 = trio.socket.socket() diff --git a/src/trio/_tests/test_file_io.py b/src/trio/_tests/test_file_io.py index cd02d4f768..138417ef40 100644 --- a/src/trio/_tests/test_file_io.py +++ b/src/trio/_tests/test_file_io.py @@ -59,13 +59,15 @@ def write(self) -> None: # pragma: no cover def test_wrapped_property( - async_file: AsyncIOWrapper[mock.Mock], wrapped: mock.Mock + async_file: AsyncIOWrapper[mock.Mock], + wrapped: mock.Mock, ) -> None: assert async_file.wrapped is wrapped def test_dir_matches_wrapped( - async_file: AsyncIOWrapper[mock.Mock], wrapped: mock.Mock + async_file: AsyncIOWrapper[mock.Mock], + wrapped: mock.Mock, ) -> None: attrs = _FILE_SYNC_ATTRS.union(_FILE_ASYNC_METHODS) @@ -132,7 +134,8 @@ def test_type_stubs_match_lists() -> None: def test_sync_attrs_forwarded( - async_file: AsyncIOWrapper[mock.Mock], wrapped: mock.Mock + async_file: AsyncIOWrapper[mock.Mock], + wrapped: mock.Mock, ) -> None: for attr_name in _FILE_SYNC_ATTRS: if attr_name not in dir(async_file): @@ -142,7 +145,8 @@ def test_sync_attrs_forwarded( def test_sync_attrs_match_wrapper( - async_file: AsyncIOWrapper[mock.Mock], wrapped: mock.Mock + async_file: AsyncIOWrapper[mock.Mock], + wrapped: mock.Mock, ) -> None: for attr_name in _FILE_SYNC_ATTRS: if attr_name in dir(async_file): @@ -174,7 +178,8 @@ def test_async_methods_signature(async_file: AsyncIOWrapper[mock.Mock]) -> None: async def test_async_methods_wrap( - async_file: AsyncIOWrapper[mock.Mock], wrapped: mock.Mock + async_file: AsyncIOWrapper[mock.Mock], + wrapped: mock.Mock, ) -> None: for meth_name in _FILE_ASYNC_METHODS: if meth_name not in dir(async_file): @@ -186,7 +191,8 @@ async def test_async_methods_wrap( value = await meth(sentinel.argument, keyword=sentinel.keyword) wrapped_meth.assert_called_once_with( - sentinel.argument, keyword=sentinel.keyword + sentinel.argument, + keyword=sentinel.keyword, ) assert value == wrapped_meth() @@ -194,7 +200,8 @@ async def test_async_methods_wrap( async def test_async_methods_match_wrapper( - async_file: AsyncIOWrapper[mock.Mock], wrapped: mock.Mock + async_file: AsyncIOWrapper[mock.Mock], + wrapped: mock.Mock, ) -> None: for meth_name in _FILE_ASYNC_METHODS: if meth_name in dir(async_file): diff --git a/src/trio/_tests/test_highlevel_open_tcp_listeners.py b/src/trio/_tests/test_highlevel_open_tcp_listeners.py index 3196c9e533..6e2abf112a 100644 --- a/src/trio/_tests/test_highlevel_open_tcp_listeners.py +++ b/src/trio/_tests/test_highlevel_open_tcp_listeners.py @@ -160,7 +160,11 @@ def getsockopt(self, /, level: int, optname: int) -> int: ... def getsockopt(self, /, level: int, optname: int, buflen: int) -> bytes: ... def getsockopt( - self, /, level: int, optname: int, buflen: int | None = None + self, + /, + level: int, + optname: int, + buflen: int | None = None, ) -> int | bytes: if (level, optname) == (tsocket.SOL_SOCKET, tsocket.SO_ACCEPTCONN): return True @@ -171,7 +175,12 @@ def setsockopt(self, /, level: int, optname: int, value: int | Buffer) -> None: @overload def setsockopt( - self, /, level: int, optname: int, value: None, optlen: int + self, + /, + level: int, + optname: int, + value: None, + optlen: int, ) -> None: ... def setsockopt( @@ -252,7 +261,9 @@ async def getaddrinfo( ] async def getnameinfo( - self, sockaddr: tuple[str, int] | tuple[str, int, int, int], flags: int + self, + sockaddr: tuple[str, int] | tuple[str, int, int, int], + flags: int, ) -> tuple[str, str]: raise NotImplementedError() @@ -268,8 +279,8 @@ async def test_open_tcp_listeners_multiple_host_cleanup_on_error() -> None: (tsocket.AF_INET, "1.1.1.1"), (tsocket.AF_INET, "2.2.2.2"), (tsocket.AF_INET, "3.3.3.3"), - ] - ) + ], + ), ) with pytest.raises(FakeOSError): @@ -313,14 +324,16 @@ async def handler(stream: SendStream) -> None: [{tsocket.AF_INET}, {tsocket.AF_INET6}, {tsocket.AF_INET, tsocket.AF_INET6}], ) async def test_open_tcp_listeners_some_address_families_unavailable( - try_families: set[AddressFamily], fail_families: set[AddressFamily] + try_families: set[AddressFamily], + fail_families: set[AddressFamily], ) -> None: fsf = FakeSocketFactory( - 10, raise_on_family={family: errno.EAFNOSUPPORT for family in fail_families} + 10, + raise_on_family={family: errno.EAFNOSUPPORT for family in fail_families}, ) tsocket.set_custom_socket_factory(fsf) tsocket.set_custom_hostname_resolver( - FakeHostnameResolver([(family, "foo") for family in try_families]) + FakeHostnameResolver([(family, "foo") for family in try_families]), ) should_succeed = try_families - fail_families @@ -352,7 +365,7 @@ async def test_open_tcp_listeners_socket_fails_not_afnosupport() -> None: ) tsocket.set_custom_socket_factory(fsf) tsocket.set_custom_hostname_resolver( - FakeHostnameResolver([(tsocket.AF_INET, "foo"), (tsocket.AF_INET6, "bar")]) + FakeHostnameResolver([(tsocket.AF_INET, "foo"), (tsocket.AF_INET6, "bar")]), ) with pytest.raises(OSError, match="nope") as exc_info: @@ -391,6 +404,7 @@ async def test_open_tcp_listeners_backlog_float_error() -> None: tsocket.set_custom_socket_factory(fsf) for should_fail in (0.0, 2.18, 3.14, 9.75): with pytest.raises( - TypeError, match=f"backlog must be an int or None, not {should_fail!r}" + TypeError, + match=f"backlog must be an int or None, not {should_fail!r}", ): await open_tcp_listeners(0, backlog=should_fail) # type: ignore[arg-type] diff --git a/src/trio/_tests/test_highlevel_open_tcp_stream.py b/src/trio/_tests/test_highlevel_open_tcp_stream.py index ce1b1ac1de..81d2b403bb 100644 --- a/src/trio/_tests/test_highlevel_open_tcp_stream.py +++ b/src/trio/_tests/test_highlevel_open_tcp_stream.py @@ -156,12 +156,14 @@ async def test_local_address_real() -> None: local_address = "127.0.0.2" if can_bind_127_0_0_2() else "127.0.0.1" async with await open_tcp_stream( - *listener.getsockname(), local_address=local_address + *listener.getsockname(), + local_address=local_address, ) as client_stream: assert client_stream.socket.getsockname()[0] == local_address if hasattr(trio.socket, "IP_BIND_ADDRESS_NO_PORT"): assert client_stream.socket.getsockopt( - trio.socket.IPPROTO_IP, trio.socket.IP_BIND_ADDRESS_NO_PORT + trio.socket.IPPROTO_IP, + trio.socket.IP_BIND_ADDRESS_NO_PORT, ) server_sock, remote_addr = await listener.accept() await client_stream.aclose() @@ -172,13 +174,15 @@ async def test_local_address_real() -> None: # Trying to connect to an ipv4 address with the ipv6 wildcard # local_address should fail with pytest.raises( - OSError, match=r"^all attempts to connect* to *127\.0\.0\.\d:\d+ failed$" + OSError, + match=r"^all attempts to connect* to *127\.0\.0\.\d:\d+ failed$", ): await open_tcp_stream(*listener.getsockname(), local_address="::") # But the ipv4 wildcard address should work async with await open_tcp_stream( - *listener.getsockname(), local_address="0.0.0.0" + *listener.getsockname(), + local_address="0.0.0.0", ) as client_stream: server_sock, remote_addr = await listener.accept() server_sock.close() @@ -318,7 +322,9 @@ async def getaddrinfo( return [self._ip_to_gai_entry(ip) for ip in self.ip_order] async def getnameinfo( - self, sockaddr: tuple[str, int] | tuple[str, int, int, int], flags: int + self, + sockaddr: tuple[str, int] | tuple[str, int, int, int], + flags: int, ) -> tuple[str, str]: raise NotImplementedError @@ -391,7 +397,9 @@ async def test_one_host_slow_success(autojump_clock: MockClock) -> None: async def test_one_host_quick_fail(autojump_clock: MockClock) -> None: exc, scenario = await run_scenario( - 82, [("1.2.3.4", 0.123, "error")], expect_error=OSError + 82, + [("1.2.3.4", 0.123, "error")], + expect_error=OSError, ) assert isinstance(exc, OSError) assert trio.current_time() == 0.123 @@ -399,7 +407,9 @@ async def test_one_host_quick_fail(autojump_clock: MockClock) -> None: async def test_one_host_slow_fail(autojump_clock: MockClock) -> None: exc, scenario = await run_scenario( - 83, [("1.2.3.4", 100, "error")], expect_error=OSError + 83, + [("1.2.3.4", 100, "error")], + expect_error=OSError, ) assert isinstance(exc, OSError) assert trio.current_time() == 100 @@ -407,7 +417,9 @@ async def test_one_host_slow_fail(autojump_clock: MockClock) -> None: async def test_one_host_failed_after_connect(autojump_clock: MockClock) -> None: exc, scenario = await run_scenario( - 83, [("1.2.3.4", 1, "postconnect_fail")], expect_error=KeyboardInterrupt + 83, + [("1.2.3.4", 1, "postconnect_fail")], + expect_error=KeyboardInterrupt, ) assert isinstance(exc, KeyboardInterrupt) @@ -532,7 +544,8 @@ async def test_all_fail(autojump_clock: MockClock) -> None: subexceptions = (Matcher(OSError, match="^sorry$"),) * 4 assert RaisesGroup( - *subexceptions, match="all attempts to connect to test.example.com:80 failed" + *subexceptions, + match="all attempts to connect to test.example.com:80 failed", ).matches(exc.__cause__) assert trio.current_time() == (0.1 + 0.2 + 10) diff --git a/src/trio/_tests/test_highlevel_open_unix_stream.py b/src/trio/_tests/test_highlevel_open_unix_stream.py index 38c31b8a4a..4441d751d2 100644 --- a/src/trio/_tests/test_highlevel_open_unix_stream.py +++ b/src/trio/_tests/test_highlevel_open_unix_stream.py @@ -12,7 +12,8 @@ assert not TYPE_CHECKING or sys.platform != "win32" skip_if_not_unix = pytest.mark.skipif( - not hasattr(socket, "AF_UNIX"), reason="Needs unix socket support" + not hasattr(socket, "AF_UNIX"), + reason="Needs unix socket support", ) @@ -79,6 +80,7 @@ async def test_open_unix_socket() -> None: @pytest.mark.skipif(hasattr(socket, "AF_UNIX"), reason="Test for non-unix platforms") async def test_error_on_no_unix() -> None: with pytest.raises( - RuntimeError, match="^Unix sockets are not supported on this platform$" + RuntimeError, + match="^Unix sockets are not supported on this platform$", ): await open_unix_socket("") diff --git a/src/trio/_tests/test_highlevel_serve_listeners.py b/src/trio/_tests/test_highlevel_serve_listeners.py index 4909b90d04..6ecd42694f 100644 --- a/src/trio/_tests/test_highlevel_serve_listeners.py +++ b/src/trio/_tests/test_highlevel_serve_listeners.py @@ -95,7 +95,9 @@ async def do_tests(parent_nursery: Nursery) -> None: async with trio.open_nursery() as nursery: l2: list[MemoryListener] = await nursery.start( - trio.serve_listeners, handler, listeners + trio.serve_listeners, + handler, + listeners, ) assert l2 == listeners # This is just split into another function because gh-136 isn't @@ -123,7 +125,8 @@ def check_error(e: BaseException) -> bool: async def test_serve_listeners_accept_capacity_error( - autojump_clock: MockClock, caplog: pytest.LogCaptureFixture + autojump_clock: MockClock, + caplog: pytest.LogCaptureFixture, ) -> None: listener = MemoryListener() @@ -155,7 +158,8 @@ class Done(Exception): pass async def connection_watcher( - *, task_status: TaskStatus[Nursery] = trio.TASK_STATUS_IGNORED + *, + task_status: TaskStatus[Nursery] = trio.TASK_STATUS_IGNORED, ) -> NoReturn: async with trio.open_nursery() as nursery: task_status.started(nursery) @@ -173,7 +177,7 @@ async def connection_watcher( handler, [listener], handler_nursery=handler_nursery, - ) + ), ) for _ in range(10): nursery.start_soon(listener.connect) diff --git a/src/trio/_tests/test_highlevel_socket.py b/src/trio/_tests/test_highlevel_socket.py index 976a3b5e04..735353ab7e 100644 --- a/src/trio/_tests/test_highlevel_socket.py +++ b/src/trio/_tests/test_highlevel_socket.py @@ -27,7 +27,8 @@ async def test_SocketStream_basics() -> None: # DGRAM socket bad with tsocket.socket(type=tsocket.SOCK_DGRAM) as sock: with pytest.raises( - ValueError, match="^SocketStream requires a SOCK_STREAM socket$" + ValueError, + match="^SocketStream requires a SOCK_STREAM socket$", ): # TODO: does not raise an error? SocketStream(sock) @@ -155,7 +156,8 @@ async def test_SocketListener() -> None: with tsocket.socket(type=tsocket.SOCK_DGRAM) as s: await s.bind(("127.0.0.1", 0)) with pytest.raises( - ValueError, match="^SocketListener requires a SOCK_STREAM socket$" + ValueError, + match="^SocketListener requires a SOCK_STREAM socket$", ) as excinfo: SocketListener(s) excinfo.match(r".*SOCK_STREAM") @@ -166,7 +168,8 @@ async def test_SocketListener() -> None: with tsocket.socket() as s: await s.bind(("127.0.0.1", 0)) with pytest.raises( - ValueError, match="^SocketListener requires a listening socket$" + ValueError, + match="^SocketListener requires a listening socket$", ) as excinfo: SocketListener(s) excinfo.match(r".*listen") @@ -228,22 +231,39 @@ def getsockopt(self, /, level: int, optname: int) -> int: ... @overload def getsockopt( # noqa: F811 - self, /, level: int, optname: int, buflen: int + self, + /, + level: int, + optname: int, + buflen: int, ) -> bytes: ... def getsockopt( # noqa: F811 - self, /, level: int, optname: int, buflen: int | None = None + self, + /, + level: int, + optname: int, + buflen: int | None = None, ) -> int | bytes: return True @overload def setsockopt( - self, /, level: int, optname: int, value: int | Buffer + self, + /, + level: int, + optname: int, + value: int | Buffer, ) -> None: ... @overload def setsockopt( # noqa: F811 - self, /, level: int, optname: int, value: None, optlen: int + self, + /, + level: int, + optname: int, + value: None, + optlen: int, ) -> None: ... def setsockopt( # noqa: F811 @@ -276,7 +296,7 @@ async def accept(self) -> tuple[SocketType, object]: OSError(errno.EFAULT, "attempt to write to read-only memory"), OSError(errno.ENOBUFS, "out of buffers"), fake_server_sock, - ] + ], ) listener = SocketListener(fake_listen_sock) diff --git a/src/trio/_tests/test_highlevel_ssl_helpers.py b/src/trio/_tests/test_highlevel_ssl_helpers.py index 53f687d7c3..ca23c333c7 100644 --- a/src/trio/_tests/test_highlevel_ssl_helpers.py +++ b/src/trio/_tests/test_highlevel_ssl_helpers.py @@ -82,8 +82,12 @@ async def test_open_ssl_over_tcp_stream_and_everything_else( res: list[SSLListener[SocketListener]] = ( # type: ignore[type-var] await nursery.start( partial( - serve_ssl_over_tcp, echo_handler, 0, SERVER_CTX, host="127.0.0.1" - ) + serve_ssl_over_tcp, + echo_handler, + 0, + SERVER_CTX, + host="127.0.0.1", + ), ) ) (listener,) = res @@ -105,7 +109,9 @@ async def test_open_ssl_over_tcp_stream_and_everything_else( # We have the trust but not the hostname # (checks custom ssl_context + hostname checking) stream = await open_ssl_over_tcp_stream( - "xyzzy.example.org", 80, ssl_context=client_ctx + "xyzzy.example.org", + 80, + ssl_context=client_ctx, ) async with stream: with pytest.raises(trio.BrokenResourceError): @@ -113,7 +119,9 @@ async def test_open_ssl_over_tcp_stream_and_everything_else( # This one should work! stream = await open_ssl_over_tcp_stream( - "trio-test-1.example.org", 80, ssl_context=client_ctx + "trio-test-1.example.org", + 80, + ssl_context=client_ctx, ) async with stream: assert isinstance(stream, trio.SSLStream) @@ -149,7 +157,10 @@ async def test_open_ssl_over_tcp_listeners() -> None: assert not listener._https_compatible (listener,) = await open_ssl_over_tcp_listeners( - 0, SERVER_CTX, host="127.0.0.1", https_compatible=True + 0, + SERVER_CTX, + host="127.0.0.1", + https_compatible=True, ) async with listener: assert listener._https_compatible diff --git a/src/trio/_tests/test_path.py b/src/trio/_tests/test_path.py index af29a0604b..bbe502afd6 100644 --- a/src/trio/_tests/test_path.py +++ b/src/trio/_tests/test_path.py @@ -98,11 +98,14 @@ async def test_div_magic(cls_a: PathOrStrType, cls_b: PathOrStrType) -> None: @pytest.mark.parametrize( - ("cls_a", "cls_b"), [(trio.Path, pathlib.Path), (trio.Path, trio.Path)] + ("cls_a", "cls_b"), + [(trio.Path, pathlib.Path), (trio.Path, trio.Path)], ) @pytest.mark.parametrize("path", ["foo", "foo/bar/baz", "./foo"]) async def test_hash_magic( - cls_a: EitherPathType, cls_b: EitherPathType, path: str + cls_a: EitherPathType, + cls_b: EitherPathType, + path: str, ) -> None: a, b = cls_a(path), cls_b(path) assert hash(a) == hash(b) @@ -264,7 +267,7 @@ async def test_classmethods() -> None: ], ) def test_wrapping_without_docstrings( - wrapper: Callable[[Callable[[], None]], Callable[[], None]] + wrapper: Callable[[Callable[[], None]], Callable[[], None]], ) -> None: @wrapper def func_without_docstring() -> None: ... # pragma: no cover diff --git a/src/trio/_tests/test_repl.py b/src/trio/_tests/test_repl.py index fbfdb07a05..d585276b9d 100644 --- a/src/trio/_tests/test_repl.py +++ b/src/trio/_tests/test_repl.py @@ -76,7 +76,7 @@ async def test_basic_interaction( # import works "import sys", "sys.stdout.write('hello stdout\\n')", - ] + ], ) monkeypatch.setattr(console, "raw_input", raw_input) await trio._repl.run_repl(console) @@ -89,7 +89,7 @@ async def test_system_exits_quit_interpreter(monkeypatch: pytest.MonkeyPatch) -> raw_input = build_raw_input( [ "raise SystemExit", - ] + ], ) monkeypatch.setattr(console, "raw_input", raw_input) with pytest.raises(SystemExit): @@ -115,7 +115,7 @@ async def test_KI_interrupts( "", "await f()", "print('AFTER KeyboardInterrupt')", - ] + ], ) monkeypatch.setattr(console, "raw_input", raw_input) await trio._repl.run_repl(console) @@ -138,7 +138,7 @@ async def test_system_exits_in_exc_group( "", "raise BaseExceptionGroup('', [RuntimeError(), SystemExit()])", "print('AFTER BaseExceptionGroup')", - ] + ], ) monkeypatch.setattr(console, "raw_input", raw_input) await trio._repl.run_repl(console) @@ -162,7 +162,7 @@ async def test_system_exits_in_nested_exc_group( "raise BaseExceptionGroup(", " '', [BaseExceptionGroup('', [RuntimeError(), SystemExit()])])", "print('AFTER BaseExceptionGroup')", - ] + ], ) monkeypatch.setattr(console, "raw_input", raw_input) await trio._repl.run_repl(console) @@ -182,7 +182,7 @@ async def test_base_exception_captured( # The statement after raise should still get executed "raise BaseException", "print('AFTER BaseException')", - ] + ], ) monkeypatch.setattr(console, "raw_input", raw_input) await trio._repl.run_repl(console) @@ -202,7 +202,7 @@ async def test_exc_group_captured( # The statement after raise should still get executed "raise ExceptionGroup('', [KeyError()])", "print('AFTER ExceptionGroup')", - ] + ], ) monkeypatch.setattr(console, "raw_input", raw_input) await trio._repl.run_repl(console) @@ -224,7 +224,7 @@ async def test_base_exception_capture_from_coroutine( # be executed "await async_func_raises_base_exception()", "print('AFTER BaseException')", - ] + ], ) monkeypatch.setattr(console, "raw_input", raw_input) await trio._repl.run_repl(console) diff --git a/src/trio/_tests/test_signals.py b/src/trio/_tests/test_signals.py index 5e639652ef..fd4e32a4c3 100644 --- a/src/trio/_tests/test_signals.py +++ b/src/trio/_tests/test_signals.py @@ -43,7 +43,8 @@ async def test_open_signal_receiver() -> None: async def test_open_signal_receiver_restore_handler_after_one_bad_signal() -> None: orig = signal.getsignal(signal.SIGILL) with pytest.raises( - ValueError, match="(signal number out of range|invalid signal value)$" + ValueError, + match="(signal number out of range|invalid signal value)$", ): with open_signal_receiver(signal.SIGILL, 1234567): pass # pragma: no cover diff --git a/src/trio/_tests/test_socket.py b/src/trio/_tests/test_socket.py index b98b3246e9..f2c2ee955e 100644 --- a/src/trio/_tests/test_socket.py +++ b/src/trio/_tests/test_socket.py @@ -6,6 +6,7 @@ import socket as stdlib_socket import sys import tempfile +from pathlib import Path from socket import AddressFamily, SocketKind from typing import TYPE_CHECKING, Any, Callable, List, Tuple, Union @@ -55,7 +56,10 @@ def _frozenbind(self, *args: Any, **kwargs: Any) -> tuple[Any, ...]: return frozenbound def set( - self, response: GetAddrInfoResponse | str, *args: Any, **kwargs: Any + self, + response: GetAddrInfoResponse | str, + *args: Any, + **kwargs: Any, ) -> None: self._responses[self._frozenbind(*args, **kwargs)] = response @@ -472,7 +476,8 @@ async def test_SocketType_shutdown() -> None: ], ) async def test_SocketType_simple_server( - address: str, socket_type: AddressFamily + address: str, + socket_type: AddressFamily, ) -> None: # listen, bind, accept, connect, getpeername, getsockname listener = tsocket.socket(socket_type) @@ -556,7 +561,8 @@ def pad(addr: tuple[str | int, ...]) -> tuple[str | int, ...]: return addr def assert_eq( - actual: tuple[str | int, ...], expected: tuple[str | int, ...] + actual: tuple[str | int, ...], + expected: tuple[str | int, ...], ) -> None: assert pad(expected) == pad(actual) @@ -588,7 +594,7 @@ async def res( | tuple[str, str] | tuple[str, str, int] | tuple[str, str, int, int] - ) + ), ) -> Any: return await sock._resolve_address_nocp( args, @@ -639,7 +645,8 @@ async def res( # smoke test the basic functionality... try: netlink_sock = tsocket.socket( - family=tsocket.AF_NETLINK, type=tsocket.SOCK_DGRAM + family=tsocket.AF_NETLINK, + type=tsocket.SOCK_DGRAM, ) except (AttributeError, OSError): pass @@ -793,7 +800,9 @@ def connect(self, *args: Any, **kwargs: Any) -> None: cancel_scope.cancel() sock._sock = stdlib_socket.fromfd( - self.detach(), self.family, self.type + self.detach(), + self.family, + self.type, ) sock._sock.connect(*args, **kwargs) # If connect *doesn't* raise, then pretend it did @@ -842,7 +851,9 @@ async def test_resolve_address_exception_in_connect_closes_socket() -> None: with tsocket.socket() as sock: async def _resolve_address_nocp( - self: Any, *args: Any, **kwargs: Any + self: Any, + *args: Any, + **kwargs: Any, ) -> None: cancel_scope.cancel() await _core.checkpoint() @@ -991,7 +1002,9 @@ async def getaddrinfo( return ("custom_gai", host, port, family, type, proto, flags) async def getnameinfo( - self, sockaddr: tuple[str, int] | tuple[str, int, int, int], flags: int + self, + sockaddr: tuple[str, int] | tuple[str, int, int, int], + flags: int, ) -> tuple[str, tuple[str, int] | tuple[str, int, int, int], int]: return ("custom_gni", sockaddr, flags) @@ -1077,7 +1090,7 @@ async def test_unix_domain_socket() -> None: # Bind has a special branch to use a thread, since it has to do filesystem # traversal. Maybe connect should too? Not sure. - async def check_AF_UNIX(path: str | bytes) -> None: + async def check_AF_UNIX(path: str | bytes | os.PathLike[str]) -> None: with tsocket.socket(family=tsocket.AF_UNIX) as lsock: await lsock.bind(path) lsock.listen(10) @@ -1091,8 +1104,11 @@ async def check_AF_UNIX(path: str | bytes) -> None: # Can't use tmpdir fixture, because we can exceed the maximum AF_UNIX path # length on macOS. with tempfile.TemporaryDirectory() as tmpdir: - path = f"{tmpdir}/sock" - await check_AF_UNIX(path) + # Test passing various supported types as path + # Must use different filenames to prevent "address already in use" + await check_AF_UNIX(f"{tmpdir}/sock") + await check_AF_UNIX(Path(f"{tmpdir}/sock1")) + await check_AF_UNIX(os.fsencode(f"{tmpdir}/sock2")) try: cookie = os.urandom(20).hex().encode("ascii") diff --git a/src/trio/_tests/test_ssl.py b/src/trio/_tests/test_ssl.py index 8e780b2f9c..7197a47cc3 100644 --- a/src/trio/_tests/test_ssl.py +++ b/src/trio/_tests/test_ssl.py @@ -116,11 +116,15 @@ def client_ctx(request: pytest.FixtureRequest) -> ssl.SSLContext: # The blocking socket server. def ssl_echo_serve_sync( - sock: stdlib_socket.socket, *, expect_fail: bool = False + sock: stdlib_socket.socket, + *, + expect_fail: bool = False, ) -> None: try: wrapped = SERVER_CTX.wrap_socket( - sock, server_side=True, suppress_ragged_eofs=False + sock, + server_side=True, + suppress_ragged_eofs=False, ) with wrapped: wrapped.do_handshake() @@ -175,7 +179,8 @@ async def ssl_echo_server_raw(**kwargs: Any) -> AsyncIterator[SocketStream]: # nursery context manager to exit too. with a, b: nursery.start_soon( - trio.to_thread.run_sync, partial(ssl_echo_serve_sync, b, **kwargs) + trio.to_thread.run_sync, + partial(ssl_echo_serve_sync, b, **kwargs), ) yield SocketStream(tsocket.from_stdlib_socket(a)) @@ -185,7 +190,8 @@ async def ssl_echo_server_raw(**kwargs: Any) -> AsyncIterator[SocketStream]: # echo server (running in a thread) @asynccontextmanager # type: ignore[misc] # decorated contains Any async def ssl_echo_server( - client_ctx: SSLContext, **kwargs: Any + client_ctx: SSLContext, + **kwargs: Any, ) -> AsyncIterator[SSLStream[Stream]]: async with ssl_echo_server_raw(**kwargs) as sock: yield SSLStream(sock, client_ctx, server_hostname="trio-test-1.example.org") @@ -239,10 +245,10 @@ def __init__(self, sleeper: None = None) -> None: self._pending_cleartext = bytearray() self._send_all_conflict_detector = ConflictDetector( - "simultaneous calls to PyOpenSSLEchoStream.send_all" + "simultaneous calls to PyOpenSSLEchoStream.send_all", ) self._receive_some_conflict_detector = ConflictDetector( - "simultaneous calls to PyOpenSSLEchoStream.receive_some" + "simultaneous calls to PyOpenSSLEchoStream.receive_some", ) if sleeper is None: @@ -358,7 +364,10 @@ async def test_PyOpenSSLEchoStream_gives_resource_busy_errors() -> None: # PyOpenSSLEchoStream will notice and complain. async def do_test( - func1: str, args1: tuple[object, ...], func2: str, args2: tuple[object, ...] + func1: str, + args1: tuple[object, ...], + func2: str, + args2: tuple[object, ...], ) -> None: s = PyOpenSSLEchoStream() with RaisesGroup(Matcher(_core.BusyResourceError, "simultaneous")): @@ -369,14 +378,18 @@ async def do_test( await do_test("send_all", (b"x",), "send_all", (b"x",)) await do_test("send_all", (b"x",), "wait_send_all_might_not_block", ()) await do_test( - "wait_send_all_might_not_block", (), "wait_send_all_might_not_block", () + "wait_send_all_might_not_block", + (), + "wait_send_all_might_not_block", + (), ) await do_test("receive_some", (1,), "receive_some", (1,)) @contextmanager # type: ignore[misc] # decorated contains Any def virtual_ssl_echo_server( - client_ctx: SSLContext, **kwargs: Any + client_ctx: SSLContext, + **kwargs: Any, ) -> Iterator[SSLStream[PyOpenSSLEchoStream]]: fakesock = PyOpenSSLEchoStream(**kwargs) yield SSLStream(fakesock, client_ctx, server_hostname="trio-test-1.example.org") @@ -401,7 +414,10 @@ def ssl_wrap_pair( **client_kwargs, ) server_ssl = SSLStream( - server_transport, SERVER_CTX, server_side=True, **server_kwargs + server_transport, + SERVER_CTX, + server_side=True, + **server_kwargs, ) return client_ssl, server_ssl @@ -462,13 +478,16 @@ async def test_ssl_server_basics(client_ctx: SSLContext) -> None: with a, b: server_sock = tsocket.from_stdlib_socket(b) server_transport = SSLStream( - SocketStream(server_sock), SERVER_CTX, server_side=True + SocketStream(server_sock), + SERVER_CTX, + server_side=True, ) assert server_transport.server_side def client() -> None: with client_ctx.wrap_socket( - a, server_hostname="trio-test-1.example.org" + a, + server_hostname="trio-test-1.example.org", ) as client_sock: client_sock.sendall(b"x") assert client_sock.recv(1) == b"y" @@ -612,7 +631,8 @@ async def test_renegotiation_simple(client_ctx: SSLContext) -> None: @slow async def test_renegotiation_randomized( - mock_clock: MockClock, client_ctx: SSLContext + mock_clock: MockClock, + client_ctx: SSLContext, ) -> None: # The only blocking things in this function are our random sleeps, so 0 is # a good threshold. @@ -716,7 +736,8 @@ async def sleeper_with_slow_wait_writable_and_expect(method: str) -> None: await trio.sleep(1000) with virtual_ssl_echo_server( - client_ctx, sleeper=sleeper_with_slow_wait_writable_and_expect + client_ctx, + sleeper=sleeper_with_slow_wait_writable_and_expect, ) as s: await send(b"x") s.transport_stream.renegotiate() @@ -747,7 +768,8 @@ async def do_wait_send_all_might_not_block(s: S) -> None: await s.wait_send_all_might_not_block() async def do_test( - func1: Callable[[S], Awaitable[None]], func2: Callable[[S], Awaitable[None]] + func1: Callable[[S], Awaitable[None]], + func2: Callable[[S], Awaitable[None]], ) -> None: s, _ = ssl_lockstep_stream_pair(client_ctx) with RaisesGroup(Matcher(_core.BusyResourceError, "another task")): @@ -835,7 +857,8 @@ async def test_send_all_empty_string(client_ctx: SSLContext) -> None: @pytest.mark.parametrize("https_compatible", [False, True]) async def test_SSLStream_generic( - client_ctx: SSLContext, https_compatible: bool + client_ctx: SSLContext, + https_compatible: bool, ) -> None: async def stream_maker() -> tuple[ SSLStream[MemoryStapledStream], @@ -1017,12 +1040,16 @@ async def test_ssl_over_ssl(client_ctx: SSLContext) -> None: client_0, server_0 = memory_stream_pair() client_1 = SSLStream( - client_0, client_ctx, server_hostname="trio-test-1.example.org" + client_0, + client_ctx, + server_hostname="trio-test-1.example.org", ) server_1 = SSLStream(server_0, SERVER_CTX, server_side=True) client_2 = SSLStream( - client_1, client_ctx, server_hostname="trio-test-1.example.org" + client_1, + client_ctx, + server_hostname="trio-test-1.example.org", ) server_2 = SSLStream(server_1, SERVER_CTX, server_side=True) @@ -1302,7 +1329,9 @@ async def setup( transport_client = await open_tcp_stream(*listen_sock.getsockname()) ssl_client = SSLStream( - transport_client, client_ctx, server_hostname="trio-test-1.example.org" + transport_client, + client_ctx, + server_hostname="trio-test-1.example.org", ) return listen_sock, ssl_listener, ssl_client diff --git a/src/trio/_tests/test_subprocess.py b/src/trio/_tests/test_subprocess.py index 0a70e7a974..d65a07614a 100644 --- a/src/trio/_tests/test_subprocess.py +++ b/src/trio/_tests/test_subprocess.py @@ -131,7 +131,8 @@ async def test_basic(background_process: BackgroundProcessType) -> None: await proc.wait() assert proc.returncode == 1 assert repr(proc) == "".format( - EXIT_FALSE, "exited with status 1" + EXIT_FALSE, + "exited with status 1", ) @@ -173,7 +174,7 @@ async def test_multi_wait(background_process: BackgroundProcessType) -> None: COPY_STDIN_TO_STDOUT_AND_BACKWARD_TO_STDERR = python( "data = sys.stdin.buffer.read(); " "sys.stdout.buffer.write(data); " - "sys.stderr.buffer.write(data[::-1])" + "sys.stderr.buffer.write(data[::-1])", ) @@ -234,7 +235,7 @@ async def test_interactive(background_process: BackgroundProcessType) -> None: " request = int(line.strip())\n" " print(str(idx * 2) * request)\n" " print(str(idx * 2 + 1) * request * 2, file=sys.stderr)\n" - " idx += 1\n" + " idx += 1\n", ), stdin=subprocess.PIPE, stdout=subprocess.PIPE, @@ -246,7 +247,9 @@ async def expect(idx: int, request: int) -> None: async with _core.open_nursery() as nursery: async def drain_one( - stream: ReceiveStream, count: int, digit: int + stream: ReceiveStream, + count: int, + digit: int, ) -> None: while count > 0: result = await stream.receive_some(count) @@ -291,7 +294,10 @@ async def test_run() -> None: data = bytes(random.randint(0, 255) for _ in range(2**18)) result = await run_process( - CAT, stdin=data, capture_stdout=True, capture_stderr=True + CAT, + stdin=data, + capture_stdout=True, + capture_stderr=True, ) assert result.args == CAT assert result.returncode == 0 @@ -325,7 +331,8 @@ async def test_run() -> None: with pytest.raises(ValueError, match=pipe_stdout_error): await run_process(CAT, stdout=subprocess.PIPE) with pytest.raises( - ValueError, match=pipe_stdout_error.replace("stdout", "stderr", 1) + ValueError, + match=pipe_stdout_error.replace("stdout", "stderr", 1), ): await run_process(CAT, stderr=subprocess.PIPE) with pytest.raises( @@ -350,7 +357,10 @@ async def test_run_check() -> None: assert excinfo.value.stdout is None result = await run_process( - cmd, capture_stdout=True, capture_stderr=True, check=False + cmd, + capture_stdout=True, + capture_stderr=True, + check=False, ) assert result.args == cmd assert result.stdout == b"" @@ -361,7 +371,8 @@ async def test_run_check() -> None: @skip_if_fbsd_pipes_broken async def test_run_with_broken_pipe() -> None: result = await run_process( - [sys.executable, "-c", "import sys; sys.stdin.close()"], stdin=b"x" * 131072 + [sys.executable, "-c", "import sys; sys.stdin.close()"], + stdin=b"x" * 131072, ) assert result.returncode == 0 assert result.stdout is result.stderr is None @@ -404,7 +415,9 @@ async def test_stderr_stdout(background_process: BackgroundProcessType) -> None: # this one hits the branch where stderr=STDOUT but stdout # is not redirected async with background_process( - CAT, stdin=subprocess.PIPE, stderr=subprocess.STDOUT + CAT, + stdin=subprocess.PIPE, + stderr=subprocess.STDOUT, ) as proc: assert proc.stdout is None assert proc.stderr is None @@ -452,7 +465,8 @@ async def test_errors() -> None: @background_process_param async def test_signals(background_process: BackgroundProcessType) -> None: async def test_one_signal( - send_it: Callable[[Process], None], signum: signal.Signals | None + send_it: Callable[[Process], None], + signum: signal.Signals | None, ) -> None: with move_on_after(1.0) as scope: async with background_process(SLEEP(3600)) as proc: @@ -557,7 +571,7 @@ async def custom_deliver_cancel(proc: Process) -> None: async with _core.open_nursery() as nursery: nursery.start_soon( - partial(run_process, SLEEP(9999), deliver_cancel=custom_deliver_cancel) + partial(run_process, SLEEP(9999), deliver_cancel=custom_deliver_cancel), ) await wait_all_tasks_blocked() nursery.cancel_scope.cancel() @@ -573,7 +587,7 @@ async def custom_deliver_cancel(proc: Process) -> None: async def do_stuff() -> None: async with _core.open_nursery() as nursery: nursery.start_soon( - partial(run_process, SLEEP(9999), deliver_cancel=custom_deliver_cancel) + partial(run_process, SLEEP(9999), deliver_cancel=custom_deliver_cancel), ) await wait_all_tasks_blocked() nursery.cancel_scope.cancel() @@ -601,7 +615,8 @@ def broken_terminate(self: Process) -> NoReturn: @pytest.mark.skipif(not posix, reason="posix only") async def test_warn_on_cancel_SIGKILL_escalation( - autojump_clock: MockClock, monkeypatch: pytest.MonkeyPatch + autojump_clock: MockClock, + monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(Process, "terminate", lambda *args: None) diff --git a/src/trio/_tests/test_sync.py b/src/trio/_tests/test_sync.py index e4d04202cb..f506e84ffc 100644 --- a/src/trio/_tests/test_sync.py +++ b/src/trio/_tests/test_sync.py @@ -1,11 +1,15 @@ from __future__ import annotations +import re import weakref from typing import TYPE_CHECKING, Callable, Union import pytest +from trio.testing import Matcher, RaisesGroup + from .. import _core +from .._core._parking_lot import GLOBAL_PARKING_LOT_BREAKER from .._sync import * from .._timeouts import sleep_forever from ..testing import assert_checkpoints, wait_all_tasks_blocked @@ -494,7 +498,9 @@ def release(self) -> None: ] generic_lock_test = pytest.mark.parametrize( - "lock_factory", lock_factories, ids=lock_factory_names + "lock_factory", + lock_factories, + ids=lock_factory_names, ) LockLike: TypeAlias = Union[ @@ -584,3 +590,66 @@ async def lock_taker() -> None: await wait_all_tasks_blocked() assert record == ["started"] lock_like.release() + + +async def test_lock_acquire_unowned_lock() -> None: + """Test that trying to acquire a lock whose owner has exited raises an error. + see https://github.com/python-trio/trio/issues/3035 + """ + assert not GLOBAL_PARKING_LOT_BREAKER + lock = trio.Lock() + async with trio.open_nursery() as nursery: + nursery.start_soon(lock.acquire) + owner_str = re.escape(str(lock._lot.broken_by[0])) + with pytest.raises( + trio.BrokenResourceError, + match=f"^Owner of this lock exited without releasing: {owner_str}$", + ): + await lock.acquire() + assert not GLOBAL_PARKING_LOT_BREAKER + + +async def test_lock_multiple_acquire() -> None: + """Test for error if awaiting on a lock whose owner exits without releasing. + see https://github.com/python-trio/trio/issues/3035""" + assert not GLOBAL_PARKING_LOT_BREAKER + lock = trio.Lock() + with RaisesGroup( + Matcher( + trio.BrokenResourceError, + match="^Owner of this lock exited without releasing: ", + ), + ): + async with trio.open_nursery() as nursery: + nursery.start_soon(lock.acquire) + nursery.start_soon(lock.acquire) + assert not GLOBAL_PARKING_LOT_BREAKER + + +async def test_lock_handover() -> None: + assert not GLOBAL_PARKING_LOT_BREAKER + child_task: Task | None = None + lock = trio.Lock() + + # this task acquires the lock + lock.acquire_nowait() + assert GLOBAL_PARKING_LOT_BREAKER == { + _core.current_task(): [ + lock._lot, + ], + } + + async with trio.open_nursery() as nursery: + nursery.start_soon(lock.acquire) + await wait_all_tasks_blocked() + + # hand over the lock to the child task + lock.release() + + # check values, and get the identifier out of the dict for later check + assert len(GLOBAL_PARKING_LOT_BREAKER) == 1 + child_task = next(iter(GLOBAL_PARKING_LOT_BREAKER)) + assert GLOBAL_PARKING_LOT_BREAKER[child_task] == [lock._lot] + + assert lock._lot.broken_by == [child_task] + assert not GLOBAL_PARKING_LOT_BREAKER diff --git a/src/trio/_tests/test_testing.py b/src/trio/_tests/test_testing.py index 0f2778dc15..07ab6a5a3a 100644 --- a/src/trio/_tests/test_testing.py +++ b/src/trio/_tests/test_testing.py @@ -393,7 +393,9 @@ def close_hook() -> None: record.append("close_hook") mss2 = MemorySendStream( - send_all_hook, wait_send_all_might_not_block_hook, close_hook + send_all_hook, + wait_send_all_might_not_block_hook, + close_hook, ) assert mss2.send_all_hook is send_all_hook @@ -670,10 +672,13 @@ async def check(listener: SocketListener) -> None: def test_trio_test() -> None: async def busy_kitchen( - *, mock_clock: object, autojump_clock: object + *, + mock_clock: object, + autojump_clock: object, ) -> None: ... # pragma: no cover with pytest.raises(ValueError, match="^too many clocks spoil the broth!$"): trio_test(busy_kitchen)( - mock_clock=MockClock(), autojump_clock=MockClock(autojump_threshold=0) + mock_clock=MockClock(), + autojump_clock=MockClock(autojump_threshold=0), ) diff --git a/src/trio/_tests/test_testing_raisesgroup.py b/src/trio/_tests/test_testing_raisesgroup.py index 1e96d38e52..17eb6afcc7 100644 --- a/src/trio/_tests/test_testing_raisesgroup.py +++ b/src/trio/_tests/test_testing_raisesgroup.py @@ -22,7 +22,7 @@ def test_raises_group() -> None: with pytest.raises( ValueError, match=wrap_escape( - f'Invalid argument "{TypeError()!r}" must be exception type, Matcher, or RaisesGroup.' + f'Invalid argument "{TypeError()!r}" must be exception type, Matcher, or RaisesGroup.', ), ): RaisesGroup(TypeError()) @@ -94,7 +94,8 @@ def test_flatten_subgroups() -> None: raise ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),)) with RaisesGroup(RaisesGroup(ValueError, flatten_subgroups=True)): raise ExceptionGroup( - "", (ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),)),) + "", + (ExceptionGroup("", (ExceptionGroup("", (ValueError(),)),)),), ) with pytest.raises(ExceptionGroup): with RaisesGroup(RaisesGroup(ValueError, flatten_subgroups=True)): @@ -116,7 +117,8 @@ def test_catch_unwrapped_exceptions() -> None: # expecting multiple unwrapped exceptions is not possible with pytest.raises( - ValueError, match="^You cannot specify multiple exceptions with" + ValueError, + match="^You cannot specify multiple exceptions with", ): RaisesGroup(SyntaxError, ValueError, allow_unwrapped=True) # type: ignore[call-overload] # if users want one of several exception types they need to use a Matcher @@ -245,7 +247,8 @@ def check_message(message: str, body: RaisesGroup[Any]) -> None: check_message("ExceptionGroup(ValueError)", RaisesGroup(ValueError)) # multiple exceptions check_message( - "ExceptionGroup(ValueError, ValueError)", RaisesGroup(ValueError, ValueError) + "ExceptionGroup(ValueError, ValueError)", + RaisesGroup(ValueError, ValueError), ) # nested check_message( @@ -265,7 +268,8 @@ def check_message(message: str, body: RaisesGroup[Any]) -> None: # BaseExceptionGroup check_message( - "BaseExceptionGroup(KeyboardInterrupt)", RaisesGroup(KeyboardInterrupt) + "BaseExceptionGroup(KeyboardInterrupt)", + RaisesGroup(KeyboardInterrupt), ) # BaseExceptionGroup with type inside Matcher check_message( @@ -286,7 +290,8 @@ def check_message(message: str, body: RaisesGroup[Any]) -> None: def test_matcher() -> None: with pytest.raises( - ValueError, match="^You must specify at least one parameter to match on.$" + ValueError, + match="^You must specify at least one parameter to match on.$", ): Matcher() # type: ignore[call-overload] with pytest.raises( @@ -367,12 +372,3 @@ def test__ExceptionInfo(monkeypatch: pytest.MonkeyPatch) -> None: assert excinfo.type is ExceptionGroup assert excinfo.value.exceptions[0].args == ("hello",) assert isinstance(excinfo.tb, TracebackType) - - -def test_deprecated_strict() -> None: - """`strict` has been replaced with `flatten_subgroups`""" - # parameter is not included in overloaded signatures at all - with pytest.deprecated_call(): - RaisesGroup(ValueError, strict=False) # type: ignore[call-overload] - with pytest.deprecated_call(): - RaisesGroup(ValueError, strict=True) # type: ignore[call-overload] diff --git a/src/trio/_tests/test_threads.py b/src/trio/_tests/test_threads.py index b4a5842ff0..f2f1cf7815 100644 --- a/src/trio/_tests/test_threads.py +++ b/src/trio/_tests/test_threads.py @@ -336,7 +336,8 @@ def g() -> NoReturn: raise ValueError(threading.current_thread()) with pytest.raises( - ValueError, match=r"^$" + ValueError, + match=r"^$", ) as excinfo: await to_thread_run_sync(g) print(excinfo.value.args) @@ -404,7 +405,8 @@ async def child(q: stdlib_queue.Queue[None], abandon_on_cancel: bool) -> None: # handled gracefully. (Requires that the thread result machinery be prepared # for call_soon to raise RunFinishedError.) def test_run_in_worker_thread_abandoned( - capfd: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch + capfd: pytest.CaptureFixture[str], + monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.setattr(_core._thread_cache, "IDLE_TIMEOUT", 0.01) @@ -444,7 +446,9 @@ async def child() -> None: @pytest.mark.parametrize("cancel", [False, True]) @pytest.mark.parametrize("use_default_limiter", [False, True]) async def test_run_in_worker_thread_limiter( - MAX: int, cancel: bool, use_default_limiter: bool + MAX: int, + cancel: bool, + use_default_limiter: bool, ) -> None: # This test is a bit tricky. The goal is to make sure that if we set # limiter=CapacityLimiter(MAX), then in fact only MAX threads are ever @@ -647,7 +651,7 @@ async def async_fn() -> None: # pragma: no cover trio_test_contextvar: contextvars.ContextVar[str] = contextvars.ContextVar( - "trio_test_contextvar" + "trio_test_contextvar", ) @@ -879,7 +883,7 @@ async def agen(token: _core.TrioToken | None) -> AsyncGenerator[None, None]: with _core.CancelScope(shield=True): try: await to_thread_run_sync( - partial(from_thread_run, sleep, 0, trio_token=token) + partial(from_thread_run, sleep, 0, trio_token=token), ) except _core.RunFinishedError: record.append("finished") diff --git a/src/trio/_tests/test_timeouts.py b/src/trio/_tests/test_timeouts.py index 98c3d18def..574e1db1a4 100644 --- a/src/trio/_tests/test_timeouts.py +++ b/src/trio/_tests/test_timeouts.py @@ -1,12 +1,23 @@ import time -from typing import Awaitable, Callable, TypeVar +from typing import Awaitable, Callable, Protocol, TypeVar import outcome import pytest +import trio + from .. import _core from .._core._tests.tutil import slow -from .._timeouts import * +from .._timeouts import ( + TooSlowError, + fail_after, + fail_at, + move_on_after, + move_on_at, + sleep, + sleep_forever, + sleep_until, +) from ..testing import assert_checkpoints T = TypeVar("T") @@ -75,6 +86,64 @@ async def sleep_3() -> None: await check_takes_about(sleep_3, TARGET) +async def test_cannot_wake_sleep_forever() -> None: + # Test an error occurs if you manually wake sleep_forever(). + task = trio.lowlevel.current_task() + + async def wake_task() -> None: + await trio.lowlevel.checkpoint() + trio.lowlevel.reschedule(task, outcome.Value(None)) + + async with trio.open_nursery() as nursery: + nursery.start_soon(wake_task) + with pytest.raises(RuntimeError): + await trio.sleep_forever() + + +class TimeoutScope(Protocol): + def __call__(self, seconds: float, *, shield: bool) -> trio.CancelScope: ... + + +@pytest.mark.parametrize("scope", [move_on_after, fail_after]) +async def test_context_shields_from_outer(scope: TimeoutScope) -> None: + with _core.CancelScope() as outer, scope(TARGET, shield=True) as inner: + outer.cancel() + try: + await trio.lowlevel.checkpoint() + except trio.Cancelled: + pytest.fail("shield didn't work") + inner.shield = False + with pytest.raises(trio.Cancelled): + await trio.lowlevel.checkpoint() + + +@slow +async def test_move_on_after_moves_on_even_if_shielded() -> None: + async def task() -> None: + with _core.CancelScope() as outer, move_on_after(TARGET, shield=True): + 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 + await sleep_forever() + + await check_takes_about(task, TARGET) + + +@slow +async def test_fail_after_fails_even_if_shielded() -> None: + async def task() -> None: + with pytest.raises(TooSlowError), _core.CancelScope() as outer, fail_after( + TARGET, + shield=True, + ): + 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 + await sleep_forever() + + await check_takes_about(task, TARGET) + + @slow async def test_fail() -> None: async def sleep_4() -> None: @@ -111,7 +180,7 @@ async def test_timeouts_raise_value_error() -> None: ): with pytest.raises( ValueError, - match="^(duration|deadline|timeout) must (not )*be (non-negative|NaN)$", + match="^(deadline|`seconds`) must (not )*be (non-negative|NaN)$", ): await fun(val) @@ -125,7 +194,79 @@ async def test_timeouts_raise_value_error() -> None: ): with pytest.raises( ValueError, - match="^(duration|deadline|timeout) must (not )*be (non-negative|NaN)$", + match="^(deadline|`seconds`) must (not )*be (non-negative|NaN)$", ): with cm(val): pass # pragma: no cover + + +async def test_timeout_deadline_on_entry(mock_clock: _core.MockClock) -> None: + rcs = move_on_after(5) + assert rcs.relative_deadline == 5 + + mock_clock.jump(3) + start = _core.current_time() + with rcs as cs: + assert cs.is_relative is None + + # This would previously be start+2 + assert cs.deadline == start + 5 + assert cs.relative_deadline == 5 + + cs.deadline = start + 3 + assert cs.deadline == start + 3 + assert cs.relative_deadline == 3 + + cs.relative_deadline = 4 + assert cs.deadline == start + 4 + assert cs.relative_deadline == 4 + + rcs = move_on_after(5) + assert rcs.shield is False + rcs.shield = True + assert rcs.shield is True + + mock_clock.jump(3) + start = _core.current_time() + with rcs as cs: + assert cs.deadline == start + 5 + + assert rcs is cs + + +async def test_invalid_access_unentered(mock_clock: _core.MockClock) -> None: + cs = move_on_after(5) + mock_clock.jump(3) + start = _core.current_time() + + match_str = "^unentered relative cancel scope does not have an absolute deadline" + with pytest.warns(DeprecationWarning, match=match_str): + assert cs.deadline == start + 5 + mock_clock.jump(1) + # this is hella sketchy, but they *have* been warned + with pytest.warns(DeprecationWarning, match=match_str): + assert cs.deadline == start + 6 + + with pytest.warns(DeprecationWarning, match=match_str): + cs.deadline = 7 + # now transformed into absolute + assert cs.deadline == 7 + assert not cs.is_relative + + cs = move_on_at(5) + + match_str = ( + "^unentered non-relative cancel scope does not have a relative deadline$" + ) + with pytest.raises(RuntimeError, match=match_str): + assert cs.relative_deadline + with pytest.raises(RuntimeError, match=match_str): + cs.relative_deadline = 7 + + +@pytest.mark.xfail(reason="not implemented") +async def test_fail_access_before_entering() -> None: # pragma: no cover + my_fail_at = fail_at(5) + assert my_fail_at.deadline # type: ignore[attr-defined] + my_fail_after = fail_after(5) + assert my_fail_after.relative_deadline # type: ignore[attr-defined] diff --git a/src/trio/_tests/test_wait_for_object.py b/src/trio/_tests/test_wait_for_object.py index 54bbb77567..58f2198fa6 100644 --- a/src/trio/_tests/test_wait_for_object.py +++ b/src/trio/_tests/test_wait_for_object.py @@ -81,7 +81,9 @@ async def test_WaitForMultipleObjects_sync_slow() -> None: t0 = _core.current_time() async with _core.open_nursery() as nursery: nursery.start_soon( - trio.to_thread.run_sync, WaitForMultipleObjects_sync, handle1 + trio.to_thread.run_sync, + WaitForMultipleObjects_sync, + handle1, ) await _timeouts.sleep(TIMEOUT) # If we would comment the line below, the above thread will be stuck, @@ -98,7 +100,10 @@ async def test_WaitForMultipleObjects_sync_slow() -> None: t0 = _core.current_time() async with _core.open_nursery() as nursery: nursery.start_soon( - trio.to_thread.run_sync, WaitForMultipleObjects_sync, handle1, handle2 + trio.to_thread.run_sync, + WaitForMultipleObjects_sync, + handle1, + handle2, ) await _timeouts.sleep(TIMEOUT) kernel32.SetEvent(handle1) @@ -114,7 +119,10 @@ async def test_WaitForMultipleObjects_sync_slow() -> None: t0 = _core.current_time() async with _core.open_nursery() as nursery: nursery.start_soon( - trio.to_thread.run_sync, WaitForMultipleObjects_sync, handle1, handle2 + trio.to_thread.run_sync, + WaitForMultipleObjects_sync, + handle1, + handle2, ) await _timeouts.sleep(TIMEOUT) kernel32.SetEvent(handle2) diff --git a/src/trio/_tests/tools/test_gen_exports.py b/src/trio/_tests/tools/test_gen_exports.py index 19158451f7..669df968e0 100644 --- a/src/trio/_tests/tools/test_gen_exports.py +++ b/src/trio/_tests/tools/test_gen_exports.py @@ -91,7 +91,11 @@ def test_create_pass_through_args() -> None: @skip_lints @pytest.mark.parametrize("imports", [IMPORT_1, IMPORT_2, IMPORT_3]) -def test_process(tmp_path: Path, imports: str) -> None: +def test_process( + tmp_path: Path, + imports: str, + capsys: pytest.CaptureFixture[str], +) -> None: try: import black # noqa: F401 # there's no dedicated CI run that has astor+isort, but lacks black. @@ -106,7 +110,13 @@ def test_process(tmp_path: Path, imports: str) -> None: with pytest.raises(SystemExit) as excinfo: process([file], do_test=True) assert excinfo.value.code == 1 - process([file], do_test=False) + captured = capsys.readouterr() + assert "Generated sources are outdated. Please regenerate." in captured.out + with pytest.raises(SystemExit) as excinfo: + process([file], do_test=False) + assert excinfo.value.code == 1 + captured = capsys.readouterr() + assert "Regenerated sources successfully." in captured.out assert genpath.exists() process([file], do_test=True) # But if we change the lookup path it notices diff --git a/src/trio/_tests/tools/test_mypy_annotate.py b/src/trio/_tests/tools/test_mypy_annotate.py index 1289e2ad48..09a57ce745 100644 --- a/src/trio/_tests/tools/test_mypy_annotate.py +++ b/src/trio/_tests/tools/test_mypy_annotate.py @@ -119,7 +119,7 @@ def test_endtoend( monkeypatch.setattr(sys, "stdin", io.StringIO(inp_text)) mypy_annotate.main( - ["--dumpfile", str(result_file), "--platform", "SomePlatform"] + ["--dumpfile", str(result_file), "--platform", "SomePlatform"], ) std = capsys.readouterr() diff --git a/src/trio/_tests/type_tests/path.py b/src/trio/_tests/type_tests/path.py index 15d25ae954..5c7300fab5 100644 --- a/src/trio/_tests/type_tests/path.py +++ b/src/trio/_tests/type_tests/path.py @@ -113,7 +113,8 @@ async def async_attrs(path: trio.Path) -> None: assert_type(await path.unlink(missing_ok=True), None) assert_type(await path.write_bytes(b"123"), int) assert_type( - await path.write_text("hello", encoding="utf32le", errors="ignore"), int + await path.write_text("hello", encoding="utf32le", errors="ignore"), + int, ) diff --git a/src/trio/_tests/type_tests/raisesgroup.py b/src/trio/_tests/type_tests/raisesgroup.py index fe4053ebc5..e637ace076 100644 --- a/src/trio/_tests/type_tests/raisesgroup.py +++ b/src/trio/_tests/type_tests/raisesgroup.py @@ -64,7 +64,8 @@ def check_matches_with_different_exception_type() -> None: # This should probably raise some type error somewhere, since # ValueError != KeyboardInterrupt e: BaseExceptionGroup[KeyboardInterrupt] = BaseExceptionGroup( - "", (KeyboardInterrupt(),) + "", + (KeyboardInterrupt(),), ) if RaisesGroup(ValueError).matches(e): assert_type(e, BaseExceptionGroup[ValueError]) @@ -192,7 +193,8 @@ def check_nested_raisesgroups_contextmanager() -> None: def check_nested_raisesgroups_matches() -> None: """Check nested RaisesGroups with .matches""" exc: ExceptionGroup[ExceptionGroup[ValueError]] = ExceptionGroup( - "", (ExceptionGroup("", (ValueError(),)),) + "", + (ExceptionGroup("", (ValueError(),)),), ) # has the same problems as check_nested_raisesgroups_contextmanager if RaisesGroup(RaisesGroup(ValueError)).matches(exc): diff --git a/src/trio/_threads.py b/src/trio/_threads.py index 0f0e585003..4cd460078a 100644 --- a/src/trio/_threads.py +++ b/src/trio/_threads.py @@ -150,10 +150,12 @@ class Run(Generic[RetT]): afn: Callable[..., Awaitable[RetT]] args: tuple[object, ...] context: contextvars.Context = attrs.field( - init=False, factory=contextvars.copy_context + init=False, + factory=contextvars.copy_context, ) queue: stdlib_queue.SimpleQueue[outcome.Outcome[RetT]] = attrs.field( - init=False, factory=stdlib_queue.SimpleQueue + init=False, + factory=stdlib_queue.SimpleQueue, ) @disable_ki_protection @@ -190,11 +192,13 @@ def run_in_system_nursery(self, token: TrioToken) -> None: def in_trio_thread() -> None: try: trio.lowlevel.spawn_system_task( - self.run_system, name=self.afn, context=self.context + self.run_system, + name=self.afn, + context=self.context, ) except RuntimeError: # system nursery is closed self.queue.put_nowait( - outcome.Error(trio.RunFinishedError("system nursery is closed")) + outcome.Error(trio.RunFinishedError("system nursery is closed")), ) token.run_sync_soon(in_trio_thread) @@ -205,10 +209,12 @@ class RunSync(Generic[RetT]): fn: Callable[..., RetT] args: tuple[object, ...] context: contextvars.Context = attrs.field( - init=False, factory=contextvars.copy_context + init=False, + factory=contextvars.copy_context, ) queue: stdlib_queue.SimpleQueue[outcome.Outcome[RetT]] = attrs.field( - init=False, factory=stdlib_queue.SimpleQueue + init=False, + factory=stdlib_queue.SimpleQueue, ) @disable_ki_protection @@ -220,7 +226,7 @@ def unprotected_fn(self) -> RetT: ret.close() raise TypeError( "Trio expected a synchronous function, but {!r} appears to be " - "asynchronous".format(getattr(self.fn, "__qualname__", self.fn)) + "asynchronous".format(getattr(self.fn, "__qualname__", self.fn)), ) return ret @@ -386,7 +392,7 @@ def worker_fn() -> RetT: ret.close() raise TypeError( "Trio expected a sync function, but {!r} appears to be " - "asynchronous".format(getattr(sync_fn, "__qualname__", sync_fn)) + "asynchronous".format(getattr(sync_fn, "__qualname__", sync_fn)), ) return ret @@ -441,7 +447,7 @@ def abort(raise_cancel: RaiseCancelT) -> trio.lowlevel.Abort: msg_from_thread.run_sync() else: # pragma: no cover, internal debugging guard TODO: use assert_never raise TypeError( - f"trio.to_thread.run_sync received unrecognized thread message {msg_from_thread!r}." + f"trio.to_thread.run_sync received unrecognized thread message {msg_from_thread!r}.", ) del msg_from_thread @@ -477,14 +483,15 @@ def from_thread_check_cancelled() -> None: raise_cancel = PARENT_TASK_DATA.cancel_register[0] except AttributeError: raise RuntimeError( - "this thread wasn't created by Trio, can't check for cancellation" + "this thread wasn't created by Trio, can't check for cancellation", ) from None if raise_cancel is not None: raise_cancel() def _send_message_to_trio( - trio_token: TrioToken | None, message_to_trio: Run[RetT] | RunSync[RetT] + trio_token: TrioToken | None, + message_to_trio: Run[RetT] | RunSync[RetT], ) -> RetT: """Shared logic of from_thread functions""" token_provided = trio_token is not None @@ -494,7 +501,7 @@ def _send_message_to_trio( trio_token = PARENT_TASK_DATA.token except AttributeError: raise RuntimeError( - "this thread wasn't created by Trio, pass kwarg trio_token=..." + "this thread wasn't created by Trio, pass kwarg trio_token=...", ) from None elif not isinstance(trio_token, TrioToken): raise RuntimeError("Passed kwarg trio_token is not of type TrioToken") diff --git a/src/trio/_timeouts.py b/src/trio/_timeouts.py index 1d03b2f2e3..417f1818fa 100644 --- a/src/trio/_timeouts.py +++ b/src/trio/_timeouts.py @@ -1,51 +1,71 @@ from __future__ import annotations import math -from contextlib import AbstractContextManager, contextmanager -from typing import TYPE_CHECKING +import sys +from contextlib import contextmanager +from typing import TYPE_CHECKING, NoReturn import trio +if TYPE_CHECKING: + from collections.abc import Generator -def move_on_at(deadline: float) -> trio.CancelScope: + +def move_on_at(deadline: float, *, shield: bool = False) -> trio.CancelScope: """Use as a context manager to create a cancel scope with the given absolute deadline. Args: deadline (float): The deadline. + shield (bool): Initial value for the `~trio.CancelScope.shield` attribute + of the newly created cancel scope. Raises: ValueError: if deadline is NaN. """ - if math.isnan(deadline): - raise ValueError("deadline must not be NaN") - return trio.CancelScope(deadline=deadline) + # CancelScope validates that deadline isn't math.nan + return trio.CancelScope(deadline=deadline, shield=shield) -def move_on_after(seconds: float) -> trio.CancelScope: +def move_on_after( + seconds: float, + *, + shield: bool = False, +) -> trio.CancelScope: """Use as a context manager to create a cancel scope whose deadline is set to now + *seconds*. + The deadline of the cancel scope is calculated upon entering. + Args: seconds (float): The timeout. + shield (bool): Initial value for the `~trio.CancelScope.shield` attribute + of the newly created cancel scope. Raises: - ValueError: if timeout is less than zero or NaN. + ValueError: if ``seconds`` is less than zero or NaN. """ + # duplicate validation logic to have the correct parameter name if seconds < 0: - raise ValueError("timeout must be non-negative") - return move_on_at(trio.current_time() + seconds) + raise ValueError("`seconds` must be non-negative") + if math.isnan(seconds): + raise ValueError("`seconds` must not be NaN") + return trio.CancelScope( + shield=shield, + relative_deadline=seconds, + ) -async def sleep_forever() -> None: +async def sleep_forever() -> NoReturn: """Pause execution of the current task forever (or until cancelled). Equivalent to calling ``await sleep(math.inf)``. """ await trio.lowlevel.wait_task_rescheduled(lambda _: trio.lowlevel.Abort.SUCCEEDED) + raise RuntimeError("Should never have been rescheduled!") async def sleep_until(deadline: float) -> None: @@ -80,7 +100,7 @@ async def sleep(seconds: float) -> None: """ if seconds < 0: - raise ValueError("duration must be non-negative") + raise ValueError("`seconds` must be non-negative") if seconds == 0: await trio.lowlevel.checkpoint() else: @@ -94,9 +114,12 @@ class TooSlowError(Exception): """ -# workaround for PyCharm not being able to infer return type from @contextmanager -# see https://youtrack.jetbrains.com/issue/PY-36444/PyCharm-doesnt-infer-types-when-using-contextlib.contextmanager-decorator -def fail_at(deadline: float) -> AbstractContextManager[trio.CancelScope]: # type: ignore[misc] +@contextmanager +def fail_at( + deadline: float, + *, + shield: bool = False, +) -> Generator[trio.CancelScope, None, None]: """Creates a cancel scope with the given deadline, and raises an error if it is actually cancelled. @@ -110,6 +133,8 @@ def fail_at(deadline: float) -> AbstractContextManager[trio.CancelScope]: # typ Args: deadline (float): The deadline. + shield (bool): Initial value for the `~trio.CancelScope.shield` attribute + of the newly created cancel scope. Raises: TooSlowError: if a :exc:`Cancelled` exception is raised in this scope @@ -117,17 +142,18 @@ def fail_at(deadline: float) -> AbstractContextManager[trio.CancelScope]: # typ ValueError: if deadline is NaN. """ - with move_on_at(deadline) as scope: + with move_on_at(deadline, shield=shield) as scope: yield scope if scope.cancelled_caught: raise TooSlowError -if not TYPE_CHECKING: - fail_at = contextmanager(fail_at) - - -def fail_after(seconds: float) -> AbstractContextManager[trio.CancelScope]: +@contextmanager +def fail_after( + seconds: float, + *, + shield: bool = False, +) -> Generator[trio.CancelScope, None, None]: """Creates a cancel scope with the given timeout, and raises an error if it is actually cancelled. @@ -138,8 +164,12 @@ def fail_after(seconds: float) -> AbstractContextManager[trio.CancelScope]: it's caught and discarded. When it reaches :func:`fail_after`, then it's caught and :exc:`TooSlowError` is raised in its place. + The deadline of the cancel scope is calculated upon entering. + Args: seconds (float): The timeout. + shield (bool): Initial value for the `~trio.CancelScope.shield` attribute + of the newly created cancel scope. Raises: TooSlowError: if a :exc:`Cancelled` exception is raised in this scope @@ -147,6 +177,17 @@ def fail_after(seconds: float) -> AbstractContextManager[trio.CancelScope]: ValueError: if *seconds* is less than zero or NaN. """ - if seconds < 0: - raise ValueError("timeout must be non-negative") - return fail_at(trio.current_time() + seconds) + with move_on_after(seconds, shield=shield) as scope: + yield scope + if scope.cancelled_caught: + raise TooSlowError + + +# 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 + import inspect + + for c in (fail_at, fail_after): + c.__signature__ = inspect.Signature.from_callable(c).replace(return_annotation=trio.CancelScope) # type: ignore[union-attr] diff --git a/src/trio/_tools/gen_exports.py b/src/trio/_tools/gen_exports.py index 4ecb29511e..91969d6bfe 100755 --- a/src/trio/_tools/gen_exports.py +++ b/src/trio/_tools/gen_exports.py @@ -191,6 +191,11 @@ def run_linters(file: File, source: str) -> str: print(response) sys.exit(1) + success, response = run_black(file, response) + if not success: + print(response) + sys.exit(1) + return response @@ -211,7 +216,7 @@ def gen_public_wrappers_source(file: File) -> str: if "import sys" not in file.imports: # pragma: no cover header.append("import sys\n") header.append( - f'\nassert not TYPE_CHECKING or sys.platform=="{file.platform}"\n' + f'\nassert not TYPE_CHECKING or sys.platform=="{file.platform}"\n', ) generated = ["".join(header)] @@ -289,27 +294,34 @@ def process(files: Iterable[File], *, do_test: bool) -> None: dirname, basename = os.path.split(file.path) new_path = os.path.join(dirname, PREFIX + basename) new_files[new_path] = new_source + matches_disk = matches_disk_files(new_files) if do_test: - if not matches_disk_files(new_files): + if not matches_disk: print("Generated sources are outdated. Please regenerate.") sys.exit(1) else: 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") as f: + with open(new_path, "w", encoding="utf-8", newline="\n") as f: f.write(new_source) print("Regenerated sources successfully.") + if not matches_disk: + # With pre-commit integration, show that we edited files. + sys.exit(1) # This is in fact run in CI, but only in the formatting check job, which # doesn't collect coverage. def main() -> None: # pragma: no cover parser = argparse.ArgumentParser( - description="Generate python code for public api wrappers" + description="Generate python code for public api wrappers", ) parser.add_argument( - "--test", "-t", action="store_true", help="test if code is still up to date" + "--test", + "-t", + action="store_true", + help="test if code is still up to date", ) parsed_args = parser.parse_args() diff --git a/src/trio/_tools/mypy_annotate.py b/src/trio/_tools/mypy_annotate.py index 6bd20f401c..5acb9b993c 100644 --- a/src/trio/_tools/mypy_annotate.py +++ b/src/trio/_tools/mypy_annotate.py @@ -86,7 +86,9 @@ def main(argv: list[str]) -> None: """Look for error messages, and convert the format.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( - "--dumpfile", help="File to write pickled messages to.", required=True + "--dumpfile", + help="File to write pickled messages to.", + required=True, ) parser.add_argument( "--platform", diff --git a/src/trio/_unix_pipes.py b/src/trio/_unix_pipes.py index 34340d2b36..a95f761bcc 100644 --- a/src/trio/_unix_pipes.py +++ b/src/trio/_unix_pipes.py @@ -121,10 +121,10 @@ class FdStream(Stream): def __init__(self, fd: int) -> None: self._fd_holder = _FdHolder(fd) self._send_conflict_detector = ConflictDetector( - "another task is using this stream for send" + "another task is using this stream for send", ) self._receive_conflict_detector = ConflictDetector( - "another task is using this stream for receive" + "another task is using this stream for receive", ) async def send_all(self, data: bytes) -> None: @@ -147,7 +147,7 @@ async def send_all(self, data: bytes) -> None: except OSError as e: if e.errno == errno.EBADF: raise trio.ClosedResourceError( - "file was already closed" + "file was already closed", ) from None else: raise trio.BrokenResourceError from e @@ -182,7 +182,7 @@ async def receive_some(self, max_bytes: int | None = None) -> bytes: except OSError as exc: if exc.errno == errno.EBADF: raise trio.ClosedResourceError( - "file was already closed" + "file was already closed", ) from None else: raise trio.BrokenResourceError from exc diff --git a/src/trio/_util.py b/src/trio/_util.py index 7c9e194d19..3af8b5cfde 100644 --- a/src/trio/_util.py +++ b/src/trio/_util.py @@ -154,7 +154,7 @@ def _return_value_looks_like_wrong_library(value: object) -> bool: "Instead, you want (notice the parentheses!):\n" "\n" f" trio.run({async_fn.__name__}, ...) # correct!\n" - f" nursery.start_soon({async_fn.__name__}, ...) # correct!" + f" nursery.start_soon({async_fn.__name__}, ...) # correct!", ) from None # Give good error for: nursery.start_soon(future) @@ -163,7 +163,7 @@ def _return_value_looks_like_wrong_library(value: object) -> bool: "Trio was expecting an async function, but instead it got " f"{async_fn!r} – are you trying to use a library written for " "asyncio/twisted/tornado or similar? That won't work " - "without some sort of compatibility shim." + "without some sort of compatibility shim.", ) from None raise @@ -183,19 +183,19 @@ def _return_value_looks_like_wrong_library(value: object) -> bool: raise TypeError( f"Trio got unexpected {coro!r} – are you trying to use a " "library written for asyncio/twisted/tornado or similar? " - "That won't work without some sort of compatibility shim." + "That won't work without some sort of compatibility shim.", ) if inspect.isasyncgen(coro): raise TypeError( "start_soon expected an async function but got an async " - f"generator {coro!r}" + f"generator {coro!r}", ) # Give good error for: nursery.start_soon(some_sync_fn) raise TypeError( "Trio expected an async function, but {!r} appears to be " - "synchronous".format(getattr(async_fn, "__qualname__", async_fn)) + "synchronous".format(getattr(async_fn, "__qualname__", async_fn)), ) return coro @@ -243,7 +243,7 @@ def async_wraps( def decorator(func: CallT) -> CallT: func.__name__ = attr_name - func.__qualname__ = ".".join((cls.__qualname__, attr_name)) + func.__qualname__ = f"{cls.__qualname__}.{attr_name}" func.__doc__ = f"Like :meth:`~{wrapped_cls.__module__}.{wrapped_cls.__qualname__}.{attr_name}`, but async." @@ -253,7 +253,8 @@ def decorator(func: CallT) -> CallT: def fixup_module_metadata( - module_name: str, namespace: collections.abc.Mapping[str, object] + module_name: str, + namespace: collections.abc.Mapping[str, object], ) -> None: seen_ids: set[int] = set() @@ -370,7 +371,7 @@ class SomeClass(metaclass=NoPublicConstructor): def __call__(cls, *args: object, **kwargs: object) -> None: raise TypeError( - f"{cls.__module__}.{cls.__qualname__} has no public constructor" + f"{cls.__module__}.{cls.__qualname__} has no public constructor", ) def _create(cls: type[T], *args: object, **kwargs: object) -> T: diff --git a/src/trio/_version.py b/src/trio/_version.py index 21669c472c..f9218d068c 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.26.2" +__version__ = "0.27.0" diff --git a/src/trio/_windows_pipes.py b/src/trio/_windows_pipes.py index 43592807b8..e1eea1e72d 100644 --- a/src/trio/_windows_pipes.py +++ b/src/trio/_windows_pipes.py @@ -49,7 +49,7 @@ class PipeSendStream(SendStream): def __init__(self, handle: int) -> None: self._handle_holder = _HandleHolder(handle) self._conflict_detector = ConflictDetector( - "another task is currently using this pipe" + "another task is currently using this pipe", ) async def send_all(self, data: bytes) -> None: @@ -93,7 +93,7 @@ class PipeReceiveStream(ReceiveStream): def __init__(self, handle: int) -> None: self._handle_holder = _HandleHolder(handle) self._conflict_detector = ConflictDetector( - "another task is currently using this pipe" + "another task is currently using this pipe", ) async def receive_some(self, max_bytes: int | None = None) -> bytes: @@ -112,12 +112,13 @@ async def receive_some(self, max_bytes: int | None = None) -> bytes: buffer = bytearray(max_bytes) try: size = await _core.readinto_overlapped( - self._handle_holder.handle, buffer + self._handle_holder.handle, + buffer, ) except BrokenPipeError: if self._handle_holder.closed: raise _core.ClosedResourceError( - "another task closed this pipe" + "another task closed this pipe", ) from None # Windows raises BrokenPipeError on one end of a pipe diff --git a/src/trio/lowlevel.py b/src/trio/lowlevel.py index 1df7019637..9e385a0045 100644 --- a/src/trio/lowlevel.py +++ b/src/trio/lowlevel.py @@ -25,6 +25,7 @@ UnboundedQueue as UnboundedQueue, UnboundedQueueStatistics as UnboundedQueueStatistics, add_instrument as add_instrument, + add_parking_lot_breaker as add_parking_lot_breaker, cancel_shielded_checkpoint as cancel_shielded_checkpoint, checkpoint as checkpoint, checkpoint_if_cancelled as checkpoint_if_cancelled, @@ -40,6 +41,7 @@ permanently_detach_coroutine_object as permanently_detach_coroutine_object, reattach_detached_coroutine_object as reattach_detached_coroutine_object, remove_instrument as remove_instrument, + remove_parking_lot_breaker as remove_parking_lot_breaker, reschedule as reschedule, spawn_system_task as spawn_system_task, start_guest_run as start_guest_run, diff --git a/src/trio/socket.py b/src/trio/socket.py index e38501fb60..617f0382c0 100644 --- a/src/trio/socket.py +++ b/src/trio/socket.py @@ -29,7 +29,7 @@ _name: getattr(_stdlib_socket, _name) for _name in _stdlib_socket.__all__ # type: ignore if _name.isupper() and _name not in _bad_symbols - } + }, ) # import the overwrites diff --git a/src/trio/testing/_check_streams.py b/src/trio/testing/_check_streams.py index c54c99c1fe..335c9c1ea5 100644 --- a/src/trio/testing/_check_streams.py +++ b/src/trio/testing/_check_streams.py @@ -57,7 +57,8 @@ async def __aexit__( # on pytest, as the check_* functions are publicly exported. @contextmanager def _assert_raises( - expected_exc: type[BaseException], wrapped: bool = False + expected_exc: type[BaseException], + wrapped: bool = False, ) -> Generator[None, None, None]: __tracebackhide__ = True try: @@ -174,7 +175,8 @@ async def simple_check_wait_send_all_might_not_block( async with _core.open_nursery() as nursery: nursery.start_soon( - simple_check_wait_send_all_might_not_block, nursery.cancel_scope + simple_check_wait_send_all_might_not_block, + nursery.cancel_scope, ) nursery.start_soon(do_receive_some, 1) @@ -467,7 +469,9 @@ async def flipped_clogged_stream_maker() -> tuple[Stream, Stream]: test_data = i.to_bytes(DUPLEX_TEST_SIZE, "little") async def sender( - s: Stream, data: bytes | bytearray | memoryview, seed: int + s: Stream, + data: bytes | bytearray | memoryview, + seed: int, ) -> None: r = random.Random(seed) m = memoryview(data) diff --git a/src/trio/testing/_fake_net.py b/src/trio/testing/_fake_net.py index f8589f3a9c..ed0f6980f7 100644 --- a/src/trio/testing/_fake_net.py +++ b/src/trio/testing/_fake_net.py @@ -99,7 +99,8 @@ def as_python_sockaddr(self) -> tuple[str, int] | tuple[str, int, int, int]: @classmethod def from_python_sockaddr( - cls: type[T_UDPEndpoint], sockaddr: tuple[str, int] | tuple[str, int, int, int] + cls: type[T_UDPEndpoint], + sockaddr: tuple[str, int] | tuple[str, int, int, int], ) -> T_UDPEndpoint: ip, port = sockaddr[:2] return cls(ip=ipaddress.ip_address(ip), port=port) @@ -120,7 +121,9 @@ class UDPPacket: # not used/tested anywhere def reply(self, payload: bytes) -> UDPPacket: # pragma: no cover return UDPPacket( - source=self.destination, destination=self.source, payload=payload + source=self.destination, + destination=self.source, + payload=payload, ) @@ -156,7 +159,9 @@ async def getaddrinfo( raise NotImplementedError("FakeNet doesn't do fake DNS yet") async def getnameinfo( - self, sockaddr: tuple[str, int] | tuple[str, int, int, int], flags: int + self, + sockaddr: tuple[str, int] | tuple[str, int, int, int], + flags: int, ) -> tuple[str, str]: raise NotImplementedError("FakeNet doesn't do fake DNS yet") @@ -256,7 +261,10 @@ def close(self) -> None: self._packet_receiver.close() async def _resolve_address_nocp( - self, address: object, *, local: bool + self, + address: object, + *, + local: bool, ) -> tuple[str, int]: return await trio._socket._resolve_address_nocp( # type: ignore[no-any-return] self.type, @@ -360,7 +368,7 @@ async def _recvmsg_into( raise NotImplementedError( "The code will most likely hang if you try to receive on a fakesocket " "without a binding. If that is not the case, or you explicitly want to " - "test that, remove this warning." + "test that, remove this warning.", ) self._check_closed() @@ -399,11 +407,13 @@ def getpeername(self) -> tuple[str, int] | tuple[str, int, int, int]: self._check_closed() if self._binding is not None: assert hasattr( - self._binding, "remote" + self._binding, + "remote", ), "This method seems to assume that self._binding has a remote UDPEndpoint" if self._binding.remote is not None: # pragma: no cover assert isinstance( - self._binding.remote, UDPEndpoint + self._binding.remote, + UDPEndpoint, ), "Self._binding.remote should be a UDPEndpoint" return self._binding.remote.as_python_sockaddr() _fake_err(errno.ENOTCONN) @@ -415,7 +425,11 @@ def getsockopt(self, /, level: int, optname: int) -> int: ... def getsockopt(self, /, level: int, optname: int, buflen: int) -> bytes: ... def getsockopt( - self, /, level: int, optname: int, buflen: int | None = None + self, + /, + level: int, + optname: int, + buflen: int | None = None, ) -> int | bytes: self._check_closed() raise OSError(f"FakeNet doesn't implement getsockopt({level}, {optname})") @@ -425,7 +439,12 @@ def setsockopt(self, /, level: int, optname: int, value: int | Buffer) -> None: @overload def setsockopt( - self, /, level: int, optname: int, value: None, optlen: int + self, + /, + level: int, + optname: int, + value: None, + optlen: int, ) -> None: ... def setsockopt( @@ -466,7 +485,9 @@ async def send(self, data: Buffer, flags: int = 0) -> int: @overload async def sendto( - self, __data: Buffer, __address: tuple[object, ...] | str | Buffer + self, + __data: Buffer, + __address: tuple[object, ...] | str | Buffer, ) -> int: ... @overload @@ -503,21 +524,31 @@ async def recvfrom(self, bufsize: int, flags: int = 0) -> tuple[bytes, Any]: return data, address async def recvfrom_into( - self, buf: Buffer, nbytes: int = 0, flags: int = 0 + self, + buf: Buffer, + nbytes: int = 0, + flags: int = 0, ) -> tuple[int, Any]: if nbytes != 0 and nbytes != memoryview(buf).nbytes: raise NotImplementedError("partial recvfrom_into") got_nbytes, ancdata, msg_flags, address = await self._recvmsg_into( - [buf], 0, flags + [buf], + 0, + flags, ) return got_nbytes, address async def _recvmsg( - self, bufsize: int, ancbufsize: int = 0, flags: int = 0 + self, + bufsize: int, + ancbufsize: int = 0, + flags: int = 0, ) -> tuple[bytes, list[tuple[int, int, bytes]], int, Any]: buf = bytearray(bufsize) got_nbytes, ancdata, msg_flags, address = await self._recvmsg_into( - [buf], ancbufsize, flags + [buf], + ancbufsize, + flags, ) return (bytes(buf[:got_nbytes]), ancdata, msg_flags, address) diff --git a/src/trio/testing/_memory_streams.py b/src/trio/testing/_memory_streams.py index c9d430a9e6..26ddde8565 100644 --- a/src/trio/testing/_memory_streams.py +++ b/src/trio/testing/_memory_streams.py @@ -29,7 +29,7 @@ def __init__(self) -> None: self._closed = False self._lot = _core.ParkingLot() self._fetch_lock = _util.ConflictDetector( - "another task is already fetching data" + "another task is already fetching data", ) # This object treats "close" as being like closing the send side of a @@ -115,7 +115,7 @@ def __init__( close_hook: SyncHook | None = None, ): self._conflict_detector = _util.ConflictDetector( - "another task is using this stream" + "another task is using this stream", ) self._outgoing = _UnboundedByteQueue() self.send_all_hook = send_all_hook @@ -225,7 +225,7 @@ def __init__( close_hook: SyncHook | None = None, ): self._conflict_detector = _util.ConflictDetector( - "another task is using this stream" + "another task is using this stream", ) self._incoming = _UnboundedByteQueue() self._closed = False @@ -356,7 +356,7 @@ async def async_pump_from_send_stream_to_recv_stream() -> None: def _make_stapled_pair( - one_way_pair: Callable[[], tuple[SendStreamT, ReceiveStreamT]] + one_way_pair: Callable[[], tuple[SendStreamT, ReceiveStreamT]], ) -> tuple[ StapledStream[SendStreamT, ReceiveStreamT], StapledStream[SendStreamT, ReceiveStreamT], @@ -461,10 +461,10 @@ def __init__(self) -> None: self._receiver_waiting = False self._waiters = _core.ParkingLot() self._send_conflict_detector = _util.ConflictDetector( - "another task is already sending" + "another task is already sending", ) self._receive_conflict_detector = _util.ConflictDetector( - "another task is already receiving" + "another task is already receiving", ) def _something_happened(self) -> None: diff --git a/src/trio/testing/_raises_group.py b/src/trio/testing/_raises_group.py index f96dcb2351..551398a302 100644 --- a/src/trio/testing/_raises_group.py +++ b/src/trio/testing/_raises_group.py @@ -14,7 +14,6 @@ overload, ) -from trio._deprecate import warn_deprecated from trio._util import final if TYPE_CHECKING: @@ -28,7 +27,10 @@ from typing_extensions import TypeGuard, TypeVar MatchE = TypeVar( - "MatchE", bound=BaseException, default=BaseException, covariant=True + "MatchE", + bound=BaseException, + default=BaseException, + covariant=True, ) else: from typing import TypeVar @@ -49,12 +51,14 @@ class _ExceptionInfo(Generic[MatchE]): _excinfo: tuple[type[MatchE], MatchE, types.TracebackType] | None def __init__( - self, excinfo: tuple[type[MatchE], MatchE, types.TracebackType] | None + self, + excinfo: tuple[type[MatchE], MatchE, types.TracebackType] | None, ): self._excinfo = excinfo def fill_unfilled( - self, exc_info: tuple[type[MatchE], MatchE, types.TracebackType] + self, + exc_info: tuple[type[MatchE], MatchE, types.TracebackType], ) -> None: """Fill an unfilled ExceptionInfo created with ``for_later()``.""" assert self._excinfo is None, "ExceptionInfo was already filled" @@ -92,7 +96,7 @@ def tb(self) -> types.TracebackType: def exconly(self, tryshort: bool = False) -> str: raise NotImplementedError( - "This is a helper method only available if you use RaisesGroup with the pytest package installed" + "This is a helper method only available if you use RaisesGroup with the pytest package installed", ) def errisinstance( @@ -100,7 +104,7 @@ def errisinstance( exc: builtins.type[BaseException] | tuple[builtins.type[BaseException], ...], ) -> bool: raise NotImplementedError( - "This is a helper method only available if you use RaisesGroup with the pytest package installed" + "This is a helper method only available if you use RaisesGroup with the pytest package installed", ) def getrepr( @@ -114,7 +118,7 @@ def getrepr( chain: bool = True, ) -> ReprExceptionInfo | ExceptionChainRepr: raise NotImplementedError( - "This is a helper method only available if you use RaisesGroup with the pytest package installed" + "This is a helper method only available if you use RaisesGroup with the pytest package installed", ) @@ -139,7 +143,7 @@ def _stringify_exception(exc: BaseException) -> str: [ getattr(exc, "message", str(exc)), *getattr(exc, "__notes__", []), - ] + ], ) @@ -195,7 +199,7 @@ def __init__( raise ValueError("You must specify at least one parameter to match on.") if exception_type is not None and not issubclass(exception_type, BaseException): raise ValueError( - f"exception_type {exception_type} must be a subclass of BaseException" + f"exception_type {exception_type} must be a subclass of BaseException", ) self.exception_type = exception_type self.match: Pattern[str] | None @@ -224,11 +228,13 @@ def matches(self, exception: BaseException) -> TypeGuard[MatchE]: """ if self.exception_type is not None and not isinstance( - exception, self.exception_type + exception, + self.exception_type, ): return False if self.match is not None and not re.search( - self.match, _stringify_exception(exception) + self.match, + _stringify_exception(exception), ): return False # If exception_type is None check() accepts BaseException. @@ -242,7 +248,7 @@ def __str__(self) -> str: if (match := self.match) is not None: # If no flags were specified, discard the redundant re.compile() here. reqs.append( - f"match={match.pattern if match.flags == _regex_no_flags else match!r}" + f"match={match.pattern if match.flags == _regex_no_flags else match!r}", ) if self.check is not None: reqs.append(f"check={self.check!r}") @@ -377,7 +383,6 @@ def __init__( flatten_subgroups: bool = False, match: str | Pattern[str] | None = None, check: Callable[[BaseExceptionGroup[E]], bool] | None = None, - strict: None = None, ): self.expected_exceptions: tuple[type[E] | Matcher[E] | E, ...] = ( exception, @@ -389,27 +394,18 @@ def __init__( self.check = check self.is_baseexceptiongroup = False - if strict is not None: - warn_deprecated( - "The `strict` parameter", - "0.25.1", - issue=2989, - instead="flatten_subgroups=True (for strict=False}", - ) - self.flatten_subgroups = not strict - if allow_unwrapped and other_exceptions: raise ValueError( "You cannot specify multiple exceptions with `allow_unwrapped=True.`" " If you want to match one of multiple possible exceptions you should" " use a `Matcher`." - " E.g. `Matcher(check=lambda e: isinstance(e, (...)))`" + " E.g. `Matcher(check=lambda e: isinstance(e, (...)))`", ) if allow_unwrapped and isinstance(exception, RaisesGroup): raise ValueError( "`allow_unwrapped=True` has no effect when expecting a `RaisesGroup`." " You might want it in the expected `RaisesGroup`, or" - " `flatten_subgroups=True` if you don't care about the structure." + " `flatten_subgroups=True` if you don't care about the structure.", ) if allow_unwrapped and (match is not None or check is not None): raise ValueError( @@ -418,7 +414,7 @@ def __init__( " exception you should use a `Matcher` object. If you want to match/check" " the exceptiongroup when the exception *is* wrapped you need to" " do e.g. `if isinstance(exc.value, ExceptionGroup):" - " assert RaisesGroup(...).matches(exc.value)` afterwards." + " assert RaisesGroup(...).matches(exc.value)` afterwards.", ) # verify `expected_exceptions` and set `self.is_baseexceptiongroup` @@ -429,7 +425,7 @@ def __init__( "You cannot specify a nested structure inside a RaisesGroup with" " `flatten_subgroups=True`. The parameter will flatten subgroups" " in the raised exceptiongroup before matching, which would never" - " match a nested structure." + " match a nested structure.", ) self.is_baseexceptiongroup |= exc.is_baseexceptiongroup elif isinstance(exc, Matcher): @@ -439,14 +435,15 @@ def __init__( continue # Matcher __init__ assures it's a subclass of BaseException self.is_baseexceptiongroup |= not issubclass( - exc.exception_type, Exception + exc.exception_type, + Exception, ) elif isinstance(exc, type) and issubclass(exc, BaseException): self.is_baseexceptiongroup |= not issubclass(exc, Exception) else: raise ValueError( f'Invalid argument "{exc!r}" must be exception type, Matcher, or' - " RaisesGroup." + " RaisesGroup.", ) def __enter__(self) -> ExceptionInfo[BaseExceptionGroup[E]]: @@ -454,7 +451,8 @@ def __enter__(self) -> ExceptionInfo[BaseExceptionGroup[E]]: return self.excinfo def _unroll_exceptions( - self, exceptions: Sequence[BaseException] + self, + exceptions: Sequence[BaseException], ) -> Sequence[BaseException]: """Used if `flatten_subgroups=True`.""" res: list[BaseException] = [] @@ -498,7 +496,8 @@ def matches( return False if self.match_expr is not None and not re.search( - self.match_expr, _stringify_exception(exc_val) + self.match_expr, + _stringify_exception(exc_val), ): return False if self.check is not None and not self.check(exc_val): diff --git a/src/trio/testing/_sequencer.py b/src/trio/testing/_sequencer.py index 9c01e902f2..32171cb2a2 100644 --- a/src/trio/testing/_sequencer.py +++ b/src/trio/testing/_sequencer.py @@ -55,7 +55,8 @@ async def main(): """ _sequence_points: defaultdict[int, Event] = attrs.field( - factory=lambda: defaultdict(Event), init=False + factory=lambda: defaultdict(Event), + init=False, ) _claimed: set[int] = attrs.field(factory=set, init=False) _broken: bool = attrs.field(default=False, init=False) @@ -75,7 +76,7 @@ async def __call__(self, position: int) -> AsyncIterator[None]: for event in self._sequence_points.values(): event.set() raise RuntimeError( - "Sequencer wait cancelled -- sequence broken" + "Sequencer wait cancelled -- sequence broken", ) from None else: if self._broken: diff --git a/src/trio/testing/_trio_test.py b/src/trio/testing/_trio_test.py index a57c0ee4c7..226e559196 100644 --- a/src/trio/testing/_trio_test.py +++ b/src/trio/testing/_trio_test.py @@ -42,7 +42,9 @@ def wrapper(*args: ArgsT.args, **kwargs: ArgsT.kwargs) -> RetT: raise ValueError("too many clocks spoil the broth!") instruments = [i for i in kwargs.values() if isinstance(i, Instrument)] return _core.run( - partial(fn, *args, **kwargs), clock=clock, instruments=instruments + partial(fn, *args, **kwargs), + clock=clock, + instruments=instruments, ) return wrapper diff --git a/test-requirements.in b/test-requirements.in index af8a751b13..227a25afc0 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -6,22 +6,22 @@ pyright pyOpenSSL >= 22.0.0 # for the ssl + DTLS tests trustme # for the ssl + DTLS tests pylint # for pylint finding all symbols tests -jedi # for jedi code completion tests +jedi; implementation_name == "cpython" # for jedi code completion tests cryptography>=41.0.0 # cryptography<41 segfaults on pypy3.10 # Tools black; implementation_name == "cpython" -mypy; implementation_name == "cpython" -types-pyOpenSSL; implementation_name == "cpython" # and annotations +mypy ruff >= 0.4.3 astor # code generation uv >= 0.2.24 codespell # https://github.com/python-trio/trio/pull/654#issuecomment-420518745 -mypy-extensions; implementation_name == "cpython" +mypy-extensions typing-extensions -types-cffi; implementation_name == "cpython" +types-cffi +types-pyOpenSSL # annotations in doc files types-docutils sphinx diff --git a/test-requirements.txt b/test-requirements.txt index 4768062864..a544456455 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,21 +4,21 @@ alabaster==0.7.13 # via sphinx astor==0.8.1 # via -r test-requirements.in -astroid==3.2.2 +astroid==3.2.4 # via pylint async-generator==1.10 # via -r test-requirements.in -attrs==24.1.0 +attrs==24.2.0 # via # -r test-requirements.in # outcome -babel==2.15.0 +babel==2.16.0 # via sphinx -black==24.4.2 ; implementation_name == 'cpython' +black==24.8.0 ; implementation_name == 'cpython' # via -r test-requirements.in -certifi==2024.7.4 +certifi==2024.8.30 # via requests -cffi==1.17.0rc1 ; os_name == 'nt' or platform_python_implementation != 'PyPy' +cffi==1.17.1 ; platform_python_implementation != 'PyPy' or os_name == 'nt' # via # -r test-requirements.in # cryptography @@ -28,42 +28,42 @@ click==8.1.7 ; implementation_name == 'cpython' # via black codespell==2.3.0 # via -r test-requirements.in -colorama==0.4.6 ; sys_platform == 'win32' or (implementation_name == 'cpython' and platform_system == 'Windows') +colorama==0.4.6 ; (implementation_name != 'cpython' and sys_platform == 'win32') or (platform_system != 'Windows' and sys_platform == 'win32') or (implementation_name == 'cpython' and platform_system == 'Windows') # via # click # pylint # pytest # sphinx -coverage==7.5.4 +coverage==7.6.1 # via -r test-requirements.in -cryptography==42.0.8 +cryptography==43.0.1 # via # -r test-requirements.in # pyopenssl # trustme # types-pyopenssl -dill==0.3.8 +dill==0.3.9 # via pylint docutils==0.20.1 # via sphinx -exceptiongroup==1.2.1 ; python_version < '3.11' +exceptiongroup==1.2.2 ; python_full_version < '3.11' # via # -r test-requirements.in # pytest -idna==3.7 +idna==3.10 # via # -r test-requirements.in # requests # trustme imagesize==1.4.1 # via sphinx -importlib-metadata==8.0.0 ; python_version < '3.10' +importlib-metadata==8.5.0 ; python_full_version < '3.10' # via sphinx iniconfig==2.0.0 # via pytest isort==5.13.2 # via pylint -jedi==0.19.1 +jedi==0.19.1 ; implementation_name == 'cpython' # via -r test-requirements.in jinja2==3.1.4 # via sphinx @@ -71,9 +71,9 @@ markupsafe==2.1.5 # via jinja2 mccabe==0.7.0 # via pylint -mypy==1.11.0 ; implementation_name == 'cpython' +mypy==1.11.2 # via -r test-requirements.in -mypy-extensions==1.0.0 ; implementation_name == 'cpython' +mypy-extensions==1.0.0 # via # -r test-requirements.in # black @@ -87,33 +87,33 @@ packaging==24.1 # black # pytest # sphinx -parso==0.8.4 +parso==0.8.4 ; implementation_name == 'cpython' # via jedi pathspec==0.12.1 ; implementation_name == 'cpython' # via black -platformdirs==4.2.2 +platformdirs==4.3.6 # via # black # pylint pluggy==1.5.0 # via pytest -pycparser==2.22 ; os_name == 'nt' or platform_python_implementation != 'PyPy' +pycparser==2.22 ; platform_python_implementation != 'PyPy' or os_name == 'nt' # via cffi pygments==2.18.0 # via sphinx -pylint==3.2.5 +pylint==3.2.7 # via -r test-requirements.in -pyopenssl==24.1.0 +pyopenssl==24.2.1 # via -r test-requirements.in -pyright==1.1.370 +pyright==1.1.382.post1 # via -r test-requirements.in -pytest==8.2.2 +pytest==8.3.3 # via -r test-requirements.in -pytz==2024.1 ; python_version < '3.9' +pytz==2024.2 ; python_full_version < '3.9' # via babel requests==2.32.3 # via sphinx -ruff==0.5.1 +ruff==0.6.8 # via -r test-requirements.in sniffio==1.3.1 # via -r test-requirements.in @@ -135,25 +135,25 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx -tomli==2.0.1 ; python_version < '3.11' +tomli==2.0.1 ; python_full_version < '3.11' # via # black # mypy # pylint # pytest -tomlkit==0.12.5 +tomlkit==0.13.2 # via pylint trustme==1.1.0 # via -r test-requirements.in -types-cffi==1.16.0.20240331 ; implementation_name == 'cpython' +types-cffi==1.16.0.20240331 # via # -r test-requirements.in # types-pyopenssl -types-docutils==0.21.0.20240704 +types-docutils==0.21.0.20240907 # via -r test-requirements.in -types-pyopenssl==24.1.0.20240425 ; implementation_name == 'cpython' +types-pyopenssl==24.1.0.20240722 # via -r test-requirements.in -types-setuptools==70.2.0.20240704 ; implementation_name == 'cpython' +types-setuptools==75.1.0.20240917 # via types-cffi typing-extensions==4.12.2 # via @@ -162,9 +162,10 @@ typing-extensions==4.12.2 # black # mypy # pylint -urllib3==2.2.2 + # pyright +urllib3==2.2.3 # via requests -uv==0.2.26 +uv==0.4.17 # via -r test-requirements.in -zipp==3.19.2 ; python_version < '3.10' +zipp==3.20.2 ; python_full_version < '3.10' # via importlib-metadata