Skip to content

Commit 740af18

Browse files
committed
Close event loops when replacing them
1 parent a516134 commit 740af18

File tree

6 files changed

+107
-29
lines changed

6 files changed

+107
-29
lines changed

Makefile

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.PHONY: clean clean-build clean-pyc clean-test
2+
3+
clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
4+
5+
clean-build: ## remove build artifacts
6+
rm -fr build/
7+
rm -fr dist/
8+
rm -fr .eggs/
9+
find . -name '*.egg-info' -exec rm -fr {} +
10+
find . -name '*.egg' -exec rm -f {} +
11+
12+
clean-pyc: ## remove Python file artifacts
13+
find . -name '*.pyc' -exec rm -f {} +
14+
find . -name '*.pyo' -exec rm -f {} +
15+
find . -name '*~' -exec rm -f {} +
16+
find . -name '__pycache__' -exec rm -fr {} +
17+
18+
clean-test: ## remove test and coverage artifacts
19+
rm -fr .tox/
20+
rm -f .coverage
21+
rm -fr htmlcov/

README.rst

+2
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ Changelog
168168
- Abandon support for Python 3.5. If you still require support for Python 3.5, please use pytest-asyncio v0.14 or earlier.
169169
- Set ``unused_tcp_port_factory`` fixture scope to 'session'.
170170
`#163 <https://github.com/pytest-dev/pytest-asyncio/pull/163>`_
171+
- Properly close event loops when replacing them.
172+
`#208 <https://github.com/pytest-dev/pytest-asyncio/issues/208>`_
171173

172174
0.14.0 (2020-06-24)
173175
~~~~~~~~~~~~~~~~~~~

pytest_asyncio/plugin.py

+48-29
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
import socket
77

88
import pytest
9+
910
try:
1011
from _pytest.python import transfer_markers
1112
except ImportError: # Pytest 4.1.0 removes the transfer_marker api (#104)
13+
1214
def transfer_markers(*args, **kwargs): # noqa
1315
"""Noop when over pytest 4.1.0"""
1416
pass
1517

18+
1619
from inspect import isasyncgenfunction
1720

1821

@@ -23,10 +26,12 @@ def _is_coroutine(obj):
2326

2427
def pytest_configure(config):
2528
"""Inject documentation."""
26-
config.addinivalue_line("markers",
27-
"asyncio: "
28-
"mark the test as a coroutine, it will be "
29-
"run using an asyncio event loop")
29+
config.addinivalue_line(
30+
"markers",
31+
"asyncio: "
32+
"mark the test as a coroutine, it will be "
33+
"run using an asyncio event loop",
34+
)
3035

3136

3237
@pytest.mark.tryfirst
@@ -41,12 +46,13 @@ def pytest_pycollect_makeitem(collector, name, obj):
4146
transfer_markers(obj, item.cls, item.module)
4247
item = pytest.Function.from_parent(collector, name=name) # To reload keywords.
4348

44-
if 'asyncio' in item.keywords:
49+
if "asyncio" in item.keywords:
4550
return list(collector._genfunctions(name, obj))
4651

4752

4853
class FixtureStripper:
4954
"""Include additional Fixture, and then strip them"""
55+
5056
REQUEST = "request"
5157
EVENT_LOOP = "event_loop"
5258

@@ -59,7 +65,7 @@ def add(self, name):
5965
and record in to_strip list (If not previously included)"""
6066
if name in self.fixturedef.argnames:
6167
return
62-
self.fixturedef.argnames += (name, )
68+
self.fixturedef.argnames += (name,)
6369
self.to_strip.add(name)
6470

6571
def get_and_strip_from(self, name, data_dict):
@@ -69,6 +75,7 @@ def get_and_strip_from(self, name, data_dict):
6975
del data_dict[name]
7076
return result
7177

78+
7279
@pytest.hookimpl(trylast=True)
7380
def pytest_fixture_post_finalizer(fixturedef, request):
7481
"""Called after fixture teardown"""
@@ -77,14 +84,16 @@ def pytest_fixture_post_finalizer(fixturedef, request):
7784
asyncio.set_event_loop_policy(None)
7885

7986

80-
8187
@pytest.hookimpl(hookwrapper=True)
8288
def pytest_fixture_setup(fixturedef, request):
8389
"""Adjust the event loop policy when an event loop is produced."""
8490
if fixturedef.argname == "event_loop":
8591
outcome = yield
8692
loop = outcome.get_result()
8793
policy = asyncio.get_event_loop_policy()
94+
old_loop = policy.get_event_loop()
95+
if old_loop is not loop:
96+
old_loop.close()
8897
policy.set_event_loop(loop)
8998
return
9099

@@ -96,10 +105,13 @@ def pytest_fixture_setup(fixturedef, request):
96105
fixture_stripper.add(FixtureStripper.EVENT_LOOP)
97106
fixture_stripper.add(FixtureStripper.REQUEST)
98107

99-
100108
def wrapper(*args, **kwargs):
101-
loop = fixture_stripper.get_and_strip_from(FixtureStripper.EVENT_LOOP, kwargs)
102-
request = fixture_stripper.get_and_strip_from(FixtureStripper.REQUEST, kwargs)
109+
loop = fixture_stripper.get_and_strip_from(
110+
FixtureStripper.EVENT_LOOP, kwargs
111+
)
112+
request = fixture_stripper.get_and_strip_from(
113+
FixtureStripper.REQUEST, kwargs
114+
)
103115

104116
gen_obj = generator(*args, **kwargs)
105117

@@ -109,6 +121,7 @@ async def setup():
109121

110122
def finalizer():
111123
"""Yield again, to finalize."""
124+
112125
async def async_finalizer():
113126
try:
114127
await gen_obj.__anext__()
@@ -118,6 +131,7 @@ async def async_finalizer():
118131
msg = "Async generator fixture didn't stop."
119132
msg += "Yield only once."
120133
raise ValueError(msg)
134+
121135
loop.run_until_complete(async_finalizer())
122136

123137
request.addfinalizer(finalizer)
@@ -131,7 +145,9 @@ async def async_finalizer():
131145
fixture_stripper.add(FixtureStripper.EVENT_LOOP)
132146

133147
def wrapper(*args, **kwargs):
134-
loop = fixture_stripper.get_and_strip_from(FixtureStripper.EVENT_LOOP, kwargs)
148+
loop = fixture_stripper.get_and_strip_from(
149+
FixtureStripper.EVENT_LOOP, kwargs
150+
)
135151

136152
async def setup():
137153
res = await coro(*args, **kwargs)
@@ -149,16 +165,15 @@ def pytest_pyfunc_call(pyfuncitem):
149165
Run asyncio marked test functions in an event loop instead of a normal
150166
function call.
151167
"""
152-
if 'asyncio' in pyfuncitem.keywords:
153-
if getattr(pyfuncitem.obj, 'is_hypothesis_test', False):
168+
if "asyncio" in pyfuncitem.keywords:
169+
if getattr(pyfuncitem.obj, "is_hypothesis_test", False):
154170
pyfuncitem.obj.hypothesis.inner_test = wrap_in_sync(
155171
pyfuncitem.obj.hypothesis.inner_test,
156-
_loop=pyfuncitem.funcargs['event_loop']
172+
_loop=pyfuncitem.funcargs["event_loop"],
157173
)
158174
else:
159175
pyfuncitem.obj = wrap_in_sync(
160-
pyfuncitem.obj,
161-
_loop=pyfuncitem.funcargs['event_loop']
176+
pyfuncitem.obj, _loop=pyfuncitem.funcargs["event_loop"]
162177
)
163178
yield
164179

