📝 Write and configure documentation

This commit is contained in:
Daniil Fajnberg 2023-03-11 16:17:12 +01:00
parent 72218fd2cb
commit 094e8b93f0
Signed by: daniil-berg
GPG Key ID: BE187C50903BEE97
8 changed files with 234 additions and 25 deletions

View File

@ -0,0 +1 @@
::: marshmallow_generic.decorators

View File

@ -0,0 +1 @@
::: marshmallow_generic.schema

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@ -1,15 +1,82 @@
# marshmallow-generic # marshmallow-generic
Generic schema with full typing support and minimal boilerplate **Generic schema with full typing support and minimal boilerplate**
## Usage ---
**Documentation**: <a href="http://daniil-berg.github.io/marshmallow-generic" target="_blank"> daniil-berg.github.io/marshmallow-generic </a>
**Source Code**: <a href="https://github.com/daniil-berg/marshmallow-generic" target="_blank"> github.com/daniil-berg/marshmallow-generic </a>
---
Extension for <a href="https://github.com/marshmallow-code/marshmallow" target="_blank">`marshmallow`</a> to make <a href="https://marshmallow.readthedocs.io/en/stable/quickstart.html#deserializing-to-objects" target="_blank">deserialization to objects</a> easier and improve type safety.
The main `GenericSchema` class extends <a href="https://marshmallow.readthedocs.io/en/stable/marshmallow.schema.html#marshmallow.schema.Schema" target="_blank">`marshmallow.Schema`</a> making it **generic** in terms of the class that data should be deserialized to, when calling <a href="https://marshmallow.readthedocs.io/en/stable/marshmallow.schema.html#marshmallow.schema.Schema.load" target="_blank">`load`/`loads`</a>.
With `GenericSchema` there is no need to explicitly write `post_load` hooks to initialize the object anymore. 🎉
If the "model" class is (for example) `User`, it just needs to be passed as the type argument, when subclassing `GenericSchema`. Depending on whether `many` is `True` or not, the output of the `load`/`loads` method will then be automatically inferred as either `User` or `list[User]` by any competent type checker. ✨
## Usage Example
```python
from marshmallow import fields
from marshmallow_generic import GenericSchema
class User:
def __init__(self, name: str, email: str) -> None:
self.name = name
self.email = email
def __repr__(self) -> str:
return "<User(name={self.name!r})>".format(self=self)
... ...
class UserSchema(GenericSchema[User]):
name = fields.Str()
email = fields.Email()
user_data = {"name": "Monty", "email": "monty@python.org"}
schema = UserSchema()
single_user = schema.load(user_data)
print(single_user) # <User(name='Monty')>
json_data = '''[
{"name": "Monty", "email": "monty@python.org"},
{"name": "Ronnie", "email": "ronnie@stones.com"}
]'''
multiple_users = schema.loads(json_data, many=True)
print(multiple_users) # [<User(name='Monty')>, <User(name='Ronnie')>]
```
Adding `reveal_type(single_user)` and `reveal_type(multiple_users)` at the bottom and running that code through <a href="https://mypy.readthedocs.io/en/stable/" target="_blank">`mypy`</a> would yield the following output:
```
# note: Revealed type is "User"
# note: Revealed type is "builtins.list[User]"
```
With the regular `marshmallow.Schema`, the output of `mypy` would instead be this:
```
# note: Revealed type is "Any"
# note: Revealed type is "Any"
```
This also means your IDE will be able to infer the types and thus provide useful auto-suggestions for the loaded objects. 👨‍💻
Here is PyCharm with the example from above:
![Image title](img/ide_suggestion_user.png){ width="540" }
## Installation ## Installation
`pip install marshmallow-generic` `pip install marshmallow-generic`
## Dependencies ## Dependencies
Python Version ..., OS ... Python Version `3.9+` and `marshmallow` (duh)

View File

@ -26,13 +26,28 @@ extra_css:
plugins: plugins:
- search - search
- mkdocstrings - mkdocstrings:
handlers:
python:
options:
show_source: false
show_root_toc_entry: false
import:
- https://marshmallow.readthedocs.io/en/stable/objects.inv
markdown_extensions: markdown_extensions:
- admonition - admonition
- codehilite - codehilite
- extra - extra
- pymdownx.superfences - pymdownx.superfences
- toc:
permalink: true
watch:
- src
nav: nav:
- Home: index.md - Home: index.md
- 'API Reference':
- api_reference/schema.md
- api_reference/decorators.md

View File

@ -120,6 +120,7 @@ ignore = [
"D203", # 1 blank line required before class docstring -> D211 is better "D203", # 1 blank line required before class docstring -> D211 is better
"D212", # Multi-line docstring summary should start at the first line -> ugly, D212 is better "D212", # Multi-line docstring summary should start at the first line -> ugly, D212 is better
"D401", # First line of docstring should be in imperative mood -> no, it shouldn't "D401", # First line of docstring should be in imperative mood -> no, it shouldn't
"D407", # Missing dashed underline after section -> different docstring style
] ]
[tool.ruff.per-file-ignores] [tool.ruff.per-file-ignores]

