diff --git a/pytest_twisted.py b/pytest_twisted.py index 1c080a4..52a8e4b 100644 --- a/pytest_twisted.py +++ b/pytest_twisted.py @@ -1,11 +1,13 @@ import functools import inspect import sys +import traceback import warnings import decorator import greenlet import pytest +import six from twisted.internet import defer # from twisted.internet import error @@ -13,6 +15,17 @@ from twisted.python import failure +class AddReaderNotImplementedError(Exception): + @classmethod + def build(cls): + return cls( + "Failed to install asyncio reactor. The proactor was" + " used and is lacking the needed `.add_reader()` method" + " as of Python 3.8 on Windows." + " https://twistedmatrix.com/trac/ticket/9766" + ) + + class WrongReactorAlreadyInstalledError(Exception): pass @@ -386,10 +399,29 @@ def init_asyncio_reactor(): """Install the Twisted reactor for asyncio.""" from twisted.internet import asyncioreactor - _install_reactor( - reactor_installer=asyncioreactor.install, - reactor_type=asyncioreactor.AsyncioSelectorReactor, - ) + try: + _install_reactor( + reactor_installer=asyncioreactor.install, + reactor_type=asyncioreactor.AsyncioSelectorReactor, + ) + except NotImplementedError as e: + import asyncio + + _, _, traceback_object = sys.exc_info() + stack_summary = traceback.extract_tb(traceback_object) + source_function_name = stack_summary[-1].name + event_loop = asyncio.get_event_loop() + + if ( + source_function_name == "add_reader" + and isinstance(event_loop, asyncio.ProactorEventLoop) + ): + six.raise_from( + value=AddReaderNotImplementedError.build(), + from_value=e, + ) + + raise reactor_installers = { diff --git a/setup.py b/setup.py index 9744681..ba0c369 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ url="https://github.com/pytest-dev/pytest-twisted", py_modules=["pytest_twisted"], python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*', - install_requires=["greenlet", "pytest>=2.3", "decorator"], + install_requires=["greenlet", "pytest>=2.3", "decorator", "six"], extras_require={"dev": ["pre-commit", "black"]}, classifiers=[ "Development Status :: 5 - Production/Stable", diff --git a/testing/test_basic.py b/testing/test_basic.py index 1b7194d..6799ad3 100755 --- a/testing/test_basic.py +++ b/testing/test_basic.py @@ -51,6 +51,34 @@ def format_run_result_output_for_assert(run_result): ) +def proactor_add_reader_is_implemented(): + try: + import asyncio + + add_reader = asyncio.proactor_event_loop.add_reader + + try: + add_reader(None, None, None) + except NotImplementedError: + return False + except: # noqa: E722 + return True + except: # noqa: E722 + return False + + +def proactor_is_default(): + try: + import asyncio + + return ( + asyncio.DefaultEventLoopPolicy + is asyncio.WindowsProactorEventLoopPolicy + ) + except: # noqa: E722 + return False + + @pytest.fixture(name="default_conftest", autouse=True) def _default_conftest(testdir): testdir.makeconftest(textwrap.dedent(""" @@ -912,3 +940,38 @@ def test_second(foo): testdir.makepyfile(test_file) rr = testdir.run(*cmd_opts, timeout=timeout) assert_outcomes(rr, {"passed": 2}) + + +@pytest.mark.skipif( + condition=sys.platform != 'win32', + reason="Relevant only on Windows", +) +@pytest.mark.skipif( + condition=proactor_add_reader_is_implemented(), + reason=( + "Relevant only if asyncio.ProactorEventLoop.add_reader()" + " is not implemented" + ), +) +@pytest.mark.skipif( + condition=not proactor_is_default(), + reason="Proactor is not the default event loop policy." +) +def test_add_reader_exception_expounded_upon(testdir, cmd_opts, request): + skip_if_reactor_not(request, "asyncio") + + # block the default conftest.py + testdir.makeconftest("") + test_file = """ + def test_succeed(): + pass + """ + testdir.makepyfile(test_file) + rr = testdir.run(sys.executable, "-m", "pytest", "-v", *cmd_opts) + assert all( + s in rr.stderr.str() + for s in [ + "AddReaderNotImplementedError", + "https://twistedmatrix.com/trac/ticket/9766", + ] + )