-
Notifications
You must be signed in to change notification settings - Fork 142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
avoid refcycles in tracebacks from happy eyeballs exceptions #809
base: master
Are you sure you want to change the base?
Changes from 16 commits
272d59c
c217d95
9b46a18
222c6cd
a2baf53
06c77dc
2387f99
0203d03
6fba791
6dd2010
877855e
4bb150d
4eea30f
7ddcd12
0542d94
e53e9eb
3d626c9
05d9f63
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ | |
from threading import Thread | ||
from typing import TYPE_CHECKING, Any, Literal, NoReturn, TypeVar, cast | ||
|
||
import ephemeral_port_reserve | ||
import psutil | ||
import pytest | ||
from _pytest.fixtures import SubRequest | ||
|
@@ -43,6 +44,7 @@ | |
create_unix_datagram_socket, | ||
create_unix_listener, | ||
fail_after, | ||
get_current_task, | ||
getaddrinfo, | ||
getnameinfo, | ||
move_on_after, | ||
|
@@ -58,6 +60,7 @@ | |
SocketAttribute, | ||
SocketListener, | ||
SocketStream, | ||
TaskStatus, | ||
) | ||
from anyio.lowlevel import checkpoint | ||
from anyio.streams.stapled import MultiListener | ||
|
@@ -133,6 +136,31 @@ def check_asyncio_bug(anyio_backend_name: str, family: AnyIPAddressFamily) -> No | |
pytest.skip("Does not work due to a known bug (39148)") | ||
|
||
|
||
if sys.version_info >= (3, 14): | ||
|
||
async def no_other_refs() -> list[object]: | ||
frame = sys._getframe(1) | ||
coro = get_current_task().coro | ||
|
||
async def get_coro_for_frame(*, task_status: TaskStatus[object]) -> None: | ||
my_coro = coro | ||
while my_coro.cr_frame is not frame: | ||
my_coro = my_coro.cr_await | ||
task_status.started(my_coro) | ||
|
||
async with create_task_group() as tg: | ||
return [await tg.start(get_coro_for_frame)] | ||
|
||
elif sys.version_info >= (3, 11): | ||
|
||
async def no_other_refs() -> list[object]: | ||
return [] | ||
else: | ||
|
||
async def no_other_refs() -> list[object]: | ||
return [sys._getframe(1)] | ||
|
||
|
||
_T = TypeVar("_T") | ||
|
||
|
||
|
@@ -315,6 +343,33 @@ def serve() -> None: | |
server_sock.close() | ||
assert client_addr[0] == expected_client_addr | ||
|
||
@pytest.mark.skipif( | ||
sys.implementation.name == "pypy", | ||
reason=( | ||
"gc.get_referrers is broken on PyPy see " | ||
"https://github.com/pypy/pypy/issues/5075" | ||
), | ||
) | ||
async def test_happy_eyeballs_refcycles(self, anyio_backend_name: str) -> None: | ||
""" | ||
Test derived from https://github.com/python/cpython/pull/124859 | ||
""" | ||
if anyio_backend_name == "asyncio" and sys.version_info < (3, 10): | ||
pytest.skip( | ||
"asyncio.BaseEventLoop.create_connection creates refcycles on py 3.9" | ||
) | ||
ip = "127.0.0.1" | ||
port = ephemeral_port_reserve.reserve(ip=ip) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have plenty of tests that need ephemeral ports. Why do we have to add a new test dependency for this specific test? (I'd be happy to add such a fixture to anyio's pytest plugin though!) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh is there already a tool for this? I'll have a look There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I didn't say there was a tool yet (that's what my suggestion in parentheses was about). Rather, we currently do this in a rather manual fashion. |
||
exc = None | ||
try: | ||
async with await connect_tcp(ip, port): | ||
pass | ||
except OSError as e: | ||
exc = e.__cause__ | ||
|
||
assert isinstance(exc, OSError) | ||
assert gc.get_referrers(exc) == await no_other_refs() | ||
|
||
@pytest.mark.parametrize( | ||
"target, exception_class", | ||
[ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't really need to duplicate this from
test_taskgroups.py
, do we? Why not just move it to a utility module in the test suite, or something?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could put it in conftest.py ? happy to have a utils.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've usually gone with a
utils.py
in the test suite. So long as it's structured as a package (which it is in anyio's case), it works fine.