Skip to content

Commit 802e096

Browse files
committed
add the installable policy API
1 parent 02e4922 commit 802e096

8 files changed

Lines changed: 152 additions & 10 deletions

File tree

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ The package exposes:
1919

2020
- a native extension module at `rsloop._loop`
2121
- a Python wrapper in [`python/rsloop/__init__.py`](./python/rsloop/__init__.py)
22-
- `rsloop.Loop`, `rsloop.new_event_loop()`, and `rsloop.run(...)`
22+
- `rsloop.Loop`, `rsloop.EventLoopPolicy`, `rsloop.new_event_loop()`,
23+
`rsloop.run(...)`, `rsloop.install()`, and `rsloop.uninstall()`
2324

2425
Repository metadata currently targets Python `>=3.8`. The packaged project now
2526
supports the core event-loop surface on Linux, macOS, and Windows, including
@@ -69,6 +70,19 @@ async def main():
6970
rsloop.run(main())
7071
```
7172

73+
Install as the default asyncio event loop policy:
74+
75+
```python
76+
import asyncio
77+
import rsloop
78+
79+
rsloop.install()
80+
try:
81+
asyncio.run(main())
82+
finally:
83+
rsloop.uninstall()
84+
```
85+
7286
Manual loop creation also works:
7387

7488
```python

docs/getting-started.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ uv add rsloop
2121
The package exports a small public surface:
2222

2323
- `rsloop.Loop`
24+
- `rsloop.EventLoopPolicy`
2425
- `rsloop.new_event_loop()`
2526
- `rsloop.run(...)`
27+
- `rsloop.install()`
28+
- `rsloop.uninstall()`
2629
- `rsloop.profile()`
2730
- `rsloop.start_profiler()`
2831
- `rsloop.stop_profiler()`
@@ -46,6 +49,28 @@ print(result)
4649

4750
This is similar to `asyncio.run(...)`, but it creates and uses an `rsloop` loop.
4851

52+
## Install as the default asyncio loop
53+
54+
Use `install()` when you want plain `asyncio` entry points to create `rsloop`
55+
loops:
56+
57+
```python
58+
import asyncio
59+
import rsloop
60+
61+
62+
rsloop.install()
63+
try:
64+
asyncio.run(main())
65+
finally:
66+
rsloop.uninstall()
67+
```
68+
69+
`uninstall()` restores the event loop policy that was active before
70+
`install()`, which is useful in tests that switch between loop implementations.
71+
If another library has already installed a different policy, `uninstall()`
72+
leaves that newer policy in place.
73+
4974
## Manual loop creation
5075

5176
Use manual loop creation when you need more control:

docs/how-it-works.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ The Python package lives in `python/rsloop/`.
2828
Important files:
2929

3030
- `__init__.py`: exports the public API
31-
- `_run.py`: defines `run(...)` and `new_event_loop()`
31+
- `_run.py`: defines `run(...)`, `new_event_loop()`, and the installable event
32+
loop policy
3233
- `_loop_compat.py`: compatibility helpers and monkeypatches
3334
- `_bootstrap.py`: startup helpers, including Windows DLL and SSL-related setup
3435
- `_profile.py`: small Python wrappers around the profiler API

docs/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ That means:
2424

2525
- `rsloop.run(...)` is the easiest way to start.
2626
- `rsloop.new_event_loop()` creates a loop object manually.
27+
- `rsloop.install()` installs an asyncio event loop policy that creates `rsloop`
28+
loops by default.
2729
- `rsloop.Loop` is the main event loop class.
2830
- Importing `rsloop` also installs a few compatibility patches so it fits better into normal `asyncio` code.
2931

python/rsloop/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,22 @@
1010
from ._profile import profiler_running
1111
from ._profile import start_profiler
1212
from ._profile import stop_profiler
13+
from ._run import EventLoopPolicy
14+
from ._run import install
1315
from ._run import new_event_loop
1416
from ._run import run
17+
from ._run import uninstall
1518

1619
__all__: tuple[str, ...] = (
20+
"EventLoopPolicy",
1721
"Loop",
1822
"__version__",
23+
"install",
1924
"new_event_loop",
2025
"profile",
2126
"profiler_running",
2227
"run",
2328
"start_profiler",
2429
"stop_profiler",
30+
"uninstall",
2531
)

python/rsloop/_run.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,46 @@
88
from ._loop_compat import cancel_all_tasks as __cancel_all_tasks
99

1010
_T = __typing.TypeVar("_T")
11+
_PREVIOUS_EVENT_LOOP_POLICY: __typing.Optional[__asyncio.AbstractEventLoopPolicy] = None
1112

1213

1314
def new_event_loop() -> Loop:
1415
return Loop()
1516

1617

