Skip to content

Commit 9e1f75f

Browse files
committed
Make propagate_unknown respect any explicit value
propagate_unknown will still traverse any series of nested documents, meaning that once you set propagate_unknown=True, it is true for the whole schema structure. However, this introduces tracking for whether or not `unknown` was set explicitly. If `unknown=RAISE` is set because no value was specified, we will set a new flag on the schema, `auto_unknown=True`. propagate_unknown now has the following behavior: - if the nested schema has auto_unknown=False, use the current value for `unknown` in the nested `load` call - if a nested field has its `unknown` attribute set, use that in place of any value sent via `propagate_unknown` Effectively, this means that if you set `unknown` explicitly anywhere in a nested schema structure, it will propagate downwards from that point. Combined with the fact that propagate_unknown=True propagates downwards across all schema barriers, including if `propagate_unknown=False` is set explicitly somewhere, this could be confusing. However, because the idea is for `propagate_unknown=True` to eventually be the only supported behavior for marshmallow, this is acceptable as a limitation. auto_unknown is an attribute of schema opts and of schema instances, with the same kind of inheritance behavior as other fields.
1 parent 9b97235 commit 9e1f75f

File tree

3 files changed

+83
-1
lines changed

3 files changed

+83
-1
lines changed

src/marshmallow/fields.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,15 @@ def _deserialize(
608608
Add ``partial`` parameter.
609609
"""
610610
self._test_collection(value)
611+
# check if self.unknown or self.schema.unknown is set
612+
# however, we should only respect `self.schema.unknown` if
613+
# `auto_unknown` is False, meaning that it was set explicitly on the
614+
# schema class or instance
615+
explicit_unknown = self.unknown or (
616+
self.schema.unknown if not self.schema.auto_unknown else None
617+
)
618+
if explicit_unknown:
619+
unknown = explicit_unknown
611620
return self._load(
612621
value,
613622
data,

src/marshmallow/schema.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,14 @@ def __init__(self, meta, ordered: bool = False):
227227
self.include = getattr(meta, "include", {})
228228
self.load_only = getattr(meta, "load_only", ())
229229
self.dump_only = getattr(meta, "dump_only", ())
230-
self.unknown = getattr(meta, "unknown", RAISE)
230+
# self.unknown defaults to "RAISE", but note whether it was explicit or
231+
# not, so that when we're handling propagate_unknown we can decide
232+
# whether or not to propagate based on whether or not it was set
233+
# explicitly
234+
self.unknown = getattr(meta, "unknown", None)
235+
self.auto_unknown = self.unknown is None
236+
if self.auto_unknown:
237+
self.unknown = RAISE
231238
self.propagate_unknown = getattr(meta, "propagate_unknown", False)
232239
self.register = getattr(meta, "register", True)
233240

@@ -391,6 +398,9 @@ def __init__(
391398
self.dump_only = set(dump_only) or set(self.opts.dump_only)
392399
self.partial = partial
393400
self.unknown = unknown or self.opts.unknown
401+
# if unknown was not set explicitly AND self.opts.auto_unknown is true,
402+
# then the value should be considered "automatic"
403+
self.auto_unknown = (not unknown) and self.opts.auto_unknown
394404
self.propagate_unknown = propagate_unknown or self.opts.propagate_unknown
395405
self.context = context or {}
396406
self._normalize_nested_options()

tests/test_schema.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2901,3 +2901,66 @@ class DefinitelyUniqueSchema(Schema):
29012901

29022902
SchemaClass = class_registry.get_class(DefinitelyUniqueSchema.__name__)
29032903
assert SchemaClass is DefinitelyUniqueSchema
2904+
2905+
2906+
def test_propagate_unknown_stops_at_explicit_value_for_nested():
2907+
# propagate_unknown=True should traverse any "auto_unknown" values and
2908+
# replace them with the "unknown" value from the parent context (schema or
2909+
# load arguments)
2910+
# this test makes sure that it stops when a nested field or schema has
2911+
# "unknown" set explicitly (so auto_unknown=False)
2912+
2913+
class Bottom(Schema):
2914+
x = fields.Str()
2915+
2916+
class Middle(Schema):
2917+
x = fields.Str()
2918+
# set unknown explicitly on a nested field, so auto_unknown will be
2919+
# false going into Bottom
2920+
child = fields.Nested(Bottom, unknown=EXCLUDE)
2921+
2922+
class Top(Schema):
2923+
x = fields.Str()
2924+
child = fields.Nested(Middle)
2925+
2926+
data = {
2927+
"x": "hi",
2928+
"y": "bye",
2929+
"child": {"x": "hi", "y": "bye", "child": {"x": "hi", "y": "bye"}},
2930+
}
2931+
result = Top(unknown=INCLUDE, propagate_unknown=True).load(data)
2932+
assert result == {
2933+
"x": "hi",
2934+
"y": "bye",
2935+
"child": {"x": "hi", "y": "bye", "child": {"x": "hi"}},
2936+
}
2937+
2938+
2939+
def test_propagate_unknown_stops_at_explicit_value_for_meta():
2940+
# this is the same as the above test of propagate_unknown stopping where
2941+
# auto_unknown=False, but it checks that this applies when `unknown` is set
2942+
# by means of `Meta`
2943+
2944+
class Bottom(Schema):
2945+
x = fields.Str()
2946+
2947+
class Middle(Schema):
2948+
x = fields.Str()
2949+
child = fields.Nested(Bottom)
2950+
2951+
# set unknown explicitly on a nested field, so auto_unknown will be
2952+
# false going into Bottom
2953+
class Meta:
2954+
unknown = EXCLUDE
2955+
2956+
class Top(Schema):
2957+
x = fields.Str()
2958+
child = fields.Nested(Middle)
2959+
2960+
data = {
2961+
"x": "hi",
2962+
"y": "bye",
2963+
"child": {"x": "hi", "y": "bye", "child": {"x": "hi", "y": "bye"}},
2964+
}
2965+
result = Top(unknown=INCLUDE, propagate_unknown=True).load(data)
2966+
assert result == {"x": "hi", "y": "bye", "child": {"x": "hi", "child": {"x": "hi"}}}

0 commit comments

Comments
 (0)