diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1b759918d4..593c26074d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,18 +13,12 @@ repos: rev: 23.9.1 hooks: - id: black - - repo: https://github.com/pycqa/isort - rev: 5.12.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.0.291 hooks: - - id: isort - - repo: https://github.com/pycqa/flake8 - rev: 6.1.0 - hooks: - - id: flake8 - additional_dependencies: - - "flake8-pyproject==1.2.3" + - id: ruff types: [file] - types_or: [python, pyi] + types_or: [python, pyi, toml] - repo: https://github.com/codespell-project/codespell rev: v2.2.5 hooks: @@ -32,8 +26,7 @@ repos: ci: autofix_commit_msg: "[pre-commit.ci] auto fixes from pre-commit.com hooks" - autofix_prs: true + autofix_prs: false autoupdate_commit_msg: "[pre-commit.ci] pre-commit autoupdate" autoupdate_schedule: weekly - skip: [black,isort] submodules: false diff --git a/check.sh b/check.sh index 95049080f3..78ddd04443 100755 --- a/check.sh +++ b/check.sh @@ -2,11 +2,13 @@ set -ex +ON_GITHUB_CI=true EXIT_STATUS=0 # If not running on Github's CI, discard the summaries if [ -z "${GITHUB_STEP_SUMMARY+x}" ]; then GITHUB_STEP_SUMMARY=/dev/null + ON_GITHUB_CI=false fi # Test if the generated code is still up to date @@ -22,7 +24,7 @@ echo "::endgroup::" # pyupgrade --py3-plus $(find . -name "*.py") echo "::group::Black" if ! black --check setup.py trio; then - echo "* Black found issues" >> $GITHUB_STEP_SUMMARY + echo "* Black found issues" >> "$GITHUB_STEP_SUMMARY" EXIT_STATUS=1 black --diff setup.py trio echo "::endgroup::" @@ -31,22 +33,22 @@ else echo "::endgroup::" fi -echo "::group::ISort" -if ! isort --check setup.py trio; then - echo "* isort found issues." >> $GITHUB_STEP_SUMMARY +# Run ruff, configured in pyproject.toml +echo "::group::Ruff" +if ! ruff check .; then + echo "* ruff found issues." >> "$GITHUB_STEP_SUMMARY" EXIT_STATUS=1 - isort --diff setup.py trio + if $ON_GITHUB_CI; then + ruff check --format github --diff . + else + ruff check --diff . + fi echo "::endgroup::" - echo "::error:: isort found issues" + echo "::error:: ruff found issues" else echo "::endgroup::" fi -# Run flake8, configured in pyproject.toml -echo "::group::Flake8" -flake8 trio/ || EXIT_STATUS=$? -echo "::endgroup::" - # Run mypy on all supported platforms # MYPY is set if any of them fail. MYPY=0 @@ -56,12 +58,12 @@ rm -f mypy_annotate.dat # Pipefail makes these pipelines fail if mypy does, even if mypy_annotate.py succeeds. set -o pipefail mypy trio --show-error-end --platform linux | python ./trio/_tools/mypy_annotate.py --dumpfile mypy_annotate.dat --platform Linux \ - || { echo "* Mypy (Linux) found type errors." >> $GITHUB_STEP_SUMMARY; MYPY=1; } + || { echo "* Mypy (Linux) found type errors." >> "$GITHUB_STEP_SUMMARY"; MYPY=1; } # Darwin tests FreeBSD too mypy trio --show-error-end --platform darwin | python ./trio/_tools/mypy_annotate.py --dumpfile mypy_annotate.dat --platform Mac \ - || { echo "* Mypy (Mac) found type errors." >> $GITHUB_STEP_SUMMARY; MYPY=1; } + || { echo "* Mypy (Mac) found type errors." >> "$GITHUB_STEP_SUMMARY"; MYPY=1; } mypy trio --show-error-end --platform win32 | python ./trio/_tools/mypy_annotate.py --dumpfile mypy_annotate.dat --platform Windows \ - || { echo "* Mypy (Windows) found type errors." >> $GITHUB_STEP_SUMMARY; MYPY=1; } + || { echo "* Mypy (Windows) found type errors." >> "$GITHUB_STEP_SUMMARY"; MYPY=1; } set +o pipefail # Re-display errors using Github's syntax, read out of mypy_annotate.dat python ./trio/_tools/mypy_annotate.py --dumpfile mypy_annotate.dat @@ -85,9 +87,9 @@ echo "::endgroup::" if git status --porcelain | grep -q "requirements.txt"; then echo "::error::requirements.txt changed." echo "::group::requirements.txt changed" - echo "* requirements.txt changed" >> $GITHUB_STEP_SUMMARY + echo "* requirements.txt changed" >> "$GITHUB_STEP_SUMMARY" git status --porcelain - git --no-pager diff --color *requirements.txt + git --no-pager diff --color ./*requirements.txt EXIT_STATUS=1 echo "::endgroup::" fi @@ -96,7 +98,7 @@ codespell || EXIT_STATUS=$? python trio/_tests/check_type_completeness.py --overwrite-file || EXIT_STATUS=$? if git status --porcelain trio/_tests/verify_types*.json | grep -q "M"; then - echo "* Type completeness changed, please update!" >> $GITHUB_STEP_SUMMARY + echo "* Type completeness changed, please update!" >> "$GITHUB_STEP_SUMMARY" echo "::error::Type completeness changed, please update!" git --no-pager diff --color trio/_tests/verify_types*.json EXIT_STATUS=1 @@ -123,5 +125,5 @@ in your local checkout. EOF exit 1 fi -echo "# Formatting checks succeeded." >> $GITHUB_STEP_SUMMARY +echo "# Formatting checks succeeded." >> "$GITHUB_STEP_SUMMARY" exit 0 diff --git a/ci.sh b/ci.sh index 863564a943..157b3ce8b2 100755 --- a/ci.sh +++ b/ci.sh @@ -120,9 +120,6 @@ else INSTALLDIR=$(python -c "import os, trio; print(os.path.dirname(trio.__file__))") cp ../pyproject.toml $INSTALLDIR - # TODO: remove this once we have a py.typed file - touch "$INSTALLDIR/py.typed" - # get mypy tests a nice cache MYPYPATH=".." mypy --config-file= --cache-dir=./.mypy_cache -c "import trio" >/dev/null 2>/dev/null || true diff --git a/docs/source/local_customization.py b/docs/source/local_customization.py index f071b6dfbb..96014f46f9 100644 --- a/docs/source/local_customization.py +++ b/docs/source/local_customization.py @@ -1,7 +1,7 @@ -from docutils.parsers.rst import directives as directives # noqa: F401 +from docutils.parsers.rst import directives as directives from sphinx import addnodes from sphinx.domains.python import PyClasslike -from sphinx.ext.autodoc import ( # noqa: F401 +from sphinx.ext.autodoc import ( ClassLevelDocumenter as ClassLevelDocumenter, FunctionDocumenter as FunctionDocumenter, MethodDocumenter as MethodDocumenter, diff --git a/docs/source/typevars.py b/docs/source/typevars.py index ab492b98b8..c98f995a7d 100644 --- a/docs/source/typevars.py +++ b/docs/source/typevars.py @@ -7,13 +7,12 @@ import re from pathlib import Path +import trio from sphinx.addnodes import Element, pending_xref from sphinx.application import Sphinx from sphinx.environment import BuildEnvironment from sphinx.errors import NoUri -import trio - def identify_typevars(trio_folder: Path) -> None: """Record all typevars in trio.""" diff --git a/notes-to-self/aio-guest-test.py b/notes-to-self/aio-guest-test.py index 17d4bfb9e0..7bd92aa309 100644 --- a/notes-to-self/aio-guest-test.py +++ b/notes-to-self/aio-guest-test.py @@ -27,7 +27,7 @@ async def trio_main(): to_trio, from_aio = trio.open_memory_channel(float("inf")) from_trio = asyncio.Queue() - asyncio.create_task(aio_pingpong(from_trio, to_trio)) + _task_ref = asyncio.create_task(aio_pingpong(from_trio, to_trio)) from_trio.put_nowait(0) diff --git a/notes-to-self/atomic-local.py b/notes-to-self/atomic-local.py index 429211eaf6..643bc16c6a 100644 --- a/notes-to-self/atomic-local.py +++ b/notes-to-self/atomic-local.py @@ -20,7 +20,7 @@ def f(): f.__code__.co_code, f.__code__.co_consts, f.__code__.co_names, - f.__code__.co_varnames + (sentinel,), + (*f.__code__.co_varnames, sentinel), f.__code__.co_filename, f.__code__.co_name, f.__code__.co_firstlineno, diff --git a/notes-to-self/graceful-shutdown-idea.py b/notes-to-self/graceful-shutdown-idea.py index b454d7610a..76516b9f53 100644 --- a/notes-to-self/graceful-shutdown-idea.py +++ b/notes-to-self/graceful-shutdown-idea.py @@ -1,3 +1,6 @@ +import signal + +import gsm import trio 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 3189d4d594..c6d356f072 100644 --- a/notes-to-self/how-does-windows-so-reuseaddr-work.py +++ b/notes-to-self/how-does-windows-so-reuseaddr-work.py @@ -62,11 +62,11 @@ def table_entry(mode1, bind_type1, mode2, bind_type2): # default | wildcard | INUSE | Success | ACCESS | Success | INUSE | Success ) -for i, mode1 in enumerate(modes): - for j, bind_type1 in enumerate(bind_types): +for mode1 in modes: + for bind_type1 in bind_types: row = [] - for k, mode2 in enumerate(modes): - for l, bind_type2 in enumerate(bind_types): + for mode2 in modes: + for bind_type2 in bind_types: entry = table_entry(mode1, bind_type1, mode2, bind_type2) row.append(entry) # print(mode1, bind_type1, mode2, bind_type2, entry) diff --git a/notes-to-self/manual-signal-handler.py b/notes-to-self/manual-signal-handler.py index e1b5ee3036..d865fa89ee 100644 --- a/notes-to-self/manual-signal-handler.py +++ b/notes-to-self/manual-signal-handler.py @@ -1,5 +1,7 @@ # How to manually call the SIGINT handler on Windows without using raise() or # similar. +import os +import sys if os.name == "nt": import cffi diff --git a/notes-to-self/win-waitable-timer.py b/notes-to-self/win-waitable-timer.py index 5309f43867..a04028d846 100644 --- a/notes-to-self/win-waitable-timer.py +++ b/notes-to-self/win-waitable-timer.py @@ -27,7 +27,6 @@ from datetime import datetime, timedelta, timezone import cffi - import trio from trio._core._windows_cffi import ffi, kernel32, raise_winerror diff --git a/pyproject.toml b/pyproject.toml index 3fe1372e20..73b110ebac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,27 +10,50 @@ force-exclude = ''' [tool.codespell] ignore-words-list = 'astroid,crasher,asend' -[tool.flake8] -extend-ignore = ['D', 'E', 'W', 'F403', 'F405', 'F821', 'F822'] -per-file-ignores = [ - 'trio/__init__.py: F401', - 'trio/_core/__init__.py: F401', - 'trio/_core/_tests/test_multierror_scripts/*: F401', - 'trio/abc.py: F401', - 'trio/lowlevel.py: F401', - 'trio/socket.py: F401', - 'trio/testing/__init__.py: F401' +[tool.ruff] +target-version = "py38" +respect-gitignore = true +fix = true + +allowed-confusables = ["–"] + +# The directories to consider when resolving first vs. third-party imports. +# Does not control what files to include/exclude! +src = ["trio", "notes-to-self"] + +select = [ + "RUF", # Ruff-specific rules + "E", # Error + "F", # pyflakes + "I", # isort + "YTT", # flake8-2020 ] +extend-ignore = [ + 'F403', # undefined-local-with-import-star + 'F405', # undefined-local-with-import-star-usage + 'E402', # module-import-not-at-top-of-file (usually OS-specific) + 'E501', # line-too-long +] + +include = ["*.py", "*.pyi", "**/pyproject.toml"] -[tool.isort] -combine_as_imports = true -profile = "black" -skip_gitignore = true -skip_glob = [ +extend-exclude = [ "docs/source/reference-*", - "docs/source/tutorial/*" + "docs/source/tutorial/*", ] +[tool.ruff.per-file-ignores] +'trio/__init__.py' = ['F401'] +'trio/_core/__init__.py' = ['F401'] +'trio/_core/_tests/test_multierror_scripts/*' = ['F401'] +'trio/abc.py' = ['F401'] +'trio/lowlevel.py' = ['F401'] +'trio/socket.py' = ['F401'] +'trio/testing/__init__.py' = ['F401'] + +[tool.ruff.isort] +combine-as-imports = true + [tool.mypy] python_version = "3.8" diff --git a/setup.py b/setup.py index dbce61c0fd..ce27d916e8 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,7 @@ from setuptools import find_packages, setup +__version__ = "0.0.0" # Overwritten from _version.py below, needed for linter to identify that this variable is defined. + exec(open("trio/_version.py", encoding="utf-8").read()) LONG_DESC = """\ diff --git a/test-requirements.in b/test-requirements.in index 86e733657f..37fb6b5157 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -13,8 +13,7 @@ cryptography>=41.0.0 # cryptography<41 segfaults on pypy3.10 black; implementation_name == "cpython" mypy; implementation_name == "cpython" types-pyOpenSSL; implementation_name == "cpython" # and annotations -flake8 -flake8-pyproject +ruff >= 0.0.291 astor # code generation pip-tools >= 6.13.0 codespell diff --git a/test-requirements.txt b/test-requirements.txt index 7c85cbb7cb..dd2caf58dc 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -40,12 +40,6 @@ exceptiongroup==1.1.3 ; python_version < "3.11" # via # -r test-requirements.in # pytest -flake8==6.1.0 - # via - # -r test-requirements.in - # flake8-pyproject -flake8-pyproject==1.2.3 - # via -r test-requirements.in idna==3.4 # via # -r test-requirements.in @@ -61,9 +55,7 @@ jedi==0.19.0 lazy-object-proxy==1.9.0 # via astroid mccabe==0.7.0 - # via - # flake8 - # pylint + # via pylint mypy==1.5.1 ; implementation_name == "cpython" # via -r test-requirements.in mypy-extensions==1.0.0 ; implementation_name == "cpython" @@ -92,12 +84,8 @@ platformdirs==3.10.0 # pylint pluggy==1.3.0 # via pytest -pycodestyle==2.11.0 - # via flake8 pycparser==2.21 # via cffi -pyflakes==3.1.0 - # via flake8 pylint==2.17.7 # via -r test-requirements.in pyopenssl==23.2.0 @@ -108,6 +96,8 @@ pyright==1.1.329 # via -r test-requirements.in pytest==7.4.2 # via -r test-requirements.in +ruff==0.0.291 + # via -r test-requirements.in sniffio==1.3.0 # via -r test-requirements.in sortedcontainers==2.4.0 @@ -116,7 +106,6 @@ tomli==2.0.1 # via # black # build - # flake8-pyproject # mypy # pip-tools # pylint diff --git a/trio/__init__.py b/trio/__init__.py index be7de42cde..0574186c5d 100644 --- a/trio/__init__.py +++ b/trio/__init__.py @@ -15,7 +15,7 @@ # innocuous bits of the _core API + the higher-level tools from trio/*.py. # # Uses `from x import y as y` for compatibility with `pyright --verifytypes` (#2625) - +# # must be imported early to avoid circular import from ._core import TASK_STATUS_IGNORED as TASK_STATUS_IGNORED # isort: split diff --git a/trio/_channel.py b/trio/_channel.py index 999a634394..48f80daf1c 100644 --- a/trio/_channel.py +++ b/trio/_channel.py @@ -3,8 +3,11 @@ from collections import OrderedDict, deque from math import inf from types import TracebackType -from typing import Tuple # only needed for typechecking on <3.9 -from typing import TYPE_CHECKING, Generic +from typing import ( + TYPE_CHECKING, + Generic, + Tuple, # only needed for typechecking on <3.9 +) import attr from outcome import Error, Value diff --git a/trio/_core/_generated_io_epoll.py b/trio/_core/_generated_io_epoll.py index af73bd21cf..6df3950a6d 100644 --- a/trio/_core/_generated_io_epoll.py +++ b/trio/_core/_generated_io_epoll.py @@ -10,7 +10,6 @@ if TYPE_CHECKING: from .._file_io import _HasFileNo - import sys assert not TYPE_CHECKING or sys.platform == "linux" diff --git a/trio/_core/_generated_io_kqueue.py b/trio/_core/_generated_io_kqueue.py index a0883d0179..6367bc654d 100644 --- a/trio/_core/_generated_io_kqueue.py +++ b/trio/_core/_generated_io_kqueue.py @@ -11,11 +11,9 @@ if TYPE_CHECKING: import select - from ._traps import Abort, RaiseCancelT - from .. import _core from .._file_io import _HasFileNo - + from ._traps import Abort, RaiseCancelT import sys assert not TYPE_CHECKING or sys.platform == "darwin" diff --git a/trio/_core/_generated_io_windows.py b/trio/_core/_generated_io_windows.py index ca444373fa..4ed97a21db 100644 --- a/trio/_core/_generated_io_windows.py +++ b/trio/_core/_generated_io_windows.py @@ -9,12 +9,11 @@ from ._run import GLOBAL_RUN_CONTEXT if TYPE_CHECKING: - from .._file_io import _HasFileNo - from ._windows_cffi import Handle, CData from typing_extensions import Buffer + from .._file_io import _HasFileNo from ._unbounded_queue import UnboundedQueue - + from ._windows_cffi import CData, Handle import sys assert not TYPE_CHECKING or sys.platform == "win32" diff --git a/trio/_core/_instrumentation.py b/trio/_core/_instrumentation.py index a0757a5b83..c1063b0e3e 100644 --- a/trio/_core/_instrumentation.py +++ b/trio/_core/_instrumentation.py @@ -98,7 +98,7 @@ def call(self, hookname: str, *args: Any) -> None: for instrument in list(self[hookname]): try: getattr(instrument, hookname)(*args) - except: + except BaseException: self.remove_instrument(instrument) INSTRUMENT_LOGGER.exception( "Exception raised when calling %r on instrument %r. " diff --git a/trio/_core/_multierror.py b/trio/_core/_multierror.py index 45d725f2c6..05eaad33e3 100644 --- a/trio/_core/_multierror.py +++ b/trio/_core/_multierror.py @@ -3,7 +3,7 @@ import sys from collections.abc import Callable, Sequence from types import TracebackType -from typing import TYPE_CHECKING, Any, cast, overload +from typing import TYPE_CHECKING, Any, ClassVar, cast, overload import attr @@ -87,7 +87,9 @@ def filter_tree( new_exceptions = [] changed = False for child_exc in exc.exceptions: - new_child_exc = filter_tree(child_exc, preserved) + new_child_exc = filter_tree( # noqa: F821 # Deleted in local scope below, causes ruff to think it's not defined (astral-sh/ruff#7733) + child_exc, preserved + ) if new_child_exc is not child_exc: changed = True if new_child_exc is not None: @@ -114,7 +116,9 @@ def push_tb_down( new_tb = concat_tb(tb, exc.__traceback__) if isinstance(exc, MultiError): for child_exc in exc.exceptions: - push_tb_down(new_tb, child_exc, preserved) + push_tb_down( # noqa: F821 # Deleted in local scope below, causes ruff to think it's not defined (astral-sh/ruff#7733) + new_tb, child_exc, preserved + ) exc.__traceback__ = None else: exc.__traceback__ = new_tb @@ -371,7 +375,7 @@ class NonBaseMultiError(MultiError, _ExceptionGroup): import _ctypes class CTraceback(ctypes.Structure): - _fields_ = [ + _fields_: ClassVar = [ ("PyObject_HEAD", ctypes.c_byte * object().__sizeof__()), ("tb_next", ctypes.c_void_p), ("tb_frame", ctypes.c_void_p), diff --git a/trio/_core/_run.py b/trio/_core/_run.py index ecef4f4b06..5a07ad9f01 100644 --- a/trio/_core/_run.py +++ b/trio/_core/_run.py @@ -1478,6 +1478,8 @@ class RunStatistics: # So this object can reference Runner, but Runner can't reference it. The only # references to it are the "in flight" callback chain on the host loop / # worker thread. + + @attr.s(eq=False, hash=False, slots=True) class GuestState: runner: Runner = attr.ib() @@ -1485,8 +1487,7 @@ class GuestState: run_sync_soon_not_threadsafe: Callable[[Callable[[], object]], object] = attr.ib() done_callback: Callable[[Outcome[Any]], object] = attr.ib() unrolled_run_gen: Generator[float, EventResult, None] = attr.ib() - _value_factory: Callable[[], Value[Any]] = lambda: Value(None) - unrolled_run_next_send: Outcome[Any] = attr.ib(factory=_value_factory) + unrolled_run_next_send: Outcome[Any] = attr.ib(factory=lambda: Value(None)) def guest_tick(self) -> None: prev_library, sniffio_library.name = sniffio_library.name, "trio" diff --git a/trio/_core/_tests/test_asyncgen.py b/trio/_core/_tests/test_asyncgen.py index 7e6a5fb4b9..bc7848df8e 100644 --- a/trio/_core/_tests/test_asyncgen.py +++ b/trio/_core/_tests/test_asyncgen.py @@ -182,7 +182,7 @@ async def async_main() -> None: assert record == [] _core.run(async_main) - assert record == ["innermost"] + list(range(100)) + assert record == ["innermost", *range(100)] @restore_unraisablehook() diff --git a/trio/_core/_tests/test_guest_mode.py b/trio/_core/_tests/test_guest_mode.py index 6b1bc2df51..80180be805 100644 --- a/trio/_core/_tests/test_guest_mode.py +++ b/trio/_core/_tests/test_guest_mode.py @@ -32,6 +32,7 @@ def trivial_guest_run(trio_fn, *, in_host_after_start=None, **start_guest_run_kw host_thread = threading.current_thread() def run_sync_soon_threadsafe(fn): + nonlocal todo if host_thread is threading.current_thread(): # pragma: no cover crash = partial( pytest.fail, "run_sync_soon_threadsafe called from host thread" @@ -40,6 +41,7 @@ def run_sync_soon_threadsafe(fn): todo.put(("run", fn)) def run_sync_soon_not_threadsafe(fn): + 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" @@ -48,6 +50,7 @@ def run_sync_soon_not_threadsafe(fn): todo.put(("run", fn)) def done_callback(outcome): + nonlocal todo todo.put(("unwrap", outcome)) trio.lowlevel.start_guest_run( diff --git a/trio/_core/_tests/test_io.py b/trio/_core/_tests/test_io.py index 7a4689d3c1..039dfbef01 100644 --- a/trio/_core/_tests/test_io.py +++ b/trio/_core/_tests/test_io.py @@ -424,7 +424,7 @@ async def allow_OSError( # do that has been pulled out from under our feet... so test that we can # survive this. a, b = stdlib_socket.socketpair() - with a, b, a.dup() as a2: # noqa: F841 + with a, b, a.dup() as a2: a.setblocking(False) b.setblocking(False) fill_socket(a) @@ -439,7 +439,7 @@ async def allow_OSError( # arriving, not a cancellation, so the operation gets re-issued from # handle_io context rather than abort context. a, b = stdlib_socket.socketpair() - with a, b, a.dup() as a2: # noqa: F841 + with a, b, a.dup() as a2: print(f"a={a.fileno()}, b={b.fileno()}, a2={a2.fileno()}") a.setblocking(False) b.setblocking(False) diff --git a/trio/_core/_tests/test_ki.py b/trio/_core/_tests/test_ki.py index b6eef68e22..dc9f2f51e5 100644 --- a/trio/_core/_tests/test_ki.py +++ b/trio/_core/_tests/test_ki.py @@ -212,7 +212,7 @@ async def agen_unprotected(): async def _check_agen(agen_fn): - async for _ in agen_fn(): # noqa + async for _ in agen_fn(): assert not _core.currently_ki_protected() # asynccontextmanager insists that the function passed must itself be an diff --git a/trio/_core/_tests/test_run.py b/trio/_core/_tests/test_run.py index 5c45cf828f..f67f83a4b8 100644 --- a/trio/_core/_tests/test_run.py +++ b/trio/_core/_tests/test_run.py @@ -2015,7 +2015,7 @@ async def test_Nursery_init() -> None: async def test_Nursery_private_init() -> None: # context manager creation should not raise async with _core.open_nursery() as nursery: - assert False == nursery._closed + assert not nursery._closed def test_Nursery_subclass() -> None: diff --git a/trio/_core/_tests/test_unbounded_queue.py b/trio/_core/_tests/test_unbounded_queue.py index 801c34ce46..cffeed1618 100644 --- a/trio/_core/_tests/test_unbounded_queue.py +++ b/trio/_core/_tests/test_unbounded_queue.py @@ -123,7 +123,7 @@ async def test_UnboundedQueue_trivial_yields(): q.put_nowait(None) with assert_checkpoints(): - async for _ in q: # noqa # pragma: no branch + async for _ in q: # pragma: no branch break diff --git a/trio/_path.py b/trio/_path.py index f46b047cf9..a2d171d23b 100644 --- a/trio/_path.py +++ b/trio/_path.py @@ -239,7 +239,7 @@ def __dir__(self) -> list[str]: return [*super().__dir__(), *self._forward] def __repr__(self) -> str: - return f"trio.Path({repr(str(self))})" + return f"trio.Path({str(self)!r})" def __fspath__(self) -> str: return os.fspath(self._wrapped) diff --git a/trio/_socket.py b/trio/_socket.py index 77731708ba..8281a8db62 100644 --- a/trio/_socket.py +++ b/trio/_socket.py @@ -1123,7 +1123,7 @@ def recvmsg( ) -> Awaitable[tuple[bytes, list[tuple[int, int, bytes]], int, Any]]: ... - recvmsg = _make_simple_sock_method_wrapper( # noqa: F811 + recvmsg = _make_simple_sock_method_wrapper( _stdlib_socket.socket.recvmsg, _core.wait_readable, maybe_avail=True ) @@ -1144,7 +1144,7 @@ def recvmsg_into( ) -> Awaitable[tuple[int, list[tuple[int, int, bytes]], int, Any]]: ... - recvmsg_into = _make_simple_sock_method_wrapper( # noqa: F811 + recvmsg_into = _make_simple_sock_method_wrapper( _stdlib_socket.socket.recvmsg_into, _core.wait_readable, maybe_avail=True ) diff --git a/trio/_ssl.py b/trio/_ssl.py index c31383cec5..21c4deabb3 100644 --- a/trio/_ssl.py +++ b/trio/_ssl.py @@ -4,7 +4,7 @@ import ssl as _stdlib_ssl from collections.abc import Awaitable, Callable from enum import Enum as _Enum -from typing import Any, Final as TFinal, TypeVar +from typing import Any, ClassVar, Final as TFinal, TypeVar import trio @@ -378,7 +378,7 @@ def __init__( self._estimated_receive_size = STARTING_RECEIVE_SIZE - _forwarded = { + _forwarded: ClassVar = { "context", "server_side", "server_hostname", @@ -395,7 +395,7 @@ def __init__( "version", } - _after_handshake = { + _after_handshake: ClassVar = { "session_reused", "getpeercert", "selected_npn_protocol", diff --git a/trio/_subprocess_platform/__init__.py b/trio/_subprocess_platform/__init__.py index b6767af8f5..793d8d7f23 100644 --- a/trio/_subprocess_platform/__init__.py +++ b/trio/_subprocess_platform/__init__.py @@ -72,10 +72,10 @@ def create_pipe_from_child_output() -> Tuple["ClosableReceiveStream", int]: if sys.platform == "win32": from .windows import wait_child_exiting # noqa: F811 elif sys.platform != "linux" and (TYPE_CHECKING or hasattr(_core, "wait_kevent")): - from .kqueue import wait_child_exiting # noqa: F811 + from .kqueue import wait_child_exiting else: - # noqa'd as it's an exported symbol - from .waitid import wait_child_exiting # noqa: F811, F401 + # as it's an exported symbol, noqa'd + from .waitid import wait_child_exiting # noqa: F401 except ImportError as ex: # pragma: no cover _wait_child_exiting_error = ex @@ -107,12 +107,12 @@ def create_pipe_from_child_output(): # noqa: F811 from .._windows_pipes import PipeReceiveStream, PipeSendStream - def create_pipe_to_child_stdin(): # noqa: F811 + def create_pipe_to_child_stdin(): # for stdin, we want the write end (our end) to use overlapped I/O rh, wh = windows_pipe(overlapped=(False, True)) return PipeSendStream(wh), msvcrt.open_osfhandle(rh, os.O_RDONLY) - def create_pipe_from_child_output(): # noqa: F811 + def create_pipe_from_child_output(): # for stdout/err, it's the read end that's overlapped rh, wh = windows_pipe(overlapped=(True, False)) return PipeReceiveStream(rh), msvcrt.open_osfhandle(wh, 0) diff --git a/trio/_subprocess_platform/kqueue.py b/trio/_subprocess_platform/kqueue.py index efd0562fc2..a65045670f 100644 --- a/trio/_subprocess_platform/kqueue.py +++ b/trio/_subprocess_platform/kqueue.py @@ -19,9 +19,10 @@ async def wait_child_exiting(process: "_subprocess.Process") -> None: # I verified this value against both Darwin and FreeBSD KQ_NOTE_EXIT = 0x80000000 - make_event = lambda flags: select.kevent( - process.pid, filter=select.KQ_FILTER_PROC, flags=flags, fflags=KQ_NOTE_EXIT - ) + def make_event(flags: int) -> select.kevent: + return select.kevent( + process.pid, filter=select.KQ_FILTER_PROC, flags=flags, fflags=KQ_NOTE_EXIT + ) try: kqueue.control([make_event(select.KQ_EV_ADD | select.KQ_EV_ONESHOT)], 0) diff --git a/trio/_sync.py b/trio/_sync.py index df0a44c3dc..58a265cd97 100644 --- a/trio/_sync.py +++ b/trio/_sync.py @@ -748,7 +748,7 @@ class Condition(AsyncContextManagerMixin): def __init__(self, lock: Lock | None = None): if lock is None: lock = Lock() - if not type(lock) is Lock: + if type(lock) is not Lock: raise TypeError("lock must be a trio.Lock") self._lock = lock self._lot = trio.lowlevel.ParkingLot() diff --git a/trio/_tests/test_exports.py b/trio/_tests/test_exports.py index 4512192850..c3d8a03b63 100644 --- a/trio/_tests/test_exports.py +++ b/trio/_tests/test_exports.py @@ -1,4 +1,5 @@ from __future__ import annotations # isort: split + import __future__ # Regular import, not special! import functools diff --git a/trio/_tests/test_highlevel_open_tcp_listeners.py b/trio/_tests/test_highlevel_open_tcp_listeners.py index 6f39446c7e..2c190debc0 100644 --- a/trio/_tests/test_highlevel_open_tcp_listeners.py +++ b/trio/_tests/test_highlevel_open_tcp_listeners.py @@ -49,7 +49,7 @@ async def test_open_tcp_listeners_basic() -> None: assert await c1.receive_some(1) == b"x" assert await c2.receive_some(1) == b"x" - for resource in [c1, c2, s1, s2] + listeners: + for resource in [c1, c2, s1, s2, *listeners]: await resource.aclose() diff --git a/trio/_tests/test_highlevel_open_tcp_stream.py b/trio/_tests/test_highlevel_open_tcp_stream.py index eb7929c2db..f875bfa019 100644 --- a/trio/_tests/test_highlevel_open_tcp_stream.py +++ b/trio/_tests/test_highlevel_open_tcp_stream.py @@ -329,11 +329,11 @@ def check(self, succeeded: SocketType | None) -> None: # all the sockets that were created did in fact go in there. assert self.socket_count == len(self.sockets) - for ip, socket in self.sockets.items(): + for ip, socket_ in self.sockets.items(): assert ip in self.ip_dict - if socket is not succeeded: - assert socket.closed - assert socket.port == self.port + if socket_ is not succeeded: + assert socket_.closed + assert socket_.port == self.port async def run_scenario( diff --git a/trio/_tests/test_highlevel_socket.py b/trio/_tests/test_highlevel_socket.py index 830a153c00..514b6ad196 100644 --- a/trio/_tests/test_highlevel_socket.py +++ b/trio/_tests/test_highlevel_socket.py @@ -278,8 +278,8 @@ async def accept(self) -> tuple[SocketType, object]: listener = SocketListener(fake_listen_sock) with assert_checkpoints(): - s = await listener.accept() - assert s.socket is fake_server_sock + stream = await listener.accept() + assert stream.socket is fake_server_sock for code in [errno.EMFILE, errno.EFAULT, errno.ENOBUFS]: with assert_checkpoints(): @@ -288,8 +288,8 @@ async def accept(self) -> tuple[SocketType, object]: assert excinfo.value.errno == code with assert_checkpoints(): - s = await listener.accept() - assert s.socket is fake_server_sock + stream = await listener.accept() + assert stream.socket is fake_server_sock async def test_socket_stream_works_when_peer_has_already_closed() -> None: diff --git a/trio/_tests/test_highlevel_ssl_helpers.py b/trio/_tests/test_highlevel_ssl_helpers.py index f6eda0b578..89d921476a 100644 --- a/trio/_tests/test_highlevel_ssl_helpers.py +++ b/trio/_tests/test_highlevel_ssl_helpers.py @@ -13,7 +13,7 @@ serve_ssl_over_tcp, ) -# noqa is needed because flake8 doesn't understand how pytest fixtures work. +# using noqa because linters don't understand how pytest fixtures work. from .test_ssl import SERVER_CTX, client_ctx # noqa: F401 @@ -43,7 +43,7 @@ async def getnameinfo(self, *args): # pragma: no cover # This uses serve_ssl_over_tcp, which uses open_ssl_over_tcp_listeners... -# noqa is needed because flake8 doesn't understand how pytest fixtures work. +# using noqa because linters don't understand how pytest fixtures work. async def test_open_ssl_over_tcp_stream_and_everything_else(client_ctx): # noqa: F811 async with trio.open_nursery() as nursery: (listener,) = await nursery.start( diff --git a/trio/_tests/test_signals.py b/trio/_tests/test_signals.py index 1e42239e35..d0f1bd1c74 100644 --- a/trio/_tests/test_signals.py +++ b/trio/_tests/test_signals.py @@ -108,7 +108,7 @@ async def test_open_signal_receiver_no_starvation() -> None: # Clear out the last signal so that it doesn't get redelivered while get_pending_signal_count(receiver) != 0: await receiver.__anext__() - except: # pragma: no cover + except BaseException: # pragma: no cover # If there's an unhandled exception above, then exiting the # open_signal_receiver block might cause the signal to be # redelivered and give us a core dump instead of a traceback... diff --git a/trio/_tests/test_subprocess.py b/trio/_tests/test_subprocess.py index 7986dfd71e..17cf740012 100644 --- a/trio/_tests/test_subprocess.py +++ b/trio/_tests/test_subprocess.py @@ -44,7 +44,7 @@ # Since Windows has very few command-line utilities generally available, # all of our subprocesses are Python processes running short bits of # (mostly) cross-platform code. -def python(code): +def python(code: str) -> list[str]: return [sys.executable, "-u", "-c", "import sys; " + code] @@ -53,9 +53,14 @@ def python(code): CAT = python("sys.stdout.buffer.write(sys.stdin.buffer.read())") if posix: - SLEEP = lambda seconds: ["/bin/sleep", str(seconds)] + + def SLEEP(seconds: int) -> list[str]: + return ["/bin/sleep", str(seconds)] + else: - SLEEP = lambda seconds: python(f"import time; time.sleep({seconds})") + + def SLEEP(seconds: int) -> list[str]: + return python(f"import time; time.sleep({seconds})") def got_signal(proc, sig): diff --git a/trio/_tests/tools/test_gen_exports.py b/trio/_tests/tools/test_gen_exports.py index 007bfbb097..b4e23916a0 100644 --- a/trio/_tests/tools/test_gen_exports.py +++ b/trio/_tests/tools/test_gen_exports.py @@ -2,6 +2,7 @@ import sys import pytest + from trio._tests.pytest_plugin import skip_if_optional_else_raise # imports in gen_exports that are not in `install_requires` in setup.py @@ -17,7 +18,9 @@ create_passthrough_args, get_public_methods, process, + run_black, run_linters, + run_ruff, ) SOURCE = '''from _run import _public @@ -119,12 +122,55 @@ def test_process(tmp_path, imports): assert excinfo.value.code == 1 +@skip_lints +def test_run_black(tmp_path) -> None: + """Test that processing properly fails if black does.""" + try: + import black # noqa: F401 + except ImportError as error: # pragma: no cover + skip_if_optional_else_raise(error) + + file = File(tmp_path / "module.py", "module") + + success, _ = run_black(file, "class not valid code ><") + assert not success + + success, _ = run_black(file, "import waffle\n;import trio") + assert not success + + +@skip_lints +def test_run_ruff(tmp_path) -> None: + """Test that processing properly fails if black does.""" + try: + import ruff # noqa: F401 + except ImportError as error: # pragma: no cover + skip_if_optional_else_raise(error) + + file = File(tmp_path / "module.py", "module") + + success, _ = run_ruff(file, "class not valid code ><") + assert not success + + test_function = '''def combine_and(data: list[str]) -> str: + """Join values of text, and have 'and' with the last one properly.""" + if len(data) >= 2: + data[-1] = 'and ' + data[-1] + if len(data) > 2: + return ', '.join(data) + return ' '.join(data)''' + + success, response = run_ruff(file, test_function) + assert success + assert response == test_function + + @skip_lints def test_lint_failure(tmp_path) -> None: - """Test that processing properly fails if black or isort does.""" + """Test that processing properly fails if black or ruff does.""" try: import black # noqa: F401 - # there's no dedicated CI run that has astor+isort, but lacks black. + import ruff # noqa: F401 except ImportError as error: # pragma: no cover skip_if_optional_else_raise(error) @@ -134,4 +180,4 @@ def test_lint_failure(tmp_path) -> None: run_linters(file, "class not valid code ><") with pytest.raises(SystemExit): - run_linters(file, "# isort: skip_file") + run_linters(file, "import waffle\n;import trio") diff --git a/trio/_tools/gen_exports.py b/trio/_tools/gen_exports.py index 40ac4c0602..153070bfc9 100755 --- a/trio/_tools/gen_exports.py +++ b/trio/_tools/gen_exports.py @@ -10,7 +10,6 @@ import os import subprocess import sys -import traceback from collections.abc import Iterable, Iterator from pathlib import Path from textwrap import indent @@ -24,8 +23,6 @@ # keep these imports up to date with conditional imports in test_gen_exports # isort: split import astor -import isort.api -import isort.exceptions PREFIX = "_generated" @@ -105,42 +102,96 @@ def create_passthrough_args(funcdef: ast.FunctionDef | ast.AsyncFunctionDef) -> return "({})".format(", ".join(call_args)) -def run_linters(file: File, source: str) -> str: - """Run isort and black on the specified file, returning the new source. +def run_black(file: File, source: str) -> tuple[bool, str]: + """Run black on the specified file. + + Returns: + Tuple of success and result string. + ex.: + (False, "Failed to run black!\nerror: cannot format ...") + (True, "") - :raises ImportError: If black is not installed - :raises SystemExit: If either failed. + Raises: + ImportError: If black is not installed. """ - # imported to check that `subprocess` calls to black will succeed + # imported to check that `subprocess` calls will succeed import black # noqa: F401 # Black has an undocumented API, but it doesn't easily allow reading configuration from # pyproject.toml, and simultaneously pass in / receive the code as a string. # https://github.com/psf/black/issues/779 - try: - result = subprocess.run( - # "-" as a filename = use stdin, return on stdout. - [sys.executable, "-m", "black", "--stdin-filename", file.path, "-"], - input=source, - capture_output=True, - encoding="utf8", - check=True, - ) - except subprocess.CalledProcessError as exc: - print("Failed to run black!") - traceback.print_exception(type(exc), exc, exc.__traceback__) + result = subprocess.run( + # "-" as a filename = use stdin, return on stdout. + [sys.executable, "-m", "black", "--stdin-filename", file.path, "-"], + input=source, + capture_output=True, + encoding="utf8", + ) + + if result.returncode != 0: + return False, f"Failed to run black!\n{result.stderr}" + return True, result.stdout + + +def run_ruff(file: File, source: str) -> tuple[bool, str]: + """Run ruff on the specified file. + + Returns: + Tuple of success and result string. + ex.: + (False, "Failed to run ruff!\nerror: Failed to parse ...") + (True, "") + + Raises: + ImportError: If ruff is not installed. + """ + # imported to check that `subprocess` calls will succeed + import ruff # noqa: F401 + + result = subprocess.run( + # "-" as a filename = use stdin, return on stdout. + [ + sys.executable, + "-m", + "ruff", + "--fix-only", + "--output-format=text", + "--stdin-filename", + file.path, + "-", + ], + input=source, + capture_output=True, + encoding="utf8", + ) + + if result.returncode != 0 or result.stderr: + return False, f"Failed to run ruff!\n{result.stderr}" + return True, result.stdout + + +def run_linters(file: File, source: str) -> str: + """Format the specified file using black and ruff. + + Returns: + Formatted source code. + + Raises: + ImportError: If either is not installed. + SystemExit: If either failed. + """ + + success, response = run_black(file, source) + if not success: + print(response) sys.exit(1) - # isort does have a public API, makes things easy. - try: - isort_res = isort.api.sort_code_string( - result.stdout, - file_path=file.path, - ) - except isort.exceptions.ISortError as exc: - print("Failed to run isort!") - traceback.print_exception(type(exc), exc, exc.__traceback__) + + success, response = run_ruff(file, response) + if not success: # pragma: no cover # Test for run_ruff should catch + print(response) sys.exit(1) - return isort_res + + return response def gen_public_wrappers_source(file: File) -> str: @@ -318,11 +369,9 @@ def main() -> None: # pragma: no cover if TYPE_CHECKING: import select - from ._traps import Abort, RaiseCancelT - from .. import _core + from ._traps import Abort, RaiseCancelT from .._file_io import _HasFileNo - """ IMPORTS_WINDOWS = """\