Skip to content

Commit 88a9fee

Browse files
authored
Merge branch 'main' into fix/js_css_include_perms
2 parents b48aea9 + 5bd00b6 commit 88a9fee

File tree

17 files changed

+248
-25
lines changed

17 files changed

+248
-25
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3333

3434
### Improvements
3535

36+
* Add support for selecting menu items in `Navset` controllers to improve dropdown navigation test coverage. (#2066)
37+
3638
* `input_date()`, `input_date_range()`, `update_date()`, and `update_date_range()` now supports `""` for values, mins, and maxes. In this case, no date will be specified on the client. (#1713) (#1689)
3739

3840
* Restricted the allowable types of the `choices` parameter of `input_select()`, `input_selectize()`, `update_select()`, and `update_selectize()` to actual set of allowable types (previously, the type was suggesting HTML-like values were supported). (#2048)
@@ -47,6 +49,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4749

4850
* Fixed false positive warning in `layout_columns()` about number of widths vs elements. (#1704)
4951

52+
* When errors occur in a bookmarking context, they are now reported in the Python console. (#2076)
53+
5054
### Bug fixes
5155

5256
* Fixed several issues related to including custom js and css fil
@@ -69,6 +73,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6973

7074
* Fixed `set()` method of `InputSelectize` controller so it clears existing selections before applying new values. (#2024)
7175

76+
* `include_js()` and `include_css()` now work as expected when trying to include multiple files from the same directory. (#2069)
77+
7278
### Deprecations
7379

7480
* `ui.update_navs()` has been deprecated in favor of `ui.update_navset()`. (#2047)

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,4 +260,4 @@ narwhals-test-integration: FORCE
260260
@echo "-------- Running py-shiny format, lint, typing, and unit tests ----------"
261261
$(MAKE) check
262262
@echo "-------- Running py-shiny playwright tests ----------"
263-
$(MAKE) playwright TEST_FILE="tests/playwright/shiny/components/data_frame" PYTEST_BROWSERS="--browser chromium"
263+
$(MAKE) playwright TEST_FILE="tests/playwright/shiny/components/data_frame/data_type/" PYTEST_BROWSERS="--browser chromium"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ To learn more about Shiny see the [Shiny for Python website](https://shiny.posit
1616

1717
- How to [use modules](https://shiny.posit.co/py/docs/workflow-modules.html) to efficiently develop large applications.
1818

19-
- Hosting applications for free on [shinyapps.io](https://shiny.posit.co/py/docs/deploy.html#deploy-to-shinyapps.io-cloud-hosting), [Hugging Face](https://shiny.posit.co/blog/posts/shiny-on-hugging-face/), or [Shinylive](https://shiny.posit.co/py/docs/shinylive.html).
19+
- Hosting applications for free on [Connect Cloud](https://docs.posit.co/connect-cloud/how-to/python/shiny-python.html), [shinyapps.io](https://shiny.posit.co/py/docs/deploy.html#deploy-to-shinyapps.io-cloud-hosting), [Hugging Face](https://shiny.posit.co/blog/posts/shiny-on-hugging-face/), or [Shinylive](https://shiny.posit.co/py/docs/shinylive.html).
2020

2121
## Join the conversation
2222

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ test = [
6060
"playwright>=1.48.0",
6161
"pytest-xdist",
6262
"pytest-timeout",
63-
"pytest-rerunfailures",
63+
"pytest-rerunfailures<16",
6464
"pytest-cov",
6565
"coverage",
6666
"syrupy>=4.7.1",

shiny/bookmark/_bookmark.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -558,15 +558,10 @@ async def do_bookmark(self) -> None:
558558
await self.show_bookmark_url_modal(full_url)
559559

560560
except Exception as e:
561-
msg = f"Error bookmarking state: {e}"
562-
from ..ui._notification import notification_show
563-
564-
notification_show(
565-
msg,
566-
duration=None,
567-
type="error",
568-
session=self._root_session,
569-
)
561+
from ..types import NotifyException
562+
563+
msg = f"Error bookmarking state: {str(e)}"
564+
raise NotifyException(msg) from e
570565

571566

572567
class BookmarkProxy(Bookmark):

shiny/module.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,23 @@ def ui(fn: Callable[P, R]) -> Callable[Concatenate[str, P], R]:
2929
"""
3030
Decorator for defining a Shiny module UI function.
3131
32-
This decorator allows you to write the UI portion of a Shiny module.
33-
When your decorated `ui` function is called with an `id`,
34-
the UI elements defined within will automatically be namespaced using that `id`.
32+
A Shiny module is a reusable component that can be embedded within Shiny apps or
33+
other Shiny modules. Each module consists of a UI function and a server function.
34+
Use this decorator to mark the UI function for a module.
35+
36+
The UI function can take whatever parameters are required to create the UI; for
37+
example, a label or a default value. It can also take no parameters, if none are
38+
required.
39+
40+
Whatever parameters the UI function takes, the `ui` decorator will prepend the
41+
signature with a new `id` argument. This argument will be an id string passed by the
42+
caller, that uniquely identifies the module instance within the calling scope.
43+
44+
When the decorated function is called, any Shiny input or output elements created
45+
within the function will automatically have their `id` values prefixed with the
46+
module instance's `id`. This ensures that the input and output elements are uniquely
47+
namespaced and won't conflict with other elements in the same app.
48+
3549
This enables reuse of UI components and consistent input/output handling
3650
when paired with a :func:`shiny.module.server` function.
3751
@@ -44,8 +58,9 @@ def ui(fn: Callable[P, R]) -> Callable[Concatenate[str, P], R]:
4458
Returns
4559
-------
4660
:
47-
A function that takes a `str` `id` as its first argument, followed by any additional
48-
parameters accepted by `fn`. When called, it returns UI elements with input/output
61+
The decorated UI function. The function takes a `str` `id` as its first argument,
62+
followed by any additional parameters accepted by `fn`.
63+
When called, it returns UI elements with input/output
4964
IDs automatically namespaced using the provided module `id`.
5065
5166
See Also
@@ -68,6 +83,21 @@ def server(
6883
"""
6984
Decorator for defining a Shiny module server function.
7085
86+
A Shiny module is a reusable component that can be embedded within Shiny apps or
87+
other Shiny modules. Each module consists of a UI function and a server function.
88+
This decorator is used to encapsulate the server logic for a Shiny module.
89+
90+
Every Shiny module server function must always begin with the same three arguments:
91+
`input`, `output`, and `session`, just like a Shiny app's server function.
92+
93+
After `input`, `output`, and `session`, the server function may include additional
94+
parameters to be used in the server logic; for example, reactive data sources or
95+
file paths that need to be provided by the caller.
96+
97+
This decorator modifies the signature of the decorated server function. The `input`,
98+
`output`, and `session` parameters are removed, and a new `id` parameter is
99+
prepended to the signature.
100+
71101
This decorator is used to encapsulate the server logic for a Shiny module.
72102
It automatically creates a namespaced child `Session` using the provided module `id`,
73103
and passes the appropriate `input`, `output`, and `session` objects to your server function.
@@ -84,9 +114,13 @@ def server(
84114
Returns
85115
-------
86116
:
117+
The decorated server function.
87118
A function that takes a module `id` (as a string) as its first argument,
88119
followed by any arguments expected by `fn`. When called, it will register
89120
the module's server logic in a namespaced context.
121+
The function signature of `fn` will have been
122+
modified to remove `input`, `output`, and `session`, and to prepend a new `id`
123+
parameter.
90124
91125
See Also
92126
--------

shiny/playwright/controller/_navs.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,35 @@ def click(self, *, timeout: Timeout = None) -> None:
145145
"""
146146
Clicks the nav panel.
147147
148+
If the nav panel is inside a dropdown, playwright will first open the dropdown before selecting the nav panel.
149+
148150
Parameters
149151
----------
150152
timeout
151153
The maximum time to wait for the nav panel to be visible and interactable. Defaults to `None`.
152154
"""
155+
156+
parent_ul_loc = self.loc.locator("..").locator("..")
157+
158+
parent_ul_cls = parent_ul_loc.element_handle().get_attribute("class")
159+
cls_menu_regex = re.compile(rf"(^|\s+){re.escape('dropdown-menu')}(\s+|$)")
160+
cls_show_regex = re.compile(rf"(^|\s+){re.escape('show')}(\s+|$)")
161+
cls_dropdown_regex = re.compile(rf"(^|\s+){re.escape('dropdown')}(\s+|$)")
162+
163+
# If the item is in a dropdown and the dropdown is closed
164+
if (
165+
parent_ul_cls
166+
and cls_menu_regex.search(parent_ul_cls)
167+
and not cls_show_regex.search(parent_ul_cls)
168+
):
169+
grandparent_li_loc = parent_ul_loc.locator("..")
170+
gnd_li_cls = grandparent_li_loc.element_handle().get_attribute("class")
171+
172+
# Confirm it is a dropdown
173+
if gnd_li_cls and cls_dropdown_regex.search(gnd_li_cls):
174+
# click the grandparent list item to open it before clicking the target item
175+
grandparent_li_loc.click()
176+
153177
self.loc.click(timeout=timeout)
154178

155179
def expect_active(self, value: bool, *, timeout: Timeout = None) -> None:

shiny/reactive/_reactives.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -597,9 +597,10 @@ async def _run(self) -> None:
597597
from ..ui import notification_show
598598

599599
msg = str(e)
600+
warnings.warn(msg, ReactiveWarning, stacklevel=2)
600601
if e.sanitize:
601602
msg = SANITIZE_ERROR_MSG
602-
notification_show(msg, type="error", duration=5000)
603+
notification_show(msg, type="error", duration=None)
603604
if e.close:
604605
await self._session._unhandled_error(e)
605606
except Exception as e:

shiny/ui/_include_helpers.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,20 +36,20 @@ def include_js(
3636
method
3737
One of the following:
3838
39-
* ``"link"`` is the link to the CSS file via a :func:`~shiny.ui.tags.link` tag. This
39+
* ``"link"`` is the link to the JS file via a :func:`~shiny.ui.tags.script` tag. This
4040
method is generally preferable to ``"inline"`` since it allows the browser to
4141
cache the file.
42-
* ``"link_files"`` is the same as ``"link"``, but also allow for the CSS file to
42+
* ``"link_files"`` is the same as ``"link"``, but also allow for the JS file to
4343
request other files within ``path``'s immediate parent directory (e.g.,
44-
``@import()`` another file). Note that this isn't the default behavior because
44+
``import`` another file). Note that this isn't the default behavior because
4545
you should **be careful not to include files in the same directory as ``path``
4646
that contain sensitive information**. A good general rule of thumb to follow
4747
is to have ``path`` be located in a subdirectory of the app directory. For
4848
example, if the app's source is located at ``/app/app.py``, then ``path``
49-
should be somewhere like ``/app/css/custom.css`` (and all the other relevant
49+
should be somewhere like ``/app/js/custom.js`` (and all the other relevant
5050
accompanying 'safe' files should be located under ``/app/css/``).
51-
* ``"inline"`` is the inline the CSS file contents within a
52-
:func:`~shiny.ui.tags.style` tag.
51+
* ``"inline"`` is the inline the JS file contents within a
52+
:func:`~shiny.ui.tags.script` tag.
5353
**kwargs
5454
Attributes which are passed on to `~shiny.ui.tags.script`.
5555
@@ -234,7 +234,6 @@ def maybe_copy_files(path: Path | str, include_files: bool) -> tuple[str, str]:
234234
else:
235235
os.makedirs(tmpdir, exist_ok=True)
236236
shutil.copy(path, path_dest)
237-
238237
return path_dest, hash
239238

240239

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from pathlib import Path
2+
3+
from shiny import App, Inputs, Outputs, Session, ui
4+
5+
custom_css = ui.include_css(
6+
Path(__file__).parent / "css" / "style.css",
7+
method="link_files",
8+
)
9+
10+
custom_js = ui.include_js(
11+
Path(__file__).parent / "js" / "customjs.js",
12+
method="link_files",
13+
)
14+
15+
# path where the JS file's parent directory is mounted
16+
href = custom_js.get_dependencies()[0].source_path_map()["href"]
17+
18+
# Define the UI
19+
app_ui = ui.page_fluid(
20+
custom_css,
21+
custom_js,
22+
ui.tags.script(src=href + "/customjs2.js"),
23+
ui.h1("Simple Shiny App with External CSS"),
24+
ui.div(
25+
ui.p("This is a simple Shiny app that demonstrates ui.include_css()"),
26+
ui.p("The styling comes from an external CSS file!"),
27+
class_="content",
28+
),
29+
)
30+
31+
32+
# Define the server
33+
def server(input: Inputs, output: Outputs, session: Session):
34+
pass
35+
36+
37+
# Create and run the app
38+
app = App(app_ui, server)

0 commit comments

Comments
 (0)