Skip to content

Commit d78d1b1

Browse files
committed
Add automated test to detect asyncio usage during module load
1 parent e9813d3 commit d78d1b1

File tree

3 files changed

+41
-3
lines changed

3 files changed

+41
-3
lines changed

.github/workflows/pytest.yaml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,7 @@ jobs:
7575
- name: Run unit tests
7676
if: steps.install.outcome == 'success' && (success() || failure())
7777
run: |
78-
pwd
79-
cd tests
80-
pytest
78+
make test
8179
8280
- name: Type check with pyright
8381
if: steps.install.outcome == 'success' && (success() || failure())

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ lint: ## check style with flake8
5858
flake8 --show-source --max-line-length=127 shiny tests examples
5959

6060
test: ## run tests quickly with the default Python
61+
python3 tests/asyncio_prevent.py
6162
pytest
6263

6364
test-all: ## run tests on every Python version with tox

tests/asyncio_prevent.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""This script is NOT designed to be run as part of a pytest suite; it demands a clean
2+
Python process because it needs to be run before the shiny module is loaded.
3+
4+
The purpose of this script is to ensure that no shiny module initialization code uses
5+
the event loop. The running event loop during shiny module startup is different than the
6+
running event loop during the app's operation (I assume uvicorn creates the latter). The
7+
creation of, say, an asyncio.Lock during the course of shiny module startup will result
8+
in race conditions if the lock is used during the app's operation."""
9+
10+
if __name__ == "__main__":
11+
import sys
12+
import asyncio
13+
import importlib
14+
from typing import Optional
15+
16+
if "shiny" in sys.modules:
17+
raise RuntimeError(
18+
"Bad test: shiny was already loaded, it's important that SpyEventLoopPolicy"
19+
" is installed before shiny loads"
20+
)
21+
22+
class SpyEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
23+
def get_event_loop(self):
24+
raise RuntimeError("get_event_loop called")
25+
26+
def set_event_loop(self, loop: Optional[asyncio.AbstractEventLoop]):
27+
raise RuntimeError("set_event_loop called")
28+
29+
def new_event_loop(self):
30+
raise RuntimeError("new_event_loop called")
31+
32+
asyncio.set_event_loop_policy(SpyEventLoopPolicy())
33+
34+
# Doing this instead of "import shiny" so no linter is tempted to remove it
35+
importlib.import_module("shiny")
36+
sys.stderr.write(
37+
"Success; shiny module loading did not attempt to access an asyncio event "
38+
"loop\n"
39+
)

0 commit comments

Comments
 (0)