generated from daniil-berg/boilerplate-py
Compare commits
4 Commits
fe5decad4f
...
d272864e44
Author | SHA1 | Date | |
---|---|---|---|
d272864e44 | |||
4dd1fbaf53 | |||
4878d550fe | |||
2a5e35b334 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -21,5 +21,5 @@ __pycache__/
|
|||||||
# Testing:
|
# Testing:
|
||||||
/.coverage
|
/.coverage
|
||||||
|
|
||||||
# mypy:
|
# Miscellaneous cache:
|
||||||
.mypy_cache/
|
.cache/
|
||||||
|
@ -10,19 +10,18 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
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.
|
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>.
|
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. 🎉
|
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. ✨
|
If the "model" class is (for example) `User`, it just needs to be passed as the type argument, when subclassing `GenericSchema`. The output of the `load`/`loads` method will then be automatically inferred as either `User` or `list[User]` (depending on whether `many` is `True` or not) by any competent type checker. ✨
|
||||||
|
|
||||||
## Usage Example
|
## Usage Example
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from marshmallow import fields
|
from marshmallow_generic import GenericSchema, fields
|
||||||
from marshmallow_generic import GenericSchema
|
|
||||||
|
|
||||||
|
|
||||||
class User:
|
class User:
|
||||||
|
@ -65,6 +65,7 @@ version = {attr = "marshmallow_generic.__version__"}
|
|||||||
# Static type checking: #
|
# Static type checking: #
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
|
cache_dir = ".cache/mypy"
|
||||||
files = [
|
files = [
|
||||||
"src/",
|
"src/",
|
||||||
"tests/",
|
"tests/",
|
||||||
@ -80,6 +81,7 @@ plugins = [
|
|||||||
# Unit test coverage: #
|
# Unit test coverage: #
|
||||||
|
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
|
data_file = ".cache/coverage"
|
||||||
source = [
|
source = [
|
||||||
"src/",
|
"src/",
|
||||||
]
|
]
|
||||||
@ -105,6 +107,7 @@ omit = [
|
|||||||
# Linting and style checking: #
|
# Linting and style checking: #
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
|
cache-dir = ".cache/ruff"
|
||||||
select = [
|
select = [
|
||||||
"E", # pycodestyle errors
|
"E", # pycodestyle errors
|
||||||
"W", # pycodestyle warnings
|
"W", # pycodestyle warnings
|
||||||
|
@ -19,5 +19,41 @@ __doc__ = """
|
|||||||
Generic schema with full typing support and minimal boilerplate.
|
Generic schema with full typing support and minimal boilerplate.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .decorators import post_load
|
__all__ = [
|
||||||
from .schema import GenericSchema
|
# Custom:
|
||||||
|
"GenericSchema",
|
||||||
|
"post_load",
|
||||||
|
# Re-exports from marshmallow:
|
||||||
|
"EXCLUDE",
|
||||||
|
"INCLUDE",
|
||||||
|
"RAISE",
|
||||||
|
"Schema",
|
||||||
|
"SchemaOpts",
|
||||||
|
"fields",
|
||||||
|
"validates",
|
||||||
|
"validates_schema",
|
||||||
|
"pre_dump",
|
||||||
|
"post_dump",
|
||||||
|
"pre_load",
|
||||||
|
# "post_load",
|
||||||
|
"pprint",
|
||||||
|
"ValidationError",
|
||||||
|
"missing",
|
||||||
|
]
|
||||||
|
|
||||||
|
from marshmallow import fields
|
||||||
|
|
||||||
|
from marshmallow.decorators import (
|
||||||
|
post_dump,
|
||||||
|
# post_load, # overloaded
|
||||||
|
pre_dump,
|
||||||
|
pre_load,
|
||||||
|
validates,
|
||||||
|
validates_schema,
|
||||||
|
)
|
||||||
|
from marshmallow.exceptions import ValidationError
|
||||||
|
from marshmallow.schema import Schema, SchemaOpts
|
||||||
|
from marshmallow.utils import EXCLUDE, INCLUDE, RAISE, missing, pprint
|
||||||
|
|
||||||
|
from marshmallow_generic.decorators import post_load
|
||||||
|
from marshmallow_generic.schema import GenericSchema
|
||||||
|
@ -54,10 +54,10 @@ class GenericSchema(GenericInsightMixin1[Model], Schema):
|
|||||||
dump_only: Union[Sequence[str], set[str]] = (),
|
dump_only: Union[Sequence[str], set[str]] = (),
|
||||||
partial: Union[bool, Sequence[str], set[str]] = False,
|
partial: Union[bool, Sequence[str], set[str]] = False,
|
||||||
unknown: Optional[str] = None,
|
unknown: Optional[str] = None,
|
||||||
many: Optional[bool] = None,
|
many: bool = False, # usage discouraged
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Emits a warning, if the `many` argument is not `None`.
|
Emits a warning, if the `many` argument is not `False`.
|
||||||
|
|
||||||
Otherwise the same as in [`marshmallow.Schema`][marshmallow.Schema].
|
Otherwise the same as in [`marshmallow.Schema`][marshmallow.Schema].
|
||||||
|
|
||||||
@ -72,40 +72,34 @@ class GenericSchema(GenericInsightMixin1[Model], Schema):
|
|||||||
it is not used. Nested fields can be represented with dot
|
it is not used. Nested fields can be represented with dot
|
||||||
delimiters.
|
delimiters.
|
||||||
context:
|
context:
|
||||||
Optional context passed to [`Method`]
|
Optional context passed to
|
||||||
[marshmallow.fields.Method] and [`Function`]
|
[`Method`][marshmallow.fields.Method] and
|
||||||
[marshmallow.fields.Function] fields.
|
[`Function`][marshmallow.fields.Function] fields.
|
||||||
load_only:
|
load_only:
|
||||||
Fields to skip during serialization (write-only fields)
|
Fields to skip during serialization (write-only fields)
|
||||||
dump_only:
|
dump_only:
|
||||||
Fields to skip during deserialization (read-only fields)
|
Fields to skip during deserialization (read-only fields)
|
||||||
partial:
|
partial:
|
||||||
Whether to ignore missing fields and not require any fields
|
Whether to ignore missing fields and not require any fields
|
||||||
declared. Propagates down to [`Nested`]
|
declared. Propagates down to
|
||||||
[marshmallow.fields.Nested] fields as well. If its value is an
|
[`Nested`][marshmallow.fields.Nested] fields as well. If its
|
||||||
iterable, only missing fields listed in that iterable will be
|
value is an iterable, only missing fields listed in that
|
||||||
ignored. Use dot delimiters to specify nested fields.
|
iterable will be ignored. Use dot delimiters to specify nested
|
||||||
|
fields.
|
||||||
unknown:
|
unknown:
|
||||||
Whether to exclude, include, or raise an error for unknown
|
Whether to exclude, include, or raise an error for unknown
|
||||||
fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`.
|
fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`.
|
||||||
many:
|
many:
|
||||||
!!! warning
|
!!! warning
|
||||||
Specifying this option schema-wide undermines the type
|
Changing this option schema-wide undermines the type
|
||||||
safety that this class aims to provide and passing any
|
safety that this class aims to provide. Passing `True`
|
||||||
value other than `None` will trigger a warning. Use the
|
will therefore trigger a warning. You should instead use
|
||||||
method-specific `many` parameter, when calling
|
the method-specific `many` parameter, when calling
|
||||||
[`dump`][marshmallow_generic.GenericSchema.dump]/
|
[`dump`][marshmallow_generic.GenericSchema.dump]/
|
||||||
[`dumps`][marshmallow_generic.GenericSchema.dumps] or
|
[`dumps`][marshmallow_generic.GenericSchema.dumps] or
|
||||||
[`load`][marshmallow_generic.GenericSchema.load]/
|
[`load`][marshmallow_generic.GenericSchema.load]/
|
||||||
[`loads`][marshmallow_generic.GenericSchema.loads] instead.
|
[`loads`][marshmallow_generic.GenericSchema.loads].
|
||||||
"""
|
"""
|
||||||
if many is not None:
|
|
||||||
warn(
|
|
||||||
"Setting `many` schema-wide breaks type safety. Use the the "
|
|
||||||
"`many` parameter of specific methods (like `load`) instead."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
many = bool(many)
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
only=only,
|
only=only,
|
||||||
exclude=exclude,
|
exclude=exclude,
|
||||||
@ -117,13 +111,28 @@ class GenericSchema(GenericInsightMixin1[Model], Schema):
|
|||||||
unknown=unknown,
|
unknown=unknown,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __setattr__(self, name: str, value: Any) -> None:
|
||||||
|
"""
|
||||||
|
Warns, when trying to set `many` to anything other than `False`.
|
||||||
|
|
||||||
|
Otherwise the same the normal
|
||||||
|
[`object.__setattr__`](https://docs.python.org/3/reference/datamodel.html#object.__setattr__).
|
||||||
|
"""
|
||||||
|
if name == "many" and value is not False:
|
||||||
|
warn(
|
||||||
|
"Changing `many` schema-wide breaks type safety. Use the the "
|
||||||
|
"`many` parameter of specific methods (like `load`) instead."
|
||||||
|
)
|
||||||
|
super().__setattr__(name, value)
|
||||||
|
|
||||||
@post_load
|
@post_load
|
||||||
def instantiate(self, data: dict[str, Any], **_kwargs: Any) -> Model:
|
def instantiate(self, data: dict[str, Any], **_kwargs: Any) -> Model:
|
||||||
"""
|
"""
|
||||||
Unpacks `data` into the constructor of the specified **`Model`**.
|
Unpacks `data` into the constructor of the specified **`Model`**.
|
||||||
|
|
||||||
Registered as a [`@post_load`]
|
Registered as a
|
||||||
[marshmallow_generic.decorators.post_load] hook for the schema.
|
[`@post_load`][marshmallow_generic.decorators.post_load]
|
||||||
|
hook for the schema.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
You should probably not use this method directly. No parsing,
|
You should probably not use this method directly. No parsing,
|
||||||
@ -169,8 +178,9 @@ class GenericSchema(GenericInsightMixin1[Model], Schema):
|
|||||||
"""
|
"""
|
||||||
Serializes **`Model`** objects to native Python data types.
|
Serializes **`Model`** objects to native Python data types.
|
||||||
|
|
||||||
Same as [`marshmallow.Schema.dump`]
|
Same as
|
||||||
[marshmallow.schema.Schema.dump] at runtime.
|
[`marshmallow.Schema.dump`][marshmallow.schema.Schema.dump]
|
||||||
|
at runtime.
|
||||||
|
|
||||||
Annotations ensure that type checkers will infer the return type
|
Annotations ensure that type checkers will infer the return type
|
||||||
correctly based on the `many` argument, and also enforce the `obj`
|
correctly based on the `many` argument, and also enforce the `obj`
|
||||||
@ -254,10 +264,10 @@ class GenericSchema(GenericInsightMixin1[Model], Schema):
|
|||||||
"""
|
"""
|
||||||
Deserializes data to objects of the specified **`Model`** class.
|
Deserializes data to objects of the specified **`Model`** class.
|
||||||
|
|
||||||
Same as [`marshmallow.Schema.load`]
|
Same as
|
||||||
[marshmallow.schema.Schema.load] at runtime, but data will always
|
[`marshmallow.Schema.load`][marshmallow.schema.Schema.load] at
|
||||||
pass through the [`instantiate`]
|
runtime, but data will always pass through the
|
||||||
[marshmallow_generic.schema.GenericSchema.instantiate]
|
[`instantiate`][marshmallow_generic.schema.GenericSchema.instantiate]
|
||||||
hook after deserialization.
|
hook after deserialization.
|
||||||
|
|
||||||
Annotations ensure that type checkers will infer the return type
|
Annotations ensure that type checkers will infer the return type
|
||||||
@ -271,11 +281,11 @@ class GenericSchema(GenericInsightMixin1[Model], Schema):
|
|||||||
the value for `self.many` is used.
|
the value for `self.many` is used.
|
||||||
partial:
|
partial:
|
||||||
Whether to ignore missing fields and not require any
|
Whether to ignore missing fields and not require any
|
||||||
fields declared. Propagates down to [`Nested`]
|
fields declared. Propagates down to
|
||||||
[marshmallow.fields.Nested] fields as well. If its value
|
[`Nested`][marshmallow.fields.Nested] fields as well. If
|
||||||
is an iterable, only missing fields listed in that
|
its value is an iterable, only missing fields listed in
|
||||||
iterable will be ignored. Use dot delimiters to specify
|
that iterable will be ignored. Use dot delimiters to
|
||||||
nested fields.
|
specify nested fields.
|
||||||
unknown:
|
unknown:
|
||||||
Whether to exclude, include, or raise an error for unknown
|
Whether to exclude, include, or raise an error for unknown
|
||||||
fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`.
|
fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`.
|
||||||
@ -323,10 +333,10 @@ class GenericSchema(GenericInsightMixin1[Model], Schema):
|
|||||||
"""
|
"""
|
||||||
Deserializes data to objects of the specified **`Model`** class.
|
Deserializes data to objects of the specified **`Model`** class.
|
||||||
|
|
||||||
Same as [`marshmallow.Schema.loads`]
|
Same as
|
||||||
[marshmallow.schema.Schema.loads] at runtime, but data will always
|
[`marshmallow.Schema.loads`][marshmallow.schema.Schema.loads] at
|
||||||
pass through the [`instantiate`]
|
runtime, but data will always pass through the
|
||||||
[marshmallow_generic.schema.GenericSchema.instantiate]
|
[`instantiate`][marshmallow_generic.schema.GenericSchema.instantiate]
|
||||||
hook after deserialization.
|
hook after deserialization.
|
||||||
|
|
||||||
Annotations ensure that type checkers will infer the return type
|
Annotations ensure that type checkers will infer the return type
|
||||||
@ -340,11 +350,11 @@ class GenericSchema(GenericInsightMixin1[Model], Schema):
|
|||||||
the value for `self.many` is used.
|
the value for `self.many` is used.
|
||||||
partial:
|
partial:
|
||||||
Whether to ignore missing fields and not require any
|
Whether to ignore missing fields and not require any
|
||||||
fields declared. Propagates down to [`Nested`]
|
fields declared. Propagates down to
|
||||||
[marshmallow.fields.Nested] fields as well. If its value
|
[`Nested`][marshmallow.fields.Nested] fields as well. If
|
||||||
is an iterable, only missing fields listed in that
|
its value is an iterable, only missing fields listed in
|
||||||
iterable will be ignored. Use dot delimiters to specify
|
that iterable will be ignored. Use dot delimiters to
|
||||||
nested fields.
|
specify nested fields.
|
||||||
unknown:
|
unknown:
|
||||||
Whether to exclude, include, or raise an error for unknown
|
Whether to exclude, include, or raise an error for unknown
|
||||||
fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`.
|
fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`.
|
||||||
|
@ -19,16 +19,20 @@ class GenericSchemaTestCase(TestCase):
|
|||||||
"dump_only": object(),
|
"dump_only": object(),
|
||||||
"partial": object(),
|
"partial": object(),
|
||||||
"unknown": object(),
|
"unknown": object(),
|
||||||
"many": None,
|
"many": object(),
|
||||||
}
|
}
|
||||||
schema.GenericSchema[Foo](**kwargs)
|
schema.GenericSchema[Foo](**kwargs)
|
||||||
mock_super_init.assert_called_once_with(**kwargs | {"many": False})
|
|
||||||
mock_super_init.reset_mock()
|
|
||||||
kwargs["many"] = True
|
|
||||||
with self.assertWarns(UserWarning):
|
|
||||||
schema.GenericSchema[Foo](**kwargs)
|
|
||||||
mock_super_init.assert_called_once_with(**kwargs)
|
mock_super_init.assert_called_once_with(**kwargs)
|
||||||
|
|
||||||
|
def test___setattr__(self) -> None:
|
||||||
|
class Foo:
|
||||||
|
pass
|
||||||
|
|
||||||
|
obj = schema.GenericSchema[Foo]()
|
||||||
|
with self.assertWarns(UserWarning):
|
||||||
|
obj.many = new = MagicMock()
|
||||||
|
self.assertIs(new, obj.many)
|
||||||
|
|
||||||
@patch.object(_util.GenericInsightMixin, "_get_type_arg")
|
@patch.object(_util.GenericInsightMixin, "_get_type_arg")
|
||||||
def test_instantiate(self, mock__get_type_arg: MagicMock) -> None:
|
def test_instantiate(self, mock__get_type_arg: MagicMock) -> None:
|
||||||
mock__get_type_arg.return_value = mock_cls = MagicMock()
|
mock__get_type_arg.return_value = mock_cls = MagicMock()
|
||||||
|
Loading…
Reference in New Issue
Block a user