diff --git a/frontend/code/componentManagement.ts b/frontend/code/componentManagement.ts index 79b771e1..3457d951 100644 --- a/frontend/code/componentManagement.ts +++ b/frontend/code/componentManagement.ts @@ -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, diff --git a/frontend/code/components/buildFailed.ts b/frontend/code/components/errorPlaceholder.ts similarity index 69% rename from frontend/code/components/buildFailed.ts rename to frontend/code/components/errorPlaceholder.ts index 79e53cc5..e86abd7b 100644 --- a/frontend/code/components/buildFailed.ts +++ b/frontend/code/components/errorPlaceholder.ts @@ -7,7 +7,7 @@ export type BuildFailedState = ComponentState & { error_details: string; }; -export class BuildFailedComponent extends ComponentBase { +export class ErrorPlaceholderComponent extends ComponentBase { state: Required; 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 = ` -
-
-
-
-
+
+
+
+
+
-
+
-
+
`; // 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 diff --git a/frontend/css/style.scss b/frontend/css/style.scss index 8e43df69..d26abbcc 100644 --- a/frontend/css/style.scss +++ b/frontend/css/style.scss @@ -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; } diff --git a/rio/components/build_failed.py b/rio/components/error_placeholder.py similarity index 65% rename from rio/components/build_failed.py rename to rio/components/error_placeholder.py index 2e8f1dea..c2a078f8 100644 --- a/rio/components/build_failed.py +++ b/rio/components/error_placeholder.py @@ -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" diff --git a/rio/deprecations.py b/rio/deprecations.py index bf3dbf79..25cc633d 100644 --- a/rio/deprecations.py +++ b/rio/deprecations.py @@ -44,7 +44,6 @@ def warn( "asyncio", "threading", ): - print(frame.globals["__name__"]) break else: stacklevel = 0 diff --git a/rio/routing.py b/rio/routing.py index 63fd6886..2c094ac1 100644 --- a/rio/routing.py +++ b/rio/routing.py @@ -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 + ), + ) diff --git a/rio/session.py b/rio/session.py index db399cf4..6bb875d3 100644 --- a/rio/session.py +++ b/rio/session.py @@ -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]() diff --git a/rio/utils.py b/rio/utils.py index 3fba0437..8562aa58 100644 --- a/rio/utils.py +++ b/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: