8000 Deprecation warnings for marshmallow 4 changes (#2732) · marshmallow-code/marshmallow@8ca596d · GitHub
[go: up one dir, main page]

Skip to content

Commit 8ca596d

Browse files
authored
Deprecation warnings for marshmallow 4 changes (#2732)
* Warn when instantiating base fields * Deprecate returning False from validators
1 parent 71ab95a commit 8ca596d

File tree

11 files changed

+221
-151
lines changed

11 files changed

+221
-151
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
10000
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,14 @@ Bug fixes:
1515

1616
Deprecations:
1717

18+
- Custom validators should raise a `ValidationError <marshmallow.exceptions.ValidationError>` for invalid values.
19+
Returning `False`` is no longer supported .
1820
- Deprecate ``context`` parameter of `Schema <marshmallow.schema.Schema>` (:issue:`1826`).
1921
Use `contextVars.ContextVar` to pass context data instead.
22+
- `Field <marshmallow.fields.Field>`, `Mapping <marshmallow.fields.Mapping>`,
23+
and `Number <marshmallow.fields.Number>` should no longer be used as fields within schemas.
24+
Use their subclasses instead.
25+
2026

2127
3.23.3 (2025-01-03)
2228
*******************

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,6 @@ no_implicit_optional = true
9090
[tool.pytest.ini_options]
9191
norecursedirs = ".git .ropeproject .tox docs env venv tests/mypy_test_cases"
9292
addopts = "-v --tb=short"
93+
filterwarnings = [
94+
"ignore:Returning `False` from a validator:marshmallow.warnings.ChangedInMarshmallow4Warning",
95+
]

src/marshmallow/fields.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@
2929
missing as missing_,
3030
)
3131
from marshmallow.validate import And, Length
32-
from marshmallow.warnings import RemovedInMarshmallow4Warning
32+
from marshmallow.warnings import (
33+
ChangedInMarshmallow4Warning,
34+
RemovedInMarshmallow4Warning,
35+
)
3336

3437
if typing.TYPE_CHECKING:
3538
from marshmallow.schema import Schema, SchemaMeta
@@ -136,6 +139,16 @@ class Field(FieldABC):
136139
"validator_failed": "Invalid value.",
137140
}
138141

142+
def __new__(cls, *args, **kwargs):
143+
if cls is Field:
144+
warnings.warn(
145+
"`Field` should not be instantiated. Use `fields.Raw` or "
146+
"another field subclass instead.",
147+
ChangedInMarshmallow4Warning,
148+
stacklevel=2,
149+
)
150+
return super().__new__(cls)
151+
139152
def __init__(
140153
self,
141154
*,
@@ -949,6 +962,15 @@ class Number(Field, typing.Generic[_NumType]):
949962
"too_large": "Number too large.",
950963
}
951964

965+
def __new__(cls, *args, **kwargs):
966+
if cls is Number:
967+
warnings.warn(
968+
"`Number` field should not be instantiated. Use `Integer`, `Float`, or `Decimal` instead.",
969+
ChangedInMarshmallow4Warning,
970+
stacklevel=2,
971+
)
972+
return super().__new__(cls)
973+
952974
def __init__(self, *, as_string: bool = False, **kwargs):
953975
self.as_string = as_string
954976
super().__init__(**kwargs)
@@ -1556,6 +1578,15 @@ class Mapping(Field):
15561578
#: Default error messages.
15571579
default_error_messages = {"invalid": "Not a valid mapping type."}
15581580

1581+
def __new__(cls, *args, **kwargs):
1582+
if cls is Mapping:
1583+
warnings.warn(
1584+
"`Mapping` field should not be instantiated. Use `Dict` instead.",
1585+
ChangedInMarshmallow4Warning,
1586+
stacklevel=2,
1587+
)
1588+
return super().__new__(cls)
1589+
15591590
def __init__(
15601591
self,
15611592
keys: Field | type[Field] | None = None,
@@ -1868,7 +1899,7 @@ class Enum(Field):
18681899
or Field class or instance to use to (de)serialize by value. Defaults to False.
18691900
18701901
If `by_value` is `False` (default), enum members are (de)serialized by symbol (name).
1871-
If it is `True`, they are (de)serialized by value using :class:`Field`.
1902+
If it is `True`, they are (de)serialized by value using :class:`Raw`.
18721903
If it is a field instance or class, they are (de)serialized by value using this field.
18731904
18741905
.. versionadded:: 3.18.0
@@ -1898,7 +1929,7 @@ def __init__(
18981929
# Serialization by value
18991930
else:
19001931
if by_value is True:
1901-
self.field = Field()
1932+
self.field = Raw()
19021933
else:
19031934
try:
19041935
self.field = resolve_field_instance(by_value)

src/marshmallow/validate.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44

55
import re
66
import typing
7+
import warnings
78
from abc import ABC, abstractmethod
89
from itertools import zip_longest
910
from operator import attrgetter
1011

1112
from marshmallow import types
1213
from marshmallow.exceptions import ValidationError
14+
from marshmallow.warnings import ChangedInMarshmallow4Warning
1315

1416
_T = typing.TypeVar("_T")
1517

@@ -77,6 +79,12 @@ def __call__(self, value: typing.Any) -> typing.Any:
7779
try:
7880
r = validator(value)
7981
if not isinstance(validator, Validator) and r is False:
82+
warnings.warn(
83+
"Returning `False` from a validator is deprecated. "
84+
"Raise a `ValidationError` instead.",
85+
ChangedInMarshmallow4Warning,
86+
stacklevel=2,
87+
)
8088
raise ValidationError(self.error)
8189
except ValidationError as err:
8290
kwargs.update(err.kwargs)

src/marshmallow/warnings.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
1-
class RemovedInMarshmallow4Warning(DeprecationWarning):
1+
class Marshmallow4Warning(DeprecationWarning):
2+
pass
3+
4+
5+
class ChangedInMarshmallow4Warning(Marshmallow4Warning):
6+
pass
7+
8+
9+
class RemovedInMarshmallow4Warning(Marshmallow4Warning):
210
pass

tests/base.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ class DateEnum(Enum):
3838
fields.Integer,
3939
fields.Boolean,
4040
fields.Float,
41-
fields.Number,
4241
fields.DateTime,
4342
fields.Time,
4443
fields.Date,

tests/test_decorators.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def dump_none(self, item, **kwargs):
127127
class TestPassOriginal:
128128
def test_pass_original_single(self):
129129
class MySchema(Schema):
130-
foo = fields.Field()
130+
foo = fields.Raw()
131131

132132
@post_load(pass_original=True)
133133
def post_load(self, data, original_data, **kwargs):
@@ -154,7 +154,7 @@ def post_dump(self, data, obj, **kwargs):
154154

155155
def test_pass_original_many(self):
156156
class MySchema(Schema):
157-
foo = fields.Field()
157+
foo = fields.Raw()
158158

159159
@post_load(pass_many=True, pass_original=True)
160160
def post_load(self, data, original, many, **kwargs):

tests/test_deserialization.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
from marshmallow import EXCLUDE, INCLUDE, RAISE, Schema, fields, validate
1111
from marshmallow.exceptions import ValidationError
1212
from marshmallow.validate import Equal
13-
from marshmallow.warnings import RemovedInMarshmallow4Warning
13+
from marshmallow.warnings import (
14+
ChangedInMarshmallow4Warning,
15+
RemovedInMarshmallow4Warning,
16+
)
1417
from tests.base import (
1518
ALL_FIELDS,
1619
DateEnum,
@@ -50,7 +53,7 @@ def test_fields_dont_allow_none_by_default(self, FieldClass):
5053
field.deserialize(None)
5154

5255
def test_allow_none_is_true_if_missing_is_true(self):
53-
field = fields.Field(load_default=None)
56+
field = fields.Raw(load_default=None)
5457
assert field.allow_none is True
5558
assert field.deserialize(None) is None
5659

@@ -402,7 +405,8 @@ def test_field_toggle_show_invalid_value_in_error_message(self):
402405
boolfield.deserialize("notabool")
403406
assert str(excinfo.value.args[0]) == "Not valid: notabool"
404407

405-
numfield = fields.Number(error_messages=error_messages)
408+
with pytest.warns(ChangedInMarshmallow4Warning):
409+
numfield = fields.Number(error_messages=error_messages)
406410
with pytest.raises(ValidationError) as excinfo:
407411
numfield.deserialize("notanum")
408412
assert str(excinfo.value.args[0]) == "Not valid: notanum"
@@ -1410,7 +1414,7 @@ def __call__(self, val):
14101414
return True
14111415
return False
14121416

1413-
field = fields.Field(validate=MyValidator())
1417+
field = fields.Raw(validate=MyValidator())
14141418
assert field.deserialize("valid") == "valid"
14151419
with pytest.raises(ValidationError, match="Invalid value."):
14161420
field.deserialize("invalid")
@@ -1422,7 +1426,7 @@ def validator(val):
14221426
raise ValidationError(["err1", "err2"])
14231427

14241428
class MySchema(Schema):
1425-
foo = fields.Field(validate=validator)
1429+
foo = fields.Raw(validate=validator)
14261430

14271431
errors = MySchema().validate({"foo": 42})
14281432
assert errors["foo"] == ["err1", "err2"]
@@ -1654,7 +1658,7 @@ class AliasingUserSerializer(Schema):
16541658
# regression test for https://github.com/marshmallow-code/marshmallow/issues/450
16551659
def test_deserialize_with_attribute_param_symmetry(self):
16561660
class MySchema(Schema):
1657-
foo = fields.Field(attribute="bar.baz")
1661+
foo = fields.Raw(attribute="bar.baz")
16581662

16591663
schema = MySchema()
16601664
dump_data = schema.dump({"bar": {"baz": 42}})
@@ -1705,7 +1709,7 @@ class AliasingUserSerializer(Schema):
17051709

17061710
def test_deserialize_with_data_key_as_empty_string(self):
17071711
class MySchema(Schema):
1708-
name = fields.Field(data_key="")
1712+
name = fields.Raw(data_key="")
17091713

17101714
schema = MySchema()
17111715
assert schema.load({"": "Grace"}) == {"name": "Grace"}
@@ -1797,7 +1801,7 @@ def validate_field(val):
17971801
raise ValidationError("Something went wrong")
17981802

17991803
class MySchema(Schema):
1800-
foo = fields.Field(validate=validate_field)
1804+
foo = fields.Raw(validate=validate_field)
18011805

18021806
with pytest.raises(ValidationError) as excinfo:
18031807
MySchema().load({"foo": 42})
@@ -1812,7 +1816,7 @@ def validate_with_error(n):
18121816
raise ValidationError("foo is not valid")
18131817

18141818
class MySchema(Schema):
1815-
foo = fields.Field(
1819+
foo = fields.Raw(
18161820
required=True, validate=[validate_with_bool, validate_with_error]
18171821
)
18181822

@@ -1851,7 +1855,7 @@ class MySchema(Schema):
18511855

18521856
def test_required_value_only_passed_to_validators_if_provided(self):
18531857
class MySchema(Schema):
1854-
foo = fields.Field(required=True, validate=lambda f: False)
1858+
foo = fields.Raw(required=True, validate=lambda f: False)
18551859

18561860
with pytest.raises(ValidationError) as excinfo:
18571861
MySchema().load({})
@@ -1863,8 +1867,8 @@ class MySchema(Schema):
18631867
@pytest.mark.parametrize("partial_schema", [True, False])
18641868
def test_partial_deserialization(self, partial_schema):
18651869
class MySchema(Schema):
1866-
foo = fields.Field(required=True)
1867-
bar = fields.Field(required=True)
1870+
foo = fields.Raw(required=True)
1871+
bar = fields.Raw(required=True)
18681872

18691873
schema_args = {}
18701874
load_args = {}
@@ -1879,9 +1883,9 @@ class MySchema(Schema):
18791883

18801884
def test_partial_fields_deserialization(self):
18811885
class MySchema(Schema):
1882-
foo = fields.Field(required=True)
1883-
bar = fields.Field(required=True)
1884-
baz = fields.Field(required=True)
1886+
foo = fields.Raw(required=True)
1887+
bar = fields.Raw(required=True)
1888+
baz = fields.Raw(required=True)
18851889

18861890
with pytest.raises(ValidationError) as excinfo:
18871891
MySchema().load({"foo": 3}, partial=tuple())
@@ -1902,9 +1906,9 @@ class MySchema(Schema):
19021906

19031907
def test_partial_fields_validation(self):
19041908
class MySchema(Schema):
1905-
foo = fields.Field(required=True)
1906-
bar = fields.Field(required=True)
1907-
baz = fields.Field(required=True)
1909+
foo = fields.Raw(required=True)
1910+
bar = fields.Raw(required=True)
1911+
baz = fields.Raw(required=True)
19081912

19091913
errors = MySchema().validate({"foo": 3}, partial=tuple())
19101914
assert "bar" in errors
@@ -2291,8 +2295,8 @@ class RequireSchema(Schema):
22912295
@pytest.mark.parametrize("data", [True, False, 42, None, []])
22922296
def test_deserialize_raises_exception_if_input_type_is_incorrect(data, unknown):
22932297
class MySchema(Schema):
2294-
foo = fields.Field()
2295-
bar = fields.Field()
2298+
foo = fields.Raw()
2299+
bar = fields.Raw()
22962300

22972301
with pytest.raises(ValidationError, match="Invalid input type.") as excinfo:
22982302
MySchema(unknown=unknown).load(data)

tests/test_fields.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ def test_field_aliases(alias, field):
3030
class TestField:
3131
def test_repr(self):
3232
default = "œ∑´"
33-
field = fields.Field(dump_default=default, attribute=None)
33+
field = fields.Raw(dump_default=default, attribute=None)
3434
assert repr(field) == (
35-
f"<fields.Field(dump_default={default!r}, attribute=None, "
35+
f"<fields.Raw(dump_default={default!r}, attribute=None, "
3636
"validate=None, required=False, "
3737
"load_only=False, dump_only=False, "
3838
f"load_default={missing}, allow_none=False, "
@@ -43,13 +43,13 @@ def test_repr(self):
4343

4444
def test_error_raised_if_uncallable_validator_passed(self):
4545
with pytest.raises(ValueError, match="must be a callable"):
46-
fields.Field(validate="notcallable")
46+
fields.Raw(validate="notcallable")
4747

4848
def test_error_raised_if_missing_is_set_on_required_field(self):
4949
with pytest.raises(
5050
ValueError, match="'load_default' must not be set for required fields"
5151
):
52-
fields.Field(required=True, load_default=42)
52+
fields.Raw(required=True, load_default=42)
5353

5454
def test_custom_field_receives_attr_and_obj(self):
5555
class MyField(fields.Field):
@@ -93,10 +93,10 @@ class MySchema(Schema):
9393

9494
class TestParentAndName:
9595
class MySchema(Schema):
96-
foo = fields.Field()
96+
foo = fields.Raw()
9797
bar = fields.List(fields.Str())
9898
baz = fields.Tuple([fields.Str(), fields.Int()])
99-
bax = fields.Mapping(fields.Str(), fields.Int())
99+
bax = fields.Dict(fields.Str(), fields.Int())
100100

101101
@pytest.fixture()
102102
def schema(self):
@@ -188,7 +188,7 @@ class Meta:
188188
# Regression test for https://github.com/marshmallow-code/marshmallow/issues/1808
189189
def test_field_named_parent_has_root(self, schema):
190190
class MySchema(Schema):
191-
parent = fields.Field()
191+
parent = fields.Raw()
192192

193193
schema = MySchema()
194194
assert schema.fields["parent"].root == schema

0 commit comments

Comments
 (0)
0