Repository: marshmallow-code/marshmallow
Pull request #1986 (2022-05-10 07:37:46)

Add stacklevel to print appropriate location in warnings.
Adding stacklevel will print the location of the warning in source code than Marshmallow which is more helpful.

https://docs.python.org/3/library/warnings.html#warnings.warn

```python
# /tmp/foo.py
import json
from marshmallow.utils import pprint
from marshmallow import Schema, fields, SchemaOpts

class CustomOpts(SchemaOpts):
    pass


class CustomField(fields.Field):
    pass


class FooSchema(Schema):
    custom_field = CustomField(default=True, missing=True, foo=1)
    custom_nested_field = fields.Nested('self')

    class Meta:
        json_module = json

custom_field = CustomField()
custom_field.default
custom_field.default = True
custom_field.missing
custom_field.missing = True
pprint("hello")
custom_field.fail("custom_field")

```

Current warning

```
/home/karthikeyan/stuff/python/marshmallow/src/marshmallow/fields.py:173: RemovedInMarshmallow4Warning: The 'default' argument to fields is deprecated. Use 'dump_default' instead.
  warnings.warn(
```

With PR : 

```
/tmp/foo.py:14: RemovedInMarshmallow4Warning: The 'default' argument to fields is deprecated. Use 'dump_default' instead.
  custom_field = CustomField(default=True, missing=True, foo=1)
```

lafrech: This makes sense.

Should we wrap this in a `utils.deprecate` function?

Also please add yourself to AUTHORS file.
tirkarthi: Thanks, added myself to authors. `stacklevel` depends on how deeply the deprecation warning is raised and 2 is more common since it's direct call. In case of nested calls 2 might print stacklevel that is not useful. I feel it's okay to add stacklevel to individual instances instead of a wrapper function for this.

Patch:
diff --git a/AUTHORS.rst b/AUTHORS.rst
index a877bc46..7f557b7f 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -168,3 +168,4 @@ Contributors (chronological)
 - Ben Windsor `@bwindsor <https://github.com/bwindsor>`_
 - Kevin Kirsche `@kkirsche <https://github.com/kkirsche>`_
 - Isira Seneviratne `@Isira-Seneviratne <https://github.com/Isira-Seneviratne>`_
+- Karthikeyan Singaravelan `@tirkarthi  <https://github.com/tirkarthi>`_
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 77922a0a..5fb2ba6d 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -4,10 +4,17 @@ Changelog
 (unreleased)
 ************
 
+Features:
+
+- Raise `ValueError` if an invalid value is passed to the ``unknown`` argument (:issue:`1721`, :issue:`1732`).
+  Thanks :user:`sirosen` for the PR.
+
 Other changes:
 
 - Set lower bound for `packaging` requirement (:issue:`1957`).
   Thanks :user:`MatthewNicolTR` for reporting and thanks :user:`sirosen` for the PR.
+- Improve warning messages by passing `stacklevel` (:pr:`1986`).
+  Thanks :user:`tirkarthi` for the PR.
 
 3.15.0 (2022-03-12)
 *******************
diff --git a/src/marshmallow/fields.py b/src/marshmallow/fields.py
index 09df0d99..6b26126e 100644
--- a/src/marshmallow/fields.py
+++ b/src/marshmallow/fields.py
@@ -174,6 +174,7 @@ class Field(FieldABC):
                 "The 'default' argument to fields is deprecated. "
                 "Use 'dump_default' instead.",
                 RemovedInMarshmallow4Warning,
+                stacklevel=2,
             )
             if dump_default is missing_:
                 dump_default = default
@@ -182,6 +183,7 @@ class Field(FieldABC):
                 "The 'missing' argument to fields is deprecated. "
                 "Use 'load_default' instead.",
                 RemovedInMarshmallow4Warning,
+                stacklevel=2,
             )
             if load_default is missing_:
                 load_default = missing
@@ -220,6 +222,7 @@ class Field(FieldABC):
                 "explicit `metadata=...` argument instead. "
                 f"Additional metadata: {additional_metadata}",
                 RemovedInMarshmallow4Warning,
