Skip to content

Commit 6a28a59

Browse files
authored
Better messages for AttributeError in Shiny Express (#888)
1 parent bac1c48 commit 6a28a59

File tree

1 file changed

+53
-40
lines changed

1 file changed

+53
-40
lines changed

shiny/express/_run.py

Lines changed: 53 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -72,51 +72,64 @@ def set_result(x: object):
7272
nonlocal ui_result
7373
ui_result = cast(Tag, x)
7474

75+
prev_displayhook = sys.displayhook
7576
sys.displayhook = set_result
7677

77-
reset_top_level_recall_context_manager()
78-
get_top_level_recall_context_manager().__enter__()
79-
80-
file_path = str(file.resolve())
78+
try:
79+
reset_top_level_recall_context_manager()
80+
get_top_level_recall_context_manager().__enter__()
81+
82+
file_path = str(file.resolve())
83+
84+
var_context: dict[str, object] = {
85+
"__file__": file_path,
86+
display_decorator_func_name: _display_decorator_function_def,
87+
}
88+
89+
# Execute each top-level node in the AST
90+
for node in tree.body:
91+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
92+
exec(
93+
compile(ast.Module([node], type_ignores=[]), file_path, "exec"),
94+
var_context,
95+
var_context,
96+
)
97+
else:
98+
exec(
99+
compile(
100+
ast.Interactive([node], type_ignores=[]), file_path, "single"
101+
),
102+
var_context,
103+
var_context,
104+
)
105+
106+
# When we called the function to get the top level recall context manager, we didn't
107+
# store the result in a variable and re-use that variable here. That is intentional,
108+
# because during the evaluation of the app code,
109+
# replace_top_level_recall_context_manager() may have been called, which swaps
110+
# out the context manager, and it's the new one that we need to exit here.
111+
get_top_level_recall_context_manager().__exit__(None, None, None)
112+
113+
# If we're running as an Express app but there's also a top-level item named app
114+
# which is a shiny.App object, the user probably made a mistake.
115+
if "app" in var_context and isinstance(var_context["app"], App):
116+
raise RuntimeError(
117+
"This looks like a Shiny Express app because it imports shiny.express, "
118+
"but it also looks like a Shiny Classic app because it has a variable named "
119+
"`app` which is a shiny.App object. Remove either the shiny.express import, "
120+
"or the app=App()."
121+
)
81122

82-
var_context: dict[str, object] = {
83-
"__file__": file_path,
84-
display_decorator_func_name: _display_decorator_function_def,
85-
}
123+
return ui_result
86124

87-
# Execute each top-level node in the AST
88-
for node in tree.body:
89-
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
90-
exec(
91-
compile(ast.Module([node], type_ignores=[]), file_path, "exec"),
92-
var_context,
93-
var_context,
94-
)
95-
else:
96-
exec(
97-
compile(ast.Interactive([node], type_ignores=[]), file_path, "single"),
98-
var_context,
99-
var_context,
100-
)
125+
except AttributeError as e:
126+
# Need to catch AttributeError and convert to a different type of error, because
127+
# uvicorn specifically catches AttributeErrors and prints an error message that
128+
# is helpful for normal ASGI apps, but misleading in the case of Shiny Express.
129+
raise RuntimeError(e) from e
101130

102-
# When we called the function to get the top level recall context manager, we didn't
103-
# store the result in a variable and re-use that variable here. That is intentional,
104-
# because during the evaluation of the app code,
105-
# replace_top_level_recall_context_manager() may have been called, which swaps
106-
# out the context manager, and it's the new one that we need to exit here.
107-
get_top_level_recall_context_manager().__exit__(None, None, None)
108-
109-
# If we're running as an Express app but there's also a top-level item named app
110-
# which is a shiny.App object, the user probably made a mistake.
111-
if "app" in var_context and isinstance(var_context["app"], App):
112-
raise RuntimeError(
113-
"This looks like a Shiny Express app because it imports shiny.express, "
114-
"but it also looks like a Shiny Classic app because it has a variable named "
115-
"`app` which is a shiny.App object. Remove either the shiny.express import, "
116-
"or the app=App()."
117-
)
118-
119-
return ui_result
131+
finally:
132+
sys.displayhook = prev_displayhook
120133

121134

122135
_top_level_recall_context_manager: RecallContextManager[Tag]

0 commit comments

Comments
 (0)