Skip to content

Commit dcac2bb

Browse files
committed
fix: unclosed event loop on Python 3.12+
1 parent e295495 commit dcac2bb

File tree

5 files changed

+71
-17
lines changed

5 files changed

+71
-17
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ generally can't be patched.
4444

4545
## Comparison with `nest_asyncio`
4646
`nest-asyncio2` is a fork of the unmaintained [`nest_asyncio`](https://github.com/erdewit/nest_asyncio), with the following changes:
47-
- Python 3.12 `loop_factory` parameter support
47+
- Python 3.12 support
48+
- `loop_factory` parameter support
49+
- Fix `ResourceWarning: unclosed event loop` at exit
50+
51+
Note that if you call `asyncio.get_event_loop()` on the main thread without setting the loop before, `ResourceWarning` is expected on Python 3.12~3.13, not caused by `nest-asyncio2`.
4852
- Python 3.14 support
4953
- Fix broken `asyncio.current_task()` and others
5054
- Fix `DeprecationWarning: 'asyncio.get_event_loop_policy' is deprecated and slated for removal in Python 3.16`

nest_asyncio2.py

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,29 @@ def apply(loop=None):
1515
_patch_policy()
1616
_patch_tornado()
1717

18-
loop = loop or asyncio.get_event_loop()
19-
_patch_loop(loop)
18+
loop = loop or _get_event_loop()
19+
if loop is not None:
20+
_patch_loop(loop)
21+
22+
if sys.version_info < (3, 12, 0):
23+
def _get_event_loop():
24+
return asyncio.get_event_loop()
25+
elif sys.version_info < (3, 14, 0):
26+
def _get_event_loop():
27+
# Python 3.12~3.13:
28+
# Calling get_event_loop() will result in ResourceWarning: unclosed event loop
29+
loop = events._get_running_loop()
30+
if loop is None:
31+
policy = events.get_event_loop_policy()
32+
loop = policy._local._loop
33+
return loop
34+
else:
35+
def _get_event_loop():
36+
# Python 3.14: Raises a RuntimeError if there is no current event loop.
37+
try:
38+
return asyncio.get_event_loop()
39+
except RuntimeError:
40+
return None
2041

2142
if sys.version_info < (3, 12, 0):
2243
def run(main, *, debug=False):
@@ -32,11 +53,11 @@ def run(main, *, debug=False):
3253
loop.run_until_complete(task)
3354
else:
3455
def run(main, *, debug=False, loop_factory=None):
56+
new_event_loop = False
57+
set_event_loop = None
3558
try:
3659
loop = asyncio.get_running_loop()
3760
except RuntimeError:
38-
if loop_factory is None:
39-
loop_factory = asyncio.new_event_loop
4061
# if sys.version_info < (3, 16, 0):
4162
# policy = asyncio.events._get_event_loop_policy()
4263
# try:
@@ -45,7 +66,14 @@ def run(main, *, debug=False, loop_factory=None):
4566
# loop = loop_factory()
4667
# else:
4768
# loop = loop_factory()
48-
loop = loop_factory()
69+
if loop_factory is None:
70+
loop = asyncio.new_event_loop()
71+
# Not running
72+
set_event_loop = _get_event_loop()
73+
asyncio.set_event_loop(loop)
74+
else:
75+
loop = loop_factory()
76+
new_event_loop = True
4977

5078
loop.set_debug(debug)
5179
task = asyncio.ensure_future(main, loop=loop)
@@ -56,6 +84,12 @@ def run(main, *, debug=False, loop_factory=None):
5684
task.cancel()
5785
with suppress(asyncio.CancelledError):
5886
loop.run_until_complete(task)
87+
if set_event_loop:
88+
# asyncio.Runner just set_event_loop(None) but we are nested
89+
asyncio.set_event_loop(set_event_loop)
90+
if new_event_loop:
91+
# Avoid ResourceWarning: unclosed event loop
92+
loop.close()
5993

6094
def _patch_asyncio():
6195
"""Patch asyncio module to use pure Python tasks and futures."""
@@ -94,9 +128,13 @@ def _get_event_loop(stacklevel=3):
94128
def _patch_policy():
95129
"""Patch the policy to always return a patched loop."""
96130

131+
# Python 3.14:
132+
# get_event_loop() raises a RuntimeError if there is no current event loop.
133+
# So there is no need to _patch_loop() in it.
134+
# Patching new_event_loop() may be better, but policy is going to be removed...
97135
# Removed in Python 3.16
98136
# https://github.com/python/cpython/issues/127949
99-
if sys.version_info >= (3, 16, 0):
137+
if sys.version_info >= (3, 14, 0):
100138
return
101139

102140
def get_event_loop(self):

tests/314_task.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# /// script
2-
# requires-python = ">=3.14"
2+
# requires-python = ">=3.12"
33
# dependencies = [
44
# "nest-asyncio2",
55
# ]

tests/314_task_mix.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# /// script
2-
# requires-python = ">=3.14"
2+
# requires-python = ">=3.12"
33
# dependencies = [
44
# "nest-asyncio2",
55
# ]

tests/test.ps1

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
pushd tests
22

3+
function Test {
4+
param(
5+
[string]$Py,
6+
[string[]]$V
7+
)
8+
9+
foreach ($v in $V) {
10+
Write-Host Test: $Py on $v
11+
# Catch ResourceWarning: unclosed event loop
12+
$out = uv run --python $v $Py 2>&1
13+
Write-Output $out
14+
if (!$? -or $out -match "Warning") {
15+
throw "$Py on $v : $out"
16+
}
17+
Write-Host
18+
}
19+
}
20+
321
# uv run --python 3.5 nest_test.py
422
uv run --python 3.8 nest_test.py
523
if (!$?) {
@@ -39,13 +57,7 @@ if (!$?) {
3957
throw "314_loop_factory"
4058
}
4159

42-
uv run --python 3.14 314_task.py
43-
if (!$?) {
44-
throw "314_task"
45-
}
46-
uv run --python 3.14 314_task_mix.py
47-
if (!$?) {
48-
throw "314_task_mix"
49-
}
60+
Test -V @("3.12", "3.13", "3.14") -Py 314_task.py
61+
Test -V @("3.12", "3.13", "3.14") -Py 314_task_mix.py
5062

5163
popd

0 commit comments

Comments
 (0)