Skip to content

Commit

Permalink
Shorthand docs
Browse files Browse the repository at this point in the history
  • Loading branch information
Tinche committed Dec 14, 2023
1 parent 8ed19ae commit ec90c8a
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 2 deletions.
12 changes: 11 additions & 1 deletion docs/handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ Whether the response contains the `content-type` header is up to the underlying
Flask, Quart and Django add a `text/html` content type by default.
```

A longer equivalent, with the added benefit of being able to specify response headers, is returning the {py:class}`NoContent <uapi.status.NoContent>` response explicitly.
A longer equivalent, with the added benefit of being able to specify response headers, is returning the {class}`NoContent <uapi.status.NoContent>` response explicitly.

```python
from uapi.status import NoContent
Expand All @@ -462,6 +462,9 @@ async def delete_article() -> NoContent:
return NoContent(headers={"key": "value"})
```

_This functionality is handled by {class}`NoneShorthand <uapi.shorthands.NoneShorthand>`._


### Strings and Bytes `(200 OK)`

If your handler returns a string or bytes, the response will be returned directly alongside the `200 OK` status code.
Expand All @@ -474,6 +477,8 @@ async def get_article_image() -> bytes:

For strings, the `content-type` header is set to `text/plain`, and for bytes to `application/octet-stream`.

_This functionality is handled by {class}`StrShorthand <uapi.shorthands.StrShorthand>` and {class}`BytesShorthand <uapi.shorthands.BytesShorthand>`._

### _attrs_ Classes

Handlers can return an instance of an _attrs_ class.
Expand All @@ -493,6 +498,11 @@ async def get_article() -> Article:
...
```

### Custom Response Shorthands

The `str`, `bytes` or `None` return types are examples of _response shorthands_.
Custom response shorthands can be defined and added to apps; [see the Response Shorthands section for the details](response_shorthands.md).

### _uapi_ Status Code Classes

_uapi_ {py:obj}`contains a variety of classes <uapi.status>`, mapping to status codes, for returning from handlers.
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ handlers.md
composition.md
openapi.md
addons.md
response_shorthands.md
changelog.md
indices.md
modules.rst
Expand Down
100 changes: 100 additions & 0 deletions docs/response_shorthands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
```{currentmodule} uapi.shorthands
```
# Response Shorthands

Custom response shorthands are created by defining a custom instance of the {class}`ResponseShorthand` protocol.
This involves implementing two to four functions, depending on the amount of functionality required.

## A `datetime.datetime` Shorthand

