generated from daniil-berg/boilerplate-py
✨ Add main GenericSchema
class
This commit is contained in:
parent
127ba69d3b
commit
bff3c7ef52
68
src/marshmallow_generic/schema.py
Normal file
68
src/marshmallow_generic/schema.py
Normal file
@ -0,0 +1,68 @@
|
||||
"""Definition of the `GenericSchema` base class."""
|
||||
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional, TypeVar, Union, overload
|
||||
|
||||
from marshmallow import Schema
|
||||
|
||||
from ._util import GenericInsightMixin
|
||||
from .decorators import post_load
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
class GenericSchema(GenericInsightMixin[_T], Schema):
|
||||
"""
|
||||
Schema parameterized by the class it deserializes data to.
|
||||
|
||||
Registers a `post_load` hook to pass validated data to the constructor
|
||||
of the specified class.
|
||||
|
||||
Requires a specific (non-generic) class to be passed as the type argument
|
||||
for deserialization to work properly.
|
||||
"""
|
||||
|
||||
@post_load
|
||||
def instantiate(self, data: dict[str, Any], **_kwargs: Any) -> _T:
|
||||
"""Unpacks `data` into the constructor of the specified type."""
|
||||
return self._get_type_arg()(**data)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@overload # type: ignore[override]
|
||||
def load(
|
||||
self,
|
||||
data: Union[Mapping[str, Any], Iterable[Mapping[str, Any]]],
|
||||
*,
|
||||
many: Literal[True],
|
||||
partial: Union[bool, Sequence[str], set[str], None] = None,
|
||||
unknown: Optional[str] = None,
|
||||
) -> list[_T]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def load(
|
||||
self,
|
||||
data: Union[Mapping[str, Any], Iterable[Mapping[str, Any]]],
|
||||
*,
|
||||
many: Optional[Literal[False]] = None,
|
||||
partial: Union[bool, Sequence[str], set[str], None] = None,
|
||||
unknown: Optional[str] = None,
|
||||
) -> _T:
|
||||
...
|
||||
|
||||
def load(
|
||||
self,
|
||||
data: Union[Mapping[str, Any], Iterable[Mapping[str, Any]]],
|
||||
*,
|
||||
many: Optional[bool] = None,
|
||||
partial: Union[bool, Sequence[str], set[str], None] = None,
|
||||
unknown: Optional[str] = None,
|
||||
) -> Union[list[_T], _T]:
|
||||
"""
|
||||
Same as `marshmallow.Schema.load` at runtime.
|
||||
|
||||
Annotations ensure that type checkers will infer the return type
|
||||
correctly based on the type argument passed to a specific subclass.
|
||||
"""
|
||||
...
|
37
tests/test_schema.py
Normal file
37
tests/test_schema.py
Normal file
@ -0,0 +1,37 @@
|
||||
from unittest import TestCase
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from marshmallow_generic import _util, schema
|
||||
|
||||
|
||||
class GenericSchemaTestCase(TestCase):
|
||||
@patch.object(_util.GenericInsightMixin, "_get_type_arg")
|
||||
def test_instantiate(self, mock__get_type_arg: MagicMock) -> None:
|
||||
mock__get_type_arg.return_value = mock_cls = MagicMock()
|
||||
mock_data = {"foo": "bar", "spam": 123}
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
schema_obj = schema.GenericSchema[Foo]()
|
||||
# Explicit annotation to possibly catch mypy errors:
|
||||
output: Foo = schema_obj.instantiate(mock_data)
|
||||
self.assertIs(mock_cls.return_value, output)
|
||||
mock__get_type_arg.assert_called_once_with()
|
||||
mock_cls.assert_called_once_with(**mock_data)
|
||||
|
||||
def test_load(self) -> None:
|
||||
"""Mainly for static type checking purposes."""
|
||||
|
||||
class Foo:
|
||||
pass
|
||||
|
||||
class TestSchema(schema.GenericSchema[Foo]):
|
||||
pass
|
||||
|
||||
single: Foo = TestSchema().load({})
|
||||
self.assertIsInstance(single, Foo)
|
||||
|
||||
multiple: list[Foo] = TestSchema().load([{}], many=True)
|
||||
self.assertIsInstance(multiple, list)
|
||||
self.assertIsInstance(multiple[0], Foo)
|
Loading…
Reference in New Issue
Block a user