1
1
from __future__ import annotations
2
2
3
3
import ast
4
+ import os
4
5
import sys
5
6
from pathlib import Path
6
7
from typing import cast
9
10
10
11
from .._app import App
11
12
from .._docstring import no_example
12
- from ..session import Inputs , Outputs , Session , session_context
13
- from ._mock_session import MockSession
13
+ from .._typing_extensions import NotRequired , TypedDict
14
+ from ..session import Inputs , Outputs , Session , get_current_session , session_context
15
+ from ..types import MISSING , MISSING_TYPE
16
+ from ._mock_session import ExpressMockSession
14
17
from ._recall_context import RecallContextManager
15
18
from .expressify_decorator ._func_displayhook import _expressify_decorator_function_def
16
19
from .expressify_decorator ._node_transformers import (
17
20
DisplayFuncsTransformer ,
18
21
expressify_decorator_func_name ,
19
22
)
20
23
21
- __all__ = ("wrap_express_app" ,)
24
+ __all__ = (
25
+ "app_opts" ,
26
+ "wrap_express_app" ,
27
+ )
22
28
23
29
24
30
@no_example ()
@@ -35,8 +41,10 @@ def wrap_express_app(file: Path) -> App:
35
41
:
36
42
A :class:`shiny.App` object.
37
43
"""
44
+
38
45
try :
39
- with session_context (cast (Session , MockSession ())):
46
+ mock_session = ExpressMockSession ()
47
+ with session_context (cast (Session , mock_session )):
40
48
# We tagify here, instead of waiting for the App object to do it when it wraps
41
49
# the UI in a HTMLDocument and calls render() on it. This is because
42
50
# AttributeErrors can be thrown during the tagification process, and we need to
@@ -59,7 +67,20 @@ def express_server(input: Inputs, output: Outputs, session: Session):
59
67
traceback .print_exception (* sys .exc_info ())
60
68
raise
61
69
62
- app = App (app_ui , express_server )
70
+ app_opts : AppOpts = {}
71
+
72
+ www_dir = file .parent / "www"
73
+ if www_dir .is_dir ():
74
+ app_opts ["static_assets" ] = {"/" : www_dir }
75
+
76
+ app_opts = _merge_app_opts (app_opts , mock_session .app_opts )
77
+ app_opts = _normalize_app_opts (app_opts , file .parent )
78
+
79
+ app = App (
80
+ app_ui ,
81
+ express_server ,
82
+ ** app_opts ,
83
+ )
63
84
64
85
return app
65
86
@@ -164,3 +185,86 @@ def __getattr__(self, name: str):
164
185
"Tried to access `input`, but it was not imported. "
165
186
"Perhaps you need `from shiny.express import input`?"
166
187
)
188
+
189
+
190
+ class AppOpts (TypedDict ):
191
+ static_assets : NotRequired [dict [str , Path ]]
192
+ debug : NotRequired [bool ]
193
+
194
+
195
+ @no_example ()
196
+ def app_opts (
197
+ static_assets : (
198
+ str | os .PathLike [str ] | dict [str , str | Path ] | MISSING_TYPE
199
+ ) = MISSING ,
200
+ debug : bool | MISSING_TYPE = MISSING ,
201
+ ):
202
+ """
203
+ Set App-level options in Shiny Express
204
+
205
+ This function sets application-level options for Shiny Express. These options are
206
+ the same as those from the :class:`shiny.App` constructor.
207
+
208
+ Parameters
209
+ ----------
210
+ static_assets
211
+ Static files to be served by the app. If this is a string or Path object, it
212
+ must be a directory, and it will be mounted at `/`. If this is a dictionary,
213
+ each key is a mount point and each value is a file or directory to be served at
214
+ that mount point. In Shiny Express, if there is a `www` subdirectory of the
215
+ directory containing the app file, it will automatically be mounted at `/`, even
216
+ without needing to set the option here.
217
+ debug
218
+ Whether to enable debug mode.
219
+ """
220
+
221
+ # Store these options only if we're in the UI-rendering phase of Shiny Express.
222
+ mock_session = get_current_session ()
223
+ if not isinstance (mock_session , ExpressMockSession ):
224
+ return
225
+
226
+ if not isinstance (static_assets , MISSING_TYPE ):
227
+ if isinstance (static_assets , (str , os .PathLike )):
228
+ static_assets = {"/" : Path (static_assets )}
229
+
230
+ # Convert string values to Paths. (Need new var name to help type checker.)
231
+ static_assets_paths = {k : Path (v ) for k , v in static_assets .items ()}
232
+
233
+ mock_session .app_opts ["static_assets" ] = static_assets_paths
234
+
235
+ if not isinstance (debug , MISSING_TYPE ):
236
+ mock_session .app_opts ["debug" ] = debug
237
+
238
+
239
+ def _merge_app_opts (app_opts : AppOpts , app_opts_new : AppOpts ) -> AppOpts :
240
+ """
241
+ Merge a set of app options into an existing set of app options. The values from
242
+ `app_opts_new` take precedence. This will alter the original app_opts and return it.
243
+ """
244
+
245
+ # We can't just do a `app_opts.update(app_opts_new)` because we need to handle the
246
+ # case where app_opts["static_assets"] and app_opts_new["static_assets"] are
247
+ # dictionaries, and we need to merge those dictionaries.
248
+ if "static_assets" in app_opts and "static_assets" in app_opts_new :
249
+ app_opts ["static_assets" ].update (app_opts_new ["static_assets" ])
250
+ elif "static_assets" in app_opts_new :
251
+ app_opts ["static_assets" ] = app_opts_new ["static_assets" ].copy ()
252
+
253
+ if "debug" in app_opts_new :
254
+ app_opts ["debug" ] = app_opts_new ["debug" ]
255
+
256
+ return app_opts
257
+
258
+
259
+ def _normalize_app_opts (app_opts : AppOpts , parent_dir : Path ) -> AppOpts :
260
+ """
261
+ Normalize the app options, ensuring that all paths in static_assets are absolute.
262
+ Modifies the original in place.
263
+ """
264
+ if "static_assets" in app_opts :
265
+ for mount_point , path in app_opts ["static_assets" ].items ():
266
+ if not path .is_absolute ():
267
+ path = parent_dir / path
268
+ app_opts ["static_assets" ][mount_point ] = path
269
+
270
+ return app_opts
0 commit comments