Skip to content

Commit a6bcda1

Browse files
authored
Merge branch 'main' into raisesgroup-typing
2 parents ed8d8b1 + 7d9c4a6 commit a6bcda1

15 files changed

+119
-36
lines changed

.github/workflows/ci.yml

+6-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
strategy:
1919
fail-fast: false
2020
matrix:
21-
python: ['pypy-3.10', '3.9', '3.10', '3.11', '3.12', '3.13']
21+
python: ['3.9', '3.10', '3.11', '3.12', '3.13']
2222
arch: ['x86', 'x64']
2323
lsp: ['']
2424
lsp_extract_file: ['']
@@ -34,6 +34,11 @@ jobs:
3434
lsp: 'https://www.proxifier.com/download/legacy/ProxifierSetup342.exe'
3535
lsp_extract_file: ''
3636
extra_name: ', with IFS LSP'
37+
- python: 'pypy-3.10'
38+
arch: 'x64'
39+
lsp: ''
40+
lsp_extract_file: ''
41+
extra_name: ''
3742
#- python: '3.9'
3843
# arch: 'x64'
3944
# lsp: 'http://download.pctools.com/mirror/updates/9.0.0.2308-SDavfree-lite_en.exe'

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ repos:
2222
hooks:
2323
- id: black
2424
- repo: https://github.com/astral-sh/ruff-pre-commit
25-
rev: v0.7.4
25+
rev: v0.8.0
2626
hooks:
2727
- id: ruff
2828
types: [file]

newsfragments/3087.doc.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve error message when run after gevent's monkey patching.

newsfragments/3112.bugfix.rst

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Rework foreign async generator finalization to track async generator
2+
ids rather than mutating ``ag_frame.f_locals``. This fixes an issue
3+
with the previous implementation: locals' lifetimes will no longer be
4+
extended by materialization in the ``ag_frame.f_locals`` dictionary that
5+
the previous finalization dispatcher logic needed to access to do its work.

pyproject.toml

