Skip to content
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

[3.10] Restore the ability to create objects without a running event loop #8580

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions CHANGES/8555.breaking.rst

This file was deleted.

20 changes: 20 additions & 0 deletions CHANGES/8555.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Restored the ability to create
:py:class:`aiohttp.TCPConnector`,
:py:class:`aiohttp.ClientSession`,
:py:class:`~aiohttp.resolver.ThreadedResolver`
:py:class:`aiohttp.web.Server`,
and :py:class:`aiohttp.CookieJar` instances
without a running event loop -- by :user:`bdraco`.

This is a partial revert of :issue:`5278`. Creating these
objects without a running event loop is highly discouraged
because it can lead to confusing behavior.

This is accomplished by falling back to calling
``asyncio.get_event_loop()`` if ``asyncio.get_running_loop()``
raises :exc:`RuntimeError`. As of Python 3.12, calling
``asyncio.get_running_loop()`` without a running event loop
also emits a Deprecation warning.

As with Python, creating these objects without a running event
loop will become an error in a future version of ``aiohttp``.
3 changes: 2 additions & 1 deletion aiohttp/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from multidict import CIMultiDict
from yarl import URL

from .helpers import get_running_loop
from .typedefs import LooseCookies

if TYPE_CHECKING:
Expand Down Expand Up @@ -169,7 +170,7 @@ class AbstractCookieJar(Sized, IterableBase):
"""Abstract Cookie Jar."""

def __init__(self, *, loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
self._loop = loop or asyncio.get_running_loop()
self._loop = loop or get_running_loop()

@abstractmethod
def clear(self, predicate: Optional[ClearCookiePredicate] = None) -> None:
Expand Down
3 changes: 2 additions & 1 deletion aiohttp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
TimeoutHandle,
ceil_timeout,
get_env_proxy_for_url,
get_running_loop,
method_must_be_empty_body,
sentinel,
strip_auth_from_url,
Expand Down Expand Up @@ -293,7 +294,7 @@ def __init__(
if connector is not None:
loop = connector._loop

loop = loop or asyncio.get_running_loop()
loop = loop or get_running_loop()

if base_url is None or isinstance(base_url, URL):
self._base_url: Optional[URL] = base_url
Expand Down
4 changes: 2 additions & 2 deletions aiohttp/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
)
from .client_proto import ResponseHandler
from .client_reqrep import ClientRequest, Fingerprint, _merge_ssl_params
from .helpers import ceil_timeout, is_ip_address, noop, sentinel
from .helpers import ceil_timeout, get_running_loop, is_ip_address, noop, sentinel
from .locks import EventResultOrError
from .resolver import DefaultResolver

Expand Down Expand Up @@ -105,7 +105,7 @@ def __init__(
) -> None:
self._key = key
self._connector = connector
self._loop = loop
self._loop = loop or get_running_loop()
self._protocol: Optional[ResponseHandler] = protocol
self._callbacks: List[Callable[[], None]] = []

Expand Down
23 changes: 21 additions & 2 deletions aiohttp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import re
import sys
import time
import warnings
import weakref
from collections import namedtuple
from contextlib import suppress
Expand Down Expand Up @@ -51,7 +52,7 @@
from yarl import URL

from . import hdrs
from .log import client_logger
from .log import client_logger, internal_logger

if sys.version_info >= (3, 11):
import asyncio as async_timeout
Expand Down Expand Up @@ -286,6 +287,24 @@ def proxies_from_env() -> Dict[str, ProxyInfo]:
return ret


def get_running_loop() -> asyncio.AbstractEventLoop:
"""Get the running event loop."""
try:
return asyncio.get_running_loop()
except RuntimeError:
warnings.warn(
"The object should be created within an async function",
DeprecationWarning,
stacklevel=3,
)
loop = asyncio.get_event_loop()
if loop.get_debug():
internal_logger.warning(
"The object should be created within an async function", stack_info=True
)
return loop


def get_env_proxy_for_url(url: URL) -> Tuple[URL, Optional[BasicAuth]]:
"""Get a permitted proxy for the given URL from the env."""
if url.host is not None and proxy_bypass(url.host):
Expand Down Expand Up @@ -716,7 +735,7 @@ def ceil_timeout(
if delay is None or delay <= 0:
return async_timeout.timeout(None)

loop = asyncio.get_running_loop()
loop = get_running_loop()
now = loop.time()
when = now + delay
if delay > ceil_threshold:
Expand Down
3 changes: 2 additions & 1 deletion aiohttp/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Any, Dict, List, Optional, Tuple, Type, Union

from .abc import AbstractResolver, ResolveResult
from .helpers import get_running_loop

__all__ = ("ThreadedResolver", "AsyncResolver", "DefaultResolver")

Expand All @@ -29,7 +30,7 @@ class ThreadedResolver(AbstractResolver):
"""

def __init__(self, loop: Optional[asyncio.AbstractEventLoop] = None) -> None:
self._loop = loop or asyncio.get_running_loop()
self._loop = loop or get_running_loop()

async def resolve(
self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET
Expand Down
3 changes: 2 additions & 1 deletion aiohttp/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Any, Awaitable, Callable, Dict, List, Optional # noqa

from .abc import AbstractStreamWriter
from .helpers import get_running_loop
from .http_parser import RawRequestMessage
from .streams import StreamReader
from .web_protocol import RequestHandler, _RequestFactory, _RequestHandler
Expand All @@ -22,7 +23,7 @@ def __init__(
loop: Optional[asyncio.AbstractEventLoop] = None,
**kwargs: Any
) -> None:
self._loop = loop or asyncio.get_running_loop()
self._loop = loop or get_running_loop()
self._connections: Dict[RequestHandler, asyncio.Transport] = {}
self._kwargs = kwargs
self.requests_count = 0
Expand Down
24 changes: 24 additions & 0 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,30 @@ def test_proxies_from_env_http_with_auth(url_input, expected_scheme) -> None:
assert proxy_auth.encoding == "latin1"


# ------------ get_running_loop ---------------------------------


def test_get_running_loop_not_running(
loop: asyncio.AbstractEventLoop, caplog: pytest.LogCaptureFixture
) -> None:
with pytest.warns(DeprecationWarning):
helpers.get_running_loop()
assert "The object should be created within an async function" not in caplog.text
loop.set_debug(True)
with pytest.warns(DeprecationWarning):
helpers.get_running_loop()
assert "The object should be created within an async function" in caplog.text
loop.set_debug(False)
caplog.clear()
with pytest.warns(DeprecationWarning):
helpers.get_running_loop()
assert "The object should be created within an async function" not in caplog.text


async def test_get_running_loop_ok(loop: asyncio.AbstractEventLoop) -> None:
assert helpers.get_running_loop() is loop


# --------------------- get_env_proxy_for_url ------------------------------


Expand Down
Loading