Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* `shiny create` includes new and improved `ui.Chat()` template options. Most of these templates leverage the new [`{chatlas}` package](https://posit-dev.github.io/chatlas/), our opinionated approach to interfacing with various LLM. (#1806)

* Client data values (e.g., url info, output sizes/styles, etc.) can now be accessed in the server-side Python code via `session.clientdata`, which is collection of reactive `Inputs`. For example, `session.clientdata.url_search()` reactively reads the URL search parameters. See [this PR](https://github.com/posit-dev/py-shiny/pull/1832) for a more complete example. (#1832)

* Available `input` ids can now be listed via `dir(input)`. This also works on the new `session.clientdata` object. (#1832)

### Bug fixes

* `ui.Chat()` now correctly handles new `ollama.chat()` return value introduced in `ollama` v0.4. (#1787)
Expand Down
12 changes: 11 additions & 1 deletion shiny/session/_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ class Session(ABC):
id: str
input: Inputs
output: Outputs
clientdata: Inputs
user: str | None
groups: list[str] | None

Expand Down Expand Up @@ -519,6 +520,7 @@ def __init__(

self.input: Inputs = Inputs(dict())
self.output: Outputs = Outputs(self, self.ns, outputs=dict())
self.clientdata: Inputs = Inputs(dict())

self.user: str | None = None
self.groups: list[str] | None = None
Expand Down Expand Up @@ -692,7 +694,12 @@ def _manage_inputs(self, data: dict[str, object]) -> None:
# The keys[0] value is already a fully namespaced id; make that explicit by
# wrapping it in ResolvedId, otherwise self.input will throw an id
# validation error.
self.input[ResolvedId(keys[0])]._set(val)
k = keys[0]
self.input[ResolvedId(k)]._set(val)

if k.startswith(".clientdata_"):
k2 = k.split("_", 1)[1]
self.clientdata[ResolvedId(k2)]._set(val)

self.output._manage_hidden()

Expand Down Expand Up @@ -1352,6 +1359,9 @@ def __contains__(self, key: str) -> bool:
# creates a reactive dependency, and returns whether the value is set.
return self[key].is_set()

def __dir__(self):
return list(self._map.keys())


# ======================================================================================
# Outputs
Expand Down
21 changes: 21 additions & 0 deletions tests/playwright/shiny/session/clientdata/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# pyright: reportUnknownMemberType=false, reportUnknownVariableType=false
import matplotlib.pyplot as plt
import numpy as np

from shiny.express import input, render, session, ui

with ui.sidebar(open="closed"):
ui.input_slider("obs", "Number of observations:", min=0, max=1000, value=500)


@render.code
def clientdatatext():
cdata = session.clientdata
return "\n".join([f"{name} = {cdata[name]()}" for name in reversed(dir(cdata))])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def clientdatatext():
cdata = session.clientdata
return "\n".join([f"{name} = {cdata[name]()}" for name in reversed(dir(cdata))])
def client_data_text():
client_data = session.clientdata
return "\n".join([
f"{name} = {client_data[name]()}" for name in reversed(dir(client_data))
])

To avoid CDATA vibes.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't get autocomplete for session.clientdata, right? So you can use dir() to find out the methods but you won't get autocomplete for session.clientdata.url_*(). The autocomplete would be helpful but isn't a blocker.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, no autocomplete. And agreed, it'd be useful, but I also don't see a low effort way to faithfully type those values (not to mention them easily getting out of sync when shiny.js changes).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess maybe it'd be worth having a ClientData class though so we can at least make them discoverable through the API reference?

class ClientData(Inputs):
     "TODO: docs here"
    pass

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, hold on, maybe there is a sensible thing to do for autocomplete as well...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I also don't think all the values need to be accessible in autocomplete, but it'd be useful to have the url_ and other stable data as well

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the nudge here -- e1e4526 takes a more typing and documentation friendly approach



@render.plot
def myplot():
plt.figure()
plt.hist(np.random.normal(size=input.obs())) # type: ignore
plt.title("This is myplot")
25 changes: 25 additions & 0 deletions tests/playwright/shiny/session/clientdata/test_clientdata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import re

from playwright.sync_api import Page

from shiny.playwright import controller
from shiny.run import ShinyAppProc


def test_output_image_kitchen(page: Page, local_app: ShinyAppProc) -> None:

page.goto(local_app.url)

text = controller.OutputTextVerbatim(page, "clientdatatext")

# This doesn't cover all the clientdata values since some of them
# are environment-dependent. However, this at least checks that the
# clientdata object is available and that some values are present.
text.expect.to_contain_text("url_protocol = http")
text.expect.to_contain_text("url_pathname = /")
text.expect.to_contain_text(
re.compile("url_hostname = (localhost|127\\.0\\.0\\.1)")
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you test url_search, url_hash_initial and url_hash here? Those are all more valuable and likely to be used than, say, url_protocol.

Copy link
Collaborator Author

@cpsievert cpsievert Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a shinycoreci app that already does a good job of covering the front end logic (see comment added in a30b8c8), so I'd rather not redo it

text.expect.to_contain_text("output_myplot_hidden = False")
text.expect.to_contain_text("output_myplot_bg = rgb(255, 255, 255)")
text.expect.to_contain_text("output_clientdatatext_hidden = False")
Loading