diff --git a/oxyroute/app.py b/oxyroute/app.py index 2e48ced..f029f79 100644 --- a/oxyroute/app.py +++ b/oxyroute/app.py @@ -383,6 +383,23 @@ def add_exception_handler( """ self._app.add_exception_handler(exc_type, handler) + def exception_handler(self, exc_type: type[BaseException]) -> Callable[[F], F]: + """ + Decorator to register a global exception handler for a specific exception type. + + Example:: + + @app.exception_handler(ValueError) + def handle_value_error(scope, exc: ValueError): + return Response(status=400, body=str(exc).encode()) + """ + + def decorator(handler: F) -> F: + self.add_exception_handler(exc_type, handler) + return handler + + return decorator + def set_middleware(self, handler: Callable[..., Any] | None) -> None: """ One optional pre-route callback ``(scope, protocol)`` — return ``None`` to pass through. diff --git a/tests/test_exception_handlers.py b/tests/test_exception_handlers.py index 69c6ed7..8167176 100644 --- a/tests/test_exception_handlers.py +++ b/tests/test_exception_handlers.py @@ -15,11 +15,6 @@ class SubCustomError(CustomError): app = App() - # Note: the `add_exception_handler` method might be used as a decorator or a regular method. - # We didn't implement it as a decorator returning the function, but in our `app.py` it's just: - # def add_exception_handler(self, exc_type: type[BaseException], handler: Callable[..., Any]) -> None: - # So we call it directly. - def handle_custom_error(scope, exc): return Response(status=400, body=exc.msg.encode()) @@ -56,3 +51,30 @@ async def _run() -> None: assert r3.status_code == 500 asyncio.run(_run()) + + +def test_exception_handler_decorator(): + """@app.exception_handler(exc_type) decorator syntax (issue #274).""" + + class DecoratorError(Exception): + def __init__(self, msg: str): + self.msg = msg + + app = App() + + @app.exception_handler(DecoratorError) + def handle_decorator_error(scope, exc: DecoratorError): + return Response(status=422, body=f"decorated:{exc.msg}".encode()) + + @app.get("/decorated-error") + def raise_decorated() -> str: + raise DecoratorError("via-decorator") + + async def _run() -> None: + transport = httpx.ASGITransport(app=asgi_test_app(app)) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as c: + r = await c.get("/decorated-error") + assert r.status_code == 422 + assert r.text == "decorated:via-decorator" + + asyncio.run(_run())