+                stacklevel=2,
             )
 
         self._creation_index = Field._creation_index
@@ -298,6 +301,7 @@ class Field(FieldABC):
                 key
             ),
             RemovedInMarshmallow4Warning,
+            stacklevel=2,
         )
         raise self.make_error(key=key, **kwargs)
 
@@ -439,6 +443,7 @@ class Field(FieldABC):
             "The 'default' attribute of fields is deprecated. "
             "Use 'dump_default' instead.",
             RemovedInMarshmallow4Warning,
+            stacklevel=2,
         )
         return self.dump_default
 
@@ -448,6 +453,7 @@ class Field(FieldABC):
             "The 'default' attribute of fields is deprecated. "
             "Use 'dump_default' instead.",
             RemovedInMarshmallow4Warning,
+            stacklevel=2,
         )
         self.dump_default = value
 
@@ -457,6 +463,7 @@ class Field(FieldABC):
             "The 'missing' attribute of fields is deprecated. "
             "Use 'load_default' instead.",
             RemovedInMarshmallow4Warning,
+            stacklevel=2,
         )
         return self.load_default
 
@@ -466,6 +473,7 @@ class Field(FieldABC):
             "The 'missing' attribute of fields is deprecated. "
             "Use 'load_default' instead.",
             RemovedInMarshmallow4Warning,
+            stacklevel=2,
         )
         self.load_default = value
 
@@ -550,6 +558,7 @@ class Nested(Field):
                 "Passing 'self' to `Nested` is deprecated. "
                 "Use `Nested(lambda: MySchema(...))` instead.",
                 RemovedInMarshmallow4Warning,
+                stacklevel=2,
             )
         self.nested = nested
         self.only = only
diff --git a/src/marshmallow/schema.py b/src/marshmallow/schema.py
index ff119a46..00e697e1 100644
--- a/src/marshmallow/schema.py
+++ b/src/marshmallow/schema.py
@@ -34,6 +34,7 @@ from marshmallow.utils import (
     get_value,
     is_collection,
     is_instance_or_subclass,
+    validate_unknown_parameter_value,
 )
 from marshmallow.warnings import RemovedInMarshmallow4Warning
 
@@ -227,7 +228,7 @@ class SchemaOpts:
         self.include = getattr(meta, "include", {})
         self.load_only = getattr(meta, "load_only", ())
         self.dump_only = getattr(meta, "dump_only", ())
-        self.unknown = getattr(meta, "unknown", RAISE)
+        self.unknown = validate_unknown_parameter_value(getattr(meta, "unknown", RAISE))
         self.register = getattr(meta, "register", True)
 
 
@@ -389,7 +390,11 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
         self.load_only = set(load_only) or set(self.opts.load_only)
         self.dump_only = set(dump_only) or set(self.opts.dump_only)
         self.partial = partial
-        self.unknown = unknown or self.opts.unknown
+        self.unknown = (
+            self.opts.unknown
+            if unknown is None
+            else validate_unknown_parameter_value(unknown)
+        )
         self.context = context or {}
         self._normalize_nested_options()
         #: Dictionary mapping field_names -> :class:`Field` objects
@@ -833,7 +838,11 @@ class Schema(base.SchemaABC, metaclass=SchemaMeta):
         error_store = ErrorStore()
         errors = {}  # type: dict[str, list[str]]
         many = self.many if many is None else bool(many)
-        unknown = unknown or self.unknown
+        unknown = (
+            self.unknown
+            if unknown is None
+            else validate_unknown_parameter_value(unknown)
+        )
         if partial is None:
             partial = self.partial
         # Run preprocessors
diff --git a/src/marshmallow/utils.py b/src/marshmallow/utils.py
index 2aa6dfe1..fd651335 100644
--- a/src/marshmallow/utils.py
+++ b/src/marshmallow/utils.py
@@ -20,6 +20,7 @@ from marshmallow.warnings import RemovedInMarshmallow4Warning
 EXCLUDE = "exclude"
 INCLUDE = "include"
 RAISE = "raise"
