Skip to content

Commit 2a94e46

Browse files
committed
Inject uvloop in Python 3.11+ explicitly (mandatory for Python 3.14+)
Signed-off-by: Sergey Vasilyev <[email protected]>
1 parent c54ddf8 commit 2a94e46

File tree

2 files changed

+42
-17
lines changed

2 files changed

+42
-17
lines changed

kopf/_kits/loops.py

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import asyncio
22
import contextlib
3-
from collections.abc import Generator
3+
import sys
4+
from collections.abc import Iterator
5+
from typing import Optional
46

57

68
@contextlib.contextmanager
7-
def proper_loop(suggested_loop: asyncio.AbstractEventLoop | None = None) -> Generator[None, None, None]:
9+
def proper_loop(suggested_loop: Optional[asyncio.AbstractEventLoop] = None) -> Iterator[asyncio.AbstractEventLoop | None]:
810
"""
911
Ensure that we have the proper loop, either suggested or properly managed.
1012
@@ -15,22 +17,43 @@ def proper_loop(suggested_loop: asyncio.AbstractEventLoop | None = None) -> Gene
1517
This loop manager is usually used in CLI only, not deeper than that;
1618
i.e. not even in ``kopf.run()``, since uvloop is only auto-managed for CLI.
1719
"""
18-
original_policy = asyncio.get_event_loop_policy()
19-
if suggested_loop is None: # the pure CLI use, not a KopfRunner or other code
20+
# Event loop policies were deprecated in 3.14 entirely. Yet they still exist in older versions.
21+
# However, the asyncio.Runner was introduced in Python 3.11, so we can use the logic from there.
22+
if suggested_loop is not None:
23+
yield suggested_loop
24+
25+
elif sys.version_info >= (3, 11): # optional in 3.11-3.13, mandatory in >=3.14
26+
# Use uvloop if available by injecting it as the selected loop.
2027
try:
2128
import uvloop
2229
except ImportError:
2330
pass
2431
else:
25-
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
32+
with asyncio.Runner(loop_factory=uvloop.new_event_loop) as runner:
33+
yield runner.get_loop()
34+
return
35+
36+
# Use the default loop/runner in place, do not inject anything.
37+
yield None
2638

27-
try:
28-
yield
39+
# For Python<=3.10, use the event-loop-policy-based injection.
40+
else:
41+
original_policy = asyncio.get_event_loop_policy()
42+
if suggested_loop is None: # the pure CLI use, not a KopfRunner or other code
43+
try:
44+
import uvloop
45+
except ImportError:
46+
pass
47+
else:
48+
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
2949

30-
finally:
3150
try:
32-
import uvloop
33-
except ImportError:
34-
pass
35-
else:
36-
asyncio.set_event_loop_policy(original_policy)
51+
yield
52+
53+
finally:
54+
try:
55+
import uvloop
56+
except ImportError:
57+
pass
58+
else:
59+
asyncio.set_event_loop_policy(original_policy)

kopf/cli.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def run(
9595
paths=paths,
9696
modules=modules,
9797
)
98-
with loops.proper_loop(__controls.loop):
98+
with loops.proper_loop(suggested_loop=__controls.loop) as actual_loop:
9999
return running.run(
100100
standalone=standalone,
101101
namespaces=namespaces,
@@ -108,7 +108,7 @@ def run(
108108
stop_flag=__controls.stop_flag,
109109
ready_flag=__controls.ready_flag,
110110
vault=__controls.vault,
111-
loop=__controls.loop,
111+
loop=actual_loop,
112112
)
113113

114114

@@ -141,13 +141,14 @@ def freeze(
141141
settings = configuration.OperatorSettings()
142142
settings.peering.name = peering_name
143143
settings.peering.priority = priority
144-
with loops.proper_loop(__controls.loop):
144+
with loops.proper_loop(suggested_loop=__controls.loop) as actual_loop:
145145
return running.run(
146146
clusterwide=clusterwide,
147147
namespaces=namespaces,
148148
insights=insights,
149149
identity=identity,
150150
settings=settings,
151+
loop=actual_loop,
151152
_command=peering.touch_command(
152153
insights=insights,
153154
identity=identity,
@@ -174,13 +175,14 @@ def resume(
174175
insights = references.Insights()
175176
settings = configuration.OperatorSettings()
176177
settings.peering.name = peering_name
177-
with loops.proper_loop(__controls.loop):
178+
with loops.proper_loop(suggested_loop=__controls.loop) as actual_loop:
178179
return running.run(
179180
clusterwide=clusterwide,
180181
namespaces=namespaces,
181182
insights=insights,
182183
identity=identity,
183184
settings=settings,
185+
loop=actual_loop,
184186
_command=peering.touch_command(
185187
insights=insights,
186188
identity=identity,

0 commit comments

Comments
 (0)