Skip to content

Commit f6e92fa

Browse files
authored
Merge pull request #1023 from marshmallow-code/bump_frameworks
Bump supported framework versions
2 parents c25f908 + f7e2b5b commit f6e92fa

12 files changed

Lines changed: 64 additions & 214 deletions

File tree

.github/workflows/build-release.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,8 @@ jobs:
1313
fail-fast: false
1414
matrix:
1515
include:
16-
- { name: "3.10", python: "3.10", tox: py310-marshmallow3 }
17-
- { name: "3.14", python: "3.14", tox: py314-marshmallow3 }
18-
- { name: "3.10", python: "3.10", tox: py310-marshmallow4 }
19-
- { name: "3.14", python: "3.14", tox: py314-marshmallow4 }
16+
- { name: "3.10", python: "3.10", tox: py310-marshmallow }
17+
- { name: "3.14", python: "3.14", tox: py314-marshmallow }
2018
- { name: "lowest", python: "3.10", tox: py310-lowest }
2119
- { name: "dev", python: "3.14", tox: py314-marshmallowdev }
2220
steps:

pyproject.toml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ keywords = [
4242
]
4343
requires-python = ">=3.10"
4444
dependencies = [
45-
"marshmallow>=3.13.0,<5.0.0",
45+
"marshmallow>=4.0.0,<5.0.0",
4646
"packaging>=17.0",
4747
# depend on typing-extensions conditionally for annotations
4848
'typing_extensions>=4.0; python_version<"3.10"',
@@ -57,13 +57,13 @@ Tidelift = "https://tidelift.com/subscription/pkg/pypi-webargs?utm_source=pypi-w
5757

5858
[project.optional-dependencies]
5959
frameworks = [
60-
"Flask>=0.12.5",
61-
"Django>=2.2.0",
60+
"Flask>=3.1.0",
61+
"Django>=5.2.0",
6262
"bottle>=0.13.0",
63-
"tornado>=6.0.0",
64-
"pyramid>=1.9.1",
65-
"falcon>=2.0.0",
66-
"aiohttp>=3.0.8",
63+
"tornado>=6.5.0",
64+
"pyramid>=2.0.2",
65+
"falcon>=4.1.0",
66+
"aiohttp>=3.13.0",
6767
]
6868
tests = [
6969
"webargs[frameworks]",

src/webargs/fields.py

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Field classes.
22
3-
Includes all fields from `marshmallow.fields` in addition to a custom
4-
`Nested` field and `DelimitedList`.
3+
Includes all fields from `marshmallow.fields` in addition to `DelimitedList`.
54
65
All fields can optionally take a special `location` keyword argument, which
76
tells webargs where to parse the request argument from.
@@ -26,30 +25,6 @@
2625
__all__ = ["DelimitedList", "DelimitedTuple"] + ma.fields.__all__
2726

2827

29-
# TODO: remove custom `Nested` in the next major release
30-
#
31-
# the `Nested` class is only needed on versions of marshmallow prior to v3.15.0
32-
# in that version, `ma.fields.Nested` gained the ability to consume dict inputs
33-
# prior to that, this subclass adds this capability
34-
#
35-
# if we drop support for ma.__version_info__ < (3, 15) we can do this
36-
class Nested(ma.fields.Nested): # type: ignore[no-redef]
37-
"""Same as `marshmallow.fields.Nested`, except can be passed a dictionary
38-
as the first argument, which will be converted to a `marshmallow.Schema`.
39-
40-
.. note::
41-
42-
The schema class here will always be `marshmallow.Schema`, regardless
43-
of whether a custom schema class is set on the parser. Pass an explicit schema
44-
class if necessary.
45-
"""
46-
47-
def __init__(self, nested, *args, **kwargs):
48-
if isinstance(nested, dict):
49-
nested = ma.Schema.from_dict(nested)
50-
super().__init__(nested, *args, **kwargs)
51-
52-
5328
class DelimitedFieldMixin:
5429
"""
5530
This is a mixin class for subclasses of ma.fields.List and ma.fields.Tuple

tests/apps/django_app/__init__.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1 @@
1-
import importlib.metadata
21

3-
DJANGO_MAJOR_VERSION = int(importlib.metadata.version("django").split(".")[0])
4-
DJANGO_SUPPORTS_ASYNC = DJANGO_MAJOR_VERSION >= 3

tests/apps/falcon_app.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import importlib.metadata
2-
31
import falcon
42
import marshmallow as ma
53

@@ -10,9 +8,6 @@
108
hello_args = {"name": fields.Str(load_default="World", validate=validate.Length(min=3))}
119
hello_multiple = {"name": fields.List(fields.Str())}
1210

13-
FALCON_MAJOR_VERSION = int(importlib.metadata.version("falcon").split(".")[0])
14-
FALCON_SUPPORTS_ASYNC = FALCON_MAJOR_VERSION >= 3
15-
1611

1712
class HelloSchema(ma.Schema):
1813
name = fields.Str(load_default="World", validate=validate.Length(min=3))
@@ -25,10 +20,7 @@ class HelloSchema(ma.Schema):
2520

2621

2722
def set_text(resp, value):
28-
if FALCON_MAJOR_VERSION >= 3:
29-
resp.text = value
30-
else:
31-
resp.body = value
23+
resp.text = value
3224

3325

3426
class Echo:
@@ -191,11 +183,7 @@ def on_get(self, req, resp):
191183

192184

193185
def create_app():
194-
if FALCON_MAJOR_VERSION >= 3:
195-
app = falcon.App()
196-
else:
197-
app = falcon.API()
198-
186+
app = falcon.App()
199187
app.add_route("/echo", Echo())
200188
app.add_route("/echo_form", EchoForm())
201189
app.add_route("/echo_json", EchoJSON())

tests/apps/flask_app.py

Lines changed: 42 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import importlib.metadata
2-
31
import marshmallow as ma
42
from flask import Flask, Response, request
53
from flask import jsonify as J
@@ -13,9 +11,6 @@
1311
use_kwargs,
1412
)
1513

16-
FLASK_MAJOR_VERSION = int(importlib.metadata.version("flask").split(".")[0])
17-
FLASK_SUPPORTS_ASYNC = FLASK_MAJOR_VERSION >= 2
18-
1914

2015
class TestAppConfig:
2116
TESTING = True
@@ -138,12 +133,10 @@ def echo_headers_raising(args):
138133
return J(args)
139134

140135

141-
if FLASK_SUPPORTS_ASYNC:
142-
143-
@app.route("/echo_headers_raising_async")
144-
@use_args(HelloSchema(), location="headers", unknown=None)
145-
async def echo_headers_raising_async(args):
146-
return J(args)
136+
@app.route("/echo_headers_raising_async")
137+
@use_args(HelloSchema(), location="headers", unknown=None)
138+
async def echo_headers_raising_async(args):
139+
return J(args)
147140

148141

149142
@app.route("/echo_cookie")
@@ -165,14 +158,12 @@ def echo_view_arg(view_arg):
165158
return J(parser.parse({"view_arg": fields.Int()}, location="view_args"))
166159

167160

168-
if FLASK_SUPPORTS_ASYNC:
169-
170-
@app.route("/echo_view_arg_async/<view_arg>")
171-
async def echo_view_arg_async(view_arg):
172-
parsed_view_arg = await parser.async_parse(
173-
{"view_arg": fields.Int()}, location="view_args"
174-
)
175-
return J(parsed_view_arg)
161+
@app.route("/echo_view_arg_async/<view_arg>")
162+
async def echo_view_arg_async(view_arg):
163+
parsed_view_arg = await parser.async_parse(
164+
{"view_arg": fields.Int()}, location="view_args"
165+
)
166+
return J(parsed_view_arg)
176167

177168

178169
@app.route("/echo_view_arg_use_args/<view_arg>")
@@ -181,12 +172,10 @@ def echo_view_arg_with_use_args(args, **kwargs):
181172
return J(args)
182173

183174

184-
if FLASK_SUPPORTS_ASYNC:
185-
186-
@app.route("/echo_view_arg_use_args_async/<view_arg>")
187-
@use_args({"view_arg": fields.Int()}, location="view_args")
188-
async def echo_view_arg_with_use_args_async(args, **kwargs):
189-
return J(args)
175+
@app.route("/echo_view_arg_use_args_async/<view_arg>")
176+
@use_args({"view_arg": fields.Int()}, location="view_args")
177+
async def echo_view_arg_with_use_args_async(args, **kwargs):
178+
return J(args)
190179

191180

192181
@app.route("/echo_nested", methods=["POST"])
@@ -211,16 +200,12 @@ def echo_nested_many_with_data_key():
211200
return J(parser.parse(args))
212201

213202

214-
if FLASK_SUPPORTS_ASYNC:
215-
216-
@app.route("/echo_nested_many_data_key_async", methods=["POST"])
217-
async def echo_nested_many_with_data_key_async():
218-
args = {
219-
"x_field": fields.Nested(
220-
{"id": fields.Int()}, many=True, data_key="X-Field"
221-
)
222-
}
223-
return J(await parser.async_parse(args))
203+
@app.route("/echo_nested_many_data_key_async", methods=["POST"])
204+
async def echo_nested_many_with_data_key_async():
205+
args = {
206+
"x_field": fields.Nested({"id": fields.Int()}, many=True, data_key="X-Field")
207+
}
208+
return J(await parser.async_parse(args))
224209

225210

226211
class EchoMethodViewUseArgs(MethodView):
@@ -235,17 +220,16 @@ def post(self, args):
235220
)
236221

237222

238-
if FLASK_SUPPORTS_ASYNC:
223+
class EchoMethodViewUseArgsAsync(MethodView):
224+
@use_args({"val": fields.Int()})
225+
async def post(self, args):
226+
return J(args)
239227

240-
class EchoMethodViewUseArgsAsync(MethodView):
241-
@use_args({"val": fields.Int()})
242-
async def post(self, args):
243-
return J(args)
244228

245-
app.add_url_rule(
246-
"/echo_method_view_use_args_async",
247-
view_func=EchoMethodViewUseArgsAsync.as_view("echo_method_view_use_args_async"),
248-
)
229+
app.add_url_rule(
230+
"/echo_method_view_use_args_async",
231+
view_func=EchoMethodViewUseArgsAsync.as_view("echo_method_view_use_args_async"),
232+
)
249233

250234

251235
class EchoMethodViewUseKwargs(MethodView):
@@ -259,19 +243,17 @@ def post(self, val):
259243
view_func=EchoMethodViewUseKwargs.as_view("echo_method_view_use_kwargs"),
260244
)
261245

262-
if FLASK_SUPPORTS_ASYNC:
263246

264-
class EchoMethodViewUseKwargsAsync(MethodView):
265-
@use_kwargs({"val": fields.Int()})
266-
async def post(self, val):
267-
return J({"val": val})
247+
class EchoMethodViewUseKwargsAsync(MethodView):
248+
@use_kwargs({"val": fields.Int()})
249+
async def post(self, val):
250+
return J({"val": val})
268251

269-
app.add_url_rule(
270-
"/echo_method_view_use_kwargs_async",
271-
view_func=EchoMethodViewUseKwargsAsync.as_view(
272-
"echo_method_view_use_kwargs_async"
273-
),
274-
)
252+
253+
app.add_url_rule(
254+
"/echo_method_view_use_kwargs_async",
255+
view_func=EchoMethodViewUseKwargsAsync.as_view("echo_method_view_use_kwargs_async"),
256+
)
275257

276258

277259
@app.route("/echo_use_kwargs_missing", methods=["post"])
@@ -281,13 +263,11 @@ def echo_use_kwargs_missing(username, **kwargs):
281263
return J({"username": username})
282264

283265

284-
if FLASK_SUPPORTS_ASYNC:
285-
286-
@app.route("/echo_use_kwargs_missing_async", methods=["post"])
287-
@use_kwargs({"username": fields.Str(required=True), "password": fields.Str()})
288-
async def echo_use_kwargs_missing_async(username, **kwargs):
289-
assert "password" not in kwargs
290-
return J({"username": username})
266+
@app.route("/echo_use_kwargs_missing_async", methods=["post"])
267+
@use_kwargs({"username": fields.Str(required=True), "password": fields.Str()})
268+
async def echo_use_kwargs_missing_async(username, **kwargs):
269+
assert "password" not in kwargs
270+
return J({"username": username})
291271

292272

293273
# Return validation errors as JSON

tests/test_core.py

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import collections
22
import datetime
3-
import importlib.metadata
43
import typing
54
from unittest import mock
65

@@ -16,15 +15,12 @@
1615
pre_load,
1716
validates_schema,
1817
)
19-
from packaging.version import Version
2018
from werkzeug.datastructures import MultiDict as WerkMultiDict
2119

2220
from webargs import ValidationError, fields
2321
from webargs.core import Parser, get_mimetype, is_json
2422
from webargs.multidictproxy import MultiDictProxy
2523

26-
MARSHMALLOW_VERSION = Version(importlib.metadata.version("marshmallow"))
27-
2824

2925
class MockHTTPError(Exception):
3026
def __init__(self, status_code, headers):
@@ -557,27 +553,6 @@ def test_required_with_custom_error(parser, web_request):
557553
assert "We need foo" in excinfo.value.messages["json"]["foo"]
558554

559555

560-
@pytest.mark.filterwarnings("ignore:Returning `False` from a validator is deprecated")
561-
@pytest.mark.skipif(
562-
MARSHMALLOW_VERSION.major >= 4,
563-
reason="marshmallow 4+ does not support validators returning False",
564-
)
565-
def test_required_with_custom_error_and_validation_error(parser, web_request):
566-
web_request.json = {"foo": ""}
567-
args = {
568-
"foo": fields.Str(
569-
required="We need foo",
570-
validate=lambda s: len(s) > 1,
571-
error_messages={"validator_failed": "foo required length is 3"},
572-
)
573-
}
574-
with pytest.raises(ValidationError) as excinfo:
575-
# Test that `validate` receives dictionary of args
576-
parser.parse(args, web_request)
577-
578-
assert "foo required length is 3" in excinfo.value.args[0]["foo"]
579-
580-
581556
def test_full_input_validator_receives_nonascii_input(web_request):
582557
def validate(val):
583558
return False

tests/test_djangoparser.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import pytest
22

3-
from tests.apps.django_app import DJANGO_SUPPORTS_ASYNC
43
from tests.apps.django_app.base.wsgi import application
54
from webargs.testing import CommonTestCase
65

@@ -29,14 +28,8 @@ def test_use_args_in_class_based_view_with_path_param(self, testapp):
2928
res = testapp.get("/echo_use_args_with_path_param_cbv/42?name=Fred")
3029
assert res.json == {"name": "Fred"}
3130

32-
@pytest.mark.skipif(
33-
not DJANGO_SUPPORTS_ASYNC, reason="requires a django version with async support"
34-
)
3531
def test_parse_querystring_args_async(self, testapp):
3632
assert testapp.get("/async_echo?name=Fred").json == {"name": "Fred"}
3733

38-
@pytest.mark.skipif(
39-
not DJANGO_SUPPORTS_ASYNC, reason="requires a django version with async support"
40-
)
4134
def test_async_use_args_decorator(self, testapp):
4235
assert testapp.get("/async_echo_use_args?name=Fred").json == {"name": "Fred"}

tests/test_falconparser.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import falcon.testing
22
import pytest
33

4-
from tests.apps.falcon_app import FALCON_SUPPORTS_ASYNC, create_app, create_async_app
4+
from tests.apps.falcon_app import create_app, create_async_app
55
from webargs.testing import CommonTestCase
66

77

@@ -73,17 +73,11 @@ def test_body_parsing_works_with_simulate(self):
7373
)
7474
assert res.json == {"name": "Fred"}
7575

76-
@pytest.mark.skipif(
77-
not FALCON_SUPPORTS_ASYNC, reason="requires a falcon version with async support"
78-
)
7976
def test_parse_querystring_args_async(self):
8077
app = create_async_app()
8178
client = falcon.testing.TestClient(app)
8279
assert client.simulate_get("/async_echo?name=Fred").json == {"name": "Fred"}
8380

84-
@pytest.mark.skipif(
85-
not FALCON_SUPPORTS_ASYNC, reason="requires a falcon version with async support"
86-
)
8781
def test_async_use_args_decorator(self):
8882
app = create_async_app()
8983
client = falcon.testing.TestClient(app)

0 commit comments

Comments
 (0)