Browse Source

use fastapi exception_handler

Falko Schindler 2 years ago
parent
commit
e9a18bcd49
4 changed files with 55 additions and 62 deletions
  1. 25 1
      nicegui/client.py
  2. 9 3
      nicegui/nicegui.py
  3. 19 51
      nicegui/page.py
  4. 2 7
      tests/test_page.py

+ 25 - 1
nicegui/client.py

@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
 
 from fastapi.responses import HTMLResponse
 
-from . import globals, vue
+from . import globals, ui, vue
 from .element import Element
 from .favicon import get_favicon_url
 from .slot import Slot
@@ -104,3 +104,27 @@ class Client:
     def open(self, target: Union[Callable, str]) -> None:
         path = target if isinstance(target, str) else globals.page_routes[target]
         create_task(globals.sio.emit('open', path, room=str(self.id)))
+
+
+class ErrorClient(Client):
+
+    def __init__(self, page: 'page') -> None:
+        super().__init__(page)
+        with self:
+            with ui.column().classes('w-full py-20 items-center gap-0'):
+                ui.icon('☹').classes('text-8xl py-5') \
+                    .style('font-family: "Arial Unicode MS", "Times New Roman", Times, serif;')
+                self.status_code = ui.label().classes('text-6xl py-5')
+                self.title = ui.label().classes('text-xl py-5')
+                self.message = ui.label().classes('text-lg py-2 text-gray-500')
+
+    def build_response(self, status_code: int, message: str = '') -> HTMLResponse:
+        self.status_code.text = status_code
+        if 400 <= status_code <= 499:
+            self.title.text = "This page doesn't exist"
+        elif 500 <= status_code <= 599:
+            self.title.text = 'Server error'
+        else:
+            self.title.text = 'Unknown error'
+        self.message.text = message
+        return super().build_response()

+ 9 - 3
nicegui/nicegui.py

@@ -3,14 +3,14 @@ import urllib.parse
 from pathlib import Path
 from typing import Dict
 
-from fastapi import FastAPI
+from fastapi import FastAPI, Request
 from fastapi.middleware.gzip import GZipMiddleware
 from fastapi.responses import FileResponse
 from fastapi.staticfiles import StaticFiles
 from fastapi_socketio import SocketManager
 
 from . import binding, globals, vue
-from .client import Client
+from .client import Client, ErrorClient
 from .favicon import create_favicon_routes
 from .helpers import safe_invoke
 from .page import page
@@ -22,7 +22,8 @@ globals.sio = sio = SocketManager(app=app)._sio
 app.add_middleware(GZipMiddleware)
 app.mount("/static", StaticFiles(directory=Path(__file__).parent / 'static'), name='static')
 
-Client(page('/')).__enter__()
+index_client = Client(page('/')).__enter__()
+error_client = ErrorClient(page(''))
 
 
 @app.get('/')
@@ -59,6 +60,11 @@ def shutdown() -> None:
     globals.state = globals.State.STOPPED
 
 
+@app.exception_handler(Exception)
+async def exception_handler(_: Request, exc: Exception):
+    return error_client.build_response(500, str(exc))
+
+
 @sio.on('connect')
 async def handle_connect(sid: str, _) -> None:
     client = get_client(sid)

+ 19 - 51
nicegui/page.py

@@ -4,9 +4,8 @@ import time
 from typing import Callable, Optional
 
 from fastapi import Response
-from fastapi.responses import HTMLResponse
 
-from . import globals, ui
+from . import globals
 from .client import Client
 from .task_logger import create_task
 
@@ -47,28 +46,24 @@ class page:
         globals.app.routes[:] = [r for r in globals.app.routes if r.path != self.path]
 
         async def decorated(*dec_args, **dec_kwargs) -> Response:
