Skip to content

Commit be27e97

Browse files
committed
Fix InputsProxy and add tests
1 parent fb26cf3 commit be27e97

File tree

3 files changed

+98
-37
lines changed

3 files changed

+98
-37
lines changed

examples/moduleapp/app.py

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,32 @@
1-
# To run this app:
2-
# python3 app.py
3-
4-
# Then point web browser to:
5-
# http://localhost:8000/
6-
7-
# Add parent directory to path, so we can find the prism module.
8-
# (This is just a temporary fix)
9-
import os
10-
import sys
11-
12-
# This will load the shiny module dynamically, without having to install it.
13-
# This makes the debug/run cycle quicker.
14-
shiny_module_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
15-
sys.path.insert(0, shiny_module_dir)
16-
1+
import shiny.ui_toolkit as st
172
from shiny import *
183

4+
195
# =============================================================================
206
# Counter module
217
# =============================================================================
228
def counter_module_ui(
239
ns: Callable[[str], str], label: str = "Increment counter"
2410
) -> TagChildArg:
2511
return TagList(
26-
input_action_button(id=ns("button"), label=label),
27-
output_text_verbatim(id=ns("out")),
12+
st.input_action_button(id=ns("button"), label=label),
13+
st.output_text_verbatim(id=ns("out")),
2814
)
2915

3016

31-
def counter_module_server(session: SessionProxy):
32-
count: ReactiveVal[int] = ReactiveVal(0)
17+
def counter_module_server(
18+
input: InputsProxy, output: OutputsProxy, session: SessionProxy
19+
):
20+
count: reactive.Value[int] = reactive.Value(0)
3321

34-
@observe()
22+
@reactive.effect()
23+
@event(session.input.button)
3524
def _():
36-
session.input["button"]
37-
with isolate():
38-
count(count() + 1)
25+
count.set(count() + 1)
3926

40-
@session.output("out")
41-
def _() -> str:
27+
@output()
28+
@render_text()
29+
def out() -> str:
4230
return f"Click count is {count()}"
4331

4432

@@ -48,19 +36,15 @@ def _() -> str:
4836
# =============================================================================
4937
# App that uses module
5038
# =============================================================================
51-
ui = page_fluid(
39+
ui = st.page_fluid(
5240
counter_module.ui("counter1", "Counter 1"),
5341
counter_module.ui("counter2", "Counter 2"),
5442
)
5543

5644

57-
def server(session: Session):
45+
def server(input: Inputs, output: Outputs, session: Session):
5846
counter_module.server("counter1")
5947
counter_module.server("counter2")
6048

6149

6250
app = App(ui, server)
63-
64-
65-
if __name__ == "__main__":
66-
app.run()

shiny/shinymodule.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
"ShinyModule",
77
)
88

9-
from typing import Optional, Union, Callable, Any
9+
from typing import Any, Callable, Optional
1010

1111
from htmltools.core import TagChildArg
1212

13-
from .session import Session, Inputs, Outputs, _require_active_session
1413
from .reactive import Value
1514
from .render import RenderFunction
15+
from .session import Inputs, Outputs, Session, _require_active_session
1616

1717

1818
class InputsProxy(Inputs):
@@ -34,14 +34,14 @@ def __delitem__(self, key: str) -> None:
3434

3535
# Allow access of values as attributes.
3636
def __setattr__(self, attr: str, value: Value[Any]) -> None:
37-
if attr in ("_values", "_ns"):
38-
super().__setattr__(attr, value)
37+
if attr in ("_values", "_ns", "_ns_key"):
38+
object.__setattr__(self, attr, value)
3939
return
4040
else:
4141
self.__setitem__(attr, value)
4242

4343
def __getattr__(self, attr: str) -> Value[Any]:
44-
if attr in ("_values", "_ns"):
44+
if attr in ("_values", "_ns", "_ns_key"):
4545
return object.__getattribute__(self, attr)
4646
else:
4747
return self.__getitem__(attr)

tests/test_modules.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
"""Tests for `ShinyModule`."""
2+
3+
import pytest
4+
5+
import shiny.ui_toolkit as ui
6+
from shiny import *
7+
8+
9+
def mod_ui(ns: Callable[[str], str]) -> TagChildArg:
10+
return TagList(
11+
ui.input_action_button(id=ns("button"), label="module1"),
12+
ui.output_text_verbatim(id=ns("out")),
13+
)
14+
15+
16+
# Note: We currently can't test Session; this is just here for future use.
17+
def mod_server(input: InputsProxy, output: OutputsProxy, session: SessionProxy):
18+
count: reactive.Value[int] = reactive.Value(0)
19+
20+
@reactive.effect()
21+
@event(session.input.button)
22+
def _():
23+
count.set(count() + 1)
24+
25+
@output()
26+
@render_text()
27+
def out() -> str:
28+
return f"Click count is {count()}"
29+
30+
31+
mod = ShinyModule(mod_ui, mod_server)
32+
33+
34+
def test_module_ui():
35+
x = mod.ui("mod1")
36+
assert x[0].attrs["id"] == "mod1-button"
37+
assert x[1].attrs["id"] == "mod1-out"
38+
39+
40+
@pytest.mark.asyncio
41+
async def test_inputs_proxy():
42+
input = session.Inputs(a=1)
43+
input_proxy = InputsProxy("mod1", input)
44+
45+
with isolate():
46+
assert input.a() == 1
47+
# Different ways of accessing "a" from the input proxy.
48+
assert input_proxy.a() is None
49+
assert input_proxy["a"]() is None
50+
assert input["mod1-a"]() is None
51+
52+
input_proxy.a.set(2)
53+
54+
with isolate():
55+
assert input.a() == 1
56+
assert input_proxy.a() == 2
57+
assert input_proxy["a"]() == 2
58+
assert input["mod1-a"]() == 2
59+
60+
# Nested input proxies
61+
input_proxy_proxy = InputsProxy("mod2", input_proxy)
62+
with isolate():
63+
assert input.a() == 1
64+
assert input_proxy.a() == 2
65+
# Different ways of accessing "a" from the input proxy.
66+
assert input_proxy_proxy.a() is None
67+
assert input_proxy_proxy["a"]() is None
68+
assert input["mod1-mod2-a"]() is None
69+
70+
input_proxy_proxy.a.set(3)
71+
72+
with isolate():
73+
assert input.a() == 1
74+
assert input_proxy.a() == 2
75+
assert input_proxy_proxy.a() == 3
76+
assert input_proxy_proxy["a"]() == 3
77+
assert input["mod1-mod2-a"]() == 3

0 commit comments

Comments
 (0)