From a3342fd382af5b0eeb2732688866cb964249253e Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Sun, 8 Sep 2019 14:28:24 -0400 Subject: [PATCH 01/11] Allow passing lambda functions to `Nested` --- src/marshmallow/fields.py | 66 +++++++++++++++++++++++---------------- tests/test_schema.py | 57 ++++++++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 32 deletions(-) diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py index 527583985..8bd06a1b5 100644 --- a/src/marshmallow/fields.py +++ b/src/marshmallow/fields.py @@ -7,6 +7,7 @@ import uuid import decimal import math +import typing import warnings from collections.abc import Mapping as _Mapping @@ -407,27 +408,25 @@ class Nested(Field): Examples: :: - user = fields.Nested(UserSchema) - user2 = fields.Nested('UserSchema') # Equivalent to above - collaborators = fields.Nested(UserSchema, many=True, only=('id',)) - parent = fields.Nested('self') + class ChildSchema(Schema): + id = fields.Str() + name = fields.Str() + # Use lambda functions when you need two-way nesting or self-nesting + parent = fields.Nested(lambda: ParentSchema(only=("id",)), dump_only=True) + siblings = fields.List(fields.Nested(lambda: ChildSchema(only=("id", "name")))) + + class ParentSchema(Schema): + id = fields.Str() + children = fields.List( + fields.Nested(ChildSchema(only=("id", "parent", "siblings"))) + ) + spouse = fields.Nested(lambda: ParentSchema(only=("id",))) When passing a `Schema ` instance as the first argument, the instance's ``exclude``, ``only``, and ``many`` attributes will be respected. - Therefore, when passing the ``exclude``, ``only``, or ``many`` arguments to `fields.Nested`, - you should pass a `Schema ` class (not an instance) as the first argument. - - :: - - # Yes - author = fields.Nested(UserSchema, only=('id', 'name')) - - # No - author = fields.Nested(UserSchema(), only=('id', 'name')) - - :param Schema nested: The Schema class or class name (string) - to nest, or ``"self"`` to nest the :class:`Schema` within itself. + :param Schema nested: A `Schema` class, `Schema` instance, or class name (string) + to nest, or a callable that returns a `Schema` instance. :param tuple exclude: A list or tuple of fields to exclude. :param only: A list or tuple of fields to marshal. If `None`, all fields are marshalled. This parameter takes precedence over ``exclude``. @@ -440,7 +439,15 @@ class Nested(Field): default_error_messages = {"type": "Invalid type."} def __init__( - self, nested, *, default=missing_, exclude=tuple(), only=None, **kwargs + self, + nested: typing.Union[ + SchemaABC, typing.Type[SchemaABC], str, typing.Callable[[], SchemaABC] + ], + *, + default=missing_, + exclude=tuple(), + only=None, + **kwargs ): # Raise error if only or exclude is passed as string, not list of strings if only is not None and not is_collection(only): @@ -458,7 +465,7 @@ def __init__( super().__init__(default=default, **kwargs) @property - def schema(self): + def schema(self) -> SchemaABC: """The nested Schema object. .. versionchanged:: 1.0.0 @@ -467,24 +474,29 @@ def schema(self): if not self._schema: # Inherit context from parent. context = getattr(self.parent, "context", {}) - if isinstance(self.nested, SchemaABC): - self._schema = self.nested + if callable(self.nested) and not isinstance(self.nested, type): + nested = self.nested() + else: + nested = self.nested + + if isinstance(nested, SchemaABC): + self._schema = nested self._schema.context.update(context) else: - if isinstance(self.nested, type) and issubclass(self.nested, SchemaABC): - schema_class = self.nested - elif not isinstance(self.nested, (str, bytes)): + if isinstance(nested, type) and issubclass(nested, SchemaABC): + schema_class = nested + elif not isinstance(nested, (str, bytes)): raise ValueError( "Nested fields must be passed a " - "Schema, not {}.".format(self.nested.__class__) + "Schema, not {}.".format(nested.__class__) ) - elif self.nested == "self": + elif nested == "self": ret = self while not isinstance(ret, SchemaABC): ret = ret.parent schema_class = ret.__class__ else: - schema_class = class_registry.get_class(self.nested) + schema_class = class_registry.get_class(nested) self._schema = schema_class( many=self.many, only=self.only, diff --git a/tests/test_schema.py b/tests/test_schema.py index c492088de..ac248ef73 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -1081,11 +1081,9 @@ class Meta: def test_nested_custom_set_not_implementing_getitem(): - """ - This test checks that Marshmallow can serialize implementations of - :mod:`collections.abc.MutableSequence`, with ``__getitem__`` arguments - that are not integers. - """ + # This test checks that marshmallow can serialize implementations of + # :mod:`collections.abc.MutableSequence`, with ``__getitem__`` arguments + # that are not integers. class ListLikeParent: """ @@ -1171,6 +1169,55 @@ class ParentSchema(Schema): assert "bah" not in grand_child +def test_nested_lambda(): + class ChildSchema(Schema): + id = fields.Str() + name = fields.Str() + parent = fields.Nested(lambda: ParentSchema(only=("id",)), dump_only=True) + siblings = fields.List(fields.Nested(lambda: ChildSchema(only=("id", "name")))) + + class ParentSchema(Schema): + id = fields.Str() + spouse = fields.Nested(lambda: ParentSchema(only=("id",))) + children = fields.List( + fields.Nested(lambda: ChildSchema(only=("id", "parent", "siblings"))) + ) + + sch = ParentSchema() + data_to_load = { + "id": "p1", + "spouse": {"id": "p2"}, + "children": [{"id": "c1", "siblings": [{"id": "c2", "name": "sis"}]}], + } + loaded = sch.load(data_to_load) + assert loaded == data_to_load + + data_to_dump = dict( + id="p2", + spouse=dict(id="p2"), + children=[ + dict( + id="c1", + name="bar", + parent=dict(id="p2"), + siblings=[dict(id="c2", name="sis")], + ) + ], + ) + dumped = sch.dump(data_to_dump) + assert dumped == { + "id": "p2", + "spouse": {"id": "p2"}, + "children": [ + { + "id": "c1", + "parent": {"id": "p2"}, + "siblings": [{"id": "c2", "name": "sis"}], + } + ], + } + + @pytest.mark.parametrize("data_key", ("f1", "f5", None)) def test_data_key_collision(data_key): class MySchema(Schema): From c52e021c8240f421d7492c200c75a3fd4ca471e0 Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Sun, 8 Sep 2019 15:10:44 -0400 Subject: [PATCH 02/11] Deprecate only, exclude, many, and unknown params of Nested --- src/marshmallow/fields.py | 67 ++++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 11 deletions(-) diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py index 8bd06a1b5..131ac7091 100644 --- a/src/marshmallow/fields.py +++ b/src/marshmallow/fields.py @@ -434,6 +434,11 @@ class ParentSchema(Schema): :param unknown: Whether to exclude, include, or raise an error for unknown fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`. :param kwargs: The same keyword arguments that :class:`Field` receives. + + .. versionchanged:: 3.1.0 + Deprecated ``only``, ``exclude``, and ``unknown`` parameters. + Pass these to the schema instance instead. + ``many`` was also deprecated in favor of the ``List(Nested(...))`` usage. """ default_error_messages = {"type": "Invalid type."} @@ -445,23 +450,63 @@ def __init__( ], *, default=missing_, - exclude=tuple(), - only=None, **kwargs ): - # Raise error if only or exclude is passed as string, not list of strings - if only is not None and not is_collection(only): - raise StringNotCollectionError('"only" should be a collection of strings.') - if exclude is not None and not is_collection(exclude): - raise StringNotCollectionError( - '"exclude" should be a collection of strings.' + # Raise DeprecationWarnings for only, exclude, many, and unknown + if "only" in kwargs: + only = kwargs.pop("only") + if not is_collection(only): + raise StringNotCollectionError( + '"only" should be a collection of strings.' + ) + warnings.warn( + "Passing `only` to `Nested` is deprecated. " + "Pass `only` to the schema instance instead.", + DeprecationWarning, + ) + else: + only = None + if "exclude" in kwargs: + exclude = kwargs.pop("exclude") + if not is_collection(exclude): + raise StringNotCollectionError( + '"exclude" should be a collection of strings.' + ) + warnings.warn( + "Passing `exclude` to `Nested` is deprecated. " + "Pass `only` to the schema instance instead.", + DeprecationWarning, + ) + else: + exclude = tuple() + + if "many" in kwargs: + many = kwargs.pop("many") + warnings.warn( + "Passing `many` to `Nested` is deprecated. " + "Use List(Nested(...)) instead.", + DeprecationWarning, ) + else: + many = False + + if "unknown" in kwargs: + unknown = kwargs.pop("unknown") + warnings.warn( + "Passing `unknown` to `Nested` is deprecated. " + "Pass `unknown` to the schema instance instead.", + DeprecationWarning, + ) + else: + unknown = None + self.nested = nested + self._schema = None # Cached Schema instance + # Deprecated attributes self.only = only self.exclude = exclude - self.many = kwargs.get("many", False) - self.unknown = kwargs.get("unknown") - self._schema = None # Cached Schema instance + self.many = many + self.unknown = unknown super().__init__(default=default, **kwargs) @property From 05c95c5ae0f10d0c583e843134f8bc8e18465684 Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Sun, 8 Sep 2019 15:29:06 -0400 Subject: [PATCH 03/11] Deprecate passing `self` to Nested --- src/marshmallow/fields.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py index 131ac7091..02310f019 100644 --- a/src/marshmallow/fields.py +++ b/src/marshmallow/fields.py @@ -536,10 +536,14 @@ def schema(self) -> SchemaABC: "Schema, not {}.".format(nested.__class__) ) elif nested == "self": - ret = self - while not isinstance(ret, SchemaABC): - ret = ret.parent - schema_class = ret.__class__ + schema_class = self.root.__class__ + warnings.warn( + "Passing 'self' to `Nested` is deprecated. " + "Use `Nested(lambda: {Class}(...))` instead.".format( + Class=schema_class.__name__ + ), + DeprecationWarning, + ) else: schema_class = class_registry.get_class(nested) self._schema = schema_class( From fb84c5c1583e7628497b3909ec7dbc8f2452efbe Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Sun, 8 Sep 2019 15:33:47 -0400 Subject: [PATCH 04/11] Minor consistency fix --- src/marshmallow/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py index 02310f019..ca9568fe3 100644 --- a/src/marshmallow/fields.py +++ b/src/marshmallow/fields.py @@ -532,8 +532,8 @@ def schema(self) -> SchemaABC: schema_class = nested elif not isinstance(nested, (str, bytes)): raise ValueError( - "Nested fields must be passed a " - "Schema, not {}.".format(nested.__class__) + "`Nested` fields must be passed a " + "`Schema`, not {}.".format(nested.__class__) ) elif nested == "self": schema_class = self.root.__class__ From ec9157ff204b0da84ca17d2fef048b9e5b2e61f8 Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Thu, 12 Sep 2019 21:36:48 -0400 Subject: [PATCH 05/11] Remove `many` from test schemas; add test for merging only and exclude --- src/marshmallow/fields.py | 2 +- tests/base.py | 8 ++++---- tests/test_fields.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py index 1d4199ef7..53c2712a7 100644 --- a/src/marshmallow/fields.py +++ b/src/marshmallow/fields.py @@ -478,7 +478,7 @@ def __init__( DeprecationWarning, ) else: - exclude = tuple() + exclude = () if "many" in kwargs: many = kwargs.pop("many") diff --git a/tests/base.py b/tests/base.py index b1661f3d5..db3a94ab7 100644 --- a/tests/base.py +++ b/tests/base.py @@ -275,21 +275,21 @@ class UserRelativeUrlSchema(UserSchema): class BlogSchema(Schema): title = fields.String() user = fields.Nested(UserSchema) - collaborators = fields.Nested(UserSchema, many=True) + collaborators = fields.List(fields.Nested(UserSchema())) categories = fields.List(fields.String) id = fields.String() class BlogUserMetaSchema(Schema): user = fields.Nested(UserMetaSchema()) - collaborators = fields.Nested(UserMetaSchema, many=True) + collaborators = fields.List(fields.Nested(UserMetaSchema())) class BlogSchemaMeta(Schema): """Same as BlogSerializer but using ``fields`` options.""" user = fields.Nested(UserSchema) - collaborators = fields.Nested(UserSchema, many=True) + collaborators = fields.List(fields.Nested(UserSchema())) class Meta: fields = ("title", "user", "collaborators", "categories", "id") @@ -298,7 +298,7 @@ class Meta: class BlogOnlySchema(Schema): title = fields.String() user = fields.Nested(UserSchema) - collaborators = fields.Nested(UserSchema, only=("id",), many=True) + collaborators = fields.List(fields.Nested(UserSchema(only=("id",)))) class BlogSchemaExclude(BlogSchema): diff --git a/tests/test_fields.py b/tests/test_fields.py index f4cb3bd24..2ec7596b7 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -377,6 +377,34 @@ class Family(Schema): "children": [{"name": "Lily"}] } + @pytest.mark.parametrize( + ("param", "expected_attribute", "expected_dump"), + ( + ("only", {"name"}, {"children": [{"name": "Lily"}]}), + ("exclude", {"name", "surname", "age"}, {"children": [{}]}), + ), + ) + def test_list_nested_lambda_only_and_exclude_merged_with_nested( + self, param, expected_attribute, expected_dump + ): + class Child(Schema): + name = fields.String() + surname = fields.String() + age = fields.Integer() + + class Family(Schema): + children = fields.List( + fields.Nested(lambda: Child(**{param: ("name", "surname")})) + ) + + schema = Family(**{param: ["children.name", "children.age"]}) + assert ( + getattr(schema.fields["children"].inner.schema, param) == expected_attribute + ) + + family = {"children": [{"name": "Lily", "surname": "Martinez", "age": 15}]} + assert schema.dump(family) == expected_dump + def test_list_nested_partial_propagated_to_nested(self): class Child(Schema): name = fields.String(required=True) From c556772b784a3438c5fbc6bd9047e4974bf87ec8 Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Thu, 12 Sep 2019 23:35:13 -0400 Subject: [PATCH 06/11] Revert `only` and `exclude` deprecation; move 'self' deprecation; more tests --- src/marshmallow/fields.py | 64 +++++------------------ tests/test_schema.py | 106 +++++++++++++++++++++++++++++++++----- 2 files changed, 105 insertions(+), 65 deletions(-) diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py index 750f3a06b..2a73b5281 100644 --- a/src/marshmallow/fields.py +++ b/src/marshmallow/fields.py @@ -449,57 +449,26 @@ def __init__( SchemaABC, typing.Type[SchemaABC], str, typing.Callable[[], SchemaABC] ], *, + exclude: typing.Sequence = (), + only: typing.Sequence = None, default=missing_, + many: bool = False, + unknown: str = None, **kwargs ): - # Raise DeprecationWarnings for only, exclude, many, and unknown - if "only" in kwargs: - only = kwargs.pop("only") - if not is_collection(only): - raise StringNotCollectionError( - '"only" should be a collection of strings.' - ) - warnings.warn( - "Passing `only` to `Nested` is deprecated. " - "Pass `only` to the schema instance instead.", - DeprecationWarning, - ) - else: - only = None - if "exclude" in kwargs: - exclude = kwargs.pop("exclude") - if not is_collection(exclude): - raise StringNotCollectionError( - '"exclude" should be a collection of strings.' - ) - warnings.warn( - "Passing `exclude` to `Nested` is deprecated. " - "Pass `only` to the schema instance instead.", - DeprecationWarning, - ) - else: - exclude = () - - if "many" in kwargs: - many = kwargs.pop("many") - warnings.warn( - "Passing `many` to `Nested` is deprecated. " - "Use List(Nested(...)) instead.", - DeprecationWarning, + # Raise error if only or exclude is passed as string, not list of strings + if only is not None and not is_collection(only): + raise StringNotCollectionError('"only" should be a collection of strings.') + if exclude is not None and not is_collection(exclude): + raise StringNotCollectionError( + '"exclude" should be a collection of strings.' ) - else: - many = False - - if "unknown" in kwargs: - unknown = kwargs.pop("unknown") + if nested == "self": warnings.warn( - "Passing `unknown` to `Nested` is deprecated. " - "Pass `unknown` to the schema instance instead.", + "Passing 'self' to `Nested` is deprecated. " + "Use `Nested(lambda: MySchema(...))` instead.", DeprecationWarning, ) - else: - unknown = None - self.nested = nested self._schema = None # Cached Schema instance # Deprecated attributes @@ -549,13 +518,6 @@ def schema(self) -> SchemaABC: ) elif nested == "self": schema_class = self.root.__class__ - warnings.warn( - "Passing 'self' to `Nested` is deprecated. " - "Use `Nested(lambda: {Class}(...))` instead.".format( - Class=schema_class.__name__ - ), - DeprecationWarning, - ) else: schema_class = class_registry.get_class(nested) self._schema = schema_class( diff --git a/tests/test_schema.py b/tests/test_schema.py index 8a28ee9ff..60c3625e2 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -2210,13 +2210,15 @@ def employer(self): def user(self, employer): return User(name="Tom", employer=employer, age=28) - def test_nesting_schema_within_itself(self, user, employer): - class SelfSchema(Schema): - name = fields.String() - age = fields.Integer() - employer = fields.Nested("self", exclude=("employer",)) + def test_nesting_schema_by_passing_lambda(self, user, employer): + class SelfReferencingSchema(Schema): + name = fields.Str() + age = fields.Int() + employer = fields.Nested( + lambda: SelfReferencingSchema(exclude=("employer",)) + ) - data = SelfSchema().dump(user) + data = SelfReferencingSchema().dump(user) assert data["name"] == user.name assert data["employer"]["name"] == employer.name assert data["employer"]["age"] == employer.age @@ -2232,9 +2234,24 @@ class SelfReferencingSchema(Schema): assert data["employer"]["name"] == employer.name assert data["employer"]["age"] == employer.age + def test_nesting_schema_self_string(self, user, employer): + with pytest.warns( + DeprecationWarning, match="Passing 'self' to `Nested` is deprecated" + ): + + class SelfSchema(Schema): + name = fields.String() + age = fields.Integer() + employer = fields.Nested("self", exclude=("employer",)) + + data = SelfSchema().dump(user) + assert data["name"] == user.name + assert data["employer"]["name"] == employer.name + assert data["employer"]["age"] == employer.age + def test_nesting_within_itself_meta(self, user, employer): class SelfSchema(Schema): - employer = fields.Nested("self", exclude=("employer",)) + employer = fields.Nested(lambda: SelfSchema(exclude=("employer",))) class Meta: additional = ("name", "age") @@ -2247,7 +2264,7 @@ class Meta: def test_nested_self_with_only_param(self, user, employer): class SelfSchema(Schema): - employer = fields.Nested("self", only=("name",)) + employer = fields.Nested(lambda: SelfSchema(only=("name",))) class Meta: fields = ("name", "employer") @@ -2257,10 +2274,14 @@ class Meta: assert data["employer"]["name"] == employer.name assert "age" not in data["employer"] - def test_multiple_pluck_self_field(self, user): + def test_multiple_pluck_self_lambda(self, user): class MultipleSelfSchema(Schema): - emp = fields.Pluck("self", "name", attribute="employer") - rels = fields.Pluck("self", "name", many=True, attribute="relatives") + emp = fields.Pluck( + lambda: MultipleSelfSchema(), "name", attribute="employer" + ) + rels = fields.Pluck( + lambda: MultipleSelfSchema(), "name", many=True, attribute="relatives" + ) class Meta: fields = ("name", "emp", "rels") @@ -2272,9 +2293,28 @@ class Meta: relative = data["rels"][0] assert relative == user.relatives[0].name - def test_nested_self_many(self): + def test_multiple_pluck_self_string(self, user): + with pytest.warns( + DeprecationWarning, match="Passing 'self' to `Nested` is deprecated" + ): + + class MultipleSelfSchema(Schema): + emp = fields.Pluck("self", "name", attribute="employer") + rels = fields.Pluck("self", "name", many=True, attribute="relatives") + + class Meta: + fields = ("name", "emp", "rels") + + schema = MultipleSelfSchema() + user.relatives = [User(name="Bar", age=12), User(name="Baz", age=34)] + data = schema.dump(user) + assert len(data["rels"]) == len(user.relatives) + relative = data["rels"][0] + assert relative == user.relatives[0].name + + def test_nested_self_many_lambda(self): class SelfManySchema(Schema): - relatives = fields.Nested("self", many=True) + relatives = fields.Nested(lambda: SelfManySchema(), many=True) class Meta: additional = ("name", "age") @@ -2287,9 +2327,28 @@ class Meta: assert data["relatives"][0]["name"] == person.relatives[0].name assert data["relatives"][0]["age"] == person.relatives[0].age + def test_nested_self_many_string(self): + with pytest.warns( + DeprecationWarning, match="Passing 'self' to `Nested` is deprecated" + ): + + class SelfManySchema(Schema): + relatives = fields.Nested("self", many=True) + + class Meta: + additional = ("name", "age") + + person = User(name="Foo") + person.relatives = [User(name="Bar", age=12), User(name="Baz", age=34)] + data = SelfManySchema().dump(person) + assert data["name"] == person.name + assert len(data["relatives"]) == len(person.relatives) + assert data["relatives"][0]["name"] == person.relatives[0].name + assert data["relatives"][0]["age"] == person.relatives[0].age + def test_nested_self_list(self): class SelfListSchema(Schema): - relatives = fields.List(fields.Nested("self")) + relatives = fields.List(fields.Nested(lambda: SelfListSchema())) class Meta: additional = ("name", "age") @@ -2302,6 +2361,25 @@ class Meta: assert data["relatives"][0]["name"] == person.relatives[0].name assert data["relatives"][0]["age"] == person.relatives[0].age + def test_nested_self_list_string(self): + with pytest.warns( + DeprecationWarning, match="Passing 'self' to `Nested` is deprecated" + ): + + class SelfListSchema(Schema): + relatives = fields.List(fields.Nested("self")) + + class Meta: + additional = ("name", "age") + + person = User(name="Foo") + person.relatives = [User(name="Bar", age=12), User(name="Baz", age=34)] + data = SelfListSchema().dump(person) + assert data["name"] == person.name + assert len(data["relatives"]) == len(person.relatives) + assert data["relatives"][0]["name"] == person.relatives[0].name + assert data["relatives"][0]["age"] == person.relatives[0].age + class RequiredUserSchema(Schema): name = fields.Field(required=True) From 669980968ca8d1b6826b9a23d0ecb844f729defb Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Fri, 13 Sep 2019 00:17:02 -0400 Subject: [PATCH 07/11] Update documentation --- docs/nesting.rst | 81 +++++++++++++++++++++++++------------- docs/upgrading.rst | 49 +++++++++++++++++++++++ docs/why.rst | 2 +- examples/peewee_example.py | 2 +- 4 files changed, 105 insertions(+), 29 deletions(-) diff --git a/docs/nesting.rst b/docs/nesting.rst index 66da5df70..6ded19d7a 100644 --- a/docs/nesting.rst +++ b/docs/nesting.rst @@ -22,7 +22,7 @@ Schemas can be nested to represent relationships between objects (e.g. foreign k self.title = title self.author = author # A User object -Use a :class:`Nested ` field to represent the relationship, passing in a nested schema class. +Use a :class:`Nested ` field to represent the relationship, passing in a nested schema. .. code-block:: python @@ -53,24 +53,24 @@ The serialized blog will have the nested user representation. # 'created_at': '2014-08-17T14:58:57.600623+00:00'}} .. note:: - If the field is a collection of nested objects, you must set ``many=True``. + If the field is a collection of nested objects, pass the `Nested ` field to `List `. .. code-block:: python - collaborators = fields.Nested(UserSchema, many=True) + collaborators = fields.List(fields.Nested(UserSchema)) .. _specifying-nested-fields: Specifying Which Fields to Nest ------------------------------- -You can explicitly specify which attributes of the nested objects you want to serialize with the ``only`` argument. +You can explicitly specify which attributes of the nested objects you want to (de)serialize with the ``only`` argument to the schema. .. code-block:: python class BlogSchema2(Schema): title = fields.String() - author = fields.Nested(UserSchema, only=["email"]) + author = fields.Nested(UserSchema(only=("email",))) schema = BlogSchema2() @@ -81,7 +81,7 @@ You can explicitly specify which attributes of the nested objects you want to se # 'author': {'email': u'monty@python.org'} # } -You can represent the attributes of deeply nested objects using dot delimiters. +Dotted paths may be passed to ``only`` and ``exclude`` to specify nested attributes. .. code-block:: python @@ -89,7 +89,7 @@ You can represent the attributes of deeply nested objects using dot delimiters. blog = fields.Nested(BlogSchema2) - schema = SiteSchema(only=["blog.author.email"]) + schema = SiteSchema(only=("blog.author.email",)) result = schema.dump(site) pprint(result) # { @@ -125,8 +125,6 @@ You can replace nested data with a single value (or flat list of values if ``man # } -You can also exclude fields by passing in an ``exclude`` list. This argument also allows representing the attributes of deeply nested objects using dot delimiters. - .. _partial-loading: Partial Loading @@ -168,27 +166,29 @@ You can specify a subset of the fields to allow partial loading using dot delimi Two-way Nesting --------------- -If you have two objects that nest each other, you can refer to a nested schema by its class name. This allows you to nest Schemas that have not yet been defined. - +If you have two objects that nest each other, you can pass a callable to `Nested `. +This allows you to resolve order-of-declaration issues, such as when one schema nests a schema that is declared below it. -For example, a representation of an ``Author`` model might include the books that have a foreign-key (many-to-one) relationship to it. Correspondingly, a representation of a ``Book`` will include its author representation. +For example, a representation of an ``Author`` model might include the books that have a many-to-one relationship to it. +Correspondingly, a representation of a ``Book`` will include its author representation. .. code-block:: python - class AuthorSchema(Schema): - # Make sure to use the 'only' or 'exclude' params + class BookSchema(Schema): + id = fields.Int(dump_only=True) + title = fields.Str() + + # Make sure to use the 'only' or 'exclude' # to avoid infinite recursion - books = fields.Nested("BookSchema", many=True, exclude=("author",)) + author = fields.Nested(lambda: AuthorSchema(only=("id", "title"))) - class Meta: - fields = ("id", "name", "books") + class AuthorSchema(Schema): + id = fields.Int(dump_only=True) + title = fields.Str() - class BookSchema(Schema): - author = fields.Nested(AuthorSchema, only=("id", "name")) + books = fields.List(fields.Nested(BookSchema(exclude=("author",)))) - class Meta: - fields = ("id", "title", "author") .. code-block:: python @@ -221,27 +221,54 @@ For example, a representation of an ``Author`` model might include the books tha # ] # } +You can also pass a class name as a string to `Nested `. +This is useful for avoiding circular imports when your schemas are located in different modules. + +.. code-block:: python + + # books.py + from marshmallow import Schema, fields + + + class BookSchema(Schema): + id = fields.Int(dump_only=True) + title = fields.Str() + + author = fields.Nested("AuthorSchema", only=("id", "title")) + +.. code-block:: python + + # authors.py + from marshmallow import Schema, fields + + + class AuthorSchema(Schema): + id = fields.Int(dump_only=True) + title = fields.Str() + + books = fields.List(fields.Nested("BookSchema", exclude=("author",))) + .. note:: - If you need to, you can also pass the full, module-qualified path to `fields.Nested`. :: - books = fields.Nested('path.to.BookSchema', - many=True, exclude=('author', )) + If you have multiple schemas with the same class name, you must pass the full, module-qualified path. :: + + author = fields.Nested("authors.BookSchema", only=("id", "title")) .. _self-nesting: Nesting A Schema Within Itself ------------------------------ -If the object to be marshalled has a relationship to an object of the same type, you can nest the `Schema` within itself by passing ``"self"`` (with quotes) to the :class:`Nested ` constructor. +If the object to be marshalled has a relationship to an object of the same type, you can nest the `Schema` within itself by passing a callable that returns an instance of the same schema. .. code-block:: python class UserSchema(Schema): name = fields.String() email = fields.Email() - friends = fields.Nested("self", many=True) # Use the 'exclude' argument to avoid infinite recursion - employer = fields.Nested("self", exclude=("employer",), default=None) + employer = fields.Nested(lambda: UserSchema(exclude=("employer",))) + friends = fields.List(fields.Nested(lambda: UserSchema())) user = User("Steve", "steve@example.com") diff --git a/docs/upgrading.rst b/docs/upgrading.rst index c01d13a9d..6a238bc28 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -3,6 +3,55 @@ Upgrading to Newer Releases This section documents migration paths to new releases. +Upgrading to 3.1 +++++++++++++++++ + +In 3.1, `fields.Nested ` may take a callable that returns a schema instance. +Use this to resolve order-of-declaration issues when schemas nest each other. + +.. code-block:: python + + from marshmallow import Schema, fields + + # <3.1 + class AlbumSchema(Schema): + title = fields.Str() + artist = fields.Nested("ArtistSchema", only=("name",)) + + + class ArtistSchema(Schema): + name = fields.Str() + albums = fields.List(fields.Nested(AlbumSchema)) + + + # >=3.1 + class AlbumSchema(Schema): + title = fields.Str() + artist = fields.Nested(lambda: ArtistSchema(only=("name",))) + + + class ArtistSchema(Schema): + name = fields.Str() + albums = fields.List(fields.Nested(AlbumSchema)) + +A callable should also be used when nesting a schema within itself. +Passing ``"self"`` is deprecated. + +.. code-block:: python + + from marshmallow import Schema, fields + + # <3.1 + class PersonSchema(Schema): + partner = fields.Nested("self", exclude=("partner",)) + friends = fields.List(fields.Nested("self")) + + + # >=3.1 + class PersonSchema(Schema): + partner = fields.Nested(lambda: PersonSchema(exclude=("partner"))) + friends = fields.List(fields.Nested(lambda: PersonSchema())) + .. _upgrading_3_0: Upgrading to 3.0 diff --git a/docs/why.rst b/docs/why.rst index 08444e301..9f86bbaa1 100644 --- a/docs/why.rst +++ b/docs/why.rst @@ -33,8 +33,8 @@ As an example, you might have a JSON endpoint for retrieving all information abo class GameStateSchema(Schema): _id = fields.UUID(required=True) - players = fields.Nested(PlayerSchema, many=True) score = fields.Nested(ScoreSchema) + players = fields.List(fields.Nested(PlayerSchema)) last_changed = fields.DateTime(format="rfc") class Meta: diff --git a/examples/peewee_example.py b/examples/peewee_example.py index a28c4fefe..d4da8bf92 100644 --- a/examples/peewee_example.py +++ b/examples/peewee_example.py @@ -77,7 +77,7 @@ def wrap(self, data, many, **kwargs): class TodoSchema(Schema): id = fields.Int(dump_only=True) done = fields.Boolean(attribute="is_done", missing=False) - user = fields.Nested(UserSchema, exclude=("joined_on", "password"), dump_only=True) + user = fields.Nested(UserSchema(exclude=("joined_on", "password")), dump_only=True) content = fields.Str(required=True) posted_on = fields.DateTime(dump_only=True) From 4e6a3633562c8b3014868574a4bf668d9081bfa8 Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Fri, 13 Sep 2019 00:31:06 -0400 Subject: [PATCH 08/11] Remove incorrect comment --- src/marshmallow/fields.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py index 2a73b5281..c53268fdb 100644 --- a/src/marshmallow/fields.py +++ b/src/marshmallow/fields.py @@ -470,12 +470,11 @@ def __init__( DeprecationWarning, ) self.nested = nested - self._schema = None # Cached Schema instance - # Deprecated attributes self.only = only self.exclude = exclude self.many = many self.unknown = unknown + self._schema = None # Cached Schema instance super().__init__(default=default, **kwargs) @property From 97979d85a04e6a482902c5180e65a35c856ed623 Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Wed, 4 Dec 2019 14:57:41 -0500 Subject: [PATCH 09/11] Remove outdated doc re: deprecated params --- src/marshmallow/fields.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py index 5753b1cc2..4ace65807 100644 --- a/src/marshmallow/fields.py +++ b/src/marshmallow/fields.py @@ -474,11 +474,6 @@ class ParentSchema(Schema): :param unknown: Whether to exclude, include, or raise an error for unknown fields in the data. Use `EXCLUDE`, `INCLUDE` or `RAISE`. :param kwargs: The same keyword arguments that :class:`Field` receives. - - .. versionchanged:: 3.1.0 - Deprecated ``only``, ``exclude``, and ``unknown`` parameters. - Pass these to the schema instance instead. - ``many`` was also deprecated in favor of the ``List(Nested(...))`` usage. """ default_error_messages = {"type": "Invalid type."} From 6c706151cb0e2fb3d1299a4408bb0b6234b4d87c Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Thu, 5 Dec 2019 19:22:50 -0500 Subject: [PATCH 10/11] Update upgrading guide and changelog --- CHANGELOG.rst | 53 +++++++++++++++++++++++++++++++++++++++++++++- docs/upgrading.rst | 12 +++++------ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 15993b2c8..f252533f4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,12 +1,63 @@ Changelog --------- +3.3.0 (unreleased) +****************** + +Features: + +- ``fields.Nested`` may take a callable that returns a schema instance. + Use this to resolve order-of-declaration issues when schemas nest each other. + +.. code-block:: python + + # <3.3 + class AlbumSchema(Schema): + title = fields.Str() + artist = fields.Nested("ArtistSchema", only=("name",)) + + + class ArtistSchema(Schema): + name = fields.Str() + albums = fields.List(fields.Nested(AlbumSchema)) + + + # >=3.3 + class AlbumSchema(Schema): + title = fields.Str() + artist = fields.Nested(lambda: ArtistSchema(only=("name",))) + + + class ArtistSchema(Schema): + name = fields.Str() + albums = fields.List(fields.Nested(AlbumSchema)) + +Deprecations: + +- Passing the string ``"self"`` to ``fields.Nested`` is deprecated. + Use a callable instead. + +.. code-block:: python + + from marshmallow import Schema, fields + + # <3.3 + class PersonSchema(Schema): + partner = fields.Nested("self", exclude=("partner",)) + friends = fields.List(fields.Nested("self")) + + + # >=3.3 + class PersonSchema(Schema): + partner = fields.Nested(lambda: PersonSchema(exclude=("partner"))) + friends = fields.List(fields.Nested(lambda: PersonSchema())) + 3.2.2 (2019-11-04) ****************** Bug fixes: -- Don't load fields for which ``load_only`` and ``dump_only`` are both ``True`` (:pr:`1448`). +- Don't load fields for which ``load_only`` and ``dump_only`` are both ``True`` (:pr:`1448`). - Fix types in ``marshmallow.validate`` (:pr:`1446`). Support: diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 6a238bc28..ea7d6f9d2 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -3,17 +3,17 @@ Upgrading to Newer Releases This section documents migration paths to new releases. -Upgrading to 3.1 +Upgrading to 3.3 ++++++++++++++++ -In 3.1, `fields.Nested ` may take a callable that returns a schema instance. +In 3.3, `fields.Nested ` may take a callable that returns a schema instance. Use this to resolve order-of-declaration issues when schemas nest each other. .. code-block:: python from marshmallow import Schema, fields - # <3.1 + # <3.3 class AlbumSchema(Schema): title = fields.Str() artist = fields.Nested("ArtistSchema", only=("name",)) @@ -24,7 +24,7 @@ Use this to resolve order-of-declaration issues when schemas nest each other. albums = fields.List(fields.Nested(AlbumSchema)) - # >=3.1 + # >=3.3 class AlbumSchema(Schema): title = fields.Str() artist = fields.Nested(lambda: ArtistSchema(only=("name",))) @@ -41,13 +41,13 @@ Passing ``"self"`` is deprecated. from marshmallow import Schema, fields - # <3.1 + # <3.3 class PersonSchema(Schema): partner = fields.Nested("self", exclude=("partner",)) friends = fields.List(fields.Nested("self")) - # >=3.1 + # >=3.3 class PersonSchema(Schema): partner = fields.Nested(lambda: PersonSchema(exclude=("partner"))) friends = fields.List(fields.Nested(lambda: PersonSchema())) From 2a2861881b11521eaa999942430af11b0cdab78b Mon Sep 17 00:00:00 2001 From: Steven Loria Date: Thu, 5 Dec 2019 19:27:58 -0500 Subject: [PATCH 11/11] Link to issue --- CHANGELOG.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f252533f4..1a4e0ed84 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,7 +7,7 @@ Changelog Features: - ``fields.Nested`` may take a callable that returns a schema instance. - Use this to resolve order-of-declaration issues when schemas nest each other. + Use this to resolve order-of-declaration issues when schemas nest each other (:issue:`1146`). .. code-block:: python