diff --git a/python-flask-restx.changes b/python-flask-restx.changes index 2f33775..145ce32 100644 --- a/python-flask-restx.changes +++ b/python-flask-restx.changes @@ -1,3 +1,8 @@ +------------------------------------------------------------------- +Fri Jul 15 13:18:17 UTC 2022 - Markéta Machová + +- Add upstream patches werkzeug.patch and redirect.patch to fix the tests. + ------------------------------------------------------------------- Mon Mar 21 09:38:48 UTC 2022 - pgajdos@suse.com diff --git a/python-flask-restx.spec b/python-flask-restx.spec index 45a0a70..4c59b99 100644 --- a/python-flask-restx.spec +++ b/python-flask-restx.spec @@ -24,6 +24,10 @@ Release: 0 Summary: Framework for fast, easy and documented API development with Flask License: BSD-3-Clause Group: Development/Languages/Python +#PATCH-FIX-UPSTREAM https://github.com/python-restx/flask-restx/pull/423 Handle change to Werkzeug 2.1.0 change to Request.get_json(). +Patch0: werkzeug.patch +#PATCH-FIX-UPSTREAM https://github.com/python-restx/flask-restx/pull/427 Handle Werkzeug 2.1.0 change to Response.autocorrect_location_header. +Patch1: redirect.patch URL: https://github.com/python-restx/flask-restx Source: https://github.com/python-restx/flask-restx/archive/%{version}.tar.gz BuildRequires: %{python_module Faker} @@ -62,6 +66,7 @@ its documentation properly using Swagger. %prep %setup -q -n flask-restx-%{version} +%autopatch -p1 %build %python_build diff --git a/redirect.patch b/redirect.patch new file mode 100644 index 0000000..03da5ab --- /dev/null +++ b/redirect.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/werkzeug.patch b/werkzeug.patch new file mode 100644 index 0000000..c268e69 --- /dev/null +++ b/werkzeug.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, ++ } + ]