Skip to content

Commit 73da176

Browse files
committed
test renderer
tests pass but logs show a task is not getting cancelled appropriately when the loop closes
1 parent b93bbd9 commit 73da176

File tree

5 files changed

+115
-15
lines changed

5 files changed

+115
-15
lines changed

idom/core/render.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
RecvCoroutine = Callable[[], Awaitable[LayoutEvent]]
1212

1313

14+
class StopRendering(Exception):
15+
"""Raised to gracefully stop :meth:`AbstractRenderer.run`"""
16+
17+
1418
class AbstractRenderer(abc.ABC):
1519
"""A base class for implementing :class:`~idom.core.layout.Layout` renderers."""
1620

@@ -23,10 +27,24 @@ async def run(self, send: SendCoroutine, recv: RecvCoroutine, context: Any) -> N
2327
This will call :meth:`AbstractLayout.render` and :meth:`AbstractLayout.trigger`
2428
to render new models and execute events respectively.
2529
"""
30+
self._running = True
31+
try:
32+
await self._run(send, recv, context)
33+
except StopRendering:
34+
return None
35+
finally:
36+
await self._stop()
37+
38+
async def _run(
39+
self, send: SendCoroutine, recv: RecvCoroutine, context: Any
40+
) -> None:
2641
await asyncio.gather(
2742
self._outgoing_loop(send, context), self._incoming_loop(recv, context)
2843
)
2944

45+
async def _stop(self) -> None:
46+
pass
47+
3048
async def _outgoing_loop(self, send: SendCoroutine, context: Any) -> None:
3149
while True:
3250
await send(await self._outgoing(self._layout, context))
@@ -58,6 +76,7 @@ async def _outgoing(self, layout: AbstractLayout, context: Any) -> Dict[str, Any
5876
try:
5977
src, new, old = await layout.render()
6078
except RenderError as error:
79+
raise
6180
if error.partial_render is None:
6281
raise
6382
logger.exception("Render failed")
@@ -84,10 +103,12 @@ def __init__(self, layout: AbstractLayout) -> None:
84103
self._updates: Dict[str, asyncio.Queue[LayoutUpdate]] = {}
85104
self._render_task = asyncio.ensure_future(self._render_loop(), loop=layout.loop)
86105

87-
async def run(self, send: SendCoroutine, recv: RecvCoroutine, context: str) -> None:
106+
async def _run(
107+
self, send: SendCoroutine, recv: RecvCoroutine, context: str
108+
) -> None:
88109
self._updates[context] = asyncio.Queue()
89110
try:
90-
await asyncio.gather(super().run(send, recv, context), self._render_task)
111+
await asyncio.gather(super()._run(send, recv, context), self._render_task)
91112
except Exception:
92113
del self._updates[context]
93114
raise
@@ -127,5 +148,5 @@ async def _outgoing(self, layout: AbstractLayout, context: str) -> Dict[str, Any
127148
src, new, old = await self._updates[context].get()
128149
return {"root": layout.root, "src": src, "new": new, "old": old}
129150

130-
def __del__(self) -> None:
151+
async def _stop(self) -> None:
131152
self._render_task.cancel()

requirements/extras.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# extra=stable,sanic
22
sanic <19.12.0
3-
sanic-cors >=0.9.9, <1.0
3+
sanic-cors >=0.9.9
44

55
# extra=matplotlib
66
matplotlib

requirements/prod.txt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
loguru ==0.3.2
2-
typing-extensions ==3.7.4
3-
mypy-extensions ==0.4.3
1+
loguru >=0.3.2
2+
typing-extensions >=3.7.4
3+
mypy-extensions >=0.4.3
4+
anyio >=1.3.0

tests/conftest.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,6 @@ def emit(self, record):
5050
logger.remove(handler_id)
5151

5252

53-
@pytest.fixture(scope="session", autouse=True)
54-
def fresh_client():
55-
idom.client.restore()
56-
yield
57-
idom.client.restore()
58-
59-
6053
@pytest.fixture
6154
def display(driver_get, server, mount, host, port, last_server_error):
6255
def display(element, query=""):
@@ -83,7 +76,7 @@ def get(query=""):
8376

8477

8578
@pytest.fixture(scope="session")
86-
def driver(pytestconfig):
79+
def driver(pytestconfig, fresh_client):
8780
chrome_options = Options()
8881

8982
if getattr(pytestconfig.option, "headless", False):
@@ -153,3 +146,10 @@ def last_server_error():
153146
def _clean_last_server_error(last_server_error):
154147
last_server_error.set(default_error)
155148
yield
149+
150+
151+
@pytest.fixture(scope="session")
152+
def fresh_client():
153+
idom.client.restore()
154+
yield
155+
idom.client.restore()

tests/test_core/test_render.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import asyncio
2+
import pytest
3+
4+
import idom
5+
from idom.core.layout import Layout, RenderError, LayoutEvent
6+
from idom.core.render import SingleStateRenderer, SharedStateRenderer, StopRendering
7+
8+
9+
async def test_render_error_without_partial_render_raises():
10+
class LayoutWithBadRenderError(Layout):
11+
async def render(self):
12+
raise RenderError("no partial render")
13+
14+
@idom.element
15+
async def pass_element(self):
16+
return idom.html.div()
17+
18+
bad_layout = LayoutWithBadRenderError(pass_element())
19+
renderer = SingleStateRenderer(bad_layout)
20+
21+
async def send(data):
22+
return None
23+
24+
async def recv():
25+
while True:
26+
await asyncio.sleep(1)
27+
28+
with pytest.raises(RenderError, match="no partial render"):
29+
await renderer.run(send, recv, None)
30+
31+
32+
async def test_shared_state_renderer():
33+
done = asyncio.Event()
34+
data_sent_1 = asyncio.Queue()
35+
data_sent_2 = []
36+
37+
async def send_1(data):
38+
await data_sent_1.put(data)
39+
40+
async def recv_1():
41+
sent = await data_sent_1.get()
42+
43+
element_id = sent["root"]
44+
element_data = sent["new"][element_id]
45+
46+
if element_data["attributes"]["count"] == 4:
47+
done.set()
48+
raise StopRendering()
49+
50+
target = element_data["eventHandlers"]["anEvent"]["target"]
51+
return LayoutEvent(target=target, data=[])
52+
53+
async def send_2(data):
54+
element_id = data["root"]
55+
element_data = data["new"][element_id]
56+
data_sent_2.append(element_data["attributes"]["count"])
57+
if done.is_set():
58+
raise ValueError(data_sent_2)
59+
60+
async def recv_2():
61+
await done.wait()
62+
raise StopRendering()
63+
64+
@idom.element
65+
async def Clickable(self, count=0):
66+
@idom.event
67+
async def an_event():
68+
self.update(count=count + 1)
69+
70+
return idom.html.div({"anEvent": an_event, "count": count})
71+
72+
renderer = SharedStateRenderer(Layout(Clickable()))
73+
74+
await asyncio.gather(
75+
renderer.run(send_1, recv_1, "1"), renderer.run(send_2, recv_2, "2"),
76+
)
77+
78+
assert data_sent_2 == [0, 1, 2, 3, 4]

0 commit comments

Comments
 (0)