Skip to content

Commit a990005

Browse files
committed
Make unknown propagation override child schemas
In order to simplify, the value of `unknown` will not be respected in child schemas if `PROPAGATE` is used. This removes any need for the `auto_unknown` tracking, so there's less complexity added to schemas in order to implement this behavior. Adjust tests and changelog to match. Also make minor tweaks to ensure UnknownParam usage is consistent and keep the diff with the dev branch smaller.
1 parent 08e1950 commit a990005

File tree

6 files changed

+33
-74
lines changed

6 files changed

+33
-74
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,16 @@ Features:
88

99
- The behavior of the ``unknown`` option can be further customized with a new
1010
value, ``PROPAGATE``. If ``unknown=EXCLUDE | PROPAGATE`` is set, then the
11-
value of ``unknown=EXCLUDE | PROPAGATE`` will be passed to any nested
12-
schemas which do not explicitly set ``unknown`` in ``Nested`` or schema
13-
options. This works for ``INCLUDE | PROPAGATE`` and ``RAISE | PROPAGATE`` as
14-
well.
11+
value of ``unknown=EXCLUDE | PROPAGATE`` will be passed to any nested schemas.
12+
This works for ``INCLUDE | PROPAGATE`` and ``RAISE | PROPAGATE`` as well.
1513
(:issue:`1490`, :issue:`1428`)
16-
Thanks :user:`lmignon` and :user:`mahenzon`.
1714

1815
.. note::
1916

20-
When a schema is being loaded with ``unknown=... | PROPAGATE``, you can still
21-
set ``unknown`` explicitly on child schemas. However, be aware that such a
22-
value may turn off propagation at that point in the schema hierarchy.
23-
24-
For example, a schema which specifies ``unknown=EXCLUDE`` will set
25-
``EXCLUDE`` for itself. But because the value is ``EXCLUDE`` rather than
26-
``EXCLUDE | PROPAGATE``, that setting will not be propagated to its
27-
children, even if there is a parent schema which sets
28-
``unknown=EXCLUDE | PROPAGATE``.
17+
When a schema is being loaded with ``unknown=... | PROPAGATE``, this will
18+
override any values set for ``unknown`` in child schemas. Therefore,
19+
``PROPAGATE`` should only be used in cases in which you want to change
20+
the behavior of an entire schema heirarchy.
2921

3022
3.7.0 (2020-07-08)
3123
******************

src/marshmallow/base.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,7 @@ def dump(self, obj, *, many: bool = None):
3838
def dumps(self, obj, *, many: bool = None):
3939
raise NotImplementedError
4040

41-
def load(
42-
self, data, *, many: bool = None, partial=None, unknown=None
43-
):
41+
def load(self, data, *, many: bool = None, partial=None, unknown=None):
4442
raise NotImplementedError
4543