+1-3
Original file line numberDiff line numberDiff line change
@@ -125,15 +125,13 @@ select = [
125125
"Q", # flake8-quotes
126126
"RUF", # Ruff-specific rules
127127
"SIM", # flake8-simplify
128-
"TCH", # flake8-type-checking
128+
"TC", # flake8-type-checking
129129
"UP", # pyupgrade
130130
"W", # Warning
131131
"YTT", # flake8-2020
132132
]
133133
extend-ignore = [
134134
'A002', # builtin-argument-shadowing
135-
'ANN101', # missing-type-self
136-
'ANN102', # missing-type-cls
137135
'ANN401', # any-type (mypy's `disallow_any_explicit` is better)
138136
'E402', # module-import-not-at-top-of-file (usually OS-specific)
139137
'E501', # line-too-long

src/trio/_core/_asyncgens.py

+36-10
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import sys
55
import warnings
66
import weakref
7-
from typing import TYPE_CHECKING, NoReturn
7+
from typing import TYPE_CHECKING, NoReturn, TypeVar
88

99
import attrs
1010

@@ -16,14 +16,31 @@
1616
ASYNCGEN_LOGGER = logging.getLogger("trio.async_generator_errors")
1717

1818
if TYPE_CHECKING:
19+
from collections.abc import Callable
1920
from types import AsyncGeneratorType
2021

22+
from typing_extensions import ParamSpec
23+
24+
_P = ParamSpec("_P")
25+
2126
_WEAK_ASYNC_GEN_SET = weakref.WeakSet[AsyncGeneratorType[object, NoReturn]]
2227
_ASYNC_GEN_SET = set[AsyncGeneratorType[object, NoReturn]]
2328
else:
2429
_WEAK_ASYNC_GEN_SET = weakref.WeakSet
2530
_ASYNC_GEN_SET = set
2631

32+
_R = TypeVar("_R")
33+
34+
35+
@_core.disable_ki_protection
36+
def _call_without_ki_protection(
37+
f: Callable[_P, _R],
38+
/,
39+
*args: _P.args,
40+
**kwargs: _P.kwargs,
41+
) -> _R:
42+
return f(*args, **kwargs)
43+
2744

2845
@attrs.define(eq=False)
2946
class AsyncGenerators:
@@ -35,6 +52,11 @@ class AsyncGenerators:
3552
# regular set so we don't have to deal with GC firing at
3653
# unexpected times.
3754
alive: _WEAK_ASYNC_GEN_SET | _ASYNC_GEN_SET = attrs.Factory(_WEAK_ASYNC_GEN_SET)
55+
# The ids of foreign async generators are added to this set when first
56+
# iterated. Usually it is not safe to refer to ids like this, but because
57+
# we're using a finalizer we can ensure ids in this set do not outlive
58+
# their async generator.
59+
foreign: set[int] = attrs.Factory(set)
3860

3961
# This collects async generators that get garbage collected during
4062
# the one-tick window between the system nursery closing and the
@@ -51,10 +73,10 @@ def firstiter(agen: AsyncGeneratorType[object, NoReturn]) -> None:
5173
# An async generator first iterated outside of a Trio
5274
# task doesn't belong to Trio. Probably we're in guest
5375
# mode and the async generator belongs to our host.
54-
# The locals dictionary is the only good place to
76+
# A strong set of ids is one of the only good places to
5577
# remember this fact, at least until
56-
# https://bugs.python.org/issue40916 is implemented.
57-
agen.ag_frame.f_locals["@trio_foreign_asyncgen"] = True
78+
# https://github.com/python/cpython/issues/85093 is implemented.
79+
self.foreign.add(id(agen))
5880
if self.prev_hooks.firstiter is not None:
5981
self.prev_hooks.firstiter(agen)
6082

@@ -76,13 +98,16 @@ def finalize_in_trio_context(
7698
# have hit it.
7799
self.trailing_needs_finalize.add(agen)
78100

101+
@_core.enable_ki_protection
79102
def finalizer(agen: AsyncGeneratorType[object, NoReturn]) -> None:
80-
agen_name = name_asyncgen(agen)
81103
try:
82-
is_ours = not agen.ag_frame.f_locals.get("@trio_foreign_asyncgen")
83-
except AttributeError: # pragma: no cover
104+
self.foreign.remove(id(agen))
105+
except KeyError:
84106
is_ours = True
107+
else:
108+
is_ours = False
85109

110+
agen_name = name_asyncgen(agen)
86111
if is_ours:
87112
runner.entry_queue.run_sync_soon(
88113
finalize_in_trio_context,
@@ -105,8 +130,9 @@ def finalizer(agen: AsyncGeneratorType[object, NoReturn]) -> None:
105130
)
106131
else:
107132
# Not ours -> forward to the host loop's async generator finalizer
108-
if self.prev_hooks.finalizer is not None:
109-
self.prev_hooks.finalizer(agen)
133+
finalizer = self.prev_hooks.finalizer
134+
if finalizer is not None:
135+
_call_without_ki_protection(finalizer, agen)
110136
else:
111137
# Host has no finalizer. Reimplement the default
112138
# Python behavior with no hooks installed: throw in
@@ -116,7 +142,7 @@ def finalizer(agen: AsyncGeneratorType[object, NoReturn]) -> None:
116142
try:
117143
# If the next thing is a yield, this will raise RuntimeError
118144
# which we allow to propagate
119-
closer.send(None)
145+
_call_without_ki_protection(closer.send, None)
120146
except StopIteration:
121147
pass
122148
else:

src/trio/_core/_run.py

+7
Original file line numberDiff line numberDiff line change
@@ -2953,6 +2953,13 @@ async def checkpoint_if_cancelled() -> None:
29532953
_KqueueStatistics as IOStatistics,
29542954
)
29552955
else: # pragma: no cover
2956+
_patchers = sorted({"eventlet", "gevent"}.intersection(sys.modules))
2957+
if _patchers:
2958+
raise NotImplementedError(
2959+
"unsupported platform or primitives trio depends on are monkey-patched out by "
2960+
+ ", ".join(_patchers),
2961+
)
2962+
29562963
raise NotImplementedError("unsupported platform")
29572964

29582965
from ._generated_instrumentation import *

src/trio/_core/_tests/test_guest_mode.py

+53-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import asyncio
44
import contextlib
5-
import contextvars
65
import queue
76
import signal
87
import socket
@@ -11,6 +10,7 @@
1110
import time
1211
import traceback
1312
import warnings
13+
import weakref
1414
from collections.abc import AsyncGenerator, Awaitable, Callable, Sequence
1515
from functools import partial
1616
from math import inf
@@ -22,6 +22,7 @@
2222
)
2323

