-
Notifications
You must be signed in to change notification settings - Fork 114
Add support for express mode apps #767
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
I don't feel comfortable enough with the def no_default_ui(output_renderer: OutputRenderer[OT]) -> OutputRenderer[OT]:
output_renderer.default_ui = lambda x: ""
return output_renderer Bool-on-OutputRenderer route is equally easy. I put it in |
} else { | ||
// This is a later run. Reset attributes to their inital state. | ||
for (const attr of this.originalBodyTagAttrs) { | ||
el.setAttribute(attr.name, attr.value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to also clear added attributes
} | ||
} | ||
|
||
let content = typeof data === "string" ? data : data.html; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
data will never be a string
|
||
// Parse the HTML | ||
const parser = new DOMParser(); | ||
const doc = parser.parseFromString(content, "text/html"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be a little slow but whatever
app: App | ||
|
||
|
||
def __getattr__(name: str): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overkill?
"output_args", | ||
"suspend_display", | ||
"wrap_express_app", | ||
"app", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems private
return old_cm | ||
|
||
|
||
class DetectShinyExpressVisitor(ast.NodeVisitor): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider rejecting if shiny.App() call is found
return app | ||
|
||
|
||
def is_express_app(app: str, app_dir: str | None) -> bool: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe have a way out as well--environment variable or something to force one mode or another.
# returns the input for the current session. This will work in the vast majority of | ||
# cases, but when it fails, it will be very confusing. | ||
def __getattr__(name: str) -> object: | ||
if name == "input": |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider detecting if input, output, or session are requested more than once from the same scope. (using inspect
?)
shiny/express/_run.py
Outdated
app_ui = ui.page_output("__page__") | ||
|
||
def express_server(input: Inputs, output: Outputs, session: Session): | ||
try: | ||
dyn_ui = run_express(file) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
app_ui = ui.page_output("__page__") | |
def express_server(input: Inputs, output: Outputs, session: Session): | |
try: | |
dyn_ui = run_express(file) | |
app_ui = run_express(file) | |
def express_server(input: Inputs, output: Outputs, session: Session): | |
try: | |
run_express(file) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reasons for suggesting this (during live discussion):
- HTML dependencies load with the page load which leads to smoother rendering (less fouc)
Downside:
- Process startup is slightly slower
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, also would need to get rid of render.ui below
shiny/express/_run.py
Outdated
file = Path(os.getcwd()) / app_file | ||
|
||
# TODO: title and lang | ||
app_ui = ui.page_output("__page__") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whatever we do, need to catch errors that happen during UI rendering/app.py loading and have the stack trace appear in the console.
sys.displayhook = set_result | ||
|
||
reset_top_level_recall_context_manager() | ||
get_top_level_recall_context_manager().__enter__() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should use with
nonlocal ui_result | ||
ui_result = cast(Tag, x) | ||
|
||
sys.displayhook = set_result |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably this too
reset_top_level_recall_context_manager() | ||
get_top_level_recall_context_manager().__enter__() | ||
|
||
file_path = str(file.resolve()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe require this to be absolute rather than resolving it here? Or not, I don't know.
for node in tree.body: | ||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): | ||
exec( | ||
compile(ast.Module([node], type_ignores=[]), file_path, "exec"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AST loading, transformation, and compiling could be a separate step that we only do once. Then separate step for running each code block, which we do every time.
|
||
|
||
_top_level_recall_context_manager: RecallContextManager[Tag] | ||
_top_level_recall_context_manager_has_been_replaced = False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you use a with layout.foo()
function in the wrong setting, you get an unhelpful error message
return ui_result | ||
|
||
|
||
_top_level_recall_context_manager: RecallContextManager[Tag] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider making all this state/logic an object/methods
if value is not None: | ||
self.args.append(tags.pre(repr(value))) | ||
|
||
def __enter__(self) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The global context manager object could accumulate the results, and then only decide to create the page at the end.
|
||
self._prev_displayhook = sys.displayhook | ||
# Collect each of the "printed" values in the args list. | ||
sys.displayhook = self.append_arg |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should consider just replacing sys.displayhook
once, at process start time, with a smart version of our own. And then have that smart version look at a different variable, backed by a contextvar, to decide where things actually go. This would let us keep "displayhooks" straight even when using async logic.
Q: Would this work in Quarto? In JupyterLab?
* main: Update shinylive to v0.1.1 (#825) Deploy dev docs on GitHub Pages (#824) Add docstrings Check for 'app' object in Express mode Use path to app as part of entrypoint for Express apps (#816) Install htmltools from github in CI (#811) Change htmltools dependency to use version number For layout.sidebar, default to open="always" Fix CSS class name Make page_sidebar main area white Update VS Code settings For quarto apps, check for `import *` (#810) Restore workaround for type stubs from typing_extensions Add try-except so parsing doesn't result in error Move is_express_app to separate file Add support for express mode apps (#767) Un-pin pyright version and fix type issues (#800) chore: Penguin app updates (#798) bug(`accordion(multiple=)`): Pass in accordion ID into accordion panel objects for `multiple` functionality (#799)
To install:
Express apps can be run with:
You can also run it with an ASGI server like
uvicorn
, like so:This works because
shiny.express.app:app
is an ASGI application, and so ASGI servers likeuvicorn
will be able to use it. However, it is part of the shiny package and can't be user-modified, so the the way you tell it which file to use for the flat app source is by settingSHINY_EXPRESS_APP_FILE
.