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

Workaround for asyncio.get_event_loop() deprecation #355

Merged
merged 11 commits into from
Jan 13, 2023
19 changes: 18 additions & 1 deletion aiosmtpd/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,21 @@
# Copyright 2014-2021 The aiosmtpd Developers
# SPDX-License-Identifier: Apache-2.0
import asyncio
import warnings

__version__ = "1.4.4a0"

__version__ = "1.4.4a1"


def _get_or_new_eventloop() -> asyncio.AbstractEventLoop:
loop = None
with warnings.catch_warnings():
warnings.simplefilter("error")
try:
loop = asyncio.get_event_loop()
except (DeprecationWarning, RuntimeError): # pragma: py-lt-310
if loop is None: # pragma: py-lt-312
Copy link
Collaborator Author

@pepoluan pepoluan Dec 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Python 3.11 (and I believe also 3.10), though get_event_loop() raises a warning when there's no existing event loop, it does indeed return a newly-created event loop. This defensive check prevents (re-)creation of a new event loop in that situation.

On Python 3.12 the behavior likely changes, i.e., no implicit creation of event loop inside get_event_loop(). In that situation, loop will remain None and only in that case will we create a new event loop.

That is why this particular check has a # pragma: py-lt-312 mark there, because the branch will never be taken for Python<3.12

loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
assert isinstance(loop, asyncio.AbstractEventLoop)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment FWIW this is probably less useful, since assertions can be disabled. And everything will go sideways shortly after this function gets called anyway 😂

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's mostly for type checking, not expected to do anything useful 😂

return loop
6 changes: 4 additions & 2 deletions aiosmtpd/docs/NEWS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ Fixed/Improved
* A whole bunch of annotations


1.4.4a0 (ad hoc)
1.4.4a1 (ad hoc)
================

