generated from daniil-berg/boilerplate-py
Compare commits
No commits in common. "65df91ed3c6620ca661d57fd8f9ad597d31a3c6c" and "e90652ced5714f649ee22235cc0f71885fc31ecc" have entirely different histories.
65df91ed3c
...
e90652ced5
@ -1 +0,0 @@
|
|||||||
::: marshmallow_generic.decorators
|
|
@ -1 +0,0 @@
|
|||||||
::: marshmallow_generic.schema
|
|
Binary file not shown.
Before Width: | Height: | Size: 72 KiB |
@ -1,82 +1,15 @@
|
|||||||
# 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 `3.9+` and `marshmallow` (duh)
|
Python Version ..., OS ...
|
||||||
|
17
mkdocs.yaml
17
mkdocs.yaml
@ -26,28 +26,13 @@ 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
|
|
||||||
|
@ -28,7 +28,6 @@ classifiers = [
|
|||||||
"Operating System :: OS Independent",
|
"Operating System :: OS Independent",
|
||||||
"License :: OSI Approved :: Apache Software License",
|
"License :: OSI Approved :: Apache Software License",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"Typing :: Typed",
|
|
||||||
]
|
]
|
||||||
dynamic = [
|
dynamic = [
|
||||||
"dependencies",
|
"dependencies",
|
||||||
@ -43,12 +42,11 @@ full = [
|
|||||||
dev = [
|
dev = [
|
||||||
"black",
|
"black",
|
||||||
"build",
|
"build",
|
||||||
"coverage[toml]",
|
"coverage",
|
||||||
"isort",
|
"flake8",
|
||||||
"mkdocs-material",
|
"mkdocs-material",
|
||||||
"mkdocstrings[python]",
|
"mkdocstrings[python]",
|
||||||
"mypy",
|
"mypy",
|
||||||
"ruff",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
@ -61,14 +59,11 @@ dependencies = { file = "requirements/common.txt" }
|
|||||||
readme = { file = ["README.md"] }
|
readme = { file = ["README.md"] }
|
||||||
version = {attr = "marshmallow_generic.__version__"}
|
version = {attr = "marshmallow_generic.__version__"}
|
||||||
|
|
||||||
#########################
|
#########
|
||||||
# Static type checking: #
|
# Mypy: #
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
files = [
|
files = "src/"
|
||||||
"src/",
|
|
||||||
"tests/",
|
|
||||||
]
|
|
||||||
warn_unused_configs = true
|
warn_unused_configs = true
|
||||||
strict = true
|
strict = true
|
||||||
show_error_codes = true
|
show_error_codes = true
|
||||||
@ -76,8 +71,8 @@ plugins = [
|
|||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
#######################
|
#############
|
||||||
# Unit test coverage: #
|
# Coverage: #
|
||||||
|
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
source = [
|
source = [
|
||||||
@ -100,44 +95,3 @@ exclude_lines = [
|
|||||||
omit = [
|
omit = [
|
||||||
"tests/*",
|
"tests/*",
|
||||||
]
|
]
|
||||||
|
|
||||||
###############################
|
|
||||||
# Linting and style checking: #
|
|
||||||
|
|
||||||
[tool.ruff]
|
|
||||||
select = [
|
|
||||||
"E", # pycodestyle errors
|
|
||||||
"W", # pycodestyle warnings
|
|
||||||
"F", # pyflakes
|
|
||||||
"D", # pydocstyle
|
|
||||||
"C", # flake8-comprehensions
|
|
||||||
"B", # flake8-bugbear
|
|
||||||
"PL", # pylint
|
|
||||||
"RUF", # ruff-specific
|
|
||||||
]
|
|
||||||
ignore = [
|
|
||||||
"E501", # Line too long -> handled by black
|
|
||||||
"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]
|
|
||||||
"src/**/__init__.py" = [
|
|
||||||
"D104", # Missing docstring in public package
|
|
||||||
"F401", # {...} imported but unused
|
|
||||||
]
|
|
||||||
"tests/*.py" = [
|
|
||||||
"D100", # Missing docstring in public module
|
|
||||||
"D101", # Missing docstring in public class
|
|
||||||
"D102", # Missing docstring in public method
|
|
||||||
"D104", # Missing docstring in public package
|
|
||||||
]
|
|
||||||
|
|
||||||
###################
|
|
||||||
# Import sorting: #
|
|
||||||
|
|
||||||
[tool.isort]
|
|
||||||
profile = "black"
|
|
||||||
extra_standard_library = ["typing_extensions"]
|
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
-r common.txt
|
-r common.txt
|
||||||
black
|
black
|
||||||
build
|
build
|
||||||
coverage[toml]
|
coverage
|
||||||
isort
|
flake8
|
||||||
mkdocs-material
|
mkdocs-material
|
||||||
mkdocstrings[python]
|
mkdocstrings[python]
|
||||||
mypy
|
mypy
|
||||||
ruff
|
|
||||||
|
@ -12,7 +12,5 @@ mypy
|
|||||||
echo
|
echo
|
||||||
|
|
||||||
echo 'Linting source and test files...'
|
echo 'Linting source and test files...'
|
||||||
isort src/ tests/ --check-only
|
flake8 src/ tests/
|
||||||
ruff src/ tests/
|
|
||||||
black src/ tests/ --check
|
|
||||||
echo -e 'No issues found.'
|
echo -e 'No issues found.'
|
||||||
|
@ -16,8 +16,5 @@ limitations under the License."""
|
|||||||
__version__ = "0.0.1"
|
__version__ = "0.0.1"
|
||||||
|
|
||||||
__doc__ = """
|
__doc__ = """
|
||||||
Generic schema with full typing support and minimal boilerplate.
|
PLACEHOLDER
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .decorators import post_load
|
|
||||||
from .schema import GenericSchema
|
|
||||||
|
@ -1,30 +0,0 @@
|
|||||||
from typing import Any, Generic, Optional, TypeVar, get_args, get_origin
|
|
||||||
|
|
||||||
_T = TypeVar("_T")
|
|
||||||
|
|
||||||
|
|
||||||
class GenericInsightMixin(Generic[_T]):
|
|
||||||
_type_arg: Optional[type[_T]] = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
||||||
"""Saves the type argument in the `_type_arg` class attribute."""
|
|
||||||
super().__init_subclass__(**kwargs)
|
|
||||||
for base in cls.__orig_bases__: # type: ignore[attr-defined]
|
|
||||||
origin = get_origin(base)
|
|
||||||
if origin is None or not issubclass(origin, GenericInsightMixin):
|
|
||||||
continue
|
|
||||||
type_arg = get_args(base)[0]
|
|
||||||
# Do not set the attribute for GENERIC subclasses!
|
|
||||||
if not isinstance(type_arg, TypeVar):
|
|
||||||
cls._type_arg = type_arg
|
|
||||||
return
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _get_type_arg(cls) -> type[_T]:
|
|
||||||
"""Returns the type argument of the class (if specified)."""
|
|
||||||
if cls._type_arg is None:
|
|
||||||
raise AttributeError(
|
|
||||||
f"{cls.__name__} is generic; type argument unspecified"
|
|
||||||
)
|
|
||||||
return cls._type_arg
|
|
@ -1,68 +0,0 @@
|
|||||||
"""
|
|
||||||
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
|
|
||||||
from typing_extensions import ParamSpec
|
|
||||||
|
|
||||||
from marshmallow.decorators import post_load as _post_load
|
|
||||||
|
|
||||||
_R = TypeVar("_R")
|
|
||||||
_P = ParamSpec("_P")
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def post_load(
|
|
||||||
fn: Callable[_P, _R],
|
|
||||||
pass_many: bool = False,
|
|
||||||
pass_original: bool = False,
|
|
||||||
) -> Callable[_P, _R]:
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def post_load(
|
|
||||||
fn: None = None,
|
|
||||||
pass_many: bool = False,
|
|
||||||
pass_original: bool = False,
|
|
||||||
) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
def post_load(
|
|
||||||
fn: Optional[Callable[..., Any]] = None,
|
|
||||||
pass_many: bool = False,
|
|
||||||
pass_original: bool = False,
|
|
||||||
) -> Callable[..., Any]:
|
|
||||||
"""
|
|
||||||
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)
|
|
@ -1,207 +0,0 @@
|
|||||||
"""
|
|
||||||
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
|
|
||||||
|
|
||||||
from marshmallow import Schema
|
|
||||||
|
|
||||||
from ._util import GenericInsightMixin
|
|
||||||
from .decorators import post_load
|
|
||||||
|
|
||||||
Model = TypeVar("Model")
|
|
||||||
|
|
||||||
|
|
||||||
class GenericSchema(GenericInsightMixin[Model], Schema):
|
|
||||||
"""
|
|
||||||
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 **`Model`**.
|
|
||||||
|
|
||||||
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) -> 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:
|
|
||||||
|
|
||||||
@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[Model]:
|
|
||||||
...
|
|
||||||
|
|
||||||
@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,
|
|
||||||
) -> Model:
|
|
||||||
...
|
|
||||||
|
|
||||||
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[Model], Model]:
|
|
||||||
"""
|
|
||||||
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 **`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`
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|
||||||
@overload # type: ignore[override]
|
|
||||||
def loads(
|
|
||||||
self,
|
|
||||||
json_data: str,
|
|
||||||
*,
|
|
||||||
many: Literal[True],
|
|
||||||
partial: Union[bool, Sequence[str], set[str], None] = None,
|
|
||||||
unknown: Optional[str] = None,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> list[Model]:
|
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def loads(
|
|
||||||
self,
|
|
||||||
json_data: str,
|
|
||||||
*,
|
|
||||||
many: Optional[Literal[False]] = None,
|
|
||||||
partial: Union[bool, Sequence[str], set[str], None] = None,
|
|
||||||
unknown: Optional[str] = None,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> Model:
|
|
||||||
...
|
|
||||||
|
|
||||||
def loads(
|
|
||||||
self,
|
|
||||||
json_data: str,
|
|
||||||
*,
|
|
||||||
many: Optional[bool] = None,
|
|
||||||
partial: Union[bool, Sequence[str], set[str], None] = None,
|
|
||||||
unknown: Optional[str] = None,
|
|
||||||
**kwargs: Any,
|
|
||||||
) -> Union[list[Model], Model]:
|
|
||||||
"""
|
|
||||||
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 **`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`
|
|
||||||
"""
|
|
||||||
...
|
|
@ -1,6 +1,7 @@
|
|||||||
import sys
|
import sys
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
pattern = sys.argv[1]
|
pattern = sys.argv[1]
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
from typing import Generic, TypeVar
|
|
||||||
from unittest import TestCase
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
from marshmallow_generic import _util
|
|
||||||
|
|
||||||
|
|
||||||
class GenericInsightMixinTestCase(TestCase):
|
|
||||||
@patch.object(_util, "super")
|
|
||||||
def test___init_subclass__(self, mock_super: MagicMock) -> None:
|
|
||||||
mock_super_meth = MagicMock()
|
|
||||||
mock_super.return_value = MagicMock(__init_subclass__=mock_super_meth)
|
|
||||||
|
|
||||||
# Should be `None` by default:
|
|
||||||
self.assertIsNone(_util.GenericInsightMixin._type_arg) # type: ignore[misc]
|
|
||||||
|
|
||||||
# If the mixin type argument was not specified (still generic),
|
|
||||||
# ensure that the attribute remains `None` on the subclass:
|
|
||||||
t = TypeVar("t")
|
|
||||||
|
|
||||||
class Foo:
|
|
||||||
pass
|
|
||||||
|
|
||||||
class Bar(Generic[t]):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class TestSchema1(Bar[str], _util.GenericInsightMixin[t]):
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.assertIsNone(TestSchema1._type_arg) # type: ignore[misc]
|
|
||||||
mock_super.assert_called_once()
|
|
||||||
mock_super_meth.assert_called_once_with()
|
|
||||||
|
|
||||||
mock_super.reset_mock()
|
|
||||||
mock_super_meth.reset_mock()
|
|
||||||
|
|
||||||
# If the mixin type argument was specified,
|
|
||||||
# ensure it was assigned to the attribute on the child class:
|
|
||||||
|
|
||||||
class TestSchema2(Bar[str], _util.GenericInsightMixin[Foo]):
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.assertIs(Foo, TestSchema2._type_arg) # type: ignore[misc]
|
|
||||||
mock_super.assert_called_once()
|
|
||||||
mock_super_meth.assert_called_once_with()
|
|
||||||
|
|
||||||
def test__get_type_arg(self) -> None:
|
|
||||||
with self.assertRaises(AttributeError):
|
|
||||||
_util.GenericInsightMixin._get_type_arg()
|
|
||||||
|
|
||||||
_type = object()
|
|
||||||
with patch.object(_util.GenericInsightMixin, "_type_arg", new=_type):
|
|
||||||
self.assertIs(_type, _util.GenericInsightMixin._get_type_arg())
|
|
@ -1,28 +0,0 @@
|
|||||||
from collections.abc import Callable
|
|
||||||
from unittest import TestCase
|
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
|
|
||||||
from marshmallow_generic import decorators
|
|
||||||
|
|
||||||
|
|
||||||
class DecoratorsTestCase(TestCase):
|
|
||||||
@patch.object(decorators, "_post_load")
|
|
||||||
def test_post_load(self, mock_original_post_load: MagicMock) -> None:
|
|
||||||
mock_original_post_load.return_value = expected_output = object()
|
|
||||||
|
|
||||||
def test_function(x: int) -> str:
|
|
||||||
return str(x)
|
|
||||||
|
|
||||||
pass_many, pass_original = MagicMock(), MagicMock()
|
|
||||||
# Explicit annotation to possibly catch mypy errors:
|
|
||||||
output: Callable[[int], str] = decorators.post_load(
|
|
||||||
test_function,
|
|
||||||
pass_many=pass_many,
|
|
||||||
pass_original=pass_original,
|
|
||||||
)
|
|
||||||
self.assertIs(expected_output, output)
|
|
||||||
mock_original_post_load.assert_called_once_with(
|
|
||||||
test_function,
|
|
||||||
pass_many=pass_many,
|
|
||||||
pass_original=pass_original,
|
|
||||||
)
|
|
@ -1,44 +0,0 @@
|
|||||||
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_and_loads(self) -> None:
|
|
||||||
"""Mainly for static type checking purposes."""
|
|
||||||
|
|
||||||
class Foo:
|
|
||||||
pass
|
|
||||||
|
|
||||||
class TestSchema(schema.GenericSchema[Foo]):
|
|
||||||
pass
|
|
||||||
|
|
||||||
single: Foo
|
|
||||||
single = TestSchema().load({})
|
|
||||||
self.assertIsInstance(single, Foo)
|
|
||||||
single = TestSchema().loads("{}")
|
|
||||||
self.assertIsInstance(single, Foo)
|
|
||||||
|
|
||||||
multiple: list[Foo]
|
|
||||||
multiple = TestSchema().load([{}], many=True)
|
|
||||||
self.assertIsInstance(multiple, list)
|
|
||||||
self.assertIsInstance(multiple[0], Foo)
|
|
||||||
multiple = TestSchema().loads("[{}]", many=True)
|
|
||||||
self.assertIsInstance(multiple, list)
|
|
||||||
self.assertIsInstance(multiple[0], Foo)
|
|
Loading…
Reference in New Issue
Block a user