Skip to content

Commit e1e4526

Browse files
committed
Take a fresh approach that is typing and documentation friendly
1 parent 1547687 commit e1e4526

File tree

3 files changed

+251
-21
lines changed

3 files changed

+251
-21
lines changed

shiny/session/_session.py

Lines changed: 216 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ class Session(ABC):
170170
id: str
171171
input: Inputs
172172
output: Outputs
173-
clientdata: Inputs
173+
clientdata: ClientData
174174
user: str | None
175175
groups: list[str] | None
176176

@@ -520,7 +520,7 @@ def __init__(
520520

521521
self.input: Inputs = Inputs(dict())
522522
self.output: Outputs = Outputs(self, self.ns, outputs=dict())
523-
self.clientdata: Inputs = Inputs(dict())
523+
self.clientdata: ClientData = ClientData(self)
524524

525525
self.user: str | None = None
526526
self.groups: list[str] | None = None
@@ -694,16 +694,8 @@ def _manage_inputs(self, data: dict[str, object]) -> None:
694694
# The keys[0] value is already a fully namespaced id; make that explicit by
695695
# wrapping it in ResolvedId, otherwise self.input will throw an id
696696
# validation error.
697-
k = keys[0]
698-
self.input[ResolvedId(k)]._set(val)
699697

700-
# shiny.js sets a special class of inputs (clientdata) for things like the
701-
# URL, output sizes, etc. On the frontend, these all have the prefix
702-
# ".clientdata_". For example, this is where .clientdata_url_search is set:
703-
# https://github.com/rstudio/shiny/blob/58e1521/srcts/src/shiny/index.ts#L631-L632
704-
if k.startswith(".clientdata_"):
705-
k2 = k.split("_", 1)[1]
706-
self.clientdata[ResolvedId(k2)]._set(val)
698+
self.input[ResolvedId(keys[0])]._set(val)
707699

708700
self.output._manage_hidden()
709701

@@ -1367,6 +1359,219 @@ def __dir__(self):
13671359
return list(self._map.keys())
13681360

13691361

1362+
@add_example()
1363+
class ClientData:
1364+
"""
1365+
Reactively read client data from the browser.
1366+
1367+
This class provides access to client data values, such as the URL components, the
1368+
pixel ratio of the device, and the properties of outputs.
1369+
1370+
Raises
1371+
------
1372+
RuntimeError
1373+
If a method is called outside of a reactive context.
1374+
"""
1375+
1376+
def __init__(self, session: Session) -> None:
1377+
self._session: Session = session
1378+
1379+
def url_hash(self) -> str:
1380+
"""
1381+
Reactively read the hash part of the URL.
1382+
"""
1383+
return self._read_input("url_hash")
1384+
1385+
def url_hash_initial(self) -> str:
1386+
"""
1387+
Reactively read the initial hash part of the URL.
1388+
"""
1389+
return self._read_input("url_hash_initial")
1390+
1391+
def url_hostname(self) -> str:
1392+
"""
1393+
Reactively read the hostname part of the URL.
1394+
"""
1395+
return self._read_input("url_hostname")
1396+
1397+
def url_pathname(self) -> str:
1398+
"""
1399+
The pathname part of the URL.
1400+
"""
1401+
return self._read_input("url_pathname")
1402+
1403+
def url_port(self) -> int:
1404+
"""
1405+
Reactively read the port part of the URL.
1406+
"""
1407+
return cast(int, self._read_input("url_port"))
1408+
1409+
def url_protocol(self) -> str:
1410+
"""
1411+
Reactively read the protocol part of the URL.
1412+
"""
1413+
return self._read_input("url_protocol")
1414+
1415+
def url_search(self) -> str:
1416+
"""
1417+
Reactively read the search part of the URL.
1418+
"""
1419+
return self._read_input("url_search")
1420+
1421+
def pixelratio(self) -> float:
1422+
"""
1423+
Reactively read the pixel ratio of the device.
1424+
"""
1425+
return cast(int, self._read_input("pixelratio"))
1426+
1427+
def output_height(self, id: str) -> float | None:
1428+
"""
1429+
Reactively read the height of an output.
1430+
1431+
Parameters
1432+
----------
1433+
id
1434+
The id of the output.
1435+
1436+
Returns
1437+
-------
1438+
float | None
1439+
The height of the output, or None if the output does not exist (or does not
1440+
report its height).
1441+
"""
1442+
return cast(float, self._read_output(id, "height"))
1443+
1444+
def output_width(self, id: str) -> float | None:
1445+
"""
1446+
Reactively read the width of an output.
1447+
1448+
Parameters
1449+
----------
1450+
id
1451+
The id of the output.
1452+
1453+
Returns
1454+
-------
1455+
float | None
1456+
The width of the output, or None if the output does not exist (or does not
1457+
report its width).
1458+
"""
1459+
return cast(float, self._read_output(id, "width"))
1460+
1461+
def output_hidden(self, id: str) -> bool | None:
1462+
"""
1463+
Reactively read whether an output is hidden.
1464+
1465+
Parameters
1466+
----------
1467+
id
1468+
The id of the output.
1469+
1470+
Returns
1471+
-------
1472+
bool | None
1473+
Whether the output is hidden, or None if the output does not exist.
1474+
"""
1475+
return cast(bool, self._read_output(id, "hidden"))
1476+
1477+
def output_bg_color(self, id: str) -> str | None:
1478+
"""
1479+
Reactively read the background color of an output.
1480+
1481+
Parameters
1482+
----------
1483+
id
1484+
The id of the output.
1485+
1486+
Returns
1487+
-------
1488+
str | None
1489+
The background color of the output, or None if the output does not exist (or
1490+
does not report its bg color).
1491+
"""
1492+
return cast(str, self._read_output(id, "bg"))
1493+
1494+
def output_fg_color(self, id: str) -> str | None:
1495+
"""
1496+
Reactively read the foreground color of an output.
1497+
1498+
Parameters
1499+
----------
1500+
id
1501+
The id of the output.
1502+
1503+
Returns
1504+
-------
1505+
str | None
1506+
The foreground color of the output, or None if the output does not exist (or
1507+
does not report its fg color).
1508+
"""
1509+
return cast(str, self._read_output(id, "fg"))
1510+
1511+
def output_accent_color(self, id: str) -> str | None:
1512+
"""
1513+
Reactively read the accent color of an output.
1514+
1515+
Parameters
1516+
----------
1517+
id
1518+
The id of the output.
1519+
1520+
Returns
1521+
-------
1522+
str | None
1523+
The accent color of the output, or None if the output does not exist (or
1524+
does not report its accent color).
1525+
"""
1526+
return cast(str, self._read_output(id, "accent"))
1527+
1528+
def output_font(self, id: str) -> str | None:
1529+
"""
1530+
Reactively read the font(s) of an output.
1531+
1532+
Parameters
1533+
----------
1534+
id
1535+
The id of the output.
1536+
1537+
Returns
1538+
-------
1539+
str | None
1540+
The font family of the output, or None if the output does not exist (or
1541+
does not report its font styles).
1542+
"""
1543+
return cast(str, self._read_output(id, "font"))
1544+
1545+
def _read_input(self, key: str) -> str:
1546+
self._check_current_context(key)
1547+
1548+
id = ResolvedId(f".clientdata_{key}")
1549+
if id not in self._session.input:
1550+
raise ValueError(
1551+
f"ClientData value '{key}' not found. Please report this issue."
1552+
)
1553+
1554+
return self._session.input[id]()
1555+
1556+
def _read_output(self, id: str, key: str) -> str | None:
1557+
self._check_current_context(f"output_{key}")
1558+
1559+
input_id = ResolvedId(f".clientdata_output_{id}_{key}")
1560+
if input_id in self._session.input:
1561+
return self._session.input[input_id]()
1562+
else:
1563+
return None
1564+
1565+
@staticmethod
1566+
def _check_current_context(key: str) -> None:
1567+
try:
1568+
reactive.get_current_context()
1569+
except RuntimeError:
1570+
raise RuntimeError(
1571+
f"session.clientdata.{key}() must be called from within a reactive context."
1572+
)
1573+
1574+
13701575
# ======================================================================================
13711576
# Outputs
13721577
# ======================================================================================

tests/playwright/shiny/session/clientdata/app.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,36 @@
77
with ui.sidebar(open="closed"):
88
ui.input_slider("obs", "Number of observations:", min=0, max=1000, value=500)
99

10+
ui.markdown(
11+
"""
12+
#### `session.clientdata` values
13+
14+
The following methods are available from the `session.clientdata` object and allow you
15+
to reactively read the client data values from the browser.
16+
"""
17+
)
18+
1019

1120
@render.code
1221
def clientdatatext():
13-
client_data = session.clientdata
14-
return "\n".join(
15-
[f"{name} = {client_data[name]()}" for name in reversed(dir(client_data))]
16-
)
22+
return f"""
23+
.url_hash() -> {session.clientdata.url_hash()}
24+
.url_hash_initial() -> {session.clientdata.url_hash_initial()}
25+
.url_hostname() -> {session.clientdata.url_hostname()}
26+
.url_pathname() -> {session.clientdata.url_pathname()}
27+
.url_port() -> {session.clientdata.url_port()}
28+
.url_protocol() -> {session.clientdata.url_protocol()}
29+
.url_search() -> {session.clientdata.url_search()}
30+
.pixelratio() -> {session.clientdata.pixelratio()}
31+
32+
.output_height("myplot") -> {session.clientdata.output_height("myplot")}
33+
.output_width("myplot") -> {session.clientdata.output_width("myplot")}
34+
.output_hidden("myplot") -> {session.clientdata.output_hidden("myplot")}
35+
.output_bg_color("myplot") -> {session.clientdata.output_bg_color("myplot")}
36+
.output_fg_color("myplot") -> {session.clientdata.output_fg_color("myplot")}
37+
.output_accent_color("myplot") -> {session.clientdata.output_accent_color("myplot")}
38+
.output_font("myplot") -> {session.clientdata.output_font("myplot")}
39+
"""
1740

1841

1942
@render.plot

tests/playwright/shiny/session/clientdata/test_clientdata.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ def test_output_image_kitchen(page: Page, local_app: ShinyAppProc) -> None:
1818
#
1919
# The important part is that we're testing here is that at least
2020
# some of these values are available in Python via session.clientdata
21-
text.expect.to_contain_text("url_protocol = http")
22-
text.expect.to_contain_text("url_pathname = /")
21+
text.expect.to_contain_text(re.compile(r"url_protocol\(\)\s+->\s+http"))
22+
text.expect.to_contain_text(re.compile(r"url_pathname\(\)\s+->\s+/"))
2323
text.expect.to_contain_text(
24-
re.compile("url_hostname = (localhost|127\\.0\\.0\\.1)")
24+
re.compile(r"url_hostname\(\)\s+->\s+(localhost|127\.0\.0\.1)")
2525
)
26-
text.expect.to_contain_text("output_myplot_hidden = False")
27-
text.expect.to_contain_text("output_myplot_bg = rgb(255, 255, 255)")
28-
text.expect.to_contain_text("output_clientdatatext_hidden = False")
26+
text.expect.to_contain_text(re.compile(r'output_hidden\("myplot"\)\s+->\s+False'))
27+
text.expect.to_contain_text(
28+
re.compile(r'output_bg_color\("myplot"\)\s+->\s+rgb\(255, 255, 255\)')
29+
)
30+
text.expect.to_contain_text(re.compile(r'output_hidden\("myplot"\)\s+->\s+False'))

0 commit comments

Comments
 (0)