-            try:
-                with Client(self) as client:
-                    if any(p.name == 'client' for p in inspect.signature(func).parameters.values()):
-                        dec_kwargs['client'] = client
-                    result = func(*dec_args, **dec_kwargs)
-                if inspect.isawaitable(result):
-                    async def wait_for_result() -> Response:
-                        with client:
-                            await result
-                    task = create_task(wait_for_result())
-                    deadline = time.time() + self.response_timeout
-                    while task and not client.is_waiting_for_handshake and not task.done():
-                        if time.time() > deadline:
-                            raise TimeoutError(f'Response not ready after {self.response_timeout} seconds')
-                        await asyncio.sleep(0.1)
-                    result = task.result() if task.done() else None
-                if isinstance(result, Response):  # NOTE if setup returns a response, we don't need to render the page
-                    return result
-                return client.build_response()
-            except Exception as e:
-                globals.log.exception(e)
-                return error_client.build_response(500, str(e))
+            with Client(self) as client:
+                if any(p.name == 'client' for p in inspect.signature(func).parameters.values()):
+                    dec_kwargs['client'] = client
+                result = func(*dec_args, **dec_kwargs)
+            if inspect.isawaitable(result):
+                async def wait_for_result() -> Response:
+                    with client:
+                        await result
+                task = create_task(wait_for_result())
+                deadline = time.time() + self.response_timeout
+                while task and not client.is_waiting_for_handshake and not task.done():
+                    if time.time() > deadline:
+                        raise TimeoutError(f'Response not ready after {self.response_timeout} seconds')
+                    await asyncio.sleep(0.1)
+                result = task.result() if task.done() else None
+            if isinstance(result, Response):  # NOTE if setup returns a response, we don't need to render the page
+                return result
+            return client.build_response()
 
         parameters = [p for p in inspect.signature(func).parameters.values() if p.name != 'client']
         decorated.__signature__ = inspect.Signature(parameters)
@@ -76,30 +71,3 @@ class page:
         globals.page_routes[decorated] = self.path
 
         return globals.app.get(self.path)(decorated)
-
-
-class ErrorClient(Client):
-
-    def __init__(self) -> None:
-        super().__init__(page(''))
-        with self:
-            with ui.column().classes('w-full py-20 items-center gap-0'):
-                ui.icon('☹').classes('text-8xl py-5') \
-                    .style('font-family: "Arial Unicode MS", "Times New Roman", Times, serif;')
-                self.status_code = ui.label().classes('text-6xl py-5')
-                self.title = ui.label().classes('text-xl py-5')
-                self.message = ui.label().classes('text-lg py-2 text-gray-500')
-
-    def build_response(self, status_code: int, message: str = '') -> HTMLResponse:
-        self.status_code.text = status_code
-        if 400 <= status_code <= 499:
-            self.title.text = "This page doesn't exist"
-        elif 500 <= status_code <= 599:
-            self.title.text = 'Server error'
-        else:
-            self.title.text = 'Unknown error'
-        self.message.text = message
-        return super().build_response()
-
-
-error_client = ErrorClient()

+ 2 - 7
tests/test_page.py

@@ -1,11 +1,6 @@
 import asyncio
-from time import time
-from typing import Generator
 from uuid import uuid4
 
-import justpy.htmlcomponents
-from starlette.requests import Request
-
 from nicegui import Client, task_logger, ui
 
 from .screen import Screen
@@ -144,7 +139,7 @@ def test_exception(screen: Screen):
     screen.open('/')
     screen.should_contain('500')
     screen.should_contain('Server error')
-    screen.assert_py_logger('ERROR', 'some exception')
+    screen.assert_py_logger('ERROR', 'Exception in ASGI application\n')
 
 
 def test_exception_after_handshake(screen: Screen):
@@ -156,7 +151,7 @@ def test_exception_after_handshake(screen: Screen):
 
     screen.open('/')
     screen.should_contain('this is shown')
-    screen.assert_py_logger('ERROR', 'Failed to execute page-ready')
+    screen.assert_py_logger('ERROR', 'Task raised an exception')
 
 
 def test_page_with_args(screen: Screen):