Skip to content

Commit 54101ac

Browse files
authored
Allow specifying multiple static assets (#763)
1 parent 6225a0e commit 54101ac

File tree

3 files changed

+55
-16
lines changed

3 files changed

+55
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
* Added `shiny.experimental.ui.toggle_switch()` (#680).
1818
* Added CSS classes to UI input methods (#680) .
1919
* `Session` objects can now accept an asynchronous (or synchronous) function for `.on_flush(fn=)`, `.on_flushed(fn=)`, and `.on_ended(fn=)` (#686).
20+
* `App()` now allows `static_assets` to represent multiple paths. To do this, pass in a dictionary instead of a string (#763).
2021

2122
### API changes
2223

shiny/_app.py

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727
from ._connection import Connection, StarletteConnection
2828
from ._error import ErrorMiddleware
2929
from ._shinyenv import is_pyodide
30-
from ._utils import is_async_callable
30+
from ._utils import guess_mime_type, is_async_callable
3131
from .html_dependencies import jquery_deps, require_deps, shiny_deps
32-
from .http_staticfiles import StaticFiles
32+
from .http_staticfiles import FileResponse, StaticFiles
3333
from .session import Inputs, Outputs, Session, session_context
3434

3535
# Default values for App options.
@@ -54,7 +54,10 @@ class App:
5454
A function which is called once for each session, ensuring that each app is
5555
independent.
5656
static_assets
57-
An absolute directory containing static files to be served by the app.
57+
Static files to be served by the app. If this is a string or Path object, it
58+
must be a directory, and it will be mounted at `/`. If this is a dictionary,
59+
each key is a mount point and each value is a file or directory to be served at
60+
that mount point.
5861
debug
5962
Whether to enable debug mode.
6063
@@ -100,7 +103,7 @@ def __init__(
100103
ui: Tag | TagList | Callable[[Request], Tag | TagList] | Path,
101104
server: Optional[Callable[[Inputs, Outputs, Session], None]],
102105
*,
103-
static_assets: Optional["str" | "os.PathLike[str]"] = None,
106+
static_assets: Optional["str" | "os.PathLike[str]" | dict[str, Path]] = None,
104107
debug: bool = False,
105108
) -> None:
106109
if server is None:
@@ -119,15 +122,16 @@ def _server(inputs: Inputs, outputs: Outputs, session: Session):
119122
self.sanitize_errors: bool = SANITIZE_ERRORS
120123
self.sanitize_error_msg: str = SANITIZE_ERROR_MSG
121124

122-
if static_assets is not None:
123-
if not os.path.isdir(static_assets):
124-
raise ValueError(f"static_assets must be a directory: {static_assets}")
125+
if static_assets is None:
126+
static_assets = {}
127+
if isinstance(static_assets, (str, os.PathLike)):
125128
if not os.path.isabs(static_assets):
126129
raise ValueError(
127130
f"static_assets must be an absolute path: {static_assets}"
128131
)
132+
static_assets = {"/": Path(static_assets)}
129133

130-
self._static_assets: str | os.PathLike[str] | None = static_assets
134+
self._static_assets: dict[str, Path] = static_assets
131135

132136
self._sessions: dict[str, Session] = {}
133137

@@ -136,13 +140,9 @@ def _server(inputs: Inputs, outputs: Outputs, session: Session):
136140
self._registered_dependencies: dict[str, HTMLDependency] = {}
137141
self._dependency_handler = starlette.routing.Router()
138142

139-
if self._static_assets is not None:
143+
for mount_point, static_asset_path in self._static_assets.items():
140144
self._dependency_handler.routes.append(
141-
starlette.routing.Mount(
142-
"/",
143-
StaticFiles(directory=self._static_assets),
144-
name="shiny-app-static-assets-directory",
145-
)
145+
create_static_asset_route(mount_point, static_asset_path)
146146
)
147147

148148
starlette_app = self.init_starlette_app()
@@ -414,3 +414,35 @@ def is_uifunc(x: Path | Tag | TagList | Callable[[Request], Tag | TagList]):
414414

415415
def html_dep_name(dep: HTMLDependency) -> str:
416416
return dep.name + "-" + str(dep.version)
417+
418+
419+
def create_static_asset_route(
420+
mount_point: str, static_asset_path: Path
421+
) -> starlette.routing.BaseRoute:
422+
"""
423+
Create a Starlette route for serving static assets.
424+
425+
Parameters
426+
----------
427+
mount_point
428+
The mount point where the static assets will be served.
429+
static_asset_path
430+
The path on disk to the static assets.
431+
"""
432+
if static_asset_path.is_dir():
433+
return starlette.routing.Mount(
434+
mount_point,
435+
StaticFiles(directory=static_asset_path),
436+
name="shiny-app-static-assets-" + mount_point,
437+
)
438+
else:
439+
mime_type = guess_mime_type(static_asset_path, strict=False)
440+
441+
def file_response_handler(req: Request) -> FileResponse:
442+
return FileResponse(static_asset_path, media_type=mime_type)
443+
444+
return starlette.routing.Route(
445+
mount_point,
446+
file_response_handler,
447+
name="shiny-app-static-assets-" + mount_point,
448+
)

shiny/quarto.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ def convert_code_cells_to_app_py(json_file: str | Path, app_file: str | Path) ->
6666

6767
app_content = f"""# This file generated by Quarto; do not edit by hand.
6868
69+
from __future__ import annotations
70+
6971
from pathlib import Path
7072
from shiny import App, Inputs, Outputs, Session, ui
7173
@@ -75,12 +77,16 @@ def convert_code_cells_to_app_py(json_file: str | Path, app_file: str | Path) ->
7577
def server(input: Inputs, output: Outputs, session: Session) -> None:
7678
{ "".join(session_code_cell_texts) }
7779
80+
81+
_static_assets = ##STATIC_ASSETS_PLACEHOLDER##
82+
_static_assets = {{"/" + sa: Path(__file__).parent / sa for sa in _static_assets}}
83+
7884
app = App(
7985
Path(__file__).parent / "{ data["html_file"] }",
8086
server,
81-
static_assets=Path(__file__).parent,
87+
static_assets=_static_assets,
8288
)
83-
"""
89+
"""
8490

8591
with open(app_file, "w") as f:
8692
f.write(app_content)

0 commit comments

Comments
 (0)