From dde31c939e41137eaff4fed586eab2dbe00daec0 Mon Sep 17 00:00:00 2001 From: Jeff Dairiki Date: Fri, 23 Sep 2022 15:16:44 -0700 Subject: [PATCH 1/3] Add .load_to_mapping method to base schema This essentially does a raw marshmallow.Schema.load without converting any values to dataclasses. The main point is to allow doing loads with `partial=True`. --- marshmallow_dataclass/__init__.py | 29 +++++++++++++++++++ tests/test_load_to_mapping.py | 47 +++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 tests/test_load_to_mapping.py diff --git a/marshmallow_dataclass/__init__.py b/marshmallow_dataclass/__init__.py index c7b1e0a..5a817ed 100644 --- a/marshmallow_dataclass/__init__.py +++ b/marshmallow_dataclass/__init__.py @@ -40,6 +40,7 @@ class User: import threading import types import warnings +from contextlib import contextmanager from enum import EnumMeta from functools import lru_cache, partial from typing import ( @@ -733,6 +734,28 @@ def field_for_schema( return marshmallow.fields.Nested(nested, **metadata) +class _ThreadLocalBool(threading.local): + """A thread-local boolean flag.""" + + def __init__(self, value: bool = False): + self.value = value + + def __bool__(self): + return self.value + + @contextmanager + def temporarily(self, value: bool): + orig = self.value + self.value = value + try: + yield self + finally: + self.value = orig + + +_disable_magic = _ThreadLocalBool(False) + + def _base_schema( clazz: type, base_schema: Optional[Type[marshmallow.Schema]] = None ) -> Type[marshmallow.Schema]: @@ -746,12 +769,18 @@ def _base_schema( class BaseSchema(base_schema or marshmallow.Schema): # type: ignore def load(self, data: Mapping, *, many: bool = None, **kwargs): all_loaded = super().load(data, many=many, **kwargs) + if _disable_magic: + return all_loaded many = self.many if many is None else bool(many) if many: return [clazz(**loaded) for loaded in all_loaded] else: return clazz(**all_loaded) + def load_to_mapping(self, data: Mapping, **kwargs) -> Mapping[str, Any]: + with _disable_magic.temporarily(True): + return self.load(data, **kwargs) + return BaseSchema diff --git a/tests/test_load_to_mapping.py b/tests/test_load_to_mapping.py new file mode 100644 index 0000000..e941078 --- /dev/null +++ b/tests/test_load_to_mapping.py @@ -0,0 +1,47 @@ +import dataclasses +import unittest + +from marshmallow_dataclass import class_schema + + +class Test_Schema_load_to_mapping(unittest.TestCase): + def test_simple(self): + @dataclasses.dataclass + class Simple: + one: int = dataclasses.field() + two: str = dataclasses.field() + + simple_schema = class_schema(Simple)() + assert simple_schema.load_to_mapping({"one": "1", "two": "b"}) == { + "one": 1, + "two": "b", + } + + def test_partial(self): + @dataclasses.dataclass + class Simple: + one: int = dataclasses.field() + two: str = dataclasses.field() + + simple_schema = class_schema(Simple)() + assert simple_schema.load_to_mapping({"one": "1"}, partial=True) == {"one": 1} + + def test_nested(self): + @dataclasses.dataclass + class Simple: + one: int = dataclasses.field() + two: str = dataclasses.field() + + @dataclasses.dataclass + class Nested: + x: str = dataclasses.field() + child: Simple = dataclasses.field() + + nested_schema = class_schema(Nested)() + assert nested_schema.load_to_mapping({"child": {"one": "1"}}, partial=True) == { + "child": {"one": 1}, + } + + +if __name__ == "__main__": + unittest.main() From 07e7486d54c4d67d7ee3d72eacd2ad6be6cf9fc6 Mon Sep 17 00:00:00 2001 From: Jeff Dairiki Date: Sat, 24 Sep 2022 13:28:36 -0700 Subject: [PATCH 2/3] =?UTF-8?q?Rename=20load=5Fto=5Fmapping=20=E2=86=92=20?= =?UTF-8?q?load=5Fto=5Fdict?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- marshmallow_dataclass/__init__.py | 4 +++- tests/{test_load_to_mapping.py => test_load_to_dict.py} | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) rename tests/{test_load_to_mapping.py => test_load_to_dict.py} (76%) diff --git a/marshmallow_dataclass/__init__.py b/marshmallow_dataclass/__init__.py index 5a817ed..b7c57ae 100644 --- a/marshmallow_dataclass/__init__.py +++ b/marshmallow_dataclass/__init__.py @@ -777,7 +777,9 @@ def load(self, data: Mapping, *, many: bool = None, **kwargs): else: return clazz(**all_loaded) - def load_to_mapping(self, data: Mapping, **kwargs) -> Mapping[str, Any]: + def load_to_dict( + self, data: Mapping, **kwargs + ) -> Union[Dict[str, Any], List[Dict[str, Any]]]: with _disable_magic.temporarily(True): return self.load(data, **kwargs) diff --git a/tests/test_load_to_mapping.py b/tests/test_load_to_dict.py similarity index 76% rename from tests/test_load_to_mapping.py rename to tests/test_load_to_dict.py index e941078..817ffd4 100644 --- a/tests/test_load_to_mapping.py +++ b/tests/test_load_to_dict.py @@ -4,7 +4,7 @@ from marshmallow_dataclass import class_schema -class Test_Schema_load_to_mapping(unittest.TestCase): +class Test_Schema_load_to_dict(unittest.TestCase): def test_simple(self): @dataclasses.dataclass class Simple: @@ -12,7 +12,7 @@ class Simple: two: str = dataclasses.field() simple_schema = class_schema(Simple)() - assert simple_schema.load_to_mapping({"one": "1", "two": "b"}) == { + assert simple_schema.load_to_dict({"one": "1", "two": "b"}) == { "one": 1, "two": "b", } @@ -24,7 +24,7 @@ class Simple: two: str = dataclasses.field() simple_schema = class_schema(Simple)() - assert simple_schema.load_to_mapping({"one": "1"}, partial=True) == {"one": 1} + assert simple_schema.load_to_dict({"one": "1"}, partial=True) == {"one": 1} def test_nested(self): @dataclasses.dataclass @@ -38,7 +38,7 @@ class Nested: child: Simple = dataclasses.field() nested_schema = class_schema(Nested)() - assert nested_schema.load_to_mapping({"child": {"one": "1"}}, partial=True) == { + assert nested_schema.load_to_dict({"child": {"one": "1"}}, partial=True) == { "child": {"one": 1}, } From b33ecbfb30495d4a9d6556e955910fdfc965ca1a Mon Sep 17 00:00:00 2001 From: Jeff Dairiki Date: Sat, 24 Sep 2022 13:30:54 -0700 Subject: [PATCH 3/3] Document new load_to_dict method --- README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/README.md b/README.md index 2079155..4482db3 100644 --- a/README.md +++ b/README.md @@ -304,6 +304,36 @@ class Point: ordered = True ``` +### Partial Loading + +The `.load` method of a `marshmallow_dataclass` schema can not be used to load partial data. +This is because `load` always wraps the deserialized data in the target dataclass type, +and it is not possible to partially construct a dataclass. + +However `marshmallow_dataclass` schema now provide a `load_to_dict` method that can be +used to deserialize data with `partial=True`. `Load_to_dict` works just like +the plain `marshmallow.Schema.load` method. It returns the deserialized data as a `dict` +(or a list of `dict`s if `many=True`) — no construction of dataclasses is done. + +```pycon +from marshmallow_dataclass import dataclass + + +@dataclass +class Person: + first_name: str + last_name: str + + +>>> Person.Schema().load_to_dict({"first_name": "Joe"}, partial=True) +# => {"first_name": "Joe"} + +>>> Person.Schema().load({"first_name": "Joe"}, partial=True) +# => Traceback (most recent call last): +# ... +# TypeError: Person.__init__() missing 1 required positional argument: 'last_name' +``` + ## Documentation The project documentation is hosted on GitHub Pages: https://lovasoa.github.io/marshmallow_dataclass/