diff --git a/CHANGES/11235.feature.rst b/CHANGES/11235.feature.rst new file mode 100644 index 00000000000..fd5a88581cb --- /dev/null +++ b/CHANGES/11235.feature.rst @@ -0,0 +1,2 @@ +Included object creation tracebacks in unclosed resource warnings when loop debug mode is enabled. +-- by :user:`nightcityblade`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index b65e45ddc4a..097d1092e10 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -286,6 +286,7 @@ Moss Collum Mun Gwan-gyeong Navid Sheikhol Nicolas Braem +Nightcity Blade Nikolay Kim Nikolay Novik Nikolay Tiunov diff --git a/aiohttp/client.py b/aiohttp/client.py index 4852b5b706b..425e6888572 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -98,6 +98,7 @@ _auth_header_from_netrc, frozen_dataclass_decorator, get_env_proxy_for_url, + get_unclosed_warning_message, netrc_from_env, sentinel, strip_auth_from_url, @@ -431,7 +432,9 @@ def __init_subclass__(cls: type["ClientSession"]) -> None: def __del__(self, _warnings: Any = warnings) -> None: if not self.closed: _warnings.warn( - f"Unclosed client session {self!r}", + get_unclosed_warning_message( + f"Unclosed client session {self!r}", self._source_traceback + ), ResourceWarning, source=self, ) diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index 310f744390d..f5c59863cac 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -42,6 +42,7 @@ TimerNoop, encode_basic_auth, frozen_dataclass_decorator, + get_unclosed_warning_message, is_expected_content_type, parse_mimetype, reify, @@ -390,7 +391,11 @@ def __del__(self, _warnings: Any = warnings) -> None: if self._loop.get_debug(): _warnings.warn( - f"Unclosed response {self!r}", ResourceWarning, source=self + get_unclosed_warning_message( + f"Unclosed response {self!r}", self._source_traceback + ), + ResourceWarning, + source=self, ) context = {"client_response": self, "message": "Unclosed response"} if self._source_traceback: diff --git a/aiohttp/connector.py b/aiohttp/connector.py index 3f73b1f6418..58afde39ddd 100644 --- a/aiohttp/connector.py +++ b/aiohttp/connector.py @@ -43,6 +43,7 @@ from .helpers import ( _SENTINEL, ceil_timeout, + get_unclosed_warning_message, is_ip_address, sentinel, set_exception, @@ -130,7 +131,11 @@ def __repr__(self) -> str: def __del__(self, _warnings: Any = warnings) -> None: if self._protocol is not None: _warnings.warn( - f"Unclosed connection {self!r}", ResourceWarning, source=self + get_unclosed_warning_message( + f"Unclosed connection {self!r}", self._source_traceback + ), + ResourceWarning, + source=self, ) if self._loop.is_closed(): return @@ -333,7 +338,13 @@ def __del__(self, _warnings: Any = warnings) -> None: self._close_immediately() - _warnings.warn(f"Unclosed connector {self!r}", ResourceWarning, source=self) + _warnings.warn( + get_unclosed_warning_message( + f"Unclosed connector {self!r}", self._source_traceback + ), + ResourceWarning, + source=self, + ) context = { "connector": self, "connections": conns, diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index 55a5e01edcc..cb72df9d3d9 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -14,6 +14,7 @@ import re import sys import time +import traceback import warnings import weakref from collections.abc import Callable, Iterable, Iterator, Mapping @@ -119,6 +120,14 @@ ) +def get_unclosed_warning_message( + message: str, source_traceback: traceback.StackSummary | None +) -> str: + if source_traceback is None: + return message + return f"{message}\nThe object was created at (most recent call last):\n{''.join(traceback.format_list(source_traceback)).rstrip()}" + + CHAR = {chr(i) for i in range(0, 128)} CTL = {chr(i) for i in range(0, 32)} | { chr(127), diff --git a/tests/test_client_session.py b/tests/test_client_session.py index 1d6381a442f..2b93c818ed9 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -534,10 +534,12 @@ async def test_del_debug(connector: BaseConnector) -> None: logs = [] loop.set_exception_handler(lambda loop, ctx: logs.append(ctx)) - with pytest.warns(ResourceWarning): + with pytest.warns(ResourceWarning) as cm: del session gc.collect() + warning_message = str(cm[0].message) + assert "The object was created at" in warning_message assert len(logs) == 1 expected = { "client_session": mock.ANY, diff --git a/tests/test_connector.py b/tests/test_connector.py index 3b73677cbc6..2ae4d5fd678 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -176,10 +176,12 @@ async def test_connection_del_loop_debug() -> None: exc_handler = mock.Mock() loop.set_exception_handler(exc_handler) - with pytest.warns(ResourceWarning): + with pytest.warns(ResourceWarning) as cm: del conn gc.collect() + warning_message = str(cm[0].message) + assert "The object was created at" in warning_message msg = { "message": mock.ANY, "client_connection": mock.ANY,