generated from daniil-berg/boilerplate-py
📝 Write and configure documentation
This commit is contained in:
parent
72218fd2cb
commit
094e8b93f0
1
docs/api_reference/decorators.md
Normal file
1
docs/api_reference/decorators.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
::: marshmallow_generic.decorators
|
1
docs/api_reference/schema.md
Normal file
1
docs/api_reference/schema.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
::: marshmallow_generic.schema
|
BIN
docs/img/ide_suggestion_user.png
Normal file
BIN
docs/img/ide_suggestion_user.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
@ -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)
|
||||||
|
17
mkdocs.yaml
17
mkdocs.yaml
@ -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
|
||||||
|
@ -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]
|
||||||
|
@ -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)
|
||||||
|
@ -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`
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
Loading…
Reference in New Issue
Block a user