Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Added `shiny.experimental.ui.toggle_switch()` (#680).
* Added CSS classes to UI input methods (#680) .
* `Session` objects can now accept an asynchronous (or synchronous) function for `.on_flush(fn=)`, `.on_flushed(fn=)`, and `.on_ended(fn=)` (#686).
* `App()` now allows `static_assets` to represent multiple paths. To do this, pass in a dictionary instead of a string (#763).

### API changes

Expand Down
60 changes: 46 additions & 14 deletions shiny/_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
from ._connection import Connection, StarletteConnection
from ._error import ErrorMiddleware
from ._shinyenv import is_pyodide
from ._utils import is_async_callable
from ._utils import guess_mime_type, is_async_callable
from .html_dependencies import jquery_deps, require_deps, shiny_deps
from .http_staticfiles import StaticFiles
from .http_staticfiles import FileResponse, StaticFiles
from .session import Inputs, Outputs, Session, session_context

# Default values for App options.
Expand All @@ -54,7 +54,10 @@ class App:
A function which is called once for each session, ensuring that each app is
independent.
static_assets
An absolute directory containing static files to be served by the app.
Static files to be served by the app. If this is a string or Path object, it
must be a directory, and it will be mounted at `/`. If this is a dictionary,
each key is a mount point and each value is a file or directory to be served at
that mount point.
debug
Whether to enable debug mode.

Expand Down Expand Up @@ -100,7 +103,7 @@ def __init__(
ui: Tag | TagList | Callable[[Request], Tag | TagList] | Path,
server: Optional[Callable[[Inputs, Outputs, Session], None]],
*,
static_assets: Optional["str" | "os.PathLike[str]"] = None,
static_assets: Optional["str" | "os.PathLike[str]" | dict[str, Path]] = None,
debug: bool = False,
) -> None:
if server is None:
Expand All @@ -119,15 +122,16 @@ def _server(inputs: Inputs, outputs: Outputs, session: Session):
self.sanitize_errors: bool = SANITIZE_ERRORS
self.sanitize_error_msg: str = SANITIZE_ERROR_MSG

if static_assets is not None:
if not os.path.isdir(static_assets):
raise ValueError(f"static_assets must be a directory: {static_assets}")
if static_assets is None:
static_assets = {}
if isinstance(static_assets, (str, os.PathLike)):
if not os.path.isabs(static_assets):
raise ValueError(
f"static_assets must be an absolute path: {static_assets}"
)
static_assets = {"/": Path(static_assets)}

self._static_assets: str | os.PathLike[str] | None = static_assets
self._static_assets: dict[str, Path] = static_assets

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

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

if self._static_assets is not None:
for mount_point, static_asset_path in self._static_assets.items():
self._dependency_handler.routes.append(
starlette.routing.Mount(
"/",
StaticFiles(directory=self._static_assets),
name="shiny-app-static-assets-directory",
)
create_static_asset_route(mount_point, static_asset_path)
)

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

def html_dep_name(dep: HTMLDependency) -> str:
return dep.name + "-" + str(dep.version)


def create_static_asset_route(
mount_point: str, static_asset_path: Path
) -> starlette.routing.BaseRoute:
"""
Create a Starlette route for serving static assets.

Parameters
----------
mount_point
The mount point where the static assets will be served.
static_asset_path
The path on disk to the static assets.
"""
if static_asset_path.is_dir():
return starlette.routing.Mount(
mount_point,
StaticFiles(directory=static_asset_path),
name="shiny-app-static-assets-" + mount_point,
)
else:
mime_type = guess_mime_type(static_asset_path, strict=False)

def file_response_handler(req: Request) -> FileResponse:
return FileResponse(static_asset_path, media_type=mime_type)

return starlette.routing.Route(
mount_point,
file_response_handler,
name="shiny-app-static-assets-" + mount_point,
)
10 changes: 8 additions & 2 deletions shiny/quarto.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ def convert_code_cells_to_app_py(json_file: str | Path, app_file: str | Path) ->

app_content = f"""# This file generated by Quarto; do not edit by hand.

from __future__ import annotations

from pathlib import Path
from shiny import App, Inputs, Outputs, Session, ui

Expand All @@ -75,12 +77,16 @@ def convert_code_cells_to_app_py(json_file: str | Path, app_file: str | Path) ->
def server(input: Inputs, output: Outputs, session: Session) -> None:
{ "".join(session_code_cell_texts) }


_static_assets = ##STATIC_ASSETS_PLACEHOLDER##
_static_assets = {{"/" + sa: Path(__file__).parent / sa for sa in _static_assets}}

app = App(
Path(__file__).parent / "{ data["html_file"] }",
server,
static_assets=Path(__file__).parent,
static_assets=_static_assets,
)
"""
"""

with open(app_file, "w") as f:
f.write(app_content)
Expand Down