Codebase list python-webargs / bcd95e2
Update upstream source from tag 'upstream/5.5.3' Update to upstream version '5.5.3' with Debian dir 498e56d7a77923be9eecbe236110d11ebf064bad Sophie Brun 3 years ago
57 changed file(s) with 1880 addition(s) and 2800 deletion(s). Raw diff Collapse all Expand all
00 repos:
1 - repo: https://github.com/asottile/pyupgrade
2 rev: v1.26.0
3 hooks:
4 - id: pyupgrade
5 args: ["--py3-plus"]
6 - repo: https://github.com/psf/black
7 rev: 19.10b0
1 - repo: https://github.com/python/black
2 rev: 19.3b0
83 hooks:
94 - id: black
10 args: ["--target-version", "py35"]
115 language_version: python3
126 - repo: https://gitlab.com/pycqa/flake8
13 rev: 3.7.9
7 rev: 3.7.8
148 hooks:
159 - id: flake8
16 additional_dependencies: [flake8-bugbear==20.1.0]
10 additional_dependencies: ['flake8-bugbear==19.8.0; python_version >= "3.5"']
1711 - repo: https://github.com/asottile/blacken-docs
18 rev: v1.5.0-1
12 rev: v1.3.0
1913 hooks:
2014 - id: blacken-docs
21 additional_dependencies: [black==19.10b0]
22 args: ["--target-version", "py35"]
15 additional_dependencies: [black==19.3b0]
2316 - repo: https://github.com/pre-commit/mirrors-mypy
24 rev: v0.761
17 rev: v0.730
2518 hooks:
2619 - id: mypy
2720 language_version: python3
44 Lead
55 ----
66
7 * Steven Loria `@sloria <https://github.com/sloria>`_
8 * Jérôme Lafréchoux `@lafrech <https://github.com/lafrech>`_
7 * Steven Loria <[email protected]>
8 * Jérôme Lafréchoux <https://github.com/lafrech>
99
1010 Contributors (chronological)
1111 ----------------------------
1212
13 * Steven Manuatu `@venuatu <https://github.com/venuatu>`_
14 * Javier Santacruz `@jvrsantacruz <https://github.com/jvrsantacruz>`_
15 * Josh Carp `@jmcarp <https://github.com/jmcarp>`_
16 * `@philtay <https://github.com/philtay>`_
17 * Andriy Yurchuk `@Ch00k <https://github.com/Ch00k>`_
18 * Stas Sușcov `@stas <https://github.com/stas>`_
19 * Josh Johnston `@Trii <https://github.com/Trii>`_
20 * Rory Hart `@hartror <https://github.com/hartror>`_
21 * Jace Browning `@jacebrowning <https://github.com/jacebrowning>`_
22 * marcellarius `@marcellarius <https://github.com/marcellarius>`_
23 * Damian Heard `@DamianHeard <https://github.com/DamianHeard>`_
24 * Daniel Imhoff `@dwieeb <https://github.com/dwieeb>`_
25 * `@immerrr <https://github.com/immerrr>`_
26 * Brett Higgins `@brettdh <https://github.com/brettdh>`_
27 * Vlad Frolov `@frol <https://github.com/frol>`_
28 * Tuukka Mustonen `@tuukkamustonen <https://github.com/tuukkamustonen>`_
29 * Francois-Xavier Darveau `@EFF <https://github.com/EFF>`_
30 * Jérôme Lafréchoux `@lafrech <https://github.com/lafrech>`_
31 * `@DmitriyS <https://github.com/DmitriyS>`_
32 * Svetlozar Argirov `@zaro <https://github.com/zaro>`_
33 * Florian S. `@nebularazer <https://github.com/nebularazer>`_
34 * `@daniel98321 <https://github.com/daniel98321>`_
35 * `@Itayazolay <https://github.com/Itayazolay>`_
36 * `@Reskov <https://github.com/Reskov>`_
37 * `@cedzz <https://github.com/cedzz>`_
38 * F. Moukayed (כוכב) `@kochab <https://github.com/kochab>`_
39 * Xiaoyu Lee `@lee3164 <https://github.com/lee3164>`_
40 * Jonathan Angelo `@jangelo <https://github.com/jangelo>`_
41 * `@zhenhua32 <https://github.com/zhenhua32>`_
42 * Martin Roy `@lindycoder <https://github.com/lindycoder>`_
43 * Kubilay Kocak `@koobs <https://github.com/koobs>`_
44 * Stephen Rosen `@sirosen <https://github.com/sirosen>`_
45 * `@dodumosu <https://github.com/dodumosu>`_
46 * Nate Dellinger `@Nateyo <https://github.com/Nateyo>`_
47 * Karthikeyan Singaravelan `@tirkarthi <https://github.com/tirkarthi>`_
48 * Sami Salonen `@suola <https://github.com/suola>`_
49 * Tim Gates `@timgates42 <https://github.com/timgates42>`_
50 * Lefteris Karapetsas `@lefterisjp <https://github.com/lefterisjp>`_
51 * Utku Gultopu `@ugultopu <https://github.com/ugultopu>`_
13 * @venuatu <https://github.com/venuatu>
14 * Javier Santacruz @jvrsantacruz <[email protected]>
15 * Josh Carp <https://github.com/jmcarp>
16 * @philtay <https://github.com/philtay>
17 * Andriy Yurchuk <https://github.com/Ch00k>
18 * Stas Sușcov <https://github.com/stas>
19 * Josh Johnston <https://github.com/Trii>
20 * Rory Hart <https://github.com/hartror>
21 * Jace Browning <https://github.com/jacebrowning>
22 * @marcellarius <https://github.com/marcellarius>
23 * Damian Heard <https://github.com/DamianHeard>
24 * Daniel Imhoff <https://github.com/dwieeb>
25 * immerrr <https://github.com/immerrr>
26 * Brett Higgins <https://github.com/brettdh>
27 * Vlad Frolov <https://github.com/frol>
28 * Tuukka Mustonen <https://github.com/tuukkamustonen>
29 * Francois-Xavier Darveau <https://github.com/EFF>
30 * Jérôme Lafréchoux <https://github.com/lafrech>
31 * @DmitriyS <https://github.com/DmitriyS>
32 * Svetlozar Argirov <https://github.com/zaro>
33 * Florian S. <https://github.com/nebularazer>
34 * @daniel98321 <https://github.com/daniel98321>
35 * @Itayazolay <https://github.com/Itayazolay>
36 * @Reskov <https://github.com/Reskov>
37 * @cedzz <https://github.com/cedzz>
38 * F. Moukayed (כוכב) <https://github.com/kochab>
39 * Xiaoyu Lee <https://github.com/lee3164>
40 * Jonathan Angelo <https://github.com/jangelo>
41 * @zhenhua32 <https://github.com/zhenhua32>
42 * Martin Roy <https://github.com/lindycoder>
00 Changelog
11 ---------
22
3 6.1.0 (2020-04-05)
4 ******************
5
6 Features:
7
8 * Add ``fields.DelimitedTuple`` when using marshmallow 3. This behaves as a
9 combination of ``fields.DelimitedList`` and ``marshmallow.fields.Tuple``. It
10 takes an iterable of fields, plus a delimiter (defaults to ``,``), and parses
11 delimiter-separated strings into tuples. (:pr:`509`)
12
13 * Add ``__str__`` and ``__repr__`` to MultiDictProxy to make it easier to work
14 with (:pr:`488`)
15
16 Support:
17
18 * Various docs updates (:pr:`482`, :pr:`486`, :pr:`489`, :pr:`498`, :pr:`508`).
19 Thanks :user:`lefterisjp`, :user:`timgates42`, and :user:`ugultopu` for the PRs.
20
21
22 6.0.0 (2020-02-27)
23 ******************
24
25 Features:
26
27 * ``FalconParser``: Pass request content length to ``req.stream.read`` to
28 provide compatibility with ``falcon.testing`` (:pr:`477`).
29 Thanks :user:`suola` for the PR.
30
31 * *Backwards-incompatible*: Factorize the ``use_args`` / ``use_kwargs`` branch
32 in all parsers. When ``as_kwargs`` is ``False``, arguments are now
33 consistently appended to the arguments list by the ``use_args`` decorator.
34 Before this change, the ``PyramidParser`` would prepend the argument list on
35 each call to ``use_args``. Pyramid view functions must reverse the order of
36 their arguments. (:pr:`478`)
37
38 6.0.0b8 (2020-02-16)
39 ********************
40
41 Refactoring:
42
43 * *Backwards-incompatible*: Use keyword-only arguments (:pr:`472`).
44
45 6.0.0b7 (2020-02-14)
46 ********************
47
48 Features:
49
50 * *Backwards-incompatible*: webargs will rewrite the error messages in
51 ValidationErrors to be namespaced under the location which raised the error.
52 The `messages` field on errors will therefore be one layer deeper with a
53 single top-level key.
54
55 6.0.0b6 (2020-01-31)
56 ********************
57
58 Refactoring:
59
60 * Remove the cache attached to webargs parsers. Due to changes between webargs
61 v5 and v6, the cache is no longer considered useful.
62
63 Other changes:
64
65 * Import ``Mapping`` from ``collections.abc`` in pyramidparser.py (:pr:`471`).
66 Thanks :user:`tirkarthi` for the PR.
67
68 6.0.0b5 (2020-01-30)
69 ********************
70
71 Refactoring:
72
73 * *Backwards-incompatible*: `DelimitedList` now requires that its input be a
74 string and always serializes as a string. It can still serialize and deserialize
75 using another field, e.g. `DelimitedList(Int())` is still valid and requires
76 that the values in the list parse as ints.
77
78 6.0.0b4 (2020-01-28)
79 ********************
80
81 Bug fixes:
82
83 * :cve:`CVE-2020-7965`: Don't attempt to parse JSON if request's content type is mismatched
84 (bugfix from 5.5.3).
85
86 6.0.0b3 (2020-01-21)
87 ********************
88
89 Features:
90
91 * *Backwards-incompatible*: Support Falcon 2.0. Drop support for Falcon 1.x
92 (:pr:`459`). Thanks :user:`dodumosu` and :user:`Nateyo` for the PR.
93
94 6.0.0b2 (2020-01-07)
95 ********************
96
97 Other changes:
98
99 * *Backwards-incompatible*: Drop support for Python 2 (:issue:`440`).
100 Thanks :user:`hugovk` for the PR.
101
102 6.0.0b1 (2020-01-06)
103 ********************
104
105 Features:
106
107 * *Backwards-incompatible*: Schemas will now load all data from a location, not
108 only data specified by fields. As a result, schemas with validators which
109 examine the full input data may change in behavior. The `unknown` parameter
110 on schemas may be used to alter this. For example,
111 `unknown=marshmallow.EXCLUDE` will produce a behavior similar to webargs v5.
112
113 Bug fixes:
114
115 * *Backwards-incompatible*: All parsers now require the Content-Type to be set
116 correctly when processing JSON request bodies. This impacts ``DjangoParser``,
117 ``FalconParser``, ``FlaskParser``, and ``PyramidParser``
118
119 Refactoring:
120
121 * *Backwards-incompatible*: Schema fields may not specify a location any
122 longer, and `Parser.use_args` and `Parser.use_kwargs` now accept `location`
123 (singular) instead of `locations` (plural). Instead of using a single field or
124 schema with multiple `locations`, users are recommended to make multiple
125 calls to `use_args` or `use_kwargs` with a distinct schema per location. For
126 example, code should be rewritten like this:
127
128 .. code-block:: python
129
130 # webargs 5.x and older
131 @parser.use_args(
132 {
133 "q1": ma.fields.Int(location="query"),
134 "q2": ma.fields.Int(location="query"),
135 "h1": ma.fields.Int(location="headers"),
136 },
137 locations=("query", "headers"),
138 )
139 def foo(q1, q2, h1):
140 ...
141
142
143 # webargs 6.x
144 @parser.use_args({"q1": ma.fields.Int(), "q2": ma.fields.Int()}, location="query")
145 @parser.use_args({"h1": ma.fields.Int()}, location="headers")
146 def foo(q1, q2, h1):
147 ...
148
149 * The `location_handler` decorator has been removed and replaced with
150 `location_loader`. `location_loader` serves the same purpose (letting you
151 write custom hooks for loading data) but its expected method signature is
152 different. See the docs on `location_loader` for proper usage.
153
154 Thanks :user:`sirosen` for the PR!
155
1563 5.5.3 (2020-01-28)
1574 ******************
1585
1596 Bug fixes:
1607
161 * :cve:`CVE-2020-7965`: Don't attempt to parse JSON if request's content type is mismatched.
8 * :cve:`CVE-2020-7965`: Don't attempt to parse JSON if the request's Content-Type is mismatched.
1629
16310 5.5.2 (2019-10-06)
16411 ******************
0 Copyright 2014-2020 Steven Loria and contributors
0 Copyright 2014-2019 Steven Loria and contributors
11
22 Permission is hereby granted, free of charge, to any person obtaining a copy
33 of this software and associated documentation files (the "Software"), to deal
0 graft tests
10 include LICENSE
21 include *.rst
3 include tox.ini
3535
3636
3737 @app.route("/")
38 @use_args({"name": fields.Str(required=True)}, location="query")
38 @use_args({"name": fields.Str(required=True)})
3939 def index(args):
4040 return "Hello " + args["name"]
4141
5353
5454 pip install -U webargs
5555
56 webargs supports Python >= 3.5.
56 webargs supports Python >= 2.7 or >= 3.5.
5757
5858
5959 Documentation
2525 parameters:
2626 toxenvs:
2727 - lint
28 - py27-marshmallow2
2829
2930 - py35-marshmallow2
3031 - py35-marshmallow3
3132
33 - py36-marshmallow2
3234 - py36-marshmallow3
3335
36 - py37-marshmallow2
3437 - py37-marshmallow3
3538
36 - py38-marshmallow2
37 - py38-marshmallow3
38
39 - py38-marshmallowdev
39 - py37-marshmallowdev
4040
4141 - docs
4242 os: linux
43 # Build wheels
43 # Build separate wheels for python 2 and 3
4444 - template: job--pypi-release.yml@sloria
4545 parameters:
4646 python: "3.7"
4747 distributions: "sdist bdist_wheel"
48 name_postfix: "_py3"
4849 dependsOn:
4950 - tox_linux
51 - template: job--pypi-release.yml@sloria
52 parameters:
53 python: "2.7"
54 distributions: "bdist_wheel"
55 name_postfix: "_py2"
56 dependsOn:
57 - tox_linux
55 Custom Location Handlers
66 ------------------------
77
8 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>`.
8 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>`.
99
1010
1111 .. code-block:: python
1414 from webargs.flaskparser import parser
1515
1616
17 @parser.location_loader("data")
18 def load_data(request, schema):
19 return request.data
17 @parser.location_handler("data")
18 def parse_data(request, name, field):
19 return request.data.get(name)
2020
2121
2222 # Now 'data' can be specified as a location
23 @parser.use_args({"per_page": fields.Int()}, location="data")
23 @parser.use_args({"per_page": fields.Int()}, locations=("data",))
2424 def posts(args):
2525 return "displaying {} posts".format(args["per_page"])
2626
27
28 .. NOTE::
29
30 The schema is passed so that it can be used to wrap multidict types and
31 unpack List fields correctly. If you are writing a loader for a multidict
32 type, consider looking at
33 :class:`MultiDictProxy <webargs.multidictproxy.MultiDictProxy>` for an
34 example of how to do this.
35
36 "meta" Locations
37 ~~~~~~~~~~~~~~~~
38
39 You can define your own locations which mix data from several existing
40 locations.
41
42 The `json_or_form` location does this -- first trying to load data as JSON and
43 then falling back to a form body -- and its implementation is quite simple:
44
45
46 .. code-block:: python
47
48 def load_json_or_form(self, req, schema):
49 """Load data from a request, accepting either JSON or form-encoded
50 data.
51
52 The data will first be loaded as JSON, and, if that fails, it will be
53 loaded as a form post.
54 """
55 data = self.load_json(req, schema)
56 if data is not missing:
57 return data
58 return self.load_form(req, schema)
59
60
61 You can imagine your own locations with custom behaviors like this.
62 For example, to mix query parameters and form body data, you might write the
63 following:
64
65 .. code-block:: python
66
67 from webargs import fields
68 from webargs.multidictproxy import MultiDictProxy
69 from webargs.flaskparser import parser
70
71
72 @parser.location_loader("query_and_form")
73 def load_data(request, schema):
74 # relies on the Flask (werkzeug) MultiDict type's implementation of
75 # these methods, but when you're extending webargs, you may know things
76 # about your framework of choice
77 newdata = request.args.copy()
78 newdata.update(request.form)
79 return MultiDictProxy(newdata, schema)
80
81
82 # Now 'query_and_form' means you can send these values in either location,
83 # and they will be *mixed* together into a new dict to pass to your schema
84 @parser.use_args({"favorite_food": fields.String()}, location="query_and_form")
85 def set_favorite_food(args):
86 ... # do stuff
87 return "your favorite food is now set to {}".format(args["favorite_food"])
8827
8928 marshmallow Integration
9029 -----------------------
12463
12564
12665 # You can add additional parameters
127 @use_kwargs({"posts_per_page": fields.Int(missing=10)}, location="query")
66 @use_kwargs({"posts_per_page": fields.Int(missing=10, location="query")})
12867 @use_args(UserSchema())
12968 def profile_posts(args, posts_per_page):
13069 username = args["username"]
13372 .. warning::
13473 If you're using marshmallow 2, you should always set ``strict=True`` (either as a ``class Meta`` option or in the Schema's constructor) when passing a schema to webargs. This will ensure that the parser's error handler is invoked when expected.
13574
136
137 When to avoid `use_kwargs`
138 --------------------------
139
140 Any `Schema <marshmallow.Schema>` passed to `use_kwargs <webargs.core.Parser.use_kwargs>` MUST deserialize to a dictionary of data.
141 If your schema has a `post_load <marshmallow.decorators.post_load>` method
142 that returns a non-dictionary,
143 you should use `use_args <webargs.core.Parser.use_args>` instead.
144
145 .. code-block:: python
146
147 from marshmallow import Schema, fields, post_load
148 from webargs.flaskparser import use_args
149
150
151 class Rectangle:
152 def __init__(self, length, width):
153 self.length = length
154 self.width = width
155
156
157 class RectangleSchema(Schema):
158 length = fields.Float()
159 width = fields.Float()
160
161 @post_load
162 def make_object(self, data, **kwargs):
163 return Rectangle(**data)
164
165
166 @use_args(RectangleSchema)
167 def post(self, rect: Rectangle):
168 return f"Area: {rect.length * rect.width}"
169
170 Packages such as `marshmallow-sqlalchemy <https://github.com/marshmallow-code/marshmallow-sqlalchemy>`_ and `marshmallow-dataclass <https://github.com/lovasoa/marshmallow_dataclass>`_ generate schemas that deserialize to non-dictionary objects.
171 Therefore, `use_args <webargs.core.Parser.use_args>` should be used with those schemas.
75 .. warning::
76 Any `Schema <marshmallow.Schema>` passed to `use_kwargs <webargs.core.Parser.use_kwargs>` MUST deserialize to a dictionary of data. Keep this in mind when writing `post_load <marshmallow.decorators.post_load>` methods.
17277
17378
17479 Schema Factories
271176 cube = args["cube"]
272177 # ...
273178
274 .. _custom-loaders:
179 .. _custom-parsers:
275180
276181 Custom Parsers
277182 --------------
278183
279 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.
184 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.
280185
281186
282187 .. code-block:: python
305210 }
306211 """
307212
308 def load_querystring(self, req, schema):
309 return _structure_dict(req.args)
213 def parse_querystring(self, req, name, field):
214 return core.get_value(_structure_dict(req.args), name, field)
310215
311216
312217 def _structure_dict(dict_):
369274
370275
371276 @app.route("/profile/", methods=["patch"])
372 @use_args(PatchSchema(many=True))
277 @use_args(PatchSchema(many=True), locations=("json",))
373278 def patch_blog(args):
374279 """Implements JSON Patch for the user profile
375280
384289 Mixing Locations
385290 ----------------
386291
387 Arguments for different locations can be specified by passing ``location`` to each `use_args <webargs.core.Parser.use_args>` call:
388
389 .. code-block:: python
390
391 # "json" is the default, used explicitly below
292 Arguments for different locations can be specified by passing ``location`` to each field individually:
293
294 .. code-block:: python
295
392296 @app.route("/stacked", methods=["POST"])
393 @use_args({"page": fields.Int(), "q": fields.Str()}, location="query")
394 @use_args({"name": fields.Str()}, location="json")
297 @use_args(
298 {
299 "page": fields.Int(location="query"),
300 "q": fields.Str(location="query"),
301 "name": fields.Str(location="json"),
302 }
303 )
304 def viewfunc(args):
305 page = args["page"]
306 # ...
307
308 Alternatively, you can pass multiple locations to `use_args <webargs.core.Parser.use_args>`:
309
310 .. code-block:: python
311
312 @app.route("/stacked", methods=["POST"])
313 @use_args(
314 {"page": fields.Int(), "q": fields.Str(), "name": fields.Str()},
315 locations=("query", "json"),
316 )
317 def viewfunc(args):
318 page = args["page"]
319 # ...
320
321 However, this allows ``page`` and ``q`` to be passed in the request body and ``name`` to be passed as a query parameter.
322
323 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:
324
325 .. code-block:: python
326
327 query_args = {"page": fields.Int(), "q": fields.Int()}
328 json_args = {"name": fields.Str()}
329
330
331 @app.route("/stacked", methods=["POST"])
332 @use_args(query_args, locations=("query",))
333 @use_args(json_args, locations=("json",))
395334 def viewfunc(query_parsed, json_parsed):
396335 page = query_parsed["page"]
397336 name = json_parsed["name"]
403342
404343 import functools
405344
406 query = functools.partial(use_args, location="query")
407 body = functools.partial(use_args, location="json")
408
409
410 @query({"page": fields.Int(), "q": fields.Int()})
411 @body({"name": fields.Str()})
345 query = functools.partial(use_args, locations=("query",))
346 body = functools.partial(use_args, locations=("json",))
347
348
349 @query(query_args)
350 @body(json_args)
412351 def viewfunc(query_parsed, json_parsed):
413352 page = query_parsed["page"]
414353 name = json_parsed["name"]
1414
1515 .. automodule:: webargs.fields
1616 :members: Nested, DelimitedList
17
18
19 webargs.multidictproxy
20 ----------------------
21
22 .. automodule:: webargs.multidictproxy
23 :members:
24
2517
2618 webargs.asyncparser
2719 -------------------
0 # -*- coding: utf-8 -*-
01 import datetime as dt
12 import sys
23 import os
3536
3637 html_domain_indices = False
3738 source_suffix = ".rst"
38 project = "webargs"
39 copyright = "2014-{:%Y}, Steven Loria and contributors".format(dt.datetime.utcnow())
39 project = u"webargs"
40 copyright = u"2014-{0:%Y}, Steven Loria and contributors".format(dt.datetime.utcnow())
4041 version = release = webargs.__version__
4142 templates_path = ["_templates"]
4243 exclude_patterns = ["_build"]
2121
2222
2323 @app.route("/user/<int:uid>")
24 @use_args({"per_page": fields.Int()}, location="query")
24 @use_args({"per_page": fields.Int()})
2525 def user_detail(args, uid):
26 return ("The user page for user {uid}, showing {per_page} posts.").format(
26 return ("The user page for user {uid}, " "showing {per_page} posts.").format(
2727 uid=uid, per_page=args["per_page"]
2828 )
2929
6363
6464
6565 @app.route("/greeting/<name>/")
66 @use_args({"name": fields.Str()}, location="view_args")
66 @use_args({"name": fields.Str(location="view_args")})
6767 def greeting(args, **kwargs):
6868 return "Hello {}".format(args["name"])
6969
9494 }
9595
9696
97 @use_args(account_args, location="form")
97 @use_args(account_args)
9898 def login_user(request, args):
9999 if request.method == "POST":
100100 login(args["username"], args["password"])
113113
114114
115115 class BlogPostView(View):
116 @use_args(blog_args, location="query")
116 @use_args(blog_args)
117117 def get(self, request, args):
118118 blog_post = Post.objects.get(title__iexact=args["title"], author=args["author"])
119119 return render_to_response("post_template.html", {"post": blog_post})
238238 from webargs.pyramidparser import use_args
239239
240240
241 @use_args({"uid": fields.Str(), "per_page": fields.Int()}, location="query")
241 @use_args({"uid": fields.Str(), "per_page": fields.Int()})
242242 def user_detail(request, args):
243243 uid = args["uid"]
244244 return Response(
260260 from webargs.pyramidparser import use_args
261261
262262
263 @use_args({"mymatch": fields.Int()}, location="matchdict")
263 @use_args({"mymatch": fields.Int()}, locations=("matchdict",))
264264 def matched(request, args):
265265 return Response("The value for mymatch is {}".format(args["mymatch"]))
266266
309309
310310
311311 def add_args(argmap, **kwargs):
312 def hook(req, resp, resource, params):
312 def hook(req, resp, params):
313313 parsed_args = parser.parse(argmap, req=req, **kwargs)
314314 req.context["args"] = parsed_args
315315
316316 return hook
317317
318318
319 @falcon.before(add_args({"page": fields.Int()}, location="query"))
319 @falcon.before(add_args({"page": fields.Int(location="query")}))
320320 class AuthorResource:
321321 def on_get(self, req, resp):
322322 args = req.context["args"]
413413 from webargs.aiohttpparser import use_args
414414
415415
416 @parser.use_args({"slug": fields.Str()}, location="match_info")
416 @parser.use_args({"slug": fields.Str(location="match_info")})
417417 def article_detail(request, args):
418418 return web.Response(body="Slug: {}".format(args["slug"]).encode("utf-8"))
419419
55
66 webargs is a Python library for parsing and validating HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, webapp2, Falcon, and aiohttp.
77
8 Upgrading from an older version?
9 --------------------------------
10
11 See the :doc:`Upgrading to Newer Releases <upgrading>` page for notes on getting your code up-to-date with the latest version.
12
13
14 Usage and Simple Examples
15 -------------------------
168
179 .. code-block:: python
1810
2416
2517
2618 @app.route("/")
27 @use_args({"name": fields.Str(required=True)}, location="query")
19 @use_args({"name": fields.Str(required=True)})
2820 def index(args):
2921 return "Hello " + args["name"]
3022
3527 # curl http://localhost:5000/\?name\='World'
3628 # Hello World
3729
38 By default Webargs will automatically parse JSON request bodies. But it also
39 has support for:
30 Webargs will automatically parse:
4031
4132 **Query Parameters**
4233 ::
43 $ curl http://localhost:5000/\?name\='Freddie'
44 Hello Freddie
4534
46 # pass location="query" to use_args
35 $ curl http://localhost:5000/\?name\='Freddie'
36 Hello Freddie
4737
4838 **Form Data**
4939 ::
5141 $ curl -d 'name=Brian' http://localhost:5000/
5242 Hello Brian
5343
54 # pass location="form" to use_args
55
5644 **JSON Data**
5745 ::
5846
5947 $ curl -X POST -H "Content-Type: application/json" -d '{"name":"Roger"}' http://localhost:5000/
6048 Hello Roger
61
62 # pass location="json" (or omit location) to use_args
6349
6450 and, optionally:
6551
116102
117103 license
118104 changelog
119 upgrading
120105 authors
121106 contributing
00 Install
11 =======
22
3 **webargs** requires Python >= 3.5. It depends on `marshmallow <https://marshmallow.readthedocs.io/en/latest/>`_ >= 2.7.0.
3 **webargs** requires Python >= 2.7 or >= 3.5. It depends on `marshmallow <https://marshmallow.readthedocs.io/en/latest/>`_ >= 2.7.0.
44
55 From the PyPI
66 -------------
2222 "nickname": fields.List(fields.Str()),
2323 # Delimited list, e.g. "/?languages=python,javascript"
2424 "languages": fields.DelimitedList(fields.Str()),
25 # When you know where an argument should be parsed from
26 "active": fields.Bool(location="query"),
2527 # When value is keyed on a variable-unsafe name
2628 # or you want to rename a key
27 "user_type": fields.Str(load_from="user-type"),
29 "content_type": fields.Str(load_from="Content-Type", location="headers"),
2830 # OR, on marshmallow 3
29 # "user_type": fields.Str(data_key="user-type"),
31 # "content_type": fields.Str(data_key="Content-Type", location="headers"),
32 # File uploads
33 "profile_image": fields.Field(
34 location="files", validate=lambda f: f.mimetype in ["image/jpeg", "image/png"]
35 ),
3036 }
3137
3238 .. note::
98104 Request "Locations"
99105 -------------------
100106
101 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:
107 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:
102108
103109 .. code-block:: python
104110
105111 @app.route("/register")
106 @use_args(user_args, location="form")
112 @use_args(user_args, locations=("json", "form"))
107113 def register(args):
108114 return "registration page"
109115
195201
196202
197203 @parser.error_handler
198 def handle_error(error, req, schema, *, status_code, headers):
204 def handle_error(error, req, schema, status_code, headers):
199205 raise CustomError(error.messages)
200206
201207 Parsing Lists in Query Strings
236242
237243 .. note::
238244
239 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.
245 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.
240246
241247 Next Steps
242248 ----------
+0
-475
docs/upgrading.rst less more
0 Upgrading to Newer Releases
1 ===========================
2
3 This section documents migration paths to new releases.
4
5 Upgrading to 6.0
6 ++++++++++++++++
7
8 Multiple Locations Are No Longer Supported In A Single Call
9 -----------------------------------------------------------
10
11 The default location is JSON/body.
12
13 Under webargs 5.x, code often did not have to specify a location.
14
15 Because webargs would parse data from multiple locations automatically, users
16 did not need to specify where a parameter, call it `q`, was passed.
17 `q` could be in a query parameter or in a JSON or form-post body.
18
19 Now, webargs requires that users specify only one location for data loading per
20 `use_args` call, and `"json"` is the default. If `q` is intended to be a query
21 parameter, the developer must be explicit and rewrite like so:
22
23 .. code-block:: python
24
25 # webargs 5.x
26 @parser.use_args({"q": ma.fields.String()})
27 def foo(args):
28 return some_function(user_query=args.get("q"))
29
30
31 # webargs 6.x
32 @parser.use_args({"q": ma.fields.String()}, location="query")
33 def foo(args):
34 return some_function(user_query=args.get("q"))
35
36 This also means that another usage from 5.x is not supported. Code with
37 multiple locations in a single `use_args`, `use_kwargs`, or `parse` call
38 must be rewritten in multiple separate `use_args` or `use_kwargs` invocations,
39 like so:
40
41 .. code-block:: python
42
43 # webargs 5.x
44 @parser.use_kwargs(
45 {
46 "q1": ma.fields.Int(location="query"),
47 "q2": ma.fields.Int(location="query"),
48 "h1": ma.fields.Int(location="headers"),
49 },
50 locations=("query", "headers"),
51 )
52 def foo(q1, q2, h1):
53 ...
54
55
56 # webargs 6.x
57 @parser.use_kwargs({"q1": ma.fields.Int(), "q2": ma.fields.Int()}, location="query")
58 @parser.use_kwargs({"h1": ma.fields.Int()}, location="headers")
59 def foo(q1, q2, h1):
60 ...
61
62
63 Fields No Longer Support location=...
64 -------------------------------------
65
66 Because a single `parser.use_args`, `parser.use_kwargs`, or `parser.parse` call
67 cannot specify multiple locations, it is not necessary for a field to be able
68 to specify its location. Rewrite code like so:
69
70 .. code-block:: python
71
72 # webargs 5.x
73 @parser.use_args({"q": ma.fields.String(location="query")})
74 def foo(args):
75 return some_function(user_query=args.get("q"))
76
77
78 # webargs 6.x
79 @parser.use_args({"q": ma.fields.String()}, location="query")
80 def foo(args):
81 return some_function(user_query=args.get("q"))
82
83 location_handler Has Been Replaced With location_loader
84 -------------------------------------------------------
85
86 This is not just a name change. The expected signature of a `location_loader`
87 is slightly different from the signature for a `location_handler`.
88
89 Where previously a `location_handler` code took the incoming request data and
90 details of a single field being loaded, a `location_loader` takes the request
91 and the schema as a pair. It does not return a specific field's data, but data
92 for the whole location.
93
94 Rewrite code like this:
95
96 .. code-block:: python
97
98 # webargs 5.x
99 @parser.location_handler("data")
100 def load_data(request, name, field):
101 return request.data.get(name)
102
103
104 # webargs 6.x
105 @parser.location_loader("data")
106 def load_data(request, schema):
107 return request.data
108
109 Data Is Not Filtered Before Being Passed To Schemas, And It May Be Proxified
110 ----------------------------------------------------------------------------
111
112 In webargs 5.x, the deserialization schema was used to pull data out of the
113 request object. That data was compiled into a dictionary which was then passed
114 to the schema.
115
116 One of the major changes in webargs 6.x allows the use of `unknown` parameter
117 on schemas. This lets a schema decide what to do with fields not specified in
118 the schema. In order to achieve this, webargs now passes the full data from
119 the specified location to the schema.
120
121 Therefore, users should specify `unknown=marshmallow.EXCLUDE` on their schemas in
122 order to filter out unknown fields. Like so:
123
124 .. code-block:: python
125
126 # webargs 5.x
127 # this can assume that "q" is the only parameter passed, and all other
128 # parameters will be ignored
129 @parser.use_kwargs({"q": ma.fields.String()}, locations=("query",))
130 def foo(q):
131 ...
132
133
134 # webargs 6.x, Solution 1: declare a schema with Meta.unknown set
135 class QuerySchema(ma.Schema):
136 q = ma.fields.String()
137
138 class Meta:
139 unknown = ma.EXCLUDE
140
141
142 @parser.use_kwargs(QuerySchema, location="query")
143 def foo(q):
144 ...
145
146
147 # webargs 6.x, Solution 2: instantiate a schema with unknown set
148 class QuerySchema(ma.Schema):
149 q = ma.fields.String()
150
151
152 @parser.use_kwargs(QuerySchema(unknown=ma.EXCLUDE), location="query")
153 def foo(q):
154 ...
155
156
157 This also allows usage which passes the unknown parameters through, like so:
158
159 .. code-block:: python
160
161 # webargs 6.x only! cannot be done in 5.x
162 class QuerySchema(ma.Schema):
163 q = ma.fields.String()
164
165
166 # will pass *all* query params through as "kwargs"
167 @parser.use_kwargs(QuerySchema(unknown=ma.INCLUDE), location="query")
168 def foo(q, **kwargs):
169 ...
170
171
172 However, many types of request data are so-called "multidicts" -- dictionary-like
173 types which can return one or multiple values. To handle `marshmallow.fields.List`
174 and `webargs.fields.DelimitedList` fields correctly, passing list data, webargs
175 must combine schema information with the raw request data. This is done in the
176 :class:`MultiDictProxy <webargs.multidictproxy.MultiDictProxy>` type, which
177 will often be passed to schemas.
178
179 This means that if a schema has a `pre_load` hook which interacts with the data,
180 it may need modifications. For example, a `flask` query string will be parsed
181 into an `ImmutableMultiDict` type, which will break pre-load hooks which modify
182 the data in-place. Such usages need rewrites like so:
183
184 .. code-block:: python
185
186 # webargs 5.x
187 # flask query params is just an example -- applies to several types
188 from webargs.flaskparser import use_kwargs
189
190
191 class QuerySchema(ma.Schema):
192 q = ma.fields.String()
193
194 @ma.pre_load
195 def convert_nil_to_none(self, obj, **kwargs):
196 if obj.get("q") == "nil":
197 obj["q"] = None
198 return obj
199
200
201 @use_kwargs(QuerySchema, locations=("query",))
202 def foo(q):
203 ...
204
205
206 # webargs 6.x
207 class QuerySchema(ma.Schema):
208 q = ma.fields.String()
209
210 # unlike under 5.x, we cannot modify 'obj' in-place because writing
211 # to the MultiDictProxy will try to write to the underlying
212 # ImmutableMultiDict, which is not allowed
213 @ma.pre_load
214 def convert_nil_to_none(self, obj, **kwargs):
215 # creating a dict from a MultiDictProxy works well because it
216 # "unwraps" lists and delimited lists correctly
217 data = dict(obj)
218 if data.get("q") == "nil":
219 data["q"] = None
220 return data
221
222
223 @parser.use_kwargs(QuerySchema, location="query")
224 def foo(q):
225 ...
226
227
228 DelimitedList Now Only Takes A String Input
229 -------------------------------------------
230
231 Combining `List` and string parsing functionality in a single type had some
232 messy corner cases. For the most part, this should not require rewrites. But
233 for APIs which need to allow both usages, rewrites are possible like so:
234
235 .. code-block:: python
236
237 # webargs 5.x
238 # this allows ...?x=1&x=2&x=3
239 # as well as ...?x=1,2,3
240 @use_kwargs({"x": webargs.fields.DelimitedList(ma.fields.Int)}, locations=("query",))
241 def foo(x):
242 ...
243
244
245 # webargs 6.x
246 # this accepts x=1,2,3 but NOT x=1&x=2&x=3
247 @use_kwargs({"x": webargs.fields.DelimitedList(ma.fields.Int)}, location="query")
248 def foo(x):
249 ...
250
251
252 # webargs 6.x
253 # this accepts x=1,2,3 ; x=1&x=2&x=3 ; x=1,2&x=3
254 # to do this, it needs a post_load hook which will flatten out the list data
255 class UnpackingDelimitedListSchema(ma.Schema):
256 x = ma.fields.List(webargs.fields.DelimitedList(ma.fields.Int))
257
258 @ma.post_load
259 def flatten_lists(self, data, **kwargs):
260 new_x = []
261 for x in data["x"]:
262 new_x.extend(x)
263 data["x"] = new_x
264 return data
265
266
267 @parser.use_kwargs(UnpackingDelimitedListSchema, location="query")
268 def foo(x):
269 ...
270
271
272 ValidationError Messages Are Namespaced Under The Location
273 ----------------------------------------------------------
274
275 Code parsing ValidationError messages will notice a change in the messages
276 produced by webargs.
277 What would previously have come back with messages like `{"foo":["Not a valid integer."]}`
278 will now have messages nested one layer deeper, like
279 `{"json":{"foo":["Not a valid integer."]}}`.
280
281 To rewrite code which was handling these errors, the handler will need to be
282 prepared to traverse messages by one additional level. For example:
283
284 .. code-block:: python
285
286 import logging
287
288 log = logging.getLogger(__name__)
289
290
291 # webargs 5.x
292 # logs debug messages like
293 # bad value for 'foo': ["Not a valid integer."]
294 # bad value for 'bar': ["Not a valid boolean."]
295 def log_invalid_parameters(validation_error):
296 for field, messages in validation_error.messages.items():
297 log.debug("bad value for '{}': {}".format(field, messages))
298
299
300 # webargs 6.x
301 # logs debug messages like
302 # bad value for 'foo' [query]: ["Not a valid integer."]
303 # bad value for 'bar' [json]: ["Not a valid boolean."]
304 def log_invalid_parameters(validation_error):
305 for location, fielddata in validation_error.messages.items():
306 for field, messages in fielddata.items():
307 log.debug("bad value for '{}' [{}]: {}".format(field, location, messages))
308
309
310 Some Functions Take Keyword-Only Arguments Now
311 ----------------------------------------------
312
313 The signature of several methods has changed to have keyword-only arguments.
314 For the most part, this should not require any changes, but here's a list of
315 the changes.
316
317 `parser.error_handler` methods:
318
319 .. code-block:: python
320
321 # webargs 5.x
322 def handle_error(error, req, schema, status_code, headers):
323 ...
324
325
326 # webargs 6.x
327 def handle_error(error, req, schema, *, status_code, headers):
328 ...
329
330 `parser.__init__` methods:
331
332 .. code-block:: python
333
334 # webargs 5.x
335 def __init__(self, location=None, error_handler=None, schema_class=None):
336 ...
337
338
339 # webargs 6.x
340 def __init__(self, location=None, *, error_handler=None, schema_class=None):
341 ...
342
343 `parser.parse`, `parser.use_args`, and `parser.use_kwargs` methods:
344
345
346 .. code-block:: python
347
348 # webargs 5.x
349 def parse(
350 self,
351 argmap,
352 req=None,
353 location=None,
354 validate=None,
355 error_status_code=None,
356 error_headers=None,
357 ):
358 ...
359
360
361 # webargs 6.x
362 def parse(
363 self,
364 argmap,
365 req=None,
366 *,
367 location=None,
368 validate=None,
369 error_status_code=None,
370 error_headers=None
371 ):
372 ...
373
374
375 # webargs 5.x
376 def use_args(
377 self,
378 argmap,
379 req=None,
380 location=None,
381 as_kwargs=False,
382 validate=None,
383 error_status_code=None,
384 error_headers=None,
385 ):
386 ...
387
388
389 # webargs 6.x
390 def use_args(
391 self,
392 argmap,
393 req=None,
394 *,
395 location=None,
396 as_kwargs=False,
397 validate=None,
398 error_status_code=None,
399 error_headers=None
400 ):
401 ...
402
403
404 # use_kwargs is just an alias for use_args with as_kwargs=True
405
406 and finally, the `dict2schema` function:
407
408 .. code-block:: python
409
410 # webargs 5.x
411 def dict2schema(dct, schema_class=ma.Schema):
412 ...
413
414
415 # webargs 6.x
416 def dict2schema(dct, *, schema_class=ma.Schema):
417 ...
418
419
420 PyramidParser Now Appends Arguments (Used To Prepend)
421 -----------------------------------------------------
422
423 `PyramidParser.use_args` was not conformant with the other parsers in webargs.
424 While all other parsers added new arguments to the end of the argument list of
425 a decorated view function, the Pyramid implementation added them to the front
426 of the argument list.
427
428 This has been corrected, but as a result pyramid views with `use_args` may need
429 to be rewritten. The `request` object is always passed first in both versions,
430 so the issue is only apparent with view functions taking other positional
431 arguments.
432
433 For example, imagine code with a decorator for passing user information,
434 `pass_userinfo`, like so:
435
436 .. code-block:: python
437
438 # a decorator which gets information about the authenticated user
439 def pass_userinfo(f):
440 def decorator(request, *args, **kwargs):
441 return f(request, get_userinfo(), *args, **kwargs)
442
443 return decorator
444
445 You will see a behavioral change if `pass_userinfo` is called on a function
446 decorated with `use_args`. The difference between the two versions will be like
447 so:
448
449 .. code-block:: python
450
451 from webargs.pyramidparser import use_args
452
453 # webargs 5.x
454 # pass_userinfo is called first, webargs sees positional arguments of
455 # (userinfo,)
456 # and changes it to
457 # (request, args, userinfo)
458 @pass_userinfo
459 @use_args({"q": ma.fields.String()}, locations=("query",))
460 def viewfunc(request, args, userinfo):
461 q = args.get("q")
462 ...
463
464
465 # webargs 6.x
466 # pass_userinfo is called first, webargs sees positional arguments of
467 # (userinfo,)
468 # and changes it to
469 # (request, userinfo, args)
470 @pass_userinfo
471 @use_args({"q": ma.fields.String()}, location="query")
472 def viewfunc(request, userinfo, args):
473 q = args.get("q")
474 ...
2626 ### Middleware and hooks ###
2727
2828
29 class JSONTranslator:
29 class JSONTranslator(object):
3030 def process_response(self, req, resp, resource):
3131 if "result" not in req.context:
3232 return
4343 ### Resources ###
4444
4545
46 class HelloResource:
46 class HelloResource(object):
4747 """A welcome page."""
4848
4949 hello_args = {"name": fields.Str(missing="Friend", location="query")}
5353 req.context["result"] = {"message": "Welcome, {}!".format(args["name"])}
5454
5555
56 class AdderResource:
56 class AdderResource(object):
5757 """An addition endpoint."""
5858
5959 adder_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)}
6363 req.context["result"] = {"result": x + y}
6464
6565
66 class DateAddResource:
66 class DateAddResource(object):
6767 """A datetime adder endpoint."""
6868
6969 dateadd_args = {
0 # -*- coding: utf-8 -*-
01 """A simple number and datetime addition JSON API.
12 Run the app:
23
6869
6970 # This error handler is necessary for usage with Flask-RESTful
7071 @parser.error_handler
71 def handle_request_parsing_error(err, req, schema, *, error_status_code, error_headers):
72 def handle_request_parsing_error(err, req, schema, error_status_code, error_headers):
7273 """webargs error handler that uses Flask-RESTful's abort function to return
7374 a JSON error response to the client.
7475 """
0 python-dateutil==2.8.1
0 python-dateutil==2.8.0
11 Flask
22 bottle
33 tornado
00 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
12 """A Hello, World! example using Webapp2 in a Google App Engine environment
23
34 Run the app:
11 license_files = LICENSE
22
33 [bdist_wheel]
4 universal = 1
4 # We build separate wheels for
5 # Python 2 and 3 because of the conditional
6 # dependency on simplejson
7 universal = 0
58
69 [flake8]
710 ignore = E203, E266, E501, W503
0 # -*- coding: utf-8 -*-
1 import sys
02 import re
13 from setuptools import setup, find_packages
4
5 INSTALL_REQUIRES = ["marshmallow>=2.15.2"]
6 if sys.version_info[0] < 3:
7 INSTALL_REQUIRES.append("simplejson>=2.1.0")
28
39 FRAMEWORKS = [
410 "Flask>=0.12.2",
713 "tornado>=4.5.2",
814 "pyramid>=1.9.1",
915 "webapp2>=3.0.0b1",
10 "falcon>=2.0.0",
11 "aiohttp>=3.0.0",
16 "falcon>=1.4.0,<2.0",
17 'aiohttp>=3.0.0; python_version >= "3.5"',
1218 ]
1319 EXTRAS_REQUIRE = {
1420 "frameworks": FRAMEWORKS,
1521 "tests": [
1622 "pytest",
17 'mock; python_version == "3.5"',
18 "webtest==2.0.35",
19 "webtest-aiohttp==2.0.0",
20 "pytest-aiohttp>=0.3.0",
23 "mock",
24 "webtest==2.0.33",
25 'webtest-aiohttp==2.0.0; python_version >= "3.5"',
26 'pytest-aiohttp>=0.3.0; python_version >= "3.5"',
2127 ]
2228 + FRAMEWORKS,
2329 "lint": [
24 "mypy==0.770",
25 "flake8==3.7.9",
26 "flake8-bugbear==20.1.4",
27 "pre-commit>=1.20,<3.0",
30 'mypy==0.730; python_version >= "3.5"',
31 "flake8==3.7.8",
32 'flake8-bugbear==19.8.0; python_version >= "3.5"',
33 "pre-commit~=1.17",
2834 ],
29 "docs": ["Sphinx==3.0.3", "sphinx-issues==1.2.0", "sphinx-typlog-theme==0.8.0"]
35 "docs": ["Sphinx==2.2.0", "sphinx-issues==1.2.0", "sphinx-typlog-theme==0.7.3"]
3036 + FRAMEWORKS,
3137 }
3238 EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"]
6975 url="https://github.com/marshmallow-code/webargs",
7076 packages=find_packages("src"),
7177 package_dir={"": "src"},
72 install_requires=["marshmallow>=2.15.2"],
78 install_requires=INSTALL_REQUIRES,
7379 extras_require=EXTRAS_REQUIRE,
7480 license="MIT",
7581 zip_safe=False,
9096 "api",
9197 "marshmallow",
9298 ),
93 python_requires=">=3.5",
9499 classifiers=[
95100 "Development Status :: 5 - Production/Stable",
96101 "Intended Audience :: Developers",
97102 "License :: OSI Approved :: MIT License",
98103 "Natural Language :: English",
104 "Programming Language :: Python :: 2",
105 "Programming Language :: Python :: 2.7",
99106 "Programming Language :: Python :: 3",
100107 "Programming Language :: Python :: 3.5",
101108 "Programming Language :: Python :: 3.6",
102109 "Programming Language :: Python :: 3.7",
103 "Programming Language :: Python :: 3.8",
104 "Programming Language :: Python :: 3 :: Only",
105110 "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
106111 "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
107112 ],
0 # -*- coding: utf-8 -*-
01 from distutils.version import LooseVersion
12 from marshmallow.utils import missing
23
78 from webargs.dict2schema import dict2schema
89 from webargs import fields
910
10 __version__ = "6.1.0"
11 __version__ = "5.5.3"
1112 __version_info__ = tuple(LooseVersion(__version__).version)
1213 __all__ = ("dict2schema", "ValidationError", "fields", "missing", "validate")
2727 from aiohttp.web import Request
2828 from aiohttp import web_exceptions
2929 from marshmallow import Schema, ValidationError
30 from marshmallow.fields import Field
3031
3132 from webargs import core
3233 from webargs.core import json
3334 from webargs.asyncparser import AsyncParser
34 from webargs.multidictproxy import MultiDictProxy
3535
3636
3737 def is_json_request(req: Request) -> bool:
7272 """aiohttp request argument parser."""
7373
7474 __location_map__ = dict(
75 match_info="load_match_info",
76 path="load_match_info",
77 **core.Parser.__location_map__,
75 match_info="parse_match_info",
76 path="parse_match_info",
77 **core.Parser.__location_map__
7878 )
7979
80 def load_querystring(self, req: Request, schema: Schema) -> MultiDictProxy:
81 """Return query params from the request as a MultiDictProxy."""
82 return MultiDictProxy(req.query, schema)
80 def parse_querystring(self, req: Request, name: str, field: Field) -> typing.Any:
81 """Pull a querystring value from the request."""
82 return core.get_value(req.query, name, field)
8383
84 async def load_form(self, req: Request, schema: Schema) -> MultiDictProxy:
85 """Return form values from the request as a MultiDictProxy."""
86 post_data = await req.post()
87 return MultiDictProxy(post_data, schema)
84 async def parse_form(self, req: Request, name: str, field: Field) -> typing.Any:
85 """Pull a form value from the request."""
86 post_data = self._cache.get("post")
87 if post_data is None:
88 self._cache["post"] = await req.post()
89 return core.get_value(self._cache["post"], name, field)
8890
89 async def load_json_or_form(
90 self, req: Request, schema: Schema
91 ) -> typing.Union[typing.Dict, MultiDictProxy]:
92 data = await self.load_json(req, schema)
93 if data is not core.missing:
94 return data
95 return await self.load_form(req, schema)
91 async def parse_json(self, req: Request, name: str, field: Field) -> typing.Any:
92 """Pull a json value from the request."""
93 json_data = self._cache.get("json")
94 if json_data is None:
95 if not (req.body_exists and is_json_request(req)):
96 return core.missing
97 try:
98 json_data = await req.json(loads=json.loads)
99 except json.JSONDecodeError as e:
100 if e.doc == "":
101 return core.missing
102 else:
103 return self.handle_invalid_json_error(e, req)
104 except UnicodeDecodeError as e:
105 return self.handle_invalid_json_error(e, req)
96106
97 async def load_json(self, req: Request, schema: Schema) -> typing.Dict:
98 """Return a parsed json payload from the request."""
99 if not (req.body_exists and is_json_request(req)):
100 return core.missing
101 try:
102 return await req.json(loads=json.loads)
103 except json.JSONDecodeError as exc:
104 if exc.doc == "":
105 return core.missing
106 return self._handle_invalid_json_error(exc, req)
107 except UnicodeDecodeError as exc:
108 return self._handle_invalid_json_error(exc, req)
107 self._cache["json"] = json_data
108 return core.get_value(json_data, name, field, allow_many_nested=True)
109109
110 def load_headers(self, req: Request, schema: Schema) -> MultiDictProxy:
111 """Return headers from the request as a MultiDictProxy."""
112 return MultiDictProxy(req.headers, schema)
110 def parse_headers(self, req: Request, name: str, field: Field) -> typing.Any:
111 """Pull a value from the header data."""
112 return core.get_value(req.headers, name, field)
113113
114 def load_cookies(self, req: Request, schema: Schema) -> MultiDictProxy:
115 """Return cookies from the request as a MultiDictProxy."""
116 return MultiDictProxy(req.cookies, schema)
114 def parse_cookies(self, req: Request, name: str, field: Field) -> typing.Any:
115 """Pull a value from the cookiejar."""
116 return core.get_value(req.cookies, name, field)
117117
118 def load_files(self, req: Request, schema: Schema) -> "typing.NoReturn":
118 def parse_files(self, req: Request, name: str, field: Field) -> None:
119119 raise NotImplementedError(
120 "load_files is not implemented. You may be able to use load_form for "
120 "parse_files is not implemented. You may be able to use parse_form for "
121121 "parsing upload data."
122122 )
123123
124 def load_match_info(self, req: Request, schema: Schema) -> typing.Mapping:
125 """Load the request's ``match_info``."""
126 return req.match_info
124 def parse_match_info(self, req: Request, name: str, field: Field) -> typing.Any:
125 """Pull a value from the request's ``match_info``."""
126 return core.get_value(req.match_info, name, field)
127127
128128 def get_request_from_view_args(
129129 self, view: typing.Callable, args: typing.Iterable, kwargs: typing.Mapping
136136 if isinstance(arg, web.Request):
137137 req = arg
138138 break
139 if isinstance(arg, web.View):
139 elif isinstance(arg, web.View):
140140 req = arg.request
141141 break
142 if not isinstance(req, web.Request):
143 raise ValueError("Request argument not found for handler")
142 assert isinstance(req, web.Request), "Request argument not found for handler"
144143 return req
145144
146145 def handle_error(
148147 error: ValidationError,
149148 req: Request,
150149 schema: Schema,
151 *,
152 error_status_code: typing.Union[int, None],
153 error_headers: typing.Union[typing.Mapping[str, str], None]
150 error_status_code: typing.Union[int, None] = None,
151 error_headers: typing.Union[typing.Mapping[str, str], None] = None,
154152 ) -> "typing.NoReturn":
155153 """Handle ValidationErrors and return a JSON response of error messages
156154 to the client.
159157 error_status_code or self.DEFAULT_VALIDATION_STATUS
160158 )
161159 if not error_class:
162 raise LookupError("No exception for {}".format(error_status_code))
160 raise LookupError("No exception for {0}".format(error_status_code))
163161 headers = error_headers
164162 raise error_class(
165163 body=json.dumps(error.messages).encode("utf-8"),
167165 content_type="application/json",
168166 )
169167
170 def _handle_invalid_json_error(
168 def handle_invalid_json_error(
171169 self,
172170 error: typing.Union[json.JSONDecodeError, UnicodeDecodeError],
173171 req: Request,
77 from marshmallow import Schema, ValidationError
88 from marshmallow.fields import Field
99 import marshmallow as ma
10 from marshmallow.utils import missing
1011
1112 from webargs import core
1213
1920 """Asynchronous variant of `webargs.core.Parser`, where parsing methods may be
2021 either coroutines or regular methods.
2122 """
23
24 async def _parse_request(
25 self, schema: Schema, req: Request, locations: typing.Iterable
26 ) -> typing.Union[dict, list]:
27 if schema.many:
28 assert (
29 "json" in locations
30 ), "schema.many=True is only supported for JSON location"
31 # The ad hoc Nested field is more like a workaround or a helper,
32 # and it servers its purpose fine. However, if somebody has a desire
33 # to re-design the support of bulk-type arguments, go ahead.
34 parsed = await self.parse_arg(
35 name="json",
36 field=ma.fields.Nested(schema, many=True),
37 req=req,
38 locations=locations,
39 )
40 if parsed is missing:
41 parsed = []
42 else:
43 argdict = schema.fields
44 parsed = {}
45 for argname, field_obj in argdict.items():
46 if core.MARSHMALLOW_VERSION_INFO[0] < 3:
47 parsed_value = await self.parse_arg(
48 argname, field_obj, req, locations
49 )
50 # If load_from is specified on the field, try to parse from that key
51 if parsed_value is missing and field_obj.load_from:
52 parsed_value = await self.parse_arg(
53 field_obj.load_from, field_obj, req, locations
54 )
55 argname = field_obj.load_from
56 else:
57 argname = field_obj.data_key or argname
58 parsed_value = await self.parse_arg(
59 argname, field_obj, req, locations
60 )
61 if parsed_value is not missing:
62 parsed[argname] = parsed_value
63 return parsed
2264
2365 # TODO: Lots of duplication from core.Parser here. Rethink.
2466 async def parse(
2567 self,
2668 argmap: ArgMap,
2769 req: Request = None,
28 *,
29 location: str = None,
70 locations: typing.Iterable = None,
3071 validate: Validate = None,
3172 error_status_code: typing.Union[int, None] = None,
32 error_headers: typing.Union[typing.Mapping[str, str], None] = None
73 error_headers: typing.Union[typing.Mapping[str, str], None] = None,
3374 ) -> typing.Union[typing.Mapping, None]:
3475 """Coroutine variant of `webargs.core.Parser`.
3576
3677 Receives the same arguments as `webargs.core.Parser.parse`.
3778 """
79 self.clear_cache() # in case someone used `parse_*()`
3880 req = req if req is not None else self.get_default_request()
39 location = location or self.location
40 if req is None:
41 raise ValueError("Must pass req object")
81 assert req is not None, "Must pass req object"
4282 data = None
4383 validators = core._ensure_list_of_callables(validate)
4484 schema = self._get_schema(argmap, req)
4585 try:
46 location_data = await self._load_location_data(
47 schema=schema, req=req, location=location
86 parsed = await self._parse_request(
87 schema=schema, req=req, locations=locations or self.locations
4888 )
49 result = schema.load(location_data)
89 result = schema.load(parsed)
5090 data = result.data if core.MARSHMALLOW_VERSION_INFO[0] < 3 else result
5191 self._validate_arguments(data, validators)
5292 except ma.exceptions.ValidationError as error:
5393 await self._on_validation_error(
54 error,
55 req,
56 schema,
57 location,
58 error_status_code=error_status_code,
59 error_headers=error_headers,
94 error, req, schema, error_status_code, error_headers
6095 )
61 return data
62
63 async def _load_location_data(self, schema, req, location):
64 """Return a dictionary-like object for the location on the given request.
65
66 Needs to have the schema in hand in order to correctly handle loading
67 lists from multidict objects and `many=True` schemas.
68 """
69 loader_func = self._get_loader(location)
70 if asyncio.iscoroutinefunction(loader_func):
71 data = await loader_func(req, schema)
72 else:
73 data = loader_func(req, schema)
74
75 # when the desired location is empty (no data), provide an empty
76 # dict as the default so that optional arguments in a location
77 # (e.g. optional JSON body) work smoothly
78 if data is core.missing:
79 data = {}
8096 return data
8197
8298 async def _on_validation_error(
84100 error: ValidationError,
85101 req: Request,
86102 schema: Schema,
87 location: str,
88 *,
89103 error_status_code: typing.Union[int, None],
90 error_headers: typing.Union[typing.Mapping[str, str], None]
104 error_headers: typing.Union[typing.Mapping[str, str], None] = None,
91105 ) -> None:
92 # rewrite messages to be namespaced under the location which created
93 # them
94 # e.g. {"json":{"foo":["Not a valid integer."]}}
95 # instead of
96 # {"foo":["Not a valid integer."]}
97 error.messages = {location: error.messages}
98106 error_handler = self.error_callback or self.handle_error
99 await error_handler(
100 error,
101 req,
102 schema,
103 error_status_code=error_status_code,
104 error_headers=error_headers,
105 )
107 await error_handler(error, req, schema, error_status_code, error_headers)
106108
107109 def use_args(
108110 self,
109111 argmap: ArgMap,
110112 req: typing.Optional[Request] = None,
111 *,
112 location: str = None,
113 locations: typing.Iterable = None,
113114 as_kwargs: bool = False,
114115 validate: Validate = None,
115116 error_status_code: typing.Optional[int] = None,
116 error_headers: typing.Union[typing.Mapping[str, str], None] = None
117 error_headers: typing.Union[typing.Mapping[str, str], None] = None,
117118 ) -> typing.Callable[..., typing.Callable]:
118119 """Decorator that injects parsed arguments into a view function or method.
119120
120121 Receives the same arguments as `webargs.core.Parser.use_args`.
121122 """
122 location = location or self.location
123 locations = locations or self.locations
123124 request_obj = req
124125 # Optimization: If argmap is passed as a dictionary, we only need
125126 # to generate a Schema once
126127 if isinstance(argmap, Mapping):
127 argmap = core.dict2schema(argmap, schema_class=self.schema_class)()
128 argmap = core.dict2schema(argmap, self.schema_class)()
128129
129130 def decorator(func: typing.Callable) -> typing.Callable:
130131 req_ = request_obj
141142 parsed_args = await self.parse(
142143 argmap,
143144 req=req_obj,
144 location=location,
145 locations=locations,
145146 validate=validate,
146147 error_status_code=error_status_code,
147148 error_headers=error_headers,
148149 )
149 args, kwargs = self._update_args_kwargs(
150 args, kwargs, parsed_args, as_kwargs
151 )
152 return await func(*args, **kwargs)
150 if as_kwargs:
151 kwargs.update(parsed_args or {})
152 return await func(*args, **kwargs)
153 else:
154 # Add parsed_args after other positional arguments
155 new_args = args + (parsed_args,)
156 return await func(*new_args, **kwargs)
153157
154158 else:
155159
163167 parsed_args = yield from self.parse( # type: ignore
164168 argmap,
165169 req=req_obj,
166 location=location,
170 locations=locations,
167171 validate=validate,
168172 error_status_code=error_status_code,
169173 error_headers=error_headers,
170174 )
171 args, kwargs = self._update_args_kwargs(
172 args, kwargs, parsed_args, as_kwargs
173 )
174 return func(*args, **kwargs)
175 if as_kwargs:
176 kwargs.update(parsed_args)
177 return func(*args, **kwargs) # noqa: B901
178 else:
179 # Add parsed_args after other positional arguments
180 new_args = args + (parsed_args,)
181 return func(*new_args, **kwargs)
175182
176183 return wrapper
177184
178185 return decorator
186
187 def use_kwargs(self, *args, **kwargs) -> typing.Callable:
188 """Decorator that injects parsed arguments into a view function or method.
189
190 Receives the same arguments as `webargs.core.Parser.use_kwargs`.
191
192 """
193 return super().use_kwargs(*args, **kwargs)
194
195 async def parse_arg(
196 self, name: str, field: Field, req: Request, locations: typing.Iterable = None
197 ) -> typing.Any:
198 location = field.metadata.get("location")
199 if location:
200 locations_to_check = self._validated_locations([location])
201 else:
202 locations_to_check = self._validated_locations(locations or self.locations)
203
204 for location in locations_to_check:
205 value = await self._get_value(name, field, req=req, location=location)
206 # Found the value; validate and return it
207 if value is not core.missing:
208 return value
209 return core.missing
210
211 async def _get_value(
212 self, name: str, argobj: Field, req: Request, location: str
213 ) -> typing.Any:
214 function = self._get_handler(location)
215 if asyncio.iscoroutinefunction(function):
216 value = await function(req, name, argobj)
217 else:
218 value = function(req, name, argobj)
219 return value
0 # -*- coding: utf-8 -*-
01 """Bottle request argument parsing module.
12
23 Example: ::
1819 import bottle
1920
2021 from webargs import core
21 from webargs.multidictproxy import MultiDictProxy
22 from webargs.core import json
2223
2324
2425 class BottleParser(core.Parser):
2526 """Bottle.py request argument parser."""
2627
27 def _handle_invalid_json_error(self, error, req, *args, **kwargs):
28 raise bottle.HTTPError(
29 status=400, body={"json": ["Invalid JSON body."]}, exception=error
30 )
28 def parse_querystring(self, req, name, field):
29 """Pull a querystring value from the request."""
30 return core.get_value(req.query, name, field)
3131
32 def _raw_load_json(self, req):
33 """Read a json payload from the request."""
34 try:
35 data = req.json
36 except AttributeError:
37 return core.missing
38
39 # unfortunately, bottle does not distinguish between an emtpy body, "",
40 # and a body containing the valid JSON value null, "null"
41 # so these can't be properly disambiguated
42 # as our best-effort solution, treat None as missing and ignore the
43 # (admittedly unusual) "null" case
44 # see: https://github.com/bottlepy/bottle/issues/1160
45 if data is None:
46 return core.missing
47 return data
48
49 def load_querystring(self, req, schema):
50 """Return query params from the request as a MultiDictProxy."""
51 return MultiDictProxy(req.query, schema)
52
53 def load_form(self, req, schema):
54 """Return form values from the request as a MultiDictProxy."""
32 def parse_form(self, req, name, field):
33 """Pull a form value from the request."""
5534 # For consistency with other parsers' behavior, don't attempt to
5635 # parse if content-type is mismatched.
5736 # TODO: Make this check more specific
5837 if core.is_json(req.content_type):
5938 return core.missing
60 return MultiDictProxy(req.forms, schema)
39 return core.get_value(req.forms, name, field)
6140
62 def load_headers(self, req, schema):
63 """Return headers from the request as a MultiDictProxy."""
64 return MultiDictProxy(req.headers, schema)
41 def parse_json(self, req, name, field):
42 """Pull a json value from the request."""
43 json_data = self._cache.get("json")
44 if json_data is None:
45 try:
46 self._cache["json"] = json_data = req.json
47 except AttributeError:
48 return core.missing
49 except json.JSONDecodeError as e:
50 if e.doc == "":
51 return core.missing
52 else:
53 return self.handle_invalid_json_error(e, req)
54 except UnicodeDecodeError as e:
55 return self.handle_invalid_json_error(e, req)
6556
66 def load_cookies(self, req, schema):
67 """Return cookies from the request."""
68 return req.cookies
57 if json_data is None:
58 return core.missing
59 return core.get_value(json_data, name, field, allow_many_nested=True)
6960
70 def load_files(self, req, schema):
71 """Return files from the request as a MultiDictProxy."""
72 return MultiDictProxy(req.files, schema)
61 def parse_headers(self, req, name, field):
62 """Pull a value from the header data."""
63 return core.get_value(req.headers, name, field)
7364
74 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
65 def parse_cookies(self, req, name, field):
66 """Pull a value from the cookiejar."""
67 return req.get_cookie(name)
68
69 def parse_files(self, req, name, field):
70 """Pull a file from the request."""
71 return core.get_value(req.files, name, field)
72
73 def handle_error(self, error, req, schema, error_status_code, error_headers):
7574 """Handles errors during parsing. Aborts the current request with a
7675 400 error.
7776 """
8382 exception=error,
8483 )
8584
85 def handle_invalid_json_error(self, error, req, *args, **kwargs):
86 raise bottle.HTTPError(
87 status=400, body={"json": ["Invalid JSON body."]}, exception=error
88 )
89
8690 def get_default_request(self):
8791 """Override to use bottle's thread-local request object by default."""
8892 return bottle.request
0 # -*- coding: utf-8 -*-
01 # flake8: noqa
2 import sys
13 from distutils.version import LooseVersion
24
35 import marshmallow as ma
46
57 MARSHMALLOW_VERSION_INFO = tuple(LooseVersion(ma.__version__).version) # type: tuple
8 PY2 = int(sys.version_info[0]) == 2
9
10 if PY2:
11 from collections import Mapping
12
13 basestring = basestring
14 text_type = unicode
15 iteritems = lambda d: d.iteritems()
16 else:
17 from collections.abc import Mapping
18
19 basestring = (str, bytes)
20 text_type = str
21 iteritems = lambda d: d.items()
0 # -*- coding: utf-8 -*-
1 from __future__ import unicode_literals
2
03 import functools
14 import inspect
2 import typing
35 import logging
46 import warnings
5 from collections.abc import Mapping
6 import json
7 from copy import copy
8
9 try:
10 import simplejson as json
11 except ImportError:
12 import json # type: ignore
713
814 import marshmallow as ma
915 from marshmallow import ValidationError
10 from marshmallow.utils import missing
11
12 from webargs.compat import MARSHMALLOW_VERSION_INFO
16 from marshmallow.utils import missing, is_collection
17
18 from webargs.compat import Mapping, iteritems, MARSHMALLOW_VERSION_INFO
1319 from webargs.dict2schema import dict2schema
1420 from webargs.fields import DelimitedList
1521
2127 "dict2schema",
2228 "is_multiple",
2329 "Parser",
30 "get_value",
2431 "missing",
2532 "parse_json",
2633 ]
3441 callable, a ValueError is raised.
3542 """
3643 if obj and not callable(obj):
37 raise ValueError("{!r} is not callable.".format(obj))
38 return obj
44 raise ValueError("{0!r} is not callable.".format(obj))
45 else:
46 return obj
3947
4048
4149 def is_multiple(field):
6573 return False
6674
6775
68 def parse_json(string, *, encoding="utf-8"):
69 if isinstance(string, bytes):
76 def get_value(data, name, field, allow_many_nested=False):
77 """Get a value from a dictionary. Handles ``MultiDict`` types when
78 ``field`` handles repeated/multi-value arguments.
79 If the value is not found, return `missing`.
80
81 :param object data: Mapping (e.g. `dict`) or list-like instance to
82 pull the value from.
83 :param str name: Name of the key.
84 :param bool allow_many_nested: Whether to allow a list of nested objects
85 (it is valid only for JSON format, so it is set to True in ``parse_json``
86 methods).
87 """
88 missing_value = missing
89 if allow_many_nested and isinstance(field, ma.fields.Nested) and field.many:
90 if is_collection(data):
91 return data
92
93 if not hasattr(data, "get"):
94 return missing_value
95
96 multiple = is_multiple(field)
97 val = data.get(name, missing_value)
98 if multiple and val is not missing:
99 if hasattr(data, "getlist"):
100 return data.getlist(name)
101 elif hasattr(data, "getall"):
102 return data.getall(name)
103 elif isinstance(val, (list, tuple)):
104 return val
105 if val is None:
106 return None
107 else:
108 return [val]
109 return val
110
111
112 def parse_json(s, encoding="utf-8"):
113 if isinstance(s, bytes):
70114 try:
71 string = string.decode(encoding)
72 except UnicodeDecodeError as exc:
115 s = s.decode(encoding)
116 except UnicodeDecodeError as e:
73117 raise json.JSONDecodeError(
74 "Bytes decoding error : {}".format(exc.reason),
75 doc=str(exc.object),
76 pos=exc.start,
118 "Bytes decoding error : {}".format(e.reason),
119 doc=str(e.object),
120 pos=e.start,
77121 )
78 return json.loads(string)
122 return json.loads(s)
79123
80124
81125 def _ensure_list_of_callables(obj):
85129 elif callable(obj):
86130 validators = [obj]
87131 else:
88 raise ValueError("{!r} is not a callable or list of callables.".format(obj))
132 raise ValueError(
133 "{0!r} is not a callable or list of callables.".format(obj)
134 )
89135 else:
90136 validators = []
91137 return validators
92138
93139
94 class Parser:
140 class Parser(object):
95141 """Base parser class that provides high-level implementation for parsing
96142 a request.
97143
98 Descendant classes must provide lower-level implementations for reading
99 data from different locations, e.g. ``load_json``, ``load_querystring``,
100 etc.
101
102 :param str location: Default location to use for data
144 Descendant classes must provide lower-level implementations for parsing
145 different locations, e.g. ``parse_json``, ``parse_querystring``, etc.
146
147 :param tuple locations: Default locations to parse.
103148 :param callable error_handler: Custom error handler function.
104149 """
105150
106 #: Default location to check for data
107 DEFAULT_LOCATION = "json"
151 #: Default locations to check for data
152 DEFAULT_LOCATIONS = ("querystring", "form", "json")
108153 #: The marshmallow Schema class to use when creating new schemas
109154 DEFAULT_SCHEMA_CLASS = ma.Schema
110155 #: Default status code to return for validation errors
114159
115160 #: Maps location => method name
116161 __location_map__ = {
117 "json": "load_json",
118 "querystring": "load_querystring",
119 "query": "load_querystring",
120 "form": "load_form",
121 "headers": "load_headers",
122 "cookies": "load_cookies",
123 "files": "load_files",
124 "json_or_form": "load_json_or_form",
162 "json": "parse_json",
163 "querystring": "parse_querystring",
164 "query": "parse_querystring",
165 "form": "parse_form",
166 "headers": "parse_headers",
167 "cookies": "parse_cookies",
168 "files": "parse_files",
125169 }
126170
127 def __init__(self, location=None, *, error_handler=None, schema_class=None):
128 self.location = location or self.DEFAULT_LOCATION
171 def __init__(self, locations=None, error_handler=None, schema_class=None):
172 self.locations = locations or self.DEFAULT_LOCATIONS
129173 self.error_callback = _callable_or_raise(error_handler)
130174 self.schema_class = schema_class or self.DEFAULT_SCHEMA_CLASS
131
132 def _get_loader(self, location):
133 """Get the loader function for the given location.
134
135 :raises: ValueError if a given location is invalid.
136 """
175 #: A short-lived cache to store results from processing request bodies.
176 self._cache = {}
177
178 def _validated_locations(self, locations):
179 """Ensure that the given locations argument is valid.
180
181 :raises: ValueError if a given locations includes an invalid location.
182 """
183 # The set difference between the given locations and the available locations
184 # will be the set of invalid locations
137185 valid_locations = set(self.__location_map__.keys())
138 if location not in valid_locations:
139 msg = "Invalid location argument: {}".format(location)
186 given = set(locations)
187 invalid_locations = given - valid_locations
188 if len(invalid_locations):
189 msg = "Invalid locations arguments: {0}".format(list(invalid_locations))
140190 raise ValueError(msg)
141
191 return locations
192
193 def _get_handler(self, location):
142194 # Parsing function to call
143195 # May be a method name (str) or a function
144196 func = self.__location_map__.get(location)
148200 else:
149201 function = getattr(self, func)
150202 else:
151 raise ValueError('Invalid location: "{}"'.format(location))
203 raise ValueError('Invalid location: "{0}"'.format(location))
152204 return function
153205
154 def _load_location_data(self, *, schema, req, location):
155 """Return a dictionary-like object for the location on the given request.
156
157 Needs to have the schema in hand in order to correctly handle loading
158 lists from multidict objects and `many=True` schemas.
159 """
160 loader_func = self._get_loader(location)
161 data = loader_func(req, schema)
162 # when the desired location is empty (no data), provide an empty
163 # dict as the default so that optional arguments in a location
164 # (e.g. optional JSON body) work smoothly
165 if data is missing:
166 data = {}
167 return data
206 def _get_value(self, name, argobj, req, location):
207 function = self._get_handler(location)
208 return function(req, name, argobj)
209
210 def parse_arg(self, name, field, req, locations=None):
211 """Parse a single argument from a request.
212
213 .. note::
214 This method does not perform validation on the argument.
215
216 :param str name: The name of the value.
217 :param marshmallow.fields.Field field: The marshmallow `Field` for the request
218 parameter.
219 :param req: The request object to parse.
220 :param tuple locations: The locations ('json', 'querystring', etc.) where
221 to search for the value.
222 :return: The unvalidated argument value or `missing` if the value cannot
223 be found on the request.
224 """
225 location = field.metadata.get("location")
226 if location:
227 locations_to_check = self._validated_locations([location])
228 else:
229 locations_to_check = self._validated_locations(locations or self.locations)
230
231 for location in locations_to_check:
232 value = self._get_value(name, field, req=req, location=location)
233 # Found the value; validate and return it
234 if value is not missing:
235 return value
236 return missing
237
238 def _parse_request(self, schema, req, locations):
239 """Return a parsed arguments dictionary for the current request."""
240 if schema.many:
241 assert (
242 "json" in locations
243 ), "schema.many=True is only supported for JSON location"
244 # The ad hoc Nested field is more like a workaround or a helper,
245 # and it servers its purpose fine. However, if somebody has a desire
246 # to re-design the support of bulk-type arguments, go ahead.
247 parsed = self.parse_arg(
248 name="json",
249 field=ma.fields.Nested(schema, many=True),
250 req=req,
251 locations=locations,
252 )
253 if parsed is missing:
254 parsed = []
255 else:
256 argdict = schema.fields
257 parsed = {}
258 for argname, field_obj in iteritems(argdict):
259 if MARSHMALLOW_VERSION_INFO[0] < 3:
260 parsed_value = self.parse_arg(argname, field_obj, req, locations)
261 # If load_from is specified on the field, try to parse from that key
262 if parsed_value is missing and field_obj.load_from:
263 parsed_value = self.parse_arg(
264 field_obj.load_from, field_obj, req, locations
265 )
266 argname = field_obj.load_from
267 else:
268 argname = field_obj.data_key or argname
269 parsed_value = self.parse_arg(argname, field_obj, req, locations)
270 if parsed_value is not missing:
271 parsed[argname] = parsed_value
272 return parsed
168273
169274 def _on_validation_error(
170 self, error, req, schema, location, *, error_status_code, error_headers
275 self, error, req, schema, error_status_code, error_headers
171276 ):
172 # rewrite messages to be namespaced under the location which created
173 # them
174 # e.g. {"json":{"foo":["Not a valid integer."]}}
175 # instead of
176 # {"foo":["Not a valid integer."]}
177 error.messages = {location: error.messages}
178277 error_handler = self.error_callback or self.handle_error
179 error_handler(
180 error,
181 req,
182 schema,
183 error_status_code=error_status_code,
184 error_headers=error_headers,
185 )
278 error_handler(error, req, schema, error_status_code, error_headers)
186279
187280 def _validate_arguments(self, data, validators):
188281 for validator in validators:
206299 elif callable(argmap):
207300 schema = argmap(req)
208301 else:
209 schema = dict2schema(argmap, schema_class=self.schema_class)()
302 schema = dict2schema(argmap, self.schema_class)()
210303 if MARSHMALLOW_VERSION_INFO[0] < 3 and not schema.strict:
211304 warnings.warn(
212305 "It is highly recommended that you set strict=True on your schema "
215308 )
216309 return schema
217310
311 def _clone(self):
312 clone = copy(self)
313 clone.clear_cache()
314 return clone
315
218316 def parse(
219317 self,
220318 argmap,
221319 req=None,
222 *,
223 location=None,
320 locations=None,
224321 validate=None,
225322 error_status_code=None,
226 error_headers=None
323 error_headers=None,
227324 ):
228325 """Main request parsing method.
229326
231328 of argname -> `marshmallow.fields.Field` pairs, or a callable
232329 which accepts a request and returns a `marshmallow.Schema`.
233330 :param req: The request object to parse.
234 :param str location: Where on the request to load values.
235 Can be any of the values in :py:attr:`~__location_map__`. By
236 default, that means one of ``('json', 'query', 'querystring',
237 'form', 'headers', 'cookies', 'files', 'json_or_form')``.
331 :param tuple locations: Where on the request to search for values.
332 Can include one or more of ``('json', 'querystring', 'form',
333 'headers', 'cookies', 'files')``.
238334 :param callable validate: Validation function or list of validation functions
239335 that receives the dictionary of parsed arguments. Validator either returns a
240336 boolean or raises a :exc:`ValidationError`.
245341
246342 :return: A dictionary of parsed arguments
247343 """
344 self.clear_cache() # in case someone used `parse_*()`
248345 req = req if req is not None else self.get_default_request()
249 location = location or self.location
250 if req is None:
251 raise ValueError("Must pass req object")
346 assert req is not None, "Must pass req object"
252347 data = None
253348 validators = _ensure_list_of_callables(validate)
349 parser = self._clone()
254350 schema = self._get_schema(argmap, req)
255351 try:
256 location_data = self._load_location_data(
257 schema=schema, req=req, location=location
352 parsed = parser._parse_request(
353 schema=schema, req=req, locations=locations or self.locations
258354 )
259 result = schema.load(location_data)
355 result = schema.load(parsed)
260356 data = result.data if MARSHMALLOW_VERSION_INFO[0] < 3 else result
261 self._validate_arguments(data, validators)
357 parser._validate_arguments(data, validators)
262358 except ma.exceptions.ValidationError as error:
263 self._on_validation_error(
264 error,
265 req,
266 schema,
267 location,
268 error_status_code=error_status_code,
269 error_headers=error_headers,
359 parser._on_validation_error(
360 error, req, schema, error_status_code, error_headers
270361 )
271362 return data
363
364 def clear_cache(self):
365 """Invalidate the parser's cache.
366
367 This is usually a no-op now since the Parser clone used for parsing a
368 request is discarded afterwards. It can still be used when manually
369 calling ``parse_*`` methods which would populate the cache on the main
370 Parser instance.
371 """
372 self._cache = {}
373 return None
272374
273375 def get_default_request(self):
274376 """Optional override. Provides a hook for frameworks that use thread-local
290392 """
291393 return None
292394
293 @staticmethod
294 def _update_args_kwargs(args, kwargs, parsed_args, as_kwargs):
295 """Update args or kwargs with parsed_args depending on as_kwargs"""
296 if as_kwargs:
297 kwargs.update(parsed_args)
298 else:
299 # Add parsed_args after other positional arguments
300 args += (parsed_args,)
301 return args, kwargs
302
303395 def use_args(
304396 self,
305397 argmap,
306398 req=None,
307 *,
308 location=None,
399 locations=None,
309400 as_kwargs=False,
310401 validate=None,
311402 error_status_code=None,
312 error_headers=None
403 error_headers=None,
313404 ):
314405 """Decorator that injects parsed arguments into a view function or method.
315406
316407 Example usage with Flask: ::
317408
318409 @app.route('/echo', methods=['get', 'post'])
319 @parser.use_args({'name': fields.Str()}, location="querystring")
410 @parser.use_args({'name': fields.Str()})
320411 def greet(args):
321412 return 'Hello ' + args['name']
322413
323414 :param argmap: Either a `marshmallow.Schema`, a `dict`
324415 of argname -> `marshmallow.fields.Field` pairs, or a callable
325416 which accepts a request and returns a `marshmallow.Schema`.
326 :param str location: Where on the request to load values.
417 :param tuple locations: Where on the request to search for values.
327418 :param bool as_kwargs: Whether to insert arguments as keyword arguments.
328419 :param callable validate: Validation function that receives the dictionary
329420 of parsed arguments. If the function returns ``False``, the parser
333424 :param dict error_headers: Headers passed to error handler functions when a
334425 a `ValidationError` is raised.
335426 """
336 location = location or self.location
427 locations = locations or self.locations
337428 request_obj = req
338429 # Optimization: If argmap is passed as a dictionary, we only need
339430 # to generate a Schema once
340431 if isinstance(argmap, Mapping):
341 argmap = dict2schema(argmap, schema_class=self.schema_class)()
432 argmap = dict2schema(argmap, self.schema_class)()
342433
343434 def decorator(func):
344435 req_ = request_obj
349440
350441 if not req_obj:
351442 req_obj = self.get_request_from_view_args(func, args, kwargs)
352
353443 # NOTE: At this point, argmap may be a Schema, or a callable
354444 parsed_args = self.parse(
355445 argmap,
356446 req=req_obj,
357 location=location,
447 locations=locations,
358448 validate=validate,
359449 error_status_code=error_status_code,
360450 error_headers=error_headers,
361451 )
362 args, kwargs = self._update_args_kwargs(
363 args, kwargs, parsed_args, as_kwargs
364 )
365 return func(*args, **kwargs)
452 if as_kwargs:
453 kwargs.update(parsed_args)
454 return func(*args, **kwargs)
455 else:
456 # Add parsed_args after other positional arguments
457 new_args = args + (parsed_args,)
458 return func(*new_args, **kwargs)
366459
367460 wrapper.__wrapped__ = func
368461 return wrapper
369462
370463 return decorator
371464
372 def use_kwargs(self, *args, **kwargs) -> typing.Callable:
465 def use_kwargs(self, *args, **kwargs):
373466 """Decorator that injects parsed arguments into a view function or method
374467 as keyword arguments.
375468
387480 kwargs["as_kwargs"] = True
388481 return self.use_args(*args, **kwargs)
389482
390 def location_loader(self, name):
391 """Decorator that registers a function for loading a request location.
392 The wrapped function receives a schema and a request.
393
394 The schema will usually not be relevant, but it's important in some
395 cases -- most notably in order to correctly load multidict values into
396 list fields. Without the schema, there would be no way to know whether
397 to simply `.get()` or `.getall()` from a multidict for a given value.
483 def location_handler(self, name):
484 """Decorator that registers a function for parsing a request location.
485 The wrapped function receives a request, the name of the argument, and
486 the corresponding `Field <marshmallow.fields.Field>` object.
398487
399488 Example: ::
400489
401490 from webargs import core
402491 parser = core.Parser()
403492
404 @parser.location_loader("name")
405 def load_data(request, schema):
406 return request.data
493 @parser.location_handler("name")
494 def parse_data(request, name, field):
495 return request.data.get(name)
407496
408497 :param str name: The name of the location to register.
409498 """
433522
434523
435524 @parser.error_handler
436 def handle_error(error, req, schema, *, status_code, headers):
525 def handle_error(error, req, schema, status_code, headers):
437526 raise CustomError(error.messages)
438527
439528 :param callable func: The error callback to register.
441530 self.error_callback = func
442531 return func
443532
444 def _handle_invalid_json_error(self, error, req, *args, **kwargs):
445 """Internal hook for overriding treatment of JSONDecodeErrors.
446
447 Invoked by default `load_json` implementation.
448
449 External parsers can just implement their own behavior for load_json ,
450 so this is not part of the public parser API.
451 """
452 raise error
453
454 def load_json(self, req, schema):
455 """Load JSON from a request object or return `missing` if no value can
456 be found.
457 """
458 # NOTE: although this implementation is real/concrete and used by
459 # several of the parsers in webargs, it relies on the internal hooks
460 # `_handle_invalid_json_error` and `_raw_load_json`
461 # these methods are not part of the public API and are used to simplify
462 # code sharing amongst the built-in webargs parsers
463 try:
464 return self._raw_load_json(req)
465 except json.JSONDecodeError as exc:
466 if exc.doc == "":
467 return missing
468 return self._handle_invalid_json_error(exc, req)
469 except UnicodeDecodeError as exc:
470 return self._handle_invalid_json_error(exc, req)
471
472 def load_json_or_form(self, req, schema):
473 """Load data from a request, accepting either JSON or form-encoded
474 data.
475
476 The data will first be loaded as JSON, and, if that fails, it will be
477 loaded as a form post.
478 """
479 data = self.load_json(req, schema)
480 if data is not missing:
481 return data
482 return self.load_form(req, schema)
483
484533 # Abstract Methods
485534
486 def _raw_load_json(self, req):
487 """Internal hook method for implementing load_json()
488
489 Get a request body for feeding in to `load_json`, and parse it either
490 using core.parse_json() or similar utilities which raise
491 JSONDecodeErrors.
492 Ensure consistent behavior when encountering decoding errors.
493
494 The default implementation here simply returns `missing`, and the default
495 implementation of `load_json` above will pass that value through.
496 However, by implementing a "mostly concrete" version of load_json with
497 this as a hook for getting data, we consolidate the logic for handling
498 those JSONDecodeErrors.
499 """
500 return missing
501
502 def load_querystring(self, req, schema):
503 """Load the query string of a request object or return `missing` if no
504 value can be found.
505 """
506 return missing
507
508 def load_form(self, req, schema):
509 """Load the form data of a request object or return `missing` if no
510 value can be found.
511 """
512 return missing
513
514 def load_headers(self, req, schema):
515 """Load the headers or return `missing` if no value can be found.
516 """
517 return missing
518
519 def load_cookies(self, req, schema):
520 """Load the cookies from the request or return `missing` if no value
521 can be found.
522 """
523 return missing
524
525 def load_files(self, req, schema):
526 """Load files from the request or return `missing` if no values can be
527 found.
528 """
529 return missing
530
531 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
535 def parse_json(self, req, name, arg):
536 """Pull a JSON value from a request object or return `missing` if the
537 value cannot be found.
538 """
539 return missing
540
541 def parse_querystring(self, req, name, arg):
542 """Pull a value from the query string of a request object or return `missing` if
543 the value cannot be found.
544 """
545 return missing
546
547 def parse_form(self, req, name, arg):
548 """Pull a value from the form data of a request object or return
549 `missing` if the value cannot be found.
550 """
551 return missing
552
553 def parse_headers(self, req, name, arg):
554 """Pull a value from the headers or return `missing` if the value
555 cannot be found.
556 """
557 return missing
558
559 def parse_cookies(self, req, name, arg):
560 """Pull a cookie value from the request or return `missing` if the value
561 cannot be found.
562 """
563 return missing
564
565 def parse_files(self, req, name, arg):
566 """Pull a file from the request or return `missing` if the value file
567 cannot be found.
568 """
569 return missing
570
571 def handle_error(
572 self, error, req, schema, error_status_code=None, error_headers=None
573 ):
532574 """Called if an error occurs while parsing args. By default, just logs and
533575 raises ``error``.
534576 """
0 # -*- coding: utf-8 -*-
01 import marshmallow as ma
12
23
3 def dict2schema(dct, *, schema_class=ma.Schema):
4 def dict2schema(dct, schema_class=ma.Schema):
45 """Generate a `marshmallow.Schema` class given a dictionary of
56 `Fields <marshmallow.fields.Field>`.
67 """
89 return schema_class.from_dict(dct)
910 attrs = dct.copy()
1011
11 class Meta:
12 class Meta(object):
1213 strict = True
1314
1415 attrs["Meta"] = Meta
15 return type("", (schema_class,), attrs)
16 return type(str(""), (schema_class,), attrs)
0 # -*- coding: utf-8 -*-
01 """Django request argument parsing.
12
23 Example usage: ::
1718 return HttpResponse('Hello ' + args['name'])
1819 """
1920 from webargs import core
20 from webargs.multidictproxy import MultiDictProxy
21
22
23 def is_json_request(req):
24 return core.is_json(req.content_type)
21 from webargs.core import json
2522
2623
2724 class DjangoParser(core.Parser):
3532 the parser and returning the appropriate `HTTPResponse`.
3633 """
3734
38 def _raw_load_json(self, req):
39 """Read a json payload from the request for the core parser's load_json
35 def parse_querystring(self, req, name, field):
36 """Pull the querystring value from the request."""
37 return core.get_value(req.GET, name, field)
4038
41 Checks the input mimetype and may return 'missing' if the mimetype is
42 non-json, even if the request body is parseable as json."""
43 if not is_json_request(req):
44 return core.missing
39 def parse_form(self, req, name, field):
40 """Pull the form value from the request."""
41 return core.get_value(req.POST, name, field)
4542
46 return core.parse_json(req.body)
43 def parse_json(self, req, name, field):
44 """Pull a json value from the request body."""
45 json_data = self._cache.get("json")
46 if json_data is None:
47 if not core.is_json(req.content_type):
48 return core.missing
4749
48 def load_querystring(self, req, schema):
49 """Return query params from the request as a MultiDictProxy."""
50 return MultiDictProxy(req.GET, schema)
50 try:
51 self._cache["json"] = json_data = core.parse_json(req.body)
52 except AttributeError:
53 return core.missing
54 except json.JSONDecodeError as e:
55 if e.doc == "":
56 return core.missing
57 else:
58 return self.handle_invalid_json_error(e, req)
59 return core.get_value(json_data, name, field, allow_many_nested=True)
5160
52 def load_form(self, req, schema):
53 """Return form values from the request as a MultiDictProxy."""
54 return MultiDictProxy(req.POST, schema)
61 def parse_cookies(self, req, name, field):
62 """Pull the value from the cookiejar."""
63 return core.get_value(req.COOKIES, name, field)
5564
56 def load_cookies(self, req, schema):
57 """Return cookies from the request."""
58 return req.COOKIES
59
60 def load_headers(self, req, schema):
65 def parse_headers(self, req, name, field):
6166 raise NotImplementedError(
62 "Header parsing not supported by {}".format(self.__class__.__name__)
67 "Header parsing not supported by {0}".format(self.__class__.__name__)
6368 )
6469
65 def load_files(self, req, schema):
66 """Return files from the request as a MultiDictProxy."""
67 return MultiDictProxy(req.FILES, schema)
70 def parse_files(self, req, name, field):
71 """Pull a file from the request."""
72 return core.get_value(req.FILES, name, field)
6873
6974 def get_request_from_view_args(self, view, args, kwargs):
7075 # The first argument is either `self` or `request`
7378 except AttributeError: # first arg is request
7479 return args[0]
7580
81 def handle_invalid_json_error(self, error, req, *args, **kwargs):
82 raise error
83
7684
7785 parser = DjangoParser()
7886 use_args = parser.use_args
0 # -*- coding: utf-8 -*-
01 """Falcon request argument parsing module.
12 """
23 import falcon
34 from falcon.util.uri import parse_query_string
45
56 from webargs import core
6 from webargs.multidictproxy import MultiDictProxy
7 from webargs.core import json
78
89 HTTP_422 = "422 Unprocessable Entity"
910
2829 return content_type and core.is_json(content_type)
2930
3031
32 def parse_json_body(req):
33 if req.content_length in (None, 0):
34 # Nothing to do
35 return {}
36 if is_json_request(req):
37 body = req.stream.read()
38 if body:
39 try:
40 return core.parse_json(body)
41 except json.JSONDecodeError as e:
42 if e.doc == "":
43 return core.missing
44 else:
45 raise
46 return {}
47
48
3149 # NOTE: Adapted from falcon.request.Request._parse_form_urlencoded
3250 def parse_form_body(req):
3351 if (
3452 req.content_type is not None
3553 and "application/x-www-form-urlencoded" in req.content_type
3654 ):
37 body = req.stream.read(req.content_length or 0)
55 body = req.stream.read()
3856 try:
3957 body = body.decode("ascii")
4058 except UnicodeDecodeError:
4765 )
4866
4967 if body:
50 return parse_query_string(body, keep_blank=req.options.keep_blank_qs_values)
51
52 return core.missing
68 return parse_query_string(
69 body, keep_blank_qs_values=req.options.keep_blank_qs_values
70 )
71 return {}
5372
5473
5574 class HTTPError(falcon.HTTPError):
5877
5978 def __init__(self, status, errors, *args, **kwargs):
6079 self.errors = errors
61 super().__init__(status, *args, **kwargs)
80 super(HTTPError, self).__init__(status, *args, **kwargs)
6281
6382 def to_dict(self, *args, **kwargs):
6483 """Override `falcon.HTTPError` to include error messages in responses."""
65 ret = super().to_dict(*args, **kwargs)
84 ret = super(HTTPError, self).to_dict(*args, **kwargs)
6685 if self.errors is not None:
6786 ret["errors"] = self.errors
6887 return ret
7190 class FalconParser(core.Parser):
7291 """Falcon request argument parser."""
7392
74 # Note on the use of MultiDictProxy throughout:
75 # Falcon parses query strings and form values into ordinary dicts, but with
76 # the values listified where appropriate
77 # it is still therefore necessary in these cases to wrap them in
78 # MultiDictProxy because we need to use the schema to determine when single
79 # values should be wrapped in lists due to the type of the destination
80 # field
93 def parse_querystring(self, req, name, field):
94 """Pull a querystring value from the request."""
95 return core.get_value(req.params, name, field)
8196
82 def load_querystring(self, req, schema):
83 """Return query params from the request as a MultiDictProxy."""
84 return MultiDictProxy(req.params, schema)
85
86 def load_form(self, req, schema):
87 """Return form values from the request as a MultiDictProxy
97 def parse_form(self, req, name, field):
98 """Pull a form value from the request.
8899
89100 .. note::
90101
91102 The request stream will be read and left at EOF.
92103 """
93 form = parse_form_body(req)
94 if form is core.missing:
95 return form
96 return MultiDictProxy(form, schema)
104 form = self._cache.get("form")
105 if form is None:
106 self._cache["form"] = form = parse_form_body(req)
107 return core.get_value(form, name, field)
97108
98 def _raw_load_json(self, req):
99 """Return a json payload from the request for the core parser's load_json
109 def parse_json(self, req, name, field):
110 """Pull a JSON body value from the request.
100111
101 Checks the input mimetype and may return 'missing' if the mimetype is
102 non-json, even if the request body is parseable as json."""
103 if not is_json_request(req) or req.content_length in (None, 0):
104 return core.missing
105 body = req.stream.read(req.content_length)
106 if body:
107 return core.parse_json(body)
108 return core.missing
112 .. note::
109113
110 def load_headers(self, req, schema):
111 """Return headers from the request."""
112 # Falcon only exposes headers as a dict (not multidict)
113 return req.headers
114 The request stream will be read and left at EOF.
115 """
116 json_data = self._cache.get("json_data")
117 if json_data is None:
118 try:
119 self._cache["json_data"] = json_data = parse_json_body(req)
120 except json.JSONDecodeError as e:
121 return self.handle_invalid_json_error(e, req)
122 return core.get_value(json_data, name, field, allow_many_nested=True)
114123
115 def load_cookies(self, req, schema):
116 """Return cookies from the request."""
117 # Cookies are expressed in Falcon as a dict, but the possibility of
118 # multiple values for a cookie is preserved internally -- if desired in
119 # the future, webargs could add a MultiDict type for Cookies here built
120 # from (req, schema), but Falcon does not provide one out of the box
121 return req.cookies
124 def parse_headers(self, req, name, field):
125 """Pull a header value from the request."""
126 # Use req.get_headers rather than req.headers for performance
127 return req.get_header(name, required=False) or core.missing
128
129 def parse_cookies(self, req, name, field):
130 """Pull a cookie value from the request."""
131 cookies = self._cache.get("cookies")
132 if cookies is None:
133 self._cache["cookies"] = cookies = req.cookies
134 return core.get_value(cookies, name, field)
122135
123136 def get_request_from_view_args(self, view, args, kwargs):
124137 """Get request from a resource method's arguments. Assumes that
125138 request is the second argument.
126139 """
127140 req = args[1]
128 if not isinstance(req, falcon.Request):
129 raise TypeError("Argument is not a falcon.Request")
141 assert isinstance(req, falcon.Request), "Argument is not a falcon.Request"
130142 return req
131143
132 def load_files(self, req, schema):
144 def parse_files(self, req, name, field):
133145 raise NotImplementedError(
134 "Parsing files not yet supported by {}".format(self.__class__.__name__)
146 "Parsing files not yet supported by {0}".format(self.__class__.__name__)
135147 )
136148
137 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
149 def handle_error(self, error, req, schema, error_status_code, error_headers):
138150 """Handles errors during parsing."""
139151 status = status_map.get(error_status_code or self.DEFAULT_VALIDATION_STATUS)
140152 if status is None:
141 raise LookupError("Status code {} not supported".format(error_status_code))
153 raise LookupError("Status code {0} not supported".format(error_status_code))
142154 raise HTTPError(status, errors=error.messages, headers=error_headers)
143155
144 def _handle_invalid_json_error(self, error, req, *args, **kwargs):
156 def handle_invalid_json_error(self, error, req, *args, **kwargs):
145157 status = status_map[400]
146158 messages = {"json": ["Invalid JSON body."]}
147159 raise HTTPError(status, errors=messages)
0 # -*- coding: utf-8 -*-
01 """Field classes.
12
23 Includes all fields from `marshmallow.fields` in addition to a custom
89 .. code-block:: python
910
1011 args = {
11 "active": fields.Bool(location="query"),
12 "active": fields.Bool(location='query'),
1213 "content_type": fields.Str(data_key="Content-Type", location="headers"),
1314 }
1415
3940 def __init__(self, nested, *args, **kwargs):
4041 if isinstance(nested, dict):
4142 nested = dict2schema(nested)
42 super().__init__(nested, *args, **kwargs)
43 super(Nested, self).__init__(nested, *args, **kwargs)
4344
4445
45 class DelimitedFieldMixin:
46 """
47 This is a mixin class for subclasses of ma.fields.List and ma.fields.Tuple
48 which split on a pre-specified delimiter. By default, the delimiter will be ","
46 class DelimitedList(ma.fields.List):
47 """Same as `marshmallow.fields.List`, except can load from either a list or
48 a delimited string (e.g. "foo,bar,baz").
4949
50 Because we want the MRO to reach this class before the List or Tuple class,
51 it must be listed first in the superclasses
52
53 For example, a DelimitedList-like type can be defined like so:
54
55 >>> class MyDelimitedList(DelimitedFieldMixin, ma.fields.List):
56 >>> pass
50 :param Field cls_or_instance: A field class or instance.
51 :param str delimiter: Delimiter between values.
52 :param bool as_string: Dump values to string.
5753 """
5854
5955 delimiter = ","
6056
57 def __init__(self, cls_or_instance, delimiter=None, as_string=False, **kwargs):
58 self.delimiter = delimiter or self.delimiter
59 self.as_string = as_string
60 super(DelimitedList, self).__init__(cls_or_instance, **kwargs)
61
6162 def _serialize(self, value, attr, obj):
62 # serializing will start with parent-class serialization, so that we correctly
63 # output lists of non-primitive types, e.g. DelimitedList(DateTime)
64 return self.delimiter.join(
65 format(each) for each in super()._serialize(value, attr, obj)
66 )
63 ret = super(DelimitedList, self)._serialize(value, attr, obj)
64 if self.as_string:
65 return self.delimiter.join(format(each) for each in ret)
66 return ret
6767
6868 def _deserialize(self, value, attr, data, **kwargs):
69 # attempting to deserialize from a non-string source is an error
70 if not isinstance(value, (str, bytes)):
69 try:
70 ret = (
71 value
72 if ma.utils.is_iterable_but_not_string(value)
73 else value.split(self.delimiter)
74 )
75 except AttributeError:
7176 if MARSHMALLOW_VERSION_INFO[0] < 3:
7277 self.fail("invalid")
7378 else:
7479 raise self.make_error("invalid")
75 return super()._deserialize(value.split(self.delimiter), attr, data, **kwargs)
76
77
78 class DelimitedList(DelimitedFieldMixin, ma.fields.List):
79 """A field which is similar to a List, but takes its input as a delimited
80 string (e.g. "foo,bar,baz").
81
82 Like List, it can be given a nested field type which it will use to
83 de/serialize each element of the list.
84
85 :param Field cls_or_instance: A field class or instance.
86 :param str delimiter: Delimiter between values.
87 """
88
89 default_error_messages = {"invalid": "Not a valid delimited list."}
90 delimiter = ","
91
92 def __init__(self, cls_or_instance, *, delimiter=None, **kwargs):
93 self.delimiter = delimiter or self.delimiter
94 super().__init__(cls_or_instance, **kwargs)
95
96
97 # DelimitedTuple can only be defined when using marshmallow3, when Tuple was
98 # added
99 if MARSHMALLOW_VERSION_INFO[0] >= 3:
100
101 class DelimitedTuple(DelimitedFieldMixin, ma.fields.Tuple):
102 """A field which is similar to a Tuple, but takes its input as a delimited
103 string (e.g. "foo,bar,baz").
104
105 Like Tuple, it can be given a tuple of nested field types which it will use to
106 de/serialize each element of the tuple.
107
108 :param Iterable[Field] tuple_fields: An iterable of field classes or instances.
109 :param str delimiter: Delimiter between values.
110 """
111
112 default_error_messages = {"invalid": "Not a valid delimited tuple."}
113 delimiter = ","
114
115 def __init__(self, tuple_fields, *, delimiter=None, **kwargs):
116 self.delimiter = delimiter or self.delimiter
117 super().__init__(tuple_fields, **kwargs)
80 return super(DelimitedList, self)._deserialize(ret, attr, data, **kwargs)
0 # -*- coding: utf-8 -*-
01 """Flask request argument parsing module.
12
23 Example: ::
89
910 app = Flask(__name__)
1011
11 user_detail_args = {
12 'per_page': fields.Int()
12 hello_args = {
13 'name': fields.Str(required=True)
1314 }
1415
15 @app.route("/user/<int:uid>")
16 @use_args(user_detail_args)
17 def user_detail(args, uid):
18 return ("The user page for user {uid}, showing {per_page} posts.").format(
19 uid=uid, per_page=args["per_page"]
20 )
16 @app.route('/')
17 @use_args(hello_args)
18 def index(args):
19 return 'Hello ' + args['name']
2120 """
2221 import flask
2322 from werkzeug.exceptions import HTTPException
2423
2524 from webargs import core
26 from webargs.compat import MARSHMALLOW_VERSION_INFO
27 from webargs.multidictproxy import MultiDictProxy
25 from webargs.core import json
2826
2927
3028 def abort(http_status_code, exc=None, **kwargs):
4947 """Flask request argument parser."""
5048
5149 __location_map__ = dict(
52 view_args="load_view_args",
53 path="load_view_args",
54 **core.Parser.__location_map__,
50 view_args="parse_view_args",
51 path="parse_view_args",
52 **core.Parser.__location_map__
5553 )
5654
57 def _raw_load_json(self, req):
58 """Return a json payload from the request for the core parser's load_json
55 def parse_view_args(self, req, name, field):
56 """Pull a value from the request's ``view_args``."""
57 return core.get_value(req.view_args, name, field)
5958
60 Checks the input mimetype and may return 'missing' if the mimetype is
61 non-json, even if the request body is parseable as json."""
62 if not is_json_request(req):
63 return core.missing
59 def parse_json(self, req, name, field):
60 """Pull a json value from the request."""
61 json_data = self._cache.get("json")
62 if json_data is None:
63 if not is_json_request(req):
64 return core.missing
6465
65 return core.parse_json(req.get_data(cache=True))
66 # We decode the json manually here instead of
67 # using req.get_json() so that we can handle
68 # JSONDecodeErrors consistently
69 data = req.get_data(cache=True)
70 try:
71 self._cache["json"] = json_data = core.parse_json(data)
72 except json.JSONDecodeError as e:
73 if e.doc == "":
74 return core.missing
75 else:
76 return self.handle_invalid_json_error(e, req)
77 return core.get_value(json_data, name, field, allow_many_nested=True)
6678
67 def _handle_invalid_json_error(self, error, req, *args, **kwargs):
68 abort(400, exc=error, messages={"json": ["Invalid JSON body."]})
79 def parse_querystring(self, req, name, field):
80 """Pull a querystring value from the request."""
81 return core.get_value(req.args, name, field)
6982
70 def load_view_args(self, req, schema):
71 """Return the request's ``view_args`` or ``missing`` if there are none."""
72 return req.view_args or core.missing
83 def parse_form(self, req, name, field):
84 """Pull a form value from the request."""
85 try:
86 return core.get_value(req.form, name, field)
87 except AttributeError:
88 pass
89 return core.missing
7390
74 def load_querystring(self, req, schema):
75 """Return query params from the request as a MultiDictProxy."""
76 return MultiDictProxy(req.args, schema)
91 def parse_headers(self, req, name, field):
92 """Pull a value from the header data."""
93 return core.get_value(req.headers, name, field)
7794
78 def load_form(self, req, schema):
79 """Return form values from the request as a MultiDictProxy."""
80 return MultiDictProxy(req.form, schema)
95 def parse_cookies(self, req, name, field):
96 """Pull a value from the cookiejar."""
97 return core.get_value(req.cookies, name, field)
8198
82 def load_headers(self, req, schema):
83 """Return headers from the request as a MultiDictProxy."""
84 return MultiDictProxy(req.headers, schema)
99 def parse_files(self, req, name, field):
100 """Pull a file from the request."""
101 return core.get_value(req.files, name, field)
85102
86 def load_cookies(self, req, schema):
87 """Return cookies from the request."""
88 return req.cookies
89
90 def load_files(self, req, schema):
91 """Return files from the request as a MultiDictProxy."""
92 return MultiDictProxy(req.files, schema)
93
94 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
103 def handle_error(self, error, req, schema, error_status_code, error_headers):
95104 """Handles errors during parsing. Aborts the current HTTP request and
96105 responds with a 422 error.
97106 """
98107 status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS
99 # on marshmallow 2, a many schema receiving a non-list value will
100 # produce this specific error back -- reformat it to match the
101 # marshmallow 3 message so that Flask can properly encode it
102 messages = error.messages
103 if (
104 MARSHMALLOW_VERSION_INFO[0] < 3
105 and schema.many
106 and messages == {0: {}, "_schema": ["Invalid input type."]}
107 ):
108 messages.pop(0)
109108 abort(
110109 status_code,
111110 exc=error,
114113 headers=error_headers,
115114 )
116115
116 def handle_invalid_json_error(self, error, req, *args, **kwargs):
117 abort(400, exc=error, messages={"json": ["Invalid JSON body."]})
118
117119 def get_default_request(self):
118 """Override to use Flask's thread-local request object by default"""
120 """Override to use Flask's thread-local request objec by default"""
119121 return flask.request
120122
121123
+0
-77
src/webargs/multidictproxy.py less more
0 from collections.abc import Mapping
1
2 from webargs.compat import MARSHMALLOW_VERSION_INFO
3 from webargs.core import missing, is_multiple
4
5
6 class MultiDictProxy(Mapping):
7 """
8 A proxy object which wraps multidict types along with a matching schema
9 Whenever a value is looked up, it is checked against the schema to see if
10 there is a matching field where `is_multiple` is True. If there is, then
11 the data should be loaded as a list or tuple.
12
13 In all other cases, __getitem__ proxies directly to the input multidict.
14 """
15
16 def __init__(self, multidict, schema):
17 self.data = multidict
18 self.multiple_keys = self._collect_multiple_keys(schema)
19
20 @staticmethod
21 def _collect_multiple_keys(schema):
22 result = set()
23 for name, field in schema.fields.items():
24 if not is_multiple(field):
25 continue
26 if MARSHMALLOW_VERSION_INFO[0] < 3:
27 result.add(field.load_from if field.load_from is not None else name)
28 else:
29 result.add(field.data_key if field.data_key is not None else name)
30 return result
31
32 def __getitem__(self, key):
33 val = self.data.get(key, missing)
34 if val is missing or key not in self.multiple_keys:
35 return val
36 if hasattr(self.data, "getlist"):
37 return self.data.getlist(key)
38 if hasattr(self.data, "getall"):
39 return self.data.getall(key)
40 if isinstance(val, (list, tuple)):
41 return val
42 if val is None:
43 return None
44 return [val]
45
46 def __str__(self): # str(proxy) proxies to str(proxy.data)
47 return str(self.data)
48
49 def __repr__(self):
50 return "MultiDictProxy(data={!r}, multiple_keys={!r})".format(
51 self.data, self.multiple_keys
52 )
53
54 def __delitem__(self, key):
55 del self.data[key]
56
57 def __setitem__(self, key, value):
58 self.data[key] = value
59
60 def __getattr__(self, name):
61 return getattr(self.data, name)
62
63 def __iter__(self):
64 return iter(self.data)
65
66 def __contains__(self, x):
67 return x in self.data
68
69 def __len__(self):
70 return len(self.data)
71
72 def __eq__(self, other):
73 return self.data == other
74
75 def __ne__(self, other):
76 return self.data != other
0 # -*- coding: utf-8 -*-
01 """Pyramid request argument parsing.
12
23 Example usage: ::
2324 server = make_server('0.0.0.0', 6543, app)
2425 server.serve_forever()
2526 """
27 import collections
2628 import functools
27 from collections.abc import Mapping
2829
2930 from webob.multidict import MultiDict
3031 from pyramid.httpexceptions import exception_response
3132
3233 from webargs import core
3334 from webargs.core import json
34 from webargs.multidictproxy import MultiDictProxy
35
36
37 def is_json_request(req):
38 return core.is_json(req.headers.get("content-type"))
35 from webargs.compat import text_type
3936
4037
4138 class PyramidParser(core.Parser):
4239 """Pyramid request argument parser."""
4340
4441 __location_map__ = dict(
45 matchdict="load_matchdict",
46 path="load_matchdict",
47 **core.Parser.__location_map__,
42 matchdict="parse_matchdict",
43 path="parse_matchdict",
44 **core.Parser.__location_map__
4845 )
4946
50 def _raw_load_json(self, req):
51 """Return a json payload from the request for the core parser's load_json
47 def parse_querystring(self, req, name, field):
48 """Pull a querystring value from the request."""
49 return core.get_value(req.GET, name, field)
5250
53 Checks the input mimetype and may return 'missing' if the mimetype is
54 non-json, even if the request body is parseable as json."""
55 if not is_json_request(req):
56 return core.missing
51 def parse_form(self, req, name, field):
52 """Pull a form value from the request."""
53 return core.get_value(req.POST, name, field)
5754
58 return core.parse_json(req.body, encoding=req.charset)
55 def parse_json(self, req, name, field):
56 """Pull a json value from the request."""
57 json_data = self._cache.get("json")
58 if json_data is None:
59 if not core.is_json(req.content_type):
60 return core.missing
5961
60 def load_querystring(self, req, schema):
61 """Return query params from the request as a MultiDictProxy."""
62 return MultiDictProxy(req.GET, schema)
62 try:
63 self._cache["json"] = json_data = core.parse_json(req.body, req.charset)
64 except json.JSONDecodeError as e:
65 if e.doc == "":
66 return core.missing
67 else:
68 return self.handle_invalid_json_error(e, req)
69 if json_data is None:
70 return core.missing
71 return core.get_value(json_data, name, field, allow_many_nested=True)
6372
64 def load_form(self, req, schema):
65 """Return form values from the request as a MultiDictProxy."""
66 return MultiDictProxy(req.POST, schema)
73 def parse_cookies(self, req, name, field):
74 """Pull the value from the cookiejar."""
75 return core.get_value(req.cookies, name, field)
6776
68 def load_cookies(self, req, schema):
69 """Return cookies from the request as a MultiDictProxy."""
70 return MultiDictProxy(req.cookies, schema)
77 def parse_headers(self, req, name, field):
78 """Pull a value from the header data."""
79 return core.get_value(req.headers, name, field)
7180
72 def load_headers(self, req, schema):
73 """Return headers from the request as a MultiDictProxy."""
74 return MultiDictProxy(req.headers, schema)
81 def parse_files(self, req, name, field):
82 """Pull a file from the request."""
83 files = ((k, v) for k, v in req.POST.items() if hasattr(v, "file"))
84 return core.get_value(MultiDict(files), name, field)
7585
76 def load_files(self, req, schema):
77 """Return files from the request as a MultiDictProxy."""
78 files = ((k, v) for k, v in req.POST.items() if hasattr(v, "file"))
79 return MultiDictProxy(MultiDict(files), schema)
86 def parse_matchdict(self, req, name, field):
87 """Pull a value from the request's `matchdict`."""
88 return core.get_value(req.matchdict, name, field)
8089
81 def load_matchdict(self, req, schema):
82 """Return the request's ``matchdict`` as a MultiDictProxy."""
83 return MultiDictProxy(req.matchdict, schema)
84
85 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
90 def handle_error(self, error, req, schema, error_status_code, error_headers):
8691 """Handles errors during parsing. Aborts the current HTTP request and
8792 responds with a 400 error.
8893 """
8994 status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS
9095 response = exception_response(
9196 status_code,
92 detail=str(error),
97 detail=text_type(error),
9398 headers=error_headers,
9499 content_type="application/json",
95100 )
96101 body = json.dumps(error.messages)
97 response.body = body.encode("utf-8") if isinstance(body, str) else body
102 response.body = body.encode("utf-8") if isinstance(body, text_type) else body
98103 raise response
99104
100 def _handle_invalid_json_error(self, error, req, *args, **kwargs):
105 def handle_invalid_json_error(self, error, req, *args, **kwargs):
101106 messages = {"json": ["Invalid JSON body."]}
102107 response = exception_response(
103 400, detail=str(messages), content_type="application/json"
108 400, detail=text_type(messages), content_type="application/json"
104109 )
105110 body = json.dumps(messages)
106 response.body = body.encode("utf-8") if isinstance(body, str) else body
111 response.body = body.encode("utf-8") if isinstance(body, text_type) else body
107112 raise response
108113
109114 def use_args(
110115 self,
111116 argmap,
112117 req=None,
113 *,
114 location=core.Parser.DEFAULT_LOCATION,
118 locations=core.Parser.DEFAULT_LOCATIONS,
115119 as_kwargs=False,
116120 validate=None,
117121 error_status_code=None,
118 error_headers=None
122 error_headers=None,
119123 ):
120124 """Decorator that injects parsed arguments into a view callable.
121125 Supports the *Class-based View* pattern where `request` is saved as an instance
125129 of argname -> `marshmallow.fields.Field` pairs, or a callable
126130 which accepts a request and returns a `marshmallow.Schema`.
127131 :param req: The request object to parse. Pulled off of the view by default.
128 :param str location: Where on the request to load values.
132 :param tuple locations: Where on the request to search for values.
129133 :param bool as_kwargs: Whether to insert arguments as keyword arguments.
130134 :param callable validate: Validation function that receives the dictionary
131135 of parsed arguments. If the function returns ``False``, the parser
135139 :param dict error_headers: Headers passed to error handler functions when a
136140 a `ValidationError` is raised.
137141 """
138 location = location or self.location
142 locations = locations or self.locations
139143 # Optimization: If argmap is passed as a dictionary, we only need
140144 # to generate a Schema once
141 if isinstance(argmap, Mapping):
142 argmap = core.dict2schema(argmap, schema_class=self.schema_class)()
145 if isinstance(argmap, collections.Mapping):
146 argmap = core.dict2schema(argmap, self.schema_class)()
143147
144148 def decorator(func):
145149 @functools.wraps(func)
153157 parsed_args = self.parse(
154158 argmap,
155159 req=request,
156 location=location,
160 locations=locations,
157161 validate=validate,
158162 error_status_code=error_status_code,
159163 error_headers=error_headers,
160164 )
161 args, kwargs = self._update_args_kwargs(
162 args, kwargs, parsed_args, as_kwargs
163 )
164 return func(obj, *args, **kwargs)
165 if as_kwargs:
166 kwargs.update(parsed_args)
167 return func(obj, *args, **kwargs)
168 else:
169 return func(obj, parsed_args, *args, **kwargs)
165170
166171 wrapper.__wrapped__ = func
167172 return wrapper
0 # -*- coding: utf-8 -*-
01 """Utilities for testing. Includes a base test class
12 for testing parsers.
23
1112 from webargs.core import json
1213
1314
14 class CommonTestCase:
15 class CommonTestCase(object):
1516 """Base test class that defines test methods for common functionality across all
1617 parsers. Subclasses must define `create_app`, which returns a WSGI-like app.
1718 """
3839 def test_parse_querystring_args(self, testapp):
3940 assert testapp.get("/echo?name=Fred").json == {"name": "Fred"}
4041
42 def test_parse_querystring_with_query_location_specified(self, testapp):
43 assert testapp.get("/echo_query?name=Steve").json == {"name": "Steve"}
44
4145 def test_parse_form(self, testapp):
42 assert testapp.post("/echo_form", {"name": "Joe"}).json == {"name": "Joe"}
46 assert testapp.post("/echo", {"name": "Joe"}).json == {"name": "Joe"}
4347
4448 def test_parse_json(self, testapp):
45 assert testapp.post_json("/echo_json", {"name": "Fred"}).json == {
46 "name": "Fred"
47 }
48
49 def test_parse_json_missing(self, testapp):
50 assert testapp.post("/echo_json", "").json == {"name": "World"}
51
52 def test_parse_json_or_form(self, testapp):
53 assert testapp.post_json("/echo_json_or_form", {"name": "Fred"}).json == {
54 "name": "Fred"
55 }
56 assert testapp.post("/echo_json_or_form", {"name": "Joe"}).json == {
57 "name": "Joe"
58 }
59 assert testapp.post("/echo_json_or_form", "").json == {"name": "World"}
49 assert testapp.post_json("/echo", {"name": "Fred"}).json == {"name": "Fred"}
6050
6151 def test_parse_querystring_default(self, testapp):
6252 assert testapp.get("/echo").json == {"name": "World"}
6353
6454 def test_parse_json_default(self, testapp):
65 assert testapp.post_json("/echo_json", {}).json == {"name": "World"}
55 assert testapp.post_json("/echo", {}).json == {"name": "World"}
6656
6757 def test_parse_json_with_charset(self, testapp):
6858 res = testapp.post(
69 "/echo_json",
59 "/echo",
7060 json.dumps({"name": "Steve"}),
7161 content_type="application/json;charset=UTF-8",
7262 )
7464
7565 def test_parse_json_with_vendor_media_type(self, testapp):
7666 res = testapp.post(
77 "/echo_json",
67 "/echo",
7868 json.dumps({"name": "Steve"}),
7969 content_type="application/vnd.api+json;charset=UTF-8",
8070 )
8171 assert res.json == {"name": "Steve"}
8272
83 def test_parse_ignore_extra_data(self, testapp):
84 assert testapp.post_json(
85 "/echo_ignoring_extra_data", {"extra": "data"}
86 ).json == {"name": "World"}
87
88 def test_parse_json_empty(self, testapp):
89 assert testapp.post_json("/echo_json", {}).json == {"name": "World"}
90
91 def test_parse_json_error_unexpected_int(self, testapp):
92 res = testapp.post_json("/echo_json", 1, expect_errors=True)
93 assert res.status_code == 422
94
95 def test_parse_json_error_unexpected_list(self, testapp):
96 res = testapp.post_json("/echo_json", [{"extra": "data"}], expect_errors=True)
97 assert res.status_code == 422
73 def test_parse_json_ignores_extra_data(self, testapp):
74 assert testapp.post_json("/echo", {"extra": "data"}).json == {"name": "World"}
75
76 def test_parse_json_blank(self, testapp):
77 assert testapp.post_json("/echo", None).json == {"name": "World"}
78
79 def test_parse_json_ignore_unexpected_int(self, testapp):
80 assert testapp.post_json("/echo", 1).json == {"name": "World"}
81
82 def test_parse_json_ignore_unexpected_list(self, testapp):
83 assert testapp.post_json("/echo", [{"extra": "data"}]).json == {"name": "World"}
9884
9985 def test_parse_json_many_schema_invalid_input(self, testapp):
10086 res = testapp.post_json(
10692 res = testapp.post_json("/echo_many_schema", [{"name": "Steve"}]).json
10793 assert res == [{"name": "Steve"}]
10894
109 def test_parse_json_many_schema_error_malformed_data(self, testapp):
110 res = testapp.post_json(
111 "/echo_many_schema", {"extra": "data"}, expect_errors=True
112 )
113 assert res.status_code == 422
95 def test_parse_json_many_schema_ignore_malformed_data(self, testapp):
96 assert testapp.post_json("/echo_many_schema", {"extra": "data"}).json == []
11497
11598 def test_parsing_form_default(self, testapp):
116 assert testapp.post("/echo_form", {}).json == {"name": "World"}
99 assert testapp.post("/echo", {}).json == {"name": "World"}
117100
118101 def test_parse_querystring_multiple(self, testapp):
119102 expected = {"name": ["steve", "Loria"]}
120103 assert testapp.get("/echo_multi?name=steve&name=Loria").json == expected
121104
122 # test that passing a single value parses correctly
123 # on parsers like falconparser, where there is no native MultiDict type,
124 # this verifies the usage of MultiDictProxy to ensure that single values
125 # are "listified"
126 def test_parse_querystring_multiple_single_value(self, testapp):
127 expected = {"name": ["steve"]}
128 assert testapp.get("/echo_multi?name=steve").json == expected
129
130105 def test_parse_form_multiple(self, testapp):
131106 expected = {"name": ["steve", "Loria"]}
132107 assert (
133 testapp.post("/echo_multi_form", {"name": ["steve", "Loria"]}).json
134 == expected
108 testapp.post("/echo_multi", {"name": ["steve", "Loria"]}).json == expected
135109 )
136110
137111 def test_parse_json_list(self, testapp):
138112 expected = {"name": ["Steve"]}
139 assert (
140 testapp.post_json("/echo_multi_json", {"name": ["Steve"]}).json == expected
141 )
142
143 def test_parse_json_list_error_malformed_data(self, testapp):
144 res = testapp.post_json(
145 "/echo_multi_json", {"name": "Steve"}, expect_errors=True
146 )
147 assert res.status_code == 422
113 assert testapp.post_json("/echo_multi", {"name": "Steve"}).json == expected
148114
149115 def test_parse_json_with_nonascii_chars(self, testapp):
150 text = "øˆƒ£ºº∆ƒˆ∆"
151 assert testapp.post_json("/echo_json", {"name": text}).json == {"name": text}
116 text = u"øˆƒ£ºº∆ƒˆ∆"
117 assert testapp.post_json("/echo", {"name": text}).json == {"name": text}
152118
153119 # https://github.com/marshmallow-code/webargs/issues/427
154120 def test_parse_json_with_nonutf8_chars(self, testapp):
155121 res = testapp.post(
156 "/echo_json",
122 "/echo",
157123 b"\xfe",
158124 headers={"Accept": "application/json", "Content-Type": "application/json"},
159125 expect_errors=True,
163129 assert res.json == {"json": ["Invalid JSON body."]}
164130
165131 def test_validation_error_returns_422_response(self, testapp):
166 res = testapp.post_json("/echo_json", {"name": "b"}, expect_errors=True)
132 res = testapp.post("/echo", {"name": "b"}, expect_errors=True)
167133 assert res.status_code == 422
168134
169135 def test_user_validation_error_returns_422_response_by_default(self, testapp):
220186 res = testapp.post_json("/echo_nested_many", in_data)
221187 assert res.json == {}
222188
189 def test_parse_json_if_no_json(self, testapp):
190 res = testapp.post("/echo")
191 assert res.json == {"name": "World"}
192
223193 def test_parse_files(self, testapp):
224194 res = testapp.post(
225195 "/echo_file", {"myfile": webtest.Upload("README.rst", b"data")}
228198
229199 # https://github.com/sloria/webargs/pull/297
230200 def test_empty_json(self, testapp):
231 res = testapp.post("/echo_json")
201 res = testapp.post(
202 "/echo",
203 "",
204 headers={"Accept": "application/json", "Content-Type": "application/json"},
205 )
232206 assert res.status_code == 200
233207 assert res.json == {"name": "World"}
234208
235 # https://github.com/sloria/webargs/pull/297
236 def test_empty_json_with_headers(self, testapp):
237 res = testapp.post(
238 "/echo_json",
239 "",
240 headers={"Accept": "application/json", "Content-Type": "application/json"},
241 )
242 assert res.status_code == 200
243 assert res.json == {"name": "World"}
244
245209 # https://github.com/sloria/webargs/issues/329
246210 def test_invalid_json(self, testapp):
247211 res = testapp.post(
248 "/echo_json",
212 "/echo",
249213 '{"foo": "bar", }',
250214 headers={"Accept": "application/json", "Content-Type": "application/json"},
251215 expect_errors=True,
0 # -*- coding: utf-8 -*-
01 """Tornado request argument parsing module.
12
23 Example: ::
1314 self.write(response)
1415 """
1516 import tornado.web
16 import tornado.concurrent
1717 from tornado.escape import _unicode
1818
1919 from webargs import core
20 from webargs.multidictproxy import MultiDictProxy
20 from webargs.compat import basestring
21 from webargs.core import json
2122
2223
2324 class HTTPError(tornado.web.HTTPError):
2627 def __init__(self, *args, **kwargs):
2728 self.messages = kwargs.pop("messages", {})
2829 self.headers = kwargs.pop("headers", None)
29 super().__init__(*args, **kwargs)
30 super(HTTPError, self).__init__(*args, **kwargs)
3031
3132
32 def is_json_request(req):
33 def parse_json_body(req):
34 """Return the decoded JSON body from the request."""
3335 content_type = req.headers.get("Content-Type")
34 return content_type is not None and core.is_json(content_type)
36 if content_type and core.is_json(content_type):
37 try:
38 return core.parse_json(req.body)
39 except TypeError:
40 pass
41 except json.JSONDecodeError as e:
42 if e.doc == "":
43 return core.missing
44 else:
45 raise
46 return {}
3547
3648
37 class WebArgsTornadoMultiDictProxy(MultiDictProxy):
49 # From tornado.web.RequestHandler.decode_argument
50 def decode_argument(value, name=None):
51 """Decodes an argument from the request.
3852 """
39 Override class for Tornado multidicts, handles argument decoding
40 requirements.
41 """
42
43 def __getitem__(self, key):
44 try:
45 value = self.data.get(key, core.missing)
46 if value is core.missing:
47 return core.missing
48 if key in self.multiple_keys:
49 return [
50 _unicode(v) if isinstance(v, (str, bytes)) else v for v in value
51 ]
52 if value and isinstance(value, (list, tuple)):
53 value = value[0]
54
55 if isinstance(value, (str, bytes)):
56 return _unicode(value)
57 return value
58 # based on tornado.web.RequestHandler.decode_argument
59 except UnicodeDecodeError:
60 raise HTTPError(400, "Invalid unicode in {}: {!r}".format(key, value[:40]))
53 try:
54 return _unicode(value)
55 except UnicodeDecodeError:
56 raise HTTPError(400, "Invalid unicode in %s: %r" % (name or "url", value[:40]))
6157
6258
63 class WebArgsTornadoCookiesMultiDictProxy(MultiDictProxy):
59 def get_value(d, name, field):
60 """Handle gets from 'multidicts' made of lists
61
62 It handles cases: ``{"key": [value]}`` and ``{"key": value}``
6463 """
65 And a special override for cookies because they come back as objects with a
66 `value` attribute we need to extract.
67 Also, does not use the `_unicode` decoding step
68 """
69
70 def __getitem__(self, key):
71 cookie = self.data.get(key, core.missing)
72 if cookie is core.missing:
73 return core.missing
74 if key in self.multiple_keys:
75 return [cookie.value]
76 return cookie.value
64 multiple = core.is_multiple(field)
65 value = d.get(name, core.missing)
66 if value is core.missing:
67 return core.missing
68 if multiple and value is not core.missing:
69 return [
70 decode_argument(v, name) if isinstance(v, basestring) else v for v in value
71 ]
72 ret = value
73 if value and isinstance(value, (list, tuple)):
74 ret = value[0]
75 if isinstance(ret, basestring):
76 return decode_argument(ret, name)
77 else:
78 return ret
7779
7880
7981 class TornadoParser(core.Parser):
8082 """Tornado request argument parser."""
8183
82 def _raw_load_json(self, req):
83 """Return a json payload from the request for the core parser's load_json
84 def parse_json(self, req, name, field):
85 """Pull a json value from the request."""
86 json_data = self._cache.get("json")
87 if json_data is None:
88 try:
89 self._cache["json"] = json_data = parse_json_body(req)
90 except json.JSONDecodeError as e:
91 return self.handle_invalid_json_error(e, req)
92 if json_data is None:
93 return core.missing
94 return core.get_value(json_data, name, field, allow_many_nested=True)
8495
85 Checks the input mimetype and may return 'missing' if the mimetype is
86 non-json, even if the request body is parseable as json."""
87 if not is_json_request(req):
88 return core.missing
96 def parse_querystring(self, req, name, field):
97 """Pull a querystring value from the request."""
98 return get_value(req.query_arguments, name, field)
8999
90 # request.body may be a concurrent.Future on streaming requests
91 # this would cause a TypeError if we try to parse it
92 if isinstance(req.body, tornado.concurrent.Future):
93 return core.missing
100 def parse_form(self, req, name, field):
101 """Pull a form value from the request."""
102 return get_value(req.body_arguments, name, field)
94103
95 return core.parse_json(req.body)
104 def parse_headers(self, req, name, field):
105 """Pull a value from the header data."""
106 return get_value(req.headers, name, field)
96107
97 def load_querystring(self, req, schema):
98 """Return query params from the request as a MultiDictProxy."""
99 return WebArgsTornadoMultiDictProxy(req.query_arguments, schema)
108 def parse_cookies(self, req, name, field):
109 """Pull a value from the header data."""
110 cookie = req.cookies.get(name)
100111
101 def load_form(self, req, schema):
102 """Return form values from the request as a MultiDictProxy."""
103 return WebArgsTornadoMultiDictProxy(req.body_arguments, schema)
112 if cookie is not None:
113 return [cookie.value] if core.is_multiple(field) else cookie.value
114 else:
115 return [] if core.is_multiple(field) else None
104116
105 def load_headers(self, req, schema):
106 """Return headers from the request as a MultiDictProxy."""
107 return WebArgsTornadoMultiDictProxy(req.headers, schema)
117 def parse_files(self, req, name, field):
118 """Pull a file from the request."""
119 return get_value(req.files, name, field)
108120
109 def load_cookies(self, req, schema):
110 """Return cookies from the request as a MultiDictProxy."""
111 # use the specialized subclass specifically for handling Tornado
112 # cookies
113 return WebArgsTornadoCookiesMultiDictProxy(req.cookies, schema)
114
115 def load_files(self, req, schema):
116 """Return files from the request as a MultiDictProxy."""
117 return WebArgsTornadoMultiDictProxy(req.files, schema)
118
119 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
121 def handle_error(self, error, req, schema, error_status_code, error_headers):
120122 """Handles errors during parsing. Raises a `tornado.web.HTTPError`
121123 with a 400 error.
122124 """
133135 headers=error_headers,
134136 )
135137
136 def _handle_invalid_json_error(self, error, req, *args, **kwargs):
138 def handle_invalid_json_error(self, error, req, *args, **kwargs):
137139 raise HTTPError(
138140 400,
139141 log_message="Invalid JSON body.",
0 # -*- coding: utf-8 -*-
01 """Webapp2 request argument parsing module.
12
23 Example: ::
2930 import webob.multidict
3031
3132 from webargs import core
32 from webargs.multidictproxy import MultiDictProxy
33 from webargs.core import json
3334
3435
3536 class Webapp2Parser(core.Parser):
3637 """webapp2 request argument parser."""
3738
38 def _raw_load_json(self, req):
39 """Return a json payload from the request for the core parser's load_json."""
40 if not core.is_json(req.content_type):
41 return core.missing
42 return core.parse_json(req.body)
39 def parse_json(self, req, name, field):
40 """Pull a json value from the request."""
41 json_data = self._cache.get("json")
42 if json_data is None:
43 if not core.is_json(req.content_type):
44 return core.missing
4345
44 def load_querystring(self, req, schema):
45 """Return query params from the request as a MultiDictProxy."""
46 return MultiDictProxy(req.GET, schema)
46 try:
47 self._cache["json"] = json_data = core.parse_json(req.body)
48 except json.JSONDecodeError as e:
49 if e.doc == "":
50 return core.missing
51 else:
52 raise
53 return core.get_value(json_data, name, field, allow_many_nested=True)
4754
48 def load_form(self, req, schema):
49 """Return form values from the request as a MultiDictProxy."""
50 return MultiDictProxy(req.POST, schema)
55 def parse_querystring(self, req, name, field):
56 """Pull a querystring value from the request."""
57 return core.get_value(req.GET, name, field)
5158
52 def load_cookies(self, req, schema):
53 """Return cookies from the request as a MultiDictProxy."""
54 return MultiDictProxy(req.cookies, schema)
59 def parse_form(self, req, name, field):
60 """Pull a form value from the request."""
61 return core.get_value(req.POST, name, field)
5562
56 def load_headers(self, req, schema):
57 """Return headers from the request as a MultiDictProxy."""
58 return MultiDictProxy(req.headers, schema)
63 def parse_cookies(self, req, name, field):
64 """Pull the value from the cookiejar."""
65 return core.get_value(req.cookies, name, field)
5966
60 def load_files(self, req, schema):
61 """Return files from the request as a MultiDictProxy."""
67 def parse_headers(self, req, name, field):
68 """Pull a value from the header data."""
69 return core.get_value(req.headers, name, field)
70
71 def parse_files(self, req, name, field):
72 """Pull a file from the request."""
6273 files = ((k, v) for k, v in req.POST.items() if hasattr(v, "file"))
63 return MultiDictProxy(webob.multidict.MultiDict(files), schema)
74 return core.get_value(webob.multidict.MultiDict(files), name, field)
6475
6576 def get_default_request(self):
6677 return webapp2.get_request()
0 # -*- coding: utf-8 -*-
11
22 import aiohttp
33 from aiohttp.web import json_response
4 from aiohttp import web
45 import marshmallow as ma
56
67 from webargs import fields
2324 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
2425 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
2526
26 # variant which ignores unknown fields
27 exclude_kwargs = (
28 {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE}
29 )
30 hello_exclude_schema = HelloSchema(**exclude_kwargs)
31
32
3327 ##### Handlers #####
3428
3529
3630 async def echo(request):
37 parsed = await parser.parse(hello_args, request, location="query")
38 return json_response(parsed)
39
40
41 async def echo_form(request):
42 parsed = await parser.parse(hello_args, request, location="form")
43 return json_response(parsed)
44
45
46 async def echo_json(request):
4731 try:
48 parsed = await parser.parse(hello_args, request, location="json")
32 parsed = await parser.parse(hello_args, request)
4933 except json.JSONDecodeError:
50 raise aiohttp.web.HTTPBadRequest(
34 raise web.HTTPBadRequest(
5135 body=json.dumps(["Invalid JSON."]).encode("utf-8"),
5236 content_type="application/json",
5337 )
5438 return json_response(parsed)
5539
5640
57 async def echo_json_or_form(request):
58 try:
59 parsed = await parser.parse(hello_args, request, location="json_or_form")
60 except json.JSONDecodeError:
61 raise aiohttp.web.HTTPBadRequest(
62 body=json.dumps(["Invalid JSON."]).encode("utf-8"),
63 content_type="application/json",
64 )
65 return json_response(parsed)
66
67
68 @use_args(hello_args, location="query")
41 async def echo_query(request):
42 parsed = await parser.parse(hello_args, request, locations=("query",))
43 return json_response(parsed)
44
45
46 async def echo_json(request):
47 parsed = await parser.parse(hello_args, request, locations=("json",))
48 return json_response(parsed)
49
50
51 async def echo_form(request):
52 parsed = await parser.parse(hello_args, request, locations=("form",))
53 return json_response(parsed)
54
55
56 @use_args(hello_args)
6957 async def echo_use_args(request, args):
7058 return json_response(args)
7159
7260
73 @use_kwargs(hello_args, location="query")
61 @use_kwargs(hello_args)
7462 async def echo_use_kwargs(request, name):
7563 return json_response({"name": name})
7664
7765
78 @use_args(
79 {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form"
80 )
66 @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42)
8167 async def echo_use_args_validated(request, args):
8268 return json_response(args)
8369
8470
85 async def echo_ignoring_extra_data(request):
86 return json_response(await parser.parse(hello_exclude_schema, request))
87
88
8971 async def echo_multi(request):
90 parsed = await parser.parse(hello_multiple, request, location="query")
91 return json_response(parsed)
92
93
94 async def echo_multi_form(request):
95 parsed = await parser.parse(hello_multiple, request, location="form")
96 return json_response(parsed)
97
98
99 async def echo_multi_json(request):
10072 parsed = await parser.parse(hello_multiple, request)
10173 return json_response(parsed)
10274
10375
10476 async def echo_many_schema(request):
105 parsed = await parser.parse(hello_many_schema, request)
106 return json_response(parsed)
107
108
109 @use_args({"value": fields.Int()}, location="query")
77 parsed = await parser.parse(hello_many_schema, request, locations=("json",))
78 return json_response(parsed)
79
80
81 @use_args({"value": fields.Int()})
11082 async def echo_use_args_with_path_param(request, args):
11183 return json_response(args)
11284
11385
114 @use_kwargs({"value": fields.Int()}, location="query")
86 @use_kwargs({"value": fields.Int()})
11587 async def echo_use_kwargs_with_path_param(request, value):
11688 return json_response({"value": value})
11789
11890
119 @use_args({"page": fields.Int(), "q": fields.Int()}, location="query")
120 @use_args({"name": fields.Str()})
91 @use_args({"page": fields.Int(), "q": fields.Int()}, locations=("query",))
92 @use_args({"name": fields.Str()}, locations=("json",))
12193 async def echo_use_args_multiple(request, query_parsed, json_parsed):
12294 return json_response({"query_parsed": query_parsed, "json_parsed": json_parsed})
12395
132104
133105
134106 async def echo_headers(request):
135 parsed = await parser.parse(hello_exclude_schema, request, location="headers")
107 parsed = await parser.parse(hello_args, request, locations=("headers",))
136108 return json_response(parsed)
137109
138110
139111 async def echo_cookie(request):
140 parsed = await parser.parse(hello_args, request, location="cookies")
112 parsed = await parser.parse(hello_args, request, locations=("cookies",))
141113 return json_response(parsed)
142114
143115
171143
172144
173145 async def echo_match_info(request):
174 parsed = await parser.parse(
175 {"mymatch": fields.Int()}, request, location="match_info"
176 )
146 parsed = await parser.parse({"mymatch": fields.Int(location="match_info")}, request)
177147 return json_response(parsed)
178148
179149
180150 class EchoHandler:
181 @use_args(hello_args, location="query")
151 @use_args(hello_args)
182152 async def get(self, request, args):
183153 return json_response(args)
184154
185155
186 class EchoHandlerView(aiohttp.web.View):
156 class EchoHandlerView(web.View):
187157 @asyncio.coroutine
188 @use_args(hello_args, location="query")
158 @use_args(hello_args)
189159 def get(self, args):
190160 return json_response(args)
191161
192162
193163 @asyncio.coroutine
194 @use_args(HelloSchema, as_kwargs=True, location="query")
164 @use_args(HelloSchema, as_kwargs=True)
195165 def echo_use_schema_as_kwargs(request, name):
196166 return json_response({"name": name})
197167
207177 def create_app():
208178 app = aiohttp.web.Application()
209179
210 add_route(app, ["GET"], "/echo", echo)
180 add_route(app, ["GET", "POST"], "/echo", echo)
181 add_route(app, ["GET"], "/echo_query", echo_query)
182 add_route(app, ["POST"], "/echo_json", echo_json)
211183 add_route(app, ["POST"], "/echo_form", echo_form)
212 add_route(app, ["POST"], "/echo_json", echo_json)
213 add_route(app, ["POST"], "/echo_json_or_form", echo_json_or_form)
214 add_route(app, ["GET"], "/echo_use_args", echo_use_args)
215 add_route(app, ["GET"], "/echo_use_kwargs", echo_use_kwargs)
216 add_route(app, ["POST"], "/echo_use_args_validated", echo_use_args_validated)
217 add_route(app, ["POST"], "/echo_ignoring_extra_data", echo_ignoring_extra_data)
218 add_route(app, ["GET"], "/echo_multi", echo_multi)
219 add_route(app, ["POST"], "/echo_multi_form", echo_multi_form)
220 add_route(app, ["POST"], "/echo_multi_json", echo_multi_json)
184 add_route(app, ["GET", "POST"], "/echo_use_args", echo_use_args)
185 add_route(app, ["GET", "POST"], "/echo_use_kwargs", echo_use_kwargs)
186 add_route(app, ["GET", "POST"], "/echo_use_args_validated", echo_use_args_validated)
187 add_route(app, ["GET", "POST"], "/echo_multi", echo_multi)
221188 add_route(app, ["GET", "POST"], "/echo_many_schema", echo_many_schema)
222189 add_route(
223190 app,
0 from webargs.core import json
01 from bottle import Bottle, HTTPResponse, debug, request, response
12
23 import marshmallow as ma
34 from webargs import fields
45 from webargs.bottleparser import parser, use_args, use_kwargs
5 from webargs.core import json, MARSHMALLOW_VERSION_INFO
6
6 from webargs.core import MARSHMALLOW_VERSION_INFO
77
88 hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)}
99 hello_multiple = {"name": fields.List(fields.Str())}
1616 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
1717 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
1818
19 # variant which ignores unknown fields
20 exclude_kwargs = (
21 {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE}
22 )
23 hello_exclude_schema = HelloSchema(**exclude_kwargs)
24
2519
2620 app = Bottle()
2721 debug(True)
2822
2923
30 @app.route("/echo", method=["GET"])
24 @app.route("/echo", method=["GET", "POST"])
3125 def echo():
32 return parser.parse(hello_args, request, location="query")
26 return parser.parse(hello_args, request)
27
28
29 @app.route("/echo_query")
30 def echo_query():
31 return parser.parse(hello_args, request, locations=("query",))
32
33
34 @app.route("/echo_json", method=["POST"])
35 def echo_json():
36 return parser.parse(hello_args, request, locations=("json",))
3337
3438
3539 @app.route("/echo_form", method=["POST"])
3640 def echo_form():
37 return parser.parse(hello_args, location="form")
41 return parser.parse(hello_args, request, locations=("form",))
3842
3943
40 @app.route("/echo_json", method=["POST"])
41 def echo_json():
42 return parser.parse(hello_args, location="json")
43
44
45 @app.route("/echo_json_or_form", method=["POST"])
46 def echo_json_or_form():
47 return parser.parse(hello_args, location="json_or_form")
48
49
50 @app.route("/echo_use_args", method=["GET"])
51 @use_args(hello_args, location="query")
44 @app.route("/echo_use_args", method=["GET", "POST"])
45 @use_args(hello_args)
5246 def echo_use_args(args):
5347 return args
5448
5549
50 @app.route("/echo_use_kwargs", method=["GET", "POST"], apply=use_kwargs(hello_args))
51 def echo_use_kwargs(name):
52 return {"name": name}
53
54
5655 @app.route(
5756 "/echo_use_args_validated",
58 method=["POST"],
59 apply=use_args(
60 {"value": fields.Int()},
61 validate=lambda args: args["value"] > 42,
62 location="form",
63 ),
57 method=["GET", "POST"],
58 apply=use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42),
6459 )
6560 def echo_use_args_validated(args):
6661 return args
6762
6863
69 @app.route("/echo_ignoring_extra_data", method=["POST"])
70 def echo_json_ignore_extra_data():
71 return parser.parse(hello_exclude_schema)
64 @app.route("/echo_multi", method=["GET", "POST"])
65 def echo_multi():
66 return parser.parse(hello_multiple, request)
7267
7368
74 @app.route(
75 "/echo_use_kwargs", method=["GET"], apply=use_kwargs(hello_args, location="query")
76 )
77 def echo_use_kwargs(name):
78 return {"name": name}
79
80
81 @app.route("/echo_multi", method=["GET"])
82 def echo_multi():
83 return parser.parse(hello_multiple, request, location="query")
84
85
86 @app.route("/echo_multi_form", method=["POST"])
87 def multi_form():
88 return parser.parse(hello_multiple, location="form")
89
90
91 @app.route("/echo_multi_json", method=["POST"])
92 def multi_json():
93 return parser.parse(hello_multiple)
94
95
96 @app.route("/echo_many_schema", method=["POST"])
69 @app.route("/echo_many_schema", method=["GET", "POST"])
9770 def echo_many_schema():
98 arguments = parser.parse(hello_many_schema, request)
71 arguments = parser.parse(hello_many_schema, request, locations=("json",))
9972 return HTTPResponse(body=json.dumps(arguments), content_type="application/json")
10073
10174
10275 @app.route(
103 "/echo_use_args_with_path_param/<name>",
104 apply=use_args({"value": fields.Int()}, location="query"),
76 "/echo_use_args_with_path_param/<name>", apply=use_args({"value": fields.Int()})
10577 )
10678 def echo_use_args_with_path_param(args, name):
10779 return args
10880
10981
11082 @app.route(
111 "/echo_use_kwargs_with_path_param/<name>",
112 apply=use_kwargs({"value": fields.Int()}, location="query"),
83 "/echo_use_kwargs_with_path_param/<name>", apply=use_kwargs({"value": fields.Int()})
11384 )
11485 def echo_use_kwargs_with_path_param(name, value):
11586 return {"value": value}
12697
12798 @app.route("/echo_headers")
12899 def echo_headers():
129 # the "exclude schema" must be used in this case because WSGI headers may
130 # be populated with many fields not sent by the caller
131 return parser.parse(hello_exclude_schema, request, location="headers")
100 return parser.parse(hello_args, request, locations=("headers",))
132101
133102
134103 @app.route("/echo_cookie")
135104 def echo_cookie():
136 return parser.parse(hello_args, request, location="cookies")
105 return parser.parse(hello_args, request, locations=("cookies",))
137106
138107
139108 @app.route("/echo_file", method=["POST"])
140109 def echo_file():
141110 args = {"myfile": fields.Field()}
142 result = parser.parse(args, location="files")
111 result = parser.parse(args, locations=("files",))
143112 myfile = result["myfile"]
144113 content = myfile.file.read().decode("utf8")
145114 return {"myfile": content}
66
77 TEMPLATE_DEBUG = True
88
9 ALLOWED_HOSTS = ["*"]
9 ALLOWED_HOSTS = []
1010 # Application definition
1111
1212 INSTALLED_APPS = ("django.contrib.contenttypes",)
11
22 from tests.apps.django_app.echo import views
33
4
54 urlpatterns = [
65 url(r"^echo$", views.echo),
6 url(r"^echo_query$", views.echo_query),
7 url(r"^echo_json$", views.echo_json),
78 url(r"^echo_form$", views.echo_form),
8 url(r"^echo_json$", views.echo_json),
9 url(r"^echo_json_or_form$", views.echo_json_or_form),
109 url(r"^echo_use_args$", views.echo_use_args),
11 url(r"^echo_use_args_validated$", views.echo_use_args_validated),
12 url(r"^echo_ignoring_extra_data$", views.echo_ignoring_extra_data),
1310 url(r"^echo_use_kwargs$", views.echo_use_kwargs),
1411 url(r"^echo_multi$", views.echo_multi),
15 url(r"^echo_multi_form$", views.echo_multi_form),
16 url(r"^echo_multi_json$", views.echo_multi_json),
1712 url(r"^echo_many_schema$", views.echo_many_schema),
1813 url(
1914 r"^echo_use_args_with_path_param/(?P<name>\w+)$",
0 from webargs.core import json
01 from django.http import HttpResponse
12 from django.views.generic import View
3
24 import marshmallow as ma
3
45 from webargs import fields
56 from webargs.djangoparser import parser, use_args, use_kwargs
6 from webargs.core import json, MARSHMALLOW_VERSION_INFO
7
7 from webargs.core import MARSHMALLOW_VERSION_INFO
88
99 hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)}
1010 hello_multiple = {"name": fields.List(fields.Str())}
1717 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
1818 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
1919
20 # variant which ignores unknown fields
21 exclude_kwargs = (
22 {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE}
23 )
24 hello_exclude_schema = HelloSchema(**exclude_kwargs)
25
2620
2721 def json_response(data, **kwargs):
2822 return HttpResponse(json.dumps(data), content_type="application/json", **kwargs)
2923
3024
31 def handle_view_errors(f):
32 def wrapped(*args, **kwargs):
33 try:
34 return f(*args, **kwargs)
35 except ma.ValidationError as err:
36 return json_response(err.messages, status=422)
37 except json.JSONDecodeError:
38 return json_response({"json": ["Invalid JSON body."]}, status=400)
39
40 return wrapped
25 def echo(request):
26 try:
27 args = parser.parse(hello_args, request)
28 except ma.ValidationError as err:
29 return json_response(err.messages, status=parser.DEFAULT_VALIDATION_STATUS)
30 except json.JSONDecodeError:
31 return json_response({"json": ["Invalid JSON body."]}, status=400)
32 return json_response(args)
4133
4234
43 @handle_view_errors
44 def echo(request):
45 return json_response(parser.parse(hello_args, request, location="query"))
35 def echo_query(request):
36 return json_response(parser.parse(hello_args, request, locations=("query",)))
4637
4738
48 @handle_view_errors
49 def echo_form(request):
50 return json_response(parser.parse(hello_args, request, location="form"))
39 def echo_json(request):
40 return json_response(parser.parse(hello_args, request, locations=("json",)))
5141
5242
53 @handle_view_errors
54 def echo_json(request):
55 return json_response(parser.parse(hello_args, request, location="json"))
43 def echo_form(request):
44 return json_response(parser.parse(hello_args, request, locations=("form",)))
5645
5746
58 @handle_view_errors
59 def echo_json_or_form(request):
60 return json_response(parser.parse(hello_args, request, location="json_or_form"))
61
62
63 @handle_view_errors
64 @use_args(hello_args, location="query")
47 @use_args(hello_args)
6548 def echo_use_args(request, args):
6649 return json_response(args)
6750
6851
69 @handle_view_errors
70 @use_args(
71 {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form"
72 )
73 def echo_use_args_validated(args):
74 return json_response(args)
75
76
77 @handle_view_errors
78 def echo_ignoring_extra_data(request):
79 return json_response(parser.parse(hello_exclude_schema, request))
80
81
82 @handle_view_errors
83 @use_kwargs(hello_args, location="query")
52 @use_kwargs(hello_args)
8453 def echo_use_kwargs(request, name):
8554 return json_response({"name": name})
8655
8756
88 @handle_view_errors
8957 def echo_multi(request):
90 return json_response(parser.parse(hello_multiple, request, location="query"))
91
92
93 @handle_view_errors
94 def echo_multi_form(request):
95 return json_response(parser.parse(hello_multiple, request, location="form"))
96
97
98 @handle_view_errors
99 def echo_multi_json(request):
10058 return json_response(parser.parse(hello_multiple, request))
10159
10260
103 @handle_view_errors
10461 def echo_many_schema(request):
105 return json_response(parser.parse(hello_many_schema, request))
62 try:
63 return json_response(
64 parser.parse(hello_many_schema, request, locations=("json",))
65 )
66 except ma.ValidationError as err:
67 return json_response(err.messages, status=parser.DEFAULT_VALIDATION_STATUS)
10668
10769
108 @handle_view_errors
109 @use_args({"value": fields.Int()}, location="query")
70 @use_args({"value": fields.Int()})
11071 def echo_use_args_with_path_param(request, args, name):
11172 return json_response(args)
11273
11374
114 @handle_view_errors
115 @use_kwargs({"value": fields.Int()}, location="query")
75 @use_kwargs({"value": fields.Int()})
11676 def echo_use_kwargs_with_path_param(request, value, name):
11777 return json_response({"value": value})
11878
11979
120 @handle_view_errors
12180 def always_error(request):
12281 def always_fail(value):
12382 raise ma.ValidationError("something went wrong")
12483
12584 argmap = {"text": fields.Str(validate=always_fail)}
126 return parser.parse(argmap, request)
85 try:
86 return parser.parse(argmap, request)
87 except ma.ValidationError as err:
88 return json_response(err.messages, status=parser.DEFAULT_VALIDATION_STATUS)
12789
12890
129 @handle_view_errors
13091 def echo_headers(request):
131 return json_response(
132 parser.parse(hello_exclude_schema, request, location="headers")
133 )
92 return json_response(parser.parse(hello_args, request, locations=("headers",)))
13493
13594
136 @handle_view_errors
13795 def echo_cookie(request):
138 return json_response(parser.parse(hello_args, request, location="cookies"))
96 return json_response(parser.parse(hello_args, request, locations=("cookies",)))
13997
14098
141 @handle_view_errors
14299 def echo_file(request):
143100 args = {"myfile": fields.Field()}
144 result = parser.parse(args, request, location="files")
101 result = parser.parse(args, request, locations=("files",))
145102 myfile = result["myfile"]
146103 content = myfile.read().decode("utf8")
147104 return json_response({"myfile": content})
148105
149106
150 @handle_view_errors
151107 def echo_nested(request):
152108 argmap = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})}
153109 return json_response(parser.parse(argmap, request))
154110
155111
156 @handle_view_errors
157112 def echo_nested_many(request):
158113 argmap = {
159114 "users": fields.Nested({"id": fields.Int(), "name": fields.Str()}, many=True)
162117
163118
164119 class EchoCBV(View):
165 @handle_view_errors
166120 def get(self, request):
167 location_kwarg = {} if request.method == "POST" else {"location": "query"}
168 return json_response(parser.parse(hello_args, self.request, **location_kwarg))
121 try:
122 args = parser.parse(hello_args, self.request)
123 except ma.ValidationError as err:
124 return json_response(err.messages, status=parser.DEFAULT_VALIDATION_STATUS)
125 return json_response(args)
169126
170127 post = get
171128
172129
173130 class EchoUseArgsCBV(View):
174 @handle_view_errors
175 @use_args(hello_args, location="query")
131 @use_args(hello_args)
176132 def get(self, request, args):
177133 return json_response(args)
178134
179 @handle_view_errors
180 @use_args(hello_args)
181 def post(self, request, args):
182 return json_response(args)
135 post = get
183136
184137
185138 class EchoUseArgsWithParamCBV(View):
186 @handle_view_errors
187 @use_args(hello_args, location="query")
139 @use_args(hello_args)
188140 def get(self, request, args, pid):
189141 return json_response(args)
190142
191 @handle_view_errors
192 @use_args(hello_args)
193 def post(self, request, args, pid):
194 return json_response(args)
143 post = get
0 from webargs.core import json
1
02 import falcon
13 import marshmallow as ma
2
34 from webargs import fields
4 from webargs.core import MARSHMALLOW_VERSION_INFO, json
55 from webargs.falconparser import parser, use_args, use_kwargs
6 from webargs.core import MARSHMALLOW_VERSION_INFO
67
78 hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)}
89 hello_multiple = {"name": fields.List(fields.Str())}
1516 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
1617 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
1718
18 # variant which ignores unknown fields
19 exclude_kwargs = (
20 {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE}
21 )
22 hello_exclude_schema = HelloSchema(**exclude_kwargs)
19
20 class Echo(object):
21 def on_get(self, req, resp):
22 try:
23 parsed = parser.parse(hello_args, req)
24 except json.JSONDecodeError:
25 resp.body = json.dumps(["Invalid JSON."])
26 resp.status = falcon.HTTP_400
27 else:
28 resp.body = json.dumps(parsed)
29
30 on_post = on_get
2331
2432
25 class Echo:
33 class EchoQuery(object):
2634 def on_get(self, req, resp):
27 parsed = parser.parse(hello_args, req, location="query")
35 parsed = parser.parse(hello_args, req, locations=("query",))
2836 resp.body = json.dumps(parsed)
2937
3038
31 class EchoForm:
39 class EchoJSON(object):
3240 def on_post(self, req, resp):
33 parsed = parser.parse(hello_args, req, location="form")
41 parsed = parser.parse(hello_args, req, locations=("json",))
3442 resp.body = json.dumps(parsed)
3543
3644
37 class EchoJSON:
45 class EchoForm(object):
3846 def on_post(self, req, resp):
39 parsed = parser.parse(hello_args, req, location="json")
47 parsed = parser.parse(hello_args, req, locations=("form",))
4048 resp.body = json.dumps(parsed)
4149
4250
43 class EchoJSONOrForm:
44 def on_post(self, req, resp):
45 parsed = parser.parse(hello_args, req, location="json_or_form")
46 resp.body = json.dumps(parsed)
47
48
49 class EchoUseArgs:
50 @use_args(hello_args, location="query")
51 class EchoUseArgs(object):
52 @use_args(hello_args)
5153 def on_get(self, req, resp, args):
5254 resp.body = json.dumps(args)
5355
56 on_post = on_get
5457
55 class EchoUseKwargs:
56 @use_kwargs(hello_args, location="query")
58
59 class EchoUseKwargs(object):
60 @use_kwargs(hello_args)
5761 def on_get(self, req, resp, name):
5862 resp.body = json.dumps({"name": name})
5963
64 on_post = on_get
6065
61 class EchoUseArgsValidated:
62 @use_args(
63 {"value": fields.Int()},
64 validate=lambda args: args["value"] > 42,
65 location="form",
66 )
67 def on_post(self, req, resp, args):
66
67 class EchoUseArgsValidated(object):
68 @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42)
69 def on_get(self, req, resp, args):
6870 resp.body = json.dumps(args)
6971
70
71 class EchoJSONIgnoreExtraData:
72 def on_post(self, req, resp):
73 resp.body = json.dumps(parser.parse(hello_exclude_schema, req))
72 on_post = on_get
7473
7574
76 class EchoMulti:
75 class EchoMulti(object):
7776 def on_get(self, req, resp):
78 resp.body = json.dumps(parser.parse(hello_multiple, req, location="query"))
77 resp.body = json.dumps(parser.parse(hello_multiple, req))
78
79 on_post = on_get
7980
8081
81 class EchoMultiForm:
82 def on_post(self, req, resp):
83 resp.body = json.dumps(parser.parse(hello_multiple, req, location="form"))
82 class EchoManySchema(object):
83 def on_get(self, req, resp):
84 resp.body = json.dumps(
85 parser.parse(hello_many_schema, req, locations=("json",))
86 )
87
88 on_post = on_get
8489
8590
86 class EchoMultiJSON:
87 def on_post(self, req, resp):
88 resp.body = json.dumps(parser.parse(hello_multiple, req))
89
90
91 class EchoManySchema:
92 def on_post(self, req, resp):
93 resp.body = json.dumps(parser.parse(hello_many_schema, req))
94
95
96 class EchoUseArgsWithPathParam:
97 @use_args({"value": fields.Int()}, location="query")
91 class EchoUseArgsWithPathParam(object):
92 @use_args({"value": fields.Int()})
9893 def on_get(self, req, resp, args, name):
9994 resp.body = json.dumps(args)
10095
10196
102 class EchoUseKwargsWithPathParam:
103 @use_kwargs({"value": fields.Int()}, location="query")
97 class EchoUseKwargsWithPathParam(object):
98 @use_kwargs({"value": fields.Int()})
10499 def on_get(self, req, resp, value, name):
105100 resp.body = json.dumps({"value": value})
106101
107102
108 class AlwaysError:
103 class AlwaysError(object):
109104 def on_get(self, req, resp):
110105 def always_fail(value):
111106 raise ma.ValidationError("something went wrong")
116111 on_post = on_get
117112
118113
119 class EchoHeaders:
114 class EchoHeaders(object):
120115 def on_get(self, req, resp):
121 class HeaderSchema(ma.Schema):
122 NAME = fields.Str(missing="World")
123
124 resp.body = json.dumps(
125 parser.parse(HeaderSchema(**exclude_kwargs), req, location="headers")
126 )
116 resp.body = json.dumps(parser.parse(hello_args, req, locations=("headers",)))
127117
128118
129 class EchoCookie:
119 class EchoCookie(object):
130120 def on_get(self, req, resp):
131 resp.body = json.dumps(parser.parse(hello_args, req, location="cookies"))
121 resp.body = json.dumps(parser.parse(hello_args, req, locations=("cookies",)))
132122
133123
134 class EchoNested:
124 class EchoNested(object):
135125 def on_post(self, req, resp):
136126 args = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})}
137127 resp.body = json.dumps(parser.parse(args, req))
138128
139129
140 class EchoNestedMany:
130 class EchoNestedMany(object):
141131 def on_post(self, req, resp):
142132 args = {
143133 "users": fields.Nested(
148138
149139
150140 def use_args_hook(args, context_key="args", **kwargs):
151 def hook(req, resp, resource, params):
141 def hook(req, resp, params):
152142 parsed_args = parser.parse(args, req=req, **kwargs)
153143 req.context[context_key] = parsed_args
154144
155145 return hook
156146
157147
158 @falcon.before(use_args_hook(hello_args, location="query"))
159 class EchoUseArgsHook:
148 @falcon.before(use_args_hook(hello_args))
149 class EchoUseArgsHook(object):
160150 def on_get(self, req, resp):
161151 resp.body = json.dumps(req.context["args"])
162152
164154 def create_app():
165155 app = falcon.API()
166156 app.add_route("/echo", Echo())
157 app.add_route("/echo_query", EchoQuery())
158 app.add_route("/echo_json", EchoJSON())
167159 app.add_route("/echo_form", EchoForm())
168 app.add_route("/echo_json", EchoJSON())
169 app.add_route("/echo_json_or_form", EchoJSONOrForm())
170160 app.add_route("/echo_use_args", EchoUseArgs())
171161 app.add_route("/echo_use_kwargs", EchoUseKwargs())
172162 app.add_route("/echo_use_args_validated", EchoUseArgsValidated())
173 app.add_route("/echo_ignoring_extra_data", EchoJSONIgnoreExtraData())
174163 app.add_route("/echo_multi", EchoMulti())
175 app.add_route("/echo_multi_form", EchoMultiForm())
176 app.add_route("/echo_multi_json", EchoMultiJSON())
177164 app.add_route("/echo_many_schema", EchoManySchema())
178165 app.add_route("/echo_use_args_with_path_param/{name}", EchoUseArgsWithPathParam())
179166 app.add_route(
0 from webargs.core import json
01 from flask import Flask, jsonify as J, Response, request
12 from flask.views import MethodView
3
24 import marshmallow as ma
3
45 from webargs import fields
56 from webargs.flaskparser import parser, use_args, use_kwargs
6 from webargs.core import json, MARSHMALLOW_VERSION_INFO
7 from webargs.core import MARSHMALLOW_VERSION_INFO
78
89
910 class TestAppConfig:
2122 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
2223 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
2324
24 # variant which ignores unknown fields
25 exclude_kwargs = (
26 {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE}
27 )
28 hello_exclude_schema = HelloSchema(**exclude_kwargs)
29
3025 app = Flask(__name__)
3126 app.config.from_object(TestAppConfig)
3227
3328
34 @app.route("/echo", methods=["GET"])
29 @app.route("/echo", methods=["GET", "POST"])
3530 def echo():
36 return J(parser.parse(hello_args, location="query"))
31 return J(parser.parse(hello_args))
32
33
34 @app.route("/echo_query")
35 def echo_query():
36 return J(parser.parse(hello_args, request, locations=("query",)))
37
38
39 @app.route("/echo_json", methods=["POST"])
40 def echo_json():
41 return J(parser.parse(hello_args, request, locations=("json",)))
3742
3843
3944 @app.route("/echo_form", methods=["POST"])
4045 def echo_form():
41 return J(parser.parse(hello_args, location="form"))
46 return J(parser.parse(hello_args, request, locations=("form",)))
4247
4348
44 @app.route("/echo_json", methods=["POST"])
45 def echo_json():
46 return J(parser.parse(hello_args, location="json"))
47
48
49 @app.route("/echo_json_or_form", methods=["POST"])
50 def echo_json_or_form():
51 return J(parser.parse(hello_args, location="json_or_form"))
52
53
54 @app.route("/echo_use_args", methods=["GET"])
55 @use_args(hello_args, location="query")
49 @app.route("/echo_use_args", methods=["GET", "POST"])
50 @use_args(hello_args)
5651 def echo_use_args(args):
5752 return J(args)
5853
5954
60 @app.route("/echo_use_args_validated", methods=["POST"])
61 @use_args(
62 {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form"
63 )
55 @app.route("/echo_use_args_validated", methods=["GET", "POST"])
56 @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42)
6457 def echo_use_args_validated(args):
6558 return J(args)
6659
6760
68 @app.route("/echo_ignoring_extra_data", methods=["POST"])
69 def echo_json_ignore_extra_data():
70 return J(parser.parse(hello_exclude_schema))
71
72
73 @app.route("/echo_use_kwargs", methods=["GET"])
74 @use_kwargs(hello_args, location="query")
61 @app.route("/echo_use_kwargs", methods=["GET", "POST"])
62 @use_kwargs(hello_args)
7563 def echo_use_kwargs(name):
7664 return J({"name": name})
7765
7866
79 @app.route("/echo_multi", methods=["GET"])
67 @app.route("/echo_multi", methods=["GET", "POST"])
8068 def multi():
81 return J(parser.parse(hello_multiple, location="query"))
82
83
84 @app.route("/echo_multi_form", methods=["POST"])
85 def multi_form():
86 return J(parser.parse(hello_multiple, location="form"))
87
88
89 @app.route("/echo_multi_json", methods=["POST"])
90 def multi_json():
9169 return J(parser.parse(hello_multiple))
9270
9371
9472 @app.route("/echo_many_schema", methods=["GET", "POST"])
9573 def many_nested():
96 arguments = parser.parse(hello_many_schema)
74 arguments = parser.parse(hello_many_schema, locations=("json",))
9775 return Response(json.dumps(arguments), content_type="application/json")
9876
9977
10078 @app.route("/echo_use_args_with_path_param/<name>")
101 @use_args({"value": fields.Int()}, location="query")
79 @use_args({"value": fields.Int()})
10280 def echo_use_args_with_path(args, name):
10381 return J(args)
10482
10583
10684 @app.route("/echo_use_kwargs_with_path_param/<name>")
107 @use_kwargs({"value": fields.Int()}, location="query")
85 @use_kwargs({"value": fields.Int()})
10886 def echo_use_kwargs_with_path(name, value):
10987 return J({"value": value})
11088
12098
12199 @app.route("/echo_headers")
122100 def echo_headers():
123 # the "exclude schema" must be used in this case because WSGI headers may
124 # be populated with many fields not sent by the caller
125 return J(parser.parse(hello_exclude_schema, location="headers"))
101 return J(parser.parse(hello_args, locations=("headers",)))
126102
127103
128104 @app.route("/echo_cookie")
129105 def echo_cookie():
130 return J(parser.parse(hello_args, request, location="cookies"))
106 return J(parser.parse(hello_args, request, locations=("cookies",)))
131107
132108
133109 @app.route("/echo_file", methods=["POST"])
134110 def echo_file():
135111 args = {"myfile": fields.Field()}
136 result = parser.parse(args, location="files")
112 result = parser.parse(args, locations=("files",))
137113 fp = result["myfile"]
138114 content = fp.read().decode("utf8")
139115 return J({"myfile": content})
141117
142118 @app.route("/echo_view_arg/<view_arg>")
143119 def echo_view_arg(view_arg):
144 return J(parser.parse({"view_arg": fields.Int()}, location="view_args"))
120 return J(parser.parse({"view_arg": fields.Int()}, locations=("view_args",)))
145121
146122
147123 @app.route("/echo_view_arg_use_args/<view_arg>")
148 @use_args({"view_arg": fields.Int()}, location="view_args")
124 @use_args({"view_arg": fields.Int(location="view_args")})
149125 def echo_view_arg_with_use_args(args, **kwargs):
150126 return J(args)
151127
210186 def handle_error(err):
211187 if err.code == 422:
212188 assert isinstance(err.data["schema"], ma.Schema)
213
214 if MARSHMALLOW_VERSION_INFO[0] >= 3:
215 return J(err.data["messages"]), err.code
216
217 # on marshmallow2, validation errors for nested schemas can fail to encode:
218 # https://github.com/marshmallow-code/marshmallow/issues/493
219 # to workaround this, convert integer keys to strings
220 def tweak_data(value):
221 if not isinstance(value, dict):
222 return value
223 return {str(k): v for k, v in value.items()}
224
225 return J({k: tweak_data(v) for k, v in err.data["messages"].items()}), err.code
189 return J(err.data["messages"]), err.code
0 from webargs.core import json
1
02 from pyramid.config import Configurator
13 from pyramid.httpexceptions import HTTPBadRequest
24 import marshmallow as ma
35
46 from webargs import fields
57 from webargs.pyramidparser import parser, use_args, use_kwargs
6 from webargs.core import json, MARSHMALLOW_VERSION_INFO
8 from webargs.core import MARSHMALLOW_VERSION_INFO
79
810 hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)}
911 hello_multiple = {"name": fields.List(fields.Str())}
1618 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
1719 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
1820
19 # variant which ignores unknown fields
20 exclude_kwargs = (
21 {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE}
22 )
23 hello_exclude_schema = HelloSchema(**exclude_kwargs)
24
2521
2622 def echo(request):
27 return parser.parse(hello_args, request, location="query")
28
29
30 def echo_form(request):
31 return parser.parse(hello_args, request, location="form")
32
33
34 def echo_json(request):
3523 try:
36 return parser.parse(hello_args, request, location="json")
37 except json.JSONDecodeError:
38 error = HTTPBadRequest()
39 error.body = json.dumps(["Invalid JSON."]).encode("utf-8")
40 error.content_type = "application/json"
41 raise error
42
43
44 def echo_json_or_form(request):
45 try:
46 return parser.parse(hello_args, request, location="json_or_form")
47 except json.JSONDecodeError:
48 error = HTTPBadRequest()
49 error.body = json.dumps(["Invalid JSON."]).encode("utf-8")
50 error.content_type = "application/json"
51 raise error
52
53
54 def echo_json_ignore_extra_data(request):
55 try:
56 return parser.parse(hello_exclude_schema, request)
24 return parser.parse(hello_args, request)
5725 except json.JSONDecodeError:
5826 error = HTTPBadRequest()
5927 error.body = json.dumps(["Invalid JSON."]).encode("utf-8")
6230
6331
6432 def echo_query(request):
65 return parser.parse(hello_args, request, location="query")
33 return parser.parse(hello_args, request, locations=("query",))
6634
6735
68 @use_args(hello_args, location="query")
36 def echo_json(request):
37 return parser.parse(hello_args, request, locations=("json",))
38
39
40 def echo_form(request):
41 return parser.parse(hello_args, request, locations=("form",))
42
43
44 @use_args(hello_args)
6945 def echo_use_args(request, args):
7046 return args
7147
7248
73 @use_args(
74 {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form"
75 )
49 @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42)
7650 def echo_use_args_validated(request, args):
7751 return args
7852
7953
80 @use_kwargs(hello_args, location="query")
54 @use_kwargs(hello_args)
8155 def echo_use_kwargs(request, name):
8256 return {"name": name}
8357
8458
8559 def echo_multi(request):
86 return parser.parse(hello_multiple, request, location="query")
87
88
89 def echo_multi_form(request):
90 return parser.parse(hello_multiple, request, location="form")
91
92
93 def echo_multi_json(request):
9460 return parser.parse(hello_multiple, request)
9561
9662
9763 def echo_many_schema(request):
98 return parser.parse(hello_many_schema, request)
64 return parser.parse(hello_many_schema, request, locations=("json",))
9965
10066
101 @use_args({"value": fields.Int()}, location="query")
67 @use_args({"value": fields.Int()})
10268 def echo_use_args_with_path_param(request, args):
10369 return args
10470
10571
106 @use_kwargs({"value": fields.Int()}, location="query")
72 @use_kwargs({"value": fields.Int()})
10773 def echo_use_kwargs_with_path_param(request, value):
10874 return {"value": value}
10975
11783
11884
11985 def echo_headers(request):
120 return parser.parse(hello_exclude_schema, request, location="headers")
86 return parser.parse(hello_args, request, locations=("headers",))
12187
12288
12389 def echo_cookie(request):
124 return parser.parse(hello_args, request, location="cookies")
90 return parser.parse(hello_args, request, locations=("cookies",))
12591
12692
12793 def echo_file(request):
12894 args = {"myfile": fields.Field()}
129 result = parser.parse(args, request, location="files")
95 result = parser.parse(args, request, locations=("files",))
13096 myfile = result["myfile"]
13197 content = myfile.file.read().decode("utf8")
13298 return {"myfile": content}
145111
146112
147113 def echo_matchdict(request):
148 return parser.parse({"mymatch": fields.Int()}, request, location="matchdict")
114 return parser.parse({"mymatch": fields.Int()}, request, locations=("matchdict",))
149115
150116
151 class EchoCallable:
117 class EchoCallable(object):
152118 def __init__(self, request):
153119 self.request = request
154120
155 @use_args({"value": fields.Int()}, location="query")
121 @use_args({"value": fields.Int()})
156122 def __call__(self, args):
157123 return args
158124
168134 config = Configurator()
169135
170136 add_route(config, "/echo", echo)
137 add_route(config, "/echo_query", echo_query)
138 add_route(config, "/echo_json", echo_json)
171139 add_route(config, "/echo_form", echo_form)
172 add_route(config, "/echo_json", echo_json)
173 add_route(config, "/echo_json_or_form", echo_json_or_form)
174 add_route(config, "/echo_query", echo_query)
175 add_route(config, "/echo_ignoring_extra_data", echo_json_ignore_extra_data)
176140 add_route(config, "/echo_use_args", echo_use_args)
177141 add_route(config, "/echo_use_args_validated", echo_use_args_validated)
178142 add_route(config, "/echo_use_kwargs", echo_use_kwargs)
179143 add_route(config, "/echo_multi", echo_multi)
180 add_route(config, "/echo_multi_form", echo_multi_form)
181 add_route(config, "/echo_multi_json", echo_multi_json)
182144 add_route(config, "/echo_many_schema", echo_many_schema)
183145 add_route(
184146 config, "/echo_use_args_with_path_param/{name}", echo_use_args_with_path_param
0 # -*- coding: utf-8 -*-
1 # flake8: noqa
2 import sys
3
4 PY2 = int(sys.version[0]) == 2
5
6 if PY2:
7 text_type = unicode
8 binary_type = str
9 string_types = (str, unicode)
10 basestring = basestring
11 else:
12 text_type = str
13 binary_type = bytes
14 string_types = (str,)
15 basestring = (str, bytes)
0 # -*- coding: utf-8 -*-
01 import itertools
2 import mock
13 import datetime
24
35 import pytest
68 from django.utils.datastructures import MultiValueDict as DjMultiDict
79 from bottle import MultiDict as BotMultiDict
810
9 from webargs import fields, ValidationError
11 from webargs import fields, missing, ValidationError
1012 from webargs.core import (
1113 Parser,
14 get_value,
1215 dict2schema,
1316 is_json,
1417 get_mimetype,
1518 MARSHMALLOW_VERSION_INFO,
1619 )
17 from webargs.multidictproxy import MultiDictProxy
18
19 try:
20 # Python 3.5
21 import mock
22 except ImportError:
23 # Python 3.6+
24 from unittest import mock
2520
2621
2722 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
3126 def __init__(self, status_code, headers):
3227 self.status_code = status_code
3328 self.headers = headers
34 super().__init__(self, "HTTP Error occurred")
29 super(MockHTTPError, self).__init__(self, "HTTP Error occurred")
3530
3631
3732 class MockRequestParser(Parser):
3833 """A minimal parser implementation that parses mock requests."""
3934
40 def load_querystring(self, req, schema):
41 return MultiDictProxy(req.query, schema)
42
43 def load_json(self, req, schema):
44 return req.json
45
46 def load_cookies(self, req, schema):
47 return req.cookies
35 def parse_querystring(self, req, name, field):
36 return get_value(req.query, name, field)
37
38 def parse_json(self, req, name, field):
39 return get_value(req.json, name, field)
40
41 def parse_cookies(self, req, name, field):
42 return get_value(req.cookies, name, field)
4843
4944
5045 @pytest.yield_fixture(scope="function")
6358 # Parser tests
6459
6560
66 @mock.patch("webargs.core.Parser.load_json")
67 def test_load_json_called_by_parse_default(load_json, web_request):
68 schema = dict2schema({"foo": fields.Field()})()
69 load_json.return_value = {"foo": 1}
70 p = Parser()
71 p.parse(schema, web_request)
72 load_json.assert_called_with(web_request, schema)
73
74
75 @pytest.mark.parametrize(
76 "location", ["querystring", "form", "headers", "cookies", "files"]
77 )
78 def test_load_nondefault_called_by_parse_with_location(location, web_request):
79 with mock.patch(
80 "webargs.core.Parser.load_{}".format(location)
81 ) as mock_loadfunc, mock.patch("webargs.core.Parser.load_json") as load_json:
82 mock_loadfunc.return_value = {}
83 load_json.return_value = {}
84 p = Parser()
85
86 # ensure that without location=..., the loader is not called (json is
87 # called)
88 p.parse({"foo": fields.Field()}, web_request)
89 assert mock_loadfunc.call_count == 0
90 assert load_json.call_count == 1
91
92 # but when location=... is given, the loader *is* called and json is
93 # not called
94 p.parse({"foo": fields.Field()}, web_request, location=location)
95 assert mock_loadfunc.call_count == 1
96 # it was already 1, should not go up
97 assert load_json.call_count == 1
98
99
100 def test_parse(parser, web_request):
101 web_request.json = {"username": 42, "password": 42}
61 @mock.patch("webargs.core.Parser.parse_json")
62 def test_parse_json_called_by_parse_arg(parse_json, web_request):
63 field = fields.Field()
64 p = Parser()
65 p.parse_arg("foo", field, web_request)
66 parse_json.assert_called_with(web_request, "foo", field)
67
68
69 @mock.patch("webargs.core.Parser.parse_querystring")
70 def test_parse_querystring_called_by_parse_arg(parse_querystring, web_request):
71 field = fields.Field()
72 p = Parser()
73 p.parse_arg("foo", field, web_request)
74 assert parse_querystring.called_once()
75
76
77 @mock.patch("webargs.core.Parser.parse_form")
78 def test_parse_form_called_by_parse_arg(parse_form, web_request):
79 field = fields.Field()
80 p = Parser()
81 p.parse_arg("foo", field, web_request)
82 assert parse_form.called_once()
83
84
85 @mock.patch("webargs.core.Parser.parse_json")
86 def test_parse_json_not_called_when_json_not_a_location(parse_json, web_request):
87 field = fields.Field()
88 p = Parser()
89 p.parse_arg("foo", field, web_request, locations=("form", "querystring"))
90 assert parse_json.call_count == 0
91
92
93 @mock.patch("webargs.core.Parser.parse_headers")
94 def test_parse_headers_called_when_headers_is_a_location(parse_headers, web_request):
95 field = fields.Field()
96 p = Parser()
97 p.parse_arg("foo", field, web_request)
98 assert parse_headers.call_count == 0
99 p.parse_arg("foo", field, web_request, locations=("headers",))
100 parse_headers.assert_called()
101
102
103 @mock.patch("webargs.core.Parser.parse_cookies")
104 def test_parse_cookies_called_when_cookies_is_a_location(parse_cookies, web_request):
105 field = fields.Field()
106 p = Parser()
107 p.parse_arg("foo", field, web_request)
108 assert parse_cookies.call_count == 0
109 p.parse_arg("foo", field, web_request, locations=("cookies",))
110 parse_cookies.assert_called()
111
112
113 @mock.patch("webargs.core.Parser.parse_json")
114 def test_parse(parse_json, web_request):
115 parse_json.return_value = 42
102116 argmap = {"username": fields.Field(), "password": fields.Field()}
103 ret = parser.parse(argmap, web_request)
117 p = Parser()
118 ret = p.parse(argmap, web_request)
104119 assert {"username": 42, "password": 42} == ret
105
106
107 @pytest.mark.skipif(
108 MARSHMALLOW_VERSION_INFO[0] < 3, reason="unknown=... added in marshmallow3"
109 )
110 def test_parse_with_unknown_behavior_specified(parser, web_request):
111 # This is new in webargs 6.x ; it's the way you can "get back" the behavior
112 # of webargs 5.x in which extra args are ignored
113 from marshmallow import EXCLUDE, INCLUDE, RAISE
114
115 web_request.json = {"username": 42, "password": 42, "fjords": 42}
116
117 class CustomSchema(Schema):
118 username = fields.Field()
119 password = fields.Field()
120
121 # with no unknown setting or unknown=RAISE, it blows up
122 with pytest.raises(ValidationError, match="Unknown field."):
123 parser.parse(CustomSchema(), web_request)
124 with pytest.raises(ValidationError, match="Unknown field."):
125 parser.parse(CustomSchema(unknown=RAISE), web_request)
126
127 # with unknown=EXCLUDE the data is omitted
128 ret = parser.parse(CustomSchema(unknown=EXCLUDE), web_request)
129 assert {"username": 42, "password": 42} == ret
130 # with unknown=INCLUDE it is added even though it isn't part of the schema
131 ret = parser.parse(CustomSchema(unknown=INCLUDE), web_request)
132 assert {"username": 42, "password": 42, "fjords": 42} == ret
133120
134121
135122 def test_parse_required_arg_raises_validation_error(parser, web_request):
153140 assert result == {"first": "Steve", "last": None}
154141
155142
156 def test_parse_required_arg(parser, web_request):
157 web_request.json = {"foo": 42}
158 result = parser.parse({"foo": fields.Field(required=True)}, web_request)
159 assert result == {"foo": 42}
143 @mock.patch("webargs.core.Parser.parse_json")
144 def test_parse_required_arg(parse_json, web_request):
145 arg = fields.Field(required=True)
146 parse_json.return_value = 42
147 p = Parser()
148 result = p.parse_arg("foo", arg, web_request, locations=("json",))
149 assert result == 42
160150
161151
162152 def test_parse_required_list(parser, web_request):
164154 args = {"foo": fields.List(fields.Field(), required=True)}
165155 with pytest.raises(ValidationError) as excinfo:
166156 parser.parse(args, web_request)
167 assert (
168 excinfo.value.messages["json"]["foo"][0] == "Missing data for required field."
169 )
157 assert excinfo.value.messages["foo"][0] == "Missing data for required field."
170158
171159
172160 # Regression test for https://github.com/marshmallow-code/webargs/issues/107
181169 args = {"foo": fields.List(fields.Field(), allow_none=False)}
182170 with pytest.raises(ValidationError) as excinfo:
183171 parser.parse(args, web_request)
184 assert excinfo.value.messages["json"]["foo"][0] == "Field may not be null."
172 assert excinfo.value.messages["foo"][0] == "Field may not be null."
185173
186174
187175 def test_parse_empty_list(parser, web_request):
196184 assert parser.parse(args, web_request) == {}
197185
198186
199 def test_default_location():
200 assert Parser.DEFAULT_LOCATION == "json"
187 def test_default_locations():
188 assert set(Parser.DEFAULT_LOCATIONS) == set(["json", "querystring", "form"])
201189
202190
203191 def test_missing_with_default(parser, web_request):
204192 web_request.json = {}
205193 args = {"val": fields.Field(missing="pizza")}
206 result = parser.parse(args, web_request)
194 result = parser.parse(args, web_request, locations=("json",))
207195 assert result["val"] == "pizza"
208196
209197
210198 def test_default_can_be_none(parser, web_request):
211199 web_request.json = {}
212200 args = {"val": fields.Field(missing=None, allow_none=True)}
213 result = parser.parse(args, web_request)
201 result = parser.parse(args, web_request, locations=("json",))
214202 assert result["val"] is None
215203
216204
221209 "p": fields.Int(
222210 missing=1,
223211 validate=lambda p: p > 0,
224 error="La page demandée n'existe pas",
212 error=u"La page demandée n'existe pas",
225213 location="querystring",
226214 )
227215 }
228216 assert parser.parse(args, web_request) == {"p": 1}
229217
230218
231 def test_value_error_raised_if_parse_called_with_invalid_location(parser, web_request):
219 def test_value_error_raised_if_parse_arg_called_with_invalid_location(web_request):
232220 field = fields.Field()
233 with pytest.raises(ValueError, match="Invalid location argument: invalidlocation"):
234 parser.parse({"foo": field}, web_request, location="invalidlocation")
221 p = Parser()
222 with pytest.raises(ValueError) as excinfo:
223 p.parse_arg("foo", field, web_request, locations=("invalidlocation", "headers"))
224 msg = "Invalid locations arguments: {0}".format(["invalidlocation"])
225 assert msg in str(excinfo.value)
226
227
228 def test_value_error_raised_if_invalid_location_on_field(web_request, parser):
229 with pytest.raises(ValueError) as excinfo:
230 parser.parse({"foo": fields.Field(location="invalidlocation")}, web_request)
231 msg = "Invalid locations arguments: {0}".format(["invalidlocation"])
232 assert msg in str(excinfo.value)
235233
236234
237235 @mock.patch("webargs.core.Parser.handle_error")
238 def test_handle_error_called_when_parsing_raises_error(handle_error, web_request):
239 def always_fail(*args, **kwargs):
240 raise ValidationError("error occurred")
241
242 p = Parser()
243 assert handle_error.call_count == 0
244 p.parse({"foo": fields.Field()}, web_request, validate=always_fail)
245 assert handle_error.call_count == 1
246 p.parse({"foo": fields.Field()}, web_request, validate=always_fail)
236 @mock.patch("webargs.core.Parser.parse_json")
237 def test_handle_error_called_when_parsing_raises_error(
238 parse_json, handle_error, web_request
239 ):
240 val_err = ValidationError("error occurred")
241 parse_json.side_effect = val_err
242 p = Parser()
243 p.parse({"foo": fields.Field()}, web_request, locations=("json",))
244 handle_error.assert_called()
245 parse_json.side_effect = ValidationError("another exception")
246 p.parse({"foo": fields.Field()}, web_request, locations=("json",))
247247 assert handle_error.call_count == 2
248248
249249
250250 def test_handle_error_reraises_errors(web_request):
251251 p = Parser()
252252 with pytest.raises(ValidationError):
253 p.handle_error(
254 ValidationError("error raised"),
255 web_request,
256 Schema(),
257 error_status_code=422,
258 error_headers={},
259 )
260
261
262 @mock.patch("webargs.core.Parser.load_headers")
263 def test_location_as_init_argument(load_headers, web_request):
264 p = Parser(location="headers")
265 load_headers.return_value = {}
253 p.handle_error(ValidationError("error raised"), web_request, Schema())
254
255
256 @mock.patch("webargs.core.Parser.parse_headers")
257 def test_locations_as_init_arguments(parse_headers, web_request):
258 p = Parser(locations=("headers",))
266259 p.parse({"foo": fields.Field()}, web_request)
267 assert load_headers.called
268
269
270 def test_custom_error_handler(web_request):
260 assert parse_headers.called
261
262
263 @mock.patch("webargs.core.Parser.parse_files")
264 def test_parse_files(parse_files, web_request):
265 p = Parser()
266 p.parse({"foo": fields.Field()}, web_request, locations=("files",))
267 assert parse_files.called
268
269
270 @mock.patch("webargs.core.Parser.parse_json")
271 def test_custom_error_handler(parse_json, web_request):
271272 class CustomError(Exception):
272273 pass
273274
274 def error_handler(error, req, schema, *, error_status_code, error_headers):
275 def error_handler(error, req, schema, status_code, headers):
275276 assert isinstance(schema, Schema)
276277 raise CustomError(error)
277278
278 def failing_validate_func(args):
279 raise ValidationError("parsing failed")
280
281 class MySchema(Schema):
282 foo = fields.Int()
283
284 myschema = MySchema(**strict_kwargs)
285 web_request.json = {"foo": "hello world"}
286
279 parse_json.side_effect = ValidationError("parse_json failed")
287280 p = Parser(error_handler=error_handler)
288281 with pytest.raises(CustomError):
289 p.parse(myschema, web_request, validate=failing_validate_func)
290
291
292 def test_custom_error_handler_decorator(web_request):
282 p.parse({"foo": fields.Field()}, web_request)
283
284
285 @mock.patch("webargs.core.Parser.parse_json")
286 def test_custom_error_handler_decorator(parse_json, web_request):
293287 class CustomError(Exception):
294288 pass
295289
296 mock_schema = mock.Mock(spec=Schema)
297 mock_schema.strict = True
298 mock_schema.load.side_effect = ValidationError("parsing json failed")
290 parse_json.side_effect = ValidationError("parse_json failed")
291
299292 parser = Parser()
300293
301294 @parser.error_handler
302 def handle_error(error, req, schema, *, error_status_code, error_headers):
295 def handle_error(error, req, schema, status_code, headers):
303296 assert isinstance(schema, Schema)
304297 raise CustomError(error)
305298
306299 with pytest.raises(CustomError):
307 parser.parse(mock_schema, web_request)
308
309
310 def test_custom_location_loader(web_request):
300 parser.parse({"foo": fields.Field()}, web_request)
301
302
303 def test_custom_location_handler(web_request):
311304 web_request.data = {"foo": 42}
312305
313306 parser = Parser()
314307
315 @parser.location_loader("data")
316 def load_data(req, schema):
317 return req.data
318
319 result = parser.parse({"foo": fields.Int()}, web_request, location="data")
308 @parser.location_handler("data")
309 def parse_data(req, name, arg):
310 return req.data.get(name, missing)
311
312 result = parser.parse({"foo": fields.Int()}, web_request, locations=("data",))
320313 assert result["foo"] == 42
321314
322315
323 def test_custom_location_loader_with_data_key(web_request):
316 def test_custom_location_handler_with_data_key(web_request):
324317 web_request.data = {"X-Foo": 42}
325318 parser = Parser()
326319
327 @parser.location_loader("data")
328 def load_data(req, schema):
329 return req.data
320 @parser.location_handler("data")
321 def parse_data(req, name, arg):
322 return req.data.get(name, missing)
330323
331324 data_key_kwarg = {
332325 "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "X-Foo"
333326 }
334327 result = parser.parse(
335 {"x_foo": fields.Int(**data_key_kwarg)}, web_request, location="data"
328 {"x_foo": fields.Int(**data_key_kwarg)}, web_request, locations=("data",)
336329 )
337330 assert result["x_foo"] == 42
338331
339332
340 def test_full_input_validation(parser, web_request):
333 def test_full_input_validation(web_request):
341334
342335 web_request.json = {"foo": 41, "bar": 42}
343336
337 parser = MockRequestParser()
344338 args = {"foo": fields.Int(), "bar": fields.Int()}
345339 with pytest.raises(ValidationError):
346340 # Test that `validate` receives dictionary of args
347 parser.parse(args, web_request, validate=lambda args: args["foo"] > args["bar"])
341 parser.parse(
342 args,
343 web_request,
344 locations=("json",),
345 validate=lambda args: args["foo"] > args["bar"],
346 )
348347
349348
350349 def test_full_input_validation_with_multiple_validators(web_request, parser):
360359 web_request.json = {"a": 2, "b": 1}
361360 validators = [validate1, validate2]
362361 with pytest.raises(ValidationError, match="b must be > a"):
363 parser.parse(args, web_request, validate=validators)
362 parser.parse(args, web_request, locations=("json",), validate=validators)
364363
365364 web_request.json = {"a": 1, "b": 2}
366365 with pytest.raises(ValidationError, match="a must be > b"):
367 parser.parse(args, web_request, validate=validators)
368
369
370 def test_required_with_custom_error(parser, web_request):
366 parser.parse(args, web_request, locations=("json",), validate=validators)
367
368
369 def test_required_with_custom_error(web_request):
371370 web_request.json = {}
371 parser = MockRequestParser()
372372 args = {
373373 "foo": fields.Str(required=True, error_messages={"required": "We need foo"})
374374 }
375375 with pytest.raises(ValidationError) as excinfo:
376376 # Test that `validate` receives dictionary of args
377 parser.parse(args, web_request)
378
379 assert "We need foo" in excinfo.value.messages["json"]["foo"]
377 parser.parse(args, web_request, locations=("json",))
378
379 assert "We need foo" in excinfo.value.messages["foo"]
380380 if MARSHMALLOW_VERSION_INFO[0] < 3:
381381 assert "foo" in excinfo.value.field_names
382382
383383
384 def test_required_with_custom_error_and_validation_error(parser, web_request):
384 def test_required_with_custom_error_and_validation_error(web_request):
385385 web_request.json = {"foo": ""}
386 parser = MockRequestParser()
386387 args = {
387388 "foo": fields.Str(
388389 required="We need foo",
392393 }
393394 with pytest.raises(ValidationError) as excinfo:
394395 # Test that `validate` receives dictionary of args
395 parser.parse(args, web_request)
396 parser.parse(args, web_request, locations=("json",))
396397
397398 assert "foo required length is 3" in excinfo.value.args[0]["foo"]
398399 if MARSHMALLOW_VERSION_INFO[0] < 3:
403404 def validate(val):
404405 return False
405406
406 text = "øœ∑∆∑"
407 text = u"øœ∑∆∑"
407408 web_request.json = {"text": text}
408409 parser = MockRequestParser()
409410 args = {"text": fields.Str()}
410411 with pytest.raises(ValidationError) as excinfo:
411 parser.parse(args, web_request, validate=validate)
412 assert excinfo.value.messages == {"json": ["Invalid value."]}
412 parser.parse(args, web_request, locations=("json",), validate=validate)
413 assert excinfo.value.messages == ["Invalid value."]
413414
414415
415416 def test_invalid_argument_for_validate(web_request, parser):
416417 with pytest.raises(ValueError) as excinfo:
417418 parser.parse({}, web_request, validate="notcallable")
418419 assert "not a callable or list of callables." in excinfo.value.args[0]
420
421
422 def test_get_value_basic():
423 assert get_value({"foo": 42}, "foo", False) == 42
424 assert get_value({"foo": 42}, "bar", False) is missing
425 assert get_value({"foos": ["a", "b"]}, "foos", True) == ["a", "b"]
426 # https://github.com/marshmallow-code/webargs/pull/30
427 assert get_value({"foos": ["a", "b"]}, "bar", True) is missing
419428
420429
421430 def create_bottle_multi_dict():
433442
434443
435444 @pytest.mark.parametrize("input_dict", multidicts)
436 def test_multidict_proxy(input_dict):
437 class ListSchema(Schema):
438 foos = fields.List(fields.Str())
439
440 class StrSchema(Schema):
441 foos = fields.Str()
442
443 # this MultiDictProxy is aware that "foos" is a list field and will
444 # therefore produce a list with __getitem__
445 list_wrapped_multidict = MultiDictProxy(input_dict, ListSchema())
446
447 # this MultiDictProxy is under the impression that "foos" is just a string
448 # and it should return "a" or "b"
449 # the decision between "a" and "b" in this case belongs to the framework
450 str_wrapped_multidict = MultiDictProxy(input_dict, StrSchema())
451
452 assert list_wrapped_multidict["foos"] == ["a", "b"]
453 assert str_wrapped_multidict["foos"] in ("a", "b")
445 def test_get_value_multidict(input_dict):
446 field = fields.List(fields.Str())
447 assert get_value(input_dict, "foos", field) == ["a", "b"]
454448
455449
456450 def test_parse_with_data_key(web_request):
461455 "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "Content-Type"
462456 }
463457 args = {"content_type": fields.Field(**data_key_kwargs)}
464 parsed = parser.parse(args, web_request)
458 parsed = parser.parse(args, web_request, locations=("json",))
465459 assert parsed == {"content_type": "application/json"}
466460
467461
475469
476470 parser = MockRequestParser()
477471 args = {"content_type": fields.Field(load_from="Content-Type")}
478 parsed = parser.parse(args, web_request)
472 parsed = parser.parse(args, web_request, locations=("json",))
479473 assert parsed == {"content_type": "application/json"}
480474
481475
488482 }
489483 args = {"content_type": fields.Str(**data_key_kwargs)}
490484 with pytest.raises(ValidationError) as excinfo:
491 parser.parse(args, web_request)
492 assert "json" in excinfo.value.messages
493 assert "Content-Type" in excinfo.value.messages["json"]
494 assert excinfo.value.messages["json"]["Content-Type"] == ["Not a valid string."]
485 parser.parse(args, web_request, locations=("json",))
486 assert "Content-Type" in excinfo.value.messages
487 assert excinfo.value.messages["Content-Type"] == ["Not a valid string."]
495488
496489
497490 def test_parse_nested_with_data_key(web_request):
502495 }
503496 args = {"nested_arg": fields.Nested({"right": fields.Field(**data_key_kwarg)})}
504497
505 parsed = parser.parse(args, web_request)
498 parsed = parser.parse(args, web_request, locations=("json",))
506499 assert parsed == {"nested_arg": {"right": "OK"}}
507500
508501
519512 )
520513 }
521514
522 parsed = parser.parse(args, web_request)
515 parsed = parser.parse(args, web_request, locations=("json",))
523516 assert parsed == {"nested_arg": {"found": None}}
524517
525518
529522 web_request.json = {"nested_arg": {}}
530523 args = {"nested_arg": fields.Nested({"miss": fields.Field(missing="<foo>")})}
531524
532 parsed = parser.parse(args, web_request)
525 parsed = parser.parse(args, web_request, locations=("json",))
533526 assert parsed == {"nested_arg": {"miss": "<foo>"}}
534527
535528
560553 web_request.json = {"username": "foo"}
561554 web_request.query = {"page": 42}
562555
563 @parser.use_args(query_args, web_request, location="query")
564 @parser.use_args(json_args, web_request)
556 @parser.use_args(query_args, web_request, locations=("query",))
557 @parser.use_args(json_args, web_request, locations=("json",))
565558 def viewfunc(query_parsed, json_parsed):
566559 return {"json": json_parsed, "query": query_parsed}
567560
576569 web_request.json = {"username": "foo"}
577570 web_request.query = {"page": 42}
578571
579 @parser.use_kwargs(query_args, web_request, location="query")
580 @parser.use_kwargs(json_args, web_request)
572 @parser.use_kwargs(query_args, web_request, locations=("query",))
573 @parser.use_kwargs(json_args, web_request, locations=("json",))
581574 def viewfunc(page, username):
582575 return {"json": {"username": username}, "query": {"page": page}}
583576
598591
599592 def test_list_allowed_missing(web_request, parser):
600593 args = {"name": fields.List(fields.Str())}
601 web_request.json = {}
594 web_request.json = {"fakedata": True}
602595 result = parser.parse(args, web_request)
603596 assert result == {}
604597
605598
606599 def test_int_list_allowed_missing(web_request, parser):
607600 args = {"name": fields.List(fields.Int())}
608 web_request.json = {}
601 web_request.json = {"fakedata": True}
609602 result = parser.parse(args, web_request)
610603 assert result == {}
611604
612605
613606 def test_multiple_arg_required_with_int_conversion(web_request, parser):
614607 args = {"ids": fields.List(fields.Int(), required=True)}
615 web_request.json = {}
608 web_request.json = {"fakedata": True}
616609 with pytest.raises(ValidationError) as excinfo:
617610 parser.parse(args, web_request)
618 assert excinfo.value.messages == {
619 "json": {"ids": ["Missing data for required field."]}
620 }
611 assert excinfo.value.messages == {"ids": ["Missing data for required field."]}
621612
622613
623614 def test_parse_with_callable(web_request, parser):
755746 assert "strict=True" in str(warning.message)
756747
757748 def test_use_kwargs_stacked(self, web_request, parser):
758 if MARSHMALLOW_VERSION_INFO[0] >= 3:
759 from marshmallow import EXCLUDE
760
761 class PageSchema(Schema):
762 page = fields.Int()
763
764 pageschema = PageSchema(unknown=EXCLUDE)
765 userschema = self.UserSchema(unknown=EXCLUDE)
766 else:
767 pageschema = {"page": fields.Int()}
768 userschema = self.UserSchema(**strict_kwargs)
769
770749 web_request.json = {"email": "[email protected]", "password": "bar", "page": 42}
771750
772 @parser.use_kwargs(pageschema, web_request)
773 @parser.use_kwargs(userschema, web_request)
751 @parser.use_kwargs({"page": fields.Int()}, web_request)
752 @parser.use_kwargs(self.UserSchema(**strict_kwargs), web_request)
774753 def viewfunc(email, password, page):
775754 return {"email": email, "password": password, "page": page}
776755
794773 return True
795774
796775 web_request.json = {"name": "Eric Cartman"}
797 res = parser.parse(UserSchema, web_request)
776 res = parser.parse(UserSchema, web_request, locations=("json",))
798777 assert res == {"name": "Eric Cartman"}
799778
800779
801 def test_use_args_with_custom_location_in_parser(web_request, parser):
780 def test_use_args_with_custom_locations_in_parser(web_request, parser):
802781 custom_args = {"foo": fields.Str()}
803782 web_request.json = {}
804 parser.location = "custom"
805
806 @parser.location_loader("custom")
807 def load_custom(schema, req):
808 return {"foo": "bar"}
783 parser.locations = ("custom",)
784
785 @parser.location_handler("custom")
786 def parse_custom(req, name, arg):
787 return "bar"
809788
810789 @parser.use_args(custom_args, web_request)
811790 def viewfunc(args):
847826
848827 dumped = schema.dump(parsed)
849828 data = dumped.data if MARSHMALLOW_VERSION_INFO[0] < 3 else dumped
829 assert data["ids"] == [1, 2, 3]
830
831
832 def test_delimited_list_as_string(web_request, parser):
833 web_request.json = {"ids": "1,2,3"}
834 schema_cls = dict2schema(
835 {"ids": fields.DelimitedList(fields.Int(), as_string=True)}
836 )
837 schema = schema_cls()
838
839 parsed = parser.parse(schema, web_request)
840 assert parsed["ids"] == [1, 2, 3]
841
842 dumped = schema.dump(parsed)
843 data = dumped.data if MARSHMALLOW_VERSION_INFO[0] < 3 else dumped
850844 assert data["ids"] == "1,2,3"
851845
852846
853 @pytest.mark.skipif(
854 MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3"
855 )
856 def test_delimited_tuple_default_delimiter(web_request, parser):
857 """
858 Test load and dump from DelimitedTuple, including the use of a datetime
859 type (similar to a DelimitedList test below) which confirms that we aren't
860 relying on __str__, but are properly de/serializing the included fields
861 """
862 web_request.json = {"ids": "1,2,2020-05-04"}
847 def test_delimited_list_as_string_v2(web_request, parser):
848 web_request.json = {"dates": "2018-11-01,2018-11-02"}
863849 schema_cls = dict2schema(
864850 {
865 "ids": fields.DelimitedTuple(
866 (fields.Int, fields.Int, fields.DateTime(format="%Y-%m-%d"))
851 "dates": fields.DelimitedList(
852 fields.DateTime(format="%Y-%m-%d"), as_string=True
867853 )
868854 }
869 )
870 schema = schema_cls()
871
872 parsed = parser.parse(schema, web_request)
873 assert parsed["ids"] == (1, 2, datetime.datetime(2020, 5, 4))
874
875 data = schema.dump(parsed)
876 assert data["ids"] == "1,2,2020-05-04"
877
878
879 @pytest.mark.skipif(
880 MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3"
881 )
882 def test_delimited_tuple_incorrect_arity(web_request, parser):
883 web_request.json = {"ids": "1,2"}
884 schema_cls = dict2schema(
885 {"ids": fields.DelimitedTuple((fields.Int, fields.Int, fields.Int))}
886 )
887 schema = schema_cls()
888
889 with pytest.raises(ValidationError):
890 parser.parse(schema, web_request)
891
892
893 def test_delimited_list_with_datetime(web_request, parser):
894 """
895 Test that DelimitedList(DateTime(format=...)) correctly parses and dumps
896 dates to and from strings -- indicates that we're doing proper
897 serialization of values in dump() and not just relying on __str__ producing
898 correct results
899 """
900 web_request.json = {"dates": "2018-11-01,2018-11-02"}
901 schema_cls = dict2schema(
902 {"dates": fields.DelimitedList(fields.DateTime(format="%Y-%m-%d"))}
903855 )
904856 schema = schema_cls()
905857
922874 parsed = parser.parse(schema, web_request)
923875 assert parsed["ids"] == [1, 2, 3]
924876
925 dumped = schema.dump(parsed)
926 data = dumped.data if MARSHMALLOW_VERSION_INFO[0] < 3 else dumped
927 assert data["ids"] == "1|2|3"
928
929
930 @pytest.mark.skipif(
931 MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3"
932 )
933 def test_delimited_tuple_custom_delimiter(web_request, parser):
934 web_request.json = {"ids": "1|2"}
935 schema_cls = dict2schema(
936 {"ids": fields.DelimitedTuple((fields.Int, fields.Int), delimiter="|")}
937 )
938 schema = schema_cls()
939
940 parsed = parser.parse(schema, web_request)
941 assert parsed["ids"] == (1, 2)
942
943 data = schema.dump(parsed)
944 assert data["ids"] == "1|2"
945
946
947 def test_delimited_list_load_list_errors(web_request, parser):
877
878 def test_delimited_list_load_list(web_request, parser):
948879 web_request.json = {"ids": [1, 2, 3]}
949880 schema_cls = dict2schema({"ids": fields.DelimitedList(fields.Int())})
950881 schema = schema_cls()
951882
952 with pytest.raises(ValidationError) as excinfo:
953 parser.parse(schema, web_request)
954 exc = excinfo.value
955 assert isinstance(exc, ValidationError)
956 errors = exc.args[0]
957 assert errors["ids"] == ["Not a valid delimited list."]
958
959
960 @pytest.mark.skipif(
961 MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3"
962 )
963 def test_delimited_tuple_load_list_errors(web_request, parser):
964 web_request.json = {"ids": [1, 2]}
965 schema_cls = dict2schema({"ids": fields.DelimitedTuple((fields.Int, fields.Int))})
966 schema = schema_cls()
967
968 with pytest.raises(ValidationError) as excinfo:
969 parser.parse(schema, web_request)
970 exc = excinfo.value
971 assert isinstance(exc, ValidationError)
972 errors = exc.args[0]
973 assert errors["ids"] == ["Not a valid delimited tuple."]
883 parsed = parser.parse(schema, web_request)
884 assert parsed["ids"] == [1, 2, 3]
974885
975886
976887 # Regresion test for https://github.com/marshmallow-code/webargs/issues/149
981892
982893 with pytest.raises(ValidationError) as excinfo:
983894 parser.parse(schema, web_request)
984 assert excinfo.value.messages == {"json": {"ids": ["Not a valid delimited list."]}}
985
986
987 @pytest.mark.skipif(
988 MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3"
989 )
990 def test_delimited_tuple_passed_invalid_type(web_request, parser):
991 web_request.json = {"ids": 1}
992 schema_cls = dict2schema({"ids": fields.DelimitedTuple((fields.Int,))})
993 schema = schema_cls()
994
995 with pytest.raises(ValidationError) as excinfo:
996 parser.parse(schema, web_request)
997 assert excinfo.value.messages == {"json": {"ids": ["Not a valid delimited tuple."]}}
895 assert excinfo.value.messages == {"ids": ["Not a valid list."]}
998896
999897
1000898 def test_missing_list_argument_not_in_parsed_result(web_request, parser):
1012910 msg = "Missing data for required field."
1013911 with pytest.raises(ValidationError, match=msg):
1014912 parser.parse(args, web_request)
913
914
915 def test_arg_location_param(web_request, parser):
916 web_request.json = {"foo": 24}
917 web_request.cookies = {"foo": 42}
918 args = {"foo": fields.Field(location="cookies")}
919
920 parsed = parser.parse(args, web_request)
921
922 assert parsed["foo"] == 42
1015923
1016924
1017925 def test_validation_errors_in_validator_are_passed_to_handle_error(parser, web_request):
11111019
11121020
11131021 class MockRequestParserWithErrorHandler(MockRequestParser):
1114 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
1022 def handle_error(
1023 self, error, req, schema, error_status_code=None, error_headers=None
1024 ):
11151025 assert isinstance(error, ValidationError)
11161026 assert isinstance(schema, Schema)
11171027 raise MockHTTPError(error_status_code, error_headers)
11301040 assert error.headers == {"X-Foo": "bar"}
11311041
11321042
1133 @mock.patch("webargs.core.Parser.load_json")
1134 def test_custom_schema_class(load_json, web_request):
1043 @mock.patch("webargs.core.Parser.parse_json")
1044 def test_custom_schema_class(parse_json, web_request):
11351045 class CustomSchema(Schema):
11361046 @pre_load
11371047 def pre_load(self, data, **kwargs):
11381048 data["value"] += " world"
11391049 return data
11401050
1141 load_json.return_value = {"value": "hello"}
1051 parse_json.return_value = "hello"
11421052 argmap = {"value": fields.Str()}
11431053 p = Parser(schema_class=CustomSchema)
11441054 ret = p.parse(argmap, web_request)
11451055 assert ret == {"value": "hello world"}
11461056
11471057
1148 @mock.patch("webargs.core.Parser.load_json")
1149 def test_custom_default_schema_class(load_json, web_request):
1058 @mock.patch("webargs.core.Parser.parse_json")
1059 def test_custom_default_schema_class(parse_json, web_request):
11501060 class CustomSchema(Schema):
11511061 @pre_load
11521062 def pre_load(self, data, **kwargs):
11561066 class CustomParser(Parser):
11571067 DEFAULT_SCHEMA_CLASS = CustomSchema
11581068
1159 load_json.return_value = {"value": "hello"}
1069 parse_json.return_value = "hello"
11601070 argmap = {"value": fields.Str()}
11611071 p = CustomParser()
11621072 ret = p.parse(argmap, web_request)
0 # -*- coding: utf-8 -*-
1 from __future__ import unicode_literals
2
03 import pytest
14 from tests.apps.django_app.base.wsgi import application
25
1922
2023 def test_parsing_in_class_based_view(self, testapp):
2124 assert testapp.get("/echo_cbv?name=Fred").json == {"name": "Fred"}
22 assert testapp.post_json("/echo_cbv", {"name": "Fred"}).json == {"name": "Fred"}
25 assert testapp.post("/echo_cbv", {"name": "Fred"}).json == {"name": "Fred"}
2326
2427 def test_use_args_in_class_based_view(self, testapp):
2528 res = testapp.get("/echo_use_args_cbv?name=Fred")
2629 assert res.json == {"name": "Fred"}
27 res = testapp.post_json("/echo_use_args_cbv", {"name": "Fred"})
30 res = testapp.post("/echo_use_args_cbv", {"name": "Fred"})
2831 assert res.json == {"name": "Fred"}
2932
3033 def test_use_args_in_class_based_view_with_path_param(self, testapp):
0 # -*- coding: utf-8 -*-
01 import pytest
1 import falcon.testing
22
33 from webargs.testing import CommonTestCase
44 from tests.apps.falcon_app import create_app
1818 # https://github.com/marshmallow-code/webargs/issues/427
1919 def test_parse_json_with_nonutf8_chars(self, testapp):
2020 res = testapp.post(
21 "/echo_json",
21 "/echo",
2222 b"\xfe",
2323 headers={"Accept": "application/json", "Content-Type": "application/json"},
2424 expect_errors=True,
3030 # https://github.com/sloria/webargs/issues/329
3131 def test_invalid_json(self, testapp):
3232 res = testapp.post(
33 "/echo_json",
33 "/echo",
3434 '{"foo": "bar", }',
3535 headers={"Accept": "application/json", "Content-Type": "application/json"},
3636 expect_errors=True,
3737 )
3838 assert res.status_code == 400
3939 assert res.json["errors"] == {"json": ["Invalid JSON body."]}
40
41 # Falcon converts headers to all-caps
42 def test_parsing_headers(self, testapp):
43 res = testapp.get("/echo_headers", headers={"name": "Fred"})
44 assert res.json == {"NAME": "Fred"}
45
46 # `falcon.testing.TestClient.simulate_request` parses request with `wsgiref`
47 def test_body_parsing_works_with_simulate(self):
48 app = self.create_app()
49 client = falcon.testing.TestClient(app)
50 res = client.simulate_post("/echo_json", json={"name": "Fred"},)
51 assert res.json == {"name": "Fred"}
0 # -*- coding: utf-8 -*-
1 from __future__ import unicode_literals
2 import threading
3
04 from werkzeug.exceptions import HTTPException
5 import mock
16 import pytest
27
38 from flask import Flask
4 from webargs import fields, ValidationError, missing, dict2schema
9 from webargs import fields, ValidationError, missing
510 from webargs.flaskparser import parser, abort
611 from webargs.core import MARSHMALLOW_VERSION_INFO, json
712
813 from .apps.flask_app import app
914 from webargs.testing import CommonTestCase
10
11 try:
12 # Python 3.5
13 import mock
14 except ImportError:
15 # Python 3.6+
16 from unittest import mock
1715
1816
1917 class TestFlaskParser(CommonTestCase):
2725 def test_parsing_invalid_view_arg(self, testapp):
2826 res = testapp.get("/echo_view_arg/foo", expect_errors=True)
2927 assert res.status_code == 422
30 assert res.json == {"view_args": {"view_arg": ["Not a valid integer."]}}
28 assert res.json == {"view_arg": ["Not a valid integer."]}
3129
3230 def test_use_args_with_view_args_parsing(self, testapp):
3331 res = testapp.get("/echo_view_arg_use_args/42")
3432 assert res.json == {"view_arg": 42}
3533
3634 def test_use_args_on_a_method_view(self, testapp):
37 res = testapp.post_json("/echo_method_view_use_args", {"val": 42})
35 res = testapp.post("/echo_method_view_use_args", {"val": 42})
3836 assert res.json == {"val": 42}
3937
4038 def test_use_kwargs_on_a_method_view(self, testapp):
41 res = testapp.post_json("/echo_method_view_use_kwargs", {"val": 42})
39 res = testapp.post("/echo_method_view_use_kwargs", {"val": 42})
4240 assert res.json == {"val": 42}
4341
4442 def test_use_kwargs_with_missing_data(self, testapp):
45 res = testapp.post_json("/echo_use_kwargs_missing", {"username": "foo"})
43 res = testapp.post("/echo_use_kwargs_missing", {"username": "foo"})
4644 assert res.json == {"username": "foo"}
4745
4846 # regression test for https://github.com/marshmallow-code/webargs/issues/145
4947 def test_nested_many_with_data_key(self, testapp):
50 post_with_raw_fieldname_args = (
51 "/echo_nested_many_data_key",
52 {"x_field": [{"id": 42}]},
53 )
54 # under marshmallow 2 this is allowed and works
48 res = testapp.post_json("/echo_nested_many_data_key", {"x_field": [{"id": 42}]})
49 # https://github.com/marshmallow-code/marshmallow/pull/714
5550 if MARSHMALLOW_VERSION_INFO[0] < 3:
56 res = testapp.post_json(*post_with_raw_fieldname_args)
5751 assert res.json == {"x_field": [{"id": 42}]}
58 # but under marshmallow3 , only data_key is checked, field name is ignored
59 else:
60 res = testapp.post_json(*post_with_raw_fieldname_args, expect_errors=True)
61 assert res.status_code == 422
6252
6353 res = testapp.post_json("/echo_nested_many_data_key", {"X-Field": [{"id": 24}]})
6454 assert res.json == {"x_field": [{"id": 24}]}
8676 abort_args, abort_kwargs = mock_abort.call_args
8777 assert abort_args[0] == 422
8878 expected_msg = "Invalid value."
89 assert abort_kwargs["messages"]["json"]["value"] == [expected_msg]
79 assert abort_kwargs["messages"]["value"] == [expected_msg]
9080 assert type(abort_kwargs["exc"]) == ValidationError
9181
9282
93 @pytest.mark.parametrize("mimetype", [None, "application/json"])
94 def test_load_json_returns_missing_if_no_data(mimetype):
83 def test_parse_form_returns_missing_if_no_form():
9584 req = mock.Mock()
96 req.mimetype = mimetype
97 req.get_data.return_value = ""
98 schema = dict2schema({"foo": fields.Field()})()
99 assert parser.load_json(req, schema) is missing
85 req.form.get.side_effect = AttributeError("no form")
86 assert parser.parse_form(req, "foo", fields.Field()) is missing
10087
10188
10289 def test_abort_with_message():
123110 error = json.loads(serialized_error)
124111 assert isinstance(error, dict)
125112 assert error["message"] == "custom error message"
113
114
115 def test_json_cache_race_condition():
116 app = Flask("testapp")
117 lock = threading.Lock()
118 lock.acquire()
119
120 class MyField(fields.Field):
121 def _deserialize(self, value, attr, data, **kwargs):
122 with lock:
123 return value
124
125 argmap = {"value": MyField()}
126 results = {}
127
128 def thread_fn(value):
129 with app.test_request_context(
130 "/foo",
131 method="post",
132 data=json.dumps({"value": value}),
133 content_type="application/json",
134 ):
135 results[value] = parser.parse(argmap)["value"]
136
137 t1 = threading.Thread(target=thread_fn, args=(42,))
138 t2 = threading.Thread(target=thread_fn, args=(23,))
139 t1.start()
140 t2.start()
141 lock.release()
142 t1.join()
143 t2.join()
144 # ensure we didn't get contaminated by a parallel request
145 assert results[42] == 42
146 assert results[23] == 23
0 # -*- coding: utf-8 -*-
1
02 import asyncio
13 import webtest
24 import webtest_aiohttp
3537
3638 # regression test for https://github.com/marshmallow-code/webargs/issues/165
3739 def test_multiple_args(self, testapp):
38 res = testapp.post_json("/echo_multiple_args", {"first": "1", "last": "2"})
40 res = testapp.post_json(
41 "/echo_multiple_args", {"first": "1", "last": "2", "_ignore": 0}
42 )
3943 assert res.json == {"first": "1", "last": "2"}
4044
4145 # regression test for https://github.com/marshmallow-code/webargs/issues/145
4246 def test_nested_many_with_data_key(self, testapp):
47 res = testapp.post_json("/echo_nested_many_data_key", {"x_field": [{"id": 42}]})
4348 # https://github.com/marshmallow-code/marshmallow/pull/714
44 # on marshmallow 2, the field name can also be used
4549 if MARSHMALLOW_VERSION_INFO[0] < 3:
46 res = testapp.post_json(
47 "/echo_nested_many_data_key", {"x_field": [{"id": 42}]}
48 )
4950 assert res.json == {"x_field": [{"id": 42}]}
5051
5152 res = testapp.post_json("/echo_nested_many_data_key", {"X-Field": [{"id": 24}]})
1010
1111
1212 async def echo_parse(request):
13 parsed = await parser.parse(hello_args, request, location="query")
13 parsed = await parser.parse(hello_args, request)
1414 return json_response(parsed)
1515
1616
17 @use_args(hello_args, location="query")
17 @use_args(hello_args)
1818 async def echo_use_args(request, args):
1919 return json_response(args)
2020
2121
22 @use_kwargs(hello_args, location="query")
22 @use_kwargs(hello_args)
2323 async def echo_use_kwargs(request, name):
2424 return json_response({"name": name})
2525
0 # -*- coding: utf-8 -*-
01 from webargs.testing import CommonTestCase
12
23
0 # -*- coding: utf-8 -*-
1
2 from webargs.core import json
3
4 try:
5 from urllib.parse import urlencode
6 except ImportError: # PY2
7 from urllib import urlencode # type: ignore
8
9 import mock
10 import pytest
11
012 import marshmallow as ma
1 import pytest
13
14 import tornado.web
15 import tornado.httputil
16 import tornado.httpserver
17 import tornado.http1connection
218 import tornado.concurrent
3 import tornado.http1connection
4 import tornado.httpserver
5 import tornado.httputil
619 import tornado.ioloop
7 import tornado.web
820 from tornado.testing import AsyncHTTPTestCase
21
922 from webargs import fields, missing
10 from webargs.core import MARSHMALLOW_VERSION_INFO, json, parse_json
11 from webargs.tornadoparser import (
12 WebArgsTornadoMultiDictProxy,
13 parser,
14 use_args,
15 use_kwargs,
16 )
17
18 from urllib.parse import urlencode
19
20 try:
21 # Python 3.5
22 import mock
23 except ImportError:
24 # Python 3.6+
25 from unittest import mock
26
23 from webargs.tornadoparser import parser, use_args, use_kwargs, get_value
24 from webargs.core import parse_json
2725
2826 name = "name"
2927 value = "value"
3028
3129
32 class AuthorSchema(ma.Schema):
33 name = fields.Str(missing="World", validate=lambda n: len(n) >= 3)
34 works = fields.List(fields.Str())
35
36
37 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
38 author_schema = AuthorSchema(**strict_kwargs)
39
40
41 def test_tornado_multidictproxy():
42 for dictval, fieldname, expected in (
43 ({"name": "Sophocles"}, "name", "Sophocles"),
44 ({"name": "Sophocles"}, "works", missing),
45 ({"works": ["Antigone", "Oedipus Rex"]}, "works", ["Antigone", "Oedipus Rex"]),
46 ({"works": ["Antigone", "Oedipus at Colonus"]}, "name", missing),
47 ):
48 proxy = WebArgsTornadoMultiDictProxy(dictval, author_schema)
49 assert proxy.get(fieldname) == expected
50
51
52 class TestQueryArgs:
30 def test_get_value_basic():
31 field, multifield = fields.Field(), fields.List(fields.Str())
32 assert get_value({"foo": 42}, "foo", field) == 42
33 assert get_value({"foo": 42}, "bar", field) is missing
34 assert get_value({"foos": ["a", "b"]}, "foos", multifield) == ["a", "b"]
35 # https://github.com/marshmallow-code/webargs/pull/30
36 assert get_value({"foos": ["a", "b"]}, "bar", multifield) is missing
37
38
39 class TestQueryArgs(object):
40 def setup_method(self, method):
41 parser.clear_cache()
42
5343 def test_it_should_get_single_values(self):
54 query = [("name", "Aeschylus")]
44 query = [(name, value)]
45 field = fields.Field()
5546 request = make_get_request(query)
56 result = parser.load_querystring(request, author_schema)
57 assert result["name"] == "Aeschylus"
47
48 result = parser.parse_querystring(request, name, field)
49
50 assert result == value
5851
5952 def test_it_should_get_multiple_values(self):
60 query = [("works", "Agamemnon"), ("works", "Nereids")]
53 query = [(name, value), (name, value)]
54 field = fields.List(fields.Field())
6155 request = make_get_request(query)
62 result = parser.load_querystring(request, author_schema)
63 assert result["works"] == ["Agamemnon", "Nereids"]
56
57 result = parser.parse_querystring(request, name, field)
58
59 assert result == [value, value]
6460
6561 def test_it_should_return_missing_if_not_present(self):
6662 query = []
63 field = fields.Field()
64 field2 = fields.List(fields.Int())
6765 request = make_get_request(query)
68 result = parser.load_querystring(request, author_schema)
69 assert result["name"] is missing
70 assert result["works"] is missing
66
67 result = parser.parse_querystring(request, name, field)
68 result2 = parser.parse_querystring(request, name, field2)
69
70 assert result is missing
71 assert result2 is missing
72
73 def test_it_should_return_empty_list_if_multiple_and_not_present(self):
74 query = []
75 field = fields.List(fields.Field())
76 request = make_get_request(query)
77
78 result = parser.parse_querystring(request, name, field)
79
80 assert result is missing
7181
7282
7383 class TestFormArgs:
84 def setup_method(self, method):
85 parser.clear_cache()
86
7487 def test_it_should_get_single_values(self):
75 query = [("name", "Aristophanes")]
88 query = [(name, value)]
89 field = fields.Field()
7690 request = make_form_request(query)
77 result = parser.load_form(request, author_schema)
78 assert result["name"] == "Aristophanes"
91
92 result = parser.parse_form(request, name, field)
93
94 assert result == value
7995
8096 def test_it_should_get_multiple_values(self):
81 query = [("works", "The Wasps"), ("works", "The Frogs")]
97 query = [(name, value), (name, value)]
98 field = fields.List(fields.Field())
8299 request = make_form_request(query)
83 result = parser.load_form(request, author_schema)
84 assert result["works"] == ["The Wasps", "The Frogs"]
100
101 result = parser.parse_form(request, name, field)
102
103 assert result == [value, value]
85104
86105 def test_it_should_return_missing_if_not_present(self):
87106 query = []
107 field = fields.Field()
88108 request = make_form_request(query)
89 result = parser.load_form(request, author_schema)
90 assert result["name"] is missing
91 assert result["works"] is missing
92
93
94 class TestJSONArgs:
109
110 result = parser.parse_form(request, name, field)
111
112 assert result is missing
113
114 def test_it_should_return_empty_list_if_multiple_and_not_present(self):
115 query = []
116 field = fields.List(fields.Field())
117 request = make_form_request(query)
118
119 result = parser.parse_form(request, name, field)
120
121 assert result is missing
122
123
124 class TestJSONArgs(object):
125 def setup_method(self, method):
126 parser.clear_cache()
127
95128 def test_it_should_get_single_values(self):
96 query = {"name": "Euripides"}
129 query = {name: value}
130 field = fields.Field()
97131 request = make_json_request(query)
98 result = parser.load_json(request, author_schema)
99 assert result["name"] == "Euripides"
132 result = parser.parse_json(request, name, field)
133
134 assert result == value
100135
101136 def test_parsing_request_with_vendor_content_type(self):
102 query = {"name": "Euripides"}
137 query = {name: value}
138 field = fields.Field()
103139 request = make_json_request(
104140 query, content_type="application/vnd.api+json; charset=UTF-8"
105141 )
106 result = parser.load_json(request, author_schema)
107 assert result["name"] == "Euripides"
142 result = parser.parse_json(request, name, field)
143
144 assert result == value
108145
109146 def test_it_should_get_multiple_values(self):
110 query = {"works": ["Medea", "Electra"]}
147 query = {name: [value, value]}
148 field = fields.List(fields.Field())
111149 request = make_json_request(query)
112 result = parser.load_json(request, author_schema)
113 assert result["works"] == ["Medea", "Electra"]
150 result = parser.parse_json(request, name, field)
151
152 assert result == [value, value]
114153
115154 def test_it_should_get_multiple_nested_values(self):
116 class CustomSchema(ma.Schema):
117 works = fields.List(
118 fields.Nested({"author": fields.Str(), "workname": fields.Str()})
119 )
120
121 custom_schema = CustomSchema(**strict_kwargs)
122
123 query = {
124 "works": [
125 {"author": "Euripides", "workname": "Hecuba"},
126 {"author": "Aristophanes", "workname": "The Birds"},
127 ]
128 }
155 query = {name: [{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}]}
156 field = fields.List(
157 fields.Nested({"id": fields.Field(), "name": fields.Field()})
158 )
129159 request = make_json_request(query)
130 result = parser.load_json(request, custom_schema)
131 assert result["works"] == [
132 {"author": "Euripides", "workname": "Hecuba"},
133 {"author": "Aristophanes", "workname": "The Birds"},
134 ]
135
136 def test_it_should_not_include_fieldnames_if_not_present(self):
160 result = parser.parse_json(request, name, field)
161 assert result == [{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}]
162
163 def test_it_should_return_missing_if_not_present(self):
137164 query = {}
165 field = fields.Field()
138166 request = make_json_request(query)
139 result = parser.load_json(request, author_schema)
140 assert result == {}
141
142 def test_it_should_handle_type_error_on_load_json(self):
143 # but this is different from the test above where the payload was valid
144 # and empty -- missing vs {}
167 result = parser.parse_json(request, name, field)
168
169 assert result is missing
170
171 def test_it_should_return_empty_list_if_multiple_and_not_present(self):
172 query = {}
173 field = fields.List(fields.Field())
174 request = make_json_request(query)
175 result = parser.parse_json(request, name, field)
176
177 assert result is missing
178
179 def test_it_should_handle_type_error_on_parse_json(self):
180 field = fields.Field()
145181 request = make_request(
146 body=tornado.concurrent.Future(),
147 headers={"Content-Type": "application/json"},
148 )
149 result = parser.load_json(request, author_schema)
182 body=tornado.concurrent.Future, headers={"Content-Type": "application/json"}
183 )
184 result = parser.parse_json(request, name, field)
185 assert parser._cache["json"] == {}
150186 assert result is missing
151187
152188 def test_it_should_handle_value_error_on_parse_json(self):
189 field = fields.Field()
153190 request = make_request("this is json not")
154 result = parser.load_json(request, author_schema)
155 assert result is missing
156
157
158 class TestHeadersArgs:
191 result = parser.parse_json(request, name, field)
192 assert parser._cache["json"] == {}
193 assert result is missing
194
195
196 class TestHeadersArgs(object):
197 def setup_method(self, method):
198 parser.clear_cache()
199
159200 def test_it_should_get_single_values(self):
160 query = {"name": "Euphorion"}
201 query = {name: value}
202 field = fields.Field()
161203 request = make_request(headers=query)
162 result = parser.load_headers(request, author_schema)
163 assert result["name"] == "Euphorion"
204
205 result = parser.parse_headers(request, name, field)
206
207 assert result == value
164208
165209 def test_it_should_get_multiple_values(self):
166 query = {"works": ["Prometheus Bound", "Prometheus Unbound"]}
210 query = {name: [value, value]}
211 field = fields.List(fields.Field())
167212 request = make_request(headers=query)
168 result = parser.load_headers(request, author_schema)
169 assert result["works"] == ["Prometheus Bound", "Prometheus Unbound"]
213
214 result = parser.parse_headers(request, name, field)
215
216 assert result == [value, value]
170217
171218 def test_it_should_return_missing_if_not_present(self):
219 field = fields.Field(multiple=False)
172220 request = make_request()
173 result = parser.load_headers(request, author_schema)
174 assert result["name"] is missing
175 assert result["works"] is missing
176
177
178 class TestFilesArgs:
221
222 result = parser.parse_headers(request, name, field)
223
224 assert result is missing
225
226 def test_it_should_return_empty_list_if_multiple_and_not_present(self):
227 query = {}
228 field = fields.List(fields.Field())
229 request = make_request(headers=query)
230
231 result = parser.parse_headers(request, name, field)
232
233 assert result is missing
234
235
236 class TestFilesArgs(object):
237 def setup_method(self, method):
238 parser.clear_cache()
239
179240 def test_it_should_get_single_values(self):
180 query = [("name", "Sappho")]
241 query = [(name, value)]
242 field = fields.Field()
181243 request = make_files_request(query)
182 result = parser.load_files(request, author_schema)
183 assert result["name"] == "Sappho"
244
245 result = parser.parse_files(request, name, field)
246
247 assert result == value
184248
185249 def test_it_should_get_multiple_values(self):
186 query = [("works", "Sappho 31"), ("works", "Ode to Aphrodite")]
250 query = [(name, value), (name, value)]
251 field = fields.List(fields.Field())
187252 request = make_files_request(query)
188 result = parser.load_files(request, author_schema)
189 assert result["works"] == ["Sappho 31", "Ode to Aphrodite"]
253
254 result = parser.parse_files(request, name, field)
255
256 assert result == [value, value]
190257
191258 def test_it_should_return_missing_if_not_present(self):
192259 query = []
260 field = fields.Field()
193261 request = make_files_request(query)
194 result = parser.load_files(request, author_schema)
195 assert result["name"] is missing
196 assert result["works"] is missing
197
198
199 class TestErrorHandler:
262
263 result = parser.parse_files(request, name, field)
264
265 assert result is missing
266
267 def test_it_should_return_empty_list_if_multiple_and_not_present(self):
268 query = []
269 field = fields.List(fields.Field())
270 request = make_files_request(query)
271
272 result = parser.parse_files(request, name, field)
273
274 assert result is missing
275
276
277 class TestErrorHandler(object):
200278 def test_it_should_raise_httperror_on_failed_validation(self):
201279 args = {"foo": fields.Field(validate=lambda x: False)}
202280 with pytest.raises(tornado.web.HTTPError):
203281 parser.parse(args, make_json_request({"foo": 42}))
204282
205283
206 class TestParse:
284 class TestParse(object):
285 def setup_method(self, method):
286 parser.clear_cache()
287
207288 def test_it_should_parse_query_arguments(self):
208289 attrs = {"string": fields.Field(), "integer": fields.List(fields.Int())}
209290
211292 [("string", "value"), ("integer", "1"), ("integer", "2")]
212293 )
213294
214 parsed = parser.parse(attrs, request, location="query")
295 parsed = parser.parse(attrs, request)
215296
216297 assert parsed["integer"] == [1, 2]
217298 assert parsed["string"] == value
223304 [("string", "value"), ("integer", "1"), ("integer", "2")]
224305 )
225306
226 parsed = parser.parse(attrs, request, location="form")
307 parsed = parser.parse(attrs, request)
227308
228309 assert parsed["integer"] == [1, 2]
229310 assert parsed["string"] == value
255336
256337 request = make_request(headers={"string": "value", "integer": ["1", "2"]})
257338
258 parsed = parser.parse(attrs, request, location="headers")
339 parsed = parser.parse(attrs, request, locations=["headers"])
259340
260341 assert parsed["string"] == value
261342 assert parsed["integer"] == [1, 2]
267348 [("string", "value"), ("integer", "1"), ("integer", "2")]
268349 )
269350
270 parsed = parser.parse(attrs, request, location="cookies")
351 parsed = parser.parse(attrs, request, locations=["cookies"])
271352
272353 assert parsed["string"] == value
273354 assert parsed["integer"] == [2]
279360 [("string", "value"), ("integer", "1"), ("integer", "2")]
280361 )
281362
282 parsed = parser.parse(attrs, request, location="files")
363 parsed = parser.parse(attrs, request, locations=["files"])
283364
284365 assert parsed["string"] == value
285366 assert parsed["integer"] == [1, 2]
301382 parser.parse(args, request)
302383
303384
304 class TestUseArgs:
385 class TestUseArgs(object):
386 def setup_method(self, method):
387 parser.clear_cache()
388
305389 def test_it_should_pass_parsed_as_first_argument(self):
306 class Handler:
390 class Handler(object):
307391 request = make_json_request({"key": "value"})
308392
309393 @use_args({"key": fields.Field()})
318402 assert result is True
319403
320404 def test_it_should_pass_parsed_as_kwargs_arguments(self):
321 class Handler:
405 class Handler(object):
322406 request = make_json_request({"key": "value"})
323407
324408 @use_kwargs({"key": fields.Field()})
333417 assert result is True
334418
335419 def test_it_should_be_validate_arguments_when_validator_is_passed(self):
336 class Handler:
420 class Handler(object):
337421 request = make_json_request({"foo": 41})
338422
339423 @use_kwargs({"foo": fields.Int()}, validate=lambda args: args["foo"] > 42)
391475
392476
393477 def make_request(uri=None, body=None, headers=None, files=None):
394 uri = uri if uri is not None else ""
395 body = body if body is not None else ""
478 uri = uri if uri is not None else u""
479 body = body if body is not None else u""
396480 method = "POST" if body else "GET"
397481 # Need to make a mock connection right now because Tornado 4.0 requires a
398482 # remote_ip in the context attribute. 4.1 addresses this, and this
401485 mock_connection = mock.Mock(spec=tornado.http1connection.HTTP1Connection)
402486 mock_connection.context = mock.Mock()
403487 mock_connection.remote_ip = None
404 content_type = headers.get("Content-Type", "") if headers else ""
488 content_type = headers.get("Content-Type", u"") if headers else u""
405489 request = tornado.httputil.HTTPServerRequest(
406490 method=method,
407491 uri=uri,
424508 class EchoHandler(tornado.web.RequestHandler):
425509 ARGS = {"name": fields.Str()}
426510
427 @use_args(ARGS, location="query")
511 @use_args(ARGS)
428512 def get(self, args):
429513 self.write(args)
430
431
432 class EchoFormHandler(tornado.web.RequestHandler):
433 ARGS = {"name": fields.Str()}
434
435 @use_args(ARGS, location="form")
436 def post(self, args):
437 self.write(args)
438
439
440 class EchoJSONHandler(tornado.web.RequestHandler):
441 ARGS = {"name": fields.Str()}
442514
443515 @use_args(ARGS)
444516 def post(self, args):
448520 class EchoWithParamHandler(tornado.web.RequestHandler):
449521 ARGS = {"name": fields.Str()}
450522
451 @use_args(ARGS, location="query")
523 @use_args(ARGS)
452524 def get(self, id, args):
453525 self.write(args)
454526
455527
456528 echo_app = tornado.web.Application(
457 [
458 (r"/echo", EchoHandler),
459 (r"/echo_form", EchoFormHandler),
460 (r"/echo_json", EchoJSONHandler),
461 (r"/echo_with_param/(\d+)", EchoWithParamHandler),
462 ]
529 [(r"/echo", EchoHandler), (r"/echo_with_param/(\d+)", EchoWithParamHandler)]
463530 )
464531
465532
469536
470537 def test_post(self):
471538 res = self.fetch(
472 "/echo_json",
539 "/echo",
473540 method="POST",
474541 headers={"Content-Type": "application/json"},
475542 body=json.dumps({"name": "Steve"}),
477544 json_body = parse_json(res.body)
478545 assert json_body["name"] == "Steve"
479546 res = self.fetch(
480 "/echo_json",
547 "/echo",
481548 method="POST",
482549 headers={"Content-Type": "application/json"},
483550 body=json.dumps({}),
509576 def post(self, args):
510577 self.write(args)
511578
512 @use_kwargs(ARGS, location="query")
579 @use_kwargs(ARGS)
513580 def get(self, name):
514581 self.write({"status": "success"})
515582
0 # -*- coding: utf-8 -*-
01 """Tests for the webapp2 parser"""
1 from urllib.parse import urlencode
2 try:
3 from urllib.parse import urlencode
4 except ImportError: # PY2
5 from urllib import urlencode # type: ignore
26 from webargs.core import json
37
48 import pytest
5 import marshmallow as ma
69 from marshmallow import fields, ValidationError
710
811 import webtest
912 import webapp2
1013 from webargs.webapp2parser import parser
11 from webargs.core import MARSHMALLOW_VERSION_INFO
1214
1315 hello_args = {"name": fields.Str(missing="World")}
1416
2224 }
2325
2426
25 class HelloSchema(ma.Schema):
26 name = fields.Str(missing="World", validate=lambda n: len(n) >= 3)
27
28
29 # variant which ignores unknown fields
30 exclude_kwargs = (
31 {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE}
32 )
33 hello_exclude_schema = HelloSchema(**exclude_kwargs)
34
35
3627 def test_parse_querystring_args():
3728 request = webapp2.Request.blank("/echo?name=Fred")
38 assert parser.parse(hello_args, req=request, location="query") == {"name": "Fred"}
29 assert parser.parse(hello_args, req=request) == {"name": "Fred"}
3930
4031
4132 def test_parse_querystring_multiple():
4233 expected = {"name": ["steve", "Loria"]}
4334 request = webapp2.Request.blank("/echomulti?name=steve&name=Loria")
44 assert parser.parse(hello_multiple, req=request, location="query") == expected
35 assert parser.parse(hello_multiple, req=request) == expected
4536
4637
4738 def test_parse_form():
4839 expected = {"name": "Joe"}
4940 request = webapp2.Request.blank("/echo", POST=expected)
50 assert parser.parse(hello_args, req=request, location="form") == expected
41 assert parser.parse(hello_args, req=request) == expected
5142
5243
5344 def test_parse_form_multiple():
5445 expected = {"name": ["steve", "Loria"]}
5546 request = webapp2.Request.blank("/echo", POST=urlencode(expected, doseq=True))
56 assert parser.parse(hello_multiple, req=request, location="form") == expected
47 assert parser.parse(hello_multiple, req=request) == expected
5748
5849
5950 def test_parsing_form_default():
6051 request = webapp2.Request.blank("/echo", POST="")
61 assert parser.parse(hello_args, req=request, location="form") == {"name": "World"}
52 assert parser.parse(hello_args, req=request) == {"name": "World"}
6253
6354
6455 def test_parse_json():
112103 request = webapp2.Request.blank(
113104 "/", headers={"Cookie": response.headers["Set-Cookie"]}
114105 )
115 assert parser.parse(hello_args, req=request, location="cookies") == expected
106 assert parser.parse(hello_args, req=request, locations=("cookies",)) == expected
116107
117108
118109 def test_parsing_headers():
119110 expected = {"name": "Fred"}
120111 request = webapp2.Request.blank("/", headers=expected)
121 assert (
122 parser.parse(hello_exclude_schema, req=request, location="headers") == expected
123 )
112 assert parser.parse(hello_args, req=request, locations=("headers",)) == expected
124113
125114
126115 def test_parse_files():
129118 """
130119
131120 class Handler(webapp2.RequestHandler):
132 @parser.use_args({"myfile": fields.List(fields.Field())}, location="files")
121 @parser.use_args({"myfile": fields.List(fields.Field())}, locations=("files",))
133122 def post(self, args):
134123 self.response.content_type = "application/json"
135124
136125 def _value(f):
137126 return f.getvalue().decode("utf-8")
138127
139 data = {i.filename: _value(i.file) for i in args["myfile"]}
128 data = dict((i.filename, _value(i.file)) for i in args["myfile"])
140129 self.response.write(json.dumps(data))
141130
142131 app = webapp2.WSGIApplication([("/", Handler)])
149138 def test_exception_on_validation_error():
150139 request = webapp2.Request.blank("/", POST={"num": "3"})
151140 with pytest.raises(ValidationError):
152 parser.parse(hello_validate, req=request, location="form")
141 parser.parse(hello_validate, req=request)
153142
154143
155144 def test_validation_error_with_message():
156145 request = webapp2.Request.blank("/", POST={"num": "3"})
157146 with pytest.raises(ValidationError) as exc:
158 parser.parse(hello_validate, req=request, location="form")
147 parser.parse(hello_validate, req=request)
159148 assert "Houston, we've had a problem." in exc.value
160149
161150
167156 request = webapp2.Request.blank("/echo", POST=expected)
168157 app = webapp2.WSGIApplication([])
169158 app.set_globals(app, request)
170 assert parser.parse(hello_args, location="form") == expected
159 assert parser.parse(hello_args) == expected
00 [tox]
11 envlist=
22 lint
3 py{35,36,37,38}-marshmallow2
4 py{35,36,37,38}-marshmallow3
5 py38-marshmallowdev
3 py{27,35,36,37}-marshmallow2
4 py{35,36,37}-marshmallow3
5 py37-marshmallowdev
66 docs
77
88 [testenv]
1111 marshmallow2: marshmallow==2.15.2
1212 marshmallow3: marshmallow>=3.0.0rc2,<4.0.0
1313 marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz
14 commands = pytest {posargs}
14 commands =
15 py27: pytest --ignore=tests/test_py3/ {posargs}
16 py{35,36,37}: pytest {posargs}
1517
1618 [testenv:lint]
17 deps = pre-commit~=1.20
19 deps = pre-commit~=1.17
1820 skip_install = true
1921 commands = pre-commit run --all-files
2022