Codebase list python-webargs / b585908
New upstream version 7.0.1 Sophie Brun 3 years ago
69 changed file(s) with 3612 addition(s) and 2609 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.3
3 hooks:
4 - id: pyupgrade
5 args: ["--py36-plus"]
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.4
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.790
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 7.0.1 (2020-12-14)
4 ******************
5
6 Bug fixes:
7
8 * Fix `DelimitedList` and `DelimitedTuple` to pass additional keyword arguments
9 through their `_serialize` methods to the child fields and fix type checking
10 on these classes. (:issue:`569`)
11 Thanks to :user:`decaz` for reporting.
12
13 7.0.0 (2020-12-10)
14 ******************
15
16 Changes:
17
18 * *Backwards-incompatible*: Drop support for webapp2 (:pr:`565`).
19
20 * Add type annotations to `Parser` class, `DelimitedList`, and
21 `DelimitedTuple`. (:issue:`566`)
22
23 7.0.0b2 (2020-12-01)
24 ********************
25
26 Features:
27
28 * `DjangoParser` now supports the `headers` location. (:issue:`540`)
29
30 * `FalconParser` now supports a new `media` location, which uses
31 Falcon's `media` decoding. (:issue:`253`)
32
33 `media` behaves very similarly to the `json` location but also supports any
34 registered media handler. See the
35 `Falcon documentation on media types
36 <https://falcon.readthedocs.io/en/stable/api/media.html>`_ for more details.
37
38 Changes:
39
40 * `FalconParser` defaults to the `media` location instead of `json`. (:issue:`253`)
41 * Test against Python 3.9 (:pr:`552`).
42 * *Backwards-incompatible*: Drop support for Python 3.5 (:pr:`553`).
43
44 7.0.0b1 (2020-09-11)
45 ********************
46
47 Refactoring:
48
49 * *Backwards-incompatible*: Remove support for marshmallow2 (:issue:`539`)
50
51 * *Backwards-incompatible*: Remove `dict2schema`
52
53 Users desiring the `dict2schema` functionality may now rely upon
54 `marshmallow.Schema.from_dict`. Rewrite any code using `dict2schema` like so:
55
56 .. code-block:: python
57
58 import marshmallow as ma
59
60 # webargs 6.x and older
61 from webargs import dict2schema
62
63 myschema = dict2schema({"q1", ma.fields.Int()})
64
65 # webargs 7.x
66 myschema = ma.Schema.from_dict({"q1", ma.fields.Int()})
67
68 Features:
69
70 * Add ``unknown`` as a parameter to ``Parser.parse``, ``Parser.use_args``,
71 ``Parser.use_kwargs``, and parser instantiation. When set, it will be passed
72 to ``Schema.load``. When not set, the value passed will depend on the parser's
73 settings. If set to ``None``, the schema's default behavior will be used (i.e.
74 no value is passed to ``Schema.load``) and parser settings will be ignored.
75
76 This allows usages like
77
78 .. code-block:: python
79
80 import marshmallow as ma
81
82
83 @parser.use_kwargs(
84 {"q1": ma.fields.Int(), "q2": ma.fields.Int()}, location="query", unknown=ma.EXCLUDE
85 )
86 def foo(q1, q2):
87 ...
88
89 * Defaults for ``unknown`` may be customized on parser classes via
90 ``Parser.DEFAULT_UNKNOWN_BY_LOCATION``, which maps location names to values
91 to use.
92
93 Usages are varied, but include
94
95 .. code-block:: python
96
97 import marshmallow as ma
98 from webargs.flaskparser import FlaskParser
99
100 # as well as...
101 class MyParser(FlaskParser):
102 DEFAULT_UNKNOWN_BY_LOCATION = {"query": ma.INCLUDE}
103
104
105 parser = MyParser()
106
107 Setting the ``unknown`` value for a Parser instance has higher precedence. So
108
109 .. code-block:: python
110
111 parser = MyParser(unknown=ma.RAISE)
112
113 will always pass ``RAISE``, even when the location is ``query``.
114
115 * By default, webargs will pass ``unknown=EXCLUDE`` for all locations except
116 for request bodies (``json``, ``form``, and ``json_or_form``) and path
117 parameters. Request bodies and path parameters will pass ``unknown=RAISE``.
118 This behavior is defined by the default value for
119 ``DEFAULT_UNKNOWN_BY_LOCATION``.
120
121 Changes:
122
123 * Registered `error_handler` callbacks are required to raise an exception.
124 If a handler is invoked and no exception is raised, `webargs` will raise
125 a `ValueError` (:issue:`527`)
126
127 6.1.1 (2020-09-08)
128 ******************
129
130 Bug fixes:
131
132 * Failure to validate flask headers would produce error data which contained
133 tuples as keys, and was therefore not JSON-serializable. (:issue:`500`)
134 These errors will now extract the headername as the key correctly.
135 Thanks to :user:`shughes-uk` for reporting.
136
137 6.1.0 (2020-04-05)
138 ******************
139
140 Features:
141
142 * Add ``fields.DelimitedTuple`` when using marshmallow 3. This behaves as a
143 combination of ``fields.DelimitedList`` and ``marshmallow.fields.Tuple``. It
144 takes an iterable of fields, plus a delimiter (defaults to ``,``), and parses
145 delimiter-separated strings into tuples. (:pr:`509`)
146
147 * Add ``__str__`` and ``__repr__`` to MultiDictProxy to make it easier to work
148 with (:pr:`488`)
149
150 Support:
151
152 * Various docs updates (:pr:`482`, :pr:`486`, :pr:`489`, :pr:`498`, :pr:`508`).
153 Thanks :user:`lefterisjp`, :user:`timgates42`, and :user:`ugultopu` for the PRs.
154
155
156 6.0.0 (2020-02-27)
157 ******************
158
159 Features:
160
161 * ``FalconParser``: Pass request content length to ``req.stream.read`` to
162 provide compatibility with ``falcon.testing`` (:pr:`477`).
163 Thanks :user:`suola` for the PR.
164
165 * *Backwards-incompatible*: Factorize the ``use_args`` / ``use_kwargs`` branch
166 in all parsers. When ``as_kwargs`` is ``False``, arguments are now
167 consistently appended to the arguments list by the ``use_args`` decorator.
168 Before this change, the ``PyramidParser`` would prepend the argument list on
169 each call to ``use_args``. Pyramid view functions must reverse the order of
170 their arguments. (:pr:`478`)
171
172 6.0.0b8 (2020-02-16)
173 ********************
174
175 Refactoring:
176
177 * *Backwards-incompatible*: Use keyword-only arguments (:pr:`472`).
178
179 6.0.0b7 (2020-02-14)
180 ********************
181
182 Features:
183
184 * *Backwards-incompatible*: webargs will rewrite the error messages in
185 ValidationErrors to be namespaced under the location which raised the error.
186 The `messages` field on errors will therefore be one layer deeper with a
187 single top-level key.
188
189 6.0.0b6 (2020-01-31)
190 ********************
191
192 Refactoring:
193
194 * Remove the cache attached to webargs parsers. Due to changes between webargs
195 v5 and v6, the cache is no longer considered useful.
196
197 Other changes:
198
199 * Import ``Mapping`` from ``collections.abc`` in pyramidparser.py (:pr:`471`).
200 Thanks :user:`tirkarthi` for the PR.
201
202 6.0.0b5 (2020-01-30)
203 ********************
204
205 Refactoring:
206
207 * *Backwards-incompatible*: `DelimitedList` now requires that its input be a
208 string and always serializes as a string. It can still serialize and deserialize
209 using another field, e.g. `DelimitedList(Int())` is still valid and requires
210 that the values in the list parse as ints.
211
212 6.0.0b4 (2020-01-28)
213 ********************
214
215 Bug fixes:
216
217 * :cve:`CVE-2020-7965`: Don't attempt to parse JSON if request's content type is mismatched
218 (bugfix from 5.5.3).
219
220 6.0.0b3 (2020-01-21)
221 ********************
222
223 Features:
224
225 * *Backwards-incompatible*: Support Falcon 2.0. Drop support for Falcon 1.x
226 (:pr:`459`). Thanks :user:`dodumosu` and :user:`Nateyo` for the PR.
227
228 6.0.0b2 (2020-01-07)
229 ********************
230
231 Other changes:
232
233 * *Backwards-incompatible*: Drop support for Python 2 (:issue:`440`).
234 Thanks :user:`hugovk` for the PR.
235
236 6.0.0b1 (2020-01-06)
237 ********************
238
239 Features:
240
241 * *Backwards-incompatible*: Schemas will now load all data from a location, not
242 only data specified by fields. As a result, schemas with validators which
243 examine the full input data may change in behavior. The `unknown` parameter
244 on schemas may be used to alter this. For example,
245 `unknown=marshmallow.EXCLUDE` will produce a behavior similar to webargs v5.
246
247 Bug fixes:
248
249 * *Backwards-incompatible*: All parsers now require the Content-Type to be set
250 correctly when processing JSON request bodies. This impacts ``DjangoParser``,
251 ``FalconParser``, ``FlaskParser``, and ``PyramidParser``
252
253 Refactoring:
254
255 * *Backwards-incompatible*: Schema fields may not specify a location any
256 longer, and `Parser.use_args` and `Parser.use_kwargs` now accept `location`
257 (singular) instead of `locations` (plural). Instead of using a single field or
258 schema with multiple `locations`, users are recommended to make multiple
259 calls to `use_args` or `use_kwargs` with a distinct schema per location. For
260 example, code should be rewritten like this:
261
262 .. code-block:: python
263
264 # webargs 5.x and older
265 @parser.use_args(
266 {
267 "q1": ma.fields.Int(location="query"),
268 "q2": ma.fields.Int(location="query"),
269 "h1": ma.fields.Int(location="headers"),
270 },
271 locations=("query", "headers"),
272 )
273 def foo(q1, q2, h1):
274 ...
275
276
277 # webargs 6.x
278 @parser.use_args({"q1": ma.fields.Int(), "q2": ma.fields.Int()}, location="query")
279 @parser.use_args({"h1": ma.fields.Int()}, location="headers")
280 def foo(q1, q2, h1):
281 ...
282
283 * The `location_handler` decorator has been removed and replaced with
284 `location_loader`. `location_loader` serves the same purpose (letting you
285 write custom hooks for loading data) but its expected method signature is
286 different. See the docs on `location_loader` for proper usage.
287
288 Thanks :user:`sirosen` for the PR!
289
3290 5.5.3 (2020-01-28)
4291 ******************
5292
6293 Bug fixes:
7294
8 * :cve:`CVE-2020-7965`: Don't attempt to parse JSON if the request's Content-Type is mismatched.
295 * :cve:`CVE-2020-7965`: Don't attempt to parse JSON if request's content type is mismatched.
9296
10297 5.5.2 (2019-10-06)
11298 ******************
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
1313 :target: https://webargs.readthedocs.io/
1414 :alt: Documentation
1515
16 .. image:: https://badgen.net/badge/marshmallow/2,3?list=1
16 .. image:: https://badgen.net/badge/marshmallow/3
1717 :target: https://marshmallow.readthedocs.io/en/latest/upgrading.html
18 :alt: marshmallow 2/3 compatible
18 :alt: marshmallow 3 compatible
1919
2020 .. image:: https://badgen.net/badge/code%20style/black/000
2121 :target: https://github.com/ambv/black
2323
2424 Homepage: https://webargs.readthedocs.io/
2525
26 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.
26 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, Falcon, and aiohttp.
2727
2828 .. code-block:: python
2929
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.6.
5757
5858
5959 Documentation
2525 parameters:
2626 toxenvs:
2727 - lint
28 - py27-marshmallow2
29
30 - py35-marshmallow2
31 - py35-marshmallow3
32
33 - py36-marshmallow2
34 - py36-marshmallow3
35
36 - py37-marshmallow2
37 - py37-marshmallow3
38
39 - py37-marshmallowdev
40
28 - mypy
29 - py36
30 - py36-mindeps
31 - py37
32 - py38
33 - py39
34 - py39-marshmallowdev
4135 - docs
4236 os: linux
43 # Build separate wheels for python 2 and 3
37 # Build wheels
4438 - template: job--pypi-release.yml@sloria
4539 parameters:
46 python: "3.7"
40 python: "3.9"
4741 distributions: "sdist bdist_wheel"
48 name_postfix: "_py3"
4942 dependsOn:
5043 - 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 -----------------------
45106 last_name = fields.Str(missing="")
46107 date_registered = fields.DateTime(dump_only=True)
47108
48 # NOTE: Uncomment below two lines if you're using marshmallow 2
49 # class Meta:
50 # strict = True
51
52109
53110 @use_args(UserSchema())
54111 def profile_view(args):
63120
64121
65122 # You can add additional parameters
66 @use_kwargs({"posts_per_page": fields.Int(missing=10, location="query")})
123 @use_kwargs({"posts_per_page": fields.Int(missing=10)}, location="query")
67124 @use_args(UserSchema())
68125 def profile_posts(args, posts_per_page):
69126 username = args["username"]
70127 # ...
71128
72 .. warning::
73 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.
74
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.
129 .. _advanced_setting_unknown:
130
131 Setting `unknown`
132 -----------------
133
134 webargs supports several ways of setting and passing the `unknown` parameter
135 for `handling unknown fields <https://marshmallow.readthedocs.io/en/stable/quickstart.html#handling-unknown-fields>`_.
136
137 You can pass `unknown=...` as a parameter to any of
138 `Parser.parse <webargs.core.Parser.parse>`,
139 `Parser.use_args <webargs.core.Parser.use_args>`, and
140 `Parser.use_kwargs <webargs.core.Parser.use_kwargs>`.
141
142
143 .. note::
144
145 The `unknown` value is passed to the schema's `load()` call. It therefore
146 only applies to the top layer when nesting is used. To control `unknown` at
147 multiple layers of a nested schema, you must use other mechanisms, like
148 the `unknown` argument to `fields.Nested`.
149
150 Default `unknown`
151 +++++++++++++++++
152
153 By default, webargs will pass `unknown=marshmallow.EXCLUDE` except when the
154 location is `json`, `form`, `json_or_form`, `path`, or `path`. In those cases,
155 it uses `unknown=marshmallow.RAISE` instead.
156
157 You can change these defaults by overriding `DEFAULT_UNKNOWN_BY_LOCATION`.
158 This is a mapping of locations to values to pass.
159
160 For example,
161
162 .. code-block:: python
163
164 from flask import Flask
165 from marshmallow import EXCLUDE, fields
166 from webargs.flaskparser import FlaskParser
167
168 app = Flask(__name__)
169
170
171 class Parser(FlaskParser):
172 DEFAULT_UNKNOWN_BY_LOCATION = {"query": EXCLUDE}
173
174
175 parser = Parser()
176
177
178 # location is "query", which is listed in DEFAULT_UNKNOWN_BY_LOCATION,
179 # so EXCLUDE will be used
180 @app.route("/", methods=["GET"])
181 @parser.use_args({"foo": fields.Int()}, location="query")
182 def get(self, args):
183 return f"foo x 2 = {args['foo'] * 2}"
184
185
186 # location is "json", which is not in DEFAULT_UNKNOWN_BY_LOCATION,
187 # so no value will be passed for `unknown`
188 @app.route("/", methods=["POST"])
189 @parser.use_args({"foo": fields.Int(), "bar": fields.Int()}, location="json")
190 def post(self, args):
191 return f"foo x bar = {args['foo'] * args['bar']}"
192
193
194 You can also define a default at parser instantiation, which will take
195 precedence over these defaults, as in
196
197 .. code-block:: python
198
199 from marshmallow import INCLUDE
200
201 parser = Parser(unknown=INCLUDE)
202
203 # because `unknown` is set on the parser, `DEFAULT_UNKNOWN_BY_LOCATION` has
204 # effect and `INCLUDE` will always be used
205 @app.route("/", methods=["POST"])
206 @parser.use_args({"foo": fields.Int(), "bar": fields.Int()}, location="json")
207 def post(self, args):
208 unexpected_args = [k for k in args.keys() if k not in ("foo", "bar")]
209 return f"foo x bar = {args['foo'] * args['bar']}; unexpected args={unexpected_args}"
210
211 Using Schema-Specfied `unknown`
212 +++++++++++++++++++++++++++++++
213
214 If you wish to use the value of `unknown` specified by a schema, simply pass
215 ``unknown=None``. This will disable webargs' automatic passing of values for
216 ``unknown``. For example,
217
218 .. code-block:: python
219
220 from flask import Flask
221 from marshmallow import Schema, fields, EXCLUDE, missing
222 from webargs.flaskparser import use_args
223
224
225 class RectangleSchema(Schema):
226 length = fields.Float()
227 width = fields.Float()
228
229 class Meta:
230 unknown = EXCLUDE
231
232
233 app = Flask(__name__)
234
235 # because unknown=None was passed, no value is passed during schema loading
236 # as a result, the schema's behavior (EXCLUDE) is used
237 @app.route("/", methods=["POST"])
238 @use_args(RectangleSchema(), location="json", unknown=None)
239 def get(self, args):
240 return f"area = {args['length'] * args['width']}"
241
242
243 You can also set ``unknown=None`` when instantiating a parser to make this
244 behavior the default for a parser.
245
246
247 When to avoid `use_kwargs`
248 --------------------------
249
250 Any `Schema <marshmallow.Schema>` passed to `use_kwargs <webargs.core.Parser.use_kwargs>` MUST deserialize to a dictionary of data.
251 If your schema has a `post_load <marshmallow.decorators.post_load>` method
252 that returns a non-dictionary,
253 you should use `use_args <webargs.core.Parser.use_args>` instead.
254
255 .. code-block:: python
256
257 from marshmallow import Schema, fields, post_load
258 from webargs.flaskparser import use_args
259
260
261 class Rectangle:
262 def __init__(self, length, width):
263 self.length = length
264 self.width = width
265
266
267 class RectangleSchema(Schema):
268 length = fields.Float()
269 width = fields.Float()
270
271 @post_load
272 def make_object(self, data, **kwargs):
273 return Rectangle(**data)
274
275
276 @use_args(RectangleSchema)
277 def post(self, rect: Rectangle):
278 return f"Area: {rect.length * rect.width}"
279
280 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.
281 Therefore, `use_args <webargs.core.Parser.use_args>` should be used with those schemas.
77282
78283
79284 Schema Factories
176381 cube = args["cube"]
177382 # ...
178383
179 .. _custom-parsers:
384 .. _custom-loaders:
180385
181386 Custom Parsers
182387 --------------
183388
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.
389 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.
185390
186391
187392 .. code-block:: python
210415 }
211416 """
212417
213 def parse_querystring(self, req, name, field):
214 return core.get_value(_structure_dict(req.args), name, field)
418 def load_querystring(self, req, schema):
419 return _structure_dict(req.args)
215420
216421
217422 def _structure_dict(dict_):
235440 If you'd prefer validation errors to return status code ``400`` instead
236441 of ``422``, you can override ``DEFAULT_VALIDATION_STATUS`` on a :class:`Parser <webargs.core.Parser>`.
237442
443 Sublcass the parser for your framework to do so. For example, using Falcon:
238444
239445 .. code-block:: python
240446
274480
275481
276482 @app.route("/profile/", methods=["patch"])
277 @use_args(PatchSchema(many=True), locations=("json",))
483 @use_args(PatchSchema(many=True))
278484 def patch_blog(args):
279485 """Implements JSON Patch for the user profile
280486
289495 Mixing Locations
290496 ----------------
291497
292 Arguments for different locations can be specified by passing ``location`` to each field individually:
293
294 .. code-block:: python
295
498 Arguments for different locations can be specified by passing ``location`` to each `use_args <webargs.core.Parser.use_args>` call:
499
500 .. code-block:: python
501
502 # "json" is the default, used explicitly below
296503 @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",))
504 @use_args({"page": fields.Int(), "q": fields.Str()}, location="query")
505 @use_args({"name": fields.Str()}, location="json")
334506 def viewfunc(query_parsed, json_parsed):
335507 page = query_parsed["page"]
336508 name = json_parsed["name"]
342514
343515 import functools
344516
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)
517 query = functools.partial(use_args, location="query")
518 body = functools.partial(use_args, location="json")
519
520
521 @query({"page": fields.Int(), "q": fields.Int()})
522 @body({"name": fields.Str()})
351523 def viewfunc(query_parsed, json_parsed):
352524 page = query_parsed["page"]
353525 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 -------------------
5159 .. automodule:: webargs.pyramidparser
5260 :members:
5361
54
55 webargs.webapp2parser
56 ---------------------
57
58 .. automodule:: webargs.webapp2parser
59 :members:
60
61
6262 webargs.falconparser
6363 ---------------------
6464
0 # -*- coding: utf-8 -*-
10 import datetime as dt
21 import sys
32 import os
2221 github_user = "marshmallow-code"
2322 github_repo = "webargs"
2423
25 issues_github_path = "{}/{}".format(github_user, github_repo)
24 issues_github_path = f"{github_user}/{github_repo}"
2625
2726 intersphinx_mapping = {
2827 "python": ("http://python.readthedocs.io/en/latest/", None),
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 = f"2014-{dt.datetime.utcnow():%Y}, Steven Loria and contributors"
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}
33
44 Release v\ |version|. (:doc:`Changelog <changelog>`)
55
6 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.
6 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, 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.6. It depends on `marshmallow <https://marshmallow.readthedocs.io/en/latest/>`_ >= 3.0.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"),
30 # 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 ),
27 "user_type": fields.Str(data_key="user-type"),
3628 }
3729
3830 .. note::
10496 Request "Locations"
10597 -------------------
10698
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:
99 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:
108100
109101 .. code-block:: python
110102
111103 @app.route("/register")
112 @use_args(user_args, locations=("json", "form"))
104 @use_args(user_args, location="form")
113105 def register(args):
114106 return "registration page"
115107
201193
202194
203195 @parser.error_handler
204 def handle_error(error, req, schema, status_code, headers):
196 def handle_error(error, req, schema, *, error_status_code, error_headers):
205197 raise CustomError(error.messages)
206198
207199 Parsing Lists in Query Strings
242234
243235 .. note::
244236
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.
237 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.
246238
247239 Next Steps
248240 ----------
0 Upgrading to Newer Releases
1 ===========================
2
3 This section documents migration paths to new releases.
4
5 Upgrading to 7.0
6 ++++++++++++++++
7
8 `unknown` is Now Settable by the Parser
9 ---------------------------------------
10
11 As of 7.0, `Parsers` have multiple settings for controlling the value for
12 `unknown` which is passed to `schema.load` when parsing.
13
14 To set unknown behavior on a parser, see the advanced doc on this topic:
15 :ref:`advanced_setting_unknown`.
16
17 Importantly, by default, any schema setting for `unknown` will be overridden by
18 the `unknown` settings for the parser.
19
20 In order to use a schema's `unknown` value, set `unknown=None` on the parser.
21 In 6.x versions of webargs, schema values for `unknown` are used, so the
22 `unknown=None` setting is the best way to emulate this.
23
24 To get identical behavior:
25
26 .. code-block:: python
27
28 # assuming you have a schema named MySchema
29
30 # webargs 6.x
31 @parser.use_args(MySchema)
32 def foo(args):
33 ...
34
35
36 # webargs 7.x
37 # as a parameter to use_args or parse
38 @parser.use_args(MySchema, unknown=None)
39 def foo(args):
40 ...
41
42
43 # webargs 7.x
44 # as a parser setting
45 # example with flaskparser, but any parser class works
46 parser = FlaskParser(unknown=None)
47
48
49 @parser.use_args(MySchema)
50 def foo(args):
51 ...
52
53 Upgrading to 6.0
54 ++++++++++++++++
55
56 Multiple Locations Are No Longer Supported In A Single Call
57 -----------------------------------------------------------
58
59 The default location is JSON/body.
60
61 Under webargs 5.x, code often did not have to specify a location.
62
63 Because webargs would parse data from multiple locations automatically, users
64 did not need to specify where a parameter, call it `q`, was passed.
65 `q` could be in a query parameter or in a JSON or form-post body.
66
67 Now, webargs requires that users specify only one location for data loading per
68 `use_args` call, and `"json"` is the default. If `q` is intended to be a query
69 parameter, the developer must be explicit and rewrite like so:
70
71 .. code-block:: python
72
73 # webargs 5.x
74 @parser.use_args({"q": ma.fields.String()})
75 def foo(args):
76 return some_function(user_query=args.get("q"))
77
78
79 # webargs 6.x
80 @parser.use_args({"q": ma.fields.String()}, location="query")
81 def foo(args):
82 return some_function(user_query=args.get("q"))
83
84 This also means that another usage from 5.x is not supported. Code with
85 multiple locations in a single `use_args`, `use_kwargs`, or `parse` call
86 must be rewritten in multiple separate `use_args` or `use_kwargs` invocations,
87 like so:
88
89 .. code-block:: python
90
91 # webargs 5.x
92 @parser.use_kwargs(
93 {
94 "q1": ma.fields.Int(location="query"),
95 "q2": ma.fields.Int(location="query"),
96 "h1": ma.fields.Int(location="headers"),
97 },
98 locations=("query", "headers"),
99 )
100 def foo(q1, q2, h1):
101 ...
102
103
104 # webargs 6.x
105 @parser.use_kwargs({"q1": ma.fields.Int(), "q2": ma.fields.Int()}, location="query")
106 @parser.use_kwargs({"h1": ma.fields.Int()}, location="headers")
107 def foo(q1, q2, h1):
108 ...
109
110
111 Fields No Longer Support location=...
112 -------------------------------------
113
114 Because a single `parser.use_args`, `parser.use_kwargs`, or `parser.parse` call
115 cannot specify multiple locations, it is not necessary for a field to be able
116 to specify its location. Rewrite code like so:
117
118 .. code-block:: python
119
120 # webargs 5.x
121 @parser.use_args({"q": ma.fields.String(location="query")})
122 def foo(args):
123 return some_function(user_query=args.get("q"))
124
125
126 # webargs 6.x
127 @parser.use_args({"q": ma.fields.String()}, location="query")
128 def foo(args):
129 return some_function(user_query=args.get("q"))
130
131 location_handler Has Been Replaced With location_loader
132 -------------------------------------------------------
133
134 This is not just a name change. The expected signature of a `location_loader`
135 is slightly different from the signature for a `location_handler`.
136
137 Where previously a `location_handler` code took the incoming request data and
138 details of a single field being loaded, a `location_loader` takes the request
139 and the schema as a pair. It does not return a specific field's data, but data
140 for the whole location.
141
142 Rewrite code like this:
143
144 .. code-block:: python
145
146 # webargs 5.x
147 @parser.location_handler("data")
148 def load_data(request, name, field):
149 return request.data.get(name)
150
151
152 # webargs 6.x
153 @parser.location_loader("data")
154 def load_data(request, schema):
155 return request.data
156
157 Data Is Not Filtered Before Being Passed To Schemas, And It May Be Proxified
158 ----------------------------------------------------------------------------
159
160 In webargs 5.x, the deserialization schema was used to pull data out of the
161 request object. That data was compiled into a dictionary which was then passed
162 to the schema.
163
164 One of the major changes in webargs 6.x allows the use of `unknown` parameter
165 on schemas. This lets a schema decide what to do with fields not specified in
166 the schema. In order to achieve this, webargs now passes the full data from
167 the specified location to the schema.
168
169 Therefore, users should specify `unknown=marshmallow.EXCLUDE` on their schemas in
170 order to filter out unknown fields. Like so:
171
172 .. code-block:: python
173
174 # webargs 5.x
175 # this can assume that "q" is the only parameter passed, and all other
176 # parameters will be ignored
177 @parser.use_kwargs({"q": ma.fields.String()}, locations=("query",))
178 def foo(q):
179 ...
180
181
182 # webargs 6.x, Solution 1: declare a schema with Meta.unknown set
183 class QuerySchema(ma.Schema):
184 q = ma.fields.String()
185
186 class Meta:
187 unknown = ma.EXCLUDE
188
189
190 @parser.use_kwargs(QuerySchema, location="query")
191 def foo(q):
192 ...
193
194
195 # webargs 6.x, Solution 2: instantiate a schema with unknown set
196 class QuerySchema(ma.Schema):
197 q = ma.fields.String()
198
199
200 @parser.use_kwargs(QuerySchema(unknown=ma.EXCLUDE), location="query")
201 def foo(q):
202 ...
203
204
205 This also allows usage which passes the unknown parameters through, like so:
206
207 .. code-block:: python
208
209 # webargs 6.x only! cannot be done in 5.x
210 class QuerySchema(ma.Schema):
211 q = ma.fields.String()
212
213
214 # will pass *all* query params through as "kwargs"
215 @parser.use_kwargs(QuerySchema(unknown=ma.INCLUDE), location="query")
216 def foo(q, **kwargs):
217 ...
218
219
220 However, many types of request data are so-called "multidicts" -- dictionary-like
221 types which can return one or multiple values. To handle `marshmallow.fields.List`
222 and `webargs.fields.DelimitedList` fields correctly, passing list data, webargs
223 must combine schema information with the raw request data. This is done in the
224 :class:`MultiDictProxy <webargs.multidictproxy.MultiDictProxy>` type, which
225 will often be passed to schemas.
226
227 This means that if a schema has a `pre_load` hook which interacts with the data,
228 it may need modifications. For example, a `flask` query string will be parsed
229 into an `ImmutableMultiDict` type, which will break pre-load hooks which modify
230 the data in-place. Such usages need rewrites like so:
231
232 .. code-block:: python
233
234 # webargs 5.x
235 # flask query params is just an example -- applies to several types
236 from webargs.flaskparser import use_kwargs
237
238
239 class QuerySchema(ma.Schema):
240 q = ma.fields.String()
241
242 @ma.pre_load
243 def convert_nil_to_none(self, obj, **kwargs):
244 if obj.get("q") == "nil":
245 obj["q"] = None
246 return obj
247
248
249 @use_kwargs(QuerySchema, locations=("query",))
250 def foo(q):
251 ...
252
253
254 # webargs 6.x
255 class QuerySchema(ma.Schema):
256 q = ma.fields.String()
257
258 # unlike under 5.x, we cannot modify 'obj' in-place because writing
259 # to the MultiDictProxy will try to write to the underlying
260 # ImmutableMultiDict, which is not allowed
261 @ma.pre_load
262 def convert_nil_to_none(self, obj, **kwargs):
263 # creating a dict from a MultiDictProxy works well because it
264 # "unwraps" lists and delimited lists correctly
265 data = dict(obj)
266 if data.get("q") == "nil":
267 data["q"] = None
268 return data
269
270
271 @parser.use_kwargs(QuerySchema, location="query")
272 def foo(q):
273 ...
274
275
276 DelimitedList Now Only Takes A String Input
277 -------------------------------------------
278
279 Combining `List` and string parsing functionality in a single type had some
280 messy corner cases. For the most part, this should not require rewrites. But
281 for APIs which need to allow both usages, rewrites are possible like so:
282
283 .. code-block:: python
284
285 # webargs 5.x
286 # this allows ...?x=1&x=2&x=3
287 # as well as ...?x=1,2,3
288 @use_kwargs({"x": webargs.fields.DelimitedList(ma.fields.Int)}, locations=("query",))
289 def foo(x):
290 ...
291
292
293 # webargs 6.x
294 # this accepts x=1,2,3 but NOT x=1&x=2&x=3
295 @use_kwargs({"x": webargs.fields.DelimitedList(ma.fields.Int)}, location="query")
296 def foo(x):
297 ...
298
299
300 # webargs 6.x
301 # this accepts x=1,2,3 ; x=1&x=2&x=3 ; x=1,2&x=3
302 # to do this, it needs a post_load hook which will flatten out the list data
303 class UnpackingDelimitedListSchema(ma.Schema):
304 x = ma.fields.List(webargs.fields.DelimitedList(ma.fields.Int))
305
306 @ma.post_load
307 def flatten_lists(self, data, **kwargs):
308 new_x = []
309 for x in data["x"]:
310 new_x.extend(x)
311 data["x"] = new_x
312 return data
313
314
315 @parser.use_kwargs(UnpackingDelimitedListSchema, location="query")
316 def foo(x):
317 ...
318
319
320 ValidationError Messages Are Namespaced Under The Location
321 ----------------------------------------------------------
322
323 Code parsing ValidationError messages will notice a change in the messages
324 produced by webargs.
325 What would previously have come back with messages like `{"foo":["Not a valid integer."]}`
326 will now have messages nested one layer deeper, like
327 `{"json":{"foo":["Not a valid integer."]}}`.
328
329 To rewrite code which was handling these errors, the handler will need to be
330 prepared to traverse messages by one additional level. For example:
331
332 .. code-block:: python
333
334 import logging
335
336 log = logging.getLogger(__name__)
337
338
339 # webargs 5.x
340 # logs debug messages like
341 # bad value for 'foo': ["Not a valid integer."]
342 # bad value for 'bar': ["Not a valid boolean."]
343 def log_invalid_parameters(validation_error):
344 for field, messages in validation_error.messages.items():
345 log.debug("bad value for '{}': {}".format(field, messages))
346
347
348 # webargs 6.x
349 # logs debug messages like
350 # bad value for 'foo' [query]: ["Not a valid integer."]
351 # bad value for 'bar' [json]: ["Not a valid boolean."]
352 def log_invalid_parameters(validation_error):
353 for location, fielddata in validation_error.messages.items():
354 for field, messages in fielddata.items():
355 log.debug("bad value for '{}' [{}]: {}".format(field, location, messages))
356
357
358 Custom Error Handler Argument Names Changed
359 -------------------------------------------
360
361 If you define a custom error handler via `@parser.error_handler` the function
362 arguments are now keyword-only and `status_code` and `headers` have been renamed
363 `error_status_code` and `error_headers`.
364
365 .. code-block:: python
366
367 # webargs 5.x
368 @parser.error_handler
369 def custom_handle_error(error, req, schema, status_code, headers):
370 ...
371
372
373 # webargs 6.x
374 @parser.error_handler
375 def custom_handle_error(error, req, schema, *, error_status_code, error_headers):
376 ...
377
378
379 Some Functions Take Keyword-Only Arguments Now
380 ----------------------------------------------
381
382 The signature of several methods has changed to have keyword-only arguments.
383 For the most part, this should not require any changes, but here's a list of
384 the changes.
385
386 `parser.error_handler` methods:
387
388 .. code-block:: python
389
390 # webargs 5.x
391 def handle_error(error, req, schema, status_code, headers):
392 ...
393
394
395 # webargs 6.x
396 def handle_error(error, req, schema, *, error_status_code, error_headers):
397 ...
398
399 `parser.__init__` methods:
400
401 .. code-block:: python
402
403 # webargs 5.x
404 def __init__(self, location=None, error_handler=None, schema_class=None):
405 ...
406
407
408 # webargs 6.x
409 def __init__(self, location=None, *, error_handler=None, schema_class=None):
410 ...
411
412 `parser.parse`, `parser.use_args`, and `parser.use_kwargs` methods:
413
414
415 .. code-block:: python
416
417 # webargs 5.x
418 def parse(
419 self,
420 argmap,
421 req=None,
422 location=None,
423 validate=None,
424 error_status_code=None,
425 error_headers=None,
426 ):
427 ...
428
429
430 # webargs 6.x
431 def parse(
432 self,
433 argmap,
434 req=None,
435 *,
436 location=None,
437 validate=None,
438 error_status_code=None,
439 error_headers=None
440 ):
441 ...
442
443
444 # webargs 5.x
445 def use_args(
446 self,
447 argmap,
448 req=None,
449 location=None,
450 as_kwargs=False,
451 validate=None,
452 error_status_code=None,
453 error_headers=None,
454 ):
455 ...
456
457
458 # webargs 6.x
459 def use_args(
460 self,
461 argmap,
462 req=None,
463 *,
464 location=None,
465 as_kwargs=False,
466 validate=None,
467 error_status_code=None,
468 error_headers=None
469 ):
470 ...
471
472
473 # use_kwargs is just an alias for use_args with as_kwargs=True
474
475 and finally, the `dict2schema` function:
476
477 .. code-block:: python
478
479 # webargs 5.x
480 def dict2schema(dct, schema_class=ma.Schema):
481 ...
482
483
484 # webargs 6.x
485 def dict2schema(dct, *, schema_class=ma.Schema):
486 ...
487
488
489 PyramidParser Now Appends Arguments (Used To Prepend)
490 -----------------------------------------------------
491
492 `PyramidParser.use_args` was not conformant with the other parsers in webargs.
493 While all other parsers added new arguments to the end of the argument list of
494 a decorated view function, the Pyramid implementation added them to the front
495 of the argument list.
496
497 This has been corrected, but as a result pyramid views with `use_args` may need
498 to be rewritten. The `request` object is always passed first in both versions,
499 so the issue is only apparent with view functions taking other positional
500 arguments.
501
502 For example, imagine code with a decorator for passing user information,
503 `pass_userinfo`, like so:
504
505 .. code-block:: python
506
507 # a decorator which gets information about the authenticated user
508 def pass_userinfo(f):
509 def decorator(request, *args, **kwargs):
510 return f(request, get_userinfo(), *args, **kwargs)
511
512 return decorator
513
514 You will see a behavioral change if `pass_userinfo` is called on a function
515 decorated with `use_args`. The difference between the two versions will be like
516 so:
517
518 .. code-block:: python
519
520 from webargs.pyramidparser import use_args
521
522 # webargs 5.x
523 # pass_userinfo is called first, webargs sees positional arguments of
524 # (userinfo,)
525 # and changes it to
526 # (request, args, userinfo)
527 @pass_userinfo
528 @use_args({"q": ma.fields.String()}, locations=("query",))
529 def viewfunc(request, args, userinfo):
530 q = args.get("q")
531 ...
532
533
534 # webargs 6.x
535 # pass_userinfo is called first, webargs sees positional arguments of
536 # (userinfo,)
537 # and changes it to
538 # (request, userinfo, args)
539 @pass_userinfo
540 @use_args({"q": ma.fields.String()}, location="query")
541 def viewfunc(request, userinfo, args):
542 q = args.get("q")
543 ...
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")):
99 return {"message": "Hello, {}!".format(name)}
98 def index(name: fields.Str(missing="Friend")): # noqa: F821
99 return {"message": f"Hello, {name}!"}
100100
101101
102102 @route("/add", methods=["POST"])
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
7776 app = config.make_wsgi_app()
7877 port = 5001
7978 server = make_server("0.0.0.0", port, app)
80 print("Serving on port {}".format(port))
79 print(f"Serving on port {port}")
8180 server.serve_forever()
0 python-dateutil==2.8.0
0 python-dateutil==2.8.1
11 Flask
22 bottle
33 tornado
4 webapp2
54 flask-restful
65 pyramid
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):
8484 )
8585 port = 5001
8686 app.listen(port)
87 print("Serving on port {}".format(port))
87 print(f"Serving on port {port}")
8888 tornado.ioloop.IOLoop.instance().start()
+0
-50
examples/webapp2_example.py less more
0 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
2 """A Hello, World! example using Webapp2 in a Google App Engine environment
3
4 Run the app:
5
6 $ python webapp2_example.py
7
8 Try the following with httpie (a cURL-like utility, http://httpie.org):
9
10 $ pip install httpie
11 $ http GET :5001/hello
12 $ http GET :5001/hello name==Ada
13 $ http POST :5001/hello_dict name=awesome
14 $ http POST :5001/hello_dict
15 """
16
17 import webapp2
18
19 from webargs import fields
20 from webargs.webapp2parser import use_args, use_kwargs
21
22 hello_args = {"name": fields.Str(missing="World")}
23
24
25 class MainPage(webapp2.RequestHandler):
26 @use_args(hello_args)
27 def get_args(self, args):
28 # args is a dict of parsed items from hello_args
29 self.response.write("Hello, {name}!".format(name=args["name"]))
30
31 @use_kwargs(hello_args)
32 def get_kwargs(self, name=None):
33 self.response.write("Hello, {name}!".format(name=name))
34
35
36 app = webapp2.WSGIApplication(
37 [
38 webapp2.Route(r"/hello", MainPage, handler_method="get_args"),
39 webapp2.Route(r"/hello_dict", MainPage, handler_method="get_kwargs"),
40 ],
41 debug=True,
42 )
43
44
45 if __name__ == "__main__":
46 from wsgiref.simple_server import make_server
47
48 httpd = make_server("", 5001, app)
49 httpd.serve_forever()
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
42
5 INSTALL_REQUIRES = ["marshmallow>=2.15.2"]
6 if sys.version_info[0] < 3:
7 INSTALL_REQUIRES.append("simplejson>=2.1.0")
8
93 FRAMEWORKS = [
10 "Flask>=0.12.2",
11 "Django>=1.11.16",
4 "Flask>=0.12.5",
5 "Django>=2.2.0",
126 "bottle>=0.12.13",
137 "tornado>=4.5.2",
148 "pyramid>=1.9.1",
15 "webapp2>=3.0.0b1",
16 "falcon>=1.4.0,<2.0",
17 'aiohttp>=3.0.0; python_version >= "3.5"',
9 "falcon>=2.0.0",
10 "aiohttp>=3.0.8",
1811 ]
1912 EXTRAS_REQUIRE = {
2013 "frameworks": FRAMEWORKS,
2114 "tests": [
2215 "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"',
16 "webtest==2.0.35",
17 "webtest-aiohttp==2.0.0",
18 "pytest-aiohttp>=0.3.0",
2719 ]
2820 + FRAMEWORKS,
2921 "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",
22 "mypy==0.790",
23 "flake8==3.8.4",
24 "flake8-bugbear==20.11.1",
25 "pre-commit~=2.4",
3426 ],
35 "docs": ["Sphinx==2.2.0", "sphinx-issues==1.2.0", "sphinx-typlog-theme==0.7.3"]
27 "docs": ["Sphinx==3.3.1", "sphinx-issues==1.2.0", "sphinx-typlog-theme==0.8.0"]
3628 + FRAMEWORKS,
3729 }
3830 EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"]
4335 Raises RuntimeError if not found.
4436 """
4537 version = ""
46 with open(fname, "r") as fp:
38 with open(fname) as fp:
4739 reg = re.compile(r'__version__ = [\'"]([^\'"]*)[\'"]')
4840 for line in fp:
4941 m = reg.match(line)
6759 description=(
6860 "Declarative parsing and validation of HTTP request objects, "
6961 "with built-in support for popular web frameworks, including "
70 "Flask, Django, Bottle, Tornado, Pyramid, webapp2, Falcon, and aiohttp."
62 "Flask, Django, Bottle, Tornado, Pyramid, Falcon, and aiohttp."
7163 ),
7264 long_description=read("README.rst"),
7365 author="Steven Loria",
7567 url="https://github.com/marshmallow-code/webargs",
7668 packages=find_packages("src"),
7769 package_dir={"": "src"},
78 install_requires=INSTALL_REQUIRES,
70 package_data={"webargs": ["py.typed"]},
71 install_requires=["marshmallow>=3.0.0"],
7972 extras_require=EXTRAS_REQUIRE,
8073 license="MIT",
8174 zip_safe=False,
8780 "bottle",
8881 "tornado",
8982 "aiohttp",
90 "webapp2",
9183 "request",
9284 "arguments",
9385 "validation",
9688 "api",
9789 "marshmallow",
9890 ),
91 python_requires=">=3.6",
9992 classifiers=[
10093 "Development Status :: 5 - Production/Stable",
10194 "Intended Audience :: Developers",
10295 "License :: OSI Approved :: MIT License",
10396 "Natural Language :: English",
104 "Programming Language :: Python :: 2",
105 "Programming Language :: Python :: 2.7",
10697 "Programming Language :: Python :: 3",
107 "Programming Language :: Python :: 3.5",
10898 "Programming Language :: Python :: 3.6",
10999 "Programming Language :: Python :: 3.7",
100 "Programming Language :: Python :: 3.8",
101 "Programming Language :: Python :: 3.9",
102 "Programming Language :: Python :: 3 :: Only",
110103 "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
111104 "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
112105 ],
0 # -*- coding: utf-8 -*-
10 from distutils.version import LooseVersion
21 from marshmallow.utils import missing
32
54 from marshmallow import validate
65
76 from webargs.core import ValidationError
8 from webargs.dict2schema import dict2schema
97 from webargs import fields
108
11 __version__ = "5.5.3"
9 __version__ = "7.0.1"
1210 __version_info__ = tuple(LooseVersion(__version__).version)
13 __all__ = ("dict2schema", "ValidationError", "fields", "missing", "validate")
11 __all__ = ("ValidationError", "fields", "missing", "validate")
2424 import typing
2525
2626 from aiohttp import web
27 from aiohttp.web import Request
2827 from aiohttp import web_exceptions
29 from marshmallow import Schema, ValidationError
30 from marshmallow.fields import Field
28 from marshmallow import Schema, ValidationError, RAISE
3129
3230 from webargs import core
3331 from webargs.core import json
3432 from webargs.asyncparser import AsyncParser
33 from webargs.multidictproxy import MultiDictProxy
3534
3635
37 def is_json_request(req: Request) -> bool:
36 def is_json_request(req) -> bool:
3837 content_type = req.content_type
3938 return core.is_json(content_type)
4039
7170 class AIOHTTPParser(AsyncParser):
7271 """aiohttp request argument parser."""
7372
73 DEFAULT_UNKNOWN_BY_LOCATION = {
74 "match_info": RAISE,
75 "path": RAISE,
76 **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION,
77 }
7478 __location_map__ = dict(
75 match_info="parse_match_info",
76 path="parse_match_info",
77 **core.Parser.__location_map__
79 match_info="load_match_info",
80 path="load_match_info",
81 **core.Parser.__location_map__,
7882 )
7983
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)
84 def load_querystring(self, req, schema: Schema) -> MultiDictProxy:
85 """Return query params from the request as a MultiDictProxy."""
86 return MultiDictProxy(req.query, schema)
8387
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)
88 async def load_form(self, req, schema: Schema) -> MultiDictProxy:
89 """Return form values from the request as a MultiDictProxy."""
90 post_data = await req.post()
91 return MultiDictProxy(post_data, schema)
9092
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)):
93 async def load_json_or_form(
94 self, req, schema: Schema
95 ) -> typing.Union[typing.Dict, MultiDictProxy]:
96 data = await self.load_json(req, schema)
97 if data is not core.missing:
98 return data
99 return await self.load_form(req, schema)
100
101 async def load_json(self, req, schema: Schema):
102 """Return a parsed json payload from the request."""
103 if not (req.body_exists and is_json_request(req)):
104 return core.missing
105 try:
106 return await req.json(loads=json.loads)
107 except json.JSONDecodeError as exc:
108 if exc.doc == "":
96109 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)
110 return self._handle_invalid_json_error(exc, req)
111 except UnicodeDecodeError as exc:
112 return self._handle_invalid_json_error(exc, req)
106113
107 self._cache["json"] = json_data
108 return core.get_value(json_data, name, field, allow_many_nested=True)
114 def load_headers(self, req, schema: Schema) -> MultiDictProxy:
115 """Return headers from the request as a MultiDictProxy."""
116 return MultiDictProxy(req.headers, schema)
109117
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)
118 def load_cookies(self, req, schema: Schema) -> MultiDictProxy:
119 """Return cookies from the request as a MultiDictProxy."""
120 return MultiDictProxy(req.cookies, schema)
113121
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:
122 def load_files(self, req, schema: Schema) -> typing.NoReturn:
119123 raise NotImplementedError(
120 "parse_files is not implemented. You may be able to use parse_form for "
124 "load_files is not implemented. You may be able to use load_form for "
121125 "parsing upload data."
122126 )
123127
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)
128 def load_match_info(self, req, schema: Schema) -> typing.Mapping:
129 """Load the request's ``match_info``."""
130 return req.match_info
127131
128132 def get_request_from_view_args(
129133 self, view: typing.Callable, args: typing.Iterable, kwargs: typing.Mapping
130 ) -> Request:
134 ):
131135 """Get request object from a handler function or method. Used internally by
132136 ``use_args`` and ``use_kwargs``.
133137 """
136140 if isinstance(arg, web.Request):
137141 req = arg
138142 break
139 elif isinstance(arg, web.View):
143 if isinstance(arg, web.View):
140144 req = arg.request
141145 break
142 assert isinstance(req, web.Request), "Request argument not found for handler"
146 if not isinstance(req, web.Request):
147 raise ValueError("Request argument not found for handler")
143148 return req
144149
145150 def handle_error(
146151 self,
147152 error: ValidationError,
148 req: Request,
153 req,
149154 schema: Schema,
150 error_status_code: typing.Union[int, None] = None,
151 error_headers: typing.Union[typing.Mapping[str, str], None] = None,
152 ) -> "typing.NoReturn":
155 *,
156 error_status_code: typing.Optional[int],
157 error_headers: typing.Optional[typing.Mapping[str, str]]
158 ) -> typing.NoReturn:
153159 """Handle ValidationErrors and return a JSON response of error messages
154160 to the client.
155161 """
157163 error_status_code or self.DEFAULT_VALIDATION_STATUS
158164 )
159165 if not error_class:
160 raise LookupError("No exception for {0}".format(error_status_code))
166 raise LookupError(f"No exception for {error_status_code}")
161167 headers = error_headers
162168 raise error_class(
163169 body=json.dumps(error.messages).encode("utf-8"),
165171 content_type="application/json",
166172 )
167173
168 def handle_invalid_json_error(
174 def _handle_invalid_json_error(
169175 self,
170176 error: typing.Union[json.JSONDecodeError, UnicodeDecodeError],
171 req: Request,
177 req,
172178 *args,
173179 **kwargs
174 ) -> "typing.NoReturn":
180 ) -> typing.NoReturn:
175181 error_class = exception_map[400]
176182 messages = {"json": ["Invalid JSON body."]}
177183 raise error_class(
0 """Asynchronous request parser. Compatible with Python>=3.5."""
0 """Asynchronous request parser."""
11 import asyncio
22 import functools
33 import inspect
55 from collections.abc import Mapping
66
77 from marshmallow import Schema, ValidationError
8 from marshmallow.fields import Field
98 import marshmallow as ma
10 from marshmallow.utils import missing
119
1210 from webargs import core
1311
14 Request = typing.TypeVar("Request")
15 ArgMap = typing.Union[Schema, typing.Mapping[str, Field]]
16 Validate = typing.Union[typing.Callable, typing.Iterable[typing.Callable]]
12 AsyncErrorHandler = typing.Callable[..., typing.Awaitable[typing.NoReturn]]
1713
1814
1915 class AsyncParser(core.Parser):
2117 either coroutines or regular methods.
2218 """
2319
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
6520 # TODO: Lots of duplication from core.Parser here. Rethink.
6621 async def parse(
6722 self,
68 argmap: ArgMap,
69 req: Request = None,
70 locations: typing.Iterable = None,
71 validate: Validate = None,
72 error_status_code: typing.Union[int, None] = None,
73 error_headers: typing.Union[typing.Mapping[str, str], None] = None,
74 ) -> typing.Union[typing.Mapping, None]:
23 argmap: core.ArgMap,
24 req: typing.Optional[core.Request] = None,
25 *,
26 location: typing.Optional[str] = None,
27 unknown: typing.Optional[str] = core._UNKNOWN_DEFAULT_PARAM,
28 validate: core.ValidateArg = None,
29 error_status_code: typing.Optional[int] = None,
30 error_headers: typing.Optional[typing.Mapping[str, str]] = None
31 ) -> typing.Optional[typing.Mapping]:
7532 """Coroutine variant of `webargs.core.Parser`.
7633
7734 Receives the same arguments as `webargs.core.Parser.parse`.
7835 """
79 self.clear_cache() # in case someone used `parse_*()`
8036 req = req if req is not None else self.get_default_request()
81 assert req is not None, "Must pass req object"
37 location = location or self.location
38 unknown = (
39 unknown
40 if unknown != core._UNKNOWN_DEFAULT_PARAM
41 else (
42 self.unknown
43 if self.unknown != core._UNKNOWN_DEFAULT_PARAM
44 else self.DEFAULT_UNKNOWN_BY_LOCATION.get(location)
45 )
46 )
47 load_kwargs: typing.Dict[str, typing.Any] = (
48 {"unknown": unknown} if unknown else {}
49 )
50 if req is None:
51 raise ValueError("Must pass req object")
8252 data = None
8353 validators = core._ensure_list_of_callables(validate)
8454 schema = self._get_schema(argmap, req)
8555 try:
86 parsed = await self._parse_request(
87 schema=schema, req=req, locations=locations or self.locations
88 )
89 result = schema.load(parsed)
90 data = result.data if core.MARSHMALLOW_VERSION_INFO[0] < 3 else result
56 location_data = await self._load_location_data(
57 schema=schema, req=req, location=location
58 )
59 data = schema.load(location_data, **load_kwargs)
9160 self._validate_arguments(data, validators)
9261 except ma.exceptions.ValidationError as error:
93 await self._on_validation_error(
94 error, req, schema, error_status_code, error_headers
62 await self._async_on_validation_error(
63 error,
64 req,
65 schema,
66 location,
67 error_status_code=error_status_code,
68 error_headers=error_headers,
9569 )
9670 return data
9771
98 async def _on_validation_error(
72 async def _load_location_data(self, schema, req, location):
73 """Return a dictionary-like object for the location on the given request.
74
75 Needs to have the schema in hand in order to correctly handle loading
76 lists from multidict objects and `many=True` schemas.
77 """
78 loader_func = self._get_loader(location)
79 if asyncio.iscoroutinefunction(loader_func):
80 data = await loader_func(req, schema)
81 else:
82 data = loader_func(req, schema)
83
84 # when the desired location is empty (no data), provide an empty
85 # dict as the default so that optional arguments in a location
86 # (e.g. optional JSON body) work smoothly
87 if data is core.missing:
88 data = {}
89 return data
90
91 async def _async_on_validation_error(
9992 self,
10093 error: ValidationError,
101 req: Request,
94 req: core.Request,
10295 schema: Schema,
103 error_status_code: typing.Union[int, None],
104 error_headers: typing.Union[typing.Mapping[str, str], None] = None,
105 ) -> None:
96 location: str,
97 *,
98 error_status_code: typing.Optional[int],
99 error_headers: typing.Optional[typing.Mapping[str, str]]
100 ) -> typing.NoReturn:
101 # rewrite messages to be namespaced under the location which created
102 # them
103 # e.g. {"json":{"foo":["Not a valid integer."]}}
104 # instead of
105 # {"foo":["Not a valid integer."]}
106 error.messages = {location: error.messages}
106107 error_handler = self.error_callback or self.handle_error
107 await error_handler(error, req, schema, error_status_code, error_headers)
108 # an async error handler was registered, await it
109 if inspect.iscoroutinefunction(error_handler):
110 async_error_handler = typing.cast(AsyncErrorHandler, error_handler)
111 await async_error_handler(
112 error,
113 req,
114 schema,
115 error_status_code=error_status_code,
116 error_headers=error_headers,
117 )
118 # workaround for mypy not understanding `await Awaitable[NoReturn]`
119 # see: https://github.com/python/mypy/issues/8974
120 raise NotImplementedError("unreachable")
121 # the error handler was synchronous (e.g. Parser.handle_error) so it
122 # will raise an error
123 else:
124 error_handler(
125 error,
126 req,
127 schema,
128 error_status_code=error_status_code,
129 error_headers=error_headers,
130 )
108131
109132 def use_args(
110133 self,
111 argmap: ArgMap,
112 req: typing.Optional[Request] = None,
113 locations: typing.Iterable = None,
134 argmap: core.ArgMap,
135 req: typing.Optional[core.Request] = None,
136 *,
137 location: str = None,
138 unknown=core._UNKNOWN_DEFAULT_PARAM,
114139 as_kwargs: bool = False,
115 validate: Validate = None,
140 validate: core.ValidateArg = None,
116141 error_status_code: typing.Optional[int] = None,
117 error_headers: typing.Union[typing.Mapping[str, str], None] = None,
142 error_headers: typing.Optional[typing.Mapping[str, str]] = None
118143 ) -> typing.Callable[..., typing.Callable]:
119144 """Decorator that injects parsed arguments into a view function or method.
120145
121146 Receives the same arguments as `webargs.core.Parser.use_args`.
122147 """
123 locations = locations or self.locations
148 location = location or self.location
124149 request_obj = req
125150 # Optimization: If argmap is passed as a dictionary, we only need
126151 # to generate a Schema once
127152 if isinstance(argmap, Mapping):
128 argmap = core.dict2schema(argmap, self.schema_class)()
153 argmap = self.schema_class.from_dict(argmap)()
129154
130155 def decorator(func: typing.Callable) -> typing.Callable:
131156 req_ = request_obj
142167 parsed_args = await self.parse(
143168 argmap,
144169 req=req_obj,
145 locations=locations,
170 location=location,
171 unknown=unknown,
146172 validate=validate,
147173 error_status_code=error_status_code,
148174 error_headers=error_headers,
149175 )
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)
176 args, kwargs = self._update_args_kwargs(
177 args, kwargs, parsed_args, as_kwargs
178 )
179 return await func(*args, **kwargs)
157180
158181 else:
159182
167190 parsed_args = yield from self.parse( # type: ignore
168191 argmap,
169192 req=req_obj,
170 locations=locations,
193 location=location,
194 unknown=unknown,
171195 validate=validate,
172196 error_status_code=error_status_code,
173197 error_headers=error_headers,
174198 )
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)
199 args, kwargs = self._update_args_kwargs(
200 args, kwargs, parsed_args, as_kwargs
201 )
202 return func(*args, **kwargs)
182203
183204 return wrapper
184205
185206 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
-22
src/webargs/compat.py less more
0 # -*- coding: utf-8 -*-
1 # flake8: noqa
2 import sys
3 from distutils.version import LooseVersion
4
5 import marshmallow as ma
6
7 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
4 import inspect
1 import typing
52 import logging
6 import warnings
7 from copy import copy
8
9 try:
10 import simplejson as json
11 except ImportError:
12 import json # type: ignore
3 from collections.abc import Mapping
4 import json
135
146 import marshmallow as ma
157 from marshmallow import ValidationError
16 from marshmallow.utils import missing, is_collection
17
18 from webargs.compat import Mapping, iteritems, MARSHMALLOW_VERSION_INFO
19 from webargs.dict2schema import dict2schema
8 from marshmallow.utils import missing
9
2010 from webargs.fields import DelimitedList
2111
2212 logger = logging.getLogger(__name__)
2414
2515 __all__ = [
2616 "ValidationError",
27 "dict2schema",
2817 "is_multiple",
2918 "Parser",
30 "get_value",
3119 "missing",
3220 "parse_json",
3321 ]
3422
3523
36 DEFAULT_VALIDATION_STATUS = 422 # type: int
37
38
39 def _callable_or_raise(obj):
24 Request = typing.TypeVar("Request")
25 ArgMap = typing.Union[
26 ma.Schema,
27 typing.Mapping[str, ma.fields.Field],
28 typing.Callable[[Request], ma.Schema],
29 ]
30 ValidateArg = typing.Union[None, typing.Callable, typing.Iterable[typing.Callable]]
31 CallableList = typing.List[typing.Callable]
32 ErrorHandler = typing.Callable[..., typing.NoReturn]
33 # generic type var with no particular meaning
34 T = typing.TypeVar("T")
35
36
37 # a value used as the default for arguments, so that when `None` is passed, it
38 # can be distinguished from the default value
39 _UNKNOWN_DEFAULT_PARAM = "_default"
40
41 DEFAULT_VALIDATION_STATUS: int = 422
42
43
44 def _iscallable(x) -> bool:
45 # workaround for
46 # https://github.com/python/mypy/issues/9778
47 return callable(x)
48
49
50 def _callable_or_raise(obj: typing.Optional[T]) -> typing.Optional[T]:
4051 """Makes sure an object is callable if it is not ``None``. If not
4152 callable, a ValueError is raised.
4253 """
43 if obj and not callable(obj):
44 raise ValueError("{0!r} is not callable.".format(obj))
45 else:
46 return obj
47
48
49 def is_multiple(field):
54 if obj and not _iscallable(obj):
55 raise ValueError(f"{obj!r} is not callable.")
56 return obj
57
58
59 def is_multiple(field: ma.fields.Field) -> bool:
5060 """Return whether or not `field` handles repeated/multi-value arguments."""
5161 return isinstance(field, ma.fields.List) and not isinstance(field, DelimitedList)
5262
5363
54 def get_mimetype(content_type):
55 return content_type.split(";")[0].strip() if content_type else None
64 def get_mimetype(content_type: str) -> str:
65 return content_type.split(";")[0].strip()
5666
5767
5868 # Adapted from werkzeug:
5969 # https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/wrappers.py
60 def is_json(mimetype):
70 def is_json(mimetype: typing.Optional[str]) -> bool:
6171 """Indicates if this mimetype is JSON or not. By default a request
6272 is considered to include JSON data if the mimetype is
6373 ``application/json`` or ``application/*+json``.
7383 return False
7484
7585
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):
86 def parse_json(s: typing.AnyStr, *, encoding: str = "utf-8") -> typing.Any:
87 if isinstance(s, str):
88 decoded = s
89 else:
11490 try:
115 s = s.decode(encoding)
116 except UnicodeDecodeError as e:
91 decoded = s.decode(encoding)
92 except UnicodeDecodeError as exc:
11793 raise json.JSONDecodeError(
118 "Bytes decoding error : {}".format(e.reason),
119 doc=str(e.object),
120 pos=e.start,
94 f"Bytes decoding error : {exc.reason}",
95 doc=str(exc.object),
96 pos=exc.start,
12197 )
122 return json.loads(s)
123
124
125 def _ensure_list_of_callables(obj):
98 return json.loads(decoded)
99
100
101 def _ensure_list_of_callables(obj: typing.Any) -> CallableList:
126102 if obj:
127103 if isinstance(obj, (list, tuple)):
128 validators = obj
104 validators = typing.cast(CallableList, list(obj))
129105 elif callable(obj):
130106 validators = [obj]
131107 else:
132 raise ValueError(
133 "{0!r} is not a callable or list of callables.".format(obj)
134 )
108 raise ValueError(f"{obj!r} is not a callable or list of callables.")
135109 else:
136110 validators = []
137111 return validators
138112
139113
140 class Parser(object):
114 class Parser:
141115 """Base parser class that provides high-level implementation for parsing
142116 a request.
143117
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.
118 Descendant classes must provide lower-level implementations for reading
119 data from different locations, e.g. ``load_json``, ``load_querystring``,
120 etc.
121
122 :param str location: Default location to use for data
123 :param str unknown: A default value to pass for ``unknown`` when calling the
124 schema's ``load`` method. Defaults to EXCLUDE for non-body
125 locations and RAISE for request bodies. Pass ``None`` to use the
126 schema's setting instead.
148127 :param callable error_handler: Custom error handler function.
149128 """
150129
151 #: Default locations to check for data
152 DEFAULT_LOCATIONS = ("querystring", "form", "json")
130 #: Default location to check for data
131 DEFAULT_LOCATION: str = "json"
132 #: Default value to use for 'unknown' on schema load
133 # on a per-location basis
134 DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, str] = {
135 "json": ma.RAISE,
136 "form": ma.RAISE,
137 "json_or_form": ma.RAISE,
138 "querystring": ma.EXCLUDE,
139 "query": ma.EXCLUDE,
140 "headers": ma.EXCLUDE,
141 "cookies": ma.EXCLUDE,
142 "files": ma.EXCLUDE,
143 }
153144 #: The marshmallow Schema class to use when creating new schemas
154 DEFAULT_SCHEMA_CLASS = ma.Schema
145 DEFAULT_SCHEMA_CLASS: typing.Type = ma.Schema
155146 #: Default status code to return for validation errors
156 DEFAULT_VALIDATION_STATUS = DEFAULT_VALIDATION_STATUS
147 DEFAULT_VALIDATION_STATUS: int = DEFAULT_VALIDATION_STATUS
157148 #: Default error message for validation errors
158 DEFAULT_VALIDATION_MESSAGE = "Invalid value."
149 DEFAULT_VALIDATION_MESSAGE: str = "Invalid value."
159150
160151 #: Maps location => method name
161 __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",
152 __location_map__: typing.Dict[str, typing.Union[str, typing.Callable]] = {
153 "json": "load_json",
154 "querystring": "load_querystring",
155 "query": "load_querystring",
156 "form": "load_form",
157 "headers": "load_headers",
158 "cookies": "load_cookies",
159 "files": "load_files",
160 "json_or_form": "load_json_or_form",
169161 }
170162
171 def __init__(self, locations=None, error_handler=None, schema_class=None):
172 self.locations = locations or self.DEFAULT_LOCATIONS
173 self.error_callback = _callable_or_raise(error_handler)
163 def __init__(
164 self,
165 location: typing.Optional[str] = None,
166 *,
167 unknown: typing.Optional[str] = _UNKNOWN_DEFAULT_PARAM,
168 error_handler: typing.Optional[ErrorHandler] = None,
169 schema_class: typing.Optional[typing.Type] = None
170 ):
171 self.location = location or self.DEFAULT_LOCATION
172 self.error_callback: typing.Optional[ErrorHandler] = _callable_or_raise(
173 error_handler
174 )
174175 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
176 self.unknown = unknown
177
178 def _get_loader(self, location: str) -> typing.Callable:
179 """Get the loader function for the given location.
180
181 :raises: ValueError if a given location is invalid.
182 """
185183 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))
190 raise ValueError(msg)
191 return locations
192
193 def _get_handler(self, location):
184 if location not in valid_locations:
185 raise ValueError(f"Invalid location argument: {location}")
186
194187 # Parsing function to call
195188 # May be a method name (str) or a function
196 func = self.__location_map__.get(location)
197 if func:
198 if inspect.isfunction(func):
199 function = func
200 else:
201 function = getattr(self, func)
202 else:
203 raise ValueError('Invalid location: "{0}"'.format(location))
204 return function
205
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
189 func = self.__location_map__[location]
190 if isinstance(func, str):
191 return getattr(self, func)
192 return func
193
194 def _load_location_data(
195 self, *, schema: ma.Schema, req: Request, location: str
196 ) -> typing.Mapping:
197 """Return a dictionary-like object for the location on the given request.
198
199 Needs to have the schema in hand in order to correctly handle loading
200 lists from multidict objects and `many=True` schemas.
201 """
202 loader_func = self._get_loader(location)
203 data = loader_func(req, schema)
204 # when the desired location is empty (no data), provide an empty
205 # dict as the default so that optional arguments in a location
206 # (e.g. optional JSON body) work smoothly
207 if data is missing:
208 data = {}
209 return data
273210
274211 def _on_validation_error(
275 self, error, req, schema, error_status_code, error_headers
276 ):
277 error_handler = self.error_callback or self.handle_error
278 error_handler(error, req, schema, error_status_code, error_headers)
279
280 def _validate_arguments(self, data, validators):
212 self,
213 error: ValidationError,
214 req: Request,
215 schema: ma.Schema,
216 location: str,
217 *,
218 error_status_code: typing.Optional[int],
219 error_headers: typing.Optional[typing.Mapping[str, str]]
220 ) -> typing.NoReturn:
221 # rewrite messages to be namespaced under the location which created
222 # them
223 # e.g. {"json":{"foo":["Not a valid integer."]}}
224 # instead of
225 # {"foo":["Not a valid integer."]}
226 error.messages = {location: error.messages}
227 error_handler: ErrorHandler = self.error_callback or self.handle_error
228 error_handler(
229 error,
230 req,
231 schema,
232 error_status_code=error_status_code,
233 error_headers=error_headers,
234 )
235
236 def _validate_arguments(self, data: typing.Any, validators: CallableList) -> None:
237 # although `data` is typically a Mapping, nothing forbids a `schema.load`
238 # from returning an arbitrary object subject to validators
281239 for validator in validators:
282240 if validator(data) is False:
283241 msg = self.DEFAULT_VALIDATION_MESSAGE
284242 raise ValidationError(msg, data=data)
285243
286 def _get_schema(self, argmap, req):
244 def _get_schema(self, argmap: ArgMap, req: Request) -> ma.Schema:
287245 """Return a `marshmallow.Schema` for the given argmap and request.
288246
289247 :param argmap: Either a `marshmallow.Schema`, `dict`
299257 elif callable(argmap):
300258 schema = argmap(req)
301259 else:
302 schema = dict2schema(argmap, self.schema_class)()
303 if MARSHMALLOW_VERSION_INFO[0] < 3 and not schema.strict:
304 warnings.warn(
305 "It is highly recommended that you set strict=True on your schema "
306 "so that the parser's error handler will be invoked when expected.",
307 UserWarning,
308 )
260 schema = self.schema_class.from_dict(argmap)()
309261 return schema
310
311 def _clone(self):
312 clone = copy(self)
313 clone.clear_cache()
314 return clone
315262
316263 def parse(
317264 self,
318 argmap,
319 req=None,
320 locations=None,
321 validate=None,
322 error_status_code=None,
323 error_headers=None,
265 argmap: ArgMap,
266 req: typing.Optional[Request] = None,
267 *,
268 location: typing.Optional[str] = None,
269 unknown: typing.Optional[str] = _UNKNOWN_DEFAULT_PARAM,
270 validate: ValidateArg = None,
271 error_status_code: typing.Optional[int] = None,
272 error_headers: typing.Optional[typing.Mapping[str, str]] = None
324273 ):
325274 """Main request parsing method.
326275
328277 of argname -> `marshmallow.fields.Field` pairs, or a callable
329278 which accepts a request and returns a `marshmallow.Schema`.
330279 :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')``.
280 :param str location: Where on the request to load values.
281 Can be any of the values in :py:attr:`~__location_map__`. By
282 default, that means one of ``('json', 'query', 'querystring',
283 'form', 'headers', 'cookies', 'files', 'json_or_form')``.
284 :param str unknown: A value to pass for ``unknown`` when calling the
285 schema's ``load`` method. Defaults to EXCLUDE for non-body
286 locations and RAISE for request bodies. Pass ``None`` to use the
287 schema's setting instead.
334288 :param callable validate: Validation function or list of validation functions
335289 that receives the dictionary of parsed arguments. Validator either returns a
336290 boolean or raises a :exc:`ValidationError`.
341295
342296 :return: A dictionary of parsed arguments
343297 """
344 self.clear_cache() # in case someone used `parse_*()`
345298 req = req if req is not None else self.get_default_request()
346 assert req is not None, "Must pass req object"
299 location = location or self.location
300 # precedence order: explicit, instance setting, default per location
301 unknown = (
302 unknown
303 if unknown != _UNKNOWN_DEFAULT_PARAM
304 else (
305 self.unknown
306 if self.unknown != _UNKNOWN_DEFAULT_PARAM
307 else self.DEFAULT_UNKNOWN_BY_LOCATION.get(location)
308 )
309 )
310 load_kwargs: typing.Dict[str, typing.Any] = (
311 {"unknown": unknown} if unknown else {}
312 )
313 if req is None:
314 raise ValueError("Must pass req object")
347315 data = None
348316 validators = _ensure_list_of_callables(validate)
349 parser = self._clone()
350317 schema = self._get_schema(argmap, req)
351318 try:
352 parsed = parser._parse_request(
353 schema=schema, req=req, locations=locations or self.locations
319 location_data = self._load_location_data(
320 schema=schema, req=req, location=location
354321 )
355 result = schema.load(parsed)
356 data = result.data if MARSHMALLOW_VERSION_INFO[0] < 3 else result
357 parser._validate_arguments(data, validators)
322 data = schema.load(location_data, **load_kwargs)
323 self._validate_arguments(data, validators)
358324 except ma.exceptions.ValidationError as error:
359 parser._on_validation_error(
360 error, req, schema, error_status_code, error_headers
325 self._on_validation_error(
326 error,
327 req,
328 schema,
329 location,
330 error_status_code=error_status_code,
331 error_headers=error_headers,
361332 )
333 raise ValueError(
334 "_on_validation_error hook did not raise an exception"
335 ) from error
362336 return data
363337
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
374
375 def get_default_request(self):
338 def get_default_request(self) -> typing.Optional[Request]:
376339 """Optional override. Provides a hook for frameworks that use thread-local
377340 request objects.
378341 """
379342 return None
380343
381 def get_request_from_view_args(self, view, args, kwargs):
344 def get_request_from_view_args(
345 self,
346 view: typing.Callable,
347 args: typing.Tuple,
348 kwargs: typing.Mapping[str, typing.Any],
349 ) -> typing.Optional[Request]:
382350 """Optional override. Returns the request object to be parsed, given a view
383351 function's args and kwargs.
384352
392360 """
393361 return None
394362
363 @staticmethod
364 def _update_args_kwargs(
365 args: typing.Tuple,
366 kwargs: typing.Dict[str, typing.Any],
367 parsed_args: typing.Tuple,
368 as_kwargs: bool,
369 ) -> typing.Tuple[typing.Tuple, typing.Mapping]:
370 """Update args or kwargs with parsed_args depending on as_kwargs"""
371 if as_kwargs:
372 kwargs.update(parsed_args)
373 else:
374 # Add parsed_args after other positional arguments
375 args += (parsed_args,)
376 return args, kwargs
377
395378 def use_args(
396379 self,
397 argmap,
398 req=None,
399 locations=None,
400 as_kwargs=False,
401 validate=None,
402 error_status_code=None,
403 error_headers=None,
404 ):
380 argmap: ArgMap,
381 req: typing.Optional[Request] = None,
382 *,
383 location: typing.Optional[str] = None,
384 unknown: typing.Optional[str] = _UNKNOWN_DEFAULT_PARAM,
385 as_kwargs: bool = False,
386 validate: ValidateArg = None,
387 error_status_code: typing.Optional[int] = None,
388 error_headers: typing.Optional[typing.Mapping[str, str]] = None
389 ) -> typing.Callable[..., typing.Callable]:
405390 """Decorator that injects parsed arguments into a view function or method.
406391
407392 Example usage with Flask: ::
408393
409394 @app.route('/echo', methods=['get', 'post'])
410 @parser.use_args({'name': fields.Str()})
395 @parser.use_args({'name': fields.Str()}, location="querystring")
411396 def greet(args):
412397 return 'Hello ' + args['name']
413398
414399 :param argmap: Either a `marshmallow.Schema`, a `dict`
415400 of argname -> `marshmallow.fields.Field` pairs, or a callable
416401 which accepts a request and returns a `marshmallow.Schema`.
417 :param tuple locations: Where on the request to search for values.
402 :param str location: Where on the request to load values.
403 :param str unknown: A value to pass for ``unknown`` when calling the
404 schema's ``load`` method.
418405 :param bool as_kwargs: Whether to insert arguments as keyword arguments.
419406 :param callable validate: Validation function that receives the dictionary
420407 of parsed arguments. If the function returns ``False``, the parser
424411 :param dict error_headers: Headers passed to error handler functions when a
425412 a `ValidationError` is raised.
426413 """
427 locations = locations or self.locations
414 location = location or self.location
428415 request_obj = req
429416 # Optimization: If argmap is passed as a dictionary, we only need
430417 # to generate a Schema once
431418 if isinstance(argmap, Mapping):
432 argmap = dict2schema(argmap, self.schema_class)()
419 argmap = self.schema_class.from_dict(argmap)()
433420
434421 def decorator(func):
435422 req_ = request_obj
440427
441428 if not req_obj:
442429 req_obj = self.get_request_from_view_args(func, args, kwargs)
430
443431 # NOTE: At this point, argmap may be a Schema, or a callable
444432 parsed_args = self.parse(
445433 argmap,
446434 req=req_obj,
447 locations=locations,
435 location=location,
436 unknown=unknown,
448437 validate=validate,
449438 error_status_code=error_status_code,
450439 error_headers=error_headers,
451440 )
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)
441 args, kwargs = self._update_args_kwargs(
442 args, kwargs, parsed_args, as_kwargs
443 )
444 return func(*args, **kwargs)
459445
460446 wrapper.__wrapped__ = func
461447 return wrapper
462448
463449 return decorator
464450
465 def use_kwargs(self, *args, **kwargs):
451 def use_kwargs(self, *args, **kwargs) -> typing.Callable:
466452 """Decorator that injects parsed arguments into a view function or method
467453 as keyword arguments.
468454
480466 kwargs["as_kwargs"] = True
481467 return self.use_args(*args, **kwargs)
482468
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.
469 def location_loader(self, name: str):
470 """Decorator that registers a function for loading a request location.
471 The wrapped function receives a schema and a request.
472
473 The schema will usually not be relevant, but it's important in some
474 cases -- most notably in order to correctly load multidict values into
475 list fields. Without the schema, there would be no way to know whether
476 to simply `.get()` or `.getall()` from a multidict for a given value.
487477
488478 Example: ::
489479
490480 from webargs import core
491481 parser = core.Parser()
492482
493 @parser.location_handler("name")
494 def parse_data(request, name, field):
495 return request.data.get(name)
483 @parser.location_loader("name")
484 def load_data(request, schema):
485 return request.data
496486
497487 :param str name: The name of the location to register.
498488 """
503493
504494 return decorator
505495
506 def error_handler(self, func):
496 def error_handler(self, func: ErrorHandler) -> ErrorHandler:
507497 """Decorator that registers a custom error handling function. The
508498 function should receive the raised error, request object,
509499 `marshmallow.Schema` instance used to parse the request, error status code,
522512
523513
524514 @parser.error_handler
525 def handle_error(error, req, schema, status_code, headers):
515 def handle_error(error, req, schema, *, error_status_code, error_headers):
526516 raise CustomError(error.messages)
527517
528518 :param callable func: The error callback to register.
530520 self.error_callback = func
531521 return func
532522
523 def _handle_invalid_json_error(
524 self,
525 error: typing.Union[json.JSONDecodeError, UnicodeDecodeError],
526 req: Request,
527 *args,
528 **kwargs
529 ) -> typing.NoReturn:
530 """Internal hook for overriding treatment of JSONDecodeErrors.
531
532 Invoked by default `load_json` implementation.
533
534 External parsers can just implement their own behavior for load_json ,
535 so this is not part of the public parser API.
536 """
537 raise error
538
539 def load_json(self, req: Request, schema: ma.Schema) -> typing.Any:
540 """Load JSON from a request object or return `missing` if no value can
541 be found.
542 """
543 # NOTE: although this implementation is real/concrete and used by
544 # several of the parsers in webargs, it relies on the internal hooks
545 # `_handle_invalid_json_error` and `_raw_load_json`
546 # these methods are not part of the public API and are used to simplify
547 # code sharing amongst the built-in webargs parsers
548 try:
549 return self._raw_load_json(req)
550 except json.JSONDecodeError as exc:
551 if exc.doc == "":
552 return missing
553 return self._handle_invalid_json_error(exc, req)
554 except UnicodeDecodeError as exc:
555 return self._handle_invalid_json_error(exc, req)
556
557 def load_json_or_form(self, req: Request, schema: ma.Schema):
558 """Load data from a request, accepting either JSON or form-encoded
559 data.
560
561 The data will first be loaded as JSON, and, if that fails, it will be
562 loaded as a form post.
563 """
564 data = self.load_json(req, schema)
565 if data is not missing:
566 return data
567 return self.load_form(req, schema)
568
533569 # Abstract Methods
534570
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.
571 def _raw_load_json(self, req: Request):
572 """Internal hook method for implementing load_json()
573
574 Get a request body for feeding in to `load_json`, and parse it either
575 using core.parse_json() or similar utilities which raise
576 JSONDecodeErrors.
577 Ensure consistent behavior when encountering decoding errors.
578
579 The default implementation here simply returns `missing`, and the default
580 implementation of `load_json` above will pass that value through.
581 However, by implementing a "mostly concrete" version of load_json with
582 this as a hook for getting data, we consolidate the logic for handling
583 those JSONDecodeErrors.
538584 """
539585 return missing
540586
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.
587 def load_querystring(self, req: Request, schema: ma.Schema):
588 """Load the query string of a request object or return `missing` if no
589 value can be found.
544590 """
545591 return missing
546592
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.
593 def load_form(self, req: Request, schema: ma.Schema):
594 """Load the form data of a request object or return `missing` if no
595 value can be found.
550596 """
551597 return missing
552598
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 """
599 def load_headers(self, req: Request, schema: ma.Schema):
600 """Load the headers or return `missing` if no value can be found."""
557601 return missing
558602
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.
603 def load_cookies(self, req: Request, schema: ma.Schema):
604 """Load the cookies from the request or return `missing` if no value
605 can be found.
562606 """
563607 return missing
564608
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.
609 def load_files(self, req: Request, schema: ma.Schema):
610 """Load files from the request or return `missing` if no values can be
611 found.
568612 """
569613 return missing
570614
571615 def handle_error(
572 self, error, req, schema, error_status_code=None, error_headers=None
573 ):
616 self,
617 error: ValidationError,
618 req: Request,
619 schema: ma.Schema,
620 *,
621 error_status_code: int,
622 error_headers: typing.Mapping[str, str]
623 ) -> typing.NoReturn:
574624 """Called if an error occurs while parsing args. By default, just logs and
575625 raises ``error``.
576626 """
+0
-17
src/webargs/dict2schema.py less more
0 # -*- coding: utf-8 -*-
1 import marshmallow as ma
2
3
4 def dict2schema(dct, schema_class=ma.Schema):
5 """Generate a `marshmallow.Schema` class given a dictionary of
6 `Fields <marshmallow.fields.Field>`.
7 """
8 if hasattr(schema_class, "from_dict"): # marshmallow 3
9 return schema_class.from_dict(dct)
10 attrs = dct.copy()
11
12 class Meta(object):
13 strict = True
14
15 attrs["Meta"] = Meta
16 return type(str(""), (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):
66 raise NotImplementedError(
67 "Header parsing not supported by {0}".format(self.__class__.__name__)
68 )
56 def load_cookies(self, req, schema):
57 """Return cookies from the request."""
58 return req.COOKIES
6959
70 def parse_files(self, req, name, field):
71 """Pull a file from the request."""
72 return core.get_value(req.FILES, name, field)
60 def load_headers(self, req, schema):
61 """Return headers from the request."""
62 # Django's HttpRequest.headers is a case-insensitive dict type, but it
63 # isn't a multidict, so this is not proxied
64 return req.headers
65
66 def load_files(self, req, schema):
67 """Return files from the request as a MultiDictProxy."""
68 return MultiDictProxy(req.FILES, schema)
7369
7470 def get_request_from_view_args(self, view, args, kwargs):
7571 # The first argument is either `self` or `request`
7874 except AttributeError: # first arg is request
7975 return args[0]
8076
81 def handle_invalid_json_error(self, error, req, *args, **kwargs):
82 raise error
83
8477
8578 parser = DjangoParser()
8679 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
5 import marshmallow as ma
6
67 from webargs import core
7 from webargs.core import json
8 from webargs.multidictproxy import MultiDictProxy
89
910 HTTP_422 = "422 Unprocessable Entity"
1011
2930 return content_type and core.is_json(content_type)
3031
3132
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
4933 # NOTE: Adapted from falcon.request.Request._parse_form_urlencoded
5034 def parse_form_body(req):
5135 if (
5236 req.content_type is not None
5337 and "application/x-www-form-urlencoded" in req.content_type
5438 ):
55 body = req.stream.read()
39 body = req.stream.read(req.content_length or 0)
5640 try:
5741 body = body.decode("ascii")
5842 except UnicodeDecodeError:
6549 )
6650
6751 if body:
68 return parse_query_string(
69 body, keep_blank_qs_values=req.options.keep_blank_qs_values
70 )
71 return {}
52 return parse_query_string(body, keep_blank=req.options.keep_blank_qs_values)
53
54 return core.missing
7255
7356
7457 class HTTPError(falcon.HTTPError):
75 """HTTPError that stores a dictionary of validation error messages.
76 """
58 """HTTPError that stores a dictionary of validation error messages."""
7759
7860 def __init__(self, status, errors, *args, **kwargs):
7961 self.errors = errors
80 super(HTTPError, self).__init__(status, *args, **kwargs)
62 super().__init__(status, *args, **kwargs)
8163
8264 def to_dict(self, *args, **kwargs):
8365 """Override `falcon.HTTPError` to include error messages in responses."""
84 ret = super(HTTPError, self).to_dict(*args, **kwargs)
66 ret = super().to_dict(*args, **kwargs)
8567 if self.errors is not None:
8668 ret["errors"] = self.errors
8769 return ret
8870
8971
9072 class FalconParser(core.Parser):
91 """Falcon request argument parser."""
73 """Falcon request argument parser.
9274
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)
75 Defaults to using the `media` location. See :py:meth:`~FalconParser.load_media` for
76 details on the media location."""
9677
97 def parse_form(self, req, name, field):
98 """Pull a form value from the request.
78 # by default, Falcon will use the 'media' location to load data
79 #
80 # this effectively looks the same as loading JSON data by default, but if
81 # you add a handler for a different media type to Falcon, webargs will
82 # automatically pick up on that capability
83 DEFAULT_LOCATION = "media"
84 DEFAULT_UNKNOWN_BY_LOCATION = dict(
85 media=ma.RAISE, **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION
86 )
87 __location_map__ = dict(media="load_media", **core.Parser.__location_map__)
88
89 # Note on the use of MultiDictProxy throughout:
90 # Falcon parses query strings and form values into ordinary dicts, but with
91 # the values listified where appropriate
92 # it is still therefore necessary in these cases to wrap them in
93 # MultiDictProxy because we need to use the schema to determine when single
94 # values should be wrapped in lists due to the type of the destination
95 # field
96
97 def load_querystring(self, req, schema):
98 """Return query params from the request as a MultiDictProxy."""
99 return MultiDictProxy(req.params, schema)
100
101 def load_form(self, req, schema):
102 """Return form values from the request as a MultiDictProxy
99103
100104 .. note::
101105
102106 The request stream will be read and left at EOF.
103107 """
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)
108 form = parse_form_body(req)
109 if form is core.missing:
110 return form
111 return MultiDictProxy(form, schema)
108112
109 def parse_json(self, req, name, field):
110 """Pull a JSON body value from the request.
113 def load_media(self, req, schema):
114 """Return data unpacked and parsed by one of Falcon's media handlers.
115 By default, Falcon only handles JSON payloads.
116
117 To configure additional media handlers, see the
118 `Falcon documentation on media types`__.
119
120 .. _FalconMedia: https://falcon.readthedocs.io/en/stable/api/media.html
121 __ FalconMedia_
111122
112123 .. note::
113124
114125 The request stream will be read and left at EOF.
115126 """
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)
127 # if there is no body, return missing instead of erroring
128 if req.content_length in (None, 0):
129 return core.missing
130 return req.media
123131
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
132 def _raw_load_json(self, req):
133 """Return a json payload from the request for the core parser's load_json
128134
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)
135 Checks the input mimetype and may return 'missing' if the mimetype is
136 non-json, even if the request body is parseable as json."""
137 if not is_json_request(req) or req.content_length in (None, 0):
138 return core.missing
139 body = req.stream.read(req.content_length)
140 if body:
141 return core.parse_json(body)
142 return core.missing
143
144 def load_headers(self, req, schema):
145 """Return headers from the request."""
146 # Falcon only exposes headers as a dict (not multidict)
147 return req.headers
148
149 def load_cookies(self, req, schema):
150 """Return cookies from the request."""
151 # Cookies are expressed in Falcon as a dict, but the possibility of
152 # multiple values for a cookie is preserved internally -- if desired in
153 # the future, webargs could add a MultiDict type for Cookies here built
154 # from (req, schema), but Falcon does not provide one out of the box
155 return req.cookies
135156
136157 def get_request_from_view_args(self, view, args, kwargs):
137158 """Get request from a resource method's arguments. Assumes that
138159 request is the second argument.
139160 """
140161 req = args[1]
141 assert isinstance(req, falcon.Request), "Argument is not a falcon.Request"
162 if not isinstance(req, falcon.Request):
163 raise TypeError("Argument is not a falcon.Request")
142164 return req
143165
144 def parse_files(self, req, name, field):
166 def load_files(self, req, schema):
145167 raise NotImplementedError(
146 "Parsing files not yet supported by {0}".format(self.__class__.__name__)
168 f"Parsing files not yet supported by {self.__class__.__name__}"
147169 )
148170
149 def handle_error(self, error, req, schema, error_status_code, error_headers):
171 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
150172 """Handles errors during parsing."""
151173 status = status_map.get(error_status_code or self.DEFAULT_VALIDATION_STATUS)
152174 if status is None:
153 raise LookupError("Status code {0} not supported".format(error_status_code))
175 raise LookupError(f"Status code {error_status_code} not supported")
154176 raise HTTPError(status, errors=error.messages, headers=error_headers)
155177
156 def handle_invalid_json_error(self, error, req, *args, **kwargs):
178 def _handle_invalid_json_error(self, error, req, *args, **kwargs):
157179 status = status_map[400]
158180 messages = {"json": ["Invalid JSON body."]}
159181 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 }
14 """
15 import typing
1516
16 Note: `data_key` replaced `load_from` in marshmallow 3.
17 When using marshmallow 2, use `load_from`.
18 """
1917 import marshmallow as ma
2018
2119 # Expose all fields from marshmallow.fields.
2220 from marshmallow.fields import * # noqa: F40
23 from webargs.compat import MARSHMALLOW_VERSION_INFO
24 from webargs.dict2schema import dict2schema
2521
2622 __all__ = ["DelimitedList"] + ma.fields.__all__
2723
2824
29 class Nested(ma.fields.Nested):
25 class Nested(ma.fields.Nested): # type: ignore[no-redef]
3026 """Same as `marshmallow.fields.Nested`, except can be passed a dictionary as
3127 the first argument, which will be converted to a `marshmallow.Schema`.
3228
3935
4036 def __init__(self, nested, *args, **kwargs):
4137 if isinstance(nested, dict):
42 nested = dict2schema(nested)
43 super(Nested, self).__init__(nested, *args, **kwargs)
38 nested = ma.Schema.from_dict(nested)
39 super().__init__(nested, *args, **kwargs)
4440
4541
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").
42 class DelimitedFieldMixin:
43 """
44 This is a mixin class for subclasses of ma.fields.List and ma.fields.Tuple
45 which split on a pre-specified delimiter. By default, the delimiter will be ","
46
47 Because we want the MRO to reach this class before the List or Tuple class,
48 it must be listed first in the superclasses
49
50 For example, a DelimitedList-like type can be defined like so:
51
52 >>> class MyDelimitedList(DelimitedFieldMixin, ma.fields.List):
53 >>> pass
54 """
55
56 delimiter: str = ","
57
58 def _serialize(self, value, attr, obj, **kwargs):
59 # serializing will start with parent-class serialization, so that we correctly
60 # output lists of non-primitive types, e.g. DelimitedList(DateTime)
61 return self.delimiter.join(
62 format(each) for each in super()._serialize(value, attr, obj, **kwargs)
63 )
64
65 def _deserialize(self, value, attr, data, **kwargs):
66 # attempting to deserialize from a non-string source is an error
67 if not isinstance(value, (str, bytes)):
68 raise self.make_error("invalid")
69 return super()._deserialize(value.split(self.delimiter), attr, data, **kwargs)
70
71
72 class DelimitedList(DelimitedFieldMixin, ma.fields.List):
73 """A field which is similar to a List, but takes its input as a delimited
74 string (e.g. "foo,bar,baz").
75
76 Like List, it can be given a nested field type which it will use to
77 de/serialize each element of the list.
4978
5079 :param Field cls_or_instance: A field class or instance.
5180 :param str delimiter: Delimiter between values.
52 :param bool as_string: Dump values to string.
5381 """
5482
55 delimiter = ","
83 default_error_messages = {"invalid": "Not a valid delimited list."}
5684
57 def __init__(self, cls_or_instance, delimiter=None, as_string=False, **kwargs):
85 def __init__(
86 self,
87 cls_or_instance: typing.Union[ma.fields.Field, type],
88 *,
89 delimiter: typing.Optional[str] = None,
90 **kwargs
91 ):
5892 self.delimiter = delimiter or self.delimiter
59 self.as_string = as_string
60 super(DelimitedList, self).__init__(cls_or_instance, **kwargs)
93 super().__init__(cls_or_instance, **kwargs)
6194
62 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
6795
68 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:
76 if MARSHMALLOW_VERSION_INFO[0] < 3:
77 self.fail("invalid")
78 else:
79 raise self.make_error("invalid")
80 return super(DelimitedList, self)._deserialize(ret, attr, data, **kwargs)
96 class DelimitedTuple(DelimitedFieldMixin, ma.fields.Tuple):
97 """A field which is similar to a Tuple, but takes its input as a delimited
98 string (e.g. "foo,bar,baz").
99
100 Like Tuple, it can be given a tuple of nested field types which it will use to
101 de/serialize each element of the tuple.
102
103 :param Iterable[Field] tuple_fields: An iterable of field classes or instances.
104 :param str delimiter: Delimiter between values.
105 """
106
107 default_error_messages = {"invalid": "Not a valid delimited tuple."}
108
109 def __init__(
110 self, tuple_fields, *, delimiter: typing.Optional[str] = None, **kwargs
111 ):
112 self.delimiter = delimiter or self.delimiter
113 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
25 import marshmallow as ma
26
2427 from webargs import core
25 from webargs.core import json
28 from webargs.multidictproxy import MultiDictProxy
2629
2730
2831 def abort(http_status_code, exc=None, **kwargs):
4649 class FlaskParser(core.Parser):
4750 """Flask request argument parser."""
4851
52 DEFAULT_UNKNOWN_BY_LOCATION = {
53 "view_args": ma.RAISE,
54 "path": ma.RAISE,
55 **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION,
56 }
4957 __location_map__ = dict(
50 view_args="parse_view_args",
51 path="parse_view_args",
52 **core.Parser.__location_map__
58 view_args="load_view_args",
59 path="load_view_args",
60 **core.Parser.__location_map__,
5361 )
5462
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)
63 def _raw_load_json(self, req):
64 """Return a json payload from the request for the core parser's load_json
5865
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
66 Checks the input mimetype and may return 'missing' if the mimetype is
67 non-json, even if the request body is parseable as json."""
68 if not is_json_request(req):
69 return core.missing
6570
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)
71 return core.parse_json(req.get_data(cache=True))
7872
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)
73 def _handle_invalid_json_error(self, error, req, *args, **kwargs):
74 abort(400, exc=error, messages={"json": ["Invalid JSON body."]})
8275
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
76 def load_view_args(self, req, schema):
77 """Return the request's ``view_args`` or ``missing`` if there are none."""
78 return req.view_args or core.missing
9079
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)
80 def load_querystring(self, req, schema):
81 """Return query params from the request as a MultiDictProxy."""
82 return MultiDictProxy(req.args, schema)
9483
95 def parse_cookies(self, req, name, field):
96 """Pull a value from the cookiejar."""
97 return core.get_value(req.cookies, name, field)
84 def load_form(self, req, schema):
85 """Return form values from the request as a MultiDictProxy."""
86 return MultiDictProxy(req.form, schema)
9887
99 def parse_files(self, req, name, field):
100 """Pull a file from the request."""
101 return core.get_value(req.files, name, field)
88 def load_headers(self, req, schema):
89 """Return headers from the request as a MultiDictProxy."""
90 return MultiDictProxy(req.headers, schema)
10291
103 def handle_error(self, error, req, schema, error_status_code, error_headers):
92 def load_cookies(self, req, schema):
93 """Return cookies from the request."""
94 return req.cookies
95
96 def load_files(self, req, schema):
97 """Return files from the request as a MultiDictProxy."""
98 return MultiDictProxy(req.files, schema)
99
100 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
104101 """Handles errors during parsing. Aborts the current HTTP request and
105102 responds with a 422 error.
106103 """
113110 headers=error_headers,
114111 )
115112
116 def handle_invalid_json_error(self, error, req, *args, **kwargs):
117 abort(400, exc=error, messages={"json": ["Invalid JSON body."]})
118
119113 def get_default_request(self):
120 """Override to use Flask's thread-local request objec by default"""
114 """Override to use Flask's thread-local request object by default"""
121115 return flask.request
122116
123117
0 from collections.abc import Mapping
1
2 import marshmallow as ma
3
4 from webargs.core import missing, is_multiple
5
6
7 class MultiDictProxy(Mapping):
8 """
9 A proxy object which wraps multidict types along with a matching schema
10 Whenever a value is looked up, it is checked against the schema to see if
11 there is a matching field where `is_multiple` is True. If there is, then
12 the data should be loaded as a list or tuple.
13
14 In all other cases, __getitem__ proxies directly to the input multidict.
15 """
16
17 def __init__(self, multidict, schema: ma.Schema):
18 self.data = multidict
19 self.multiple_keys = self._collect_multiple_keys(schema)
20
21 @staticmethod
22 def _collect_multiple_keys(schema: ma.Schema):
23 result = set()
24 for name, field in schema.fields.items():
25 if not is_multiple(field):
26 continue
27 result.add(field.data_key if field.data_key is not None else name)
28 return result
29
30 def __getitem__(self, key):
31 val = self.data.get(key, missing)
32 if val is missing or key not in self.multiple_keys:
33 return val
34 if hasattr(self.data, "getlist"):
35 return self.data.getlist(key)
36 if hasattr(self.data, "getall"):
37 return self.data.getall(key)
38 if isinstance(val, (list, tuple)):
39 return val
40 if val is None:
41 return None
42 return [val]
43
44 def __str__(self): # str(proxy) proxies to str(proxy.data)
45 return str(self.data)
46
47 def __repr__(self):
48 return "MultiDictProxy(data={!r}, multiple_keys={!r})".format(
49 self.data, self.multiple_keys
50 )
51
52 def __delitem__(self, key):
53 del self.data[key]
54
55 def __setitem__(self, key, value):
56 self.data[key] = value
57
58 def __getattr__(self, name):
59 return getattr(self.data, name)
60
61 def __iter__(self):
62 for x in iter(self.data):
63 # special case for header dicts which produce an iterator of tuples
64 # instead of an iterator of strings
65 if isinstance(x, tuple):
66 yield x[0]
67 else:
68 yield x
69
70 def __contains__(self, x):
71 return x in self.data
72
73 def __len__(self):
74 return len(self.data)
75
76 def __eq__(self, other):
77 return self.data == other
78
79 def __ne__(self, other):
80 return self.data != other
(New empty file)
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
32 import marshmallow as ma
33
3334 from webargs import core
3435 from webargs.core import json
35 from webargs.compat import text_type
36 from webargs.multidictproxy import MultiDictProxy
37
38
39 def is_json_request(req):
40 return core.is_json(req.headers.get("content-type"))
3641
3742
3843 class PyramidParser(core.Parser):
3944 """Pyramid request argument parser."""
4045
46 DEFAULT_UNKNOWN_BY_LOCATION = {
47 "matchdict": ma.RAISE,
48 "path": ma.RAISE,
49 **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION,
50 }
4151 __location_map__ = dict(
42 matchdict="parse_matchdict",
43 path="parse_matchdict",
44 **core.Parser.__location_map__
52 matchdict="load_matchdict",
53 path="load_matchdict",
54 **core.Parser.__location_map__,
4555 )
4656
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)
57 def _raw_load_json(self, req):
58 """Return a json payload from the request for the core parser's load_json
5059
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)
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
5464
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
65 return core.parse_json(req.body, encoding=req.charset)
6166
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)
67 def load_querystring(self, req, schema):
68 """Return query params from the request as a MultiDictProxy."""
69 return MultiDictProxy(req.GET, schema)
7270
73 def parse_cookies(self, req, name, field):
74 """Pull the value from the cookiejar."""
75 return core.get_value(req.cookies, name, field)
71 def load_form(self, req, schema):
72 """Return form values from the request as a MultiDictProxy."""
73 return MultiDictProxy(req.POST, schema)
7674
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)
75 def load_cookies(self, req, schema):
76 """Return cookies from the request as a MultiDictProxy."""
77 return MultiDictProxy(req.cookies, schema)
8078
81 def parse_files(self, req, name, field):
82 """Pull a file from the request."""
79 def load_headers(self, req, schema):
80 """Return headers from the request as a MultiDictProxy."""
81 return MultiDictProxy(req.headers, schema)
82
83 def load_files(self, req, schema):
84 """Return files from the request as a MultiDictProxy."""
8385 files = ((k, v) for k, v in req.POST.items() if hasattr(v, "file"))
84 return core.get_value(MultiDict(files), name, field)
86 return MultiDictProxy(MultiDict(files), schema)
8587
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)
88 def load_matchdict(self, req, schema):
89 """Return the request's ``matchdict`` as a MultiDictProxy."""
90 return MultiDictProxy(req.matchdict, schema)
8991
90 def handle_error(self, error, req, schema, error_status_code, error_headers):
92 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
9193 """Handles errors during parsing. Aborts the current HTTP request and
9294 responds with a 400 error.
9395 """
9496 status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS
9597 response = exception_response(
9698 status_code,
97 detail=text_type(error),
99 detail=str(error),
98100 headers=error_headers,
99101 content_type="application/json",
100102 )
101103 body = json.dumps(error.messages)
102 response.body = body.encode("utf-8") if isinstance(body, text_type) else body
104 response.body = body.encode("utf-8") if isinstance(body, str) else body
103105 raise response
104106
105 def handle_invalid_json_error(self, error, req, *args, **kwargs):
107 def _handle_invalid_json_error(self, error, req, *args, **kwargs):
106108 messages = {"json": ["Invalid JSON body."]}
107109 response = exception_response(
108 400, detail=text_type(messages), content_type="application/json"
110 400, detail=str(messages), content_type="application/json"
109111 )
110112 body = json.dumps(messages)
111 response.body = body.encode("utf-8") if isinstance(body, text_type) else body
113 response.body = body.encode("utf-8") if isinstance(body, str) else body
112114 raise response
113115
114116 def use_args(
115117 self,
116118 argmap,
117119 req=None,
118 locations=core.Parser.DEFAULT_LOCATIONS,
120 *,
121 location=core.Parser.DEFAULT_LOCATION,
122 unknown=None,
119123 as_kwargs=False,
120124 validate=None,
121125 error_status_code=None,
122 error_headers=None,
126 error_headers=None
123127 ):
124128 """Decorator that injects parsed arguments into a view callable.
125129 Supports the *Class-based View* pattern where `request` is saved as an instance
129133 of argname -> `marshmallow.fields.Field` pairs, or a callable
130134 which accepts a request and returns a `marshmallow.Schema`.
131135 :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.
136 :param str location: Where on the request to load values.
137 :param str unknown: A value to pass for ``unknown`` when calling the
138 schema's ``load`` method.
133139 :param bool as_kwargs: Whether to insert arguments as keyword arguments.
134140 :param callable validate: Validation function that receives the dictionary
135141 of parsed arguments. If the function returns ``False``, the parser
139145 :param dict error_headers: Headers passed to error handler functions when a
140146 a `ValidationError` is raised.
141147 """
142 locations = locations or self.locations
148 location = location or self.location
143149 # Optimization: If argmap is passed as a dictionary, we only need
144150 # to generate a Schema once
145 if isinstance(argmap, collections.Mapping):
146 argmap = core.dict2schema(argmap, self.schema_class)()
151 if isinstance(argmap, Mapping):
152 argmap = self.schema_class.from_dict(argmap)()
147153
148154 def decorator(func):
149155 @functools.wraps(func)
157163 parsed_args = self.parse(
158164 argmap,
159165 req=request,
160 locations=locations,
166 location=location,
167 unknown=unknown,
161168 validate=validate,
162169 error_status_code=error_status_code,
163170 error_headers=error_headers,
164171 )
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)
172 args, kwargs = self._update_args_kwargs(
173 args, kwargs, parsed_args, as_kwargs
174 )
175 return func(obj, *args, **kwargs)
170176
171177 wrapper.__wrapped__ = func
172178 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
54 def test_parse_json_default(self, testapp):
55 assert testapp.post_json("/echo", {}).json == {"name": "World"}
56
5764 def test_parse_json_with_charset(self, testapp):
5865 res = testapp.post(
59 "/echo",
66 "/echo_json",
6067 json.dumps({"name": "Steve"}),
6168 content_type="application/json;charset=UTF-8",
6269 )
6471
6572 def test_parse_json_with_vendor_media_type(self, testapp):
6673 res = testapp.post(
67 "/echo",
74 "/echo_json",
6875 json.dumps({"name": "Steve"}),
6976 content_type="application/vnd.api+json;charset=UTF-8",
7077 )
7178 assert res.json == {"name": "Steve"}
7279
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"}
80 def test_parse_ignore_extra_data(self, testapp):
81 assert testapp.post_json(
82 "/echo_ignoring_extra_data", {"extra": "data"}
83 ).json == {"name": "World"}
84
85 def test_parse_json_empty(self, testapp):
86 assert testapp.post_json("/echo_json", {}).json == {"name": "World"}
87
88 def test_parse_json_error_unexpected_int(self, testapp):
89 res = testapp.post_json("/echo_json", 1, expect_errors=True)
90 assert res.status_code == 422
91
92 def test_parse_json_error_unexpected_list(self, testapp):
93 res = testapp.post_json("/echo_json", [{"extra": "data"}], expect_errors=True)
94 assert res.status_code == 422
8495
8596 def test_parse_json_many_schema_invalid_input(self, testapp):
8697 res = testapp.post_json(
92103 res = testapp.post_json("/echo_many_schema", [{"name": "Steve"}]).json
93104 assert res == [{"name": "Steve"}]
94105
95 def test_parse_json_many_schema_ignore_malformed_data(self, testapp):
96 assert testapp.post_json("/echo_many_schema", {"extra": "data"}).json == []
106 def test_parse_json_many_schema_error_malformed_data(self, testapp):
107 res = testapp.post_json(
108 "/echo_many_schema", {"extra": "data"}, expect_errors=True
109 )
110 assert res.status_code == 422
97111
98112 def test_parsing_form_default(self, testapp):
99 assert testapp.post("/echo", {}).json == {"name": "World"}
113 assert testapp.post("/echo_form", {}).json == {"name": "World"}
100114
101115 def test_parse_querystring_multiple(self, testapp):
102116 expected = {"name": ["steve", "Loria"]}
103117 assert testapp.get("/echo_multi?name=steve&name=Loria").json == expected
104118
119 # test that passing a single value parses correctly
120 # on parsers like falconparser, where there is no native MultiDict type,
121 # this verifies the usage of MultiDictProxy to ensure that single values
122 # are "listified"
123 def test_parse_querystring_multiple_single_value(self, testapp):
124 expected = {"name": ["steve"]}
125 assert testapp.get("/echo_multi?name=steve").json == expected
126
105127 def test_parse_form_multiple(self, testapp):
106128 expected = {"name": ["steve", "Loria"]}
107129 assert (
108 testapp.post("/echo_multi", {"name": ["steve", "Loria"]}).json == expected
130 testapp.post("/echo_multi_form", {"name": ["steve", "Loria"]}).json
131 == expected
109132 )
110133
111134 def test_parse_json_list(self, testapp):
112135 expected = {"name": ["Steve"]}
113 assert testapp.post_json("/echo_multi", {"name": "Steve"}).json == expected
136 assert (
137 testapp.post_json("/echo_multi_json", {"name": ["Steve"]}).json == expected
138 )
139
140 def test_parse_json_list_error_malformed_data(self, testapp):
141 res = testapp.post_json(
142 "/echo_multi_json", {"name": "Steve"}, expect_errors=True
143 )
144 assert res.status_code == 422
114145
115146 def test_parse_json_with_nonascii_chars(self, testapp):
116 text = u"øˆƒ£ºº∆ƒˆ∆"
117 assert testapp.post_json("/echo", {"name": text}).json == {"name": text}
147 text = "øˆƒ£ºº∆ƒˆ∆"
148 assert testapp.post_json("/echo_json", {"name": text}).json == {"name": text}
118149
119150 # https://github.com/marshmallow-code/webargs/issues/427
120151 def test_parse_json_with_nonutf8_chars(self, testapp):
121152 res = testapp.post(
122 "/echo",
153 "/echo_json",
123154 b"\xfe",
124155 headers={"Accept": "application/json", "Content-Type": "application/json"},
125156 expect_errors=True,
129160 assert res.json == {"json": ["Invalid JSON body."]}
130161
131162 def test_validation_error_returns_422_response(self, testapp):
132 res = testapp.post("/echo", {"name": "b"}, expect_errors=True)
163 res = testapp.post_json("/echo_json", {"name": "b"}, expect_errors=True)
133164 assert res.status_code == 422
134165
135166 def test_user_validation_error_returns_422_response_by_default(self, testapp):
186217 res = testapp.post_json("/echo_nested_many", in_data)
187218 assert res.json == {}
188219
189 def test_parse_json_if_no_json(self, testapp):
190 res = testapp.post("/echo")
191 assert res.json == {"name": "World"}
192
193220 def test_parse_files(self, testapp):
194221 res = testapp.post(
195222 "/echo_file", {"myfile": webtest.Upload("README.rst", b"data")}
198225
199226 # https://github.com/sloria/webargs/pull/297
200227 def test_empty_json(self, testapp):
201 res = testapp.post(
202 "/echo",
228 res = testapp.post("/echo_json")
229 assert res.status_code == 200
230 assert res.json == {"name": "World"}
231
232 # https://github.com/sloria/webargs/pull/297
233 def test_empty_json_with_headers(self, testapp):
234 res = testapp.post(
235 "/echo_json",
203236 "",
204237 headers={"Accept": "application/json", "Content-Type": "application/json"},
205238 )
209242 # https://github.com/sloria/webargs/issues/329
210243 def test_invalid_json(self, testapp):
211244 res = testapp.post(
212 "/echo",
245 "/echo_json",
213246 '{"foo": "bar", }',
214247 headers={"Accept": "application/json", "Content-Type": "application/json"},
215248 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
-83
src/webargs/webapp2parser.py less more
0 # -*- coding: utf-8 -*-
1 """Webapp2 request argument parsing module.
2
3 Example: ::
4
5 import webapp2
6
7 from marshmallow import fields
8 from webargs.webobparser import use_args
9
10 hello_args = {
11 'name': fields.Str(missing='World')
12 }
13
14 class MainPage(webapp2.RequestHandler):
15
16 @use_args(hello_args)
17 def get_args(self, args):
18 self.response.write('Hello, {name}!'.format(name=args['name']))
19
20 @use_kwargs(hello_args)
21 def get_kwargs(self, name=None):
22 self.response.write('Hello, {name}!'.format(name=name))
23
24 app = webapp2.WSGIApplication([
25 webapp2.Route(r'/hello', MainPage, handler_method='get_args'),
26 webapp2.Route(r'/hello_dict', MainPage, handler_method='get_kwargs'),
27 ], debug=True)
28 """
29 import webapp2
30 import webob.multidict
31
32 from webargs import core
33 from webargs.core import json
34
35
36 class Webapp2Parser(core.Parser):
37 """webapp2 request argument parser."""
38
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
45
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)
54
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)
58
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)
62
63 def parse_cookies(self, req, name, field):
64 """Pull the value from the cookiejar."""
65 return core.get_value(req.cookies, name, field)
66
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."""
73 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)
75
76 def get_default_request(self):
77 return webapp2.get_request()
78
79
80 parser = Webapp2Parser()
81 use_args = parser.use_args
82 use_kwargs = parser.use_kwargs
0 # -*- coding: utf-8 -*-
0 import asyncio
1
20 import aiohttp
31 from aiohttp.web import json_response
4 from aiohttp import web
52 import marshmallow as ma
63
74 from webargs import fields
85 from webargs.aiohttpparser import parser, use_args, use_kwargs
9 from webargs.core import MARSHMALLOW_VERSION_INFO, json
6 from webargs.core import json
107
118 hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)}
129 hello_multiple = {"name": fields.List(fields.Str())}
1512 class HelloSchema(ma.Schema):
1613 name = fields.Str(missing="World", validate=lambda n: len(n) >= 3)
1714
18 if MARSHMALLOW_VERSION_INFO[0] < 3:
19
20 class Meta:
21 strict = True
22
23
24 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
25 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
15
16 hello_many_schema = HelloSchema(many=True)
17
18 # variant which ignores unknown fields
19 hello_exclude_schema = HelloSchema(unknown=ma.EXCLUDE)
20
2621
2722 ##### Handlers #####
2823
2924
3025 async def echo(request):
26 parsed = await parser.parse(hello_args, request, location="query")
27 return json_response(parsed)
28
29
30 async def echo_form(request):
31 parsed = await parser.parse(hello_args, request, location="form")
32 return json_response(parsed)
33
34
35 async def echo_json(request):
3136 try:
32 parsed = await parser.parse(hello_args, request)
37 parsed = await parser.parse(hello_args, request, location="json")
3338 except json.JSONDecodeError:
34 raise web.HTTPBadRequest(
39 raise aiohttp.web.HTTPBadRequest(
3540 body=json.dumps(["Invalid JSON."]).encode("utf-8"),
3641 content_type="application/json",
3742 )
3843 return json_response(parsed)
3944
4045
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)
46 async def echo_json_or_form(request):
47 try:
48 parsed = await parser.parse(hello_args, request, location="json_or_form")
49 except json.JSONDecodeError:
50 raise aiohttp.web.HTTPBadRequest(
51 body=json.dumps(["Invalid JSON."]).encode("utf-8"),
52 content_type="application/json",
53 )
54 return json_response(parsed)
55
56
57 @use_args(hello_args, location="query")
5758 async def echo_use_args(request, args):
5859 return json_response(args)
5960
6061
61 @use_kwargs(hello_args)
62 @use_kwargs(hello_args, location="query")
6263 async def echo_use_kwargs(request, name):
6364 return json_response({"name": name})
6465
6566
66 @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42)
67 @use_args(
68 {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form"
69 )
6770 async def echo_use_args_validated(request, args):
6871 return json_response(args)
6972
7073
74 async def echo_ignoring_extra_data(request):
75 return json_response(
76 await parser.parse(hello_exclude_schema, request, unknown=None)
77 )
78
79
7180 async def echo_multi(request):
81 parsed = await parser.parse(hello_multiple, request, location="query")
82 return json_response(parsed)
83
84
85 async def echo_multi_form(request):
86 parsed = await parser.parse(hello_multiple, request, location="form")
87 return json_response(parsed)
88
89
90 async def echo_multi_json(request):
7291 parsed = await parser.parse(hello_multiple, request)
7392 return json_response(parsed)
7493
7594
7695 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()})
96 parsed = await parser.parse(hello_many_schema, request)
97 return json_response(parsed)
98
99
100 @use_args({"value": fields.Int()}, location="query")
82101 async def echo_use_args_with_path_param(request, args):
83102 return json_response(args)
84103
85104
86 @use_kwargs({"value": fields.Int()})
105 @use_kwargs({"value": fields.Int()}, location="query")
87106 async def echo_use_kwargs_with_path_param(request, value):
88107 return json_response({"value": value})
89108
90109
91 @use_args({"page": fields.Int(), "q": fields.Int()}, locations=("query",))
92 @use_args({"name": fields.Str()}, locations=("json",))
110 @use_args({"page": fields.Int(), "q": fields.Int()}, location="query")
111 @use_args({"name": fields.Str()})
93112 async def echo_use_args_multiple(request, query_parsed, json_parsed):
94113 return json_response({"query_parsed": query_parsed, "json_parsed": json_parsed})
95114
104123
105124
106125 async def echo_headers(request):
107 parsed = await parser.parse(hello_args, request, locations=("headers",))
126 parsed = await parser.parse(hello_args, request, location="headers")
108127 return json_response(parsed)
109128
110129
111130 async def echo_cookie(request):
112 parsed = await parser.parse(hello_args, request, locations=("cookies",))
131 parsed = await parser.parse(hello_args, request, location="cookies")
113132 return json_response(parsed)
114133
115134
134153
135154
136155 async def echo_nested_many_data_key(request):
137 data_key_kwarg = {
138 "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "X-Field"
156 args = {
157 "x_field": fields.Nested({"id": fields.Int()}, many=True, data_key="X-Field")
139158 }
140 args = {"x_field": fields.Nested({"id": fields.Int()}, many=True, **data_key_kwarg)}
141159 parsed = await parser.parse(args, request)
142160 return json_response(parsed)
143161
144162
145163 async def echo_match_info(request):
146 parsed = await parser.parse({"mymatch": fields.Int(location="match_info")}, request)
164 parsed = await parser.parse(
165 {"mymatch": fields.Int()}, request, location="match_info"
166 )
147167 return json_response(parsed)
148168
149169
150170 class EchoHandler:
151 @use_args(hello_args)
171 @use_args(hello_args, location="query")
152172 async def get(self, request, args):
153173 return json_response(args)
154174
155175
156 class EchoHandlerView(web.View):
157 @asyncio.coroutine
158 @use_args(hello_args)
159 def get(self, args):
176 class EchoHandlerView(aiohttp.web.View):
177 @use_args(hello_args, location="query")
178 async def get(self, args):
160179 return json_response(args)
161180
162181
163 @asyncio.coroutine
164 @use_args(HelloSchema, as_kwargs=True)
165 def echo_use_schema_as_kwargs(request, name):
182 @use_args(HelloSchema, as_kwargs=True, location="query")
183 async def echo_use_schema_as_kwargs(request, name):
166184 return json_response({"name": name})
167185
168186
177195 def create_app():
178196 app = aiohttp.web.Application()
179197
180 add_route(app, ["GET", "POST"], "/echo", echo)
181 add_route(app, ["GET"], "/echo_query", echo_query)
198 add_route(app, ["GET"], "/echo", echo)
199 add_route(app, ["POST"], "/echo_form", echo_form)
182200 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)
201 add_route(app, ["POST"], "/echo_json_or_form", echo_json_or_form)
202 add_route(app, ["GET"], "/echo_use_args", echo_use_args)
203 add_route(app, ["GET"], "/echo_use_kwargs", echo_use_kwargs)
204 add_route(app, ["POST"], "/echo_use_args_validated", echo_use_args_validated)
205 add_route(app, ["POST"], "/echo_ignoring_extra_data", echo_ignoring_extra_data)
206 add_route(app, ["GET"], "/echo_multi", echo_multi)
207 add_route(app, ["POST"], "/echo_multi_form", echo_multi_form)
208 add_route(app, ["POST"], "/echo_multi_json", echo_multi_json)
188209 add_route(app, ["GET", "POST"], "/echo_many_schema", echo_many_schema)
189210 add_route(
190211 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
6
77
88 hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)}
99 hello_multiple = {"name": fields.List(fields.Str())}
1313 name = fields.Str(missing="World", validate=lambda n: len(n) >= 3)
1414
1515
16 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
17 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
16 hello_many_schema = HelloSchema(many=True)
17
18 # variant which ignores unknown fields
19 hello_exclude_schema = HelloSchema(unknown=ma.EXCLUDE)
1820
1921
2022 app = Bottle()
2123 debug(True)
2224
2325
24 @app.route("/echo", method=["GET", "POST"])
26 @app.route("/echo", method=["GET"])
2527 def echo():
26 return parser.parse(hello_args, request)
28 return parser.parse(hello_args, request, location="query")
2729
2830
29 @app.route("/echo_query")
30 def echo_query():
31 return parser.parse(hello_args, request, locations=("query",))
31 @app.route("/echo_form", method=["POST"])
32 def echo_form():
33 return parser.parse(hello_args, location="form")
3234
3335
3436 @app.route("/echo_json", method=["POST"])
3537 def echo_json():
36 return parser.parse(hello_args, request, locations=("json",))
38 return parser.parse(hello_args, location="json")
3739
3840
39 @app.route("/echo_form", method=["POST"])
40 def echo_form():
41 return parser.parse(hello_args, request, locations=("form",))
41 @app.route("/echo_json_or_form", method=["POST"])
42 def echo_json_or_form():
43 return parser.parse(hello_args, location="json_or_form")
4244
4345
44 @app.route("/echo_use_args", method=["GET", "POST"])
45 @use_args(hello_args)
46 @app.route("/echo_use_args", method=["GET"])
47 @use_args(hello_args, location="query")
4648 def echo_use_args(args):
4749 return args
4850
4951
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
5552 @app.route(
5653 "/echo_use_args_validated",
57 method=["GET", "POST"],
58 apply=use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42),
54 method=["POST"],
55 apply=use_args(
56 {"value": fields.Int()},
57 validate=lambda args: args["value"] > 42,
58 location="form",
59 ),
5960 )
6061 def echo_use_args_validated(args):
6162 return args
6263
6364
64 @app.route("/echo_multi", method=["GET", "POST"])
65 def echo_multi():
66 return parser.parse(hello_multiple, request)
65 @app.route("/echo_ignoring_extra_data", method=["POST"])
66 def echo_json_ignore_extra_data():
67 return parser.parse(hello_exclude_schema, unknown=None)
6768
6869
69 @app.route("/echo_many_schema", method=["GET", "POST"])
70 @app.route(
71 "/echo_use_kwargs", method=["GET"], apply=use_kwargs(hello_args, location="query")
72 )
73 def echo_use_kwargs(name):
74 return {"name": name}
75
76
77 @app.route("/echo_multi", method=["GET"])
78 def echo_multi():
79 return parser.parse(hello_multiple, request, location="query")
80
81
82 @app.route("/echo_multi_form", method=["POST"])
83 def multi_form():
84 return parser.parse(hello_multiple, location="form")
85
86
87 @app.route("/echo_multi_json", method=["POST"])
88 def multi_json():
89 return parser.parse(hello_multiple)
90
91
92 @app.route("/echo_many_schema", method=["POST"])
7093 def echo_many_schema():
71 arguments = parser.parse(hello_many_schema, request, locations=("json",))
94 arguments = parser.parse(hello_many_schema, request)
7295 return HTTPResponse(body=json.dumps(arguments), content_type="application/json")
7396
7497
7598 @app.route(
76 "/echo_use_args_with_path_param/<name>", apply=use_args({"value": fields.Int()})
99 "/echo_use_args_with_path_param/<name>",
100 apply=use_args({"value": fields.Int()}, location="query"),
77101 )
78102 def echo_use_args_with_path_param(args, name):
79103 return args
80104
81105
82106 @app.route(
83 "/echo_use_kwargs_with_path_param/<name>", apply=use_kwargs({"value": fields.Int()})
107 "/echo_use_kwargs_with_path_param/<name>",
108 apply=use_kwargs({"value": fields.Int()}, location="query"),
84109 )
85110 def echo_use_kwargs_with_path_param(name, value):
86111 return {"value": value}
97122
98123 @app.route("/echo_headers")
99124 def echo_headers():
100 return parser.parse(hello_args, request, locations=("headers",))
125 return parser.parse(hello_args, request, location="headers")
101126
102127
103128 @app.route("/echo_cookie")
104129 def echo_cookie():
105 return parser.parse(hello_args, request, locations=("cookies",))
130 return parser.parse(hello_args, request, location="cookies")
106131
107132
108133 @app.route("/echo_file", method=["POST"])
109134 def echo_file():
110135 args = {"myfile": fields.Field()}
111 result = parser.parse(args, locations=("files",))
136 result = parser.parse(args, location="files")
112137 myfile = result["myfile"]
113138 content = myfile.file.read().decode("utf8")
114139 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
7
88
99 hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)}
1010 hello_multiple = {"name": fields.List(fields.Str())}
1414 name = fields.Str(missing="World", validate=lambda n: len(n) >= 3)
1515
1616
17 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
18 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
17 hello_many_schema = HelloSchema(many=True)
18
19 # variant which ignores unknown fields
20 hello_exclude_schema = HelloSchema(unknown=ma.EXCLUDE)
1921
2022
2123 def json_response(data, **kwargs):
2224 return HttpResponse(json.dumps(data), content_type="application/json", **kwargs)
2325
2426
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)
27 def handle_view_errors(f):
28 def wrapped(*args, **kwargs):
29 try:
30 return f(*args, **kwargs)
31 except ma.ValidationError as err:
32 return json_response(err.messages, status=422)
33 except json.JSONDecodeError:
34 return json_response({"json": ["Invalid JSON body."]}, status=400)
35
36 return wrapped
3337
3438
35 def echo_query(request):
36 return json_response(parser.parse(hello_args, request, locations=("query",)))
39 @handle_view_errors
40 def echo(request):
41 return json_response(parser.parse(hello_args, request, location="query"))
3742
3843
39 def echo_json(request):
40 return json_response(parser.parse(hello_args, request, locations=("json",)))
44 @handle_view_errors
45 def echo_form(request):
46 return json_response(parser.parse(hello_args, request, location="form"))
4147
4248
43 def echo_form(request):
44 return json_response(parser.parse(hello_args, request, locations=("form",)))
49 @handle_view_errors
50 def echo_json(request):
51 return json_response(parser.parse(hello_args, request, location="json"))
4552
4653
47 @use_args(hello_args)
54 @handle_view_errors
55 def echo_json_or_form(request):
56 return json_response(parser.parse(hello_args, request, location="json_or_form"))
57
58
59 @handle_view_errors
60 @use_args(hello_args, location="query")
4861 def echo_use_args(request, args):
4962 return json_response(args)
5063
5164
52 @use_kwargs(hello_args)
65 @handle_view_errors
66 @use_args(
67 {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form"
68 )
69 def echo_use_args_validated(args):
70 return json_response(args)
71
72
73 @handle_view_errors
74 def echo_ignoring_extra_data(request):
75 return json_response(parser.parse(hello_exclude_schema, request, unknown=None))
76
77
78 @handle_view_errors
79 @use_kwargs(hello_args, location="query")
5380 def echo_use_kwargs(request, name):
5481 return json_response({"name": name})
5582
5683
84 @handle_view_errors
5785 def echo_multi(request):
86 return json_response(parser.parse(hello_multiple, request, location="query"))
87
88
89 @handle_view_errors
90 def echo_multi_form(request):
91 return json_response(parser.parse(hello_multiple, request, location="form"))
92
93
94 @handle_view_errors
95 def echo_multi_json(request):
5896 return json_response(parser.parse(hello_multiple, request))
5997
6098
99 @handle_view_errors
61100 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)
101 return json_response(parser.parse(hello_many_schema, request))
68102
69103
70 @use_args({"value": fields.Int()})
104 @handle_view_errors
105 @use_args({"value": fields.Int()}, location="query")
71106 def echo_use_args_with_path_param(request, args, name):
72107 return json_response(args)
73108
74109
75 @use_kwargs({"value": fields.Int()})
110 @handle_view_errors
111 @use_kwargs({"value": fields.Int()}, location="query")
76112 def echo_use_kwargs_with_path_param(request, value, name):
77113 return json_response({"value": value})
78114
79115
116 @handle_view_errors
80117 def always_error(request):
81118 def always_fail(value):
82119 raise ma.ValidationError("something went wrong")
83120
84121 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)
122 return parser.parse(argmap, request)
89123
90124
125 @handle_view_errors
91126 def echo_headers(request):
92 return json_response(parser.parse(hello_args, request, locations=("headers",)))
127 return json_response(parser.parse(hello_args, request, location="headers"))
93128
94129
130 @handle_view_errors
95131 def echo_cookie(request):
96 return json_response(parser.parse(hello_args, request, locations=("cookies",)))
132 return json_response(parser.parse(hello_args, request, location="cookies"))
97133
98134
135 @handle_view_errors
99136 def echo_file(request):
100137 args = {"myfile": fields.Field()}
101 result = parser.parse(args, request, locations=("files",))
138 result = parser.parse(args, request, location="files")
102139 myfile = result["myfile"]
103140 content = myfile.read().decode("utf8")
104141 return json_response({"myfile": content})
105142
106143
144 @handle_view_errors
107145 def echo_nested(request):
108146 argmap = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})}
109147 return json_response(parser.parse(argmap, request))
110148
111149
150 @handle_view_errors
112151 def echo_nested_many(request):
113152 argmap = {
114153 "users": fields.Nested({"id": fields.Int(), "name": fields.Str()}, many=True)
117156
118157
119158 class EchoCBV(View):
159 @handle_view_errors
120160 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)
161 location_kwarg = {} if request.method == "POST" else {"location": "query"}
162 return json_response(parser.parse(hello_args, self.request, **location_kwarg))
126163
127164 post = get
128165
129166
130167 class EchoUseArgsCBV(View):
131 @use_args(hello_args)
168 @handle_view_errors
169 @use_args(hello_args, location="query")
132170 def get(self, request, args):
133171 return json_response(args)
134172
135 post = get
173 @handle_view_errors
174 @use_args(hello_args)
175 def post(self, request, args):
176 return json_response(args)
136177
137178
138179 class EchoUseArgsWithParamCBV(View):
139 @use_args(hello_args)
180 @handle_view_errors
181 @use_args(hello_args, location="query")
140182 def get(self, request, args, pid):
141183 return json_response(args)
142184
143 post = get
185 @handle_view_errors
186 @use_args(hello_args)
187 def post(self, request, args, pid):
188 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 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())}
1312 name = fields.Str(missing="World", validate=lambda n: len(n) >= 3)
1413
1514
16 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
17 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
15 hello_many_schema = HelloSchema(many=True)
16
17 # variant which ignores unknown fields
18 hello_exclude_schema = HelloSchema(unknown=ma.EXCLUDE)
1819
1920
20 class Echo(object):
21 class Echo:
2122 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
31
32
33 class EchoQuery(object):
34 def on_get(self, req, resp):
35 parsed = parser.parse(hello_args, req, locations=("query",))
23 parsed = parser.parse(hello_args, req, location="query")
3624 resp.body = json.dumps(parsed)
3725
3826
39 class EchoJSON(object):
27 class EchoForm:
4028 def on_post(self, req, resp):
41 parsed = parser.parse(hello_args, req, locations=("json",))
29 parsed = parser.parse(hello_args, req, location="form")
4230 resp.body = json.dumps(parsed)
4331
4432
45 class EchoForm(object):
33 class EchoJSON:
4634 def on_post(self, req, resp):
47 parsed = parser.parse(hello_args, req, locations=("form",))
35 parsed = parser.parse(hello_args, req, location="json")
4836 resp.body = json.dumps(parsed)
4937
5038
51 class EchoUseArgs(object):
52 @use_args(hello_args)
39 class EchoMedia:
40 def on_post(self, req, resp):
41 parsed = parser.parse(hello_args, req, location="media")
42 resp.body = json.dumps(parsed)
43
44
45 class EchoJSONOrForm:
46 def on_post(self, req, resp):
47 parsed = parser.parse(hello_args, req, location="json_or_form")
48 resp.body = json.dumps(parsed)
49
50
51 class EchoUseArgs:
52 @use_args(hello_args, location="query")
5353 def on_get(self, req, resp, args):
5454 resp.body = json.dumps(args)
5555
56 on_post = on_get
5756
58
59 class EchoUseKwargs(object):
60 @use_kwargs(hello_args)
57 class EchoUseKwargs:
58 @use_kwargs(hello_args, location="query")
6159 def on_get(self, req, resp, name):
6260 resp.body = json.dumps({"name": name})
6361
64 on_post = on_get
62
63 class EchoUseArgsValidated:
64 @use_args(
65 {"value": fields.Int()},
66 validate=lambda args: args["value"] > 42,
67 location="form",
68 )
69 def on_post(self, req, resp, args):
70 resp.body = json.dumps(args)
6571
6672
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
73 class EchoJSONIgnoreExtraData:
74 def on_post(self, req, resp):
75 resp.body = json.dumps(parser.parse(hello_exclude_schema, req, unknown=None))
7376
7477
75 class EchoMulti(object):
78 class EchoMulti:
7679 def on_get(self, req, resp):
80 resp.body = json.dumps(parser.parse(hello_multiple, req, location="query"))
81
82
83 class EchoMultiForm:
84 def on_post(self, req, resp):
85 resp.body = json.dumps(parser.parse(hello_multiple, req, location="form"))
86
87
88 class EchoMultiJSON:
89 def on_post(self, req, resp):
7790 resp.body = json.dumps(parser.parse(hello_multiple, req))
7891
79 on_post = on_get
92
93 class EchoManySchema:
94 def on_post(self, req, resp):
95 resp.body = json.dumps(parser.parse(hello_many_schema, req))
8096
8197
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()})
98 class EchoUseArgsWithPathParam:
99 @use_args({"value": fields.Int()}, location="query")
93100 def on_get(self, req, resp, args, name):
94101 resp.body = json.dumps(args)
95102
96103
97 class EchoUseKwargsWithPathParam(object):
98 @use_kwargs({"value": fields.Int()})
104 class EchoUseKwargsWithPathParam:
105 @use_kwargs({"value": fields.Int()}, location="query")
99106 def on_get(self, req, resp, value, name):
100107 resp.body = json.dumps({"value": value})
101108
102109
103 class AlwaysError(object):
110 class AlwaysError:
104111 def on_get(self, req, resp):
105112 def always_fail(value):
106113 raise ma.ValidationError("something went wrong")
111118 on_post = on_get
112119
113120
114 class EchoHeaders(object):
121 class EchoHeaders:
115122 def on_get(self, req, resp):
116 resp.body = json.dumps(parser.parse(hello_args, req, locations=("headers",)))
123 class HeaderSchema(ma.Schema):
124 NAME = fields.Str(missing="World")
125
126 resp.body = json.dumps(parser.parse(HeaderSchema(), req, location="headers"))
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_media", EchoMedia())
170 app.add_route("/echo_json_or_form", EchoJSONOrForm())
160171 app.add_route("/echo_use_args", EchoUseArgs())
161172 app.add_route("/echo_use_kwargs", EchoUseKwargs())
162173 app.add_route("/echo_use_args_validated", EchoUseArgsValidated())
174 app.add_route("/echo_ignoring_extra_data", EchoJSONIgnoreExtraData())
163175 app.add_route("/echo_multi", EchoMulti())
176 app.add_route("/echo_multi_form", EchoMultiForm())
177 app.add_route("/echo_multi_json", EchoMultiJSON())
164178 app.add_route("/echo_many_schema", EchoManySchema())
165179 app.add_route("/echo_use_args_with_path_param/{name}", EchoUseArgsWithPathParam())
166180 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
87
98
109 class TestAppConfig:
1918 name = fields.Str(missing="World", validate=lambda n: len(n) >= 3)
2019
2120
22 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
23 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
21 hello_many_schema = HelloSchema(many=True)
2422
2523 app = Flask(__name__)
2624 app.config.from_object(TestAppConfig)
2725
2826
29 @app.route("/echo", methods=["GET", "POST"])
27 @app.route("/echo", methods=["GET"])
3028 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",)))
29 return J(parser.parse(hello_args, location="query"))
30
31
32 @app.route("/echo_form", methods=["POST"])
33 def echo_form():
34 return J(parser.parse(hello_args, location="form"))
3735
3836
3937 @app.route("/echo_json", methods=["POST"])
4038 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)
39 return J(parser.parse(hello_args, location="json"))
40
41
42 @app.route("/echo_json_or_form", methods=["POST"])
43 def echo_json_or_form():
44 return J(parser.parse(hello_args, location="json_or_form"))
45
46
47 @app.route("/echo_use_args", methods=["GET"])
48 @use_args(hello_args, location="query")
5149 def echo_use_args(args):
5250 return J(args)
5351
5452
55 @app.route("/echo_use_args_validated", methods=["GET", "POST"])
56 @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42)
53 @app.route("/echo_use_args_validated", methods=["POST"])
54 @use_args(
55 {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form"
56 )
5757 def echo_use_args_validated(args):
5858 return J(args)
5959
6060
61 @app.route("/echo_use_kwargs", methods=["GET", "POST"])
62 @use_kwargs(hello_args)
61 @app.route("/echo_ignoring_extra_data", methods=["POST"])
62 def echo_json_ignore_extra_data():
63 return J(parser.parse(hello_args, unknown=ma.EXCLUDE))
64
65
66 @app.route("/echo_use_kwargs", methods=["GET"])
67 @use_kwargs(hello_args, location="query")
6368 def echo_use_kwargs(name):
6469 return J({"name": name})
6570
6671
67 @app.route("/echo_multi", methods=["GET", "POST"])
72 @app.route("/echo_multi", methods=["GET"])
6873 def multi():
74 return J(parser.parse(hello_multiple, location="query"))
75
76
77 @app.route("/echo_multi_form", methods=["POST"])
78 def multi_form():
79 return J(parser.parse(hello_multiple, location="form"))
80
81
82 @app.route("/echo_multi_json", methods=["POST"])
83 def multi_json():
6984 return J(parser.parse(hello_multiple))
7085
7186
7287 @app.route("/echo_many_schema", methods=["GET", "POST"])
7388 def many_nested():
74 arguments = parser.parse(hello_many_schema, locations=("json",))
89 arguments = parser.parse(hello_many_schema)
7590 return Response(json.dumps(arguments), content_type="application/json")
7691
7792
7893 @app.route("/echo_use_args_with_path_param/<name>")
79 @use_args({"value": fields.Int()})
94 @use_args({"value": fields.Int()}, location="query")
8095 def echo_use_args_with_path(args, name):
8196 return J(args)
8297
8398
8499 @app.route("/echo_use_kwargs_with_path_param/<name>")
85 @use_kwargs({"value": fields.Int()})
100 @use_kwargs({"value": fields.Int()}, location="query")
86101 def echo_use_kwargs_with_path(name, value):
87102 return J({"value": value})
88103
98113
99114 @app.route("/echo_headers")
100115 def echo_headers():
101 return J(parser.parse(hello_args, locations=("headers",)))
116 return J(parser.parse(hello_args, location="headers"))
117
118
119 # as above, but in this case, turn off the default `EXCLUDE` behavior for
120 # `headers`, so that errors will be raised
121 @app.route("/echo_headers_raising")
122 @use_args(HelloSchema(), location="headers", unknown=None)
123 def echo_headers_raising(args):
124 return J(args)
102125
103126
104127 @app.route("/echo_cookie")
105128 def echo_cookie():
106 return J(parser.parse(hello_args, request, locations=("cookies",)))
129 return J(parser.parse(hello_args, request, location="cookies"))
107130
108131
109132 @app.route("/echo_file", methods=["POST"])
110133 def echo_file():
111134 args = {"myfile": fields.Field()}
112 result = parser.parse(args, locations=("files",))
135 result = parser.parse(args, location="files")
113136 fp = result["myfile"]
114137 content = fp.read().decode("utf8")
115138 return J({"myfile": content})
117140
118141 @app.route("/echo_view_arg/<view_arg>")
119142 def echo_view_arg(view_arg):
120 return J(parser.parse({"view_arg": fields.Int()}, locations=("view_args",)))
143 return J(parser.parse({"view_arg": fields.Int()}, location="view_args"))
121144
122145
123146 @app.route("/echo_view_arg_use_args/<view_arg>")
124 @use_args({"view_arg": fields.Int(location="view_args")})
147 @use_args({"view_arg": fields.Int()}, location="view_args")
125148 def echo_view_arg_with_use_args(args, **kwargs):
126149 return J(args)
127150
142165
143166 @app.route("/echo_nested_many_data_key", methods=["POST"])
144167 def echo_nested_many_with_data_key():
145 data_key_kwarg = {
146 "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "X-Field"
168 args = {
169 "x_field": fields.Nested({"id": fields.Int()}, many=True, data_key="X-Field")
147170 }
148 args = {"x_field": fields.Nested({"id": fields.Int()}, many=True, **data_key_kwarg)}
149171 return J(parser.parse(args))
150172
151173
186208 def handle_error(err):
187209 if err.code == 422:
188210 assert isinstance(err.data["schema"], ma.Schema)
211
189212 return J(err.data["messages"]), 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
97
108 hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)}
119 hello_multiple = {"name": fields.List(fields.Str())}
1513 name = fields.Str(missing="World", validate=lambda n: len(n) >= 3)
1614
1715
18 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
19 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
16 hello_many_schema = HelloSchema(many=True)
17
18 # variant which ignores unknown fields
19 hello_exclude_schema = HelloSchema(unknown=ma.EXCLUDE)
2020
2121
2222 def echo(request):
23 return parser.parse(hello_args, request, location="query")
24
25
26 def echo_form(request):
27 return parser.parse(hello_args, request, location="form")
28
29
30 def echo_json(request):
2331 try:
24 return parser.parse(hello_args, request)
32 return parser.parse(hello_args, request, location="json")
33 except json.JSONDecodeError:
34 error = HTTPBadRequest()
35 error.body = json.dumps(["Invalid JSON."]).encode("utf-8")
36 error.content_type = "application/json"
37 raise error
38
39
40 def echo_json_or_form(request):
41 try:
42 return parser.parse(hello_args, request, location="json_or_form")
43 except json.JSONDecodeError:
44 error = HTTPBadRequest()
45 error.body = json.dumps(["Invalid JSON."]).encode("utf-8")
46 error.content_type = "application/json"
47 raise error
48
49
50 def echo_json_ignore_extra_data(request):
51 try:
52 return parser.parse(hello_exclude_schema, request, unknown=None)
2553 except json.JSONDecodeError:
2654 error = HTTPBadRequest()
2755 error.body = json.dumps(["Invalid JSON."]).encode("utf-8")
3058
3159
3260 def echo_query(request):
33 return parser.parse(hello_args, request, locations=("query",))
61 return parser.parse(hello_args, request, location="query")
3462
3563
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)
64 @use_args(hello_args, location="query")
4565 def echo_use_args(request, args):
4666 return args
4767
4868
49 @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42)
69 @use_args(
70 {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form"
71 )
5072 def echo_use_args_validated(request, args):
5173 return args
5274
5375
54 @use_kwargs(hello_args)
76 @use_kwargs(hello_args, location="query")
5577 def echo_use_kwargs(request, name):
5678 return {"name": name}
5779
5880
5981 def echo_multi(request):
82 return parser.parse(hello_multiple, request, location="query")
83
84
85 def echo_multi_form(request):
86 return parser.parse(hello_multiple, request, location="form")
87
88
89 def echo_multi_json(request):
6090 return parser.parse(hello_multiple, request)
6191
6292
6393 def echo_many_schema(request):
64 return parser.parse(hello_many_schema, request, locations=("json",))
94 return parser.parse(hello_many_schema, request)
6595
6696
67 @use_args({"value": fields.Int()})
97 @use_args({"value": fields.Int()}, location="query")
6898 def echo_use_args_with_path_param(request, args):
6999 return args
70100
71101
72 @use_kwargs({"value": fields.Int()})
102 @use_kwargs({"value": fields.Int()}, location="query")
73103 def echo_use_kwargs_with_path_param(request, value):
74104 return {"value": value}
75105
83113
84114
85115 def echo_headers(request):
86 return parser.parse(hello_args, request, locations=("headers",))
116 return parser.parse(hello_args, request, location="headers")
87117
88118
89119 def echo_cookie(request):
90 return parser.parse(hello_args, request, locations=("cookies",))
120 return parser.parse(hello_args, request, location="cookies")
91121
92122
93123 def echo_file(request):
94124 args = {"myfile": fields.Field()}
95 result = parser.parse(args, request, locations=("files",))
125 result = parser.parse(args, request, location="files")
96126 myfile = result["myfile"]
97127 content = myfile.file.read().decode("utf8")
98128 return {"myfile": content}
111141
112142
113143 def echo_matchdict(request):
114 return parser.parse({"mymatch": fields.Int()}, request, locations=("matchdict",))
144 return parser.parse({"mymatch": fields.Int()}, request, location="matchdict")
115145
116146
117 class EchoCallable(object):
147 class EchoCallable:
118148 def __init__(self, request):
119149 self.request = request
120150
121 @use_args({"value": fields.Int()})
151 @use_args({"value": fields.Int()}, location="query")
122152 def __call__(self, args):
123153 return args
124154
134164 config = Configurator()
135165
136166 add_route(config, "/echo", echo)
167 add_route(config, "/echo_form", echo_form)
168 add_route(config, "/echo_json", echo_json)
169 add_route(config, "/echo_json_or_form", echo_json_or_form)
137170 add_route(config, "/echo_query", echo_query)
138 add_route(config, "/echo_json", echo_json)
139 add_route(config, "/echo_form", echo_form)
171 add_route(config, "/echo_ignoring_extra_data", echo_json_ignore_extra_data)
140172 add_route(config, "/echo_use_args", echo_use_args)
141173 add_route(config, "/echo_use_args_validated", echo_use_args_validated)
142174 add_route(config, "/echo_use_kwargs", echo_use_kwargs)
143175 add_route(config, "/echo_multi", echo_multi)
176 add_route(config, "/echo_multi_form", echo_multi_form)
177 add_route(config, "/echo_multi_json", echo_multi_json)
144178 add_route(config, "/echo_many_schema", echo_many_schema)
145179 add_route(
146180 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 from io import BytesIO
1 from unittest import mock
2
3 import webtest
4 import webtest_aiohttp
5 import pytest
6
7 from webargs import fields
8 from webargs.aiohttpparser import AIOHTTPParser
9 from webargs.testing import CommonTestCase
10 from tests.apps.aiohttp_app import create_app
11
12
13 @pytest.fixture
14 def web_request():
15 req = mock.Mock()
16 req.query = {}
17 yield req
18 req.query = {}
19
20
21 class TestAIOHTTPParser(CommonTestCase):
22 def create_app(self):
23 return create_app()
24
25 def create_testapp(self, app, loop):
26 return webtest_aiohttp.TestApp(app, loop=loop)
27
28 @pytest.fixture
29 def testapp(self, loop):
30 return self.create_testapp(self.create_app(), loop)
31
32 @pytest.mark.skip(reason="files location not supported for aiohttpparser")
33 def test_parse_files(self, testapp):
34 pass
35
36 def test_parse_match_info(self, testapp):
37 assert testapp.get("/echo_match_info/42").json == {"mymatch": 42}
38
39 def test_use_args_on_method_handler(self, testapp):
40 assert testapp.get("/echo_method").json == {"name": "World"}
41 assert testapp.get("/echo_method?name=Steve").json == {"name": "Steve"}
42 assert testapp.get("/echo_method_view").json == {"name": "World"}
43 assert testapp.get("/echo_method_view?name=Steve").json == {"name": "Steve"}
44
45 # regression test for https://github.com/marshmallow-code/webargs/issues/165
46 def test_multiple_args(self, testapp):
47 res = testapp.post_json("/echo_multiple_args", {"first": "1", "last": "2"})
48 assert res.json == {"first": "1", "last": "2"}
49
50 # regression test for https://github.com/marshmallow-code/webargs/issues/145
51 def test_nested_many_with_data_key(self, testapp):
52 res = testapp.post_json("/echo_nested_many_data_key", {"X-Field": [{"id": 24}]})
53 assert res.json == {"x_field": [{"id": 24}]}
54
55 res = testapp.post_json("/echo_nested_many_data_key", {})
56 assert res.json == {}
57
58 def test_schema_as_kwargs_view(self, testapp):
59 assert testapp.get("/echo_use_schema_as_kwargs").json == {"name": "World"}
60 assert testapp.get("/echo_use_schema_as_kwargs?name=Chandler").json == {
61 "name": "Chandler"
62 }
63
64 # https://github.com/marshmallow-code/webargs/pull/297
65 def test_empty_json_body(self, testapp):
66 environ = {"CONTENT_TYPE": "application/json", "wsgi.input": BytesIO(b"")}
67 req = webtest.TestRequest.blank("/echo", environ)
68 resp = testapp.do_request(req)
69 assert resp.json == {"name": "World"}
70
71 def test_use_args_multiple(self, testapp):
72 res = testapp.post_json(
73 "/echo_use_args_multiple?page=2&q=10", {"name": "Steve"}
74 )
75 assert res.json == {
76 "query_parsed": {"page": 2, "q": 10},
77 "json_parsed": {"name": "Steve"},
78 }
79
80
81 async def test_aiohttpparser_synchronous_error_handler(web_request):
82 parser = AIOHTTPParser()
83
84 class CustomError(Exception):
85 pass
86
87 @parser.error_handler
88 def custom_handle_error(error, req, schema, *, error_status_code, error_headers):
89 raise CustomError("foo")
90
91 with pytest.raises(CustomError):
92 await parser.parse(
93 {"foo": fields.Int(required=True)}, web_request, location="query"
94 )
95
96
97 async def test_aiohttpparser_asynchronous_error_handler(web_request):
98 parser = AIOHTTPParser()
99
100 class CustomError(Exception):
101 pass
102
103 @parser.error_handler
104 async def custom_handle_error(
105 error, req, schema, *, error_status_code, error_headers
106 ):
107 async def inner():
108 raise CustomError("foo")
109
110 await inner()
111
112 with pytest.raises(CustomError):
113 await parser.parse(
114 {"foo": fields.Int(required=True)}, web_request, location="query"
115 )
0 # -*- coding: utf-8 -*-
1 import itertools
2 import mock
30 import datetime
1 from unittest import mock
42
53 import pytest
6 from marshmallow import Schema, post_load, pre_load, class_registry, validates_schema
4 from marshmallow import (
5 Schema,
6 post_load,
7 pre_load,
8 validates_schema,
9 EXCLUDE,
10 INCLUDE,
11 RAISE,
12 )
713 from werkzeug.datastructures import MultiDict as WerkMultiDict
814 from django.utils.datastructures import MultiValueDict as DjMultiDict
915 from bottle import MultiDict as BotMultiDict
1016
11 from webargs import fields, missing, ValidationError
17 from webargs import fields, ValidationError
1218 from webargs.core import (
1319 Parser,
14 get_value,
15 dict2schema,
1620 is_json,
1721 get_mimetype,
18 MARSHMALLOW_VERSION_INFO,
1922 )
20
21
22 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
23 from webargs.multidictproxy import MultiDictProxy
2324
2425
2526 class MockHTTPError(Exception):
2627 def __init__(self, status_code, headers):
2728 self.status_code = status_code
2829 self.headers = headers
29 super(MockHTTPError, self).__init__(self, "HTTP Error occurred")
30 super().__init__(self, "HTTP Error occurred")
3031
3132
3233 class MockRequestParser(Parser):
3334 """A minimal parser implementation that parses mock requests."""
3435
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)
36 def load_querystring(self, req, schema):
37 return MultiDictProxy(req.query, schema)
38
39 def load_json(self, req, schema):
40 return req.json
41
42 def load_cookies(self, req, schema):
43 return req.cookies
4344
4445
4546 @pytest.yield_fixture(scope="function")
5859 # Parser tests
5960
6061
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()
62 @mock.patch("webargs.core.Parser.load_json")
63 def test_load_json_called_by_parse_default(load_json, web_request):
64 schema = Schema.from_dict({"foo": fields.Field()})()
65 load_json.return_value = {"foo": 1}
6466 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
67 p.parse(schema, web_request)
68 load_json.assert_called_with(web_request, schema)
69
70
71 @pytest.mark.parametrize(
72 "location", ["querystring", "form", "headers", "cookies", "files"]
73 )
74 def test_load_nondefault_called_by_parse_with_location(location, web_request):
75 with mock.patch(
76 f"webargs.core.Parser.load_{location}"
77 ) as mock_loadfunc, mock.patch("webargs.core.Parser.load_json") as load_json:
78 mock_loadfunc.return_value = {}
79 load_json.return_value = {}
80 p = Parser()
81
82 # ensure that without location=..., the loader is not called (json is
83 # called)
84 p.parse({"foo": fields.Field()}, web_request)
85 assert mock_loadfunc.call_count == 0
86 assert load_json.call_count == 1
87
88 # but when location=... is given, the loader *is* called and json is
89 # not called
90 p.parse({"foo": fields.Field()}, web_request, location=location)
91 assert mock_loadfunc.call_count == 1
92 # it was already 1, should not go up
93 assert load_json.call_count == 1
94
95
96 def test_parse(parser, web_request):
97 web_request.json = {"username": 42, "password": 42}
11698 argmap = {"username": fields.Field(), "password": fields.Field()}
117 p = Parser()
118 ret = p.parse(argmap, web_request)
99 ret = parser.parse(argmap, web_request)
119100 assert {"username": 42, "password": 42} == ret
101
102
103 @pytest.mark.parametrize(
104 "set_location",
105 [
106 "schema_instance",
107 "parse_call",
108 "parser_default",
109 "parser_class_default",
110 ],
111 )
112 def test_parse_with_unknown_behavior_specified(parser, web_request, set_location):
113 web_request.json = {"username": 42, "password": 42, "fjords": 42}
114
115 class CustomSchema(Schema):
116 username = fields.Field()
117 password = fields.Field()
118
119 def parse_with_desired_behavior(value):
120 if set_location == "schema_instance":
121 if value is not None:
122 # pass 'unknown=None' to parse() in order to indicate that the
123 # schema setting should be respected
124 return parser.parse(
125 CustomSchema(unknown=value), web_request, unknown=None
126 )
127 else:
128 return parser.parse(CustomSchema(), web_request)
129 elif set_location == "parse_call":
130 return parser.parse(CustomSchema(), web_request, unknown=value)
131 elif set_location == "parser_default":
132 parser.unknown = value
133 return parser.parse(CustomSchema(), web_request)
134 elif set_location == "parser_class_default":
135
136 class CustomParser(MockRequestParser):
137 DEFAULT_UNKNOWN_BY_LOCATION = {"json": value}
138
139 return CustomParser().parse(CustomSchema(), web_request)
140 else:
141 raise NotImplementedError
142
143 # with no unknown setting or unknown=RAISE, it blows up
144 with pytest.raises(ValidationError, match="Unknown field."):
145 parse_with_desired_behavior(None)
146 with pytest.raises(ValidationError, match="Unknown field."):
147 parse_with_desired_behavior(RAISE)
148
149 # with unknown=EXCLUDE the data is omitted
150 ret = parse_with_desired_behavior(EXCLUDE)
151 assert {"username": 42, "password": 42} == ret
152 # with unknown=INCLUDE it is added even though it isn't part of the schema
153 ret = parse_with_desired_behavior(INCLUDE)
154 assert {"username": 42, "password": 42, "fjords": 42} == ret
155
156
157 def test_parse_with_explicit_unknown_overrides_schema(parser, web_request):
158 web_request.json = {"username": 42, "password": 42, "fjords": 42}
159
160 class CustomSchema(Schema):
161 username = fields.Field()
162 password = fields.Field()
163
164 # setting RAISE in the parse call overrides schema setting
165 with pytest.raises(ValidationError, match="Unknown field."):
166 parser.parse(CustomSchema(unknown=EXCLUDE), web_request, unknown=RAISE)
167 with pytest.raises(ValidationError, match="Unknown field."):
168 parser.parse(CustomSchema(unknown=INCLUDE), web_request, unknown=RAISE)
169
170 # and the reverse -- setting EXCLUDE or INCLUDE in the parse call overrides
171 # a schema with RAISE already set
172 ret = parser.parse(CustomSchema(unknown=RAISE), web_request, unknown=EXCLUDE)
173 assert {"username": 42, "password": 42} == ret
174 ret = parser.parse(CustomSchema(unknown=RAISE), web_request, unknown=INCLUDE)
175 assert {"username": 42, "password": 42, "fjords": 42} == ret
176
177
178 @pytest.mark.parametrize("clear_method", ["custom_class", "instance_setting", "both"])
179 def test_parse_with_default_unknown_cleared_uses_schema_value(
180 parser, web_request, clear_method
181 ):
182 web_request.json = {"username": 42, "password": 42, "fjords": 42}
183
184 class CustomSchema(Schema):
185 username = fields.Field()
186 password = fields.Field()
187
188 if clear_method == "custom_class":
189
190 class CustomParser(MockRequestParser):
191 DEFAULT_UNKNOWN_BY_LOCATION = {}
192
193 parser = CustomParser()
194 elif clear_method == "instance_setting":
195 parser = MockRequestParser(unknown=None)
196 elif clear_method == "both":
197 # setting things in multiple ways should not result in errors
198 class CustomParser(MockRequestParser):
199 DEFAULT_UNKNOWN_BY_LOCATION = {}
200
201 parser = CustomParser(unknown=None)
202 else:
203 raise NotImplementedError
204
205 with pytest.raises(ValidationError, match="Unknown field."):
206 parser.parse(CustomSchema(), web_request)
207 with pytest.raises(ValidationError, match="Unknown field."):
208 parser.parse(CustomSchema(unknown=RAISE), web_request)
209
210 ret = parser.parse(CustomSchema(unknown=EXCLUDE), web_request)
211 assert {"username": 42, "password": 42} == ret
212 ret = parser.parse(CustomSchema(unknown=INCLUDE), web_request)
213 assert {"username": 42, "password": 42, "fjords": 42} == ret
120214
121215
122216 def test_parse_required_arg_raises_validation_error(parser, web_request):
140234 assert result == {"first": "Steve", "last": None}
141235
142236
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
237 def test_parse_required_arg(parser, web_request):
238 web_request.json = {"foo": 42}
239 result = parser.parse({"foo": fields.Field(required=True)}, web_request)
240 assert result == {"foo": 42}
150241
151242
152243 def test_parse_required_list(parser, web_request):
154245 args = {"foo": fields.List(fields.Field(), required=True)}
155246 with pytest.raises(ValidationError) as excinfo:
156247 parser.parse(args, web_request)
157 assert excinfo.value.messages["foo"][0] == "Missing data for required field."
248 assert (
249 excinfo.value.messages["json"]["foo"][0] == "Missing data for required field."
250 )
158251
159252
160253 # Regression test for https://github.com/marshmallow-code/webargs/issues/107
169262 args = {"foo": fields.List(fields.Field(), allow_none=False)}
170263 with pytest.raises(ValidationError) as excinfo:
171264 parser.parse(args, web_request)
172 assert excinfo.value.messages["foo"][0] == "Field may not be null."
265 assert excinfo.value.messages["json"]["foo"][0] == "Field may not be null."
173266
174267
175268 def test_parse_empty_list(parser, web_request):
184277 assert parser.parse(args, web_request) == {}
185278
186279
187 def test_default_locations():
188 assert set(Parser.DEFAULT_LOCATIONS) == set(["json", "querystring", "form"])
280 def test_default_location():
281 assert Parser.DEFAULT_LOCATION == "json"
189282
190283
191284 def test_missing_with_default(parser, web_request):
192285 web_request.json = {}
193286 args = {"val": fields.Field(missing="pizza")}
194 result = parser.parse(args, web_request, locations=("json",))
287 result = parser.parse(args, web_request)
195288 assert result["val"] == "pizza"
196289
197290
198291 def test_default_can_be_none(parser, web_request):
199292 web_request.json = {}
200293 args = {"val": fields.Field(missing=None, allow_none=True)}
201 result = parser.parse(args, web_request, locations=("json",))
294 result = parser.parse(args, web_request)
202295 assert result["val"] is None
203296
204297
209302 "p": fields.Int(
210303 missing=1,
211304 validate=lambda p: p > 0,
212 error=u"La page demandée n'existe pas",
305 error="La page demandée n'existe pas",
213306 location="querystring",
214307 )
215308 }
216309 assert parser.parse(args, web_request) == {"p": 1}
217310
218311
219 def test_value_error_raised_if_parse_arg_called_with_invalid_location(web_request):
312 def test_value_error_raised_if_parse_called_with_invalid_location(parser, web_request):
220313 field = fields.Field()
314 with pytest.raises(ValueError, match="Invalid location argument: invalidlocation"):
315 parser.parse({"foo": field}, web_request, location="invalidlocation")
316
317
318 @mock.patch("webargs.core.Parser.handle_error")
319 def test_handle_error_called_when_parsing_raises_error(handle_error, web_request):
320 # handle_error must raise an error to be valid
321 handle_error.side_effect = ValidationError("parsing failed")
322
323 def always_fail(*args, **kwargs):
324 raise ValidationError("error occurred")
325
221326 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",))
327 assert handle_error.call_count == 0
328 with pytest.raises(ValidationError):
329 p.parse({"foo": fields.Field()}, web_request, validate=always_fail)
330 assert handle_error.call_count == 1
331 with pytest.raises(ValidationError):
332 p.parse({"foo": fields.Field()}, web_request, validate=always_fail)
247333 assert handle_error.call_count == 2
248334
249335
250336 def test_handle_error_reraises_errors(web_request):
251337 p = Parser()
252338 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",))
339 p.handle_error(
340 ValidationError("error raised"),
341 web_request,
342 Schema(),
343 error_status_code=422,
344 error_headers={},
345 )
346
347
348 @mock.patch("webargs.core.Parser.load_headers")
349 def test_location_as_init_argument(load_headers, web_request):
350 p = Parser(location="headers")
351 load_headers.return_value = {}
259352 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):
353 assert load_headers.called
354
355
356 def test_custom_error_handler(web_request):
272357 class CustomError(Exception):
273358 pass
274359
275 def error_handler(error, req, schema, status_code, headers):
360 def error_handler(error, req, schema, *, error_status_code, error_headers):
276361 assert isinstance(schema, Schema)
277362 raise CustomError(error)
278363
279 parse_json.side_effect = ValidationError("parse_json failed")
364 def failing_validate_func(args):
365 raise ValidationError("parsing failed")
366
367 class MySchema(Schema):
368 foo = fields.Int()
369
370 myschema = MySchema()
371 web_request.json = {"foo": "hello world"}
372
280373 p = Parser(error_handler=error_handler)
281374 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):
375 p.parse(myschema, web_request, validate=failing_validate_func)
376
377
378 def test_custom_error_handler_decorator(web_request):
287379 class CustomError(Exception):
288380 pass
289381
290 parse_json.side_effect = ValidationError("parse_json failed")
291
382 mock_schema = mock.Mock(spec=Schema)
383 mock_schema.strict = True
384 mock_schema.load.side_effect = ValidationError("parsing json failed")
292385 parser = Parser()
293386
294387 @parser.error_handler
295 def handle_error(error, req, schema, status_code, headers):
388 def handle_error(error, req, schema, *, error_status_code, error_headers):
296389 assert isinstance(schema, Schema)
297390 raise CustomError(error)
298391
299392 with pytest.raises(CustomError):
300 parser.parse({"foo": fields.Field()}, web_request)
301
302
303 def test_custom_location_handler(web_request):
393 parser.parse(mock_schema, web_request)
394
395
396 def test_custom_error_handler_must_reraise(web_request):
397 class CustomError(Exception):
398 pass
399
400 mock_schema = mock.Mock(spec=Schema)
401 mock_schema.strict = True
402 mock_schema.load.side_effect = ValidationError("parsing json failed")
403 parser = Parser()
404
405 @parser.error_handler
406 def handle_error(error, req, schema, *, error_status_code, error_headers):
407 pass
408
409 # because the handler above does not raise a new error, the parser should
410 # raise a ValueError -- indicating a programming error
411 with pytest.raises(ValueError):
412 parser.parse(mock_schema, web_request)
413
414
415 def test_custom_location_loader(web_request):
304416 web_request.data = {"foo": 42}
305417
306418 parser = Parser()
307419
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",))
420 @parser.location_loader("data")
421 def load_data(req, schema):
422 return req.data
423
424 result = parser.parse({"foo": fields.Int()}, web_request, location="data")
313425 assert result["foo"] == 42
314426
315427
316 def test_custom_location_handler_with_data_key(web_request):
428 def test_custom_location_loader_with_data_key(web_request):
317429 web_request.data = {"X-Foo": 42}
318430 parser = Parser()
319431
320 @parser.location_handler("data")
321 def parse_data(req, name, arg):
322 return req.data.get(name, missing)
323
324 data_key_kwarg = {
325 "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "X-Foo"
326 }
432 @parser.location_loader("data")
433 def load_data(req, schema):
434 return req.data
435
327436 result = parser.parse(
328 {"x_foo": fields.Int(**data_key_kwarg)}, web_request, locations=("data",)
437 {"x_foo": fields.Int(data_key="X-Foo")}, web_request, location="data"
329438 )
330439 assert result["x_foo"] == 42
331440
332441
333 def test_full_input_validation(web_request):
442 def test_full_input_validation(parser, web_request):
334443
335444 web_request.json = {"foo": 41, "bar": 42}
336445
337 parser = MockRequestParser()
338446 args = {"foo": fields.Int(), "bar": fields.Int()}
339447 with pytest.raises(ValidationError):
340448 # 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 )
449 parser.parse(args, web_request, validate=lambda args: args["foo"] > args["bar"])
347450
348451
349452 def test_full_input_validation_with_multiple_validators(web_request, parser):
359462 web_request.json = {"a": 2, "b": 1}
360463 validators = [validate1, validate2]
361464 with pytest.raises(ValidationError, match="b must be > a"):
362 parser.parse(args, web_request, locations=("json",), validate=validators)
465 parser.parse(args, web_request, validate=validators)
363466
364467 web_request.json = {"a": 1, "b": 2}
365468 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()
469 parser.parse(args, web_request, validate=validators)
470
471
472 def test_required_with_custom_error(parser, web_request):
473 web_request.json = {}
372474 args = {
373475 "foo": fields.Str(required=True, error_messages={"required": "We need foo"})
374476 }
375477 with pytest.raises(ValidationError) as excinfo:
376478 # 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"]
380 if MARSHMALLOW_VERSION_INFO[0] < 3:
381 assert "foo" in excinfo.value.field_names
382
383
384 def test_required_with_custom_error_and_validation_error(web_request):
479 parser.parse(args, web_request)
480
481 assert "We need foo" in excinfo.value.messages["json"]["foo"]
482
483
484 def test_required_with_custom_error_and_validation_error(parser, web_request):
385485 web_request.json = {"foo": ""}
386 parser = MockRequestParser()
387486 args = {
388487 "foo": fields.Str(
389488 required="We need foo",
393492 }
394493 with pytest.raises(ValidationError) as excinfo:
395494 # Test that `validate` receives dictionary of args
396 parser.parse(args, web_request, locations=("json",))
495 parser.parse(args, web_request)
397496
398497 assert "foo required length is 3" in excinfo.value.args[0]["foo"]
399 if MARSHMALLOW_VERSION_INFO[0] < 3:
400 assert "foo" in excinfo.value.field_names
401498
402499
403500 def test_full_input_validator_receives_nonascii_input(web_request):
404501 def validate(val):
405502 return False
406503
407 text = u"øœ∑∆∑"
504 text = "øœ∑∆∑"
408505 web_request.json = {"text": text}
409506 parser = MockRequestParser()
410507 args = {"text": fields.Str()}
411508 with pytest.raises(ValidationError) as excinfo:
412 parser.parse(args, web_request, locations=("json",), validate=validate)
413 assert excinfo.value.messages == ["Invalid value."]
509 parser.parse(args, web_request, validate=validate)
510 assert excinfo.value.messages == {"json": ["Invalid value."]}
414511
415512
416513 def test_invalid_argument_for_validate(web_request, parser):
417514 with pytest.raises(ValueError) as excinfo:
418515 parser.parse({}, web_request, validate="notcallable")
419516 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
428517
429518
430519 def create_bottle_multi_dict():
442531
443532
444533 @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"]
534 def test_multidict_proxy(input_dict):
535 class ListSchema(Schema):
536 foos = fields.List(fields.Str())
537
538 class StrSchema(Schema):
539 foos = fields.Str()
540
541 # this MultiDictProxy is aware that "foos" is a list field and will
542 # therefore produce a list with __getitem__
543 list_wrapped_multidict = MultiDictProxy(input_dict, ListSchema())
544
545 # this MultiDictProxy is under the impression that "foos" is just a string
546 # and it should return "a" or "b"
547 # the decision between "a" and "b" in this case belongs to the framework
548 str_wrapped_multidict = MultiDictProxy(input_dict, StrSchema())
549
550 assert list_wrapped_multidict["foos"] == ["a", "b"]
551 assert str_wrapped_multidict["foos"] in ("a", "b")
448552
449553
450554 def test_parse_with_data_key(web_request):
451555 web_request.json = {"Content-Type": "application/json"}
452556
453557 parser = MockRequestParser()
454 data_key_kwargs = {
455 "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "Content-Type"
456 }
457 args = {"content_type": fields.Field(**data_key_kwargs)}
458 parsed = parser.parse(args, web_request, locations=("json",))
459 assert parsed == {"content_type": "application/json"}
460
461
462 # https://github.com/marshmallow-code/webargs/issues/118
463 @pytest.mark.skipif(
464 MARSHMALLOW_VERSION_INFO[0] >= 3, reason="Behaviour changed in marshmallow 3"
465 )
466 # https://github.com/marshmallow-code/marshmallow/pull/714
467 def test_load_from_is_checked_after_given_key(web_request):
468 web_request.json = {"content_type": "application/json"}
469
470 parser = MockRequestParser()
471 args = {"content_type": fields.Field(load_from="Content-Type")}
472 parsed = parser.parse(args, web_request, locations=("json",))
558 args = {"content_type": fields.Field(data_key="Content-Type")}
559 parsed = parser.parse(args, web_request)
473560 assert parsed == {"content_type": "application/json"}
474561
475562
477564 web_request.json = {"Content-Type": 12345}
478565
479566 parser = MockRequestParser()
480 data_key_kwargs = {
481 "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "Content-Type"
482 }
483 args = {"content_type": fields.Str(**data_key_kwargs)}
567 args = {"content_type": fields.Str(data_key="Content-Type")}
484568 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."]
569 parser.parse(args, web_request)
570 assert "json" in excinfo.value.messages
571 assert "Content-Type" in excinfo.value.messages["json"]
572 assert excinfo.value.messages["json"]["Content-Type"] == ["Not a valid string."]
488573
489574
490575 def test_parse_nested_with_data_key(web_request):
491576 parser = MockRequestParser()
492577 web_request.json = {"nested_arg": {"wrong": "OK"}}
493 data_key_kwarg = {
494 "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "wrong"
495 }
496 args = {"nested_arg": fields.Nested({"right": fields.Field(**data_key_kwarg)})}
497
498 parsed = parser.parse(args, web_request, locations=("json",))
578 args = {"nested_arg": fields.Nested({"right": fields.Field(data_key="wrong")})}
579
580 parsed = parser.parse(args, web_request)
499581 assert parsed == {"nested_arg": {"right": "OK"}}
500582
501583
503585 parser = MockRequestParser()
504586
505587 web_request.json = {"nested_arg": {}}
506 data_key_kwargs = {
507 "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "miss"
508 }
509588 args = {
510589 "nested_arg": fields.Nested(
511 {"found": fields.Field(missing=None, allow_none=True, **data_key_kwargs)}
590 {"found": fields.Field(missing=None, allow_none=True, data_key="miss")}
512591 )
513592 }
514593
515 parsed = parser.parse(args, web_request, locations=("json",))
594 parsed = parser.parse(args, web_request)
516595 assert parsed == {"nested_arg": {"found": None}}
517596
518597
522601 web_request.json = {"nested_arg": {}}
523602 args = {"nested_arg": fields.Nested({"miss": fields.Field(missing="<foo>")})}
524603
525 parsed = parser.parse(args, web_request, locations=("json",))
604 parsed = parser.parse(args, web_request)
526605 assert parsed == {"nested_arg": {"miss": "<foo>"}}
527606
528607
553632 web_request.json = {"username": "foo"}
554633 web_request.query = {"page": 42}
555634
556 @parser.use_args(query_args, web_request, locations=("query",))
557 @parser.use_args(json_args, web_request, locations=("json",))
635 @parser.use_args(query_args, web_request, location="query")
636 @parser.use_args(json_args, web_request)
558637 def viewfunc(query_parsed, json_parsed):
559638 return {"json": json_parsed, "query": query_parsed}
560639
569648 web_request.json = {"username": "foo"}
570649 web_request.query = {"page": 42}
571650
572 @parser.use_kwargs(query_args, web_request, locations=("query",))
573 @parser.use_kwargs(json_args, web_request, locations=("json",))
651 @parser.use_kwargs(query_args, web_request, location="query")
652 @parser.use_kwargs(json_args, web_request)
574653 def viewfunc(page, username):
575654 return {"json": {"username": username}, "query": {"page": page}}
576655
591670
592671 def test_list_allowed_missing(web_request, parser):
593672 args = {"name": fields.List(fields.Str())}
594 web_request.json = {"fakedata": True}
673 web_request.json = {}
595674 result = parser.parse(args, web_request)
596675 assert result == {}
597676
598677
599678 def test_int_list_allowed_missing(web_request, parser):
600679 args = {"name": fields.List(fields.Int())}
601 web_request.json = {"fakedata": True}
680 web_request.json = {}
602681 result = parser.parse(args, web_request)
603682 assert result == {}
604683
605684
606685 def test_multiple_arg_required_with_int_conversion(web_request, parser):
607686 args = {"ids": fields.List(fields.Int(), required=True)}
608 web_request.json = {"fakedata": True}
687 web_request.json = {}
609688 with pytest.raises(ValidationError) as excinfo:
610689 parser.parse(args, web_request)
611 assert excinfo.value.messages == {"ids": ["Missing data for required field."]}
690 assert excinfo.value.messages == {
691 "json": {"ids": ["Missing data for required field."]}
692 }
612693
613694
614695 def test_parse_with_callable(web_request, parser):
617698
618699 class MySchema(Schema):
619700 foo = fields.Field()
620
621 if MARSHMALLOW_VERSION_INFO[0] < 3:
622
623 class Meta:
624 strict = True
625701
626702 def make_schema(req):
627703 assert req is web_request
635711 def test_use_args_callable(web_request, parser):
636712 class HelloSchema(Schema):
637713 name = fields.Str()
638
639 if MARSHMALLOW_VERSION_INFO[0] < 3:
640
641 class Meta:
642 strict = True
643714
644715 @post_load
645716 def request_data(self, item, **kwargs):
665736 id = fields.Int(dump_only=True)
666737 email = fields.Email()
667738 password = fields.Str(load_only=True)
668 if MARSHMALLOW_VERSION_INFO[0] < 3:
669
670 class Meta:
671 strict = True
672739
673740 def test_passing_schema_to_parse(self, parser, web_request):
674741 web_request.json = {"email": "[email protected]", "password": "bar"}
675742
676 result = parser.parse(self.UserSchema(**strict_kwargs), web_request)
743 result = parser.parse(self.UserSchema(), web_request)
677744
678745 assert result == {"email": "[email protected]", "password": "bar"}
679746
681748
682749 web_request.json = {"email": "[email protected]", "password": "bar"}
683750
684 @parser.use_args(self.UserSchema(**strict_kwargs), web_request)
751 @parser.use_args(self.UserSchema(), web_request)
685752 def viewfunc(args):
686753 return args
687754
692759
693760 def factory(req):
694761 assert req is web_request
695 return self.UserSchema(context={"request": req}, **strict_kwargs)
762 return self.UserSchema(context={"request": req})
696763
697764 result = parser.parse(factory, web_request)
698765
703770
704771 def factory(req):
705772 assert req is web_request
706 return self.UserSchema(context={"request": req}, **strict_kwargs)
773 return self.UserSchema(context={"request": req})
707774
708775 @parser.use_args(factory, web_request)
709776 def viewfunc(args):
715782
716783 web_request.json = {"email": "[email protected]", "password": "bar"}
717784
718 @parser.use_kwargs(self.UserSchema(**strict_kwargs), web_request)
785 @parser.use_kwargs(self.UserSchema(), web_request)
719786 def viewfunc(email, password):
720787 return {"email": email, "password": password}
721788
726793
727794 def factory(req):
728795 assert req is web_request
729 return self.UserSchema(context={"request": req}, **strict_kwargs)
796 return self.UserSchema(context={"request": req})
730797
731798 @parser.use_kwargs(factory, web_request)
732799 def viewfunc(email, password):
734801
735802 assert viewfunc() == {"email": "[email protected]", "password": "bar"}
736803
737 @pytest.mark.skipif(
738 MARSHMALLOW_VERSION_INFO[0] >= 3,
739 reason='"strict" parameter is removed in marshmallow 3',
740 )
741 def test_warning_raised_if_schema_is_not_in_strict_mode(self, web_request, parser):
742
743 with pytest.warns(UserWarning) as record:
744 parser.parse(self.UserSchema(strict=False), web_request)
745 warning = record[0]
746 assert "strict=True" in str(warning.message)
747
748804 def test_use_kwargs_stacked(self, web_request, parser):
749805 web_request.json = {"email": "[email protected]", "password": "bar", "page": 42}
750806
751 @parser.use_kwargs({"page": fields.Int()}, web_request)
752 @parser.use_kwargs(self.UserSchema(**strict_kwargs), web_request)
807 @parser.use_kwargs({"page": fields.Int()}, web_request, unknown=EXCLUDE)
808 @parser.use_kwargs(self.UserSchema(), web_request, unknown=EXCLUDE)
753809 def viewfunc(email, password, page):
754810 return {"email": email, "password": password, "page": page}
755811
762818 class UserSchema(Schema):
763819 name = fields.Str()
764820 location = fields.Field(required=False)
765 if MARSHMALLOW_VERSION_INFO[0] < 3:
766
767 class Meta:
768 strict = True
769821
770822 @validates_schema(pass_original=True)
771823 def validate_schema(self, data, original_data, **kwargs):
773825 return True
774826
775827 web_request.json = {"name": "Eric Cartman"}
776 res = parser.parse(UserSchema, web_request, locations=("json",))
828 res = parser.parse(UserSchema, web_request)
777829 assert res == {"name": "Eric Cartman"}
778830
779831
780 def test_use_args_with_custom_locations_in_parser(web_request, parser):
832 def test_use_args_with_custom_location_in_parser(web_request, parser):
781833 custom_args = {"foo": fields.Str()}
782834 web_request.json = {}
783 parser.locations = ("custom",)
784
785 @parser.location_handler("custom")
786 def parse_custom(req, name, arg):
787 return "bar"
835 parser.location = "custom"
836
837 @parser.location_loader("custom")
838 def load_custom(schema, req):
839 return {"foo": "bar"}
788840
789841 @parser.use_args(custom_args, web_request)
790842 def viewfunc(args):
818870
819871 def test_delimited_list_default_delimiter(web_request, parser):
820872 web_request.json = {"ids": "1,2,3"}
821 schema_cls = dict2schema({"ids": fields.DelimitedList(fields.Int())})
873 schema_cls = Schema.from_dict({"ids": fields.DelimitedList(fields.Int())})
822874 schema = schema_cls()
823875
824876 parsed = parser.parse(schema, web_request)
825877 assert parsed["ids"] == [1, 2, 3]
826878
827 dumped = schema.dump(parsed)
828 data = dumped.data if MARSHMALLOW_VERSION_INFO[0] < 3 else dumped
829 assert data["ids"] == [1, 2, 3]
830
831
832 def test_delimited_list_as_string(web_request, parser):
833 web_request.json = {"ids": "1,2,3"}
834 schema_cls = dict2schema(
835 {"ids": fields.DelimitedList(fields.Int(), as_string=True)}
879 data = schema.dump(parsed)
880 assert data["ids"] == "1,2,3"
881
882
883 def test_delimited_tuple_default_delimiter(web_request, parser):
884 """
885 Test load and dump from DelimitedTuple, including the use of a datetime
886 type (similar to a DelimitedList test below) which confirms that we aren't
887 relying on __str__, but are properly de/serializing the included fields
888 """
889 web_request.json = {"ids": "1,2,2020-05-04"}
890 schema_cls = Schema.from_dict(
891 {
892 "ids": fields.DelimitedTuple(
893 (fields.Int, fields.Int, fields.DateTime(format="%Y-%m-%d"))
894 )
895 }
836896 )
837897 schema = schema_cls()
838898
839899 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):
900 assert parsed["ids"] == (1, 2, datetime.datetime(2020, 5, 4))
901
902 data = schema.dump(parsed)
903 assert data["ids"] == "1,2,2020-05-04"
904
905
906 def test_delimited_tuple_incorrect_arity(web_request, parser):
907 web_request.json = {"ids": "1,2"}
908 schema_cls = Schema.from_dict(
909 {"ids": fields.DelimitedTuple((fields.Int, fields.Int, fields.Int))}
910 )
911 schema = schema_cls()
912
913 with pytest.raises(ValidationError):
914 parser.parse(schema, web_request)
915
916
917 def test_delimited_list_with_datetime(web_request, parser):
918 """
919 Test that DelimitedList(DateTime(format=...)) correctly parses and dumps
920 dates to and from strings -- indicates that we're doing proper
921 serialization of values in dump() and not just relying on __str__ producing
922 correct results
923 """
848924 web_request.json = {"dates": "2018-11-01,2018-11-02"}
849 schema_cls = dict2schema(
850 {
851 "dates": fields.DelimitedList(
852 fields.DateTime(format="%Y-%m-%d"), as_string=True
853 )
854 }
925 schema_cls = Schema.from_dict(
926 {"dates": fields.DelimitedList(fields.DateTime(format="%Y-%m-%d"))}
855927 )
856928 schema = schema_cls()
857929
861933 datetime.datetime(2018, 11, 2),
862934 ]
863935
864 dumped = schema.dump(parsed)
865 data = dumped.data if MARSHMALLOW_VERSION_INFO[0] < 3 else dumped
936 data = schema.dump(parsed)
866937 assert data["dates"] == "2018-11-01,2018-11-02"
867938
868939
869940 def test_delimited_list_custom_delimiter(web_request, parser):
870941 web_request.json = {"ids": "1|2|3"}
871 schema_cls = dict2schema({"ids": fields.DelimitedList(fields.Int(), delimiter="|")})
942 schema_cls = Schema.from_dict(
943 {"ids": fields.DelimitedList(fields.Int(), delimiter="|")}
944 )
872945 schema = schema_cls()
873946
874947 parsed = parser.parse(schema, web_request)
875948 assert parsed["ids"] == [1, 2, 3]
876949
877
878 def test_delimited_list_load_list(web_request, parser):
950 data = schema.dump(parsed)
951 assert data["ids"] == "1|2|3"
952
953
954 def test_delimited_tuple_custom_delimiter(web_request, parser):
955 web_request.json = {"ids": "1|2"}
956 schema_cls = Schema.from_dict(
957 {"ids": fields.DelimitedTuple((fields.Int, fields.Int), delimiter="|")}
958 )
959 schema = schema_cls()
960
961 parsed = parser.parse(schema, web_request)
962 assert parsed["ids"] == (1, 2)
963
964 data = schema.dump(parsed)
965 assert data["ids"] == "1|2"
966
967
968 def test_delimited_list_load_list_errors(web_request, parser):
879969 web_request.json = {"ids": [1, 2, 3]}
880 schema_cls = dict2schema({"ids": fields.DelimitedList(fields.Int())})
970 schema_cls = Schema.from_dict({"ids": fields.DelimitedList(fields.Int())})
881971 schema = schema_cls()
882972
883 parsed = parser.parse(schema, web_request)
884 assert parsed["ids"] == [1, 2, 3]
973 with pytest.raises(ValidationError) as excinfo:
974 parser.parse(schema, web_request)
975 exc = excinfo.value
976 assert isinstance(exc, ValidationError)
977 errors = exc.args[0]
978 assert errors["ids"] == ["Not a valid delimited list."]
979
980
981 def test_delimited_tuple_load_list_errors(web_request, parser):
982 web_request.json = {"ids": [1, 2]}
983 schema_cls = Schema.from_dict(
984 {"ids": fields.DelimitedTuple((fields.Int, fields.Int))}
985 )
986 schema = schema_cls()
987
988 with pytest.raises(ValidationError) as excinfo:
989 parser.parse(schema, web_request)
990 exc = excinfo.value
991 assert isinstance(exc, ValidationError)
992 errors = exc.args[0]
993 assert errors["ids"] == ["Not a valid delimited tuple."]
885994
886995
887996 # Regresion test for https://github.com/marshmallow-code/webargs/issues/149
888997 def test_delimited_list_passed_invalid_type(web_request, parser):
889998 web_request.json = {"ids": 1}
890 schema_cls = dict2schema({"ids": fields.DelimitedList(fields.Int())})
999 schema_cls = Schema.from_dict({"ids": fields.DelimitedList(fields.Int())})
8911000 schema = schema_cls()
8921001
8931002 with pytest.raises(ValidationError) as excinfo:
8941003 parser.parse(schema, web_request)
895 assert excinfo.value.messages == {"ids": ["Not a valid list."]}
1004 assert excinfo.value.messages == {"json": {"ids": ["Not a valid delimited list."]}}
1005
1006
1007 def test_delimited_tuple_passed_invalid_type(web_request, parser):
1008 web_request.json = {"ids": 1}
1009 schema_cls = Schema.from_dict({"ids": fields.DelimitedTuple((fields.Int,))})
1010 schema = schema_cls()
1011
1012 with pytest.raises(ValidationError) as excinfo:
1013 parser.parse(schema, web_request)
1014 assert excinfo.value.messages == {"json": {"ids": ["Not a valid delimited tuple."]}}
8961015
8971016
8981017 def test_missing_list_argument_not_in_parsed_result(web_request, parser):
9101029 msg = "Missing data for required field."
9111030 with pytest.raises(ValidationError, match=msg):
9121031 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
9231032
9241033
9251034 def test_validation_errors_in_validator_are_passed_to_handle_error(parser, web_request):
9501059 parser.parse(args, web_request)
9511060
9521061
953 def test_dict2schema():
954 data_key_kwargs = {
955 "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "content-type"
956 }
957 argmap = {
958 "id": fields.Int(required=True),
959 "title": fields.Str(),
960 "description": fields.Str(),
961 "content_type": fields.Str(**data_key_kwargs),
962 }
963
964 schema_cls = dict2schema(argmap)
965 assert issubclass(schema_cls, Schema)
966
967 schema = schema_cls()
968
969 for each in ["id", "title", "description", "content_type"]:
970 assert each in schema.fields
971 assert schema.fields["id"].required
972 if MARSHMALLOW_VERSION_INFO[0] < 3:
973 assert schema.opts.strict is True
974
975
976 # Regression test for https://github.com/marshmallow-code/webargs/issues/101
977 def test_dict2schema_doesnt_add_to_class_registry():
978 old_n_entries = len(
979 list(
980 itertools.chain(
981 [classes for _, classes in class_registry._registry.items()]
982 )
983 )
984 )
985 argmap = {"id": fields.Field()}
986 dict2schema(argmap)
987 dict2schema(argmap)
988 new_n_entries = len(
989 list(
990 itertools.chain(
991 [classes for _, classes in class_registry._registry.items()]
992 )
993 )
994 )
995 assert new_n_entries == old_n_entries
996
997
998 def test_dict2schema_with_nesting():
1062 def test_nested_field_from_dict():
1063 # webargs.fields.Nested implements dict handling
9991064 argmap = {"nest": fields.Nested({"foo": fields.Field()})}
1000 schema_cls = dict2schema(argmap)
1065 schema_cls = Schema.from_dict(argmap)
10011066 assert issubclass(schema_cls, Schema)
10021067 schema = schema_cls()
10031068 assert "nest" in schema.fields
10151080 def test_get_mimetype():
10161081 assert get_mimetype("application/json") == "application/json"
10171082 assert get_mimetype("application/json;charset=utf8") == "application/json"
1018 assert get_mimetype(None) is None
10191083
10201084
10211085 class MockRequestParserWithErrorHandler(MockRequestParser):
1022 def handle_error(
1023 self, error, req, schema, error_status_code=None, error_headers=None
1024 ):
1086 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
10251087 assert isinstance(error, ValidationError)
10261088 assert isinstance(schema, Schema)
10271089 raise MockHTTPError(error_status_code, error_headers)
10401102 assert error.headers == {"X-Foo": "bar"}
10411103
10421104
1043 @mock.patch("webargs.core.Parser.parse_json")
1044 def test_custom_schema_class(parse_json, web_request):
1105 @mock.patch("webargs.core.Parser.load_json")
1106 def test_custom_schema_class(load_json, web_request):
10451107 class CustomSchema(Schema):
10461108 @pre_load
10471109 def pre_load(self, data, **kwargs):
10481110 data["value"] += " world"
10491111 return data
10501112
1051 parse_json.return_value = "hello"
1113 load_json.return_value = {"value": "hello"}
10521114 argmap = {"value": fields.Str()}
10531115 p = Parser(schema_class=CustomSchema)
10541116 ret = p.parse(argmap, web_request)
10551117 assert ret == {"value": "hello world"}
10561118
10571119
1058 @mock.patch("webargs.core.Parser.parse_json")
1059 def test_custom_default_schema_class(parse_json, web_request):
1120 @mock.patch("webargs.core.Parser.load_json")
1121 def test_custom_default_schema_class(load_json, web_request):
10601122 class CustomSchema(Schema):
10611123 @pre_load
10621124 def pre_load(self, data, **kwargs):
10661128 class CustomParser(Parser):
10671129 DEFAULT_SCHEMA_CLASS = CustomSchema
10681130
1069 parse_json.return_value = "hello"
1131 load_json.return_value = {"value": "hello"}
10701132 argmap = {"value": fields.Str()}
10711133 p = CustomParser()
10721134 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
1613 def test_use_args_with_validation(self):
1714 pass
1815
19 @pytest.mark.skip(reason="headers location not supported by DjangoParser")
20 def test_parsing_headers(self, testapp):
21 pass
22
2316 def test_parsing_in_class_based_view(self, testapp):
2417 assert testapp.get("/echo_cbv?name=Fred").json == {"name": "Fred"}
25 assert testapp.post("/echo_cbv", {"name": "Fred"}).json == {"name": "Fred"}
18 assert testapp.post_json("/echo_cbv", {"name": "Fred"}).json == {"name": "Fred"}
2619
2720 def test_use_args_in_class_based_view(self, testapp):
2821 res = testapp.get("/echo_use_args_cbv?name=Fred")
2922 assert res.json == {"name": "Fred"}
30 res = testapp.post("/echo_use_args_cbv", {"name": "Fred"})
23 res = testapp.post_json("/echo_use_args_cbv", {"name": "Fred"})
3124 assert res.json == {"name": "Fred"}
3225
3326 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
1515 def test_use_args_hook(self, testapp):
1616 assert testapp.get("/echo_use_args_hook?name=Fred").json == {"name": "Fred"}
1717
18 def test_parse_media(self, testapp):
19 assert testapp.post_json("/echo_media", {"name": "Fred"}).json == {
20 "name": "Fred"
21 }
22
23 def test_parse_media_missing(self, testapp):
24 assert testapp.post("/echo_media", "").json == {"name": "World"}
25
26 def test_parse_media_empty(self, testapp):
27 assert testapp.post_json("/echo_media", {}).json == {"name": "World"}
28
29 def test_parse_media_error_unexpected_int(self, testapp):
30 res = testapp.post_json("/echo_media", 1, expect_errors=True)
31 assert res.status_code == 422
32
1833 # https://github.com/marshmallow-code/webargs/issues/427
19 def test_parse_json_with_nonutf8_chars(self, testapp):
34 @pytest.mark.parametrize("path", ["/echo_json", "/echo_media"])
35 def test_parse_json_with_nonutf8_chars(self, testapp, path):
2036 res = testapp.post(
21 "/echo",
37 path,
2238 b"\xfe",
2339 headers={"Accept": "application/json", "Content-Type": "application/json"},
2440 expect_errors=True,
2541 )
2642
2743 assert res.status_code == 400
28 assert res.json["errors"] == {"json": ["Invalid JSON body."]}
44 if path.endswith("json"):
45 assert res.json["errors"] == {"json": ["Invalid JSON body."]}
2946
3047 # https://github.com/sloria/webargs/issues/329
31 def test_invalid_json(self, testapp):
48 @pytest.mark.parametrize("path", ["/echo_json", "/echo_media"])
49 def test_invalid_json(self, testapp, path):
3250 res = testapp.post(
33 "/echo",
51 path,
3452 '{"foo": "bar", }',
3553 headers={"Accept": "application/json", "Content-Type": "application/json"},
3654 expect_errors=True,
3755 )
3856 assert res.status_code == 400
39 assert res.json["errors"] == {"json": ["Invalid JSON body."]}
57 if path.endswith("json"):
58 assert res.json["errors"] == {"json": ["Invalid JSON body."]}
59
60 # Falcon converts headers to all-caps
61 def test_parsing_headers(self, testapp):
62 res = testapp.get("/echo_headers", headers={"name": "Fred"})
63 assert res.json == {"NAME": "Fred"}
64
65 # `falcon.testing.TestClient.simulate_request` parses request with `wsgiref`
66 def test_body_parsing_works_with_simulate(self):
67 app = self.create_app()
68 client = falcon.testing.TestClient(app)
69 res = client.simulate_post(
70 "/echo_json",
71 json={"name": "Fred"},
72 )
73 assert res.json == {"name": "Fred"}
0 # -*- coding: utf-8 -*-
1 from __future__ import unicode_literals
2 import threading
0 from unittest import mock
31
4 from werkzeug.exceptions import HTTPException
5 import mock
2 from werkzeug.exceptions import HTTPException, BadRequest
63 import pytest
74
5 from marshmallow import Schema
86 from flask import Flask
97 from webargs import fields, ValidationError, missing
108 from webargs.flaskparser import parser, abort
11 from webargs.core import MARSHMALLOW_VERSION_INFO, json
9 from webargs.core import json
1210
1311 from .apps.flask_app import app
1412 from webargs.testing import CommonTestCase
2523 def test_parsing_invalid_view_arg(self, testapp):
2624 res = testapp.get("/echo_view_arg/foo", expect_errors=True)
2725 assert res.status_code == 422
28 assert res.json == {"view_arg": ["Not a valid integer."]}
26 assert res.json == {"view_args": {"view_arg": ["Not a valid integer."]}}
2927
3028 def test_use_args_with_view_args_parsing(self, testapp):
3129 res = testapp.get("/echo_view_arg_use_args/42")
3230 assert res.json == {"view_arg": 42}
3331
3432 def test_use_args_on_a_method_view(self, testapp):
35 res = testapp.post("/echo_method_view_use_args", {"val": 42})
33 res = testapp.post_json("/echo_method_view_use_args", {"val": 42})
3634 assert res.json == {"val": 42}
3735
3836 def test_use_kwargs_on_a_method_view(self, testapp):
39 res = testapp.post("/echo_method_view_use_kwargs", {"val": 42})
37 res = testapp.post_json("/echo_method_view_use_kwargs", {"val": 42})
4038 assert res.json == {"val": 42}
4139
4240 def test_use_kwargs_with_missing_data(self, testapp):
43 res = testapp.post("/echo_use_kwargs_missing", {"username": "foo"})
41 res = testapp.post_json("/echo_use_kwargs_missing", {"username": "foo"})
4442 assert res.json == {"username": "foo"}
4543
4644 # regression test for https://github.com/marshmallow-code/webargs/issues/145
4745 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 if MARSHMALLOW_VERSION_INFO[0] < 3:
51 assert res.json == {"x_field": [{"id": 42}]}
46 post_with_raw_fieldname_args = (
47 "/echo_nested_many_data_key",
48 {"x_field": [{"id": 42}]},
49 )
50 res = testapp.post_json(*post_with_raw_fieldname_args, expect_errors=True)
51 assert res.status_code == 422
5252
5353 res = testapp.post_json("/echo_nested_many_data_key", {"X-Field": [{"id": 24}]})
5454 assert res.json == {"x_field": [{"id": 24}]}
5656 res = testapp.post_json("/echo_nested_many_data_key", {})
5757 assert res.json == {}
5858
59 # regression test for
60 # https://github.com/marshmallow-code/webargs/issues/500
61 def test_parsing_unexpected_headers_when_raising(self, testapp):
62 res = testapp.get(
63 "/echo_headers_raising", expect_errors=True, headers={"X-Unexpected": "foo"}
64 )
65 assert res.status_code == 422
66 assert "headers" in res.json
67 assert "X-Unexpected" in set(res.json["headers"].keys())
68
5969
6070 @mock.patch("webargs.flaskparser.abort")
6171 def test_abort_called_on_validation_error(mock_abort):
72 # error handling must raise an error to be valid
73 mock_abort.side_effect = BadRequest("foo")
74
6275 app = Flask("testapp")
6376
6477 def validate(x):
7184 data=json.dumps({"value": 41}),
7285 content_type="application/json",
7386 ):
74 parser.parse(argmap)
87 with pytest.raises(HTTPException):
88 parser.parse(argmap)
7589 mock_abort.assert_called()
7690 abort_args, abort_kwargs = mock_abort.call_args
7791 assert abort_args[0] == 422
7892 expected_msg = "Invalid value."
79 assert abort_kwargs["messages"]["value"] == [expected_msg]
93 assert abort_kwargs["messages"]["json"]["value"] == [expected_msg]
8094 assert type(abort_kwargs["exc"]) == ValidationError
8195
8296
83 def test_parse_form_returns_missing_if_no_form():
97 @pytest.mark.parametrize("mimetype", [None, "application/json"])
98 def test_load_json_returns_missing_if_no_data(mimetype):
8499 req = mock.Mock()
85 req.form.get.side_effect = AttributeError("no form")
86 assert parser.parse_form(req, "foo", fields.Field()) is missing
100 req.mimetype = mimetype
101 req.get_data.return_value = ""
102 schema = Schema.from_dict({"foo": fields.Field()})()
103 assert parser.load_json(req, schema) is missing
87104
88105
89106 def test_abort_with_message():
110127 error = json.loads(serialized_error)
111128 assert isinstance(error, dict)
112129 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
-79
tests/test_py3/test_aiohttpparser.py less more
0 # -*- coding: utf-8 -*-
1
2 import asyncio
3 import webtest
4 import webtest_aiohttp
5 import pytest
6
7 from io import BytesIO
8 from webargs.core import MARSHMALLOW_VERSION_INFO
9 from webargs.testing import CommonTestCase
10 from tests.apps.aiohttp_app import create_app
11
12
13 class TestAIOHTTPParser(CommonTestCase):
14 def create_app(self):
15 return create_app()
16
17 def create_testapp(self, app):
18 loop = asyncio.get_event_loop()
19 self.loop = loop
20 return webtest_aiohttp.TestApp(app, loop=loop)
21
22 def after_create_app(self):
23 self.loop.close()
24
25 @pytest.mark.skip(reason="files location not supported for aiohttpparser")
26 def test_parse_files(self, testapp):
27 pass
28
29 def test_parse_match_info(self, testapp):
30 assert testapp.get("/echo_match_info/42").json == {"mymatch": 42}
31
32 def test_use_args_on_method_handler(self, testapp):
33 assert testapp.get("/echo_method").json == {"name": "World"}
34 assert testapp.get("/echo_method?name=Steve").json == {"name": "Steve"}
35 assert testapp.get("/echo_method_view").json == {"name": "World"}
36 assert testapp.get("/echo_method_view?name=Steve").json == {"name": "Steve"}
37
38 # regression test for https://github.com/marshmallow-code/webargs/issues/165
39 def test_multiple_args(self, testapp):
40 res = testapp.post_json(
41 "/echo_multiple_args", {"first": "1", "last": "2", "_ignore": 0}
42 )
43 assert res.json == {"first": "1", "last": "2"}
44
45 # regression test for https://github.com/marshmallow-code/webargs/issues/145
46 def test_nested_many_with_data_key(self, testapp):
47 res = testapp.post_json("/echo_nested_many_data_key", {"x_field": [{"id": 42}]})
48 # https://github.com/marshmallow-code/marshmallow/pull/714
49 if MARSHMALLOW_VERSION_INFO[0] < 3:
50 assert res.json == {"x_field": [{"id": 42}]}
51
52 res = testapp.post_json("/echo_nested_many_data_key", {"X-Field": [{"id": 24}]})
53 assert res.json == {"x_field": [{"id": 24}]}
54
55 res = testapp.post_json("/echo_nested_many_data_key", {})
56 assert res.json == {}
57
58 def test_schema_as_kwargs_view(self, testapp):
59 assert testapp.get("/echo_use_schema_as_kwargs").json == {"name": "World"}
60 assert testapp.get("/echo_use_schema_as_kwargs?name=Chandler").json == {
61 "name": "Chandler"
62 }
63
64 # https://github.com/marshmallow-code/webargs/pull/297
65 def test_empty_json_body(self, testapp):
66 environ = {"CONTENT_TYPE": "application/json", "wsgi.input": BytesIO(b"")}
67 req = webtest.TestRequest.blank("/echo", environ)
68 resp = testapp.do_request(req)
69 assert resp.json == {"name": "World"}
70
71 def test_use_args_multiple(self, testapp):
72 res = testapp.post_json(
73 "/echo_use_args_multiple?page=2&q=10", {"name": "Steve"}
74 )
75 assert res.json == {
76 "query_parsed": {"page": 2, "q": 10},
77 "json_parsed": {"name": "Steve"},
78 }
+0
-57
tests/test_py3/test_aiohttpparser_async_functions.py less more
0 import pytest
1 import webtest_aiohttp
2 from aiohttp.web import Application, json_response
3
4 from webargs import fields
5 from webargs.aiohttpparser import parser, use_args, use_kwargs
6
7 ##### Test app handlers #####
8
9 hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)}
10
11
12 async def echo_parse(request):
13 parsed = await parser.parse(hello_args, request)
14 return json_response(parsed)
15
16
17 @use_args(hello_args)
18 async def echo_use_args(request, args):
19 return json_response(args)
20
21
22 @use_kwargs(hello_args)
23 async def echo_use_kwargs(request, name):
24 return json_response({"name": name})
25
26
27 ##### Fixtures #####
28
29
30 @pytest.fixture()
31 def app():
32 app_ = Application()
33 app_.router.add_route("GET", "/echo", echo_parse)
34 app_.router.add_route("GET", "/echo_use_args", echo_use_args)
35 app_.router.add_route("GET", "/echo_use_kwargs", echo_use_kwargs)
36 return app_
37
38
39 @pytest.fixture()
40 def testapp(app, loop):
41 return webtest_aiohttp.TestApp(app, loop=loop)
42
43
44 ##### Tests #####
45
46
47 def test_async_parse(testapp):
48 assert testapp.get("/echo?name=Steve").json == {"name": "Steve"}
49
50
51 def test_async_use_args(testapp):
52 assert testapp.get("/echo_use_args?name=Steve").json == {"name": "Steve"}
53
54
55 def test_async_use_kwargs(testapp):
56 assert testapp.get("/echo_use_kwargs?name=Steve").json == {"name": "Steve"}
0 # -*- coding: utf-8 -*-
10 from webargs.testing import CommonTestCase
21
32
0 # -*- coding: utf-8 -*-
1
2 from webargs.core import json
3
4 try:
5 from urllib.parse import urlencode
6 except ImportError: # PY2
7 from urllib import urlencode # type: ignore
8
9 import mock
0 from unittest import mock
1 from urllib.parse import urlencode
2
3 import marshmallow as ma
104 import pytest
11
12 import marshmallow as ma
13
5 import tornado.concurrent
6 import tornado.http1connection
7 import tornado.httpserver
8 import tornado.httputil
9 import tornado.ioloop
1410 import tornado.web
15 import tornado.httputil
16 import tornado.httpserver
17 import tornado.http1connection
18 import tornado.concurrent
19 import tornado.ioloop
2011 from tornado.testing import AsyncHTTPTestCase
21
2212 from webargs import fields, missing
23 from webargs.tornadoparser import parser, use_args, use_kwargs, get_value
24 from webargs.core import parse_json
13 from webargs.core import json, parse_json
14 from webargs.tornadoparser import (
15 WebArgsTornadoMultiDictProxy,
16 parser,
17 use_args,
18 use_kwargs,
19 )
20
2521
2622 name = "name"
2723 value = "value"
2824
2925
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
26 class AuthorSchema(ma.Schema):
27 name = fields.Str(missing="World", validate=lambda n: len(n) >= 3)
28 works = fields.List(fields.Str())
29
30
31 author_schema = AuthorSchema()
32
33
34 def test_tornado_multidictproxy():
35 for dictval, fieldname, expected in (
36 ({"name": "Sophocles"}, "name", "Sophocles"),
37 ({"name": "Sophocles"}, "works", missing),
38 ({"works": ["Antigone", "Oedipus Rex"]}, "works", ["Antigone", "Oedipus Rex"]),
39 ({"works": ["Antigone", "Oedipus at Colonus"]}, "name", missing),
40 ):
41 proxy = WebArgsTornadoMultiDictProxy(dictval, author_schema)
42 assert proxy.get(fieldname) == expected
43
44
45 class TestQueryArgs:
4346 def test_it_should_get_single_values(self):
44 query = [(name, value)]
45 field = fields.Field()
47 query = [("name", "Aeschylus")]
4648 request = make_get_request(query)
47
48 result = parser.parse_querystring(request, name, field)
49
50 assert result == value
49 result = parser.load_querystring(request, author_schema)
50 assert result["name"] == "Aeschylus"
5151
5252 def test_it_should_get_multiple_values(self):
53 query = [(name, value), (name, value)]
54 field = fields.List(fields.Field())
53 query = [("works", "Agamemnon"), ("works", "Nereids")]
5554 request = make_get_request(query)
56
57 result = parser.parse_querystring(request, name, field)
58
59 assert result == [value, value]
55 result = parser.load_querystring(request, author_schema)
56 assert result["works"] == ["Agamemnon", "Nereids"]
6057
6158 def test_it_should_return_missing_if_not_present(self):
6259 query = []
63 field = fields.Field()
64 field2 = fields.List(fields.Int())
6560 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
61 result = parser.load_querystring(request, author_schema)
62 assert result["name"] is missing
63 assert result["works"] is missing
8164
8265
8366 class TestFormArgs:
84 def setup_method(self, method):
85 parser.clear_cache()
86
8767 def test_it_should_get_single_values(self):
88 query = [(name, value)]
89 field = fields.Field()
68 query = [("name", "Aristophanes")]
9069 request = make_form_request(query)
91
92 result = parser.parse_form(request, name, field)
93
94 assert result == value
70 result = parser.load_form(request, author_schema)
71 assert result["name"] == "Aristophanes"
9572
9673 def test_it_should_get_multiple_values(self):
97 query = [(name, value), (name, value)]
98 field = fields.List(fields.Field())
74 query = [("works", "The Wasps"), ("works", "The Frogs")]
9975 request = make_form_request(query)
100
101 result = parser.parse_form(request, name, field)
102
103 assert result == [value, value]
76 result = parser.load_form(request, author_schema)
77 assert result["works"] == ["The Wasps", "The Frogs"]
10478
10579 def test_it_should_return_missing_if_not_present(self):
10680 query = []
107 field = fields.Field()
10881 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
82 result = parser.load_form(request, author_schema)
83 assert result["name"] is missing
84 assert result["works"] is missing
85
86
87 class TestJSONArgs:
12888 def test_it_should_get_single_values(self):
129 query = {name: value}
130 field = fields.Field()
89 query = {"name": "Euripides"}
13190 request = make_json_request(query)
132 result = parser.parse_json(request, name, field)
133
134 assert result == value
91 result = parser.load_json(request, author_schema)
92 assert result["name"] == "Euripides"
13593
13694 def test_parsing_request_with_vendor_content_type(self):
137 query = {name: value}
138 field = fields.Field()
95 query = {"name": "Euripides"}
13996 request = make_json_request(
14097 query, content_type="application/vnd.api+json; charset=UTF-8"
14198 )
142 result = parser.parse_json(request, name, field)
143
144 assert result == value
99 result = parser.load_json(request, author_schema)
100 assert result["name"] == "Euripides"
145101
146102 def test_it_should_get_multiple_values(self):
147 query = {name: [value, value]}
148 field = fields.List(fields.Field())
103 query = {"works": ["Medea", "Electra"]}
149104 request = make_json_request(query)
150 result = parser.parse_json(request, name, field)
151
152 assert result == [value, value]
105 result = parser.load_json(request, author_schema)
106 assert result["works"] == ["Medea", "Electra"]
153107
154108 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 )
109 class CustomSchema(ma.Schema):
110 works = fields.List(
111 fields.Nested({"author": fields.Str(), "workname": fields.Str()})
112 )
113
114 custom_schema = CustomSchema()
115
116 query = {
117 "works": [
118 {"author": "Euripides", "workname": "Hecuba"},
119 {"author": "Aristophanes", "workname": "The Birds"},
120 ]
121 }
159122 request = make_json_request(query)
160 result = parser.parse_json(request, name, field)
161 assert result == [{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}]
123 result = parser.load_json(request, custom_schema)
124 assert result["works"] == [
125 {"author": "Euripides", "workname": "Hecuba"},
126 {"author": "Aristophanes", "workname": "The Birds"},
127 ]
128
129 def test_it_should_not_include_fieldnames_if_not_present(self):
130 query = {}
131 request = make_json_request(query)
132 result = parser.load_json(request, author_schema)
133 assert result == {}
134
135 def test_it_should_handle_type_error_on_load_json(self, loop):
136 # but this is different from the test above where the payload was valid
137 # and empty -- missing vs {}
138 # NOTE: `loop` is the pytest-aiohttp event loop fixture, but it's
139 # important to get an event loop here so that we can construct a future
140 request = make_request(
141 body=tornado.concurrent.Future(),
142 headers={"Content-Type": "application/json"},
143 )
144 result = parser.load_json(request, author_schema)
145 assert result is missing
146
147 def test_it_should_handle_value_error_on_parse_json(self):
148 request = make_request("this is json not")
149 result = parser.load_json(request, author_schema)
150 assert result is missing
151
152
153 class TestHeadersArgs:
154 def test_it_should_get_single_values(self):
155 query = {"name": "Euphorion"}
156 request = make_request(headers=query)
157 result = parser.load_headers(request, author_schema)
158 assert result["name"] == "Euphorion"
159
160 def test_it_should_get_multiple_values(self):
161 query = {"works": ["Prometheus Bound", "Prometheus Unbound"]}
162 request = make_request(headers=query)
163 result = parser.load_headers(request, author_schema)
164 assert result["works"] == ["Prometheus Bound", "Prometheus Unbound"]
162165
163166 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
167 request = make_request()
168 result = parser.load_headers(request, author_schema)
169 assert result["name"] is missing
170 assert result["works"] is missing
171
172
173 class TestFilesArgs:
200174 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
175 query = [("name", "Sappho")]
176 request = make_files_request(query)
177 result = parser.load_files(request, author_schema)
178 assert result["name"] == "Sappho"
208179
209180 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()
181 query = [("works", "Sappho 31"), ("works", "Ode to Aphrodite")]
243182 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]
183 result = parser.load_files(request, author_schema)
184 assert result["works"] == ["Sappho 31", "Ode to Aphrodite"]
257185
258186 def test_it_should_return_missing_if_not_present(self):
259187 query = []
260 field = fields.Field()
261188 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):
189 result = parser.load_files(request, author_schema)
190 assert result["name"] is missing
191 assert result["works"] is missing
192
193
194 class TestErrorHandler:
278195 def test_it_should_raise_httperror_on_failed_validation(self):
279196 args = {"foo": fields.Field(validate=lambda x: False)}
280197 with pytest.raises(tornado.web.HTTPError):
281198 parser.parse(args, make_json_request({"foo": 42}))
282199
283200
284 class TestParse(object):
285 def setup_method(self, method):
286 parser.clear_cache()
287
201 class TestParse:
288202 def test_it_should_parse_query_arguments(self):
289203 attrs = {"string": fields.Field(), "integer": fields.List(fields.Int())}
290204
292206 [("string", "value"), ("integer", "1"), ("integer", "2")]
293207 )
294208
295 parsed = parser.parse(attrs, request)
209 parsed = parser.parse(attrs, request, location="query")
296210
297211 assert parsed["integer"] == [1, 2]
298212 assert parsed["string"] == value
304218 [("string", "value"), ("integer", "1"), ("integer", "2")]
305219 )
306220
307 parsed = parser.parse(attrs, request)
221 parsed = parser.parse(attrs, request, location="form")
308222
309223 assert parsed["integer"] == [1, 2]
310224 assert parsed["string"] == value
336250
337251 request = make_request(headers={"string": "value", "integer": ["1", "2"]})
338252
339 parsed = parser.parse(attrs, request, locations=["headers"])
253 parsed = parser.parse(attrs, request, location="headers")
340254
341255 assert parsed["string"] == value
342256 assert parsed["integer"] == [1, 2]
348262 [("string", "value"), ("integer", "1"), ("integer", "2")]
349263 )
350264
351 parsed = parser.parse(attrs, request, locations=["cookies"])
265 parsed = parser.parse(attrs, request, location="cookies")
352266
353267 assert parsed["string"] == value
354268 assert parsed["integer"] == [2]
360274 [("string", "value"), ("integer", "1"), ("integer", "2")]
361275 )
362276
363 parsed = parser.parse(attrs, request, locations=["files"])
277 parsed = parser.parse(attrs, request, location="files")
364278
365279 assert parsed["string"] == value
366280 assert parsed["integer"] == [1, 2]
382296 parser.parse(args, request)
383297
384298
385 class TestUseArgs(object):
386 def setup_method(self, method):
387 parser.clear_cache()
388
299 class TestUseArgs:
389300 def test_it_should_pass_parsed_as_first_argument(self):
390 class Handler(object):
301 class Handler:
391302 request = make_json_request({"key": "value"})
392303
393304 @use_args({"key": fields.Field()})
402313 assert result is True
403314
404315 def test_it_should_pass_parsed_as_kwargs_arguments(self):
405 class Handler(object):
316 class Handler:
406317 request = make_json_request({"key": "value"})
407318
408319 @use_kwargs({"key": fields.Field()})
417328 assert result is True
418329
419330 def test_it_should_be_validate_arguments_when_validator_is_passed(self):
420 class Handler(object):
331 class Handler:
421332 request = make_json_request({"foo": 41})
422333
423334 @use_kwargs({"foo": fields.Int()}, validate=lambda args: args["foo"] > 42)
475386
476387
477388 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""
389 uri = uri if uri is not None else ""
390 body = body if body is not None else ""
480391 method = "POST" if body else "GET"
481392 # Need to make a mock connection right now because Tornado 4.0 requires a
482393 # remote_ip in the context attribute. 4.1 addresses this, and this
485396 mock_connection = mock.Mock(spec=tornado.http1connection.HTTP1Connection)
486397 mock_connection.context = mock.Mock()
487398 mock_connection.remote_ip = None
488 content_type = headers.get("Content-Type", u"") if headers else u""
399 content_type = headers.get("Content-Type", "") if headers else ""
489400 request = tornado.httputil.HTTPServerRequest(
490401 method=method,
491402 uri=uri,
508419 class EchoHandler(tornado.web.RequestHandler):
509420 ARGS = {"name": fields.Str()}
510421
511 @use_args(ARGS)
422 @use_args(ARGS, location="query")
512423 def get(self, args):
513424 self.write(args)
425
426
427 class EchoFormHandler(tornado.web.RequestHandler):
428 ARGS = {"name": fields.Str()}
429
430 @use_args(ARGS, location="form")
431 def post(self, args):
432 self.write(args)
433
434
435 class EchoJSONHandler(tornado.web.RequestHandler):
436 ARGS = {"name": fields.Str()}
514437
515438 @use_args(ARGS)
516439 def post(self, args):
520443 class EchoWithParamHandler(tornado.web.RequestHandler):
521444 ARGS = {"name": fields.Str()}
522445
523 @use_args(ARGS)
446 @use_args(ARGS, location="query")
524447 def get(self, id, args):
525448 self.write(args)
526449
527450
528451 echo_app = tornado.web.Application(
529 [(r"/echo", EchoHandler), (r"/echo_with_param/(\d+)", EchoWithParamHandler)]
452 [
453 (r"/echo", EchoHandler),
454 (r"/echo_form", EchoFormHandler),
455 (r"/echo_json", EchoJSONHandler),
456 (r"/echo_with_param/(\d+)", EchoWithParamHandler),
457 ]
530458 )
531459
532460
536464
537465 def test_post(self):
538466 res = self.fetch(
539 "/echo",
467 "/echo_json",
540468 method="POST",
541469 headers={"Content-Type": "application/json"},
542470 body=json.dumps({"name": "Steve"}),
544472 json_body = parse_json(res.body)
545473 assert json_body["name"] == "Steve"
546474 res = self.fetch(
547 "/echo",
475 "/echo_json",
548476 method="POST",
549477 headers={"Content-Type": "application/json"},
550478 body=json.dumps({}),
576504 def post(self, args):
577505 self.write(args)
578506
579 @use_kwargs(ARGS)
507 @use_kwargs(ARGS, location="query")
580508 def get(self, name):
581509 self.write({"status": "success"})
582510
+0
-160
tests/test_webapp2parser.py less more
0 # -*- coding: utf-8 -*-
1 """Tests for the webapp2 parser"""
2 try:
3 from urllib.parse import urlencode
4 except ImportError: # PY2
5 from urllib import urlencode # type: ignore
6 from webargs.core import json
7
8 import pytest
9 from marshmallow import fields, ValidationError
10
11 import webtest
12 import webapp2
13 from webargs.webapp2parser import parser
14
15 hello_args = {"name": fields.Str(missing="World")}
16
17 hello_multiple = {"name": fields.List(fields.Str())}
18
19 hello_validate = {
20 "num": fields.Int(
21 validate=lambda n: n != 3,
22 error_messages={"validator_failed": "Houston, we've had a problem."},
23 )
24 }
25
26
27 def test_parse_querystring_args():
28 request = webapp2.Request.blank("/echo?name=Fred")
29 assert parser.parse(hello_args, req=request) == {"name": "Fred"}
30
31
32 def test_parse_querystring_multiple():
33 expected = {"name": ["steve", "Loria"]}
34 request = webapp2.Request.blank("/echomulti?name=steve&name=Loria")
35 assert parser.parse(hello_multiple, req=request) == expected
36
37
38 def test_parse_form():
39 expected = {"name": "Joe"}
40 request = webapp2.Request.blank("/echo", POST=expected)
41 assert parser.parse(hello_args, req=request) == expected
42
43
44 def test_parse_form_multiple():
45 expected = {"name": ["steve", "Loria"]}
46 request = webapp2.Request.blank("/echo", POST=urlencode(expected, doseq=True))
47 assert parser.parse(hello_multiple, req=request) == expected
48
49
50 def test_parsing_form_default():
51 request = webapp2.Request.blank("/echo", POST="")
52 assert parser.parse(hello_args, req=request) == {"name": "World"}
53
54
55 def test_parse_json():
56 expected = {"name": "Fred"}
57 request = webapp2.Request.blank(
58 "/echo", POST=json.dumps(expected), headers={"content-type": "application/json"}
59 )
60 assert parser.parse(hello_args, req=request) == expected
61
62
63 def test_parse_json_content_type_mismatch():
64 request = webapp2.Request.blank(
65 "/echo_json",
66 POST=json.dumps({"name": "foo"}),
67 headers={"content-type": "application/x-www-form-urlencoded"},
68 )
69 assert parser.parse(hello_args, req=request) == {"name": "World"}
70
71
72 def test_parse_invalid_json():
73 request = webapp2.Request.blank(
74 "/echo", POST='{"foo": "bar", }', headers={"content-type": "application/json"}
75 )
76 with pytest.raises(json.JSONDecodeError):
77 parser.parse(hello_args, req=request)
78
79
80 def test_parse_json_with_vendor_media_type():
81 expected = {"name": "Fred"}
82 request = webapp2.Request.blank(
83 "/echo",
84 POST=json.dumps(expected),
85 headers={"content-type": "application/vnd.api+json"},
86 )
87 assert parser.parse(hello_args, req=request) == expected
88
89
90 def test_parse_json_default():
91 request = webapp2.Request.blank(
92 "/echo", POST="", headers={"content-type": "application/json"}
93 )
94 assert parser.parse(hello_args, req=request) == {"name": "World"}
95
96
97 def test_parsing_cookies():
98 # whitespace is not valid in a cookie name or value per RFC 6265
99 # http://tools.ietf.org/html/rfc6265#section-4.1.1
100 expected = {"name": "Jean-LucPicard"}
101 response = webapp2.Response()
102 response.set_cookie("name", expected["name"])
103 request = webapp2.Request.blank(
104 "/", headers={"Cookie": response.headers["Set-Cookie"]}
105 )
106 assert parser.parse(hello_args, req=request, locations=("cookies",)) == expected
107
108
109 def test_parsing_headers():
110 expected = {"name": "Fred"}
111 request = webapp2.Request.blank("/", headers=expected)
112 assert parser.parse(hello_args, req=request, locations=("headers",)) == expected
113
114
115 def test_parse_files():
116 """Test parsing file upload using WebTest since I don't know how to mock
117 that using a webob.Request
118 """
119
120 class Handler(webapp2.RequestHandler):
121 @parser.use_args({"myfile": fields.List(fields.Field())}, locations=("files",))
122 def post(self, args):
123 self.response.content_type = "application/json"
124
125 def _value(f):
126 return f.getvalue().decode("utf-8")
127
128 data = dict((i.filename, _value(i.file)) for i in args["myfile"])
129 self.response.write(json.dumps(data))
130
131 app = webapp2.WSGIApplication([("/", Handler)])
132 testapp = webtest.TestApp(app)
133 payload = [("myfile", "baz.txt", b"bar"), ("myfile", "moo.txt", b"zoo")]
134 res = testapp.post("/", upload_files=payload)
135 assert res.json == {"baz.txt": "bar", "moo.txt": "zoo"}
136
137
138 def test_exception_on_validation_error():
139 request = webapp2.Request.blank("/", POST={"num": "3"})
140 with pytest.raises(ValidationError):
141 parser.parse(hello_validate, req=request)
142
143
144 def test_validation_error_with_message():
145 request = webapp2.Request.blank("/", POST={"num": "3"})
146 with pytest.raises(ValidationError) as exc:
147 parser.parse(hello_validate, req=request)
148 assert "Houston, we've had a problem." in exc.value
149
150
151 def test_default_app_request():
152 """Test that parser.parse uses the request from webapp2.get_request() if no
153 request is passed
154 """
155 expected = {"name": "Joe"}
156 request = webapp2.Request.blank("/echo", POST=expected)
157 app = webapp2.WSGIApplication([])
158 app.set_globals(app, request)
159 assert parser.parse(hello_args) == 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{36,37,38,39}
4 py36-mindeps
5 py39-marshmallowdev
66 docs
77
88 [testenv]
99 extras = tests
1010 deps =
11 marshmallow2: marshmallow==2.15.2
12 marshmallow3: marshmallow>=3.0.0rc2,<4.0.0
11 !marshmallowdev: marshmallow>=3.0.0,<4.0.0
1312 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}
13 mindeps: Flask==0.12.5
14 mindeps: Django==2.2.0
15 mindeps: bottle==0.12.13
16 mindeps: tornado==4.5.2
17 mindeps: pyramid==1.9.1
18 mindeps: falcon==2.0.0
19 mindeps: aiohttp==3.0.8
20 commands = pytest {posargs}
1721
1822 [testenv:lint]
19 deps = pre-commit~=1.17
23 deps = pre-commit~=2.4
2024 skip_install = true
2125 commands = pre-commit run --all-files
26
27 # a separate `mypy` target which runs `mypy` in an environment with
28 # `webargs` and `marshmallow` both installed is a valuable safeguard against
29 # issues in which `mypy` running on every file standalone won't catch things
30 [testenv:mypy]
31 deps = mypy
32 commands = mypy src/
2233
2334 [testenv:docs]
2435 extras = docs
3041 deps =
3142 sphinx-autobuild
3243 extras = docs
33 commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} -z src/webargs -s 2
44 commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} --watch src/webargs --delay 2
3445
3546 [testenv:watch-readme]
3647 deps = restview