Skip to content

Commit

Permalink
Rewrite narrative docs to be correct in v6
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sirosen committed Nov 24, 2019
1 parent 071391b commit 6b92b2a
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 78 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.rst
Expand Up @@ -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
Expand Down
87 changes: 33 additions & 54 deletions docs/advanced.rst
Expand Up @@ -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 <marshmallow.fields.Field>`, then decorate that function with :func:`Parser.location_handler <webargs.core.Parser.location_handler>`.
To add your own custom location handler, write a function that receives a request, and a :class:`Schema <marshmallow.Schema>`, then decorate that function with :func:`Parser.location_loader <webargs.core.Parser.location_loader>`.


.. code-block:: python
Expand All @@ -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"])
Expand All @@ -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.
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -259,12 +272,12 @@ Using the :class:`Method <marshmallow.fields.Method>` and :class:`Function <mars
cube = args["cube"]
# ...
.. _custom-parsers:
.. _custom-loaders:

Custom Parsers
--------------

To add your own parser, extend :class:`Parser <webargs.core.Parser>` 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 <webargs.core.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
Expand Down Expand Up @@ -293,8 +306,8 @@ To add your own parser, extend :class:`Parser <webargs.core.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_):
Expand Down Expand Up @@ -357,7 +370,7 @@ For example, you might implement JSON PATCH according to `RFC 6902 <https://tool
@app.route("/profile/", methods=["patch"])
@use_args(PatchSchema(many=True), locations=("json",))
@use_args(PatchSchema(many=True))
def patch_blog(args):
"""Implements JSON Patch for the user profile
Expand All @@ -372,48 +385,14 @@ For example, you might implement JSON PATCH according to `RFC 6902 <https://tool
Mixing Locations
----------------

Arguments for different locations can be specified by passing ``location`` to each field individually:

.. code-block:: python
@app.route("/stacked", methods=["POST"])
@use_args(
{
"page": fields.Int(location="query"),
"q": fields.Str(location="query"),
"name": fields.Str(location="json"),
}
)
def viewfunc(args):
page = args["page"]
# ...
Alternatively, you can pass multiple locations to `use_args <webargs.core.Parser.use_args>`:

.. 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 <webargs.core.Parser.use_args>` multiple times:
Arguments for different locations can be specified by passing ``location`` to each `use_args <webargs.core.Parser.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"]
Expand All @@ -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"]
Expand Down
18 changes: 9 additions & 9 deletions docs/framework_support.rst
Expand Up @@ -22,9 +22,9 @@ When using the :meth:`use_args <webargs.flaskparser.FlaskParser.use_args>` decor
@app.route("/user/<int:uid>")
@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"]
)
Expand Down Expand Up @@ -64,7 +64,7 @@ The `FlaskParser` supports parsing values from a request's ``view_args``.
@app.route("/greeting/<name>/")
@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"])
Expand Down Expand Up @@ -95,7 +95,7 @@ When using the :meth:`use_args <webargs.djangoparser.DjangoParser.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"])
Expand All @@ -114,7 +114,7 @@ When using the :meth:`use_args <webargs.djangoparser.DjangoParser.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})
Expand Down Expand Up @@ -239,7 +239,7 @@ When using the :meth:`use_args <webargs.pyramidparser.PyramidParser.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(
Expand All @@ -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"]))
Expand Down Expand Up @@ -317,7 +317,7 @@ You can easily implement hooks by using `parser.parse <webargs.falconparser.Falc
return hook
@falcon.before(add_args({"page": fields.Int(location="query")}))
@falcon.before(add_args({"page": fields.Int()}, location="query"))
class AuthorResource:
def on_get(self, req, resp):
args = req.context["args"]
Expand Down Expand Up @@ -414,7 +414,7 @@ The `AIOHTTPParser <webargs.aiohttpparser.AIOHTTPParser>` 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"))
Expand Down
14 changes: 10 additions & 4 deletions docs/index.rst
Expand Up @@ -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"]
Expand All @@ -28,26 +28,32 @@ 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**
::

$ 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
Expand Down
16 changes: 5 additions & 11 deletions docs/quickstart.rst
Expand Up @@ -23,17 +23,11 @@ Arguments are specified as a dictionary of name -> :class:`Field <marshmallow.fi
"nickname": fields.List(fields.Str()),
# Delimited list, e.g. "/?languages=python,javascript"
"languages": fields.DelimitedList(fields.Str()),
# When you know where an argument should be parsed from
"active": fields.Bool(location="query"),
# When value is keyed on a variable-unsafe name
# or you want to rename a key
"content_type": fields.Str(load_from="Content-Type", location="headers"),
"user_type": fields.Str(load_from="user-type"),
# OR, on marshmallow 3
# "content_type": fields.Str(data_key="Content-Type", location="headers"),
# File uploads
"profile_image": fields.Field(
location="files", validate=lambda f: f.mimetype in ["image/jpeg", "image/png"]
),
# "content_type": fields.Str(data_key="user-type"),
}
.. note::
Expand Down Expand Up @@ -105,12 +99,12 @@ As an alternative to `Parser.parse`, you can decorate your view with :meth:`use_
Request "Locations"
-------------------

By default, webargs will search for arguments from the URL query string (e.g. ``"/?name=foo"``), form data, and JSON data (in that order). You can explicitly specify which locations to search, like so:
By default, webargs will search for arguments from the request body as JSON. You can specify a different location from which to load data like so:

.. code-block:: python
@app.route("/register")
@use_args(user_args, locations=("json", "form"))
@use_args(user_args, location="form")
def register(args):
return "registration page"
Expand Down Expand Up @@ -243,7 +237,7 @@ Nesting Fields
.. note::

By default, webargs only parses nested fields using the ``json`` request location. You can, however, :ref:`implement your own parser <custom-parsers>` 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 <custom-loaders>` to add nested field functionality to the other locations.

Next Steps
----------
Expand Down

0 comments on commit 6b92b2a

Please sign in to comment.