Here are the steps needed to implement a new shorthand, enabling handlers to return [`datetime.datetime`](https://docs.python.org/3/library/datetime.html#datetime-objects) instances directly.

First, we need to create the shorthand class by subclassing the {class}`ResponseShorthand` generic protocol.

```python
from datetime import datetime

from uapi.shorthands import ResponseShorthand

class DatetimeShorthand(ResponseShorthand[datetime]):
pass
```

Note that the shorthand is generic over the type we want to enable.
This protocol contains four static methods (functions); two mandatory ones and two optional ones.

The first function we need to override is {meth}`ResponseShorthand.response_adapter`.
This functions needs to convert an instance of our type (`datetime`) into a _uapi_ [status code class](handlers.md#uapi-status-code-classes), so _uapi_ can adapt the value for the underlying framework.

```python
from uapi.status import BaseResponse, Ok

class DatetimeShorthand(ResponseShorthand[datetime]):

@staticmethod
def response_adapter(value: Any) -> BaseResponse:
return Ok(value.isoformat(), headers={"content-type": "date"})
```

The second function is {meth}`ResponseShorthand.is_union_member`.
This function is used to recognize if a return value is an instance of the shorthand type when the return type is a union.
For example, if the return type is `datetime | str`, uapi needs to be able to detect and handle both cases.

```python
class DatetimeShorthand(ResponseShorthand[datetime]):

@staticmethod
def is_union_member(value: Any) -> bool:
return isinstance(value, datetime)
```

With these two functions we have a minimal shorthand implementation.
We can add it to an app to be able to use it:

```
from uapi.starlette import App # Or any other app
app = App()
app = app.add_response_shorthand(DatetimeShorthand)
```

And we're done.

### OpenAPI Integration

If we stop here our shorthand won't show up in the [generated OpenAPI schema](openapi.md).
To enable OpenAPI integration we need to implement one more function, {meth}`ResponseShorthand.make_openapi_response`.

This function returns the [OpenAPI response definition](https://swagger.io/specification/#responses-object) for the shorthand.

```python
from uapi.openapi import MediaType, Response, Schema

class DatetimeShorthand(ResponseShorthand[datetime]):

@staticmethod
def make_openapi_response() -> Response:
return Response(
"OK",
{"date": MediaType(Schema(Schema.Type.STRING, format="datetime"))},
)
```

### Custom Type Matching

Registered shorthands are matched to handler return types using simple identity and [`issubclass`](https://docs.python.org/3/library/functions.html#issubclass) checks.
Sometimes, more sophisticated matching is required.

For example, the default {class}`NoneShorthand <NoneShorthand>` shorthand wouldn't work for some handlers without custom matching since it needs to match both `None` and `NoneType`. This matching can be customized by overriding the {meth}`ResponseShorthand.can_handle` function.

Here's what a dummy implementation would look like for our `DatetimeShorthand`.

```python
class DatetimeShorthand(ResponseShorthand[datetime]):

@staticmethod
def can_handle(type: Any) -> bool:
return issubclass(type, datetime)
```
8 changes: 8 additions & 0 deletions docs/uapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ uapi.responses module
:undoc-members:
:show-inheritance:

uapi.shorthands module
----------------------

.. automodule:: uapi.shorthands
:members:
:undoc-members:
:show-inheritance:

uapi.starlette module
---------------------

Expand Down
20 changes: 20 additions & 0 deletions src/uapi/shorthands.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
from .openapi import MediaType, Response, Schema
from .status import BaseResponse, NoContent, Ok

__all__ = ["ResponseShorthand", "NoneShorthand", "StrShorthand", "BytesShorthand"]

T_co = TypeVar("T_co", covariant=True)


class ResponseShorthand(Protocol[T_co]):
"""The base protocol for response shorthands."""

@staticmethod
def response_adapter(value: Any) -> BaseResponse: # pragma: no cover
"""Convert a value of this type into a base response."""
Expand Down Expand Up @@ -42,6 +46,11 @@ def can_handle(type: Any) -> bool | Literal["check_type"]:


class NoneShorthand(ResponseShorthand[None]):
"""Support for handlers returning `None`.
The response code is set to 204, and the content type is left unset.
"""

@staticmethod
def response_adapter(_: Any, _nc=NoContent()) -> BaseResponse:
return _nc
Expand All @@ -60,6 +69,11 @@ def can_handle(type: Any) -> bool | Literal["check_type"]:


class StrShorthand(ResponseShorthand[str]):
"""Support for handlers returning `str`.
The response code is set to 200 and the content type is set to `text/plain`.
"""

@staticmethod
def response_adapter(value: Any) -> BaseResponse:
return Ok(value, headers={"content-type": "text/plain"})
Expand All @@ -74,6 +88,12 @@ def make_openapi_response() -> Response:


class BytesShorthand(ResponseShorthand[bytes]):
"""Support for handlers returning `bytes`.
The response code is set to 200 and the content type is set to
`application/octet-stream`.
"""

@staticmethod
def response_adapter(value: Any) -> BaseResponse:
return Ok(value, headers={"content-type": "application/octet-stream"})
Expand Down
6 changes: 5 additions & 1 deletion tests/openapi/test_shorthands.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ class OpenAPIDateTime(DatetimeShorthand):
def make_openapi_response() -> Response | None:
return Response(
"DESC",
{"test": MediaType(Schema(Schema.Type.STRING, format="datetime"))},
{
"application/date": MediaType(
Schema(Schema.Type.STRING, format="datetime")
)
},
)

app = App()
Expand Down

0 comments on commit ec90c8a

Please sign in to comment.