Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions oxyroute/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
32 changes: 27 additions & 5 deletions tests/test_exception_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down Expand Up @@ -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())