From 6b92b2a6f03a401617a34b5903204b100d4d7ac1 Mon Sep 17 00:00:00 2001 From: Stephen Rosen Date: Tue, 17 Sep 2019 20:54:58 +0000 Subject: [PATCH] Rewrite narrative docs to be correct in v6 This is not a deep and comprehensive rewrite which aims to discuss/explain the functionality which is now available. Rather, this change merely takes everything in the docs which has become inaccurate and trims or modifies it to be accurate again. Add notes to changelog about making parsers more consistent about checking Content-Type for JSON payloads. --- CHANGELOG.rst | 6 +++ docs/advanced.rst | 87 +++++++++++++++----------------------- docs/framework_support.rst | 18 ++++---- docs/index.rst | 14 ++++-- docs/quickstart.rst | 16 +++---- 5 files changed, 63 insertions(+), 78 deletions(-) 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 ----------