View File

@ -1,4 +1,8 @@
"""Typed overloads for some of the `marshmallow.decorators` module.""" """
Typed overloads for the [`marshmallow.decorators`][marshmallow.decorators] module.
Implements decorators as generic in terms of the decorated method types.
"""
from collections.abc import Callable from collections.abc import Callable
from typing import Any, Optional, TypeVar, overload from typing import Any, Optional, TypeVar, overload
@ -34,9 +38,31 @@ def post_load(
pass_original: bool = False, pass_original: bool = False,
) -> Callable[..., Any]: ) -> Callable[..., Any]:
""" """
Typed overload of the original `marshmallow.post_load` decorator function. Register a method to invoke after deserializing an object.
Typed overload of the original [`marshmallow.post_load`]
[marshmallow.post_load] decorator function.
Generic to ensure that the decorated function retains its type. Generic to ensure that the decorated function retains its type.
Runtime behavior is unchanged. Runtime behavior is unchanged.
Receives the deserialized data and returns the processed data.
By default it receives a single object at a time, transparently handling
the `many` argument passed to the [`Schema.load`][marshmallow.Schema.load]
call.
Args:
fn (Optional[Callable[P, R]]):
The function to decorate or `None`; if a function is supplied,
a decorated version of it is returned; if `None` the decorator
is returned with its other arguments already bound.
pass_many:
If `True`, the raw data (which may be a collection) is passed
pass_original:
If `True`, the original data (before deserializing) will be passed
as an additional argument to the method
Returns:
(Callable[P, R]): if `fn` is passed a function
(Callable[[Callable[P, R]], Callable[P, R]]): if `fn` is `None`
""" """
return _post_load(fn, pass_many=pass_many, pass_original=pass_original) return _post_load(fn, pass_many=pass_many, pass_original=pass_original)

View File