2424
import pytest
25+
import sniffio
2526
from outcome import Outcome
2627

2728
import trio
@@ -234,7 +235,8 @@ async def trio_main(in_host: InHost) -> str:
234235

235236

236237
def test_guest_mode_sniffio_integration() -> None:
237-
from sniffio import current_async_library, thread_local as sniffio_library
238+
current_async_library = sniffio.current_async_library
239+
sniffio_library = sniffio.thread_local
238240

239241
async def trio_main(in_host: InHost) -> str:
240242
async def synchronize() -> None:
@@ -458,9 +460,9 @@ def aiotrio_run(
458460

459461
async def aio_main() -> T:
460462
nonlocal run_sync_soon_not_threadsafe
461-
trio_done_fut = loop.create_future()
463+
trio_done_fut: asyncio.Future[Outcome[T]] = loop.create_future()
462464

463-
def trio_done_callback(main_outcome: Outcome[object]) -> None:
465+
def trio_done_callback(main_outcome: Outcome[T]) -> None:
464466
print(f"trio_fn finished: {main_outcome!r}")
465467
trio_done_fut.set_result(main_outcome)
466468

@@ -479,9 +481,11 @@ def trio_done_callback(main_outcome: Outcome[object]) -> None:
479481
strict_exception_groups=strict_exception_groups,
480482
)
481483

482-
return (await trio_done_fut).unwrap() # type: ignore[no-any-return]
484+
return (await trio_done_fut).unwrap()
483485

484486
try:
487+
# can't use asyncio.run because that fails on Windows (3.8, x64, with
488+
# Komodia LSP) and segfaults on Windows (3.9, x64, with Komodia LSP)
485489
return loop.run_until_complete(aio_main())
486490
finally:
487491
loop.close()
@@ -655,8 +659,6 @@ async def trio_main(in_host: InHost) -> None:
655659

656660
@restore_unraisablehook()
657661
def test_guest_mode_asyncgens() -> None:
658-
import sniffio
659-
660662
record = set()
661663

662664
async def agen(label: str) -> AsyncGenerator[int, None]:
@@ -683,9 +685,49 @@ async def trio_main() -> None:
683685

684686
gc_collect_harder()
685687

686-
# Ensure we don't pollute the thread-level context if run under
687-
# an asyncio without contextvars support (3.6)
688-
context = contextvars.copy_context()
689-
context.run(aiotrio_run, trio_main, host_uses_signal_set_wakeup_fd=True)
688+
aiotrio_run(trio_main, host_uses_signal_set_wakeup_fd=True)
690689

691690
assert record == {("asyncio", "asyncio"), ("trio", "trio")}
691+
692+
693+
@restore_unraisablehook()
694+
def test_guest_mode_asyncgens_garbage_collection() -> None:
695+
record: set[tuple[str, str, bool]] = set()
696+
697+
async def agen(label: str) -> AsyncGenerator[int, None]:
698+
class A:
699+
pass
700+
701+
a = A()
702+
a_wr = weakref.ref(a)
703+
assert sniffio.current_async_library() == label
704+
try:
705+
yield 1
706+
finally:
707+
library = sniffio.current_async_library()
708+
with contextlib.suppress(trio.Cancelled):
709+
await sys.modules[library].sleep(0)
710+
711+
del a
712+
if sys.implementation.name == "pypy":
713+
gc_collect_harder()
714+
715+
record.add((label, library, a_wr() is None))
716+
717+
async def iterate_in_aio() -> None:
718+
await agen("asyncio").asend(None)
719+
720+
async def trio_main() -> None:
721+
task = asyncio.ensure_future(iterate_in_aio())
722+
done_evt = trio.Event()
723+
task.add_done_callback(lambda _: done_evt.set())
724+
with trio.fail_after(1):
725+
await done_evt.wait()
726+
727+
await agen("trio").asend(None)
728+
729+
gc_collect_harder()
730+
731+
aiotrio_run(trio_main, host_uses_signal_set_wakeup_fd=True)
732+
733+
assert record == {("asyncio", "asyncio", True), ("trio", "trio", True)}