@@ -181,22 +196,25 @@ def inner(**kwargs):
181196
if task.done() and not task.cancelled():
182197
task.exception()
183198
raise
199+
184200
return inner
185201

186202

187203
def pytest_runtest_setup(item):
188-
if 'asyncio' in item.keywords:
204+
if "asyncio" in item.keywords:
189205
# inject an event loop fixture for all async tests
190-
if 'event_loop' in item.fixturenames:
191-
item.fixturenames.remove('event_loop')
192-
item.fixturenames.insert(0, 'event_loop')
193-
if item.get_closest_marker("asyncio") is not None \
194-
and not getattr(item.obj, 'hypothesis', False) \
195-
and getattr(item.obj, 'is_hypothesis_test', False):
196-
pytest.fail(
197-
'test function `%r` is using Hypothesis, but pytest-asyncio '
198-
'only works with Hypothesis 3.64.0 or later.' % item
199-
)
206+
if "event_loop" in item.fixturenames:
207+
item.fixturenames.remove("event_loop")
208+
item.fixturenames.insert(0, "event_loop")
209+
if (
210+
item.get_closest_marker("asyncio") is not None
211+
and not getattr(item.obj, "hypothesis", False)
212+
and getattr(item.obj, "is_hypothesis_test", False)
213+
):
214+
pytest.fail(
215+
"test function `%r` is using Hypothesis, but pytest-asyncio "
216+
"only works with Hypothesis 3.64.0 or later." % item
217+
)
200218

201219

202220
@pytest.fixture
@@ -210,7 +228,7 @@ def event_loop(request):
210228
def _unused_tcp_port():
211229
"""Find an unused localhost TCP port from 1024-65535 and return it."""
212230
with contextlib.closing(socket.socket()) as sock:
213-
sock.bind(('127.0.0.1', 0))
231+
sock.bind(("127.0.0.1", 0))
214232
return sock.getsockname()[1]
215233

216234

@@ -219,7 +237,7 @@ def unused_tcp_port():
219237
return _unused_tcp_port()
220238

221239

222-
@pytest.fixture(scope='session')
240+
@pytest.fixture(scope="session")
223241
def unused_tcp_port_factory():
224242
"""A factory function, producing different unused TCP ports."""
225243
produced = set()
@@ -234,4 +252,5 @@ def factory():
234252
produced.add(port)
235253

236254
return port
255+
237256
return factory

tests/sessionloop/conftest.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import asyncio
2+
3+
import pytest
4+
5+
6+
class CustomSelectorLoopSession(asyncio.SelectorEventLoop):
7+
"""A subclass with no overrides, just to test for presence."""
8+
9+
pass
10+
11+
12+
loop = CustomSelectorLoopSession()
13+
14+
15+
@pytest.fixture(scope="package")
16+
def event_loop():
17+
"""Create an instance of the default event loop for each test case."""
18+
yield loop
19+
loop.close()
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Unit tests for overriding the event loop with a session scoped one."""
2+
import asyncio
3+
4+
import pytest
5+
6+
7+
@pytest.mark.asyncio
8+
async def test_for_custom_loop():
9+
"""This test should be executed using the custom loop."""
10+
await asyncio.sleep(0.01)
11+
assert type(asyncio.get_event_loop()).__name__ == "CustomSelectorLoopSession"
12+
13+
14+
@pytest.mark.asyncio
15+
async def test_dependent_fixture(dependent_fixture):
16+
await asyncio.sleep(0.1)

tests/test_event_loop_scope.py

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
These tests need to be run together.
44
"""
55
import asyncio
6+
67
import pytest
78

89

0 commit comments

Comments
 (0)