Skip to content

Commit

Permalink
Let unittest frameworks deal with async functions
Browse files Browse the repository at this point in the history
Instead of trying to handle unittest-async functions in pytest_pyfunc_call,
let the unittest framework handle them instead.

This lets us remove the hack in pytest_pyfunc_call, with the upside that
we should support any unittest-async based framework.

Also included 'asynctest' as test dependency for py37-twisted, and renamed
'twisted' to 'unittestextras' to better reflect that we install 'twisted' and
'asynctest' now.

Fix pytest-dev#7110
  • Loading branch information
nicoddemus committed May 1, 2020
1 parent be68496 commit 04ebaf2
Show file tree
Hide file tree
Showing 7 changed files with 50 additions and 35 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Expand Up @@ -70,7 +70,7 @@ jobs:
- name: "windows-py38"
python: "3.8"
os: windows-latest
tox_env: "py38-twisted"
tox_env: "py38-unittestextras"
use_coverage: true

- name: "ubuntu-py35"
Expand Down
7 changes: 7 additions & 0 deletions src/_pytest/compat.py
Expand Up @@ -93,6 +93,13 @@ def syntax, and doesn't contain yield), or a function decorated with
return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False)


def is_async_function(func: object) -> bool:
"""Return True if the given function seems to be an async function or async generator"""
return iscoroutinefunction(func) or (
sys.version_info >= (3, 6) and inspect.isasyncgenfunction(func)
)


def getlocation(function, curdir=None) -> str:
function = get_real_func(function)
fn = py.path.local(inspect.getfile(function))
Expand Down
30 changes: 5 additions & 25 deletions src/_pytest/python.py
Expand Up @@ -34,8 +34,8 @@
from _pytest.compat import get_real_func
from _pytest.compat import getimfunc
from _pytest.compat import getlocation
from _pytest.compat import is_async_function
from _pytest.compat import is_generator
from _pytest.compat import iscoroutinefunction
from _pytest.compat import NOTSET
from _pytest.compat import REGEX_TYPE
from _pytest.compat import safe_getattr
Expand Down Expand Up @@ -159,7 +159,7 @@ def pytest_configure(config):
)


def async_warn(nodeid: str) -> None:
def async_warn_and_skip(nodeid: str) -> None:
msg = "async def functions are not natively supported and have been skipped.\n"
msg += (
"You need to install a suitable plugin for your async framework, for example:\n"
Expand All @@ -175,33 +175,13 @@ def async_warn(nodeid: str) -> None:
@hookimpl(trylast=True)
def pytest_pyfunc_call(pyfuncitem: "Function"):
testfunction = pyfuncitem.obj

try:
# ignoring type as the import is invalid in py37 and mypy thinks its a error
from unittest import IsolatedAsyncioTestCase # type: ignore
except ImportError:
async_ok_in_stdlib = False
else:
async_ok_in_stdlib = isinstance(
getattr(testfunction, "__self__", None), IsolatedAsyncioTestCase
)

if (
iscoroutinefunction(testfunction)
or (sys.version_info >= (3, 6) and inspect.isasyncgenfunction(testfunction))
) and not async_ok_in_stdlib:
async_warn(pyfuncitem.nodeid)
if is_async_function(testfunction):
async_warn_and_skip(pyfuncitem.nodeid)
funcargs = pyfuncitem.funcargs
testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
result = testfunction(**testargs)
if hasattr(result, "__await__") or hasattr(result, "__aiter__"):
if async_ok_in_stdlib:
# todo: investigate moving this to the unittest plugin
# by a test call result hook
testcase = testfunction.__self__
testcase._callMaybeAsync(lambda: result)
else:
async_warn(pyfuncitem.nodeid)
async_warn_and_skip(pyfuncitem.nodeid)
return True


Expand Down
19 changes: 12 additions & 7 deletions src/_pytest/unittest.py
Expand Up @@ -6,6 +6,7 @@
import _pytest._code
import pytest
from _pytest.compat import getimfunc
from _pytest.compat import is_async_function
from _pytest.config import hookimpl
from _pytest.outcomes import exit
from _pytest.outcomes import fail
Expand Down Expand Up @@ -227,13 +228,17 @@ def wrapped_testMethod(*args, **kwargs):
self._needs_explicit_tearDown = True
raise _GetOutOf_testPartExecutor(exc)

setattr(self._testcase, self._testcase._testMethodName, wrapped_testMethod)
try:
self._testcase(result=self)
except _GetOutOf_testPartExecutor as exc:
raise exc.args[0] from exc.args[0]
finally:
delattr(self._testcase, self._testcase._testMethodName)
# let the unittest framework handle async functions
if is_async_function(self.obj):
self._testcase(self)
else:
setattr(self._testcase, self._testcase._testMethodName, wrapped_testMethod)
try:
self._testcase(result=self)
except _GetOutOf_testPartExecutor as exc:
raise exc.args[0] from exc.args[0]
finally:
delattr(self._testcase, self._testcase._testMethodName)

def _prunetraceback(self, excinfo):
Function._prunetraceback(self, excinfo)
Expand Down
13 changes: 13 additions & 0 deletions testing/example_scripts/unittest/test_unittest_asynctest.py
@@ -0,0 +1,13 @@
"""Issue #7110"""
import asyncio

import asynctest


class Test(asynctest.TestCase):
async def test_error(self):
await asyncio.sleep(0)
self.fail("failing on purpose")

async def test_ok(self):
await asyncio.sleep(0)
9 changes: 9 additions & 0 deletions testing/test_unittest.py
Expand Up @@ -1137,3 +1137,12 @@ def test_async_support(testdir):
testdir.copy_example("unittest/test_unittest_asyncio.py")
reprec = testdir.inline_run()
reprec.assertoutcome(failed=1, passed=1)


def test_asynctest_support(testdir):
"""Check asynctest support (#7110)"""
pytest.importorskip("asynctest")

testdir.copy_example("unittest/test_unittest_asynctest.py")
reprec = testdir.inline_run()
reprec.assertoutcome(failed=1, passed=1)
5 changes: 3 additions & 2 deletions tox.ini
Expand Up @@ -10,7 +10,7 @@ envlist =
py37
py38
pypy3
py37-{pexpect,xdist,twisted,numpy,pluggymaster}
py37-{pexpect,xdist,unittestextras,numpy,pluggymaster}
doctesting
py37-freeze
docs
Expand Down Expand Up @@ -49,7 +49,8 @@ deps =
pexpect: pexpect
pluggymaster: git+https://github.com/pytest-dev/pluggy.git@master
pygments
twisted: twisted
unittestextras: twisted
unittestextras: asynctest
xdist: pytest-xdist>=1.13
{env:_PYTEST_TOX_EXTRA_DEP:}

Expand Down

0 comments on commit 04ebaf2

Please sign in to comment.