Skip to content

Commit 499d608

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 e495fbe commit 499d608

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.1 (2020-07-20)
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
@@ -2904,12 +2904,11 @@ class DefinitelyUniqueSchema(Schema):
29042904
assert SchemaClass is DefinitelyUniqueSchema
29052905

29062906

2907-
def test_propagate_unknown_stops_at_explicit_value_for_nested():
2908-
# PROPAGATE should traverse any "auto_unknown" values and
2909-
# replace them with the "unknown" value from the parent context (schema or
2910-
# load arguments)
2911-
# this test makes sure that it stops when a nested field or schema has
2912-
# "unknown" set explicitly (so auto_unknown=False)
2907+
def test_propagate_unknown_overrides_explicit_value_for_nested():
2908+
# PROPAGATE should traverse any schemas and replace them with the
2909+
# "unknown" value from the parent context (schema or load arguments)
2910+
# this test makes sure that it takes precedence when a nested field
2911+
# or schema has "unknown" set explicitly
29132912

29142913
class Bottom(Schema):
29152914
x = fields.Str()
@@ -2933,14 +2932,13 @@ class Top(Schema):
29332932
assert result == {
29342933
"x": "hi",
29352934
"y": "bye",
2936-
"child": {"x": "hi", "y": "bye", "child": {"x": "hi"}},
2935+
"child": {"x": "hi", "y": "bye", "child": {"x": "hi", "y": "bye"}},
29372936
}
29382937

29392938

2940-
def test_propagate_unknown_stops_at_explicit_value_for_meta():
2941-
# this is the same as the above test of unknown propagation stopping where
2942-
# auto_unknown=False, but it checks that this applies when `unknown` is set
2943-
# by means of `Meta`
2939+
def test_propagate_unknown_overrides_explicit_value_for_meta():
2940+
# this is the same as the above test of unknown propagation, but it checks that
2941+
# this applies when `unknown` is set by means of `Meta` as well
29442942

29452943
class Bottom(Schema):
29462944
x = fields.Str()
@@ -2949,25 +2947,21 @@ class Middle(Schema):
29492947
x = fields.Str()
29502948
child = fields.Nested(Bottom)
29512949

2952-
# set unknown explicitly here, so auto_unknown will be
2953-
# false going into Bottom, and also set propagate to make it propagate
2954-
# in this case
29552950
class Meta:
2956-
unknown = EXCLUDE | PROPAGATE
2951+
unknown = EXCLUDE
29572952

29582953
class Top(Schema):
29592954
x = fields.Str()
29602955
child = fields.Nested(Middle)
29612956

2962-
# sanity-check that auto-unknown is being set correctly
2963-
assert Top().auto_unknown
2964-
assert not Top(unknown=INCLUDE | PROPAGATE).auto_unknown
2965-
assert not Middle().auto_unknown
2966-
29672957
data = {
29682958
"x": "hi",
29692959
"y": "bye",
29702960
"child": {"x": "hi", "y": "bye", "child": {"x": "hi", "y": "bye"}},
29712961
}
29722962
result = Top(unknown=INCLUDE | PROPAGATE).load(data)
2973-
assert result == {"x": "hi", "y": "bye", "child": {"x": "hi", "child": {"x": "hi"}}}
2963+
assert result == {
2964+
"x": "hi",
2965+
"y": "bye",
2966+
"child": {"x": "hi", "y": "bye", "child": {"x": "hi", "y": "bye"}},
2967+
}

0 commit comments

Comments
 (0)