diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e50bab3d..fc9f8781 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -12,6 +12,12 @@ Features: on schemas may be used to alter this. For example, `unknown=marshmallow.EXCLUDE` will produce behavior similar to webargs v5 +Bug fixes: + +* *Backwards-incompatible*: all parsers now require the Content-Type to be set + correctly when processing JSON request bodies. This impacts ``DjangoParser``, + ``FalconParser``, ``FlaskParser``, and ``PyramidParser`` + Refactoring: * *Backwards-incompatible*: Schema fields may not specify a location any diff --git a/docs/advanced.rst b/docs/advanced.rst index 475ac7e5..e29c9cd1 100644 --- a/docs/advanced.rst +++ b/docs/advanced.rst @@ -6,7 +6,7 @@ This section includes guides for advanced usage patterns. Custom Location Handlers ------------------------ -To add your own custom location handler, write a function that receives a request, an argument name, and a :class:`Field `, then decorate that function with :func:`Parser.location_handler `. +To add your own custom location handler, write a function that receives a request, and a :class:`Schema `, then decorate that function with :func:`Parser.location_loader `. .. code-block:: python @@ -15,13 +15,13 @@ To add your own custom location handler, write a function that receives a reques from webargs.flaskparser import parser - @parser.location_handler("data") - def parse_data(request, name, field): - return request.data.get(name) + @parser.location_loader("data") + def load_data(request, schema): + return request.data # Now 'data' can be specified as a location - @parser.use_args({"per_page": fields.Int()}, locations=("data",)) + @parser.use_args({"per_page": fields.Int()}, location="data") def posts(args): return "displaying {} posts".format(args["per_page"]) @@ -43,7 +43,20 @@ locations. The `json_or_form` location does this -- first trying to load data as JSON and then falling back to a form body -- and its implementation is quite simple: -.. autofunction:: webargs.core.Parser.load_json_or_form + +.. code-block:: python + + def load_json_or_form(self, req, schema): + """Load data from a request, accepting either JSON or form-encoded + data. + + The data will first be loaded as JSON, and, if that fails, it will be + loaded as a form post. + """ + data = self.load_json(req, schema) + if data is not missing: + return data + return self.load_form(req, schema) You can imagine your own locations with custom behaviors like this. @@ -112,7 +125,7 @@ When you need more flexibility in defining input schemas, you can pass a marshma # You can add additional parameters - @use_kwargs({"posts_per_page": fields.Int(missing=10, location="query")}) + @use_kwargs({"posts_per_page": fields.Int(missing=10)}, location="query") @use_args(UserSchema()) def profile_posts(args, posts_per_page): username = args["username"] @@ -259,12 +272,12 @@ Using the :class:`Method ` and :class:`Function ` and implement the `parse_*` method(s) you need to override. For example, here is a custom Flask parser that handles nested query string arguments. +To add your own parser, extend :class:`Parser ` and implement the `load_*` method(s) you need to override. For example, here is a custom Flask parser that handles nested query string arguments. .. code-block:: python @@ -293,8 +306,8 @@ To add your own parser, extend :class:`Parser ` and impleme } """ - def parse_querystring(self, req, name, field): - return core.get_value(_structure_dict(req.args), name, field) + def load_querystring(self, req, schema): + return _structure_dict(req.args) def _structure_dict(dict_): @@ -357,7 +370,7 @@ For example, you might implement JSON PATCH according to `RFC 6902 `: - -.. code-block:: python - - @app.route("/stacked", methods=["POST"]) - @use_args( - {"page": fields.Int(), "q": fields.Str(), "name": fields.Str()}, - locations=("query", "json"), - ) - def viewfunc(args): - page = args["page"] - # ... - -However, this allows ``page`` and ``q`` to be passed in the request body and ``name`` to be passed as a query parameter. - -To restrict the arguments to single locations without having to pass ``location`` to every field, you can call the `use_args ` multiple times: +Arguments for different locations can be specified by passing ``location`` to each `use_args ` call: .. code-block:: python - query_args = {"page": fields.Int(), "q": fields.Int()} - json_args = {"name": fields.Str()} - - + # "json" is the default, used explicitly below @app.route("/stacked", methods=["POST"]) - @use_args(query_args, locations=("query",)) - @use_args(json_args, locations=("json",)) + @use_args({"page": fields.Int(), "q": fields.Str()}, location="query") + @use_args({"name": fields.Str()}, location="json") def viewfunc(query_parsed, json_parsed): page = query_parsed["page"] name = json_parsed["name"] @@ -425,12 +404,12 @@ To reduce boilerplate, you could create shortcuts, like so: import functools - query = functools.partial(use_args, locations=("query",)) - body = functools.partial(use_args, locations=("json",)) + query = functools.partial(use_args, location="query") + body = functools.partial(use_args, location="json") - @query(query_args) - @body(json_args) + @query({"page": fields.Int(), "q": fields.Int()}) + @body({"name": fields.Str()}) def viewfunc(query_parsed, json_parsed): page = query_parsed["page"] name = json_parsed["name"] diff --git a/docs/framework_support.rst b/docs/framework_support.rst index c1b2f9a2..e58e5cd1 100644 --- a/docs/framework_support.rst +++ b/docs/framework_support.rst @@ -22,9 +22,9 @@ When using the :meth:`use_args ` decor @app.route("/user/") - @use_args({"per_page": fields.Int()}) + @use_args({"per_page": fields.Int()}, location="query") def user_detail(args, uid): - return ("The user page for user {uid}, " "showing {per_page} posts.").format( + return ("The user page for user {uid}, showing {per_page} posts.").format( uid=uid, per_page=args["per_page"] ) @@ -64,7 +64,7 @@ The `FlaskParser` supports parsing values from a request's ``view_args``. @app.route("/greeting//") - @use_args({"name": fields.Str(location="view_args")}) + @use_args({"name": fields.Str()}, location="view_args") def greeting(args, **kwargs): return "Hello {}".format(args["name"]) @@ -95,7 +95,7 @@ When using the :meth:`use_args ` dec } - @use_args(account_args) + @use_args(account_args, location="form") def login_user(request, args): if request.method == "POST": login(args["username"], args["password"]) @@ -114,7 +114,7 @@ When using the :meth:`use_args ` dec class BlogPostView(View): - @use_args(blog_args) + @use_args(blog_args, location="query") def get(self, request, args): blog_post = Post.objects.get(title__iexact=args["title"], author=args["author"]) return render_to_response("post_template.html", {"post": blog_post}) @@ -239,7 +239,7 @@ When using the :meth:`use_args ` d from webargs.pyramidparser import use_args - @use_args({"uid": fields.Str(), "per_page": fields.Int()}) + @use_args({"uid": fields.Str(), "per_page": fields.Int()}, location="query") def user_detail(request, args): uid = args["uid"] return Response( @@ -261,7 +261,7 @@ The `PyramidParser` supports parsing values from a request's matchdict. from webargs.pyramidparser import use_args - @use_args({"mymatch": fields.Int()}, locations=("matchdict",)) + @use_args({"mymatch": fields.Int()}, location="matchdict") def matched(request, args): return Response("The value for mymatch is {}".format(args["mymatch"])) @@ -317,7 +317,7 @@ You can easily implement hooks by using `parser.parse ` supports parsing value from webargs.aiohttpparser import use_args - @parser.use_args({"slug": fields.Str(location="match_info")}) + @parser.use_args({"slug": fields.Str()}, location="match_info") def article_detail(request, args): return web.Response(body="Slug: {}".format(args["slug"]).encode("utf-8")) diff --git a/docs/index.rst b/docs/index.rst index e152b9f3..6e2ae812 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,7 +17,7 @@ webargs is a Python library for parsing and validating HTTP request objects, wit @app.route("/") - @use_args({"name": fields.Str(required=True)}) + @use_args({"name": fields.Str(required=True)}, location="query") def index(args): return "Hello " + args["name"] @@ -28,13 +28,15 @@ webargs is a Python library for parsing and validating HTTP request objects, wit # curl http://localhost:5000/\?name\='World' # Hello World -Webargs will automatically parse: +By default Webargs will automatically parse JSON request bodies. But it also +has support for: **Query Parameters** :: + $ curl http://localhost:5000/\?name\='Freddie' + Hello Freddie - $ curl http://localhost:5000/\?name\='Freddie' - Hello Freddie + # pass location="query" to use_args **Form Data** :: @@ -42,12 +44,16 @@ Webargs will automatically parse: $ curl -d 'name=Brian' http://localhost:5000/ Hello Brian + # pass location="form" to use_args + **JSON Data** :: $ curl -X POST -H "Content-Type: application/json" -d '{"name":"Roger"}' http://localhost:5000/ Hello Roger + # pass location="json" (or omit location) to use_args + and, optionally: - Headers diff --git a/docs/quickstart.rst b/docs/quickstart.rst index b43371d6..c0a51f5f 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -23,17 +23,11 @@ Arguments are specified as a dictionary of name -> :class:`Field ` to add nested field functionality to the other locations. + Of the default supported locations in webargs, only the ``json`` request location supports nested datastructures. You can, however, :ref:`implement your own data loader ` to add nested field functionality to the other locations. Next Steps ----------