From cf1f2aeefc3313fdedd19d1fbd24fa97fc9d15c4 Mon Sep 17 00:00:00 2001 From: Pandu POLUAN Date: Wed, 28 Dec 2022 16:36:21 +0700 Subject: [PATCH 01/11] Workaround for asyncio.get_event_loop() deprecation --- aiosmtpd/__init__.py | 14 ++++++++++++++ aiosmtpd/docs/controller.rst | 3 ++- aiosmtpd/handlers.py | 6 +++++- aiosmtpd/main.py | 4 ++-- aiosmtpd/smtp.py | 4 ++-- aiosmtpd/tests/conftest.py | 11 +++++++++-- examples/authenticated_relayer/server.py | 3 ++- examples/basic/server.py | 3 ++- 8 files changed, 38 insertions(+), 10 deletions(-) diff --git a/aiosmtpd/__init__.py b/aiosmtpd/__init__.py index 45c434162..ce5c0bc15 100644 --- a/aiosmtpd/__init__.py +++ b/aiosmtpd/__init__.py @@ -1,4 +1,18 @@ # Copyright 2014-2021 The aiosmtpd Developers # SPDX-License-Identifier: Apache-2.0 +import asyncio +import warnings + __version__ = "1.4.4a0" + + +def _get_or_new_eventloop() -> asyncio.AbstractEventLoop: + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + try: + loop = asyncio.get_event_loop() + except (DeprecationWarning, RuntimeError): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return loop diff --git a/aiosmtpd/docs/controller.rst b/aiosmtpd/docs/controller.rst index 2fb81c027..220d45aae 100644 --- a/aiosmtpd/docs/controller.rst +++ b/aiosmtpd/docs/controller.rst @@ -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) diff --git a/aiosmtpd/handlers.py b/aiosmtpd/handlers.py index 5e44f7ca5..502bbabee 100644 --- a/aiosmtpd/handlers.py +++ b/aiosmtpd/handlers.py @@ -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 @@ -218,7 +219,10 @@ def __init__( loop: Optional[asyncio.AbstractEventLoop] = None, ): super().__init__(message_class) - self.loop = loop or asyncio.get_event_loop() + if loop is not None: + self.loop = None + else: + self.loop = _get_or_new_eventloop() async def handle_DATA( self, server: SMTPServer, session: SMTPSession, envelope: SMTPEnvelope diff --git a/aiosmtpd/main.py b/aiosmtpd/main.py index 2366ae491..f18aed96c 100644 --- a/aiosmtpd/main.py +++ b/aiosmtpd/main.py @@ -16,7 +16,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: @@ -252,7 +252,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) diff --git a/aiosmtpd/smtp.py b/aiosmtpd/smtp.py index 95181ea30..a977f7513 100644 --- a/aiosmtpd/smtp.py +++ b/aiosmtpd/smtp.py @@ -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 @@ -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 diff --git a/aiosmtpd/tests/conftest.py b/aiosmtpd/tests/conftest.py index 6a8c3dd1f..0c6910317 100644 --- a/aiosmtpd/tests/conftest.py +++ b/aiosmtpd/tests/conftest.py @@ -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 @@ -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 diff --git a/examples/authenticated_relayer/server.py b/examples/authenticated_relayer/server.py index c44160c18..f00b60201 100644 --- a/examples/authenticated_relayer/server.py +++ b/examples/authenticated_relayer/server.py @@ -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() diff --git a/examples/basic/server.py b/examples/basic/server.py index 9f6f714e0..fcb572a63 100644 --- a/examples/basic/server.py +++ b/examples/basic/server.py @@ -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() From 25cbb087bcfabc9dbb81f4d3f7af8ebf40032a85 Mon Sep 17 00:00:00 2001 From: Pandu POLUAN Date: Wed, 28 Dec 2022 16:43:10 +0700 Subject: [PATCH 02/11] Remove unused import in main.py That import was used only to get the current event loop. Now we handle that in __init__.py --- aiosmtpd/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aiosmtpd/main.py b/aiosmtpd/main.py index f18aed96c..166484ca1 100644 --- a/aiosmtpd/main.py +++ b/aiosmtpd/main.py @@ -1,7 +1,6 @@ # Copyright 2014-2021 The aiosmtpd Developers # SPDX-License-Identifier: Apache-2.0 -import asyncio import logging import os import signal From c950c78ccd0726a843832d2fba2066ec911daec2 Mon Sep 17 00:00:00 2001 From: Pandu POLUAN Date: Wed, 28 Dec 2022 16:44:54 +0700 Subject: [PATCH 03/11] BUGFIX: if loop is not None then use that Also simplify the line because we have a helper func now --- aiosmtpd/handlers.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/aiosmtpd/handlers.py b/aiosmtpd/handlers.py index 502bbabee..2d1b28fe7 100644 --- a/aiosmtpd/handlers.py +++ b/aiosmtpd/handlers.py @@ -219,10 +219,7 @@ def __init__( loop: Optional[asyncio.AbstractEventLoop] = None, ): super().__init__(message_class) - if loop is not None: - self.loop = None - else: - self.loop = _get_or_new_eventloop() + self.loop = loop or _get_or_new_eventloop() async def handle_DATA( self, server: SMTPServer, session: SMTPSession, envelope: SMTPEnvelope From 528e46efd8f6b171fb4799225db168bbdf40cd0a Mon Sep 17 00:00:00 2001 From: Pandu POLUAN Date: Wed, 28 Dec 2022 16:46:41 +0700 Subject: [PATCH 04/11] Add pragma: no cover Because at the point the expected behavior is not yet happening. --- aiosmtpd/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiosmtpd/__init__.py b/aiosmtpd/__init__.py index ce5c0bc15..ae7490979 100644 --- a/aiosmtpd/__init__.py +++ b/aiosmtpd/__init__.py @@ -12,7 +12,7 @@ def _get_or_new_eventloop() -> asyncio.AbstractEventLoop: warnings.simplefilter("ignore") try: loop = asyncio.get_event_loop() - except (DeprecationWarning, RuntimeError): + except (DeprecationWarning, RuntimeError): # pragma: no cover loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) return loop From 875679c129520b98021eab8c3a9206ecc610bc59 Mon Sep 17 00:00:00 2001 From: Pandu POLUAN Date: Wed, 28 Dec 2022 18:55:05 +0700 Subject: [PATCH 05/11] Update NEWS + bump to 1.4.4a1 --- aiosmtpd/__init__.py | 2 +- aiosmtpd/docs/NEWS.rst | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/aiosmtpd/__init__.py b/aiosmtpd/__init__.py index ae7490979..bdfac2796 100644 --- a/aiosmtpd/__init__.py +++ b/aiosmtpd/__init__.py @@ -4,7 +4,7 @@ import warnings -__version__ = "1.4.4a0" +__version__ = "1.4.4a1" def _get_or_new_eventloop() -> asyncio.AbstractEventLoop: diff --git a/aiosmtpd/docs/NEWS.rst b/aiosmtpd/docs/NEWS.rst index 31542977a..140151f19 100644 --- a/aiosmtpd/docs/NEWS.rst +++ b/aiosmtpd/docs/NEWS.rst @@ -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) From a9c5fc84ed24398475f4856ae7e218259b27d9b0 Mon Sep 17 00:00:00 2001 From: Pandu POLUAN Date: Thu, 29 Dec 2022 12:45:30 +0700 Subject: [PATCH 06/11] Don't ignore the warning, rather promote it into an error --- aiosmtpd/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiosmtpd/__init__.py b/aiosmtpd/__init__.py index bdfac2796..f587dc19e 100644 --- a/aiosmtpd/__init__.py +++ b/aiosmtpd/__init__.py @@ -9,7 +9,7 @@ def _get_or_new_eventloop() -> asyncio.AbstractEventLoop: with warnings.catch_warnings(): - warnings.simplefilter("ignore") + warnings.simplefilter("error") try: loop = asyncio.get_event_loop() except (DeprecationWarning, RuntimeError): # pragma: no cover From 502852a79b30893716d187280c4e89b5ad1fef00 Mon Sep 17 00:00:00 2001 From: Pandu POLUAN Date: Thu, 29 Dec 2022 12:46:05 +0700 Subject: [PATCH 07/11] Some test cases to ensure the get event func works as intended --- aiosmtpd/tests/test_misc.py | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 aiosmtpd/tests/test_misc.py diff --git a/aiosmtpd/tests/test_misc.py b/aiosmtpd/tests/test_misc.py new file mode 100644 index 000000000..c3214dbad --- /dev/null +++ b/aiosmtpd/tests/test_misc.py @@ -0,0 +1,44 @@ +# 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 Optional + +import pytest + +from aiosmtpd import _get_or_new_eventloop + + +@pytest.fixture(scope="module") +def close_existing_loop() -> 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) + + +class TestInit: + + def test_create_new_if_none(self, close_existing_loop): + loop: Optional[asyncio.AbstractEventLoop] + loop = _get_or_new_eventloop() + assert loop is not None + assert isinstance(loop, asyncio.AbstractEventLoop) + + def test_not_create_new_if_exist(self, close_existing_loop): + loop: Optional[asyncio.AbstractEventLoop] + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + ret_loop = _get_or_new_eventloop() + assert ret_loop == loop + assert ret_loop is loop From 667464a34e235ec9600d3afb1fdf516e188dd2e0 Mon Sep 17 00:00:00 2001 From: Pandu POLUAN Date: Thu, 29 Dec 2022 12:56:15 +0700 Subject: [PATCH 08/11] Skip coverage only on Python<3.10 --- aiosmtpd/__init__.py | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/aiosmtpd/__init__.py b/aiosmtpd/__init__.py index f587dc19e..b1a4b23e4 100644 --- a/aiosmtpd/__init__.py +++ b/aiosmtpd/__init__.py @@ -12,7 +12,7 @@ def _get_or_new_eventloop() -> asyncio.AbstractEventLoop: warnings.simplefilter("error") try: loop = asyncio.get_event_loop() - except (DeprecationWarning, RuntimeError): # pragma: no cover + except (DeprecationWarning, RuntimeError): # pragma: py-lt-310 loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) return loop diff --git a/pyproject.toml b/pyproject.toml index e067d36d9..f80638fda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ source = [ [tool.coverage.coverage_conditional_plugin.rules] # Here we specify our pragma rules: +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)" From f8128e7a2fbc65f2e3c903d4ab87547ece06d9b4 Mon Sep 17 00:00:00 2001 From: Pandu POLUAN Date: Thu, 29 Dec 2022 13:08:55 +0700 Subject: [PATCH 09/11] Create new event loop only on failure Because apparently in Python<3.12, even though get_event_loop() raises a warning, it still returns existing event loop. The behavior is expected to change on Python>=3.12, so let's cut off at that point. --- aiosmtpd/__init__.py | 7 +++++-- pyproject.toml | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/aiosmtpd/__init__.py b/aiosmtpd/__init__.py index b1a4b23e4..7431874e2 100644 --- a/aiosmtpd/__init__.py +++ b/aiosmtpd/__init__.py @@ -8,11 +8,14 @@ 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 - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) + if loop is None: # pragma: py-lt-312 + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + assert isinstance(loop, asyncio.AbstractEventLoop) return loop diff --git a/pyproject.toml b/pyproject.toml index f80638fda..8731e6055 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ 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)" From 462b3c0fb35ced703f93ecf5ebc2719828cd68ca Mon Sep 17 00:00:00 2001 From: Pandu POLUAN Date: Thu, 29 Dec 2022 13:09:19 +0700 Subject: [PATCH 10/11] More stringent testing for _get_or_new_eventloop --- aiosmtpd/tests/test_misc.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/aiosmtpd/tests/test_misc.py b/aiosmtpd/tests/test_misc.py index c3214dbad..aeb9baff2 100644 --- a/aiosmtpd/tests/test_misc.py +++ b/aiosmtpd/tests/test_misc.py @@ -13,7 +13,7 @@ @pytest.fixture(scope="module") -def close_existing_loop() -> None: +def close_existing_loop() -> Optional[asyncio.AbstractEventLoop]: loop: Optional[asyncio.AbstractEventLoop] with warnings.catch_warnings(): warnings.filterwarnings("error") @@ -25,20 +25,28 @@ def close_existing_loop() -> None: 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 From 027149737e24680cccfeb6ded3db9ee08ac4db9d Mon Sep 17 00:00:00 2001 From: Pandu POLUAN Date: Thu, 29 Dec 2022 13:17:02 +0700 Subject: [PATCH 11/11] FIX: Type hint for fixture should be Generator[] --- aiosmtpd/tests/test_misc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiosmtpd/tests/test_misc.py b/aiosmtpd/tests/test_misc.py index aeb9baff2..94f1489ac 100644 --- a/aiosmtpd/tests/test_misc.py +++ b/aiosmtpd/tests/test_misc.py @@ -5,7 +5,7 @@ import asyncio import warnings -from typing import Optional +from typing import Generator, Optional import pytest @@ -13,7 +13,7 @@ @pytest.fixture(scope="module") -def close_existing_loop() -> Optional[asyncio.AbstractEventLoop]: +def close_existing_loop() -> Generator[Optional[asyncio.AbstractEventLoop], None, None]: loop: Optional[asyncio.AbstractEventLoop] with warnings.catch_warnings(): warnings.filterwarnings("error")