(Stub ``NEWS.rst`` entry as placeholder for ``qa`` test.)
Fixed/Improved
--------------
* No longer expect an implicit creation of the event loop through ``get_event_loop()`` (Closes #353)


1.4.3 (2022-12-21)
Expand Down
3 changes: 2 additions & 1 deletion aiosmtpd/docs/controller.rst
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,8 @@ you'll have to do something similar to this:
.. doctest:: unthreaded

>>> import asyncio
>>> loop = asyncio.get_event_loop()
>>> loop = asyncio.new_event_loop()
>>> asyncio.set_event_loop(loop)
>>> from aiosmtpd.controller import UnthreadedController
>>> from aiosmtpd.handlers import Sink
>>> controller = UnthreadedController(Sink(), loop=loop)
Expand Down
3 changes: 2 additions & 1 deletion aiosmtpd/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from public import public

from aiosmtpd import _get_or_new_eventloop
from aiosmtpd.smtp import SMTP as SMTPServer
from aiosmtpd.smtp import Envelope as SMTPEnvelope
from aiosmtpd.smtp import Session as SMTPSession
Expand Down Expand Up @@ -218,7 +219,7 @@ def __init__(
loop: Optional[asyncio.AbstractEventLoop] = None,
):
super().__init__(message_class)
self.loop = loop or asyncio.get_event_loop()
self.loop = loop or _get_or_new_eventloop()

async def handle_DATA(
self, server: SMTPServer, session: SMTPSession, envelope: SMTPEnvelope
Expand Down
5 changes: 2 additions & 3 deletions aiosmtpd/main.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Copyright 2014-2021 The aiosmtpd Developers
# SPDX-License-Identifier: Apache-2.0

import asyncio
import logging
import os
import signal
Expand All @@ -16,7 +15,7 @@

from public import public

from aiosmtpd import __version__
from aiosmtpd import __version__, _get_or_new_eventloop
from aiosmtpd.smtp import DATA_SIZE_DEFAULT, SMTP

try:
Expand Down Expand Up @@ -252,7 +251,7 @@ def main(args: Optional[Sequence[str]] = None) -> None:

logging.basicConfig(level=logging.ERROR)
log = logging.getLogger("mail.log")
loop = asyncio.get_event_loop()
loop = _get_or_new_eventloop()

if args.debug > 0:
log.setLevel(logging.INFO)
Expand Down
4 changes: 2 additions & 2 deletions aiosmtpd/smtp.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
import attr
from public import public

from aiosmtpd import __version__
from aiosmtpd import __version__, _get_or_new_eventloop
from aiosmtpd.proxy_protocol import ProxyData, get_proxy


Expand Down Expand Up @@ -208,7 +208,7 @@ def __init__(self) -> None:
# unit test suite. In that case, this function is mocked to set the debug
# level on the loop (as if PYTHONASYNCIODEBUG=1 were set).
def make_loop() -> asyncio.AbstractEventLoop:
return asyncio.get_event_loop()
return _get_or_new_eventloop()


@public
Expand Down
11 changes: 9 additions & 2 deletions aiosmtpd/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import inspect
import socket
import ssl
import warnings
from contextlib import suppress
from functools import wraps
from smtplib import SMTP as SMTPClient
Expand Down Expand Up @@ -200,14 +201,20 @@ def getter(*args, **kwargs) -> Any:

@pytest.fixture
def temp_event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]:
default_loop = asyncio.get_event_loop()
with warnings.catch_warnings():
warnings.simplefilter("ignore")
try:
default_loop = asyncio.get_event_loop()
except (DeprecationWarning, RuntimeError):
default_loop = None
new_loop = asyncio.new_event_loop()
asyncio.set_event_loop(new_loop)
#
yield new_loop
#
new_loop.close()
asyncio.set_event_loop(default_loop)
if default_loop is not None:
asyncio.set_event_loop(default_loop)


@pytest.fixture
Expand Down
52 changes: 52 additions & 0 deletions aiosmtpd/tests/test_misc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright 2014-2021 The aiosmtpd Developers
# SPDX-License-Identifier: Apache-2.0

"""Test other aspects of the server implementation."""

import asyncio
import warnings
from typing import Generator, Optional

import pytest

from aiosmtpd import _get_or_new_eventloop


@pytest.fixture(scope="module")
def close_existing_loop() -> Generator[Optional[asyncio.AbstractEventLoop], None, None]:
loop: Optional[asyncio.AbstractEventLoop]
with warnings.catch_warnings():
warnings.filterwarnings("error")
try:
loop = asyncio.get_event_loop()
except (DeprecationWarning, RuntimeError):
loop = None
if loop:
loop.stop()
loop.close()
asyncio.set_event_loop(None)
yield loop
else:
yield None


class TestInit:

def test_create_new_if_none(self, close_existing_loop):
old_loop = close_existing_loop
loop: Optional[asyncio.AbstractEventLoop]
loop = _get_or_new_eventloop()
assert loop is not None
assert loop is not old_loop
assert isinstance(loop, asyncio.AbstractEventLoop)

def test_not_create_new_if_exist(self, close_existing_loop):
old_loop = close_existing_loop
loop: Optional[asyncio.AbstractEventLoop]
loop = asyncio.new_event_loop()
assert loop is not old_loop
asyncio.set_event_loop(loop)
ret_loop = _get_or_new_eventloop()
assert ret_loop is not old_loop
assert ret_loop == loop
assert ret_loop is loop
3 changes: 2 additions & 1 deletion examples/authenticated_relayer/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ async def amain():
print(f"Please create {DB_AUTH} first using make_user_db.py")
sys.exit(1)
logging.basicConfig(level=logging.DEBUG)
loop = asyncio.get_event_loop()
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.create_task(amain())
try:
loop.run_forever()
Expand Down
3 changes: 2 additions & 1 deletion examples/basic/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ async def amain(loop):

if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
loop = asyncio.get_event_loop()
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.create_task(amain(loop=loop))
try:
loop.run_forever()
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ source = [

[tool.coverage.coverage_conditional_plugin.rules]
# Here we specify our pragma rules:
py-lt-312 = "sys_version_info < (3, 12)"
py-lt-310 = "sys_version_info < (3, 10)"
py-ge-38 = "sys_version_info >= (3, 8)"
py-lt-38 = "sys_version_info < (3, 8)"
py-gt-36 = "sys_version_info > (3, 6)"
Expand Down