mirror of
https://github.com/rio-labs/rio.git
synced 2026-01-23 14:00:59 -06:00
fix crash when page can't be imported
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { AspectRatioContainerComponent } from './components/aspectRatioContainer';
|
||||
import { BuildFailedComponent } from './components/buildFailed';
|
||||
import { ErrorPlaceholderComponent } from './components/errorPlaceholder';
|
||||
import { ButtonComponent, IconButtonComponent } from './components/buttons';
|
||||
import { CalendarComponent } from './components/calendar';
|
||||
import { callRemoteMethodDiscardResponse } from './rpc';
|
||||
@@ -65,7 +65,7 @@ import { TooltipComponent } from './components/tooltip';
|
||||
|
||||
const COMPONENT_CLASSES = {
|
||||
'AspectRatioContainer-builtin': AspectRatioContainerComponent,
|
||||
'BuildFailed-builtin': BuildFailedComponent,
|
||||
'ErrorPlaceholder-builtin': ErrorPlaceholderComponent,
|
||||
'Button-builtin': ButtonComponent,
|
||||
'Calendar-builtin': CalendarComponent,
|
||||
'Card-builtin': CardComponent,
|
||||
|
||||
@@ -7,7 +7,7 @@ export type BuildFailedState = ComponentState & {
|
||||
error_details: string;
|
||||
};
|
||||
|
||||
export class BuildFailedComponent extends ComponentBase {
|
||||
export class ErrorPlaceholderComponent extends ComponentBase {
|
||||
state: Required<BuildFailedState>;
|
||||
|
||||
private iconElement: HTMLElement;
|
||||
@@ -17,31 +17,31 @@ export class BuildFailedComponent extends ComponentBase {
|
||||
createElement(): HTMLElement {
|
||||
// Create the elements
|
||||
let element = document.createElement('div');
|
||||
element.classList.add('rio-build-failed');
|
||||
element.classList.add('rio-error-placeholder');
|
||||
|
||||
element.innerHTML = `
|
||||
<div class="rio-build-failed-top"></div>
|
||||
<div class="rio-build-failed-content">
|
||||
<div class="rio-build-failed-header">
|
||||
<div class="rio-build-failed-icon"></div>
|
||||
<div class="rio-build-failed-summary"></div>
|
||||
<div class="rio-error-placeholder-top"></div>
|
||||
<div class="rio-error-placeholder-content">
|
||||
<div class="rio-error-placeholder-header">
|
||||
<div class="rio-error-placeholder-icon"></div>
|
||||
<div class="rio-error-placeholder-summary"></div>
|
||||
</div>
|
||||
<div class="rio-build-failed-details"></div>
|
||||
<div class="rio-error-placeholder-details"></div>
|
||||
</div>
|
||||
<div class="rio-build-failed-bottom"></div>
|
||||
<div class="rio-error-placeholder-bottom"></div>
|
||||
`;
|
||||
|
||||
// Expose them
|
||||
this.iconElement = element.querySelector(
|
||||
'.rio-build-failed-icon'
|
||||
'.rio-error-placeholder-icon'
|
||||
) as HTMLElement;
|
||||
|
||||
this.summaryElement = element.querySelector(
|
||||
'.rio-build-failed-summary'
|
||||
'.rio-error-placeholder-summary'
|
||||
) as HTMLElement;
|
||||
|
||||
this.detailsElement = element.querySelector(
|
||||
'.rio-build-failed-details'
|
||||
'.rio-error-placeholder-details'
|
||||
) as HTMLElement;
|
||||
|
||||
// And initialize them
|
||||
@@ -3266,7 +3266,7 @@ html.picking-component * {
|
||||
}
|
||||
|
||||
// Build failed component
|
||||
.rio-build-failed {
|
||||
.rio-error-placeholder {
|
||||
pointer-events: auto;
|
||||
color: var(--rio-global-danger-fg);
|
||||
|
||||
@@ -3277,7 +3277,7 @@ html.picking-component * {
|
||||
background: var(--rio-global-danger-bg);
|
||||
border-radius: var(--rio-global-corner-radius-small);
|
||||
|
||||
// `rio-build-failed-content` can't have a corner radius set, because that
|
||||
// `rio-error-placeholder-content` can't have a corner radius set, because that
|
||||
// would make the barber pole peek through the corners. Instead enforce
|
||||
// the corner radius from this element.
|
||||
overflow: hidden;
|
||||
@@ -3285,12 +3285,12 @@ html.picking-component * {
|
||||
@include barber-pole(var(--rio-global-danger-bg-variant));
|
||||
}
|
||||
|
||||
.rio-build-failed-top,
|
||||
.rio-build-failed-bottom {
|
||||
.rio-error-placeholder-top,
|
||||
.rio-error-placeholder-bottom {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.rio-build-failed-top {
|
||||
.rio-error-placeholder-top {
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
var(--rio-global-danger-bg),
|
||||
@@ -3299,7 +3299,7 @@ html.picking-component * {
|
||||
);
|
||||
}
|
||||
|
||||
.rio-build-failed-bottom {
|
||||
.rio-error-placeholder-bottom {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--rio-global-danger-bg),
|
||||
@@ -3308,7 +3308,7 @@ html.picking-component * {
|
||||
);
|
||||
}
|
||||
|
||||
.rio-build-failed-content {
|
||||
.rio-error-placeholder-content {
|
||||
padding: 1rem;
|
||||
|
||||
display: flex;
|
||||
@@ -3319,7 +3319,7 @@ html.picking-component * {
|
||||
background: var(--rio-global-danger-bg);
|
||||
}
|
||||
|
||||
.rio-build-failed-header {
|
||||
.rio-error-placeholder-header {
|
||||
align-self: center;
|
||||
|
||||
display: flex;
|
||||
@@ -3327,16 +3327,16 @@ html.picking-component * {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.rio-build-failed-icon {
|
||||
.rio-error-placeholder-icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.rio-build-failed-summary {
|
||||
.rio-error-placeholder-summary {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.rio-build-failed-details {
|
||||
.rio-error-placeholder-details {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,13 +2,16 @@ from uniserde import JsonDoc
|
||||
|
||||
from .fundamental_component import FundamentalComponent
|
||||
|
||||
__all__ = ["BuildFailed"]
|
||||
__all__ = ["ErrorPlaceholder"]
|
||||
|
||||
|
||||
class BuildFailed(FundamentalComponent):
|
||||
class ErrorPlaceholder(FundamentalComponent):
|
||||
"""
|
||||
Used as a placeholder in case a component's `build` function throws an
|
||||
exception.
|
||||
Used as a placeholder in case the real component isn't available for
|
||||
whatever reason. For example:
|
||||
|
||||
- When a `build` function throws an error
|
||||
- When a page can't be imported
|
||||
|
||||
|
||||
## Metadata
|
||||
@@ -34,4 +37,4 @@ class BuildFailed(FundamentalComponent):
|
||||
}
|
||||
|
||||
|
||||
BuildFailed._unique_id_ = "BuildFailed-builtin"
|
||||
ErrorPlaceholder._unique_id_ = "ErrorPlaceholder-builtin"
|
||||
@@ -44,7 +44,6 @@ def warn(
|
||||
"asyncio",
|
||||
"threading",
|
||||
):
|
||||
print(frame.globals["__name__"])
|
||||
break
|
||||
else:
|
||||
stacklevel = 0
|
||||
|
||||
@@ -437,33 +437,67 @@ def _auto_detect_pages_iter(
|
||||
if not utils.is_python_script(file_path):
|
||||
continue
|
||||
|
||||
module_name = file_path.stem
|
||||
if package is not None:
|
||||
module_name = package + "." + module_name
|
||||
yield _page_from_python_file(file_path, package)
|
||||
|
||||
|
||||
def _page_from_python_file(
|
||||
file_path: Path, package: str | None
|
||||
) -> ComponentPage:
|
||||
module_name = file_path.stem
|
||||
if package is not None:
|
||||
module_name = package + "." + module_name
|
||||
|
||||
try:
|
||||
module = utils.load_module_from_path(file_path, module_name=module_name)
|
||||
|
||||
except BaseException as error:
|
||||
# Can't import the module? Display a warning and a placeholder component
|
||||
warnings.warn(
|
||||
f"Failed to import file '{file_path}': {type(error)} {error}"
|
||||
)
|
||||
page = _error_page_from_file_name(
|
||||
file_path,
|
||||
error_summary=f"Failed to import '{file_path}'",
|
||||
error_details=f"{type(error)}: {error}",
|
||||
)
|
||||
else:
|
||||
# Search the module for the callable decorated with `@rio.page`
|
||||
for obj in vars(module).values():
|
||||
if not callable(obj):
|
||||
continue
|
||||
|
||||
try:
|
||||
page = BUILD_FUNCTIONS_FOR_PAGES[obj]
|
||||
break
|
||||
except (TypeError, KeyError):
|
||||
continue
|
||||
|
||||
sub_pages = t.cast(list, page.children)
|
||||
sub_pages.clear() # Avoid duplicate entries if this function is called twice
|
||||
sub_pages += auto_detect_pages(
|
||||
file_path.with_suffix(""),
|
||||
package=module_name,
|
||||
)
|
||||
|
||||
yield page
|
||||
break
|
||||
else:
|
||||
# Nothing found? Display a warning and a placeholder component
|
||||
warnings.warn(
|
||||
f"The file {file_path} doesn't seem to contain a page"
|
||||
f" definition. Did you forget to decorate your component/build"
|
||||
f" function with `@rio.page(...)`?"
|
||||
)
|
||||
page = _error_page_from_file_name(
|
||||
file_path,
|
||||
error_summary=f"No page found in '{file_path}'",
|
||||
error_details=f"No component in this file was decorated with `@rio.page(...)`",
|
||||
)
|
||||
|
||||
# Add sub-pages, if any
|
||||
sub_pages = t.cast(list, page.children)
|
||||
sub_pages.clear() # Avoid duplicate entries if this function is called twice
|
||||
sub_pages += auto_detect_pages(
|
||||
file_path.with_suffix(""),
|
||||
package=module_name,
|
||||
)
|
||||
|
||||
return page
|
||||
|
||||
|
||||
def _error_page_from_file_name(
|
||||
file_path: Path, error_summary: str, error_details: str
|
||||
) -> ComponentPage:
|
||||
return ComponentPage(
|
||||
name=convert_case(file_path.stem, "snake").replace("_", " ").title(),
|
||||
url_segment=convert_case(file_path.stem, "kebab").lower(),
|
||||
build=lambda: rio.components.error_placeholder.ErrorPlaceholder(
|
||||
error_summary, error_details
|
||||
),
|
||||
)
|
||||
|
||||
@@ -265,7 +265,7 @@ class Session(unicall.Unicall):
|
||||
# were last saved.
|
||||
self._last_settings_save_time: float = -float("inf")
|
||||
|
||||
# A dict of {build_function: BuildFailedComponent}. This is cleared at
|
||||
# A dict of {build_function: error_message}. This is cleared at
|
||||
# the start of every refresh, and tracks which build functions failed.
|
||||
# Used for unit testing.
|
||||
self._crashed_build_functions = dict[Callable, str]()
|
||||
|
||||
14
rio/utils.py
14
rio/utils.py
@@ -347,9 +347,9 @@ def safe_build(build_function: Callable[[], rio.Component]) -> rio.Component:
|
||||
)
|
||||
|
||||
# Screw circular imports
|
||||
from rio.components.build_failed import BuildFailed
|
||||
from rio.components.error_placeholder import ErrorPlaceholder
|
||||
|
||||
build_failed_component = BuildFailed(
|
||||
placeholder_component = ErrorPlaceholder(
|
||||
f"`{build_function_repr}` has crashed", repr(err)
|
||||
)
|
||||
else:
|
||||
@@ -366,19 +366,19 @@ def safe_build(build_function: Callable[[], rio.Component]) -> rio.Component:
|
||||
)
|
||||
|
||||
# Screw circular imports
|
||||
from rio.components.build_failed import BuildFailed
|
||||
from rio.components.error_placeholder import ErrorPlaceholder
|
||||
|
||||
build_failed_component = BuildFailed(
|
||||
placeholder_component = ErrorPlaceholder(
|
||||
f"`{build_function_repr}` has returned an invalid result",
|
||||
f"Build functions must return instances of `rio.Component`, but the result was {build_result!r}",
|
||||
)
|
||||
|
||||
# Save the error in the session, for testing purposes
|
||||
build_failed_component.session._crashed_build_functions[build_function] = (
|
||||
build_failed_component.error_details
|
||||
placeholder_component.session._crashed_build_functions[build_function] = (
|
||||
placeholder_component.error_details
|
||||
)
|
||||
|
||||
return build_failed_component
|
||||
return placeholder_component
|
||||
|
||||
|
||||
def normalize_url(url: rio.URL) -> rio.URL:
|
||||
|
||||
Reference in New Issue
Block a user