@ -1,4 +1,9 @@
"""Definition of the `GenericSchema` base class.""" """
Definition of the `GenericSchema` base class.
For details about the inherited methods and attributes, see the official
documentation of [`marshmallow.Schema`][marshmallow.Schema].
"""
from collections.abc import Iterable, Mapping, Sequence from collections.abc import Iterable, Mapping, Sequence
from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union, overload from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union, overload
@ -8,23 +13,58 @@ from marshmallow import Schema
from ._util import GenericInsightMixin from ._util import GenericInsightMixin
from .decorators import post_load from .decorators import post_load
_T = TypeVar("_T") Model = TypeVar("Model")
class GenericSchema(GenericInsightMixin[_T], Schema): class GenericSchema(GenericInsightMixin[Model], Schema):
""" """
Schema parameterized by the class it deserializes data to. Generic schema parameterized by a **`Model`** class.
Data will always be deserialized to instances of that **`Model`** class.
!!! note
The **`Model`** referred to throughout the documentation is a
**type variable**, not any concrete class. For more information about
type variables, see the "Generics" section in
[PEP 484](https://peps.python.org/pep-0484/#generics).
Registers a `post_load` hook to pass validated data to the constructor Registers a `post_load` hook to pass validated data to the constructor
of the specified class. of the specified **`Model`**.
Requires a specific (non-generic) class to be passed as the type argument Requires a specific (non-generic) class to be passed as the **`Model`**
for deserialization to work properly. type argument for deserialization to work properly:
```python
class Foo: # Model
...
class FooSchema(GenericSchema[Foo]):
...
```
""" """
@post_load @post_load
def instantiate(self, data: dict[str, Any], **_kwargs: Any) -> _T: def instantiate(self, data: dict[str, Any], **_kwargs: Any) -> Model:
"""Unpacks `data` into the constructor of the specified type.""" """
Unpacks `data` into the constructor of the specified **`Model`**.
Registered as a [`@post_load`]
[marshmallow_generic.decorators.post_load] hook for the schema.
!!! warning
You should probably **not** use this method directly;
no parsing, transformation or validation of any kind is done
in this method. The `data` passed to the **`Model`** constructor
"as is".
Args:
data:
The validated data after deserialization; will be unpacked
into the constructor of the specified **`Model`** class.
Returns:
Instance of the schema's **`Model`** initialized with `**data`
"""
return self._get_type_arg()(**data) return self._get_type_arg()(**data)
if TYPE_CHECKING: if TYPE_CHECKING:
@ -37,7 +77,7 @@ class GenericSchema(GenericInsightMixin[_T], Schema):
many: Literal[True], many: Literal[True],
partial: Union[bool, Sequence[str], set[str], None] = None, partial: Union[bool, Sequence[str], set[str], None] = None,
unknown: Optional[str] = None, unknown: Optional[str] = None,
) -> list[_T]: ) -> list[Model]:
... ...
@overload @overload
@ -48,7 +88,7 @@ class GenericSchema(GenericInsightMixin[_T], Schema):
many: Optional[Literal[False]] = None, many: Optional[Literal[False]] = None,
partial: Union[bool, Sequence[str], set[str], None] = None, partial: Union[bool, Sequence[str], set[str], None] = None,
unknown: Optional[str] = None, unknown: Optional[str] = None,
) -> _T: ) -> Model:
... ...
def load( def load(
@ -58,12 +98,40 @@ class GenericSchema(GenericInsightMixin[_T], Schema):
many: Optional[bool] = None, many: Optional[bool] = None,
partial: Union[bool, Sequence[str], set[str], None] = None, partial: Union[bool, Sequence[str], set[str], None] = None,
unknown: Optional[str] = None, unknown: Optional[str] = None,
) -> Union[list[_T], _T]: ) -> Union[list[Model], Model]:
""" """
Same as `marshmallow.Schema.load` at runtime. Deserializes data to objects of the specified **`Model`** class.
Same as [`marshmallow.Schema.load`]
[marshmallow.schema.Schema.load] at runtime, but data will always
pass through the [`instantiate`]
[marshmallow_generic.schema.GenericSchema.instantiate]
hook after deserialization.
Annotations ensure that type checkers will infer the return type Annotations ensure that type checkers will infer the return type
correctly based on the type argument passed to a specific subclass. correctly based on the **`Model`** type argument of the class.
Args:
data:
The data to deserialize
many:
Whether to deserialize `data` as a collection. If `None`,
the value for `self.many` is used.
partial:
Whether to ignore missing fields and not require any
fields declared. Propagates down to [`Nested`]
[marshmallow.fields.Nested] fields as well. If its value
is an iterable, only missing fields listed in that
iterable will be ignored. Use dot delimiters to specify
nested fields.
unknown:
Whether to exclude, include, or raise an error for unknown
fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`.
If `None`, the value for `self.unknown` is used.
Returns:
(Model): if `many` is set to `False`
(list[Model]): if `many` is set to `True`
""" """
... ...
@ -76,7 +144,7 @@ class GenericSchema(GenericInsightMixin[_T], Schema):
partial: Union[bool, Sequence[str], set[str], None] = None, partial: Union[bool, Sequence[str], set[str], None] = None,
unknown: Optional[str] = None, unknown: Optional[str] = None,
**kwargs: Any, **kwargs: Any,
) -> list[_T]: ) -> list[Model]:
... ...
@overload @overload
@ -88,7 +156,7 @@ class GenericSchema(GenericInsightMixin[_T], Schema):
partial: Union[bool, Sequence[str], set[str], None] = None, partial: Union[bool, Sequence[str], set[str], None] = None,
unknown: Optional[str] = None, unknown: Optional[str] = None,
**kwargs: Any, **kwargs: Any,
) -> _T: ) -> Model:
... ...
def loads( def loads(
@ -99,11 +167,41 @@ class GenericSchema(GenericInsightMixin[_T], Schema):
partial: Union[bool, Sequence[str], set[str], None] = None, partial: Union[bool, Sequence[str], set[str], None] = None,
unknown: Optional[str] = None, unknown: Optional[str] = None,
**kwargs: Any, **kwargs: Any,
) -> Union[list[_T], _T]: ) -> Union[list[Model], Model]:
""" """
Same as `marshmallow.Schema.loads` at runtime. Deserializes data to objects of the specified **`Model`** class.
Same as [`marshmallow.Schema.loads`]
[marshmallow.schema.Schema.loads] at runtime, but data will always
pass through the [`instantiate`]
[marshmallow_generic.schema.GenericSchema.instantiate]
hook after deserialization.
Annotations ensure that type checkers will infer the return type Annotations ensure that type checkers will infer the return type
correctly based on the type argument passed to a specific subclass. correctly based on the **`Model`** type argument of the class.
Args:
json_data:
A JSON string of the data to deserialize
many:
Whether to deserialize `data` as a collection. If `None`,
the value for `self.many` is used.
partial:
Whether to ignore missing fields and not require any
fields declared. Propagates down to [`Nested`]
[marshmallow.fields.Nested] fields as well. If its value
is an iterable, only missing fields listed in that
iterable will be ignored. Use dot delimiters to specify
nested fields.
unknown:
Whether to exclude, include, or raise an error for unknown
fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`.
If `None`, the value for `self.unknown` is used.
**kwargs:
Passed to the JSON decoder
Returns:
(Model): if `many` is set to `False`
(list[Model]): if `many` is set to `True`
""" """
... ...