Skip to content

Commit 63365a3

Browse files
committed
Make propagate_unknown respect any explciit 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 61d3057 commit 63365a3

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
@@ -2891,3 +2891,66 @@ class DefinitelyUniqueSchema(Schema):
28912891

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

0 commit comments

Comments
 (0)