+_UNKNOWN_VALUES = {EXCLUDE, INCLUDE, RAISE}
 
 
 class _Missing:
@@ -83,6 +84,7 @@ def pprint(obj, *args, **kwargs) -> None:
     warnings.warn(
         "marshmallow's pprint function is deprecated and will be removed in marshmallow 4.",
         RemovedInMarshmallow4Warning,
+        stacklevel=2,
     )
     if isinstance(obj, collections.OrderedDict):
         print(json.dumps(obj, *args, **kwargs))
@@ -333,3 +335,11 @@ def timedelta_to_microseconds(value: dt.timedelta) -> int:
     https://github.com/python/cpython/blob/bb3e0c240bc60fe08d332ff5955d54197f79751c/Lib/datetime.py#L665-L667  # noqa: B950
     """
     return (value.days * (24 * 3600) + value.seconds) * 1000000 + value.microseconds
+
+
+def validate_unknown_parameter_value(obj: typing.Any) -> str:
+    if obj not in _UNKNOWN_VALUES:
+        raise ValueError(
+            f"Object {obj!r} is not a valid value for the 'unknown' parameter"
+        )
+    return obj


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
index c00189df..4943fbd8 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -1,50 +1,47 @@
 import datetime as dt
 import decimal
-import random
 import math
-from collections import namedtuple, OrderedDict
-
-import simplejson as json
+import random
+from collections import OrderedDict, namedtuple
 
 import pytest
-
+import simplejson as json
 from marshmallow import (
+    EXCLUDE,
+    INCLUDE,
+    RAISE,
     Schema,
+    class_registry,
     fields,
     utils,
     validates,
     validates_schema,
-    EXCLUDE,
-    INCLUDE,
-    RAISE,
-    class_registry,
 )
 from marshmallow.exceptions import (
-    ValidationError,
-    StringNotCollectionError,
     RegistryError,
+    StringNotCollectionError,
+    ValidationError,
 )
 
 from tests.base import (
-    UserSchema,
-    UserMetaSchema,
-    UserRelativeUrlSchema,
-    ExtendedUserSchema,
-    UserIntSchema,
-    UserFloatStringSchema,
-    BlogSchema,
-    BlogUserMetaSchema,
+    Blog,
     BlogOnlySchema,
-    UserExcludeSchema,
-    UserAdditionalSchema,
+    BlogSchema,
     BlogSchemaExclude,
     BlogSchemaMeta,
+    BlogUserMetaSchema,
+    ExtendedUserSchema,
     User,
+    UserAdditionalSchema,
+    UserExcludeSchema,
+    UserFloatStringSchema,
+    UserIntSchema,
+    UserMetaSchema,
+    UserRelativeUrlSchema,
+    UserSchema,
     mockjson,
-    Blog,
 )
 
-
 random.seed(1)
 
 
@@ -2912,3 +2909,27 @@ def test_class_registry_returns_schema_type():
 
     SchemaClass = class_registry.get_class(DefinitelyUniqueSchema.__name__)
     assert SchemaClass is DefinitelyUniqueSchema
+
+
+@pytest.mark.parametrize("usage_location", ["meta", "init", "load"])
+def test_unknown_parameter_value_is_validated(usage_location):
+    class MySchema(Schema):
+        foo = fields.String()
+
+    with pytest.raises(
+        ValueError,
+        match="Object 'badval' is not a valid value for the 'unknown' parameter",
+    ):
+        # Meta.unknown setting gets caught at class creation time, since that's when
+        # metaclass __new__ runs
+        if usage_location == "meta":
+
+            class SubSchema(MySchema):
+                class Meta:
+                    unknown = "badval"
+
+        # usages in init and load are caught at call time, as expected
+        elif usage_location == "init":
+            MySchema(unknown="badval")
+        else:
+            MySchema().load({"foo": "bar"}, unknown="badval")