diff --git a/docs/api_reference/decorators.md b/docs/api_reference/decorators.md
new file mode 100644
index 0000000..53549cb
--- /dev/null
+++ b/docs/api_reference/decorators.md
@@ -0,0 +1 @@
+::: marshmallow_generic.decorators
\ No newline at end of file
diff --git a/docs/api_reference/schema.md b/docs/api_reference/schema.md
new file mode 100644
index 0000000..8e0a448
--- /dev/null
+++ b/docs/api_reference/schema.md
@@ -0,0 +1 @@
+::: marshmallow_generic.schema
diff --git a/docs/img/ide_suggestion_user.png b/docs/img/ide_suggestion_user.png
new file mode 100644
index 0000000..971bb6b
Binary files /dev/null and b/docs/img/ide_suggestion_user.png differ
diff --git a/docs/index.md b/docs/index.md
index 8532ea9..eb5c5d1 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,15 +1,82 @@
# marshmallow-generic
-Generic schema with full typing support and minimal boilerplate
+**Generic schema with full typing support and minimal boilerplate**
-## Usage
+---
+
+**Documentation**: daniil-berg.github.io/marshmallow-generic
+
+**Source Code**: github.com/daniil-berg/marshmallow-generic
+
+---
+
+Extension for `marshmallow` to make deserialization to objects easier and improve type safety.
+
+The main `GenericSchema` class extends `marshmallow.Schema` making it **generic** in terms of the class that data should be deserialized to, when calling `load`/`loads`.
+
+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 "".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) #
+
+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) # [, ]
+```
+
+Adding `reveal_type(single_user)` and `reveal_type(multiple_users)` at the bottom and running that code through `mypy` 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](http://daniil-berg.github.io/marshmallow-generic/img/ide_suggestion_user.png)
+
## Installation
`pip install marshmallow-generic`
## Dependencies
-Python Version ..., OS ...
+Python Version `3.9+` and `marshmallow` (duh)
diff --git a/mkdocs.yaml b/mkdocs.yaml
index 5a41e3d..2b927c5 100644
--- a/mkdocs.yaml
+++ b/mkdocs.yaml
@@ -26,13 +26,28 @@ extra_css:
plugins:
- 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:
- admonition
- codehilite
- extra
- pymdownx.superfences
+ - toc:
+ permalink: true
+
+watch:
+ - src
nav:
- Home: index.md
+ - 'API Reference':
+ - api_reference/schema.md
+ - api_reference/decorators.md
diff --git a/pyproject.toml b/pyproject.toml
index 9086c39..5182983 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -120,6 +120,7 @@ ignore = [
"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
"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]
diff --git a/src/marshmallow_generic/decorators.py b/src/marshmallow_generic/decorators.py
index 9d672a5..93f50b3 100644
--- a/src/marshmallow_generic/decorators.py
+++ b/src/marshmallow_generic/decorators.py
@@ -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 typing import Any, Optional, TypeVar, overload
@@ -34,9 +38,31 @@ def post_load(
pass_original: bool = False,
) -> 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.
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)
diff --git a/src/marshmallow_generic/schema.py b/src/marshmallow_generic/schema.py
index 797faac..8e68ecd 100644
--- a/src/marshmallow_generic/schema.py
+++ b/src/marshmallow_generic/schema.py
@@ -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 typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union, overload
@@ -8,23 +13,58 @@ from marshmallow import Schema
from ._util import GenericInsightMixin
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
- of the specified class.
+ of the specified **`Model`**.
- Requires a specific (non-generic) class to be passed as the type argument
- for deserialization to work properly.
+ Requires a specific (non-generic) class to be passed as the **`Model`**
+ type argument for deserialization to work properly:
+
+ ```python
+ class Foo: # Model
+ ...
+
+ class FooSchema(GenericSchema[Foo]):
+ ...
+ ```
"""
@post_load
- def instantiate(self, data: dict[str, Any], **_kwargs: Any) -> _T:
- """Unpacks `data` into the constructor of the specified type."""
+ def instantiate(self, data: dict[str, Any], **_kwargs: Any) -> Model:
+ """
+ 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)
if TYPE_CHECKING:
@@ -37,7 +77,7 @@ class GenericSchema(GenericInsightMixin[_T], Schema):
many: Literal[True],
partial: Union[bool, Sequence[str], set[str], None] = None,
unknown: Optional[str] = None,
- ) -> list[_T]:
+ ) -> list[Model]:
...
@overload
@@ -48,7 +88,7 @@ class GenericSchema(GenericInsightMixin[_T], Schema):
many: Optional[Literal[False]] = None,
partial: Union[bool, Sequence[str], set[str], None] = None,
unknown: Optional[str] = None,
- ) -> _T:
+ ) -> Model:
...
def load(
@@ -58,12 +98,40 @@ class GenericSchema(GenericInsightMixin[_T], Schema):
many: Optional[bool] = None,
partial: Union[bool, Sequence[str], set[str], None] = 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
- 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,
unknown: Optional[str] = None,
**kwargs: Any,
- ) -> list[_T]:
+ ) -> list[Model]:
...
@overload
@@ -88,7 +156,7 @@ class GenericSchema(GenericInsightMixin[_T], Schema):
partial: Union[bool, Sequence[str], set[str], None] = None,
unknown: Optional[str] = None,
**kwargs: Any,
- ) -> _T:
+ ) -> Model:
...
def loads(
@@ -99,11 +167,41 @@ class GenericSchema(GenericInsightMixin[_T], Schema):
partial: Union[bool, Sequence[str], set[str], None] = None,
unknown: Optional[str] = None,
**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
- 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`
"""
...