18+
class EventLoopPolicy(__asyncio.DefaultEventLoopPolicy):
19+
"""Event loop policy that creates rsloop loops by default."""
20+
21+
def new_event_loop(self) -> Loop:
22+
return new_event_loop()
23+
24+
25+
def install() -> None:
26+
"""Install rsloop as asyncio's default event loop policy."""
27+
28+
global _PREVIOUS_EVENT_LOOP_POLICY
29+
30+
policy = __asyncio.get_event_loop_policy()
31+
if isinstance(policy, EventLoopPolicy):
32+
return
33+
34+
_PREVIOUS_EVENT_LOOP_POLICY = policy
35+
__asyncio.set_event_loop_policy(EventLoopPolicy())
36+
37+
38+
def uninstall() -> None:
39+
"""Restore the event loop policy that was active before install()."""
40+
41+
global _PREVIOUS_EVENT_LOOP_POLICY
42+
43+
if _PREVIOUS_EVENT_LOOP_POLICY is None:
44+
return
45+
46+
if isinstance(__asyncio.get_event_loop_policy(), EventLoopPolicy):
47+
__asyncio.set_event_loop_policy(_PREVIOUS_EVENT_LOOP_POLICY)
48+
_PREVIOUS_EVENT_LOOP_POLICY = None
49+
50+
1751
if __typing.TYPE_CHECKING:
1852

1953
def run(

src/stream_transport.rs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,7 +1482,7 @@ impl StreamTransportCore {
14821482
};
14831483
let low = low.unwrap_or(high / 4);
14841484

1485-
if high < low {
1485+
if high < low {
14861486
return Err(PyValueError::new_err(format!(
14871487
"high ({high:?}) must be >= low ({low:?}) must be >= 0"
14881488
)));
@@ -3208,9 +3208,7 @@ pub(crate) fn run_server_accept_blocking(params: BlockingAcceptLoop<ServerListen
32083208
#[cfg(target_os = "linux")]
32093209
async fn run_tcp_accept_task(server: Arc<ServerCore>, listener: StdTcpListener) {
32103210
profiling::scope!("stream.run_tcp_accept_task");
3211-
let poll_fd = listener
3212-
.try_clone()
3213-
.and_then(PollFd::new);
3211+
let poll_fd = listener.try_clone().and_then(PollFd::new);
32143212

32153213
let Ok(poll_fd) = poll_fd else {
32163214
return;
@@ -3352,10 +3350,7 @@ fn run_unix_accept_loop(params: BlockingAcceptLoop<StdUnixListener>) {
33523350

33533351
#[cfg(target_os = "linux")]
33543352
async fn run_unix_accept_task(server: Arc<ServerCore>, listener: StdUnixListener) {
3355-
let Ok(poll_fd) = listener
3356-
.try_clone()
3357-
.and_then(PollFd::new)
3358-
else {
3353+
let Ok(poll_fd) = listener.try_clone().and_then(PollFd::new) else {
33593354
return;
33603355
};
33613356

tests/test_run.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,71 @@ async def run_in_thread(func, /, *args):
2222

2323

2424
class RunTests(unittest.TestCase):
25+
def test_install_makes_asyncio_create_rsloop_loops(self) -> None:
26+
original_policy = asyncio.get_event_loop_policy()
27+
try:
28+
asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy())
29+
30+
rsloop.install()
31+
self.assertIsInstance(
32+
asyncio.get_event_loop_policy(),
33+
rsloop.EventLoopPolicy,
34+
)
35+
36+
loop = asyncio.new_event_loop()
37+
try:
38+
self.assertIsInstance(loop, rsloop.Loop)
39+
finally:
40+
loop.close()
41+
finally:
42+
rsloop.uninstall()
43+
asyncio.set_event_loop_policy(original_policy)
44+
45+
def test_installed_policy_affects_asyncio_run(self) -> None:
46+
original_policy = asyncio.get_event_loop_policy()
47+
try:
48+
asyncio.set_event_loop_policy(asyncio.DefaultEventLoopPolicy())
49+
rsloop.install()
50+
51+
async def main() -> bool:
52+
return isinstance(asyncio.get_running_loop(), rsloop.Loop)
53+
54+
self.assertTrue(asyncio.run(main()))
55+
finally:
56+
rsloop.uninstall()
57+
asyncio.set_event_loop_policy(original_policy)
58+
59+
def test_uninstall_restores_previous_event_loop_policy(self) -> None:
60+
original_policy = asyncio.get_event_loop_policy()
61+
previous_policy = asyncio.DefaultEventLoopPolicy()
62+
try:
63+
asyncio.set_event_loop_policy(previous_policy)
64+
rsloop.install()
65+
rsloop.uninstall()
66+
67+
self.assertIs(asyncio.get_event_loop_policy(), previous_policy)
68+
loop = asyncio.new_event_loop()
69+
try:
70+
self.assertNotIsInstance(loop, rsloop.Loop)
71+
finally:
72+
loop.close()
73+
finally:
74+
rsloop.uninstall()
75+
asyncio.set_event_loop_policy(original_policy)
76+
77+
def test_uninstall_does_not_replace_newly_installed_policy(self) -> None:
78+
original_policy = asyncio.get_event_loop_policy()
79+
other_policy = asyncio.DefaultEventLoopPolicy()
80+
try:
81+
rsloop.install()
82+
asyncio.set_event_loop_policy(other_policy)
83+
rsloop.uninstall()
84+
85+
self.assertIs(asyncio.get_event_loop_policy(), other_policy)
86+
finally:
87+
rsloop.uninstall()
88+
asyncio.set_event_loop_policy(original_policy)
89+
2590
def test_set_event_loop_accepts_rsloop_loop(self) -> None:
2691
loop = rsloop.new_event_loop()
2792
try:

0 commit comments

Comments
 (0)