Codebase list python-webargs / ca5f6358-14a2-4932-82c3-43c79096fa6e/upstream
Import upstream version 6.1.1 Kali Janitor 3 years ago
66 changed file(s) with 2885 addition(s) and 1903 deletion(s). Raw diff Collapse all Expand all
00 repos:
1 - repo: https://github.com/python/black
2 rev: 19.3b0
1 - repo: https://github.com/asottile/pyupgrade
2 rev: v2.7.2
3 hooks:
4 - id: pyupgrade
5 args: ["--py3-plus", "--keep-mock"]
6 - repo: https://github.com/psf/black
7 rev: 20.8b1
38 hooks:
49 - id: black
5 language_version: python3
610 - repo: https://gitlab.com/pycqa/flake8
7 rev: 3.7.8
11 rev: 3.8.3
812 hooks:
913 - id: flake8
10 additional_dependencies: ['flake8-bugbear==19.8.0; python_version >= "3.5"']
14 additional_dependencies: [flake8-bugbear==20.1.0]
1115 - repo: https://github.com/asottile/blacken-docs
12 rev: v1.3.0
16 rev: v1.8.0
1317 hooks:
1418 - id: blacken-docs
15 additional_dependencies: [black==19.3b0]
19 additional_dependencies: [black==20.8b1]
20 args: ["--target-version", "py35"]
1621 - repo: https://github.com/pre-commit/mirrors-mypy
17 rev: v0.730
22 rev: v0.782
1823 hooks:
1924 - id: mypy
2025 language_version: python3
22 configuration: docs/conf.py
33 formats: all
44 python:
5 version: 3.7
5 version: 3.8
66 install:
77 - method: pip
88 path: .
44 Lead
55 ----
66
7 * Steven Loria <[email protected]>
8 * Jérôme Lafréchoux <https://github.com/lafrech>
7 * Steven Loria `@sloria <https://github.com/sloria>`_
8 * Jérôme Lafréchoux `@lafrech <https://github.com/lafrech>`_
99
1010 Contributors (chronological)
1111 ----------------------------
1212
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>
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>`_
52 * Jason Williams `@jaswilli <https://github.com/jaswilli>`_
00 Changelog
11 ---------
22
3 6.1.1 (2020-09-08)
4 ******************
5
6 Bug fixes:
7
8 * Failure to validate flask headers would produce error data which contained
9 tuples as keys, and was therefore not JSON-serializable. (:issue:`500`)
10 These errors will now extract the headername as the key correctly.
11 Thanks to :user:`shughes-uk` for reporting.
12
13 6.1.0 (2020-04-05)
14 ******************
15
16 Features:
17
18 * Add ``fields.DelimitedTuple`` when using marshmallow 3. This behaves as a
19 combination of ``fields.DelimitedList`` and ``marshmallow.fields.Tuple``. It
20 takes an iterable of fields, plus a delimiter (defaults to ``,``), and parses
21 delimiter-separated strings into tuples. (:pr:`509`)
22
23 * Add ``__str__`` and ``__repr__`` to MultiDictProxy to make it easier to work
24 with (:pr:`488`)
25
26 Support:
27
28 * Various docs updates (:pr:`482`, :pr:`486`, :pr:`489`, :pr:`498`, :pr:`508`).
29 Thanks :user:`lefterisjp`, :user:`timgates42`, and :user:`ugultopu` for the PRs.
30
31
32 6.0.0 (2020-02-27)
33 ******************
34
35 Features:
36
37 * ``FalconParser``: Pass request content length to ``req.stream.read`` to
38 provide compatibility with ``falcon.testing`` (:pr:`477`).
39 Thanks :user:`suola` for the PR.
40
41 * *Backwards-incompatible*: Factorize the ``use_args`` / ``use_kwargs`` branch
42 in all parsers. When ``as_kwargs`` is ``False``, arguments are now
43 consistently appended to the arguments list by the ``use_args`` decorator.
44 Before this change, the ``PyramidParser`` would prepend the argument list on
45 each call to ``use_args``. Pyramid view functions must reverse the order of
46 their arguments. (:pr:`478`)
47
48 6.0.0b8 (2020-02-16)
49 ********************
50
51 Refactoring:
52
53 * *Backwards-incompatible*: Use keyword-only arguments (:pr:`472`).
54
55 6.0.0b7 (2020-02-14)
56 ********************
57
58 Features:
59
60 * *Backwards-incompatible*: webargs will rewrite the error messages in
61 ValidationErrors to be namespaced under the location which raised the error.
62 The `messages` field on errors will therefore be one layer deeper with a
63 single top-level key.
64
65 6.0.0b6 (2020-01-31)
66 ********************
67
68 Refactoring:
69
70 * Remove the cache attached to webargs parsers. Due to changes between webargs
71 v5 and v6, the cache is no longer considered useful.
72
73 Other changes:
74
75 * Import ``Mapping`` from ``collections.abc`` in pyramidparser.py (:pr:`471`).
76 Thanks :user:`tirkarthi` for the PR.
77
78 6.0.0b5 (2020-01-30)
79 ********************
80
81 Refactoring:
82
83 * *Backwards-incompatible*: `DelimitedList` now requires that its input be a
84 string and always serializes as a string. It can still serialize and deserialize
85 using another field, e.g. `DelimitedList(Int())` is still valid and requires
86 that the values in the list parse as ints.
87
88 6.0.0b4 (2020-01-28)
89 ********************
90
91 Bug fixes:
92
93 * :cve:`CVE-2020-7965`: Don't attempt to parse JSON if request's content type is mismatched
94 (bugfix from 5.5.3).
95
96 6.0.0b3 (2020-01-21)
97 ********************
98
99 Features:
100
101 * *Backwards-incompatible*: Support Falcon 2.0. Drop support for Falcon 1.x
102 (:pr:`459`). Thanks :user:`dodumosu` and :user:`Nateyo` for the PR.
103
104 6.0.0b2 (2020-01-07)
105 ********************
106
107 Other changes:
108
109 * *Backwards-incompatible*: Drop support for Python 2 (:issue:`440`).
110 Thanks :user:`hugovk` for the PR.
111
112 6.0.0b1 (2020-01-06)
113 ********************
114
115 Features:
116
117 * *Backwards-incompatible*: Schemas will now load all data from a location, not
118 only data specified by fields. As a result, schemas with validators which
119 examine the full input data may change in behavior. The `unknown` parameter
120 on schemas may be used to alter this. For example,
121 `unknown=marshmallow.EXCLUDE` will produce a behavior similar to webargs v5.
122
123 Bug fixes:
124
125 * *Backwards-incompatible*: All parsers now require the Content-Type to be set
126 correctly when processing JSON request bodies. This impacts ``DjangoParser``,
127 ``FalconParser``, ``FlaskParser``, and ``PyramidParser``
128
129 Refactoring:
130
131 * *Backwards-incompatible*: Schema fields may not specify a location any
132 longer, and `Parser.use_args` and `Parser.use_kwargs` now accept `location`
133 (singular) instead of `locations` (plural). Instead of using a single field or
134 schema with multiple `locations`, users are recommended to make multiple
135 calls to `use_args` or `use_kwargs` with a distinct schema per location. For
136 example, code should be rewritten like this:
137
138 .. code-block:: python
139
140 # webargs 5.x and older
141 @parser.use_args(
142 {
143 "q1": ma.fields.Int(location="query"),
144 "q2": ma.fields.Int(location="query"),
145 "h1": ma.fields.Int(location="headers"),
146 },
147 locations=("query", "headers"),
148 )
149 def foo(q1, q2, h1):
150 ...
151
152
153 # webargs 6.x
154 @parser.use_args({"q1": ma.fields.Int(), "q2": ma.fields.Int()}, location="query")
155 @parser.use_args({"h1": ma.fields.Int()}, location="headers")
156 def foo(q1, q2, h1):
157 ...
158
159 * The `location_handler` decorator has been removed and replaced with
160 `location_loader`. `location_loader` serves the same purpose (letting you
161 write custom hooks for loading data) but its expected method signature is
162 different. See the docs on `location_loader` for proper usage.
163
164 Thanks :user:`sirosen` for the PR!
165
3166 5.5.3 (2020-01-28)
4167 ******************
5168
6169 Bug fixes:
7170
8 * :cve:`CVE-2020-7965`: Don't attempt to parse JSON if the request's Content-Type is mismatched.
171 * :cve:`CVE-2020-7965`: Don't attempt to parse JSON if request's content type is mismatched.
9172
10173 5.5.2 (2019-10-06)
11174 ******************
121121 Documentation
122122 +++++++++++++
123123
124 Contributions to the documentation are welcome. Documentation is written in `reStructured Text`_ (rST). A quick rST reference can be found `here <http://docutils.sourceforge.net/docs/user/rst/quickref.html>`_. Builds are powered by Sphinx_.
124 Contributions to the documentation are welcome. Documentation is written in `reStructuredText`_ (rST). A quick rST reference can be found `here <https://docutils.sourceforge.io/docs/user/rst/quickref.html>`_. Builds are powered by Sphinx_.
125125
126126 To build the docs in "watch" mode: ::
127127
136136
137137
138138 .. _Sphinx: http://sphinx.pocoo.org/
139 .. _`reStructured Text`: http://docutils.sourceforge.net/rst.html
139 .. _`reStructuredText`: https://docutils.sourceforge.io/rst.html
140140 .. _webargs: https://github.com/marshmallow-code/webargs
0 Copyright 2014-2019 Steven Loria and contributors
0 Copyright 2014-2020 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
01 include LICENSE
12 include *.rst
3 include tox.ini
3535
3636
3737 @app.route("/")
38 @use_args({"name": fields.Str(required=True)})
38 @use_args({"name": fields.Str(required=True)}, location="query")
3939 def index(args):
4040 return "Hello " + args["name"]
4141
5353
5454 pip install -U webargs
5555
56 webargs supports Python >= 2.7 or >= 3.5.
56 webargs supports Python >= 3.5.
5757
5858
5959 Documentation
2525 parameters:
2626 toxenvs:
2727 - lint
28 - py27-marshmallow2
2928
3029 - py35-marshmallow2
3130 - py35-marshmallow3
3231
33 - py36-marshmallow2
3432 - py36-marshmallow3
3533
36 - py37-marshmallow2
3734 - py37-marshmallow3
3835
39 - py37-marshmallowdev
36 - py38-marshmallow2
37 - py38-marshmallow3
38
39 - py38-marshmallowdev
4040
4141 - docs
4242 os: linux
43 # Build separate wheels for python 2 and 3
43 # Build wheels
4444 - template: job--pypi-release.yml@sloria
4545 parameters:
4646 python: "3.7"
4747 distributions: "sdist bdist_wheel"
48 name_postfix: "_py3"
4948 dependsOn:
5049 - 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, an argument name, and a :class:`Field <marshmallow.fields.Field>`, then decorate that function with :func:`Parser.location_handler <webargs.core.Parser.location_handler>`.
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>`.
99
1010
1111 .. code-block:: python
1414 from webargs.flaskparser import parser
1515
1616
17 @parser.location_handler("data")
18 def parse_data(request, name, field):
19 return request.data.get(name)
17 @parser.location_loader("data")
18 def load_data(request, schema):
19 return request.data
2020
2121
2222 # Now 'data' can be specified as a location
23 @parser.use_args({"per_page": fields.Int()}, locations=("data",))
23 @parser.use_args({"per_page": fields.Int()}, location="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"])
2788
2889 marshmallow Integration
2990 -----------------------
63124
64125
65126 # You can add additional parameters
66 @use_kwargs({"posts_per_page": fields.Int(missing=10, location="query")})
127 @use_kwargs({"posts_per_page": fields.Int(missing=10)}, location="query")
67128 @use_args(UserSchema())
68129 def profile_posts(args, posts_per_page):
69130 username = args["username"]
72133 .. warning::
73134 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.
74135
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.
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.
77172
78173
79174 Schema Factories
176271 cube = args["cube"]
177272 # ...
178273
179 .. _custom-parsers:
274 .. _custom-loaders:
180275
181276 Custom Parsers
182277 --------------
183278
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.
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.
185280
186281
187282 .. code-block:: python
210305 }
211306 """
212307
213 def parse_querystring(self, req, name, field):
214 return core.get_value(_structure_dict(req.args), name, field)
308 def load_querystring(self, req, schema):
309 return _structure_dict(req.args)
215310
216311
217312 def _structure_dict(dict_):
274369
275370
276371 @app.route("/profile/", methods=["patch"])
277 @use_args(PatchSchema(many=True), locations=("json",))
372 @use_args(PatchSchema(many=True))
278373 def patch_blog(args):
279374 """Implements JSON Patch for the user profile
280375
289384 Mixing Locations
290385 ----------------
291386
292 Arguments for different locations can be specified by passing ``location`` to each field individually:
293
294 .. code-block:: python
295
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
296392 @app.route("/stacked", methods=["POST"])
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",))
393 @use_args({"page": fields.Int(), "q": fields.Str()}, location="query")
394 @use_args({"name": fields.Str()}, location="json")
334395 def viewfunc(query_parsed, json_parsed):
335396 page = query_parsed["page"]
336397 name = json_parsed["name"]
342403
343404 import functools
344405
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)
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()})
351412 def viewfunc(query_parsed, json_parsed):
352413 page = query_parsed["page"]
353414 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
1725
1826 webargs.asyncparser
1927 -------------------
0 # -*- coding: utf-8 -*-
10 import datetime as dt
21 import sys
32 import os
3635
3736 html_domain_indices = False
3837 source_suffix = ".rst"
39 project = u"webargs"
40 copyright = u"2014-{0:%Y}, Steven Loria and contributors".format(dt.datetime.utcnow())
38 project = "webargs"
39 copyright = "2014-{:%Y}, Steven Loria and contributors".format(dt.datetime.utcnow())
4140 version = release = webargs.__version__
4241 templates_path = ["_templates"]
4342 exclude_patterns = ["_build"]
2121
2222
2323 @app.route("/user/<int:uid>")
24 @use_args({"per_page": fields.Int()})
24 @use_args({"per_page": fields.Int()}, location="query")
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)
97 @use_args(account_args, location="form")
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)
116 @use_args(blog_args, location="query")
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()})
241 @use_args({"uid": fields.Str(), "per_page": fields.Int()}, location="query")
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()}, locations=("matchdict",))
263 @use_args({"mymatch": fields.Int()}, location="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, params):
312 def hook(req, resp, resource, 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
442442
443443 @route("/users/<_id:int>", method="GET", apply=use_args(user_args))
444444 def users(args, _id):
445 """A welcome page.
446 """
445 """A welcome page."""
447446 return {"message": "Welcome, {}!".format(args["name"]), "_id": _id}
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 -------------------------
816
917 .. code-block:: python
1018
1624
1725
1826 @app.route("/")
19 @use_args({"name": fields.Str(required=True)})
27 @use_args({"name": fields.Str(required=True)}, location="query")
2028 def index(args):
2129 return "Hello " + args["name"]
2230
2735 # curl http://localhost:5000/\?name\='World'
2836 # Hello World
2937
30 Webargs will automatically parse:
38 By default Webargs will automatically parse JSON request bodies. But it also
39 has support for:
3140
3241 **Query Parameters**
3342 ::
43 $ curl http://localhost:5000/\?name\='Freddie'
44 Hello Freddie
3445
35 $ curl http://localhost:5000/\?name\='Freddie'
36 Hello Freddie
46 # pass location="query" to use_args
3747
3848 **Form Data**
3949 ::
4151 $ curl -d 'name=Brian' http://localhost:5000/
4252 Hello Brian
4353
54 # pass location="form" to use_args
55
4456 **JSON Data**
4557 ::
4658
4759 $ curl -X POST -H "Content-Type: application/json" -d '{"name":"Roger"}' http://localhost:5000/
4860 Hello Roger
61
62 # pass location="json" (or omit location) to use_args
4963
5064 and, optionally:
5165
102116
103117 license
104118 changelog
119 upgrading
105120 authors
106121 contributing
00 Install
11 =======
22
3 **webargs** requires Python >= 2.7 or >= 3.5. It depends on `marshmallow <https://marshmallow.readthedocs.io/en/latest/>`_ >= 2.7.0.
3 **webargs** requires Python >= 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"),
2725 # When value is keyed on a variable-unsafe name
2826 # or you want to rename a key
29 "content_type": fields.Str(load_from="Content-Type", location="headers"),
27 "user_type": fields.Str(load_from="user-type"),
3028 # OR, on marshmallow 3
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 ),
29 # "user_type": fields.Str(data_key="user-type"),
3630 }
3731
3832 .. note::
10498 Request "Locations"
10599 -------------------
106100
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:
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:
108102
109103 .. code-block:: python
110104
111105 @app.route("/register")
112 @use_args(user_args, locations=("json", "form"))
106 @use_args(user_args, location="form")
113107 def register(args):
114108 return "registration page"
115109
201195
202196
203197 @parser.error_handler
204 def handle_error(error, req, schema, status_code, headers):
198 def handle_error(error, req, schema, *, error_status_code, error_headers):
205199 raise CustomError(error.messages)
206200
207201 Parsing Lists in Query Strings
242236
243237 .. note::
244238
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.
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.
246240
247241 Next Steps
248242 ----------
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 Custom Error Handler Argument Names Changed
311 -------------------------------------------
312
313 If you define a custom error handler via `@parser.error_handler` the function
314 arguments are now keyword-only and `status_code` and `headers` have been renamed
315 `error_status_code` and `error_headers`.
316
317 .. code-block:: python
318
319 # webargs 5.x
320 @parser.error_handler
321 def custom_handle_error(error, req, schema, status_code, headers):
322 ...
323
324
325 # webargs 6.x
326 @parser.error_handler
327 def custom_handle_error(error, req, schema, *, error_status_code, error_headers):
328 ...
329
330
331 Some Functions Take Keyword-Only Arguments Now
332 ----------------------------------------------
333
334 The signature of several methods has changed to have keyword-only arguments.
335 For the most part, this should not require any changes, but here's a list of
336 the changes.
337
338 `parser.error_handler` methods:
339
340 .. code-block:: python
341
342 # webargs 5.x
343 def handle_error(error, req, schema, status_code, headers):
344 ...
345
346
347 # webargs 6.x
348 def handle_error(error, req, schema, *, error_status_code, error_headers):
349 ...
350
351 `parser.__init__` methods:
352
353 .. code-block:: python
354
355 # webargs 5.x
356 def __init__(self, location=None, error_handler=None, schema_class=None):
357 ...
358
359
360 # webargs 6.x
361 def __init__(self, location=None, *, error_handler=None, schema_class=None):
362 ...
363
364 `parser.parse`, `parser.use_args`, and `parser.use_kwargs` methods:
365
366
367 .. code-block:: python
368
369 # webargs 5.x
370 def parse(
371 self,
372 argmap,
373 req=None,
374 location=None,
375 validate=None,
376 error_status_code=None,
377 error_headers=None,
378 ):
379 ...
380
381
382 # webargs 6.x
383 def parse(
384 self,
385 argmap,
386 req=None,
387 *,
388 location=None,
389 validate=None,
390 error_status_code=None,
391 error_headers=None
392 ):
393 ...
394
395
396 # webargs 5.x
397 def use_args(
398 self,
399 argmap,
400 req=None,
401 location=None,
402 as_kwargs=False,
403 validate=None,
404 error_status_code=None,
405 error_headers=None,
406 ):
407 ...
408
409
410 # webargs 6.x
411 def use_args(
412 self,
413 argmap,
414 req=None,
415 *,
416 location=None,
417 as_kwargs=False,
418 validate=None,
419 error_status_code=None,
420 error_headers=None
421 ):
422 ...
423
424
425 # use_kwargs is just an alias for use_args with as_kwargs=True
426
427 and finally, the `dict2schema` function:
428
429 .. code-block:: python
430
431 # webargs 5.x
432 def dict2schema(dct, schema_class=ma.Schema):
433 ...
434
435
436 # webargs 6.x
437 def dict2schema(dct, *, schema_class=ma.Schema):
438 ...
439
440
441 PyramidParser Now Appends Arguments (Used To Prepend)
442 -----------------------------------------------------
443
444 `PyramidParser.use_args` was not conformant with the other parsers in webargs.
445 While all other parsers added new arguments to the end of the argument list of
446 a decorated view function, the Pyramid implementation added them to the front
447 of the argument list.
448
449 This has been corrected, but as a result pyramid views with `use_args` may need
450 to be rewritten. The `request` object is always passed first in both versions,
451 so the issue is only apparent with view functions taking other positional
452 arguments.
453
454 For example, imagine code with a decorator for passing user information,
455 `pass_userinfo`, like so:
456
457 .. code-block:: python
458
459 # a decorator which gets information about the authenticated user
460 def pass_userinfo(f):
461 def decorator(request, *args, **kwargs):
462 return f(request, get_userinfo(), *args, **kwargs)
463
464 return decorator
465
466 You will see a behavioral change if `pass_userinfo` is called on a function
467 decorated with `use_args`. The difference between the two versions will be like
468 so:
469
470 .. code-block:: python
471
472 from webargs.pyramidparser import use_args
473
474 # webargs 5.x
475 # pass_userinfo is called first, webargs sees positional arguments of
476 # (userinfo,)
477 # and changes it to
478 # (request, args, userinfo)
479 @pass_userinfo
480 @use_args({"q": ma.fields.String()}, locations=("query",))
481 def viewfunc(request, args, userinfo):
482 q = args.get("q")
483 ...
484
485
486 # webargs 6.x
487 # pass_userinfo is called first, webargs sees positional arguments of
488 # (userinfo,)
489 # and changes it to
490 # (request, userinfo, args)
491 @pass_userinfo
492 @use_args({"q": ma.fields.String()}, location="query")
493 def viewfunc(request, userinfo, args):
494 q = args.get("q")
495 ...
2424
2525 @use_args(hello_args)
2626 async def index(request, args):
27 """A welcome page.
28 """
27 """A welcome page."""
2928 return json_response({"message": "Welcome, {}!".format(args["name"])})
3029
3130
9595
9696
9797 @route("/", methods=["GET"])
98 def index(name: fields.Str(missing="Friend")):
98 def index(name: fields.Str(missing="Friend")): # noqa: F821
9999 return {"message": "Hello, {}!".format(name)}
100100
101101
2323
2424 @route("/", method="GET", apply=use_args(hello_args))
2525 def index(args):
26 """A welcome page.
27 """
26 """A welcome page."""
2827 return {"message": "Welcome, {}!".format(args["name"])}
2928
3029
2626 ### Middleware and hooks ###
2727
2828
29 class JSONTranslator(object):
29 class JSONTranslator:
3030 def process_response(self, req, resp, resource):
3131 if "result" not in req.context:
3232 return
4343 ### Resources ###
4444
4545
46 class HelloResource(object):
46 class HelloResource:
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(object):
56 class AdderResource:
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(object):
66 class DateAddResource:
6767 """A datetime adder endpoint."""
6868
6969 dateadd_args = {
2525 @app.route("/", methods=["GET"])
2626 @use_args(hello_args)
2727 def index(args):
28 """A welcome page.
29 """
28 """A welcome page."""
3029 return jsonify({"message": "Welcome, {}!".format(args["name"])})
3130
3231
0 # -*- coding: utf-8 -*-
10 """A simple number and datetime addition JSON API.
21 Run the app:
32
6968
7069 # This error handler is necessary for usage with Flask-RESTful
7170 @parser.error_handler
72 def handle_request_parsing_error(err, req, schema, error_status_code, error_headers):
71 def handle_request_parsing_error(err, req, schema, *, error_status_code, error_headers):
7372 """webargs error handler that uses Flask-RESTful's abort function to return
7473 a JSON error response to the client.
7574 """
2828 @view_config(route_name="hello", request_method="GET", renderer="json")
2929 @use_args(hello_args)
3030 def index(request, args):
31 """A welcome page.
32 """
31 """A welcome page."""
3332 return {"message": "Welcome, {}!".format(args["name"])}
3433
3534
0 python-dateutil==2.8.0
0 python-dateutil==2.8.1
11 Flask
22 bottle
33 tornado
6060
6161 def use_schema(schema_cls, list_view=False, locations=None):
6262 """View decorator for using a marshmallow schema to
63 (1) parse a request's input and
64 (2) serializing the view's output to a JSON response.
63 (1) parse a request's input and
64 (2) serializing the view's output to a JSON response.
6565 """
6666
6767 def decorator(func):
00 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
21 """A Hello, World! example using Webapp2 in a Google App Engine environment
32
43 Run the app:
0 [tool.black]
1 line-length = 88
2 target-version = ['py35', 'py36', 'py37', 'py38']
11 license_files = LICENSE
22
33 [bdist_wheel]
4 # We build separate wheels for
5 # Python 2 and 3 because of the conditional
6 # dependency on simplejson
7 universal = 0
4 universal = 1
85
96 [flake8]
107 ignore = E203, E266, E501, W503
0 # -*- coding: utf-8 -*-
1 import sys
20 import re
31 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")
82
93 FRAMEWORKS = [
104 "Flask>=0.12.2",
137 "tornado>=4.5.2",
148 "pyramid>=1.9.1",
159 "webapp2>=3.0.0b1",
16 "falcon>=1.4.0,<2.0",
17 'aiohttp>=3.0.0; python_version >= "3.5"',
10 "falcon>=2.0.0",
11 "aiohttp>=3.0.0",
1812 ]
1913 EXTRAS_REQUIRE = {
2014 "frameworks": FRAMEWORKS,
2115 "tests": [
2216 "pytest",
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"',
17 'mock; python_version == "3.5"',
18 "webtest==2.0.35",
19 "webtest-aiohttp==2.0.0",
20 "pytest-aiohttp>=0.3.0",
2721 ]
2822 + FRAMEWORKS,
2923 "lint": [
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",
24 "mypy==0.782",
25 "flake8==3.8.3",
26 "flake8-bugbear==20.1.4",
27 "pre-commit~=2.4",
3428 ],
35 "docs": ["Sphinx==2.2.0", "sphinx-issues==1.2.0", "sphinx-typlog-theme==0.7.3"]
29 "docs": ["Sphinx==3.2.1", "sphinx-issues==1.2.0", "sphinx-typlog-theme==0.8.0"]
3630 + FRAMEWORKS,
3731 }
3832 EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"]
4337 Raises RuntimeError if not found.
4438 """
4539 version = ""
46 with open(fname, "r") as fp:
40 with open(fname) as fp:
4741 reg = re.compile(r'__version__ = [\'"]([^\'"]*)[\'"]')
4842 for line in fp:
4943 m = reg.match(line)
7569 url="https://github.com/marshmallow-code/webargs",
7670 packages=find_packages("src"),
7771 package_dir={"": "src"},
78 install_requires=INSTALL_REQUIRES,
72 install_requires=["marshmallow>=2.15.2"],
7973 extras_require=EXTRAS_REQUIRE,
8074 license="MIT",
8175 zip_safe=False,
9690 "api",
9791 "marshmallow",
9892 ),
93 python_requires=">=3.5",
9994 classifiers=[
10095 "Development Status :: 5 - Production/Stable",
10196 "Intended Audience :: Developers",
10297 "License :: OSI Approved :: MIT License",
10398 "Natural Language :: English",
104 "Programming Language :: Python :: 2",
105 "Programming Language :: Python :: 2.7",
10699 "Programming Language :: Python :: 3",
107100 "Programming Language :: Python :: 3.5",
108101 "Programming Language :: Python :: 3.6",
109102 "Programming Language :: Python :: 3.7",
103 "Programming Language :: Python :: 3.8",
104 "Programming Language :: Python :: 3 :: Only",
110105 "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
111106 "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
112107 ],
0 # -*- coding: utf-8 -*-
10 from distutils.version import LooseVersion
21 from marshmallow.utils import missing
32
87 from webargs.dict2schema import dict2schema
98 from webargs import fields
109
11 __version__ = "5.5.3"
10 __version__ = "6.1.1"
1211 __version_info__ = tuple(LooseVersion(__version__).version)
1312 __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
3130
3231 from webargs import core
3332 from webargs.core import json
3433 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="parse_match_info",
76 path="parse_match_info",
77 **core.Parser.__location_map__
75 match_info="load_match_info",
76 path="load_match_info",
77 **core.Parser.__location_map__,
7878 )
7979
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)
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)
8383
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)
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)
9088
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)):
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)
96
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 == "":
96105 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)
106 return self._handle_invalid_json_error(exc, req)
107 except UnicodeDecodeError as exc:
108 return self._handle_invalid_json_error(exc, req)
106109
107 self._cache["json"] = json_data
108 return core.get_value(json_data, name, field, allow_many_nested=True)
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)
109113
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)
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)
113117
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)
117
118 def parse_files(self, req: Request, name: str, field: Field) -> None:
118 def load_files(self, req: Request, schema: Schema) -> "typing.NoReturn":
119119 raise NotImplementedError(
120 "parse_files is not implemented. You may be able to use parse_form for "
120 "load_files is not implemented. You may be able to use load_form for "
121121 "parsing upload data."
122122 )
123123
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)
124 def load_match_info(self, req: Request, schema: Schema) -> typing.Mapping:
125 """Load the request's ``match_info``."""
126 return req.match_info
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 elif isinstance(arg, web.View):
139 if isinstance(arg, web.View):
140140 req = arg.request
141141 break
142 assert isinstance(req, web.Request), "Request argument not found for handler"
142 if not isinstance(req, web.Request):
143 raise ValueError("Request argument not found for handler")
143144 return req
144145
145146 def handle_error(
147148 error: ValidationError,
148149 req: Request,
149150 schema: Schema,
150 error_status_code: typing.Union[int, None] = None,
151 error_headers: typing.Union[typing.Mapping[str, str], None] = None,
151 *,
152 error_status_code: typing.Union[int, None],
153 error_headers: typing.Union[typing.Mapping[str, str], None]
152154 ) -> "typing.NoReturn":
153155 """Handle ValidationErrors and return a JSON response of error messages
154156 to the client.
157159 error_status_code or self.DEFAULT_VALIDATION_STATUS
158160 )
159161 if not error_class:
160 raise LookupError("No exception for {0}".format(error_status_code))
162 raise LookupError("No exception for {}".format(error_status_code))
161163 headers = error_headers
162164 raise error_class(
163165 body=json.dumps(error.messages).encode("utf-8"),
165167 content_type="application/json",
166168 )
167169
168 def handle_invalid_json_error(
170 def _handle_invalid_json_error(
169171 self,
170172 error: typing.Union[json.JSONDecodeError, UnicodeDecodeError],
171173 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
1110
1211 from webargs import core
1312
2120 either coroutines or regular methods.
2221 """
2322
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
64
6523 # TODO: Lots of duplication from core.Parser here. Rethink.
6624 async def parse(
6725 self,
6826 argmap: ArgMap,
6927 req: Request = None,
70 locations: typing.Iterable = None,
28 *,
29 location: str = None,
7130 validate: Validate = None,
7231 error_status_code: typing.Union[int, None] = None,
73 error_headers: typing.Union[typing.Mapping[str, str], None] = None,
32 error_headers: typing.Union[typing.Mapping[str, str], None] = None
7433 ) -> typing.Union[typing.Mapping, None]:
7534 """Coroutine variant of `webargs.core.Parser`.
7635
7736 Receives the same arguments as `webargs.core.Parser.parse`.
7837 """
79 self.clear_cache() # in case someone used `parse_*()`
8038 req = req if req is not None else self.get_default_request()
81 assert req is not None, "Must pass req object"
39 location = location or self.location
40 if req is None:
41 raise ValueError("Must pass req object")
8242 data = None
8343 validators = core._ensure_list_of_callables(validate)
8444 schema = self._get_schema(argmap, req)
8545 try:
86 parsed = await self._parse_request(
87 schema=schema, req=req, locations=locations or self.locations
46 location_data = await self._load_location_data(
47 schema=schema, req=req, location=location
8848 )
89 result = schema.load(parsed)
49 result = schema.load(location_data)
9050 data = result.data if core.MARSHMALLOW_VERSION_INFO[0] < 3 else result
9151 self._validate_arguments(data, validators)
9252 except ma.exceptions.ValidationError as error:
9353 await self._on_validation_error(
94 error, req, schema, error_status_code, error_headers
54 error,
55 req,
56 schema,
57 location,
58 error_status_code=error_status_code,
59 error_headers=error_headers,
9560 )
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 = {}
9680 return data
9781
9882 async def _on_validation_error(
10084 error: ValidationError,
10185 req: Request,
10286 schema: Schema,
87 location: str,
88 *,
10389 error_status_code: typing.Union[int, None],
104 error_headers: typing.Union[typing.Mapping[str, str], None] = None,
90 error_headers: typing.Union[typing.Mapping[str, str], None]
10591 ) -> 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}
10698 error_handler = self.error_callback or self.handle_error
107 await error_handler(error, req, schema, error_status_code, error_headers)
99 await error_handler(
100 error,
101 req,
102 schema,
103 error_status_code=error_status_code,
104 error_headers=error_headers,
105 )
108106
109107 def use_args(
110108 self,
111109 argmap: ArgMap,
112110 req: typing.Optional[Request] = None,
113 locations: typing.Iterable = None,
111 *,
112 location: str = None,
114113 as_kwargs: bool = False,
115114 validate: Validate = None,
116115 error_status_code: typing.Optional[int] = None,
117 error_headers: typing.Union[typing.Mapping[str, str], None] = None,
116 error_headers: typing.Union[typing.Mapping[str, str], None] = None
118117 ) -> typing.Callable[..., typing.Callable]:
119118 """Decorator that injects parsed arguments into a view function or method.
120119
121120 Receives the same arguments as `webargs.core.Parser.use_args`.
122121 """
123 locations = locations or self.locations
122 location = location or self.location
124123 request_obj = req
125124 # Optimization: If argmap is passed as a dictionary, we only need
126125 # to generate a Schema once
127126 if isinstance(argmap, Mapping):
128 argmap = core.dict2schema(argmap, self.schema_class)()
127 argmap = core.dict2schema(argmap, schema_class=self.schema_class)()
129128
130129 def decorator(func: typing.Callable) -> typing.Callable:
131130 req_ = request_obj
142141 parsed_args = await self.parse(
143142 argmap,
144143 req=req_obj,
145 locations=locations,
144 location=location,
146145 validate=validate,
147146 error_status_code=error_status_code,
148147 error_headers=error_headers,
149148 )
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)
149 args, kwargs = self._update_args_kwargs(
150 args, kwargs, parsed_args, as_kwargs
151 )
152 return await func(*args, **kwargs)
157153
158154 else:
159155
167163 parsed_args = yield from self.parse( # type: ignore
168164 argmap,
169165 req=req_obj,
170 locations=locations,
166 location=location,
171167 validate=validate,
172168 error_status_code=error_status_code,
173169 error_headers=error_headers,
174170 )
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)
171 args, kwargs = self._update_args_kwargs(
172 args, kwargs, parsed_args, as_kwargs
173 )
174 return func(*args, **kwargs)
182175
183176 return wrapper
184177
185178 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 -*-
10 """Bottle request argument parsing module.
21
32 Example: ::
1918 import bottle
2019
2120 from webargs import core
22 from webargs.core import json
21 from webargs.multidictproxy import MultiDictProxy
2322
2423
2524 class BottleParser(core.Parser):
2625 """Bottle.py request argument parser."""
2726
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)
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 )
3131
32 def parse_form(self, req, name, field):
33 """Pull a form value from the request."""
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."""
3455 # For consistency with other parsers' behavior, don't attempt to
3556 # parse if content-type is mismatched.
3657 # TODO: Make this check more specific
3758 if core.is_json(req.content_type):
3859 return core.missing
39 return core.get_value(req.forms, name, field)
60 return MultiDictProxy(req.forms, schema)
4061
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)
62 def load_headers(self, req, schema):
63 """Return headers from the request as a MultiDictProxy."""
64 return MultiDictProxy(req.headers, schema)
5665
57 if json_data is None:
58 return core.missing
59 return core.get_value(json_data, name, field, allow_many_nested=True)
66 def load_cookies(self, req, schema):
67 """Return cookies from the request."""
68 return req.cookies
6069
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)
70 def load_files(self, req, schema):
71 """Return files from the request as a MultiDictProxy."""
72 return MultiDictProxy(req.files, schema)
6473
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):
74 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
7475 """Handles errors during parsing. Aborts the current request with a
7576 400 error.
7677 """
8283 exception=error,
8384 )
8485
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
9086 def get_default_request(self):
9187 """Override to use bottle's thread-local request object by default."""
9288 return bottle.request
0 # -*- coding: utf-8 -*-
10 # flake8: noqa
2 import sys
31 from distutils.version import LooseVersion
42
53 import marshmallow as ma
64
75 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
30 import functools
41 import inspect
2 import typing
53 import logging
64 import warnings
7 from copy import copy
8
9 try:
10 import simplejson as json
11 except ImportError:
12 import json # type: ignore
5 from collections.abc import Mapping
6 import json
137
148 import marshmallow as ma
159 from marshmallow import ValidationError
16 from marshmallow.utils import missing, is_collection
17
18 from webargs.compat import Mapping, iteritems, MARSHMALLOW_VERSION_INFO
10 from marshmallow.utils import missing
11
12 from webargs.compat import MARSHMALLOW_VERSION_INFO
1913 from webargs.dict2schema import dict2schema
2014 from webargs.fields import DelimitedList
2115
2721 "dict2schema",
2822 "is_multiple",
2923 "Parser",
30 "get_value",
3124 "missing",
3225 "parse_json",
3326 ]
4134 callable, a ValueError is raised.
4235 """
4336 if obj and not callable(obj):
44 raise ValueError("{0!r} is not callable.".format(obj))
45 else:
46 return obj
37 raise ValueError("{!r} is not callable.".format(obj))
38 return obj
4739
4840
4941 def is_multiple(field):
7365 return False
7466
7567
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):
68 def parse_json(string, *, encoding="utf-8"):
69 if isinstance(string, bytes):
11470 try:
115 s = s.decode(encoding)
116 except UnicodeDecodeError as e:
71 string = string.decode(encoding)
72 except UnicodeDecodeError as exc:
11773 raise json.JSONDecodeError(
118 "Bytes decoding error : {}".format(e.reason),
119 doc=str(e.object),
120 pos=e.start,
74 "Bytes decoding error : {}".format(exc.reason),
75 doc=str(exc.object),
76 pos=exc.start,
12177 )
122 return json.loads(s)
78 return json.loads(string)
12379
12480
12581 def _ensure_list_of_callables(obj):
12985 elif callable(obj):
13086 validators = [obj]
13187 else:
132 raise ValueError(
133 "{0!r} is not a callable or list of callables.".format(obj)
134 )
88 raise ValueError("{!r} is not a callable or list of callables.".format(obj))
13589 else:
13690 validators = []
13791 return validators
13892
13993
140 class Parser(object):
94 class Parser:
14195 """Base parser class that provides high-level implementation for parsing
14296 a request.
14397
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.
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
148103 :param callable error_handler: Custom error handler function.
149104 """
150105
151 #: Default locations to check for data
152 DEFAULT_LOCATIONS = ("querystring", "form", "json")
106 #: Default location to check for data
107 DEFAULT_LOCATION = "json"
153108 #: The marshmallow Schema class to use when creating new schemas
154109 DEFAULT_SCHEMA_CLASS = ma.Schema
155110 #: Default status code to return for validation errors
159114
160115 #: Maps location => method name
161116 __location_map__ = {
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",
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",
169125 }
170126
171 def __init__(self, locations=None, error_handler=None, schema_class=None):
172 self.locations = locations or self.DEFAULT_LOCATIONS
127 def __init__(self, location=None, *, error_handler=None, schema_class=None):
128 self.location = location or self.DEFAULT_LOCATION
173129 self.error_callback = _callable_or_raise(error_handler)
174130 self.schema_class = schema_class or self.DEFAULT_SCHEMA_CLASS
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
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 """
185137 valid_locations = set(self.__location_map__.keys())
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))
138 if location not in valid_locations:
139 msg = "Invalid location argument: {}".format(location)
190140 raise ValueError(msg)
191 return locations
192
193 def _get_handler(self, location):
141
194142 # Parsing function to call
195143 # May be a method name (str) or a function
196144 func = self.__location_map__.get(location)
200148 else:
201149 function = getattr(self, func)
202150 else:
203 raise ValueError('Invalid location: "{0}"'.format(location))
151 raise ValueError('Invalid location: "{}"'.format(location))
204152 return function
205153
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
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
273168
274169 def _on_validation_error(
275 self, error, req, schema, error_status_code, error_headers
170 self, error, req, schema, location, *, error_status_code, error_headers
276171 ):
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}
277178 error_handler = self.error_callback or self.handle_error
278 error_handler(error, req, schema, error_status_code, error_headers)
179 error_handler(
180 error,
181 req,
182 schema,
183 error_status_code=error_status_code,
184 error_headers=error_headers,
185 )
279186
280187 def _validate_arguments(self, data, validators):
281188 for validator in validators:
299206 elif callable(argmap):
300207 schema = argmap(req)
301208 else:
302 schema = dict2schema(argmap, self.schema_class)()
209 schema = dict2schema(argmap, schema_class=self.schema_class)()
303210 if MARSHMALLOW_VERSION_INFO[0] < 3 and not schema.strict:
304211 warnings.warn(
305212 "It is highly recommended that you set strict=True on your schema "
308215 )
309216 return schema
310217
311 def _clone(self):
312 clone = copy(self)
313 clone.clear_cache()
314 return clone
315
316218 def parse(
317219 self,
318220 argmap,
319221 req=None,
320 locations=None,
222 *,
223 location=None,
321224 validate=None,
322225 error_status_code=None,
323 error_headers=None,
226 error_headers=None
324227 ):
325228 """Main request parsing method.
326229
328231 of argname -> `marshmallow.fields.Field` pairs, or a callable
329232 which accepts a request and returns a `marshmallow.Schema`.
330233 :param req: The request object to parse.
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')``.
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')``.
334238 :param callable validate: Validation function or list of validation functions
335239 that receives the dictionary of parsed arguments. Validator either returns a
336240 boolean or raises a :exc:`ValidationError`.
341245
342246 :return: A dictionary of parsed arguments
343247 """
344 self.clear_cache() # in case someone used `parse_*()`
345248 req = req if req is not None else self.get_default_request()
346 assert req is not None, "Must pass req object"
249 location = location or self.location
250 if req is None:
251 raise ValueError("Must pass req object")
347252 data = None
348253 validators = _ensure_list_of_callables(validate)
349 parser = self._clone()
350254 schema = self._get_schema(argmap, req)
351255 try:
352 parsed = parser._parse_request(
353 schema=schema, req=req, locations=locations or self.locations
256 location_data = self._load_location_data(
257 schema=schema, req=req, location=location
354258 )
355 result = schema.load(parsed)
259 result = schema.load(location_data)
356260 data = result.data if MARSHMALLOW_VERSION_INFO[0] < 3 else result
357 parser._validate_arguments(data, validators)
261 self._validate_arguments(data, validators)
358262 except ma.exceptions.ValidationError as error:
359 parser._on_validation_error(
360 error, req, schema, error_status_code, error_headers
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,
270 )
271 warnings.warn(
272 "_on_validation_error hook did not raise an exception and flow "
273 "of control returned to parse(). You may get unexpected results"
361274 )
362275 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
374276
375277 def get_default_request(self):
376278 """Optional override. Provides a hook for frameworks that use thread-local
392294 """
393295 return None
394296
297 @staticmethod
298 def _update_args_kwargs(args, kwargs, parsed_args, as_kwargs):
299 """Update args or kwargs with parsed_args depending on as_kwargs"""
300 if as_kwargs:
301 kwargs.update(parsed_args)
302 else:
303 # Add parsed_args after other positional arguments
304 args += (parsed_args,)
305 return args, kwargs
306
395307 def use_args(
396308 self,
397309 argmap,
398310 req=None,
399 locations=None,
311 *,
312 location=None,
400313 as_kwargs=False,
401314 validate=None,
402315 error_status_code=None,
403 error_headers=None,
316 error_headers=None
404317 ):
405318 """Decorator that injects parsed arguments into a view function or method.
406319
407320 Example usage with Flask: ::
408321
409322 @app.route('/echo', methods=['get', 'post'])
410 @parser.use_args({'name': fields.Str()})
323 @parser.use_args({'name': fields.Str()}, location="querystring")
411324 def greet(args):
412325 return 'Hello ' + args['name']
413326
414327 :param argmap: Either a `marshmallow.Schema`, a `dict`
415328 of argname -> `marshmallow.fields.Field` pairs, or a callable
416329 which accepts a request and returns a `marshmallow.Schema`.
417 :param tuple locations: Where on the request to search for values.
330 :param str location: Where on the request to load values.
418331 :param bool as_kwargs: Whether to insert arguments as keyword arguments.
419332 :param callable validate: Validation function that receives the dictionary
420333 of parsed arguments. If the function returns ``False``, the parser
424337 :param dict error_headers: Headers passed to error handler functions when a
425338 a `ValidationError` is raised.
426339 """
427 locations = locations or self.locations
340 location = location or self.location
428341 request_obj = req
429342 # Optimization: If argmap is passed as a dictionary, we only need
430343 # to generate a Schema once
431344 if isinstance(argmap, Mapping):
432 argmap = dict2schema(argmap, self.schema_class)()
345 argmap = dict2schema(argmap, schema_class=self.schema_class)()
433346
434347 def decorator(func):
435348 req_ = request_obj
440353
441354 if not req_obj:
442355 req_obj = self.get_request_from_view_args(func, args, kwargs)
356
443357 # NOTE: At this point, argmap may be a Schema, or a callable
444358 parsed_args = self.parse(
445359 argmap,
446360 req=req_obj,
447 locations=locations,
361 location=location,
448362 validate=validate,
449363 error_status_code=error_status_code,
450364 error_headers=error_headers,
451365 )
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)
366 args, kwargs = self._update_args_kwargs(
367 args, kwargs, parsed_args, as_kwargs
368 )
369 return func(*args, **kwargs)
459370
460371 wrapper.__wrapped__ = func
461372 return wrapper
462373
463374 return decorator
464375
465 def use_kwargs(self, *args, **kwargs):
376 def use_kwargs(self, *args, **kwargs) -> typing.Callable:
466377 """Decorator that injects parsed arguments into a view function or method
467378 as keyword arguments.
468379
480391 kwargs["as_kwargs"] = True
481392 return self.use_args(*args, **kwargs)
482393
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.
394 def location_loader(self, name):
395 """Decorator that registers a function for loading a request location.
396 The wrapped function receives a schema and a request.
397
398 The schema will usually not be relevant, but it's important in some
399 cases -- most notably in order to correctly load multidict values into
400 list fields. Without the schema, there would be no way to know whether
401 to simply `.get()` or `.getall()` from a multidict for a given value.
487402
488403 Example: ::
489404
490405 from webargs import core
491406 parser = core.Parser()
492407
493 @parser.location_handler("name")
494 def parse_data(request, name, field):
495 return request.data.get(name)
408 @parser.location_loader("name")
409 def load_data(request, schema):
410 return request.data
496411
497412 :param str name: The name of the location to register.
498413 """
522437
523438
524439 @parser.error_handler
525 def handle_error(error, req, schema, status_code, headers):
440 def handle_error(error, req, schema, *, error_status_code, error_headers):
526441 raise CustomError(error.messages)
527442
528443 :param callable func: The error callback to register.
530445 self.error_callback = func
531446 return func
532447
448 def _handle_invalid_json_error(self, error, req, *args, **kwargs):
449 """Internal hook for overriding treatment of JSONDecodeErrors.
450
451 Invoked by default `load_json` implementation.
452
453 External parsers can just implement their own behavior for load_json ,
454 so this is not part of the public parser API.
455 """
456 raise error
457
458 def load_json(self, req, schema):
459 """Load JSON from a request object or return `missing` if no value can
460 be found.
461 """
462 # NOTE: although this implementation is real/concrete and used by
463 # several of the parsers in webargs, it relies on the internal hooks
464 # `_handle_invalid_json_error` and `_raw_load_json`
465 # these methods are not part of the public API and are used to simplify
466 # code sharing amongst the built-in webargs parsers
467 try:
468 return self._raw_load_json(req)
469 except json.JSONDecodeError as exc:
470 if exc.doc == "":
471 return missing
472 return self._handle_invalid_json_error(exc, req)
473 except UnicodeDecodeError as exc:
474 return self._handle_invalid_json_error(exc, req)
475
476 def load_json_or_form(self, req, schema):
477 """Load data from a request, accepting either JSON or form-encoded
478 data.
479
480 The data will first be loaded as JSON, and, if that fails, it will be
481 loaded as a form post.
482 """
483 data = self.load_json(req, schema)
484 if data is not missing:
485 return data
486 return self.load_form(req, schema)
487
533488 # Abstract Methods
534489
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.
490 def _raw_load_json(self, req):
491 """Internal hook method for implementing load_json()
492
493 Get a request body for feeding in to `load_json`, and parse it either
494 using core.parse_json() or similar utilities which raise
495 JSONDecodeErrors.
496 Ensure consistent behavior when encountering decoding errors.
497
498 The default implementation here simply returns `missing`, and the default
499 implementation of `load_json` above will pass that value through.
500 However, by implementing a "mostly concrete" version of load_json with
501 this as a hook for getting data, we consolidate the logic for handling
502 those JSONDecodeErrors.
538503 """
539504 return missing
540505
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.
506 def load_querystring(self, req, schema):
507 """Load the query string of a request object or return `missing` if no
508 value can be found.
544509 """
545510 return missing
546511
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.
512 def load_form(self, req, schema):
513 """Load the form data of a request object or return `missing` if no
514 value can be found.
550515 """
551516 return missing
552517
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 """
518 def load_headers(self, req, schema):
519 """Load the headers or return `missing` if no value can be found."""
557520 return missing
558521
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.
522 def load_cookies(self, req, schema):
523 """Load the cookies from the request or return `missing` if no value
524 can be found.
562525 """
563526 return missing
564527
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.
528 def load_files(self, req, schema):
529 """Load files from the request or return `missing` if no values can be
530 found.
568531 """
569532 return missing
570533
571 def handle_error(
572 self, error, req, schema, error_status_code=None, error_headers=None
573 ):
534 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
574535 """Called if an error occurs while parsing args. By default, just logs and
575536 raises ``error``.
576537 """
0 # -*- coding: utf-8 -*-
10 import marshmallow as ma
21
32
4 def dict2schema(dct, schema_class=ma.Schema):
3 def dict2schema(dct, *, schema_class=ma.Schema):
54 """Generate a `marshmallow.Schema` class given a dictionary of
65 `Fields <marshmallow.fields.Field>`.
76 """
98 return schema_class.from_dict(dct)
109 attrs = dct.copy()
1110
12 class Meta(object):
11 class Meta:
1312 strict = True
1413
1514 attrs["Meta"] = Meta
16 return type(str(""), (schema_class,), attrs)
15 return type("", (schema_class,), attrs)
0 # -*- coding: utf-8 -*-
10 """Django request argument parsing.
21
32 Example usage: ::
1817 return HttpResponse('Hello ' + args['name'])
1918 """
2019 from webargs import core
21 from webargs.core import json
20 from webargs.multidictproxy import MultiDictProxy
21
22
23 def is_json_request(req):
24 return core.is_json(req.content_type)
2225
2326
2427 class DjangoParser(core.Parser):
3235 the parser and returning the appropriate `HTTPResponse`.
3336 """
3437
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)
38 def _raw_load_json(self, req):
39 """Read a json payload from the request for the core parser's load_json
3840
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)
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
4245
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
46 return core.parse_json(req.body)
4947
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)
48 def load_querystring(self, req, schema):
49 """Return query params from the request as a MultiDictProxy."""
50 return MultiDictProxy(req.GET, schema)
6051
61 def parse_cookies(self, req, name, field):
62 """Pull the value from the cookiejar."""
63 return core.get_value(req.COOKIES, name, field)
52 def load_form(self, req, schema):
53 """Return form values from the request as a MultiDictProxy."""
54 return MultiDictProxy(req.POST, schema)
6455
65 def parse_headers(self, req, name, field):
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):
6661 raise NotImplementedError(
67 "Header parsing not supported by {0}".format(self.__class__.__name__)
62 "Header parsing not supported by {}".format(self.__class__.__name__)
6863 )
6964
70 def parse_files(self, req, name, field):
71 """Pull a file from the request."""
72 return core.get_value(req.FILES, name, field)
65 def load_files(self, req, schema):
66 """Return files from the request as a MultiDictProxy."""
67 return MultiDictProxy(req.FILES, schema)
7368
7469 def get_request_from_view_args(self, view, args, kwargs):
7570 # The first argument is either `self` or `request`
7873 except AttributeError: # first arg is request
7974 return args[0]
8075
81 def handle_invalid_json_error(self, error, req, *args, **kwargs):
82 raise error
83
8476
8577 parser = DjangoParser()
8678 use_args = parser.use_args
0 # -*- coding: utf-8 -*-
10 """Falcon request argument parsing module.
21 """
32 import falcon
43 from falcon.util.uri import parse_query_string
54
65 from webargs import core
7 from webargs.core import json
6 from webargs.multidictproxy import MultiDictProxy
87
98 HTTP_422 = "422 Unprocessable Entity"
109
2928 return content_type and core.is_json(content_type)
3029
3130
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
4931 # NOTE: Adapted from falcon.request.Request._parse_form_urlencoded
5032 def parse_form_body(req):
5133 if (
5234 req.content_type is not None
5335 and "application/x-www-form-urlencoded" in req.content_type
5436 ):
55 body = req.stream.read()
37 body = req.stream.read(req.content_length or 0)
5638 try:
5739 body = body.decode("ascii")
5840 except UnicodeDecodeError:
6547 )
6648
6749 if body:
68 return parse_query_string(
69 body, keep_blank_qs_values=req.options.keep_blank_qs_values
70 )
71 return {}
50 return parse_query_string(body, keep_blank=req.options.keep_blank_qs_values)
51
52 return core.missing
7253
7354
7455 class HTTPError(falcon.HTTPError):
75 """HTTPError that stores a dictionary of validation error messages.
76 """
56 """HTTPError that stores a dictionary of validation error messages."""
7757
7858 def __init__(self, status, errors, *args, **kwargs):
7959 self.errors = errors
80 super(HTTPError, self).__init__(status, *args, **kwargs)
60 super().__init__(status, *args, **kwargs)
8161
8262 def to_dict(self, *args, **kwargs):
8363 """Override `falcon.HTTPError` to include error messages in responses."""
84 ret = super(HTTPError, self).to_dict(*args, **kwargs)
64 ret = super().to_dict(*args, **kwargs)
8565 if self.errors is not None:
8666 ret["errors"] = self.errors
8767 return ret
9070 class FalconParser(core.Parser):
9171 """Falcon request argument parser."""
9272
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)
73 # Note on the use of MultiDictProxy throughout:
74 # Falcon parses query strings and form values into ordinary dicts, but with
75 # the values listified where appropriate
76 # it is still therefore necessary in these cases to wrap them in
77 # MultiDictProxy because we need to use the schema to determine when single
78 # values should be wrapped in lists due to the type of the destination
79 # field
9680
97 def parse_form(self, req, name, field):
98 """Pull a form value from the request.
81 def load_querystring(self, req, schema):
82 """Return query params from the request as a MultiDictProxy."""
83 return MultiDictProxy(req.params, schema)
84
85 def load_form(self, req, schema):
86 """Return form values from the request as a MultiDictProxy
9987
10088 .. note::
10189
10290 The request stream will be read and left at EOF.
10391 """
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)
92 form = parse_form_body(req)
93 if form is core.missing:
94 return form
95 return MultiDictProxy(form, schema)
10896
109 def parse_json(self, req, name, field):
110 """Pull a JSON body value from the request.
97 def _raw_load_json(self, req):
98 """Return a json payload from the request for the core parser's load_json
11199
112 .. note::
100 Checks the input mimetype and may return 'missing' if the mimetype is
101 non-json, even if the request body is parseable as json."""
102 if not is_json_request(req) or req.content_length in (None, 0):
103 return core.missing
104 body = req.stream.read(req.content_length)
105 if body:
106 return core.parse_json(body)
107 return core.missing
113108
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)
109 def load_headers(self, req, schema):
110 """Return headers from the request."""
111 # Falcon only exposes headers as a dict (not multidict)
112 return req.headers
123113
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)
114 def load_cookies(self, req, schema):
115 """Return cookies from the request."""
116 # Cookies are expressed in Falcon as a dict, but the possibility of
117 # multiple values for a cookie is preserved internally -- if desired in
118 # the future, webargs could add a MultiDict type for Cookies here built
119 # from (req, schema), but Falcon does not provide one out of the box
120 return req.cookies
135121
136122 def get_request_from_view_args(self, view, args, kwargs):
137123 """Get request from a resource method's arguments. Assumes that
138124 request is the second argument.
139125 """
140126 req = args[1]
141 assert isinstance(req, falcon.Request), "Argument is not a falcon.Request"
127 if not isinstance(req, falcon.Request):
128 raise TypeError("Argument is not a falcon.Request")
142129 return req
143130
144 def parse_files(self, req, name, field):
131 def load_files(self, req, schema):
145132 raise NotImplementedError(
146 "Parsing files not yet supported by {0}".format(self.__class__.__name__)
133 "Parsing files not yet supported by {}".format(self.__class__.__name__)
147134 )
148135
149 def handle_error(self, error, req, schema, error_status_code, error_headers):
136 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
150137 """Handles errors during parsing."""
151138 status = status_map.get(error_status_code or self.DEFAULT_VALIDATION_STATUS)
152139 if status is None:
153 raise LookupError("Status code {0} not supported".format(error_status_code))
140 raise LookupError("Status code {} not supported".format(error_status_code))
154141 raise HTTPError(status, errors=error.messages, headers=error_headers)
155142
156 def handle_invalid_json_error(self, error, req, *args, **kwargs):
143 def _handle_invalid_json_error(self, error, req, *args, **kwargs):
157144 status = status_map[400]
158145 messages = {"json": ["Invalid JSON body."]}
159146 raise HTTPError(status, errors=messages)
0 # -*- coding: utf-8 -*-
10 """Field classes.
21
32 Includes all fields from `marshmallow.fields` in addition to a custom
98 .. code-block:: python
109
1110 args = {
12 "active": fields.Bool(location='query'),
11 "active": fields.Bool(location="query"),
1312 "content_type": fields.Str(data_key="Content-Type", location="headers"),
1413 }
1514
4039 def __init__(self, nested, *args, **kwargs):
4140 if isinstance(nested, dict):
4241 nested = dict2schema(nested)
43 super(Nested, self).__init__(nested, *args, **kwargs)
42 super().__init__(nested, *args, **kwargs)
4443
4544
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").
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 ","
4949
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.
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
5357 """
5458
5559 delimiter = ","
5660
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
6261 def _serialize(self, value, attr, obj):
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
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 )
6767
6868 def _deserialize(self, value, attr, data, **kwargs):
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:
69 # attempting to deserialize from a non-string source is an error
70 if not isinstance(value, (str, bytes)):
7671 if MARSHMALLOW_VERSION_INFO[0] < 3:
7772 self.fail("invalid")
7873 else:
7974 raise self.make_error("invalid")
80 return super(DelimitedList, self)._deserialize(ret, attr, data, **kwargs)
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)
0 # -*- coding: utf-8 -*-
10 """Flask request argument parsing module.
21
32 Example: ::
98
109 app = Flask(__name__)
1110
12 hello_args = {
13 'name': fields.Str(required=True)
11 user_detail_args = {
12 'per_page': fields.Int()
1413 }
1514
16 @app.route('/')
17 @use_args(hello_args)
18 def index(args):
19 return 'Hello ' + args['name']
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 )
2021 """
2122 import flask
2223 from werkzeug.exceptions import HTTPException
2324
2425 from webargs import core
25 from webargs.core import json
26 from webargs.compat import MARSHMALLOW_VERSION_INFO
27 from webargs.multidictproxy import MultiDictProxy
2628
2729
2830 def abort(http_status_code, exc=None, **kwargs):
4749 """Flask request argument parser."""
4850
4951 __location_map__ = dict(
50 view_args="parse_view_args",
51 path="parse_view_args",
52 **core.Parser.__location_map__
52 view_args="load_view_args",
53 path="load_view_args",
54 **core.Parser.__location_map__,
5355 )
5456
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)
57 def _raw_load_json(self, req):
58 """Return a json payload from the request for the core parser's load_json
5859
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
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
6564
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)
65 return core.parse_json(req.get_data(cache=True))
7866
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)
67 def _handle_invalid_json_error(self, error, req, *args, **kwargs):
68 abort(400, exc=error, messages={"json": ["Invalid JSON body."]})
8269
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
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
9073
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)
74 def load_querystring(self, req, schema):
75 """Return query params from the request as a MultiDictProxy."""
76 return MultiDictProxy(req.args, schema)
9477
95 def parse_cookies(self, req, name, field):
96 """Pull a value from the cookiejar."""
97 return core.get_value(req.cookies, name, field)
78 def load_form(self, req, schema):
79 """Return form values from the request as a MultiDictProxy."""
80 return MultiDictProxy(req.form, schema)
9881
99 def parse_files(self, req, name, field):
100 """Pull a file from the request."""
101 return core.get_value(req.files, name, field)
82 def load_headers(self, req, schema):
83 """Return headers from the request as a MultiDictProxy."""
84 return MultiDictProxy(req.headers, schema)
10285
103 def handle_error(self, error, req, schema, error_status_code, error_headers):
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):
10495 """Handles errors during parsing. Aborts the current HTTP request and
10596 responds with a 422 error.
10697 """
10798 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)
108109 abort(
109110 status_code,
110111 exc=error,
113114 headers=error_headers,
114115 )
115116
116 def handle_invalid_json_error(self, error, req, *args, **kwargs):
117 abort(400, exc=error, messages={"json": ["Invalid JSON body."]})
118
119117 def get_default_request(self):
120 """Override to use Flask's thread-local request objec by default"""
118 """Override to use Flask's thread-local request object by default"""
121119 return flask.request
122120
123121
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 for x in iter(self.data):
65 # special case for header dicts which produce an iterator of tuples
66 # instead of an iterator of strings
67 if isinstance(x, tuple):
68 yield x[0]
69 else:
70 yield x
71
72 def __contains__(self, x):
73 return x in self.data
74
75 def __len__(self):
76 return len(self.data)
77
78 def __eq__(self, other):
79 return self.data == other
80
81 def __ne__(self, other):
82 return self.data != other
0 # -*- coding: utf-8 -*-
10 """Pyramid request argument parsing.
21
32 Example usage: ::
2423 server = make_server('0.0.0.0', 6543, app)
2524 server.serve_forever()
2625 """
27 import collections
2826 import functools
27 from collections.abc import Mapping
2928
3029 from webob.multidict import MultiDict
3130 from pyramid.httpexceptions import exception_response
3231
3332 from webargs import core
3433 from webargs.core import json
35 from webargs.compat import text_type
34 from webargs.multidictproxy import MultiDictProxy
35
36
37 def is_json_request(req):
38 return core.is_json(req.headers.get("content-type"))
3639
3740
3841 class PyramidParser(core.Parser):
3942 """Pyramid request argument parser."""
4043
4144 __location_map__ = dict(
42 matchdict="parse_matchdict",
43 path="parse_matchdict",
44 **core.Parser.__location_map__
45 matchdict="load_matchdict",
46 path="load_matchdict",
47 **core.Parser.__location_map__,
4548 )
4649
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)
50 def _raw_load_json(self, req):
51 """Return a json payload from the request for the core parser's load_json
5052
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)
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
5457
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
58 return core.parse_json(req.body, encoding=req.charset)
6159
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)
60 def load_querystring(self, req, schema):
61 """Return query params from the request as a MultiDictProxy."""
62 return MultiDictProxy(req.GET, schema)
7263
73 def parse_cookies(self, req, name, field):
74 """Pull the value from the cookiejar."""
75 return core.get_value(req.cookies, name, field)
64 def load_form(self, req, schema):
65 """Return form values from the request as a MultiDictProxy."""
66 return MultiDictProxy(req.POST, schema)
7667
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)
68 def load_cookies(self, req, schema):
69 """Return cookies from the request as a MultiDictProxy."""
70 return MultiDictProxy(req.cookies, schema)
8071
81 def parse_files(self, req, name, field):
82 """Pull a file from the request."""
72 def load_headers(self, req, schema):
73 """Return headers from the request as a MultiDictProxy."""
74 return MultiDictProxy(req.headers, schema)
75
76 def load_files(self, req, schema):
77 """Return files from the request as a MultiDictProxy."""
8378 files = ((k, v) for k, v in req.POST.items() if hasattr(v, "file"))
84 return core.get_value(MultiDict(files), name, field)
79 return MultiDictProxy(MultiDict(files), schema)
8580
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)
81 def load_matchdict(self, req, schema):
82 """Return the request's ``matchdict`` as a MultiDictProxy."""
83 return MultiDictProxy(req.matchdict, schema)
8984
90 def handle_error(self, error, req, schema, error_status_code, error_headers):
85 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
9186 """Handles errors during parsing. Aborts the current HTTP request and
9287 responds with a 400 error.
9388 """
9489 status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS
9590 response = exception_response(
9691 status_code,
97 detail=text_type(error),
92 detail=str(error),
9893 headers=error_headers,
9994 content_type="application/json",
10095 )
10196 body = json.dumps(error.messages)
102 response.body = body.encode("utf-8") if isinstance(body, text_type) else body
97 response.body = body.encode("utf-8") if isinstance(body, str) else body
10398 raise response
10499
105 def handle_invalid_json_error(self, error, req, *args, **kwargs):
100 def _handle_invalid_json_error(self, error, req, *args, **kwargs):
106101 messages = {"json": ["Invalid JSON body."]}
107102 response = exception_response(
108 400, detail=text_type(messages), content_type="application/json"
103 400, detail=str(messages), content_type="application/json"
109104 )
110105 body = json.dumps(messages)
111 response.body = body.encode("utf-8") if isinstance(body, text_type) else body
106 response.body = body.encode("utf-8") if isinstance(body, str) else body
112107 raise response
113108
114109 def use_args(
115110 self,
116111 argmap,
117112 req=None,
118 locations=core.Parser.DEFAULT_LOCATIONS,
113 *,
114 location=core.Parser.DEFAULT_LOCATION,
119115 as_kwargs=False,
120116 validate=None,
121117 error_status_code=None,
122 error_headers=None,
118 error_headers=None
123119 ):
124120 """Decorator that injects parsed arguments into a view callable.
125121 Supports the *Class-based View* pattern where `request` is saved as an instance
129125 of argname -> `marshmallow.fields.Field` pairs, or a callable
130126 which accepts a request and returns a `marshmallow.Schema`.
131127 :param req: The request object to parse. Pulled off of the view by default.
132 :param tuple locations: Where on the request to search for values.
128 :param str location: Where on the request to load values.
133129 :param bool as_kwargs: Whether to insert arguments as keyword arguments.
134130 :param callable validate: Validation function that receives the dictionary
135131 of parsed arguments. If the function returns ``False``, the parser
139135 :param dict error_headers: Headers passed to error handler functions when a
140136 a `ValidationError` is raised.
141137 """
142 locations = locations or self.locations
138 location = location or self.location
143139 # Optimization: If argmap is passed as a dictionary, we only need
144140 # to generate a Schema once
145 if isinstance(argmap, collections.Mapping):
146 argmap = core.dict2schema(argmap, self.schema_class)()
141 if isinstance(argmap, Mapping):
142 argmap = core.dict2schema(argmap, schema_class=self.schema_class)()
147143
148144 def decorator(func):
149145 @functools.wraps(func)
157153 parsed_args = self.parse(
158154 argmap,
159155 req=request,
160 locations=locations,
156 location=location,
161157 validate=validate,
162158 error_status_code=error_status_code,
163159 error_headers=error_headers,
164160 )
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)
161 args, kwargs = self._update_args_kwargs(
162 args, kwargs, parsed_args, as_kwargs
163 )
164 return func(obj, *args, **kwargs)
170165
171166 wrapper.__wrapped__ = func
172167 return wrapper
0 # -*- coding: utf-8 -*-
10 """Utilities for testing. Includes a base test class
21 for testing parsers.
32
1211 from webargs.core import json
1312
1413
15 class CommonTestCase(object):
14 class CommonTestCase:
1615 """Base test class that defines test methods for common functionality across all
1716 parsers. Subclasses must define `create_app`, which returns a WSGI-like app.
1817 """
3938 def test_parse_querystring_args(self, testapp):
4039 assert testapp.get("/echo?name=Fred").json == {"name": "Fred"}
4140
42 def test_parse_querystring_with_query_location_specified(self, testapp):
43 assert testapp.get("/echo_query?name=Steve").json == {"name": "Steve"}
44
4541 def test_parse_form(self, testapp):
46 assert testapp.post("/echo", {"name": "Joe"}).json == {"name": "Joe"}
42 assert testapp.post("/echo_form", {"name": "Joe"}).json == {"name": "Joe"}
4743
4844 def test_parse_json(self, testapp):
49 assert testapp.post_json("/echo", {"name": "Fred"}).json == {"name": "Fred"}
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"}
5060
5161 def test_parse_querystring_default(self, testapp):
5262 assert testapp.get("/echo").json == {"name": "World"}
5363
5464 def test_parse_json_default(self, testapp):
55 assert testapp.post_json("/echo", {}).json == {"name": "World"}
65 assert testapp.post_json("/echo_json", {}).json == {"name": "World"}
5666
5767 def test_parse_json_with_charset(self, testapp):
5868 res = testapp.post(
59 "/echo",
69 "/echo_json",
6070 json.dumps({"name": "Steve"}),
6171 content_type="application/json;charset=UTF-8",
6272 )
6474
6575 def test_parse_json_with_vendor_media_type(self, testapp):
6676 res = testapp.post(
67 "/echo",
77 "/echo_json",
6878 json.dumps({"name": "Steve"}),
6979 content_type="application/vnd.api+json;charset=UTF-8",
7080 )
7181 assert res.json == {"name": "Steve"}
7282
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"}
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
8498
8599 def test_parse_json_many_schema_invalid_input(self, testapp):
86100 res = testapp.post_json(
92106 res = testapp.post_json("/echo_many_schema", [{"name": "Steve"}]).json
93107 assert res == [{"name": "Steve"}]
94108
95 def test_parse_json_many_schema_ignore_malformed_data(self, testapp):
96 assert testapp.post_json("/echo_many_schema", {"extra": "data"}).json == []
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
97114
98115 def test_parsing_form_default(self, testapp):
99 assert testapp.post("/echo", {}).json == {"name": "World"}
116 assert testapp.post("/echo_form", {}).json == {"name": "World"}
100117
101118 def test_parse_querystring_multiple(self, testapp):
102119 expected = {"name": ["steve", "Loria"]}
103120 assert testapp.get("/echo_multi?name=steve&name=Loria").json == expected
104121
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
105130 def test_parse_form_multiple(self, testapp):
106131 expected = {"name": ["steve", "Loria"]}
107132 assert (
108 testapp.post("/echo_multi", {"name": ["steve", "Loria"]}).json == expected
133 testapp.post("/echo_multi_form", {"name": ["steve", "Loria"]}).json
134 == expected
109135 )
110136
111137 def test_parse_json_list(self, testapp):
112138 expected = {"name": ["Steve"]}
113 assert testapp.post_json("/echo_multi", {"name": "Steve"}).json == expected
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
114148
115149 def test_parse_json_with_nonascii_chars(self, testapp):
116 text = u"øˆƒ£ºº∆ƒˆ∆"
117 assert testapp.post_json("/echo", {"name": text}).json == {"name": text}
150 text = "øˆƒ£ºº∆ƒˆ∆"
151 assert testapp.post_json("/echo_json", {"name": text}).json == {"name": text}
118152
119153 # https://github.com/marshmallow-code/webargs/issues/427
120154 def test_parse_json_with_nonutf8_chars(self, testapp):
121155 res = testapp.post(
122 "/echo",
156 "/echo_json",
123157 b"\xfe",
124158 headers={"Accept": "application/json", "Content-Type": "application/json"},
125159 expect_errors=True,
129163 assert res.json == {"json": ["Invalid JSON body."]}
130164
131165 def test_validation_error_returns_422_response(self, testapp):
132 res = testapp.post("/echo", {"name": "b"}, expect_errors=True)
166 res = testapp.post_json("/echo_json", {"name": "b"}, expect_errors=True)
133167 assert res.status_code == 422
134168
135169 def test_user_validation_error_returns_422_response_by_default(self, testapp):
186220 res = testapp.post_json("/echo_nested_many", in_data)
187221 assert res.json == {}
188222
189 def test_parse_json_if_no_json(self, testapp):
190 res = testapp.post("/echo")
191 assert res.json == {"name": "World"}
192
193223 def test_parse_files(self, testapp):
194224 res = testapp.post(
195225 "/echo_file", {"myfile": webtest.Upload("README.rst", b"data")}
198228
199229 # https://github.com/sloria/webargs/pull/297
200230 def test_empty_json(self, testapp):
201 res = testapp.post(
202 "/echo",
231 res = testapp.post("/echo_json")
232 assert res.status_code == 200
233 assert res.json == {"name": "World"}
234
235 # https://github.com/sloria/webargs/pull/297
236 def test_empty_json_with_headers(self, testapp):
237 res = testapp.post(
238 "/echo_json",
203239 "",
204240 headers={"Accept": "application/json", "Content-Type": "application/json"},
205241 )
209245 # https://github.com/sloria/webargs/issues/329
210246 def test_invalid_json(self, testapp):
211247 res = testapp.post(
212 "/echo",
248 "/echo_json",
213249 '{"foo": "bar", }',
214250 headers={"Accept": "application/json", "Content-Type": "application/json"},
215251 expect_errors=True,
0 # -*- coding: utf-8 -*-
10 """Tornado request argument parsing module.
21
32 Example: ::
1413 self.write(response)
1514 """
1615 import tornado.web
16 import tornado.concurrent
1717 from tornado.escape import _unicode
1818
1919 from webargs import core
20 from webargs.compat import basestring
21 from webargs.core import json
20 from webargs.multidictproxy import MultiDictProxy
2221
2322
2423 class HTTPError(tornado.web.HTTPError):
2726 def __init__(self, *args, **kwargs):
2827 self.messages = kwargs.pop("messages", {})
2928 self.headers = kwargs.pop("headers", None)
30 super(HTTPError, self).__init__(*args, **kwargs)
29 super().__init__(*args, **kwargs)
3130
3231
33 def parse_json_body(req):
34 """Return the decoded JSON body from the request."""
32 def is_json_request(req):
3533 content_type = req.headers.get("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 {}
34 return content_type is not None and core.is_json(content_type)
4735
4836
49 # From tornado.web.RequestHandler.decode_argument
50 def decode_argument(value, name=None):
51 """Decodes an argument from the request.
37 class WebArgsTornadoMultiDictProxy(MultiDictProxy):
5238 """
53 try:
54 return _unicode(value)
55 except UnicodeDecodeError:
56 raise HTTPError(400, "Invalid unicode in %s: %r" % (name or "url", value[:40]))
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]))
5761
5862
59 def get_value(d, name, field):
60 """Handle gets from 'multidicts' made of lists
63 class WebArgsTornadoCookiesMultiDictProxy(MultiDictProxy):
64 """
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 """
6169
62 It handles cases: ``{"key": [value]}`` and ``{"key": value}``
63 """
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
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
7977
8078
8179 class TornadoParser(core.Parser):
8280 """Tornado request argument parser."""
8381
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)
82 def _raw_load_json(self, req):
83 """Return a json payload from the request for the core parser's load_json
9584
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)
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
9989
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)
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
10394
104 def parse_headers(self, req, name, field):
105 """Pull a value from the header data."""
106 return get_value(req.headers, name, field)
95 return core.parse_json(req.body)
10796
108 def parse_cookies(self, req, name, field):
109 """Pull a value from the header data."""
110 cookie = req.cookies.get(name)
97 def load_querystring(self, req, schema):
98 """Return query params from the request as a MultiDictProxy."""
99 return WebArgsTornadoMultiDictProxy(req.query_arguments, schema)
111100
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
101 def load_form(self, req, schema):
102 """Return form values from the request as a MultiDictProxy."""
103 return WebArgsTornadoMultiDictProxy(req.body_arguments, schema)
116104
117 def parse_files(self, req, name, field):
118 """Pull a file from the request."""
119 return get_value(req.files, name, field)
105 def load_headers(self, req, schema):
106 """Return headers from the request as a MultiDictProxy."""
107 return WebArgsTornadoMultiDictProxy(req.headers, schema)
120108
121 def handle_error(self, error, req, schema, error_status_code, error_headers):
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):
122120 """Handles errors during parsing. Raises a `tornado.web.HTTPError`
123121 with a 400 error.
124122 """
135133 headers=error_headers,
136134 )
137135
138 def handle_invalid_json_error(self, error, req, *args, **kwargs):
136 def _handle_invalid_json_error(self, error, req, *args, **kwargs):
139137 raise HTTPError(
140138 400,
141139 log_message="Invalid JSON body.",
0 # -*- coding: utf-8 -*-
10 """Webapp2 request argument parsing module.
21
32 Example: ::
3029 import webob.multidict
3130
3231 from webargs import core
33 from webargs.core import json
32 from webargs.multidictproxy import MultiDictProxy
3433
3534
3635 class Webapp2Parser(core.Parser):
3736 """webapp2 request argument parser."""
3837
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
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)
4543
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)
44 def load_querystring(self, req, schema):
45 """Return query params from the request as a MultiDictProxy."""
46 return MultiDictProxy(req.GET, schema)
5447
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)
48 def load_form(self, req, schema):
49 """Return form values from the request as a MultiDictProxy."""
50 return MultiDictProxy(req.POST, schema)
5851
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)
52 def load_cookies(self, req, schema):
53 """Return cookies from the request as a MultiDictProxy."""
54 return MultiDictProxy(req.cookies, schema)
6255
63 def parse_cookies(self, req, name, field):
64 """Pull the value from the cookiejar."""
65 return core.get_value(req.cookies, name, field)
56 def load_headers(self, req, schema):
57 """Return headers from the request as a MultiDictProxy."""
58 return MultiDictProxy(req.headers, schema)
6659
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."""
60 def load_files(self, req, schema):
61 """Return files from the request as a MultiDictProxy."""
7362 files = ((k, v) for k, v in req.POST.items() if hasattr(v, "file"))
74 return core.get_value(webob.multidict.MultiDict(files), name, field)
63 return MultiDictProxy(webob.multidict.MultiDict(files), schema)
7564
7665 def get_default_request(self):
7766 return webapp2.get_request()
0 # -*- coding: utf-8 -*-
11
22 import aiohttp
33 from aiohttp.web import json_response
4 from aiohttp import web
54 import marshmallow as ma
65
76 from webargs import fields
2423 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
2524 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
2625
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
2733 ##### Handlers #####
2834
2935
3036 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):
3147 try:
32 parsed = await parser.parse(hello_args, request)
48 parsed = await parser.parse(hello_args, request, location="json")
3349 except json.JSONDecodeError:
34 raise web.HTTPBadRequest(
50 raise aiohttp.web.HTTPBadRequest(
3551 body=json.dumps(["Invalid JSON."]).encode("utf-8"),
3652 content_type="application/json",
3753 )
3854 return json_response(parsed)
3955
4056
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)
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")
5769 async def echo_use_args(request, args):
5870 return json_response(args)
5971
6072
61 @use_kwargs(hello_args)
73 @use_kwargs(hello_args, location="query")
6274 async def echo_use_kwargs(request, name):
6375 return json_response({"name": name})
6476
6577
66 @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42)
78 @use_args(
79 {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form"
80 )
6781 async def echo_use_args_validated(request, args):
6882 return json_response(args)
6983
7084
85 async def echo_ignoring_extra_data(request):
86 return json_response(await parser.parse(hello_exclude_schema, request))
87
88
7189 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):
72100 parsed = await parser.parse(hello_multiple, request)
73101 return json_response(parsed)
74102
75103
76104 async def echo_many_schema(request):
77 parsed = await parser.parse(hello_many_schema, request, locations=("json",))
78 return json_response(parsed)
79
80
81 @use_args({"value": fields.Int()})
105 parsed = await parser.parse(hello_many_schema, request)
106 return json_response(parsed)
107
108
109 @use_args({"value": fields.Int()}, location="query")
82110 async def echo_use_args_with_path_param(request, args):
83111 return json_response(args)
84112
85113
86 @use_kwargs({"value": fields.Int()})
114 @use_kwargs({"value": fields.Int()}, location="query")
87115 async def echo_use_kwargs_with_path_param(request, value):
88116 return json_response({"value": value})
89117
90118
91 @use_args({"page": fields.Int(), "q": fields.Int()}, locations=("query",))
92 @use_args({"name": fields.Str()}, locations=("json",))
119 @use_args({"page": fields.Int(), "q": fields.Int()}, location="query")
120 @use_args({"name": fields.Str()})
93121 async def echo_use_args_multiple(request, query_parsed, json_parsed):
94122 return json_response({"query_parsed": query_parsed, "json_parsed": json_parsed})
95123
104132
105133
106134 async def echo_headers(request):
107 parsed = await parser.parse(hello_args, request, locations=("headers",))
135 parsed = await parser.parse(hello_exclude_schema, request, location="headers")
108136 return json_response(parsed)
109137
110138
111139 async def echo_cookie(request):
112 parsed = await parser.parse(hello_args, request, locations=("cookies",))
140 parsed = await parser.parse(hello_args, request, location="cookies")
113141 return json_response(parsed)
114142
115143
143171
144172
145173 async def echo_match_info(request):
146 parsed = await parser.parse({"mymatch": fields.Int(location="match_info")}, request)
174 parsed = await parser.parse(
175 {"mymatch": fields.Int()}, request, location="match_info"
176 )
147177 return json_response(parsed)
148178
149179
150180 class EchoHandler:
151 @use_args(hello_args)
181 @use_args(hello_args, location="query")
152182 async def get(self, request, args):
153183 return json_response(args)
154184
155185
156 class EchoHandlerView(web.View):
186 class EchoHandlerView(aiohttp.web.View):
157187 @asyncio.coroutine
158 @use_args(hello_args)
188 @use_args(hello_args, location="query")
159189 def get(self, args):
160190 return json_response(args)
161191
162192
163193 @asyncio.coroutine
164 @use_args(HelloSchema, as_kwargs=True)
194 @use_args(HelloSchema, as_kwargs=True, location="query")
165195 def echo_use_schema_as_kwargs(request, name):
166196 return json_response({"name": name})
167197
177207 def create_app():
178208 app = aiohttp.web.Application()
179209
180 add_route(app, ["GET", "POST"], "/echo", echo)
181 add_route(app, ["GET"], "/echo_query", echo_query)
210 add_route(app, ["GET"], "/echo", echo)
211 add_route(app, ["POST"], "/echo_form", echo_form)
182212 add_route(app, ["POST"], "/echo_json", echo_json)
183 add_route(app, ["POST"], "/echo_form", echo_form)
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)
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)
188221 add_route(app, ["GET", "POST"], "/echo_many_schema", echo_many_schema)
189222 add_route(
190223 app,
0 from webargs.core import json
10 from bottle import Bottle, HTTPResponse, debug, request, response
21
32 import marshmallow as ma
43 from webargs import fields
54 from webargs.bottleparser import parser, use_args, use_kwargs
6 from webargs.core import MARSHMALLOW_VERSION_INFO
5 from webargs.core import json, MARSHMALLOW_VERSION_INFO
6
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
1925
2026 app = Bottle()
2127 debug(True)
2228
2329
24 @app.route("/echo", method=["GET", "POST"])
30 @app.route("/echo", method=["GET"])
2531 def echo():
26 return parser.parse(hello_args, request)
32 return parser.parse(hello_args, request, location="query")
2733
2834
29 @app.route("/echo_query")
30 def echo_query():
31 return parser.parse(hello_args, request, locations=("query",))
35 @app.route("/echo_form", method=["POST"])
36 def echo_form():
37 return parser.parse(hello_args, location="form")
3238
3339
3440 @app.route("/echo_json", method=["POST"])
3541 def echo_json():
36 return parser.parse(hello_args, request, locations=("json",))
42 return parser.parse(hello_args, location="json")
3743
3844
39 @app.route("/echo_form", method=["POST"])
40 def echo_form():
41 return parser.parse(hello_args, request, locations=("form",))
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")
4248
4349
44 @app.route("/echo_use_args", method=["GET", "POST"])
45 @use_args(hello_args)
50 @app.route("/echo_use_args", method=["GET"])
51 @use_args(hello_args, location="query")
4652 def echo_use_args(args):
4753 return args
4854
4955
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
5556 @app.route(
5657 "/echo_use_args_validated",
57 method=["GET", "POST"],
58 apply=use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42),
58 method=["POST"],
59 apply=use_args(
60 {"value": fields.Int()},
61 validate=lambda args: args["value"] > 42,
62 location="form",
63 ),
5964 )
6065 def echo_use_args_validated(args):
6166 return args
6267
6368
64 @app.route("/echo_multi", method=["GET", "POST"])
65 def echo_multi():
66 return parser.parse(hello_multiple, request)
69 @app.route("/echo_ignoring_extra_data", method=["POST"])
70 def echo_json_ignore_extra_data():
71 return parser.parse(hello_exclude_schema)
6772
6873
69 @app.route("/echo_many_schema", method=["GET", "POST"])
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"])
7097 def echo_many_schema():
71 arguments = parser.parse(hello_many_schema, request, locations=("json",))
98 arguments = parser.parse(hello_many_schema, request)
7299 return HTTPResponse(body=json.dumps(arguments), content_type="application/json")
73100
74101
75102 @app.route(
76 "/echo_use_args_with_path_param/<name>", apply=use_args({"value": fields.Int()})
103 "/echo_use_args_with_path_param/<name>",
104 apply=use_args({"value": fields.Int()}, location="query"),
77105 )
78106 def echo_use_args_with_path_param(args, name):
79107 return args
80108
81109
82110 @app.route(
83 "/echo_use_kwargs_with_path_param/<name>", apply=use_kwargs({"value": fields.Int()})
111 "/echo_use_kwargs_with_path_param/<name>",
112 apply=use_kwargs({"value": fields.Int()}, location="query"),
84113 )
85114 def echo_use_kwargs_with_path_param(name, value):
86115 return {"value": value}
97126
98127 @app.route("/echo_headers")
99128 def echo_headers():
100 return parser.parse(hello_args, request, locations=("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")
101132
102133
103134 @app.route("/echo_cookie")
104135 def echo_cookie():
105 return parser.parse(hello_args, request, locations=("cookies",))
136 return parser.parse(hello_args, request, location="cookies")
106137
107138
108139 @app.route("/echo_file", method=["POST"])
109140 def echo_file():
110141 args = {"myfile": fields.Field()}
111 result = parser.parse(args, locations=("files",))
142 result = parser.parse(args, location="files")
112143 myfile = result["myfile"]
113144 content = myfile.file.read().decode("utf8")
114145 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
45 urlpatterns = [
56 url(r"^echo$", views.echo),
6 url(r"^echo_query$", views.echo_query),
7 url(r"^echo_form$", views.echo_form),
78 url(r"^echo_json$", views.echo_json),
8 url(r"^echo_form$", views.echo_form),
9 url(r"^echo_json_or_form$", views.echo_json_or_form),
910 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),
1013 url(r"^echo_use_kwargs$", views.echo_use_kwargs),
1114 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),
1217 url(r"^echo_many_schema$", views.echo_many_schema),
1318 url(
1419 r"^echo_use_args_with_path_param/(?P<name>\w+)$",
0 from webargs.core import json
10 from django.http import HttpResponse
21 from django.views.generic import View
2 import marshmallow as ma
33
4 import marshmallow as ma
54 from webargs import fields
65 from webargs.djangoparser import parser, use_args, use_kwargs
7 from webargs.core import MARSHMALLOW_VERSION_INFO
6 from webargs.core import json, MARSHMALLOW_VERSION_INFO
7
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
2026
2127 def json_response(data, **kwargs):
2228 return HttpResponse(json.dumps(data), content_type="application/json", **kwargs)
2329
2430
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)
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
3341
3442
35 def echo_query(request):
36 return json_response(parser.parse(hello_args, request, locations=("query",)))
43 @handle_view_errors
44 def echo(request):
45 return json_response(parser.parse(hello_args, request, location="query"))
3746
3847
39 def echo_json(request):
40 return json_response(parser.parse(hello_args, request, locations=("json",)))
48 @handle_view_errors
49 def echo_form(request):
50 return json_response(parser.parse(hello_args, request, location="form"))
4151
4252
43 def echo_form(request):
44 return json_response(parser.parse(hello_args, request, locations=("form",)))
53 @handle_view_errors
54 def echo_json(request):
55 return json_response(parser.parse(hello_args, request, location="json"))
4556
4657
47 @use_args(hello_args)
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")
4865 def echo_use_args(request, args):
4966 return json_response(args)
5067
5168
52 @use_kwargs(hello_args)
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")
5384 def echo_use_kwargs(request, name):
5485 return json_response({"name": name})
5586
5687
88 @handle_view_errors
5789 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):
58100 return json_response(parser.parse(hello_multiple, request))
59101
60102
103 @handle_view_errors
61104 def echo_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)
105 return json_response(parser.parse(hello_many_schema, request))
68106
69107
70 @use_args({"value": fields.Int()})
108 @handle_view_errors
109 @use_args({"value": fields.Int()}, location="query")
71110 def echo_use_args_with_path_param(request, args, name):
72111 return json_response(args)
73112
74113
75 @use_kwargs({"value": fields.Int()})
114 @handle_view_errors
115 @use_kwargs({"value": fields.Int()}, location="query")
76116 def echo_use_kwargs_with_path_param(request, value, name):
77117 return json_response({"value": value})
78118
79119
120 @handle_view_errors
80121 def always_error(request):
81122 def always_fail(value):
82123 raise ma.ValidationError("something went wrong")
83124
84125 argmap = {"text": fields.Str(validate=always_fail)}
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)
126 return parser.parse(argmap, request)
89127
90128
129 @handle_view_errors
91130 def echo_headers(request):
92 return json_response(parser.parse(hello_args, request, locations=("headers",)))
131 return json_response(
132 parser.parse(hello_exclude_schema, request, location="headers")
133 )
93134
94135
136 @handle_view_errors
95137 def echo_cookie(request):
96 return json_response(parser.parse(hello_args, request, locations=("cookies",)))
138 return json_response(parser.parse(hello_args, request, location="cookies"))
97139
98140
141 @handle_view_errors
99142 def echo_file(request):
100143 args = {"myfile": fields.Field()}
101 result = parser.parse(args, request, locations=("files",))
144 result = parser.parse(args, request, location="files")
102145 myfile = result["myfile"]
103146 content = myfile.read().decode("utf8")
104147 return json_response({"myfile": content})
105148
106149
150 @handle_view_errors
107151 def echo_nested(request):
108152 argmap = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})}
109153 return json_response(parser.parse(argmap, request))
110154
111155
156 @handle_view_errors
112157 def echo_nested_many(request):
113158 argmap = {
114159 "users": fields.Nested({"id": fields.Int(), "name": fields.Str()}, many=True)
117162
118163
119164 class EchoCBV(View):
165 @handle_view_errors
120166 def get(self, request):
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)
167 location_kwarg = {} if request.method == "POST" else {"location": "query"}
168 return json_response(parser.parse(hello_args, self.request, **location_kwarg))
126169
127170 post = get
128171
129172
130173 class EchoUseArgsCBV(View):
131 @use_args(hello_args)
174 @handle_view_errors
175 @use_args(hello_args, location="query")
132176 def get(self, request, args):
133177 return json_response(args)
134178
135 post = get
179 @handle_view_errors
180 @use_args(hello_args)
181 def post(self, request, args):
182 return json_response(args)
136183
137184
138185 class EchoUseArgsWithParamCBV(View):
139 @use_args(hello_args)
186 @handle_view_errors
187 @use_args(hello_args, location="query")
140188 def get(self, request, args, pid):
141189 return json_response(args)
142190
143 post = get
191 @handle_view_errors
192 @use_args(hello_args)
193 def post(self, request, args, pid):
194 return json_response(args)
0 from webargs.core import json
1
20 import falcon
31 import marshmallow as ma
2
43 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
76
87 hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)}
98 hello_multiple = {"name": fields.List(fields.Str())}
1615 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
1716 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
1817
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
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)
3123
3224
33 class EchoQuery(object):
25 class Echo:
3426 def on_get(self, req, resp):
35 parsed = parser.parse(hello_args, req, locations=("query",))
27 parsed = parser.parse(hello_args, req, location="query")
3628 resp.body = json.dumps(parsed)
3729
3830
39 class EchoJSON(object):
31 class EchoForm:
4032 def on_post(self, req, resp):
41 parsed = parser.parse(hello_args, req, locations=("json",))
33 parsed = parser.parse(hello_args, req, location="form")
4234 resp.body = json.dumps(parsed)
4335
4436
45 class EchoForm(object):
37 class EchoJSON:
4638 def on_post(self, req, resp):
47 parsed = parser.parse(hello_args, req, locations=("form",))
39 parsed = parser.parse(hello_args, req, location="json")
4840 resp.body = json.dumps(parsed)
4941
5042
51 class EchoUseArgs(object):
52 @use_args(hello_args)
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")
5351 def on_get(self, req, resp, args):
5452 resp.body = json.dumps(args)
5553
56 on_post = on_get
5754
58
59 class EchoUseKwargs(object):
60 @use_kwargs(hello_args)
55 class EchoUseKwargs:
56 @use_kwargs(hello_args, location="query")
6157 def on_get(self, req, resp, name):
6258 resp.body = json.dumps({"name": name})
6359
64 on_post = on_get
60
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):
68 resp.body = json.dumps(args)
6569
6670
67 class EchoUseArgsValidated(object):
68 @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42)
69 def on_get(self, req, resp, args):
70 resp.body = json.dumps(args)
71
72 on_post = on_get
71 class EchoJSONIgnoreExtraData:
72 def on_post(self, req, resp):
73 resp.body = json.dumps(parser.parse(hello_exclude_schema, req))
7374
7475
75 class EchoMulti(object):
76 class EchoMulti:
7677 def on_get(self, req, resp):
78 resp.body = json.dumps(parser.parse(hello_multiple, req, location="query"))
79
80
81 class EchoMultiForm:
82 def on_post(self, req, resp):
83 resp.body = json.dumps(parser.parse(hello_multiple, req, location="form"))
84
85
86 class EchoMultiJSON:
87 def on_post(self, req, resp):
7788 resp.body = json.dumps(parser.parse(hello_multiple, req))
7889
79 on_post = on_get
90
91 class EchoManySchema:
92 def on_post(self, req, resp):
93 resp.body = json.dumps(parser.parse(hello_many_schema, req))
8094
8195
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
89
90
91 class EchoUseArgsWithPathParam(object):
92 @use_args({"value": fields.Int()})
96 class EchoUseArgsWithPathParam:
97 @use_args({"value": fields.Int()}, location="query")
9398 def on_get(self, req, resp, args, name):
9499 resp.body = json.dumps(args)
95100
96101
97 class EchoUseKwargsWithPathParam(object):
98 @use_kwargs({"value": fields.Int()})
102 class EchoUseKwargsWithPathParam:
103 @use_kwargs({"value": fields.Int()}, location="query")
99104 def on_get(self, req, resp, value, name):
100105 resp.body = json.dumps({"value": value})
101106
102107
103 class AlwaysError(object):
108 class AlwaysError:
104109 def on_get(self, req, resp):
105110 def always_fail(value):
106111 raise ma.ValidationError("something went wrong")
111116 on_post = on_get
112117
113118
114 class EchoHeaders(object):
119 class EchoHeaders:
115120 def on_get(self, req, resp):
116 resp.body = json.dumps(parser.parse(hello_args, req, locations=("headers",)))
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 )
117127
118128
119 class EchoCookie(object):
129 class EchoCookie:
120130 def on_get(self, req, resp):
121 resp.body = json.dumps(parser.parse(hello_args, req, locations=("cookies",)))
131 resp.body = json.dumps(parser.parse(hello_args, req, location="cookies"))
122132
123133
124 class EchoNested(object):
134 class EchoNested:
125135 def on_post(self, req, resp):
126136 args = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})}
127137 resp.body = json.dumps(parser.parse(args, req))
128138
129139
130 class EchoNestedMany(object):
140 class EchoNestedMany:
131141 def on_post(self, req, resp):
132142 args = {
133143 "users": fields.Nested(
138148
139149
140150 def use_args_hook(args, context_key="args", **kwargs):
141 def hook(req, resp, params):
151 def hook(req, resp, resource, params):
142152 parsed_args = parser.parse(args, req=req, **kwargs)
143153 req.context[context_key] = parsed_args
144154
145155 return hook
146156
147157
148 @falcon.before(use_args_hook(hello_args))
149 class EchoUseArgsHook(object):
158 @falcon.before(use_args_hook(hello_args, location="query"))
159 class EchoUseArgsHook:
150160 def on_get(self, req, resp):
151161 resp.body = json.dumps(req.context["args"])
152162
154164 def create_app():
155165 app = falcon.API()
156166 app.add_route("/echo", Echo())
157 app.add_route("/echo_query", EchoQuery())
167 app.add_route("/echo_form", EchoForm())
158168 app.add_route("/echo_json", EchoJSON())
159 app.add_route("/echo_form", EchoForm())
169 app.add_route("/echo_json_or_form", EchoJSONOrForm())
160170 app.add_route("/echo_use_args", EchoUseArgs())
161171 app.add_route("/echo_use_kwargs", EchoUseKwargs())
162172 app.add_route("/echo_use_args_validated", EchoUseArgsValidated())
173 app.add_route("/echo_ignoring_extra_data", EchoJSONIgnoreExtraData())
163174 app.add_route("/echo_multi", EchoMulti())
175 app.add_route("/echo_multi_form", EchoMultiForm())
176 app.add_route("/echo_multi_json", EchoMultiJSON())
164177 app.add_route("/echo_many_schema", EchoManySchema())
165178 app.add_route("/echo_use_args_with_path_param/{name}", EchoUseArgsWithPathParam())
166179 app.add_route(
0 from webargs.core import json
10 from flask import Flask, jsonify as J, Response, request
21 from flask.views import MethodView
3
42 import marshmallow as ma
3
54 from webargs import fields
65 from webargs.flaskparser import parser, use_args, use_kwargs
7 from webargs.core import MARSHMALLOW_VERSION_INFO
6 from webargs.core import json, MARSHMALLOW_VERSION_INFO
87
98
109 class TestAppConfig:
2221 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
2322 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
2423
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
2530 app = Flask(__name__)
2631 app.config.from_object(TestAppConfig)
2732
2833
29 @app.route("/echo", methods=["GET", "POST"])
34 @app.route("/echo", methods=["GET"])
3035 def echo():
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",)))
36 return J(parser.parse(hello_args, location="query"))
37
38
39 @app.route("/echo_form", methods=["POST"])
40 def echo_form():
41 return J(parser.parse(hello_args, location="form"))
3742
3843
3944 @app.route("/echo_json", methods=["POST"])
4045 def echo_json():
41 return J(parser.parse(hello_args, request, locations=("json",)))
42
43
44 @app.route("/echo_form", methods=["POST"])
45 def echo_form():
46 return J(parser.parse(hello_args, request, locations=("form",)))
47
48
49 @app.route("/echo_use_args", methods=["GET", "POST"])
50 @use_args(hello_args)
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")
5156 def echo_use_args(args):
5257 return J(args)
5358
5459
55 @app.route("/echo_use_args_validated", methods=["GET", "POST"])
56 @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42)
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 )
5764 def echo_use_args_validated(args):
5865 return J(args)
5966
6067
61 @app.route("/echo_use_kwargs", methods=["GET", "POST"])
62 @use_kwargs(hello_args)
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")
6375 def echo_use_kwargs(name):
6476 return J({"name": name})
6577
6678
67 @app.route("/echo_multi", methods=["GET", "POST"])
79 @app.route("/echo_multi", methods=["GET"])
6880 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():
6991 return J(parser.parse(hello_multiple))
7092
7193
7294 @app.route("/echo_many_schema", methods=["GET", "POST"])
7395 def many_nested():
74 arguments = parser.parse(hello_many_schema, locations=("json",))
96 arguments = parser.parse(hello_many_schema)
7597 return Response(json.dumps(arguments), content_type="application/json")
7698
7799
78100 @app.route("/echo_use_args_with_path_param/<name>")
79 @use_args({"value": fields.Int()})
101 @use_args({"value": fields.Int()}, location="query")
80102 def echo_use_args_with_path(args, name):
81103 return J(args)
82104
83105
84106 @app.route("/echo_use_kwargs_with_path_param/<name>")
85 @use_kwargs({"value": fields.Int()})
107 @use_kwargs({"value": fields.Int()}, location="query")
86108 def echo_use_kwargs_with_path(name, value):
87109 return J({"value": value})
88110
98120
99121 @app.route("/echo_headers")
100122 def echo_headers():
101 return J(parser.parse(hello_args, locations=("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"))
126
127
128 @app.route("/echo_headers_raising")
129 @use_args(HelloSchema(**strict_kwargs), location="headers")
130 def echo_headers_raising(args):
131 # as above, but in this case, don't use the exclude schema (so unexpected
132 # headers will raise errors)
133 return J(args)
102134
103135
104136 @app.route("/echo_cookie")
105137 def echo_cookie():
106 return J(parser.parse(hello_args, request, locations=("cookies",)))
138 return J(parser.parse(hello_args, request, location="cookies"))
107139
108140
109141 @app.route("/echo_file", methods=["POST"])
110142 def echo_file():
111143 args = {"myfile": fields.Field()}
112 result = parser.parse(args, locations=("files",))
144 result = parser.parse(args, location="files")
113145 fp = result["myfile"]
114146 content = fp.read().decode("utf8")
115147 return J({"myfile": content})
117149
118150 @app.route("/echo_view_arg/<view_arg>")
119151 def echo_view_arg(view_arg):
120 return J(parser.parse({"view_arg": fields.Int()}, locations=("view_args",)))
152 return J(parser.parse({"view_arg": fields.Int()}, location="view_args"))
121153
122154
123155 @app.route("/echo_view_arg_use_args/<view_arg>")
124 @use_args({"view_arg": fields.Int(location="view_args")})
156 @use_args({"view_arg": fields.Int()}, location="view_args")
125157 def echo_view_arg_with_use_args(args, **kwargs):
126158 return J(args)
127159
186218 def handle_error(err):
187219 if err.code == 422:
188220 assert isinstance(err.data["schema"], ma.Schema)
189 return J(err.data["messages"]), err.code
221
222 if MARSHMALLOW_VERSION_INFO[0] >= 3:
223 return J(err.data["messages"]), err.code
224
225 # on marshmallow2, validation errors for nested schemas can fail to encode:
226 # https://github.com/marshmallow-code/marshmallow/issues/493
227 # to workaround this, convert integer keys to strings
228 def tweak_data(value):
229 if not isinstance(value, dict):
230 return value
231 return {str(k): v for k, v in value.items()}
232
233 return J({k: tweak_data(v) for k, v in err.data["messages"].items()}), err.code
0 from webargs.core import json
1
20 from pyramid.config import Configurator
31 from pyramid.httpexceptions import HTTPBadRequest
42 import marshmallow as ma
53
64 from webargs import fields
75 from webargs.pyramidparser import parser, use_args, use_kwargs
8 from webargs.core import MARSHMALLOW_VERSION_INFO
6 from webargs.core import json, MARSHMALLOW_VERSION_INFO
97
108 hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)}
119 hello_multiple = {"name": fields.List(fields.Str())}
1816 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
1917 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
2018
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
2125
2226 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):
2335 try:
24 return parser.parse(hello_args, request)
36 return parser.parse(hello_args, request, location="json")
2537 except json.JSONDecodeError:
2638 error = HTTPBadRequest()
2739 error.body = json.dumps(["Invalid JSON."]).encode("utf-8")
2941 raise error
3042
3143
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)
57 except json.JSONDecodeError:
58 error = HTTPBadRequest()
59 error.body = json.dumps(["Invalid JSON."]).encode("utf-8")
60 error.content_type = "application/json"
61 raise error
62
63
3264 def echo_query(request):
33 return parser.parse(hello_args, request, locations=("query",))
34
35
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)
65 return parser.parse(hello_args, request, location="query")
66
67
68 @use_args(hello_args, location="query")
4569 def echo_use_args(request, args):
4670 return args
4771
4872
49 @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42)
73 @use_args(
74 {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form"
75 )
5076 def echo_use_args_validated(request, args):
5177 return args
5278
5379
54 @use_kwargs(hello_args)
80 @use_kwargs(hello_args, location="query")
5581 def echo_use_kwargs(request, name):
5682 return {"name": name}
5783
5884
5985 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):
6094 return parser.parse(hello_multiple, request)
6195
6296
6397 def echo_many_schema(request):
64 return parser.parse(hello_many_schema, request, locations=("json",))
65
66
67 @use_args({"value": fields.Int()})
98 return parser.parse(hello_many_schema, request)
99
100
101 @use_args({"value": fields.Int()}, location="query")
68102 def echo_use_args_with_path_param(request, args):
69103 return args
70104
71105
72 @use_kwargs({"value": fields.Int()})
106 @use_kwargs({"value": fields.Int()}, location="query")
73107 def echo_use_kwargs_with_path_param(request, value):
74108 return {"value": value}
75109
83117
84118
85119 def echo_headers(request):
86 return parser.parse(hello_args, request, locations=("headers",))
120 return parser.parse(hello_exclude_schema, request, location="headers")
87121
88122
89123 def echo_cookie(request):
90 return parser.parse(hello_args, request, locations=("cookies",))
124 return parser.parse(hello_args, request, location="cookies")
91125
92126
93127 def echo_file(request):
94128 args = {"myfile": fields.Field()}
95 result = parser.parse(args, request, locations=("files",))
129 result = parser.parse(args, request, location="files")
96130 myfile = result["myfile"]
97131 content = myfile.file.read().decode("utf8")
98132 return {"myfile": content}
111145
112146
113147 def echo_matchdict(request):
114 return parser.parse({"mymatch": fields.Int()}, request, locations=("matchdict",))
115
116
117 class EchoCallable(object):
148 return parser.parse({"mymatch": fields.Int()}, request, location="matchdict")
149
150
151 class EchoCallable:
118152 def __init__(self, request):
119153 self.request = request
120154
121 @use_args({"value": fields.Int()})
155 @use_args({"value": fields.Int()}, location="query")
122156 def __call__(self, args):
123157 return args
124158
134168 config = Configurator()
135169
136170 add_route(config, "/echo", echo)
171 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)
137174 add_route(config, "/echo_query", echo_query)
138 add_route(config, "/echo_json", echo_json)
139 add_route(config, "/echo_form", echo_form)
175 add_route(config, "/echo_ignoring_extra_data", echo_json_ignore_extra_data)
140176 add_route(config, "/echo_use_args", echo_use_args)
141177 add_route(config, "/echo_use_args_validated", echo_use_args_validated)
142178 add_route(config, "/echo_use_kwargs", echo_use_kwargs)
143179 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)
144182 add_route(config, "/echo_many_schema", echo_many_schema)
145183 add_route(
146184 config, "/echo_use_args_with_path_param/{name}", echo_use_args_with_path_param
+0
-16
tests/compat.py less more
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 -*-
10 import itertools
2 import mock
31 import datetime
42
53 import pytest
86 from django.utils.datastructures import MultiValueDict as DjMultiDict
97 from bottle import MultiDict as BotMultiDict
108
11 from webargs import fields, missing, ValidationError
9 from webargs import fields, ValidationError
1210 from webargs.core import (
1311 Parser,
14 get_value,
1512 dict2schema,
1613 is_json,
1714 get_mimetype,
1815 MARSHMALLOW_VERSION_INFO,
1916 )
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
2025
2126
2227 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
2631 def __init__(self, status_code, headers):
2732 self.status_code = status_code
2833 self.headers = headers
29 super(MockHTTPError, self).__init__(self, "HTTP Error occurred")
34 super().__init__(self, "HTTP Error occurred")
3035
3136
3237 class MockRequestParser(Parser):
3338 """A minimal parser implementation that parses mock requests."""
3439
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)
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
4348
4449
4550 @pytest.yield_fixture(scope="function")
5863 # Parser tests
5964
6065
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()
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}
6470 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
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}
116102 argmap = {"username": fields.Field(), "password": fields.Field()}
117 p = Parser()
118 ret = p.parse(argmap, web_request)
103 ret = parser.parse(argmap, web_request)
119104 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
120133
121134
122135 def test_parse_required_arg_raises_validation_error(parser, web_request):
140153 assert result == {"first": "Steve", "last": None}
141154
142155
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
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}
150160
151161
152162 def test_parse_required_list(parser, web_request):
154164 args = {"foo": fields.List(fields.Field(), required=True)}
155165 with pytest.raises(ValidationError) as excinfo:
156166 parser.parse(args, web_request)
157 assert excinfo.value.messages["foo"][0] == "Missing data for required field."
167 assert (
168 excinfo.value.messages["json"]["foo"][0] == "Missing data for required field."
169 )
158170
159171
160172 # Regression test for https://github.com/marshmallow-code/webargs/issues/107
169181 args = {"foo": fields.List(fields.Field(), allow_none=False)}
170182 with pytest.raises(ValidationError) as excinfo:
171183 parser.parse(args, web_request)
172 assert excinfo.value.messages["foo"][0] == "Field may not be null."
184 assert excinfo.value.messages["json"]["foo"][0] == "Field may not be null."
173185
174186
175187 def test_parse_empty_list(parser, web_request):
184196 assert parser.parse(args, web_request) == {}
185197
186198
187 def test_default_locations():
188 assert set(Parser.DEFAULT_LOCATIONS) == set(["json", "querystring", "form"])
199 def test_default_location():
200 assert Parser.DEFAULT_LOCATION == "json"
189201
190202
191203 def test_missing_with_default(parser, web_request):
192204 web_request.json = {}
193205 args = {"val": fields.Field(missing="pizza")}
194 result = parser.parse(args, web_request, locations=("json",))
206 result = parser.parse(args, web_request)
195207 assert result["val"] == "pizza"
196208
197209
198210 def test_default_can_be_none(parser, web_request):
199211 web_request.json = {}
200212 args = {"val": fields.Field(missing=None, allow_none=True)}
201 result = parser.parse(args, web_request, locations=("json",))
213 result = parser.parse(args, web_request)
202214 assert result["val"] is None
203215
204216
209221 "p": fields.Int(
210222 missing=1,
211223 validate=lambda p: p > 0,
212 error=u"La page demandée n'existe pas",
224 error="La page demandée n'existe pas",
213225 location="querystring",
214226 )
215227 }
216228 assert parser.parse(args, web_request) == {"p": 1}
217229
218230
219 def test_value_error_raised_if_parse_arg_called_with_invalid_location(web_request):
231 def test_value_error_raised_if_parse_called_with_invalid_location(parser, web_request):
220232 field = fields.Field()
233 with pytest.raises(ValueError, match="Invalid location argument: invalidlocation"):
234 parser.parse({"foo": field}, web_request, location="invalidlocation")
235
236
237 @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
221242 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)
233
234
235 @mock.patch("webargs.core.Parser.handle_error")
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",))
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)
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(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",))
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 = {}
259266 p.parse({"foo": fields.Field()}, 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):
267 assert load_headers.called
268
269
270 def test_custom_error_handler(web_request):
272271 class CustomError(Exception):
273272 pass
274273
275 def error_handler(error, req, schema, status_code, headers):
274 def error_handler(error, req, schema, *, error_status_code, error_headers):
276275 assert isinstance(schema, Schema)
277276 raise CustomError(error)
278277
279 parse_json.side_effect = ValidationError("parse_json failed")
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
280287 p = Parser(error_handler=error_handler)
281288 with pytest.raises(CustomError):
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):
289 p.parse(myschema, web_request, validate=failing_validate_func)
290
291
292 def test_custom_error_handler_decorator(web_request):
287293 class CustomError(Exception):
288294 pass
289295
290 parse_json.side_effect = ValidationError("parse_json failed")
291
296 mock_schema = mock.Mock(spec=Schema)
297 mock_schema.strict = True
298 mock_schema.load.side_effect = ValidationError("parsing json failed")
292299 parser = Parser()
293300
294301 @parser.error_handler
295 def handle_error(error, req, schema, status_code, headers):
302 def handle_error(error, req, schema, *, error_status_code, error_headers):
296303 assert isinstance(schema, Schema)
297304 raise CustomError(error)
298305
299306 with pytest.raises(CustomError):
300 parser.parse({"foo": fields.Field()}, web_request)
301
302
303 def test_custom_location_handler(web_request):
307 parser.parse(mock_schema, web_request)
308
309
310 def test_custom_location_loader(web_request):
304311 web_request.data = {"foo": 42}
305312
306313 parser = Parser()
307314
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",))
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")
313320 assert result["foo"] == 42
314321
315322
316 def test_custom_location_handler_with_data_key(web_request):
323 def test_custom_location_loader_with_data_key(web_request):
317324 web_request.data = {"X-Foo": 42}
318325 parser = Parser()
319326
320 @parser.location_handler("data")
321 def parse_data(req, name, arg):
322 return req.data.get(name, missing)
327 @parser.location_loader("data")
328 def load_data(req, schema):
329 return req.data
323330
324331 data_key_kwarg = {
325332 "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "X-Foo"
326333 }
327334 result = parser.parse(
328 {"x_foo": fields.Int(**data_key_kwarg)}, web_request, locations=("data",)
335 {"x_foo": fields.Int(**data_key_kwarg)}, web_request, location="data"
329336 )
330337 assert result["x_foo"] == 42
331338
332339
333 def test_full_input_validation(web_request):
340 def test_full_input_validation(parser, web_request):
334341
335342 web_request.json = {"foo": 41, "bar": 42}
336343
337 parser = MockRequestParser()
338344 args = {"foo": fields.Int(), "bar": fields.Int()}
339345 with pytest.raises(ValidationError):
340346 # Test that `validate` receives dictionary of args
341 parser.parse(
342 args,
343 web_request,
344 locations=("json",),
345 validate=lambda args: args["foo"] > args["bar"],
346 )
347 parser.parse(args, web_request, validate=lambda args: args["foo"] > args["bar"])
347348
348349
349350 def test_full_input_validation_with_multiple_validators(web_request, parser):
359360 web_request.json = {"a": 2, "b": 1}
360361 validators = [validate1, validate2]
361362 with pytest.raises(ValidationError, match="b must be > a"):
362 parser.parse(args, web_request, locations=("json",), validate=validators)
363 parser.parse(args, web_request, validate=validators)
363364
364365 web_request.json = {"a": 1, "b": 2}
365366 with pytest.raises(ValidationError, match="a must be > b"):
366 parser.parse(args, web_request, locations=("json",), validate=validators)
367
368
369 def test_required_with_custom_error(web_request):
370 web_request.json = {}
371 parser = MockRequestParser()
367 parser.parse(args, web_request, validate=validators)
368
369
370 def test_required_with_custom_error(parser, web_request):
371 web_request.json = {}
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, locations=("json",))
378
379 assert "We need foo" in excinfo.value.messages["foo"]
377 parser.parse(args, web_request)
378
379 assert "We need foo" in excinfo.value.messages["json"]["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(web_request):
384 def test_required_with_custom_error_and_validation_error(parser, web_request):
385385 web_request.json = {"foo": ""}
386 parser = MockRequestParser()
387386 args = {
388387 "foo": fields.Str(
389388 required="We need foo",
393392 }
394393 with pytest.raises(ValidationError) as excinfo:
395394 # Test that `validate` receives dictionary of args
396 parser.parse(args, web_request, locations=("json",))
395 parser.parse(args, web_request)
397396
398397 assert "foo required length is 3" in excinfo.value.args[0]["foo"]
399398 if MARSHMALLOW_VERSION_INFO[0] < 3:
404403 def validate(val):
405404 return False
406405
407 text = u"øœ∑∆∑"
406 text = "øœ∑∆∑"
408407 web_request.json = {"text": text}
409408 parser = MockRequestParser()
410409 args = {"text": fields.Str()}
411410 with pytest.raises(ValidationError) as excinfo:
412 parser.parse(args, web_request, locations=("json",), validate=validate)
413 assert excinfo.value.messages == ["Invalid value."]
411 parser.parse(args, web_request, validate=validate)
412 assert excinfo.value.messages == {"json": ["Invalid value."]}
414413
415414
416415 def test_invalid_argument_for_validate(web_request, parser):
417416 with pytest.raises(ValueError) as excinfo:
418417 parser.parse({}, web_request, validate="notcallable")
419418 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
428419
429420
430421 def create_bottle_multi_dict():
442433
443434
444435 @pytest.mark.parametrize("input_dict", multidicts)
445 def test_get_value_multidict(input_dict):
446 field = fields.List(fields.Str())
447 assert get_value(input_dict, "foos", field) == ["a", "b"]
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")
448454
449455
450456 def test_parse_with_data_key(web_request):
455461 "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "Content-Type"
456462 }
457463 args = {"content_type": fields.Field(**data_key_kwargs)}
458 parsed = parser.parse(args, web_request, locations=("json",))
464 parsed = parser.parse(args, web_request)
459465 assert parsed == {"content_type": "application/json"}
460466
461467
469475
470476 parser = MockRequestParser()
471477 args = {"content_type": fields.Field(load_from="Content-Type")}
472 parsed = parser.parse(args, web_request, locations=("json",))
478 parsed = parser.parse(args, web_request)
473479 assert parsed == {"content_type": "application/json"}
474480
475481
482488 }
483489 args = {"content_type": fields.Str(**data_key_kwargs)}
484490 with pytest.raises(ValidationError) as excinfo:
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."]
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."]
488495
489496
490497 def test_parse_nested_with_data_key(web_request):
495502 }
496503 args = {"nested_arg": fields.Nested({"right": fields.Field(**data_key_kwarg)})}
497504
498 parsed = parser.parse(args, web_request, locations=("json",))
505 parsed = parser.parse(args, web_request)
499506 assert parsed == {"nested_arg": {"right": "OK"}}
500507
501508
512519 )
513520 }
514521
515 parsed = parser.parse(args, web_request, locations=("json",))
522 parsed = parser.parse(args, web_request)
516523 assert parsed == {"nested_arg": {"found": None}}
517524
518525
522529 web_request.json = {"nested_arg": {}}
523530 args = {"nested_arg": fields.Nested({"miss": fields.Field(missing="<foo>")})}
524531
525 parsed = parser.parse(args, web_request, locations=("json",))
532 parsed = parser.parse(args, web_request)
526533 assert parsed == {"nested_arg": {"miss": "<foo>"}}
527534
528535
553560 web_request.json = {"username": "foo"}
554561 web_request.query = {"page": 42}
555562
556 @parser.use_args(query_args, web_request, locations=("query",))
557 @parser.use_args(json_args, web_request, locations=("json",))
563 @parser.use_args(query_args, web_request, location="query")
564 @parser.use_args(json_args, web_request)
558565 def viewfunc(query_parsed, json_parsed):
559566 return {"json": json_parsed, "query": query_parsed}
560567
569576 web_request.json = {"username": "foo"}
570577 web_request.query = {"page": 42}
571578
572 @parser.use_kwargs(query_args, web_request, locations=("query",))
573 @parser.use_kwargs(json_args, web_request, locations=("json",))
579 @parser.use_kwargs(query_args, web_request, location="query")
580 @parser.use_kwargs(json_args, web_request)
574581 def viewfunc(page, username):
575582 return {"json": {"username": username}, "query": {"page": page}}
576583
591598
592599 def test_list_allowed_missing(web_request, parser):
593600 args = {"name": fields.List(fields.Str())}
594 web_request.json = {"fakedata": True}
601 web_request.json = {}
595602 result = parser.parse(args, web_request)
596603 assert result == {}
597604
598605
599606 def test_int_list_allowed_missing(web_request, parser):
600607 args = {"name": fields.List(fields.Int())}
601 web_request.json = {"fakedata": True}
608 web_request.json = {}
602609 result = parser.parse(args, web_request)
603610 assert result == {}
604611
605612
606613 def test_multiple_arg_required_with_int_conversion(web_request, parser):
607614 args = {"ids": fields.List(fields.Int(), required=True)}
608 web_request.json = {"fakedata": True}
615 web_request.json = {}
609616 with pytest.raises(ValidationError) as excinfo:
610617 parser.parse(args, web_request)
611 assert excinfo.value.messages == {"ids": ["Missing data for required field."]}
618 assert excinfo.value.messages == {
619 "json": {"ids": ["Missing data for required field."]}
620 }
612621
613622
614623 def test_parse_with_callable(web_request, parser):
746755 assert "strict=True" in str(warning.message)
747756
748757 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
749770 web_request.json = {"email": "[email protected]", "password": "bar", "page": 42}
750771
751 @parser.use_kwargs({"page": fields.Int()}, web_request)
752 @parser.use_kwargs(self.UserSchema(**strict_kwargs), web_request)
772 @parser.use_kwargs(pageschema, web_request)
773 @parser.use_kwargs(userschema, web_request)
753774 def viewfunc(email, password, page):
754775 return {"email": email, "password": password, "page": page}
755776
773794 return True
774795
775796 web_request.json = {"name": "Eric Cartman"}
776 res = parser.parse(UserSchema, web_request, locations=("json",))
797 res = parser.parse(UserSchema, web_request)
777798 assert res == {"name": "Eric Cartman"}
778799
779800
780 def test_use_args_with_custom_locations_in_parser(web_request, parser):
801 def test_use_args_with_custom_location_in_parser(web_request, parser):
781802 custom_args = {"foo": fields.Str()}
782803 web_request.json = {}
783 parser.locations = ("custom",)
784
785 @parser.location_handler("custom")
786 def parse_custom(req, name, arg):
787 return "bar"
804 parser.location = "custom"
805
806 @parser.location_loader("custom")
807 def load_custom(schema, req):
808 return {"foo": "bar"}
788809
789810 @parser.use_args(custom_args, web_request)
790811 def viewfunc(args):
826847
827848 dumped = schema.dump(parsed)
828849 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"}
850 assert data["ids"] == "1,2,3"
851
852
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"}
834863 schema_cls = dict2schema(
835 {"ids": fields.DelimitedList(fields.Int(), as_string=True)}
864 {
865 "ids": fields.DelimitedTuple(
866 (fields.Int, fields.Int, fields.DateTime(format="%Y-%m-%d"))
867 )
868 }
836869 )
837870 schema = schema_cls()
838871
839872 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
844 assert data["ids"] == "1,2,3"
845
846
847 def test_delimited_list_as_string_v2(web_request, parser):
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 """
848900 web_request.json = {"dates": "2018-11-01,2018-11-02"}
849901 schema_cls = dict2schema(
850 {
851 "dates": fields.DelimitedList(
852 fields.DateTime(format="%Y-%m-%d"), as_string=True
853 )
854 }
902 {"dates": fields.DelimitedList(fields.DateTime(format="%Y-%m-%d"))}
855903 )
856904 schema = schema_cls()
857905
874922 parsed = parser.parse(schema, web_request)
875923 assert parsed["ids"] == [1, 2, 3]
876924
877
878 def test_delimited_list_load_list(web_request, parser):
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):
879948 web_request.json = {"ids": [1, 2, 3]}
880949 schema_cls = dict2schema({"ids": fields.DelimitedList(fields.Int())})
881950 schema = schema_cls()
882951
883 parsed = parser.parse(schema, web_request)
884 assert parsed["ids"] == [1, 2, 3]
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."]
885974
886975
887976 # Regresion test for https://github.com/marshmallow-code/webargs/issues/149
892981
893982 with pytest.raises(ValidationError) as excinfo:
894983 parser.parse(schema, web_request)
895 assert excinfo.value.messages == {"ids": ["Not a valid list."]}
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."]}}
896998
897999
8981000 def test_missing_list_argument_not_in_parsed_result(web_request, parser):
9101012 msg = "Missing data for required field."
9111013 with pytest.raises(ValidationError, match=msg):
9121014 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
9231015
9241016
9251017 def test_validation_errors_in_validator_are_passed_to_handle_error(parser, web_request):
10191111
10201112
10211113 class MockRequestParserWithErrorHandler(MockRequestParser):
1022 def handle_error(
1023 self, error, req, schema, error_status_code=None, error_headers=None
1024 ):
1114 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
10251115 assert isinstance(error, ValidationError)
10261116 assert isinstance(schema, Schema)
10271117 raise MockHTTPError(error_status_code, error_headers)
10401130 assert error.headers == {"X-Foo": "bar"}
10411131
10421132
1043 @mock.patch("webargs.core.Parser.parse_json")
1044 def test_custom_schema_class(parse_json, web_request):
1133 @mock.patch("webargs.core.Parser.load_json")
1134 def test_custom_schema_class(load_json, web_request):
10451135 class CustomSchema(Schema):
10461136 @pre_load
10471137 def pre_load(self, data, **kwargs):
10481138 data["value"] += " world"
10491139 return data
10501140
1051 parse_json.return_value = "hello"
1141 load_json.return_value = {"value": "hello"}
10521142 argmap = {"value": fields.Str()}
10531143 p = Parser(schema_class=CustomSchema)
10541144 ret = p.parse(argmap, web_request)
10551145 assert ret == {"value": "hello world"}
10561146
10571147
1058 @mock.patch("webargs.core.Parser.parse_json")
1059 def test_custom_default_schema_class(parse_json, web_request):
1148 @mock.patch("webargs.core.Parser.load_json")
1149 def test_custom_default_schema_class(load_json, web_request):
10601150 class CustomSchema(Schema):
10611151 @pre_load
10621152 def pre_load(self, data, **kwargs):
10661156 class CustomParser(Parser):
10671157 DEFAULT_SCHEMA_CLASS = CustomSchema
10681158
1069 parse_json.return_value = "hello"
1159 load_json.return_value = {"value": "hello"}
10701160 argmap = {"value": fields.Str()}
10711161 p = CustomParser()
10721162 ret = p.parse(argmap, web_request)
0 # -*- coding: utf-8 -*-
1 from __future__ import unicode_literals
2
30 import pytest
41 from tests.apps.django_app.base.wsgi import application
52
2219
2320 def test_parsing_in_class_based_view(self, testapp):
2421 assert testapp.get("/echo_cbv?name=Fred").json == {"name": "Fred"}
25 assert testapp.post("/echo_cbv", {"name": "Fred"}).json == {"name": "Fred"}
22 assert testapp.post_json("/echo_cbv", {"name": "Fred"}).json == {"name": "Fred"}
2623
2724 def test_use_args_in_class_based_view(self, testapp):
2825 res = testapp.get("/echo_use_args_cbv?name=Fred")
2926 assert res.json == {"name": "Fred"}
30 res = testapp.post("/echo_use_args_cbv", {"name": "Fred"})
27 res = testapp.post_json("/echo_use_args_cbv", {"name": "Fred"})
3128 assert res.json == {"name": "Fred"}
3229
3330 def test_use_args_in_class_based_view_with_path_param(self, testapp):
0 # -*- coding: utf-8 -*-
10 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",
21 "/echo_json",
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",
33 "/echo_json",
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(
51 "/echo_json",
52 json={"name": "Fred"},
53 )
54 assert res.json == {"name": "Fred"}
0 # -*- coding: utf-8 -*-
1 from __future__ import unicode_literals
2 import threading
3
40 from werkzeug.exceptions import HTTPException
5 import mock
61 import pytest
72
83 from flask import Flask
9 from webargs import fields, ValidationError, missing
4 from webargs import fields, ValidationError, missing, dict2schema
105 from webargs.flaskparser import parser, abort
116 from webargs.core import MARSHMALLOW_VERSION_INFO, json
127
138 from .apps.flask_app import app
149 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
1517
1618
1719 class TestFlaskParser(CommonTestCase):
2527 def test_parsing_invalid_view_arg(self, testapp):
2628 res = testapp.get("/echo_view_arg/foo", expect_errors=True)
2729 assert res.status_code == 422
28 assert res.json == {"view_arg": ["Not a valid integer."]}
30 assert res.json == {"view_args": {"view_arg": ["Not a valid integer."]}}
2931
3032 def test_use_args_with_view_args_parsing(self, testapp):
3133 res = testapp.get("/echo_view_arg_use_args/42")
3234 assert res.json == {"view_arg": 42}
3335
3436 def test_use_args_on_a_method_view(self, testapp):
35 res = testapp.post("/echo_method_view_use_args", {"val": 42})
37 res = testapp.post_json("/echo_method_view_use_args", {"val": 42})
3638 assert res.json == {"val": 42}
3739
3840 def test_use_kwargs_on_a_method_view(self, testapp):
39 res = testapp.post("/echo_method_view_use_kwargs", {"val": 42})
41 res = testapp.post_json("/echo_method_view_use_kwargs", {"val": 42})
4042 assert res.json == {"val": 42}
4143
4244 def test_use_kwargs_with_missing_data(self, testapp):
43 res = testapp.post("/echo_use_kwargs_missing", {"username": "foo"})
45 res = testapp.post_json("/echo_use_kwargs_missing", {"username": "foo"})
4446 assert res.json == {"username": "foo"}
4547
4648 # regression test for https://github.com/marshmallow-code/webargs/issues/145
4749 def test_nested_many_with_data_key(self, testapp):
48 res = testapp.post_json("/echo_nested_many_data_key", {"x_field": [{"id": 42}]})
49 # https://github.com/marshmallow-code/marshmallow/pull/714
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
5055 if MARSHMALLOW_VERSION_INFO[0] < 3:
56 res = testapp.post_json(*post_with_raw_fieldname_args)
5157 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
5262
5363 res = testapp.post_json("/echo_nested_many_data_key", {"X-Field": [{"id": 24}]})
5464 assert res.json == {"x_field": [{"id": 24}]}
5565
5666 res = testapp.post_json("/echo_nested_many_data_key", {})
5767 assert res.json == {}
68
69 # regression test for
70 # https://github.com/marshmallow-code/webargs/issues/500
71 def test_parsing_unexpected_headers_when_raising(self, testapp):
72 res = testapp.get(
73 "/echo_headers_raising", expect_errors=True, headers={"X-Unexpected": "foo"}
74 )
75 # under marshmallow 2 this is allowed and works
76 if MARSHMALLOW_VERSION_INFO[0] < 3:
77 assert res.json == {"name": "World"}
78 # but on ma3 it's supposed to be a validation error
79 else:
80 assert res.status_code == 422
81 assert "headers" in res.json
82 assert "X-Unexpected" in set(res.json["headers"].keys())
5883
5984
6085 @mock.patch("webargs.flaskparser.abort")
76101 abort_args, abort_kwargs = mock_abort.call_args
77102 assert abort_args[0] == 422
78103 expected_msg = "Invalid value."
79 assert abort_kwargs["messages"]["value"] == [expected_msg]
104 assert abort_kwargs["messages"]["json"]["value"] == [expected_msg]
80105 assert type(abort_kwargs["exc"]) == ValidationError
81106
82107
83 def test_parse_form_returns_missing_if_no_form():
108 @pytest.mark.parametrize("mimetype", [None, "application/json"])
109 def test_load_json_returns_missing_if_no_data(mimetype):
84110 req = mock.Mock()
85 req.form.get.side_effect = AttributeError("no form")
86 assert parser.parse_form(req, "foo", fields.Field()) is missing
111 req.mimetype = mimetype
112 req.get_data.return_value = ""
113 schema = dict2schema({"foo": fields.Field()})()
114 assert parser.load_json(req, schema) is missing
87115
88116
89117 def test_abort_with_message():
110138 error = json.loads(serialized_error)
111139 assert isinstance(error, dict)
112140 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
20 import asyncio
31 import webtest
42 import webtest_aiohttp
3735
3836 # regression test for https://github.com/marshmallow-code/webargs/issues/165
3937 def test_multiple_args(self, testapp):
40 res = testapp.post_json(
41 "/echo_multiple_args", {"first": "1", "last": "2", "_ignore": 0}
42 )
38 res = testapp.post_json("/echo_multiple_args", {"first": "1", "last": "2"})
4339 assert res.json == {"first": "1", "last": "2"}
4440
4541 # regression test for https://github.com/marshmallow-code/webargs/issues/145
4642 def test_nested_many_with_data_key(self, testapp):
47 res = testapp.post_json("/echo_nested_many_data_key", {"x_field": [{"id": 42}]})
4843 # https://github.com/marshmallow-code/marshmallow/pull/714
44 # on marshmallow 2, the field name can also be used
4945 if MARSHMALLOW_VERSION_INFO[0] < 3:
46 res = testapp.post_json(
47 "/echo_nested_many_data_key", {"x_field": [{"id": 42}]}
48 )
5049 assert res.json == {"x_field": [{"id": 42}]}
5150
5251 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)
13 parsed = await parser.parse(hello_args, request, location="query")
1414 return json_response(parsed)
1515
1616
17 @use_args(hello_args)
17 @use_args(hello_args, location="query")
1818 async def echo_use_args(request, args):
1919 return json_response(args)
2020
2121
22 @use_kwargs(hello_args)
22 @use_kwargs(hello_args, location="query")
2323 async def echo_use_kwargs(request, name):
2424 return json_response({"name": name})
2525
0 # -*- coding: utf-8 -*-
10 from webargs.testing import CommonTestCase
21
32
0 # -*- coding: utf-8 -*-
1
2 from webargs.core import json
0 import marshmallow as ma
1 import pytest
2 import tornado.concurrent
3 import tornado.http1connection
4 import tornado.httpserver
5 import tornado.httputil
6 import tornado.ioloop
7 import tornado.web
8 from tornado.testing import AsyncHTTPTestCase
9 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
319
420 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
12 import marshmallow as ma
13
14 import tornado.web
15 import tornado.httputil
16 import tornado.httpserver
17 import tornado.http1connection
18 import tornado.concurrent
19 import tornado.ioloop
20 from tornado.testing import AsyncHTTPTestCase
21
22 from webargs import fields, missing
23 from webargs.tornadoparser import parser, use_args, use_kwargs, get_value
24 from webargs.core import parse_json
21 # Python 3.5
22 import mock
23 except ImportError:
24 # Python 3.6+
25 from unittest import mock
26
2527
2628 name = "name"
2729 value = "value"
2830
2931
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
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:
4353 def test_it_should_get_single_values(self):
44 query = [(name, value)]
45 field = fields.Field()
54 query = [("name", "Aeschylus")]
4655 request = make_get_request(query)
47
48 result = parser.parse_querystring(request, name, field)
49
50 assert result == value
56 result = parser.load_querystring(request, author_schema)
57 assert result["name"] == "Aeschylus"
5158
5259 def test_it_should_get_multiple_values(self):
53 query = [(name, value), (name, value)]
54 field = fields.List(fields.Field())
60 query = [("works", "Agamemnon"), ("works", "Nereids")]
5561 request = make_get_request(query)
56
57 result = parser.parse_querystring(request, name, field)
58
59 assert result == [value, value]
62 result = parser.load_querystring(request, author_schema)
63 assert result["works"] == ["Agamemnon", "Nereids"]
6064
6165 def test_it_should_return_missing_if_not_present(self):
6266 query = []
63 field = fields.Field()
64 field2 = fields.List(fields.Int())
6567 request = make_get_request(query)
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
68 result = parser.load_querystring(request, author_schema)
69 assert result["name"] is missing
70 assert result["works"] is missing
8171
8272
8373 class TestFormArgs:
84 def setup_method(self, method):
85 parser.clear_cache()
86
8774 def test_it_should_get_single_values(self):
88 query = [(name, value)]
89 field = fields.Field()
75 query = [("name", "Aristophanes")]
9076 request = make_form_request(query)
91
92 result = parser.parse_form(request, name, field)
93
94 assert result == value
77 result = parser.load_form(request, author_schema)
78 assert result["name"] == "Aristophanes"
9579
9680 def test_it_should_get_multiple_values(self):
97 query = [(name, value), (name, value)]
98 field = fields.List(fields.Field())
81 query = [("works", "The Wasps"), ("works", "The Frogs")]
9982 request = make_form_request(query)
100
101 result = parser.parse_form(request, name, field)
102
103 assert result == [value, value]
83 result = parser.load_form(request, author_schema)
84 assert result["works"] == ["The Wasps", "The Frogs"]
10485
10586 def test_it_should_return_missing_if_not_present(self):
10687 query = []
107 field = fields.Field()
10888 request = make_form_request(query)
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
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:
12895 def test_it_should_get_single_values(self):
129 query = {name: value}
130 field = fields.Field()
96 query = {"name": "Euripides"}
13197 request = make_json_request(query)
132 result = parser.parse_json(request, name, field)
133
134 assert result == value
98 result = parser.load_json(request, author_schema)
99 assert result["name"] == "Euripides"
135100
136101 def test_parsing_request_with_vendor_content_type(self):
137 query = {name: value}
138 field = fields.Field()
102 query = {"name": "Euripides"}
139103 request = make_json_request(
140104 query, content_type="application/vnd.api+json; charset=UTF-8"
141105 )
142 result = parser.parse_json(request, name, field)
143
144 assert result == value
106 result = parser.load_json(request, author_schema)
107 assert result["name"] == "Euripides"
145108
146109 def test_it_should_get_multiple_values(self):
147 query = {name: [value, value]}
148 field = fields.List(fields.Field())
110 query = {"works": ["Medea", "Electra"]}
149111 request = make_json_request(query)
150 result = parser.parse_json(request, name, field)
151
152 assert result == [value, value]
112 result = parser.load_json(request, author_schema)
113 assert result["works"] == ["Medea", "Electra"]
153114
154115 def test_it_should_get_multiple_nested_values(self):
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 )
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 }
159129 request = make_json_request(query)
160 result = parser.parse_json(request, name, field)
161 assert result == [{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}]
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):
137 query = {}
138 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 {}
145 request = make_request(
146 body=tornado.concurrent.Future(),
147 headers={"Content-Type": "application/json"},
148 )
149 result = parser.load_json(request, author_schema)
150 assert result is missing
151
152 def test_it_should_handle_value_error_on_parse_json(self):
153 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:
159 def test_it_should_get_single_values(self):
160 query = {"name": "Euphorion"}
161 request = make_request(headers=query)
162 result = parser.load_headers(request, author_schema)
163 assert result["name"] == "Euphorion"
164
165 def test_it_should_get_multiple_values(self):
166 query = {"works": ["Prometheus Bound", "Prometheus Unbound"]}
167 request = make_request(headers=query)
168 result = parser.load_headers(request, author_schema)
169 assert result["works"] == ["Prometheus Bound", "Prometheus Unbound"]
162170
163171 def test_it_should_return_missing_if_not_present(self):
164 query = {}
165 field = fields.Field()
166 request = make_json_request(query)
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()
181 request = make_request(
182 body=tornado.concurrent.Future, headers={"Content-Type": "application/json"}
183 )
184 result = parser.parse_json(request, name, field)
185 assert parser._cache["json"] == {}
186 assert result is missing
187
188 def test_it_should_handle_value_error_on_parse_json(self):
189 field = fields.Field()
190 request = make_request("this is json not")
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
172 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:
200179 def test_it_should_get_single_values(self):
201 query = {name: value}
202 field = fields.Field()
203 request = make_request(headers=query)
204
205 result = parser.parse_headers(request, name, field)
206
207 assert result == value
180 query = [("name", "Sappho")]
181 request = make_files_request(query)
182 result = parser.load_files(request, author_schema)
183 assert result["name"] == "Sappho"
208184
209185 def test_it_should_get_multiple_values(self):
210 query = {name: [value, value]}
211 field = fields.List(fields.Field())
212 request = make_request(headers=query)
213
214 result = parser.parse_headers(request, name, field)
215
216 assert result == [value, value]
217
218 def test_it_should_return_missing_if_not_present(self):
219 field = fields.Field(multiple=False)
220 request = make_request()
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
240 def test_it_should_get_single_values(self):
241 query = [(name, value)]
242 field = fields.Field()
186 query = [("works", "Sappho 31"), ("works", "Ode to Aphrodite")]
243187 request = make_files_request(query)
244
245 result = parser.parse_files(request, name, field)
246
247 assert result == value
248
249 def test_it_should_get_multiple_values(self):
250 query = [(name, value), (name, value)]
251 field = fields.List(fields.Field())
252 request = make_files_request(query)
253
254 result = parser.parse_files(request, name, field)
255
256 assert result == [value, value]
188 result = parser.load_files(request, author_schema)
189 assert result["works"] == ["Sappho 31", "Ode to Aphrodite"]
257190
258191 def test_it_should_return_missing_if_not_present(self):
259192 query = []
260 field = fields.Field()
261193 request = make_files_request(query)
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):
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:
278200 def test_it_should_raise_httperror_on_failed_validation(self):
279201 args = {"foo": fields.Field(validate=lambda x: False)}
280202 with pytest.raises(tornado.web.HTTPError):
281203 parser.parse(args, make_json_request({"foo": 42}))
282204
283205
284 class TestParse(object):
285 def setup_method(self, method):
286 parser.clear_cache()
287
206 class TestParse:
288207 def test_it_should_parse_query_arguments(self):
289208 attrs = {"string": fields.Field(), "integer": fields.List(fields.Int())}
290209
292211 [("string", "value"), ("integer", "1"), ("integer", "2")]
293212 )
294213
295 parsed = parser.parse(attrs, request)
214 parsed = parser.parse(attrs, request, location="query")
296215
297216 assert parsed["integer"] == [1, 2]
298217 assert parsed["string"] == value
304223 [("string", "value"), ("integer", "1"), ("integer", "2")]
305224 )
306225
307 parsed = parser.parse(attrs, request)
226 parsed = parser.parse(attrs, request, location="form")
308227
309228 assert parsed["integer"] == [1, 2]
310229 assert parsed["string"] == value
336255
337256 request = make_request(headers={"string": "value", "integer": ["1", "2"]})
338257
339 parsed = parser.parse(attrs, request, locations=["headers"])
258 parsed = parser.parse(attrs, request, location="headers")
340259
341260 assert parsed["string"] == value
342261 assert parsed["integer"] == [1, 2]
348267 [("string", "value"), ("integer", "1"), ("integer", "2")]
349268 )
350269
351 parsed = parser.parse(attrs, request, locations=["cookies"])
270 parsed = parser.parse(attrs, request, location="cookies")
352271
353272 assert parsed["string"] == value
354273 assert parsed["integer"] == [2]
360279 [("string", "value"), ("integer", "1"), ("integer", "2")]
361280 )
362281
363 parsed = parser.parse(attrs, request, locations=["files"])
282 parsed = parser.parse(attrs, request, location="files")
364283
365284 assert parsed["string"] == value
366285 assert parsed["integer"] == [1, 2]
382301 parser.parse(args, request)
383302
384303
385 class TestUseArgs(object):
386 def setup_method(self, method):
387 parser.clear_cache()
388
304 class TestUseArgs:
389305 def test_it_should_pass_parsed_as_first_argument(self):
390 class Handler(object):
306 class Handler:
391307 request = make_json_request({"key": "value"})
392308
393309 @use_args({"key": fields.Field()})
402318 assert result is True
403319
404320 def test_it_should_pass_parsed_as_kwargs_arguments(self):
405 class Handler(object):
321 class Handler:
406322 request = make_json_request({"key": "value"})
407323
408324 @use_kwargs({"key": fields.Field()})
417333 assert result is True
418334
419335 def test_it_should_be_validate_arguments_when_validator_is_passed(self):
420 class Handler(object):
336 class Handler:
421337 request = make_json_request({"foo": 41})
422338
423339 @use_kwargs({"foo": fields.Int()}, validate=lambda args: args["foo"] > 42)
475391
476392
477393 def make_request(uri=None, body=None, headers=None, files=None):
478 uri = uri if uri is not None else u""
479 body = body if body is not None else u""
394 uri = uri if uri is not None else ""
395 body = body if body is not None else ""
480396 method = "POST" if body else "GET"
481397 # Need to make a mock connection right now because Tornado 4.0 requires a
482398 # remote_ip in the context attribute. 4.1 addresses this, and this
485401 mock_connection = mock.Mock(spec=tornado.http1connection.HTTP1Connection)
486402 mock_connection.context = mock.Mock()
487403 mock_connection.remote_ip = None
488 content_type = headers.get("Content-Type", u"") if headers else u""
404 content_type = headers.get("Content-Type", "") if headers else ""
489405 request = tornado.httputil.HTTPServerRequest(
490406 method=method,
491407 uri=uri,
508424 class EchoHandler(tornado.web.RequestHandler):
509425 ARGS = {"name": fields.Str()}
510426
511 @use_args(ARGS)
427 @use_args(ARGS, location="query")
512428 def get(self, args):
513429 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()}
514442
515443 @use_args(ARGS)
516444 def post(self, args):
520448 class EchoWithParamHandler(tornado.web.RequestHandler):
521449 ARGS = {"name": fields.Str()}
522450
523 @use_args(ARGS)
451 @use_args(ARGS, location="query")
524452 def get(self, id, args):
525453 self.write(args)
526454
527455
528456 echo_app = tornado.web.Application(
529 [(r"/echo", EchoHandler), (r"/echo_with_param/(\d+)", EchoWithParamHandler)]
457 [
458 (r"/echo", EchoHandler),
459 (r"/echo_form", EchoFormHandler),
460 (r"/echo_json", EchoJSONHandler),
461 (r"/echo_with_param/(\d+)", EchoWithParamHandler),
462 ]
530463 )
531464
532465
536469
537470 def test_post(self):
538471 res = self.fetch(
539 "/echo",
472 "/echo_json",
540473 method="POST",
541474 headers={"Content-Type": "application/json"},
542475 body=json.dumps({"name": "Steve"}),
544477 json_body = parse_json(res.body)
545478 assert json_body["name"] == "Steve"
546479 res = self.fetch(
547 "/echo",
480 "/echo_json",
548481 method="POST",
549482 headers={"Content-Type": "application/json"},
550483 body=json.dumps({}),
576509 def post(self, args):
577510 self.write(args)
578511
579 @use_kwargs(ARGS)
512 @use_kwargs(ARGS, location="query")
580513 def get(self, name):
581514 self.write({"status": "success"})
582515
0 # -*- coding: utf-8 -*-
10 """Tests for the webapp2 parser"""
2 try:
3 from urllib.parse import urlencode
4 except ImportError: # PY2
5 from urllib import urlencode # type: ignore
1 from urllib.parse import urlencode
62 from webargs.core import json
73
84 import pytest
5 import marshmallow as ma
96 from marshmallow import fields, ValidationError
107
118 import webtest
129 import webapp2
1310 from webargs.webapp2parser import parser
11 from webargs.core import MARSHMALLOW_VERSION_INFO
1412
1513 hello_args = {"name": fields.Str(missing="World")}
1614
2422 }
2523
2624
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
2736 def test_parse_querystring_args():
2837 request = webapp2.Request.blank("/echo?name=Fred")
29 assert parser.parse(hello_args, req=request) == {"name": "Fred"}
38 assert parser.parse(hello_args, req=request, location="query") == {"name": "Fred"}
3039
3140
3241 def test_parse_querystring_multiple():
3342 expected = {"name": ["steve", "Loria"]}
3443 request = webapp2.Request.blank("/echomulti?name=steve&name=Loria")
35 assert parser.parse(hello_multiple, req=request) == expected
44 assert parser.parse(hello_multiple, req=request, location="query") == expected
3645
3746
3847 def test_parse_form():
3948 expected = {"name": "Joe"}
4049 request = webapp2.Request.blank("/echo", POST=expected)
41 assert parser.parse(hello_args, req=request) == expected
50 assert parser.parse(hello_args, req=request, location="form") == expected
4251
4352
4453 def test_parse_form_multiple():
4554 expected = {"name": ["steve", "Loria"]}
4655 request = webapp2.Request.blank("/echo", POST=urlencode(expected, doseq=True))
47 assert parser.parse(hello_multiple, req=request) == expected
56 assert parser.parse(hello_multiple, req=request, location="form") == expected
4857
4958
5059 def test_parsing_form_default():
5160 request = webapp2.Request.blank("/echo", POST="")
52 assert parser.parse(hello_args, req=request) == {"name": "World"}
61 assert parser.parse(hello_args, req=request, location="form") == {"name": "World"}
5362
5463
5564 def test_parse_json():
103112 request = webapp2.Request.blank(
104113 "/", headers={"Cookie": response.headers["Set-Cookie"]}
105114 )
106 assert parser.parse(hello_args, req=request, locations=("cookies",)) == expected
115 assert parser.parse(hello_args, req=request, location="cookies") == expected
107116
108117
109118 def test_parsing_headers():
110119 expected = {"name": "Fred"}
111120 request = webapp2.Request.blank("/", headers=expected)
112 assert parser.parse(hello_args, req=request, locations=("headers",)) == expected
121 assert (
122 parser.parse(hello_exclude_schema, req=request, location="headers") == expected
123 )
113124
114125
115126 def test_parse_files():
118129 """
119130
120131 class Handler(webapp2.RequestHandler):
121 @parser.use_args({"myfile": fields.List(fields.Field())}, locations=("files",))
132 @parser.use_args({"myfile": fields.List(fields.Field())}, location="files")
122133 def post(self, args):
123134 self.response.content_type = "application/json"
124135
125136 def _value(f):
126137 return f.getvalue().decode("utf-8")
127138
128 data = dict((i.filename, _value(i.file)) for i in args["myfile"])
139 data = {i.filename: _value(i.file) for i in args["myfile"]}
129140 self.response.write(json.dumps(data))
130141
131142 app = webapp2.WSGIApplication([("/", Handler)])
138149 def test_exception_on_validation_error():
139150 request = webapp2.Request.blank("/", POST={"num": "3"})
140151 with pytest.raises(ValidationError):
141 parser.parse(hello_validate, req=request)
152 parser.parse(hello_validate, req=request, location="form")
142153
143154
144155 def test_validation_error_with_message():
145156 request = webapp2.Request.blank("/", POST={"num": "3"})
146157 with pytest.raises(ValidationError) as exc:
147 parser.parse(hello_validate, req=request)
158 parser.parse(hello_validate, req=request, location="form")
148159 assert "Houston, we've had a problem." in exc.value
149160
150161
156167 request = webapp2.Request.blank("/echo", POST=expected)
157168 app = webapp2.WSGIApplication([])
158169 app.set_globals(app, request)
159 assert parser.parse(hello_args) == expected
170 assert parser.parse(hello_args, location="form") == expected
00 [tox]
11 envlist=
22 lint
3 py{27,35,36,37}-marshmallow2
4 py{35,36,37}-marshmallow3
5 py37-marshmallowdev
3 py{35,36,37,38}-marshmallow2
4 py{35,36,37,38}-marshmallow3
5 py38-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 =
15 py27: pytest --ignore=tests/test_py3/ {posargs}
16 py{35,36,37}: pytest {posargs}
14 commands = pytest {posargs}
1715
1816 [testenv:lint]
19 deps = pre-commit~=1.17
17 deps = pre-commit~=2.4
2018 skip_install = true
2119 commands = pre-commit run --all-files
2220