Skip to content

Commit 3ef36f3

Browse files
committed
Merge branch 'main' into integrate-test-generator
* main: fix: Make outside users able to read tmp files (#2070) docs: update module server and ui to incorporate #705 (#2044) fix: errors on bookmark are now surfaced in the Python console (#2076) Add Connect Cloud as a hosting option in README (#2074) Update changelog Update changelog fix: include_css and include_js can use files in same dir (#2069)
2 parents d7a06ab + c78c8f1 commit 3ef36f3

File tree

15 files changed

+193
-27
lines changed

15 files changed

+193
-27
lines changed

CHANGELOG.md

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

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

54+
* When errors occur in a bookmarking context, they are now reported in the Python console. (#2076)
55+
5456
### Bug fixes
5557

5658
* Fixed numerous issues related to programmatically updating selectize options. (#2053)
@@ -69,6 +71,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6971

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

74+
* `include_js()` and `include_css()` now work as expected when trying to include multiple files from the same directory. (#2069)
75+
76+
* `include_js()` and `include_css()` now correctly handle file permissions in multi-user settings. (#2061)
77+
7278
### Deprecations
7379

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

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

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/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: 11 additions & 11 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
@@ -217,11 +217,11 @@ def maybe_copy_files(path: Path | str, include_files: bool) -> tuple[str, str]:
217217

218218
# To avoid unnecessary work when the same file is included multiple times,
219219
# use a directory scoped by a hash of the file.
220-
tmpdir = os.path.join(tempfile.gettempdir(), "shiny_include_files", hash)
220+
tmpdir = os.path.join(tempfile.gettempdir(), f"shiny_include_{hash}")
221221
path_dest = os.path.join(tmpdir, os.path.basename(path))
222222

223223
# Since the hash/tmpdir should represent all the files in the path's directory,
224-
# we can simply return here
224+
# we can check if it exists to determine if we have a cache hit
225225
if os.path.exists(path_dest):
226226
return path_dest, hash
227227

@@ -234,17 +234,17 @@ 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

241240
def get_hash(path: Path | str, include_files: bool) -> str:
242241
if include_files:
243-
key = get_file_key(path)
244-
else:
245242
dir = os.path.dirname(path)
246243
files = glob.iglob(os.path.join(dir, "**"), recursive=True)
247244
key = "\n".join([get_file_key(x) for x in files])
245+
else:
246+
key = get_file_key(path)
247+
248248
return hash_deterministic(key)
249249

250250

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)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
body {
2+
font-family: Arial, sans-serif;
3+
}
4+
5+
h1 {
6+
color: black;
7+
border-bottom: 2px solid #4682b4;
8+
padding-bottom: 10px;
9+
}
10+
11+
.content {
12+
margin: 20px;
13+
padding: 15px;
14+
background-color: white;
15+
border-radius: 5px;
16+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
17+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const newParagraph = document.createElement('p');
2+
newParagraph.textContent = 'Heyo!';
3+
document.body.appendChild(newParagraph);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const special = document.createElement('p');
2+
special.textContent = 'Also here!';
3+
document.body.appendChild(special);

0 commit comments

Comments
 (0)