Repository: marshmallow-code/marshmallow
Pull request #1632 (2020-07-16 06:56:49)

Pass unknown parameter to pre_load, post_load, and validates_schema methods
When extending a schema, the value of self.unknown is not necessarily correct in pre_load methods.

```
class MySchema(Schema):

    ...

    @pre_load
    def this_is_wrong(self, data, **kwargs):
        print(self.unknown)
        return data


...
MySchema().load(data, unknown=INCLUDE)
```

will print 'raise' rather than 'include'.


Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 514e40c4..4efa7473 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -61,6 +61,7 @@ As a consequence of this change:
   `marshmallow.fields.TimeDelta` (:pr:`2654`).
 - Rename ``json_data`` parameter of `marshmallow.Schema.loads` to ``s``
   for compatibility with most render module implementations (`json`, ``simplejson``, etc.) (:pr:`2764`).
+  Also make it a positional-only argument.
 
 Thanks :user:`ddelange` for the PR.
 
@@ -91,6 +92,10 @@ Thanks :user:`ddelange` for the PR.
         UserSchema().dump({"name": "foo"})
         # {'name_suffixed': 'foobar'}
 
+- Methods decorated with `marshmallow.pre_load`, `marshmallow.post_load`, `marshmallow.validates_schema`,
+  receive ``unknown`` as a keyword argument (:pr:`1632`).
+  Thanks :user:`jforand` for the PR.
+
 Deprecations/Removals:
 
 - *Backwards-incompatible*: Remove implicit field creation, i.e. using the ``fields`` or ``additional`` class Meta options with undeclared fields (:issue:`1356`).