4644
def loads(

src/marshmallow/fields.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ def __init__(
494494
self.only = only
495495
self.exclude = exclude
496496
self.many = many
497-
self.unknown = UnknownParam.parse_if_str(unknown) if unknown else None
497+
self.unknown = UnknownParam.parse_if_str(unknown)
498498
self._schema = None # Cached Schema instance
499499
super().__init__(default=default, **kwargs)
500500

@@ -575,7 +575,7 @@ def _test_collection(self, value):
575575
def _load(self, value, data, partial=None, unknown=None):
576576
try:
577577
valid_data = self.schema.load(
578-
value, unknown=unknown if unknown else self.unknown, partial=partial,
578+
value, unknown=unknown or self.unknown, partial=partial,
579579
)
580580
except ValidationError as error:
581581
raise ValidationError(
@@ -593,18 +593,7 @@ def _deserialize(self, value, attr, data, partial=None, unknown=None, **kwargs):
593593
Add ``partial`` parameter.
594594
"""
595595
self._test_collection(value)
596-
# check if self.unknown or self.schema.unknown is set
597-
# however, we should only respect `self.schema.unknown` if
598-
# `auto_unknown` is False, meaning that it was set explicitly on the
599-
# schema class or instance
600-
explicit_unknown = (
601-
self.unknown
602-
if self.unknown
603-
else (self.schema.unknown if not self.schema.auto_unknown else None)
604-
)
605-
if explicit_unknown:
606-
unknown = explicit_unknown
607-
return self._load(value, data, partial=partial, unknown=unknown,)
596+
return self._load(value, data, partial=partial, unknown=unknown)
608597

609598

610599
class Pluck(Nested):

src/marshmallow/schema.py

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -228,15 +228,7 @@ def __init__(self, meta, ordered: bool = False):
228228
self.include = getattr(meta, "include", {})
229229
self.load_only = getattr(meta, "load_only", ())
230230
self.dump_only = getattr(meta, "dump_only", ())
231-
# self.unknown defaults to "RAISE", but note whether it was explicit or
232-
# not, so that when we're handling "propagate" we can decide
233-
# whether or not to propagate based on whether or not it was set
234-
# explicitly
235-
self.unknown = getattr(meta, "unknown", None)
236-
self.auto_unknown = self.unknown is None
237-
if self.auto_unknown:
238-
self.unknown = RAISE
239-
self.unknown = UnknownParam.parse_if_str(self.unknown)
231+
self.unknown = UnknownParam.parse_if_str(getattr(meta, "unknown", RAISE))
240232
self.register = getattr(meta, "register", True)
241233

242234

@@ -397,14 +389,7 @@ def __init__(
397389
self.load_only = set(load_only) or set(self.opts.load_only)
398390
self.dump_only = set(dump_only) or set(self.opts.dump_only)
399391
self.partial = partial
400-
self.unknown = (
401-
UnknownParam.parse_if_str(unknown)
402-
if unknown is not None
403-
else self.opts.unknown
404-
)
405-
# if unknown was not set explicitly AND self.opts.auto_unknown is true,
406-
# then the value should be considered "automatic"
407-
self.auto_unknown = (not unknown) and self.opts.auto_unknown
392+
self.unknown = UnknownParam.parse_if_str(unknown) or self.opts.unknown
408393
self.context = context or {}
409394
self._normalize_nested_options()
410395
#: Dictionary mapping field_names -> :class:`Field` objects
@@ -775,7 +760,7 @@ def loads(
775760
if invalid data are passed.
776761
"""
777762
data = self.opts.render_module.loads(json_data, **kwargs)
778-
return self.load(data, many=many, partial=partial, unknown=unknown,)
763+
return self.load(data, many=many, partial=partial, unknown=unknown)
779764

780765
def _run_validator(
781766
self,
@@ -857,9 +842,7 @@ def _do_load(
857842
error_store = ErrorStore()
858843
errors = {} # type: typing.Dict[str, typing.List[str]]
859844
many = self.many if many is None else bool(many)
860-
unknown = UnknownParam.parse_if_str(
861-
unknown if unknown is not None else self.unknown
862-
)
845+
unknown = UnknownParam.parse_if_str(unknown or self.unknown)
863846
if partial is None:
864847
partial = self.partial
865848
# Run preprocessors

src/marshmallow/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ def __repr__(self):
5151

5252
@classmethod
5353
def parse_if_str(cls, value):
54-
"""Given a string or UnknownParam, convert to an UnknownParam"""
54+
"""Given a string or UnknownParam, convert to an UnknownParam
55+
56+
Preserves None, which is important for making sure that it can be used
57+
blindly on `unknown` which may be a user-supplied value or a default"""
5558
if isinstance(value, str):
5659
return cls(value)
5760
return value

tests/test_schema.py

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2894,12 +2894,11 @@ class DefinitelyUniqueSchema(Schema):
28942894
assert SchemaClass is DefinitelyUniqueSchema
28952895

28962896

2897-
def test_propagate_unknown_stops_at_explicit_value_for_nested():
2898-
# PROPAGATE should traverse any "auto_unknown" values and
2899-
# replace them with the "unknown" value from the parent context (schema or
2900-
# load arguments)
2901-
# this test makes sure that it stops when a nested field or schema has
2902-
# "unknown" set explicitly (so auto_unknown=False)
2897+
def test_propagate_unknown_overrides_explicit_value_for_nested():
2898+
# PROPAGATE should traverse any schemas and replace them with the
2899+
# "unknown" value from the parent context (schema or load arguments)
2900+
# this test makes sure that it takes precedence when a nested field
2901+
# or schema has "unknown" set explicitly
29032902

29042903
class Bottom(Schema):
29052904
x = fields.Str()
@@ -2923,14 +2922,13 @@ class Top(Schema):
29232922
assert result == {
29242923
"x": "hi",
29252924
"y": "bye",
2926-
"child": {"x": "hi", "y": "bye", "child": {"x": "hi"}},
2925+
"child": {"x": "hi", "y": "bye", "child": {"x": "hi", "y": "bye"}},
29272926
}
29282927

29292928

2930-
def test_propagate_unknown_stops_at_explicit_value_for_meta():
2931-
# this is the same as the above test of unknown propagation stopping where
2932-
# auto_unknown=False, but it checks that this applies when `unknown` is set
2933-
# by means of `Meta`
2929+
def test_propagate_unknown_overrides_explicit_value_for_meta():
2930+
# this is the same as the above test of unknown propagation, but it checks that
2931+
# this applies when `unknown` is set by means of `Meta` as well
29342932

29352933
class Bottom(Schema):
29362934
x = fields.Str()
@@ -2939,25 +2937,21 @@ class Middle(Schema):
29392937
x = fields.Str()
29402938
child = fields.Nested(Bottom)
29412939

2942-
# set unknown explicitly here, so auto_unknown will be
2943-
# false going into Bottom, and also set propagate to make it propagate
2944-
# in this case
29452940
class Meta:
2946-
unknown = EXCLUDE | PROPAGATE
2941+
unknown = EXCLUDE
29472942

29482943
class Top(Schema):
29492944
x = fields.Str()
29502945
child = fields.Nested(Middle)
29512946

2952-
# sanity-check that auto-unknown is being set correctly
2953-
assert Top().auto_unknown
2954-
assert not Top(unknown=INCLUDE | PROPAGATE).auto_unknown
2955-
assert not Middle().auto_unknown
2956-
29572947
data = {
29582948
"x": "hi",
29592949
"y": "bye",
29602950
"child": {"x": "hi", "y": "bye", "child": {"x": "hi", "y": "bye"}},
29612951
}
29622952
result = Top(unknown=INCLUDE | PROPAGATE).load(data)
2963-
assert result == {"x": "hi", "y": "bye", "child": {"x": "hi", "child": {"x": "hi"}}}
2953+
assert result == {
2954+
"x": "hi",
2955+
"y": "bye",
2956+
"child": {"x": "hi", "y": "bye", "child": {"x": "hi", "y": "bye"}},
2957+
}

0 commit comments

Comments
 (0)