Skip to content

Commit f543851

Browse files
committed
Let unittest frameworks deal with async functions
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. This also fixes the problem of cleanUp functions not being properly called for async functions. Fix #7110 Fix #6924
1 parent be68496 commit f543851

File tree

8 files changed

+69
-36
lines changed

8 files changed

+69
-36
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ jobs:
7070
- name: "windows-py38"
7171
python: "3.8"
7272
os: windows-latest
73-
tox_env: "py38-twisted"
73+
tox_env: "py38-unittestextras"
7474
use_coverage: true
7575

7676
- name: "ubuntu-py35"

src/_pytest/compat.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,13 @@ def syntax, and doesn't contain yield), or a function decorated with
9393
return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False)
9494

9595

96+
def is_async_function(func: object) -> bool:
97+
"""Return True if the given function seems to be an async function or async generator"""
98+
return iscoroutinefunction(func) or (
99+
sys.version_info >= (3, 6) and inspect.isasyncgenfunction(func)
100+
)
101+
102+
96103
def getlocation(function, curdir=None) -> str:
97104
function = get_real_func(function)
98105
fn = py.path.local(inspect.getfile(function))

src/_pytest/python.py

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@
3434
from _pytest.compat import get_real_func
3535
from _pytest.compat import getimfunc
3636
from _pytest.compat import getlocation
37+
from _pytest.compat import is_async_function
3738
from _pytest.compat import is_generator
38-
from _pytest.compat import iscoroutinefunction
3939
from _pytest.compat import NOTSET
4040
from _pytest.compat import REGEX_TYPE
4141
from _pytest.compat import safe_getattr
@@ -159,7 +159,7 @@ def pytest_configure(config):
159159
)
160160

161161

162-
def async_warn(nodeid: str) -> None:
162+
def async_warn_and_skip(nodeid: str) -> None:
163163
msg = "async def functions are not natively supported and have been skipped.\n"
164164
msg += (
165165
"You need to install a suitable plugin for your async framework, for example:\n"
@@ -175,33 +175,13 @@ def async_warn(nodeid: str) -> None:
175175
@hookimpl(trylast=True)
176176
def pytest_pyfunc_call(pyfuncitem: "Function"):
177177
testfunction = pyfuncitem.obj
178-
179-
try:
180-
# ignoring type as the import is invalid in py37 and mypy thinks its a error
181-
from unittest import IsolatedAsyncioTestCase # type: ignore
182-
except ImportError:
183-
async_ok_in_stdlib = False
184-
else:
185-
async_ok_in_stdlib = isinstance(
186-
getattr(testfunction, "__self__", None), IsolatedAsyncioTestCase
187-
)
188-
189-
if (
190-
iscoroutinefunction(testfunction)
191-
or (sys.version_info >= (3, 6) and inspect.isasyncgenfunction(testfunction))
192-
) and not async_ok_in_stdlib:
193-
async_warn(pyfuncitem.nodeid)
178+
if is_async_function(testfunction):
179+
async_warn_and_skip(pyfuncitem.nodeid)
194180
funcargs = pyfuncitem.funcargs
195181
testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
196182
result = testfunction(**testargs)
197183
if hasattr(result, "__await__") or hasattr(result, "__aiter__"):
198-
if async_ok_in_stdlib:
199-
# todo: investigate moving this to the unittest plugin
200-
# by a test call result hook
201-
testcase = testfunction.__self__
202-
testcase._callMaybeAsync(lambda: result)
203-
else:
204-
async_warn(pyfuncitem.nodeid)
184+
async_warn_and_skip(pyfuncitem.nodeid)
205185
return True
206186

207187

src/_pytest/unittest.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import _pytest._code
77
import pytest
88
from _pytest.compat import getimfunc
9+
from _pytest.compat import is_async_function
910
from _pytest.config import hookimpl
1011
from _pytest.outcomes import exit
1112
from _pytest.outcomes import fail
@@ -227,13 +228,17 @@ def wrapped_testMethod(*args, **kwargs):
227228
self._needs_explicit_tearDown = True
228229
raise _GetOutOf_testPartExecutor(exc)
229230

230-
setattr(self._testcase, self._testcase._testMethodName, wrapped_testMethod)
231-
try:
232-
self._testcase(result=self)
233-
except _GetOutOf_testPartExecutor as exc:
234-
raise exc.args[0] from exc.args[0]
235-
finally:
236-
delattr(self._testcase, self._testcase._testMethodName)
231+
# let the unittest framework handle async functions
232+
if is_async_function(self.obj):
233+
self._testcase(self)
234+
else:
235+
setattr(self._testcase, self._testcase._testMethodName, wrapped_testMethod)
236+
try:
237+
self._testcase(result=self)
238+
except _GetOutOf_testPartExecutor as exc:
239+
raise exc.args[0] from exc.args[0]
240+
finally:
241+
delattr(self._testcase, self._testcase._testMethodName)
237242

238243
def _prunetraceback(self, excinfo):
239244
Function._prunetraceback(self, excinfo)

testing/example_scripts/unittest/test_unittest_asyncio.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
from unittest import IsolatedAsyncioTestCase # type: ignore
22

33

4+
teardowns = []
5+
6+
47
class AsyncArguments(IsolatedAsyncioTestCase):
8+
async def asyncTearDown(self):
9+
teardowns.append(None)
10+
511
async def test_something_async(self):
612
async def addition(x, y):
713
return x + y
@@ -13,3 +19,6 @@ async def addition(x, y):
1319
return x + y
1420

1521
self.assertEqual(await addition(2, 2), 3)
22+
23+
def test_teardowns(self):
24+
assert len(teardowns) == 2
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""Issue #7110"""
2+
import asyncio
3+
4+
import asynctest
5+
6+
7+
teardowns = []
8+
9+
10+
class Test(asynctest.TestCase):
11+
async def tearDown(self):
12+
teardowns.append(None)
13+
14+
async def test_error(self):
15+
await asyncio.sleep(0)
16+
self.fail("failing on purpose")
17+
18+
async def test_ok(self):
19+
await asyncio.sleep(0)
20+
21+
def test_teardowns(self):
22+
assert len(teardowns) == 2

testing/test_unittest.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1136,4 +1136,13 @@ def test_async_support(testdir):
11361136

11371137
testdir.copy_example("unittest/test_unittest_asyncio.py")
11381138
reprec = testdir.inline_run()
1139-
reprec.assertoutcome(failed=1, passed=1)
1139+
reprec.assertoutcome(failed=1, passed=2)
1140+
1141+
1142+
def test_asynctest_support(testdir):
1143+
"""Check asynctest support (#7110)"""
1144+
pytest.importorskip("asynctest")
1145+
1146+
testdir.copy_example("unittest/test_unittest_asynctest.py")
1147+
reprec = testdir.inline_run()
1148+
reprec.assertoutcome(failed=1, passed=2)

tox.ini

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ envlist =
1010
py37
1111
py38
1212
pypy3
13-
py37-{pexpect,xdist,twisted,numpy,pluggymaster}
13+
py37-{pexpect,xdist,unittestextras,numpy,pluggymaster}
1414
doctesting
1515
py37-freeze
1616
docs
@@ -49,7 +49,8 @@ deps =
4949
pexpect: pexpect
5050
pluggymaster: git+https://github.com/pytest-dev/pluggy.git@master
5151
pygments
52-
twisted: twisted
52+
unittestextras: twisted
53+
unittestextras: asynctest
5354
xdist: pytest-xdist>=1.13
5455
{env:_PYTEST_TOX_EXTRA_DEP:}
5556

0 commit comments

Comments
 (0)