diff --git a/src/marshmallow/decorators.py b/src/marshmallow/decorators.py
index 8e91dfc4..d752dfa0 100644
--- a/src/marshmallow/decorators.py
+++ b/src/marshmallow/decorators.py
@@ -111,10 +111,11 @@ def validates_schema(
 
     .. versionchanged:: 3.0.0b1
         ``skip_on_field_errors`` defaults to `True`.
-
     .. versionchanged:: 3.0.0
         ``partial`` and ``many`` are always passed as keyword arguments to
         the decorated method.
+    .. versionchanged:: 4.0.0
+        ``unknown`` is passed as a keyword argument to the decorated method.
     """
     return set_hook(
         fn,
@@ -175,6 +176,8 @@ def pre_load(
     .. versionchanged:: 3.0.0
         ``partial`` and ``many`` are always passed as keyword arguments to
         the decorated method.
+    .. versionchanged:: 4.0.0
+        ``unknown`` is passed as a keyword argument to the decorated method.
     """
     return set_hook(fn, PRE_LOAD, many=pass_collection)
 
@@ -197,6 +200,8 @@ def post_load(
     .. versionchanged:: 3.0.0
         ``partial`` and ``many`` are always passed as keyword arguments to
         the decorated method.
+    .. versionchanged:: 4.0.0
+        ``unknown`` is passed as a keyword argument to the decorated method.
     """
     return set_hook(fn, POST_LOAD, many=pass_collection, pass_original=pass_original)
 
diff --git a/src/marshmallow/schema.py b/src/marshmallow/schema.py
index 28324a8e..0298274e 100644
--- a/src/marshmallow/schema.py
+++ b/src/marshmallow/schema.py
@@ -727,6 +727,7 @@ class Schema(metaclass=SchemaMeta):
     def loads(
         self,
         s: str | bytes | bytearray,
+        /,
         *,
         many: bool | None = None,
         partial: bool | types.StrSequenceOrSet | None = None,
@@ -767,14 +768,17 @@ class Schema(metaclass=SchemaMeta):
         error_store,
         many,
         partial,
+        unknown,
         pass_original,
         index=None,
     ):
         try:
             if pass_original:  # Pass original, raw data (before unmarshalling)
-                validator_func(output, original_data, partial=partial, many=many)
+                validator_func(
+                    output, original_data, partial=partial, many=many, unknown=unknown
+                )
             else:
-                validator_func(output, partial=partial, many=many)
+                validator_func(output, partial=partial, many=many, unknown=unknown)
         except ValidationError as err:
             error_store.store_error(err.messages, err.field_name, index=index)
 
@@ -850,7 +854,12 @@ class Schema(metaclass=SchemaMeta):
         if self._hooks[PRE_LOAD]:
             try:
                 processed_data = self._invoke_load_processors(
-                    PRE_LOAD, data, many=many, original_data=data, partial=partial
+                    PRE_LOAD,
+                    data,
+                    many=many,
+                    original_data=data,
+                    partial=partial,
+                    unknown=unknown,
                 )
             except ValidationError as err:
                 errors = err.normalized_messages()
@@ -880,6 +889,7 @@ class Schema(metaclass=SchemaMeta):
                     original_data=data,
                     many=many,
                     partial=partial,
+                    unknown=unknown,
                     field_errors=field_errors,
                 )
                 self._invoke_schema_validators(
@@ -889,6 +899,7 @@ class Schema(metaclass=SchemaMeta):
                     original_data=data,
                     many=many,
                     partial=partial,
+                    unknown=unknown,
                     field_errors=field_errors,
                 )
             errors = error_store.errors
@@ -901,6 +912,7 @@ class Schema(metaclass=SchemaMeta):
                         many=many,
                         original_data=data,
                         partial=partial,
+                        unknown=unknown,
                     )
                 except ValidationError as err:
                     errors = err.normalized_messages()
@@ -1080,6 +1092,7 @@ class Schema(metaclass=SchemaMeta):
         many: bool,
         original_data,
         partial: bool | types.StrSequenceOrSet | None,
+        unknown: str,
     ):
         # This has to invert the order of the dump processors, so run the pass_collection
         # processors first.
@@ -1090,6 +1103,7 @@ class Schema(metaclass=SchemaMeta):
             many=many,
             original_data=original_data,
             partial=partial,
+            unknown=unknown,
         )
         data = self._invoke_processors(
             tag,
@@ -1098,6 +1112,7 @@ class Schema(metaclass=SchemaMeta):
             many=many,
             original_data=original_data,
             partial=partial,
+            unknown=unknown,
         )
         return data
 
@@ -1157,6 +1172,7 @@ class Schema(metaclass=SchemaMeta):
         many: bool,
         partial: bool | types.StrSequenceOrSet | None,
         field_errors: bool = False,
+        unknown: str,
     ):
         for attr_name, hook_many, validator_kwargs in self._hooks[VALIDATES_SCHEMA]:
             if hook_many != pass_collection:
@@ -1175,6 +1191,7 @@ class Schema(metaclass=SchemaMeta):
                         error_store=error_store,
                         many=many,
                         partial=partial,
+                        unknown=unknown,
                         index=idx,
                         pass_original=pass_original,
                     )
@@ -1187,6 +1204,7 @@ class Schema(metaclass=SchemaMeta):
                     many=many,
                     pass_original=pass_original,
                     partial=partial,
+                    unknown=unknown,
                 )
 
     def _invoke_processors(


Test patch:
diff --git a/tests/test_decorators.py b/tests/test_decorators.py
index 88019a46..d8a6f77d 100644
--- a/tests/test_decorators.py
+++ b/tests/test_decorators.py
@@ -881,3 +881,36 @@ def test_decorator_post_load_with_nested_original_and_pass_collection(
 
     schema = ExampleSchema()
     assert schema.load(data) == data
+
+
+@pytest.mark.parametrize("usage_location", ["meta", "init", "load"])
+@pytest.mark.parametrize("unknown_val", (EXCLUDE, INCLUDE))
+def test_load_processors_receive_unknown(usage_location, unknown_val):
+    class ExampleSchema(Schema):
+        foo = fields.Int()
+
+        @validates_schema
+        def check_unknown_validates(self, data, unknown, **kwargs):
+            assert unknown == unknown_val
+
+        @pre_load
+        def check_unknown_pre(self, data, unknown, **kwargs):
+            assert unknown == unknown_val
+            return data
+
+        @post_load
+        def check_unknown_post(self, data, unknown, **kwargs):
+            assert unknown == unknown_val
+            return data
+
+    if usage_location == "meta":
+
+        class ExampleSchemaChild(ExampleSchema):
+            class Meta:
+                unknown = unknown_val
+
+        ExampleSchemaChild().load({"foo": 42})
+    if usage_location == "init":
+        ExampleSchema(unknown=unknown_val).load({"foo": 42})
+    else:
+        ExampleSchema().load({"foo": 42}, unknown=unknown_val)