fix crash when page can't be imported

This commit is contained in:
Aran-Fey
2024-09-19 13:36:31 +02:00
parent 2c82525964
commit e99564a07c
8 changed files with 92 additions and 56 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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"

View File

@@ -44,7 +44,6 @@ def warn(
"asyncio",
"threading",
):
print(frame.globals["__name__"])
break
else:
stacklevel = 0

View File

@@ -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
),
)

View File

@@ -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]()

View File

@@ -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: