Skip to content

Commit

Permalink
Restore the ability to create objects without a running event loop
Browse files Browse the repository at this point in the history
  • Loading branch information
bdraco committed Aug 1, 2024
1 parent 01ed189 commit 16b9af9
Show file tree
Hide file tree
Showing 9 changed files with 75 additions and 16 deletions.
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

0 comments on commit 16b9af9

Please sign in to comment.