src/trio/_dtls.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from types import TracebackType
3737

3838
# See DTLSEndpoint.__init__ for why this is imported here
39-
from OpenSSL import SSL # noqa: TCH004
39+
from OpenSSL import SSL # noqa: TC004
4040
from typing_extensions import Self, TypeAlias, TypeVarTuple, Unpack
4141

4242
from trio._socket import AddressFormat

src/trio/_socket.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from typing import (
1313
TYPE_CHECKING,
1414
Any,
15-
Literal,
1615
SupportsIndex,
1716
TypeVar,
1817
Union,
@@ -337,7 +336,7 @@ def fromshare(info: bytes) -> SocketType:
337336
TypeT: TypeAlias = int
338337
FamilyDefault = _stdlib_socket.AF_INET
339338
else:
340-
FamilyDefault: Literal[None] = None
339+
FamilyDefault: None = None
341340
FamilyT: TypeAlias = Union[int, AddressFamily, None]
342341
TypeT: TypeAlias = Union[_stdlib_socket.socket, int]
343342

src/trio/_subprocess_platform/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import trio
99

1010
from .. import _core, _subprocess
11-
from .._abc import ReceiveStream, SendStream # noqa: TCH001
11+
from .._abc import ReceiveStream, SendStream # noqa: TC001
1212

1313
_wait_child_exiting_error: ImportError | None = None
1414
_create_child_pipe_error: ImportError | None = None

src/trio/_tests/test_testing_raisesgroup.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -353,9 +353,9 @@ def check_errno_is_5(e: OSError) -> bool:
353353
def test_matcher_tostring() -> None:
354354
assert str(Matcher(ValueError)) == "Matcher(ValueError)"
355355
assert str(Matcher(match="[a-z]")) == "Matcher(match='[a-z]')"
356-
pattern_no_flags = re.compile("noflag", 0)
356+
pattern_no_flags = re.compile(r"noflag", 0)
357357
assert str(Matcher(match=pattern_no_flags)) == "Matcher(match='noflag')"
358-
pattern_flags = re.compile("noflag", re.IGNORECASE)
358+
pattern_flags = re.compile(r"noflag", re.IGNORECASE)
359359
assert str(Matcher(match=pattern_flags)) == f"Matcher(match={pattern_flags!r})"
360360
assert (
361361
str(Matcher(ValueError, match="re", check=bool))

src/trio/_tests/test_threads.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ def f(name: str) -> Callable[[None], threading.Thread]:
220220
# test that you can set a custom name, and that it's reset afterwards
221221
async def test_thread_name(name: str) -> None:
222222
thread = await to_thread_run_sync(f(name), thread_name=name)
223-
assert re.match("Trio thread [0-9]*", thread.name)
223+
assert re.match(r"Trio thread [0-9]*", thread.name)
224224

225225
await test_thread_name("")
226226
await test_thread_name("fobiedoo")
@@ -301,7 +301,7 @@ async def test_thread_name(name: str, expected: str | None = None) -> None:
301301

302302
os_thread_name = _get_thread_name(thread.ident)
303303
assert os_thread_name is not None, "should skip earlier if this is the case"
304-
assert re.match("Trio thread [0-9]*", os_thread_name)
304+
assert re.match(r"Trio thread [0-9]*", os_thread_name)
305305

306306
await test_thread_name("")
307307
await test_thread_name("fobiedoo")

test-requirements.in

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ cryptography>=41.0.0 # cryptography<41 segfaults on pypy3.10
1313
black; implementation_name == "cpython"
1414
mypy # Would use mypy[faster-cache], but orjson has build issues on pypy
1515
orjson; implementation_name == "cpython"
16-
ruff >= 0.6.6
16+
ruff >= 0.8.0
1717
astor # code generation
1818
uv >= 0.2.24
1919
codespell

test-requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ pytest==8.3.3
113113
# via -r test-requirements.in
114114
requests==2.32.3
115115
# via sphinx
116-
ruff==0.7.3
116+
ruff==0.8.0
117117
# via -r test-requirements.in
118118
sniffio==1.3.1
119119
# via -r test-requirements.in

0 commit comments

Comments
 (0)