diff --git a/423.patch b/423.patch new file mode 100644 index 0000000..76475c9 --- /dev/null +++ b/423.patch @@ -0,0 +1,290 @@ +From bb72a51860ea8a42c928f69bdd44ad20b1f9ee7e Mon Sep 17 00:00:00 2001 +From: "Stacy W. Smith" +Date: Mon, 28 Mar 2022 17:48:36 -0600 +Subject: [PATCH 1/2] Handle Werkzeug 2.1.0 change to `Request.get_json()`. + +pallets/werkzeug#2339 changed the behavior of `Request.get_json()` +and the `Request.json` property to raise a `BadRequest` if `Request.get_json()` +is called without `silent=True`, or the `Request.json` property is accessed, +and the content type is not `"application/json"`. + +Argument parsing allows parsing from multiple locations, and defaults to +`["json", "values"]`, but if the locations include `"json"` and the content +type is not `"application/json"`, a `BadRequest` is now raised with Werkzeug >= 2.1.0. + +Invoking `Request.get_json()` with the `silent=True` parameter now handles +the situation where `"json"` is included in the locations, but the content type +is not `"application/json"`. +--- + flask_restx/reqparse.py | 10 ++++++++-- + tests/test_reqparse.py | 18 +++++++++++------- + 2 files changed, 19 insertions(+), 9 deletions(-) + +diff --git a/flask_restx/reqparse.py b/flask_restx/reqparse.py +index 63260660..6fc327b9 100644 +--- a/flask_restx/reqparse.py ++++ b/flask_restx/reqparse.py +@@ -138,7 +138,10 @@ def source(self, request): + :param request: The flask request object to parse arguments from + """ + if isinstance(self.location, six.string_types): +- value = getattr(request, self.location, MultiDict()) ++ if self.location in {"json", "get_json"}: ++ value = request.get_json(silent=True) ++ else: ++ value = getattr(request, self.location, MultiDict()) + if callable(value): + value = value() + if value is not None: +@@ -146,7 +149,10 @@ def source(self, request): + else: + values = MultiDict() + for l in self.location: +- value = getattr(request, l, None) ++ if l in {"json", "get_json"}: ++ value = request.get_json(silent=True) ++ else: ++ value = getattr(request, l, None) + if callable(value): + value = value() + if value is not None: +diff --git a/tests/test_reqparse.py b/tests/test_reqparse.py +index 18710f3b..174f2988 100644 +--- a/tests/test_reqparse.py ++++ b/tests/test_reqparse.py +@@ -41,8 +41,9 @@ def test_help(self, app, mocker): + ) + parser = RequestParser() + parser.add_argument("foo", choices=("one", "two"), help="Bad choice.") +- req = mocker.Mock(["values"]) ++ req = mocker.Mock(["values", "get_json"]) + req.values = MultiDict([("foo", "three")]) ++ req.get_json.return_value = None + with pytest.raises(BadRequest): + parser.parse_args(req) + expected = { +@@ -58,7 +59,8 @@ def test_no_help(self, app, mocker): + ) + parser = RequestParser() + parser.add_argument("foo", choices=["one", "two"]) +- req = mocker.Mock(["values"]) ++ req = mocker.Mock(["values", "get_json"]) ++ req.get_json.return_value = None + req.values = MultiDict([("foo", "three")]) + with pytest.raises(BadRequest): + parser.parse_args(req) +@@ -76,9 +78,9 @@ def test_viewargs(self, mocker): + args = parser.parse_args(req) + assert args["foo"] == "bar" + +- req = mocker.Mock() ++ req = mocker.Mock(["get_json"]) + req.values = () +- req.json = None ++ req.get_json.return_value = None + req.view_args = {"foo": "bar"} + parser = RequestParser() + parser.add_argument("foo", store_missing=True) +@@ -101,11 +103,12 @@ def test_parse_unicode_app(self, app): + args = parser.parse_args() + assert args["foo"] == "barß" + +- @pytest.mark.request_context("/bubble", method="post") ++ @pytest.mark.request_context( ++ "/bubble", method="post", content_type="application/json" ++ ) + def test_json_location(self): + parser = RequestParser() + parser.add_argument("foo", location="json", store_missing=True) +- + args = parser.parse_args() + assert args["foo"] is None + +@@ -856,7 +859,8 @@ def test_source_bad_location(self, mocker): + assert len(arg.source(req)) == 0 # yes, basically you don't find it + + def test_source_default_location(self, mocker): +- req = mocker.Mock(["values"]) ++ req = mocker.Mock(["values", "get_json"]) ++ req.get_json.return_value = None + req._get_child_mock = lambda **kwargs: MultiDict() + arg = Argument("foo") + assert arg.source(req) == req.values + +From 57c5beb313e2b297c10ec3fb305c37c0760e9209 Mon Sep 17 00:00:00 2001 +From: "Stacy W. Smith" +Date: Fri, 1 Apr 2022 15:04:45 -0600 +Subject: [PATCH 2/2] Black formatting changes in modified files which are + unrelated to the PR change. + +--- + flask_restx/reqparse.py | 5 ++- + tests/test_reqparse.py | 84 ++++++++++++++++++++++++++++++++++------- + 2 files changed, 75 insertions(+), 14 deletions(-) + +diff --git a/flask_restx/reqparse.py b/flask_restx/reqparse.py +index 6fc327b9..18ce6cf9 100644 +--- a/flask_restx/reqparse.py ++++ b/flask_restx/reqparse.py +@@ -106,7 +106,10 @@ def __init__( + required=False, + ignore=False, + type=text_type, +- location=("json", "values",), ++ location=( ++ "json", ++ "values", ++ ), + choices=(), + action="store", + help=None, +diff --git a/tests/test_reqparse.py b/tests/test_reqparse.py +index 174f2988..3ac89a7a 100644 +--- a/tests/test_reqparse.py ++++ b/tests/test_reqparse.py +@@ -914,28 +914,47 @@ def test_unknown_type(self): + parser = RequestParser() + parser.add_argument("unknown", type=lambda v: v) + assert parser.__schema__ == [ +- {"name": "unknown", "type": "string", "in": "query",} ++ { ++ "name": "unknown", ++ "type": "string", ++ "in": "query", ++ } + ] + + def test_required(self): + parser = RequestParser() + parser.add_argument("int", type=int, required=True) + assert parser.__schema__ == [ +- {"name": "int", "type": "integer", "in": "query", "required": True,} ++ { ++ "name": "int", ++ "type": "integer", ++ "in": "query", ++ "required": True, ++ } + ] + + def test_default(self): + parser = RequestParser() + parser.add_argument("int", type=int, default=5) + assert parser.__schema__ == [ +- {"name": "int", "type": "integer", "in": "query", "default": 5,} ++ { ++ "name": "int", ++ "type": "integer", ++ "in": "query", ++ "default": 5, ++ } + ] + + def test_default_as_false(self): + parser = RequestParser() + parser.add_argument("bool", type=inputs.boolean, default=False) + assert parser.__schema__ == [ +- {"name": "bool", "type": "boolean", "in": "query", "default": False,} ++ { ++ "name": "bool", ++ "type": "boolean", ++ "in": "query", ++ "default": False, ++ } + ] + + def test_choices(self): +@@ -958,31 +977,59 @@ def test_location(self): + parser.add_argument("in_headers", type=int, location="headers") + parser.add_argument("in_cookie", type=int, location="cookie") + assert parser.__schema__ == [ +- {"name": "default", "type": "integer", "in": "query",}, +- {"name": "in_values", "type": "integer", "in": "query",}, +- {"name": "in_query", "type": "integer", "in": "query",}, +- {"name": "in_headers", "type": "integer", "in": "header",}, ++ { ++ "name": "default", ++ "type": "integer", ++ "in": "query", ++ }, ++ { ++ "name": "in_values", ++ "type": "integer", ++ "in": "query", ++ }, ++ { ++ "name": "in_query", ++ "type": "integer", ++ "in": "query", ++ }, ++ { ++ "name": "in_headers", ++ "type": "integer", ++ "in": "header", ++ }, + ] + + def test_location_json(self): + parser = RequestParser() + parser.add_argument("in_json", type=str, location="json") + assert parser.__schema__ == [ +- {"name": "in_json", "type": "string", "in": "body",} ++ { ++ "name": "in_json", ++ "type": "string", ++ "in": "body", ++ } + ] + + def test_location_form(self): + parser = RequestParser() + parser.add_argument("in_form", type=int, location="form") + assert parser.__schema__ == [ +- {"name": "in_form", "type": "integer", "in": "formData",} ++ { ++ "name": "in_form", ++ "type": "integer", ++ "in": "formData", ++ } + ] + + def test_location_files(self): + parser = RequestParser() + parser.add_argument("in_files", type=FileStorage, location="files") + assert parser.__schema__ == [ +- {"name": "in_files", "type": "file", "in": "formData",} ++ { ++ "name": "in_files", ++ "type": "file", ++ "in": "formData", ++ } + ] + + def test_form_and_body_location(self): +@@ -1012,7 +1059,13 @@ def test_models(self): + ) + parser = RequestParser() + parser.add_argument("todo", type=todo_fields) +- assert parser.__schema__ == [{"name": "todo", "type": "Todo", "in": "body",}] ++ assert parser.__schema__ == [ ++ { ++ "name": "todo", ++ "type": "Todo", ++ "in": "body", ++ } ++ ] + + def test_lists(self): + parser = RequestParser() +@@ -1065,5 +1118,10 @@ def test_callable_default(self): + parser = RequestParser() + parser.add_argument("int", type=int, default=lambda: 5) + assert parser.__schema__ == [ +- {"name": "int", "type": "integer", "in": "query", "default": 5,} ++ { ++ "name": "int", ++ "type": "integer", ++ "in": "query", ++ "default": 5, ++ } + ] diff --git a/427.patch b/427.patch new file mode 100644 index 0000000..b8abf93 --- /dev/null +++ b/427.patch @@ -0,0 +1,41 @@ +From bb3e9dd83b9d4c0d0fa0de7d7ff713fae71eccee Mon Sep 17 00:00:00 2001 +From: "Stacy W. Smith" +Date: Sat, 2 Apr 2022 08:25:55 -0600 +Subject: [PATCH] Handle Werkzeug 2.1.0 change to + Response.autocorrect_location_header. + +Fixes #426 + +pallets/werkzeug#2352 changed the default value of Response.autocorrect_location_header from True to False in Werkzeug >= 2.1.0. + +tests/legacy/test_api_legacy.py::APITest::test_redirect depended upon Response.autocorrect_location_header being True. + +Change `test_redirect()` to explicitly set `Response.autocorrect_location_header` to `False`, for backwards compatibility, and change the expected result for the test from an absolute URL to the relative URL. +--- + tests/legacy/test_api_legacy.py | 8 ++++++-- + 1 file changed, 6 insertions(+), 2 deletions(-) + +diff --git a/tests/legacy/test_api_legacy.py b/tests/legacy/test_api_legacy.py +index 5d6649c8..24d7586b 100644 +--- a/tests/legacy/test_api_legacy.py ++++ b/tests/legacy/test_api_legacy.py +@@ -373,13 +373,17 @@ def get(self): + def test_redirect(self, api, client): + class FooResource(restx.Resource): + def get(self): +- return redirect("/") ++ response = redirect("/") ++ # Response.autocorrect_location_header = False is now the default in Werkzeug >= 2.1 ++ # It is explicitly set here so the test remains backwards compatible with previous versions of Werkzeug. ++ response.autocorrect_location_header = False ++ return response + + api.add_resource(FooResource, "/api") + + resp = client.get("/api") + assert resp.status_code == 302 +- assert resp.headers["Location"] == "http://localhost/" ++ assert resp.headers["Location"] == "/" + + def test_calling_owns_endpoint_before_api_init(self): + api = restx.Api() diff --git a/python-flask-restx.spec b/python-flask-restx.spec index d665c5e..32b4b88 100644 --- a/python-flask-restx.spec +++ b/python-flask-restx.spec @@ -9,7 +9,12 @@ Release: %autorelease Summary: Framework for fast, easy and documented API development with Flask License: BSD URL: https://github.com/python-restx/flask-restx -Source0: %pypi_source +Source0: %{url}/archive/%{version}/%{srcname}-%{version}.tar.gz +# Werkzeug >= 2.1 support +# https://github.com/python-restx/flask-restx/pull/423 +Patch01: 423.patch +# https://github.com/python-restx/flask-restx/pull/427 +Patch02: 427.patch BuildArch: noarch %global _description %{expand: @@ -35,7 +40,7 @@ Obsoletes: python3-flask-restplus = 0.13.0 %prep -%autosetup -n %{srcname}-%{version} +%autosetup -p1 -n %{srcname}-%{version} rm -rf %{libname}.egg-info rm -f %{libname}/static/files/.npmignore diff --git a/sources b/sources index 702344e..fbf9d31 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -SHA512 (flask-restx-0.5.1.tar.gz) = 6eb357b2650862aa8f1181808967b573b8956d24d38f32280a286d204191ba30eb661f058699b0f768c5778297ed28480ab7781b153a5035e66bb62d16f1421b +SHA512 (flask-restx-0.5.1.tar.gz) = 0e4ca95a6bb45f7dfba1758387aded6215ec64fa55060889e6843fb50eed09fda96ba936eacd0d8f27b29f5dec2468d4945386f25ff4de6a51f571e16617cf52