Import upstream version 7.0.1
Kali Janitor
3 years ago
0 | 0 | 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 | |
3 | 8 | hooks: |
4 | 9 | - id: black |
5 | language_version: python3 | |
6 | 10 | - repo: https://gitlab.com/pycqa/flake8 |
7 | rev: 3.7.8 | |
11 | rev: 3.8.4 | |
8 | 12 | hooks: |
9 | 13 | - id: flake8 |
10 | additional_dependencies: ['flake8-bugbear==19.8.0; python_version >= "3.5"'] | |
14 | additional_dependencies: [flake8-bugbear==20.1.0] | |
11 | 15 | - repo: https://github.com/asottile/blacken-docs |
12 | rev: v1.3.0 | |
16 | rev: v1.8.0 | |
13 | 17 | hooks: |
14 | 18 | - id: blacken-docs |
15 | additional_dependencies: [black==19.3b0] | |
19 | additional_dependencies: [black==20.8b1] | |
20 | args: ["--target-version", "py35"] | |
16 | 21 | - repo: https://github.com/pre-commit/mirrors-mypy |
17 | rev: v0.730 | |
22 | rev: v0.790 | |
18 | 23 | hooks: |
19 | 24 | - id: mypy |
20 | 25 | language_version: python3 |
2 | 2 | configuration: docs/conf.py |
3 | 3 | formats: all |
4 | 4 | python: |
5 | version: 3.7 | |
5 | version: 3.8 | |
6 | 6 | install: |
7 | 7 | - method: pip |
8 | 8 | path: . |
4 | 4 | Lead |
5 | 5 | ---- |
6 | 6 | |
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>`_ | |
9 | 9 | |
10 | 10 | Contributors (chronological) |
11 | 11 | ---------------------------- |
12 | 12 | |
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>`_ |
0 | 0 | Changelog |
1 | 1 | --------- |
2 | 2 | |
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 | ||
3 | 290 | 5.5.3 (2020-01-28) |
4 | 291 | ****************** |
5 | 292 | |
6 | 293 | Bug fixes: |
7 | 294 | |
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. | |
9 | 296 | |
10 | 297 | 5.5.2 (2019-10-06) |
11 | 298 | ****************** |
121 | 121 | Documentation |
122 | 122 | +++++++++++++ |
123 | 123 | |
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_. | |
125 | 125 | |
126 | 126 | To build the docs in "watch" mode: :: |
127 | 127 | |
136 | 136 | |
137 | 137 | |
138 | 138 | .. _Sphinx: http://sphinx.pocoo.org/ |
139 | .. _`reStructured Text`: http://docutils.sourceforge.net/rst.html | |
139 | .. _`reStructuredText`: https://docutils.sourceforge.io/rst.html | |
140 | 140 | .. _webargs: https://github.com/marshmallow-code/webargs |
0 | Copyright 2014-2019 Steven Loria and contributors | |
0 | Copyright 2014-2020 Steven Loria and contributors | |
1 | 1 | |
2 | 2 | Permission is hereby granted, free of charge, to any person obtaining a copy |
3 | 3 | of this software and associated documentation files (the "Software"), to deal |
13 | 13 | :target: https://webargs.readthedocs.io/ |
14 | 14 | :alt: Documentation |
15 | 15 | |
16 | .. image:: https://badgen.net/badge/marshmallow/2,3?list=1 | |
16 | .. image:: https://badgen.net/badge/marshmallow/3 | |
17 | 17 | :target: https://marshmallow.readthedocs.io/en/latest/upgrading.html |
18 | :alt: marshmallow 2/3 compatible | |
18 | :alt: marshmallow 3 compatible | |
19 | 19 | |
20 | 20 | .. image:: https://badgen.net/badge/code%20style/black/000 |
21 | 21 | :target: https://github.com/ambv/black |
23 | 23 | |
24 | 24 | Homepage: https://webargs.readthedocs.io/ |
25 | 25 | |
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. | |
27 | 27 | |
28 | 28 | .. code-block:: python |
29 | 29 | |
35 | 35 | |
36 | 36 | |
37 | 37 | @app.route("/") |
38 | @use_args({"name": fields.Str(required=True)}) | |
38 | @use_args({"name": fields.Str(required=True)}, location="query") | |
39 | 39 | def index(args): |
40 | 40 | return "Hello " + args["name"] |
41 | 41 | |
53 | 53 | |
54 | 54 | pip install -U webargs |
55 | 55 | |
56 | webargs supports Python >= 2.7 or >= 3.5. | |
56 | webargs supports Python >= 3.6. | |
57 | 57 | |
58 | 58 | |
59 | 59 | Documentation |
25 | 25 | parameters: |
26 | 26 | toxenvs: |
27 | 27 | - 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 | |
41 | 35 | - docs |
42 | 36 | os: linux |
43 | # Build separate wheels for python 2 and 3 | |
37 | # Build wheels | |
44 | 38 | - template: job--pypi-release.yml@sloria |
45 | 39 | parameters: |
46 | python: "3.7" | |
40 | python: "3.9" | |
47 | 41 | distributions: "sdist bdist_wheel" |
48 | name_postfix: "_py3" | |
49 | 42 | dependsOn: |
50 | 43 | - 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 |
5 | 5 | Custom Location Handlers |
6 | 6 | ------------------------ |
7 | 7 | |
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>`. | |
9 | 9 | |
10 | 10 | |
11 | 11 | .. code-block:: python |
14 | 14 | from webargs.flaskparser import parser |
15 | 15 | |
16 | 16 | |
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 | |
20 | 20 | |
21 | 21 | |
22 | 22 | # 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") | |
24 | 24 | def posts(args): |
25 | 25 | return "displaying {} posts".format(args["per_page"]) |
26 | 26 | |
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"]) | |
27 | 88 | |
28 | 89 | marshmallow Integration |
29 | 90 | ----------------------- |
45 | 106 | last_name = fields.Str(missing="") |
46 | 107 | date_registered = fields.DateTime(dump_only=True) |
47 | 108 | |
48 | # NOTE: Uncomment below two lines if you're using marshmallow 2 | |
49 | # class Meta: | |
50 | # strict = True | |
51 | ||
52 | 109 | |
53 | 110 | @use_args(UserSchema()) |
54 | 111 | def profile_view(args): |
63 | 120 | |
64 | 121 | |
65 | 122 | # 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") | |
67 | 124 | @use_args(UserSchema()) |
68 | 125 | def profile_posts(args, posts_per_page): |
69 | 126 | username = args["username"] |
70 | 127 | # ... |
71 | 128 | |
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. | |
77 | 282 | |
78 | 283 | |
79 | 284 | Schema Factories |
176 | 381 | cube = args["cube"] |
177 | 382 | # ... |
178 | 383 | |
179 | .. _custom-parsers: | |
384 | .. _custom-loaders: | |
180 | 385 | |
181 | 386 | Custom Parsers |
182 | 387 | -------------- |
183 | 388 | |
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. | |
185 | 390 | |
186 | 391 | |
187 | 392 | .. code-block:: python |
210 | 415 | } |
211 | 416 | """ |
212 | 417 | |
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) | |
215 | 420 | |
216 | 421 | |
217 | 422 | def _structure_dict(dict_): |
235 | 440 | If you'd prefer validation errors to return status code ``400`` instead |
236 | 441 | of ``422``, you can override ``DEFAULT_VALIDATION_STATUS`` on a :class:`Parser <webargs.core.Parser>`. |
237 | 442 | |
443 | Sublcass the parser for your framework to do so. For example, using Falcon: | |
238 | 444 | |
239 | 445 | .. code-block:: python |
240 | 446 | |
274 | 480 | |
275 | 481 | |
276 | 482 | @app.route("/profile/", methods=["patch"]) |
277 | @use_args(PatchSchema(many=True), locations=("json",)) | |
483 | @use_args(PatchSchema(many=True)) | |
278 | 484 | def patch_blog(args): |
279 | 485 | """Implements JSON Patch for the user profile |
280 | 486 | |
289 | 495 | Mixing Locations |
290 | 496 | ---------------- |
291 | 497 | |
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 | |
296 | 503 | @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") | |
334 | 506 | def viewfunc(query_parsed, json_parsed): |
335 | 507 | page = query_parsed["page"] |
336 | 508 | name = json_parsed["name"] |
342 | 514 | |
343 | 515 | import functools |
344 | 516 | |
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()}) | |
351 | 523 | def viewfunc(query_parsed, json_parsed): |
352 | 524 | page = query_parsed["page"] |
353 | 525 | name = json_parsed["name"] |
14 | 14 | |
15 | 15 | .. automodule:: webargs.fields |
16 | 16 | :members: Nested, DelimitedList |
17 | ||
18 | ||
19 | webargs.multidictproxy | |
20 | ---------------------- | |
21 | ||
22 | .. automodule:: webargs.multidictproxy | |
23 | :members: | |
24 | ||
17 | 25 | |
18 | 26 | webargs.asyncparser |
19 | 27 | ------------------- |
51 | 59 | .. automodule:: webargs.pyramidparser |
52 | 60 | :members: |
53 | 61 | |
54 | ||
55 | webargs.webapp2parser | |
56 | --------------------- | |
57 | ||
58 | .. automodule:: webargs.webapp2parser | |
59 | :members: | |
60 | ||
61 | ||
62 | 62 | webargs.falconparser |
63 | 63 | --------------------- |
64 | 64 |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | import datetime as dt |
2 | 1 | import sys |
3 | 2 | import os |
22 | 21 | github_user = "marshmallow-code" |
23 | 22 | github_repo = "webargs" |
24 | 23 | |
25 | issues_github_path = "{}/{}".format(github_user, github_repo) | |
24 | issues_github_path = f"{github_user}/{github_repo}" | |
26 | 25 | |
27 | 26 | intersphinx_mapping = { |
28 | 27 | "python": ("http://python.readthedocs.io/en/latest/", None), |
36 | 35 | |
37 | 36 | html_domain_indices = False |
38 | 37 | 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" | |
41 | 40 | version = release = webargs.__version__ |
42 | 41 | templates_path = ["_templates"] |
43 | 42 | exclude_patterns = ["_build"] |
21 | 21 | |
22 | 22 | |
23 | 23 | @app.route("/user/<int:uid>") |
24 | @use_args({"per_page": fields.Int()}) | |
24 | @use_args({"per_page": fields.Int()}, location="query") | |
25 | 25 | 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( | |
27 | 27 | uid=uid, per_page=args["per_page"] |
28 | 28 | ) |
29 | 29 | |
63 | 63 | |
64 | 64 | |
65 | 65 | @app.route("/greeting/<name>/") |
66 | @use_args({"name": fields.Str(location="view_args")}) | |
66 | @use_args({"name": fields.Str()}, location="view_args") | |
67 | 67 | def greeting(args, **kwargs): |
68 | 68 | return "Hello {}".format(args["name"]) |
69 | 69 | |
94 | 94 | } |
95 | 95 | |
96 | 96 | |
97 | @use_args(account_args) | |
97 | @use_args(account_args, location="form") | |
98 | 98 | def login_user(request, args): |
99 | 99 | if request.method == "POST": |
100 | 100 | login(args["username"], args["password"]) |
113 | 113 | |
114 | 114 | |
115 | 115 | class BlogPostView(View): |
116 | @use_args(blog_args) | |
116 | @use_args(blog_args, location="query") | |
117 | 117 | def get(self, request, args): |
118 | 118 | blog_post = Post.objects.get(title__iexact=args["title"], author=args["author"]) |
119 | 119 | return render_to_response("post_template.html", {"post": blog_post}) |
238 | 238 | from webargs.pyramidparser import use_args |
239 | 239 | |
240 | 240 | |
241 | @use_args({"uid": fields.Str(), "per_page": fields.Int()}) | |
241 | @use_args({"uid": fields.Str(), "per_page": fields.Int()}, location="query") | |
242 | 242 | def user_detail(request, args): |
243 | 243 | uid = args["uid"] |
244 | 244 | return Response( |
260 | 260 | from webargs.pyramidparser import use_args |
261 | 261 | |
262 | 262 | |
263 | @use_args({"mymatch": fields.Int()}, locations=("matchdict",)) | |
263 | @use_args({"mymatch": fields.Int()}, location="matchdict") | |
264 | 264 | def matched(request, args): |
265 | 265 | return Response("The value for mymatch is {}".format(args["mymatch"])) |
266 | 266 | |
309 | 309 | |
310 | 310 | |
311 | 311 | def add_args(argmap, **kwargs): |
312 | def hook(req, resp, params): | |
312 | def hook(req, resp, resource, params): | |
313 | 313 | parsed_args = parser.parse(argmap, req=req, **kwargs) |
314 | 314 | req.context["args"] = parsed_args |
315 | 315 | |
316 | 316 | return hook |
317 | 317 | |
318 | 318 | |
319 | @falcon.before(add_args({"page": fields.Int(location="query")})) | |
319 | @falcon.before(add_args({"page": fields.Int()}, location="query")) | |
320 | 320 | class AuthorResource: |
321 | 321 | def on_get(self, req, resp): |
322 | 322 | args = req.context["args"] |
413 | 413 | from webargs.aiohttpparser import use_args |
414 | 414 | |
415 | 415 | |
416 | @parser.use_args({"slug": fields.Str(location="match_info")}) | |
416 | @parser.use_args({"slug": fields.Str()}, location="match_info") | |
417 | 417 | def article_detail(request, args): |
418 | 418 | return web.Response(body="Slug: {}".format(args["slug"]).encode("utf-8")) |
419 | 419 | |
442 | 442 | |
443 | 443 | @route("/users/<_id:int>", method="GET", apply=use_args(user_args)) |
444 | 444 | def users(args, _id): |
445 | """A welcome page. | |
446 | """ | |
445 | """A welcome page.""" | |
447 | 446 | return {"message": "Welcome, {}!".format(args["name"]), "_id": _id} |
3 | 3 | |
4 | 4 | Release v\ |version|. (:doc:`Changelog <changelog>`) |
5 | 5 | |
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. | |
7 | 7 | |
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 | ------------------------- | |
8 | 16 | |
9 | 17 | .. code-block:: python |
10 | 18 | |
16 | 24 | |
17 | 25 | |
18 | 26 | @app.route("/") |
19 | @use_args({"name": fields.Str(required=True)}) | |
27 | @use_args({"name": fields.Str(required=True)}, location="query") | |
20 | 28 | def index(args): |
21 | 29 | return "Hello " + args["name"] |
22 | 30 | |
27 | 35 | # curl http://localhost:5000/\?name\='World' |
28 | 36 | # Hello World |
29 | 37 | |
30 | Webargs will automatically parse: | |
38 | By default Webargs will automatically parse JSON request bodies. But it also | |
39 | has support for: | |
31 | 40 | |
32 | 41 | **Query Parameters** |
33 | 42 | :: |
43 | $ curl http://localhost:5000/\?name\='Freddie' | |
44 | Hello Freddie | |
34 | 45 | |
35 | $ curl http://localhost:5000/\?name\='Freddie' | |
36 | Hello Freddie | |
46 | # pass location="query" to use_args | |
37 | 47 | |
38 | 48 | **Form Data** |
39 | 49 | :: |
41 | 51 | $ curl -d 'name=Brian' http://localhost:5000/ |
42 | 52 | Hello Brian |
43 | 53 | |
54 | # pass location="form" to use_args | |
55 | ||
44 | 56 | **JSON Data** |
45 | 57 | :: |
46 | 58 | |
47 | 59 | $ curl -X POST -H "Content-Type: application/json" -d '{"name":"Roger"}' http://localhost:5000/ |
48 | 60 | Hello Roger |
61 | ||
62 | # pass location="json" (or omit location) to use_args | |
49 | 63 | |
50 | 64 | and, optionally: |
51 | 65 | |
102 | 116 | |
103 | 117 | license |
104 | 118 | changelog |
119 | upgrading | |
105 | 120 | authors |
106 | 121 | contributing |
0 | 0 | Install |
1 | 1 | ======= |
2 | 2 | |
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. | |
4 | 4 | |
5 | 5 | From the PyPI |
6 | 6 | ------------- |
22 | 22 | "nickname": fields.List(fields.Str()), |
23 | 23 | # Delimited list, e.g. "/?languages=python,javascript" |
24 | 24 | "languages": fields.DelimitedList(fields.Str()), |
25 | # When you know where an argument should be parsed from | |
26 | "active": fields.Bool(location="query"), | |
27 | 25 | # When value is keyed on a variable-unsafe name |
28 | 26 | # 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"), | |
36 | 28 | } |
37 | 29 | |
38 | 30 | .. note:: |
104 | 96 | Request "Locations" |
105 | 97 | ------------------- |
106 | 98 | |
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: | |
108 | 100 | |
109 | 101 | .. code-block:: python |
110 | 102 | |
111 | 103 | @app.route("/register") |
112 | @use_args(user_args, locations=("json", "form")) | |
104 | @use_args(user_args, location="form") | |
113 | 105 | def register(args): |
114 | 106 | return "registration page" |
115 | 107 | |
201 | 193 | |
202 | 194 | |
203 | 195 | @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): | |
205 | 197 | raise CustomError(error.messages) |
206 | 198 | |
207 | 199 | Parsing Lists in Query Strings |
242 | 234 | |
243 | 235 | .. note:: |
244 | 236 | |
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. | |
246 | 238 | |
247 | 239 | Next Steps |
248 | 240 | ---------- |
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 | ... |
24 | 24 | |
25 | 25 | @use_args(hello_args) |
26 | 26 | async def index(request, args): |
27 | """A welcome page. | |
28 | """ | |
27 | """A welcome page.""" | |
29 | 28 | return json_response({"message": "Welcome, {}!".format(args["name"])}) |
30 | 29 | |
31 | 30 |
95 | 95 | |
96 | 96 | |
97 | 97 | @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}!"} | |
100 | 100 | |
101 | 101 | |
102 | 102 | @route("/add", methods=["POST"]) |
23 | 23 | |
24 | 24 | @route("/", method="GET", apply=use_args(hello_args)) |
25 | 25 | def index(args): |
26 | """A welcome page. | |
27 | """ | |
26 | """A welcome page.""" | |
28 | 27 | return {"message": "Welcome, {}!".format(args["name"])} |
29 | 28 | |
30 | 29 |
26 | 26 | ### Middleware and hooks ### |
27 | 27 | |
28 | 28 | |
29 | class JSONTranslator(object): | |
29 | class JSONTranslator: | |
30 | 30 | def process_response(self, req, resp, resource): |
31 | 31 | if "result" not in req.context: |
32 | 32 | return |
43 | 43 | ### Resources ### |
44 | 44 | |
45 | 45 | |
46 | class HelloResource(object): | |
46 | class HelloResource: | |
47 | 47 | """A welcome page.""" |
48 | 48 | |
49 | 49 | hello_args = {"name": fields.Str(missing="Friend", location="query")} |
53 | 53 | req.context["result"] = {"message": "Welcome, {}!".format(args["name"])} |
54 | 54 | |
55 | 55 | |
56 | class AdderResource(object): | |
56 | class AdderResource: | |
57 | 57 | """An addition endpoint.""" |
58 | 58 | |
59 | 59 | adder_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} |
63 | 63 | req.context["result"] = {"result": x + y} |
64 | 64 | |
65 | 65 | |
66 | class DateAddResource(object): | |
66 | class DateAddResource: | |
67 | 67 | """A datetime adder endpoint.""" |
68 | 68 | |
69 | 69 | dateadd_args = { |
25 | 25 | @app.route("/", methods=["GET"]) |
26 | 26 | @use_args(hello_args) |
27 | 27 | def index(args): |
28 | """A welcome page. | |
29 | """ | |
28 | """A welcome page.""" | |
30 | 29 | return jsonify({"message": "Welcome, {}!".format(args["name"])}) |
31 | 30 | |
32 | 31 |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """A simple number and datetime addition JSON API. |
2 | 1 | Run the app: |
3 | 2 | |
69 | 68 | |
70 | 69 | # This error handler is necessary for usage with Flask-RESTful |
71 | 70 | @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): | |
73 | 72 | """webargs error handler that uses Flask-RESTful's abort function to return |
74 | 73 | a JSON error response to the client. |
75 | 74 | """ |
28 | 28 | @view_config(route_name="hello", request_method="GET", renderer="json") |
29 | 29 | @use_args(hello_args) |
30 | 30 | def index(request, args): |
31 | """A welcome page. | |
32 | """ | |
31 | """A welcome page.""" | |
33 | 32 | return {"message": "Welcome, {}!".format(args["name"])} |
34 | 33 | |
35 | 34 | |
77 | 76 | app = config.make_wsgi_app() |
78 | 77 | port = 5001 |
79 | 78 | server = make_server("0.0.0.0", port, app) |
80 | print("Serving on port {}".format(port)) | |
79 | print(f"Serving on port {port}") | |
81 | 80 | server.serve_forever() |
0 | python-dateutil==2.8.0 | |
0 | python-dateutil==2.8.1 | |
1 | 1 | Flask |
2 | 2 | bottle |
3 | 3 | tornado |
4 | webapp2 | |
5 | 4 | flask-restful |
6 | 5 | pyramid |
60 | 60 | |
61 | 61 | def use_schema(schema_cls, list_view=False, locations=None): |
62 | 62 | """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. | |
65 | 65 | """ |
66 | 66 | |
67 | 67 | def decorator(func): |
84 | 84 | ) |
85 | 85 | port = 5001 |
86 | 86 | app.listen(port) |
87 | print("Serving on port {}".format(port)) | |
87 | print(f"Serving on port {port}") | |
88 | 88 | tornado.ioloop.IOLoop.instance().start() |
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() |
1 | 1 | license_files = LICENSE |
2 | 2 | |
3 | 3 | [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 | |
8 | 5 | |
9 | 6 | [flake8] |
10 | 7 | ignore = E203, E266, E501, W503 |
0 | # -*- coding: utf-8 -*- | |
1 | import sys | |
2 | 0 | import re |
3 | 1 | from setuptools import setup, find_packages |
4 | 2 | |
5 | INSTALL_REQUIRES = ["marshmallow>=2.15.2"] | |
6 | if sys.version_info[0] < 3: | |
7 | INSTALL_REQUIRES.append("simplejson>=2.1.0") | |
8 | ||
9 | 3 | FRAMEWORKS = [ |
10 | "Flask>=0.12.2", | |
11 | "Django>=1.11.16", | |
4 | "Flask>=0.12.5", | |
5 | "Django>=2.2.0", | |
12 | 6 | "bottle>=0.12.13", |
13 | 7 | "tornado>=4.5.2", |
14 | 8 | "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", | |
18 | 11 | ] |
19 | 12 | EXTRAS_REQUIRE = { |
20 | 13 | "frameworks": FRAMEWORKS, |
21 | 14 | "tests": [ |
22 | 15 | "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", | |
27 | 19 | ] |
28 | 20 | + FRAMEWORKS, |
29 | 21 | "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", | |
34 | 26 | ], |
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"] | |
36 | 28 | + FRAMEWORKS, |
37 | 29 | } |
38 | 30 | EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"] |
43 | 35 | Raises RuntimeError if not found. |
44 | 36 | """ |
45 | 37 | version = "" |
46 | with open(fname, "r") as fp: | |
38 | with open(fname) as fp: | |
47 | 39 | reg = re.compile(r'__version__ = [\'"]([^\'"]*)[\'"]') |
48 | 40 | for line in fp: |
49 | 41 | m = reg.match(line) |
67 | 59 | description=( |
68 | 60 | "Declarative parsing and validation of HTTP request objects, " |
69 | 61 | "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." | |
71 | 63 | ), |
72 | 64 | long_description=read("README.rst"), |
73 | 65 | author="Steven Loria", |
75 | 67 | url="https://github.com/marshmallow-code/webargs", |
76 | 68 | packages=find_packages("src"), |
77 | 69 | package_dir={"": "src"}, |
78 | install_requires=INSTALL_REQUIRES, | |
70 | package_data={"webargs": ["py.typed"]}, | |
71 | install_requires=["marshmallow>=3.0.0"], | |
79 | 72 | extras_require=EXTRAS_REQUIRE, |
80 | 73 | license="MIT", |
81 | 74 | zip_safe=False, |
87 | 80 | "bottle", |
88 | 81 | "tornado", |
89 | 82 | "aiohttp", |
90 | "webapp2", | |
91 | 83 | "request", |
92 | 84 | "arguments", |
93 | 85 | "validation", |
96 | 88 | "api", |
97 | 89 | "marshmallow", |
98 | 90 | ), |
91 | python_requires=">=3.6", | |
99 | 92 | classifiers=[ |
100 | 93 | "Development Status :: 5 - Production/Stable", |
101 | 94 | "Intended Audience :: Developers", |
102 | 95 | "License :: OSI Approved :: MIT License", |
103 | 96 | "Natural Language :: English", |
104 | "Programming Language :: Python :: 2", | |
105 | "Programming Language :: Python :: 2.7", | |
106 | 97 | "Programming Language :: Python :: 3", |
107 | "Programming Language :: Python :: 3.5", | |
108 | 98 | "Programming Language :: Python :: 3.6", |
109 | 99 | "Programming Language :: Python :: 3.7", |
100 | "Programming Language :: Python :: 3.8", | |
101 | "Programming Language :: Python :: 3.9", | |
102 | "Programming Language :: Python :: 3 :: Only", | |
110 | 103 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", |
111 | 104 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", |
112 | 105 | ], |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | from distutils.version import LooseVersion |
2 | 1 | from marshmallow.utils import missing |
3 | 2 | |
5 | 4 | from marshmallow import validate |
6 | 5 | |
7 | 6 | from webargs.core import ValidationError |
8 | from webargs.dict2schema import dict2schema | |
9 | 7 | from webargs import fields |
10 | 8 | |
11 | __version__ = "5.5.3" | |
9 | __version__ = "7.0.1" | |
12 | 10 | __version_info__ = tuple(LooseVersion(__version__).version) |
13 | __all__ = ("dict2schema", "ValidationError", "fields", "missing", "validate") | |
11 | __all__ = ("ValidationError", "fields", "missing", "validate") |
24 | 24 | import typing |
25 | 25 | |
26 | 26 | from aiohttp import web |
27 | from aiohttp.web import Request | |
28 | 27 | from aiohttp import web_exceptions |
29 | from marshmallow import Schema, ValidationError | |
30 | from marshmallow.fields import Field | |
28 | from marshmallow import Schema, ValidationError, RAISE | |
31 | 29 | |
32 | 30 | from webargs import core |
33 | 31 | from webargs.core import json |
34 | 32 | from webargs.asyncparser import AsyncParser |
33 | from webargs.multidictproxy import MultiDictProxy | |
35 | 34 | |
36 | 35 | |
37 | def is_json_request(req: Request) -> bool: | |
36 | def is_json_request(req) -> bool: | |
38 | 37 | content_type = req.content_type |
39 | 38 | return core.is_json(content_type) |
40 | 39 | |
71 | 70 | class AIOHTTPParser(AsyncParser): |
72 | 71 | """aiohttp request argument parser.""" |
73 | 72 | |
73 | DEFAULT_UNKNOWN_BY_LOCATION = { | |
74 | "match_info": RAISE, | |
75 | "path": RAISE, | |
76 | **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION, | |
77 | } | |
74 | 78 | __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__, | |
78 | 82 | ) |
79 | 83 | |
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) | |
83 | 87 | |
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) | |
90 | 92 | |
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 == "": | |
96 | 109 | 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) | |
106 | 113 | |
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) | |
109 | 117 | |
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) | |
113 | 121 | |
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: | |
119 | 123 | 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 " | |
121 | 125 | "parsing upload data." |
122 | 126 | ) |
123 | 127 | |
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 | |
127 | 131 | |
128 | 132 | def get_request_from_view_args( |
129 | 133 | self, view: typing.Callable, args: typing.Iterable, kwargs: typing.Mapping |
130 | ) -> Request: | |
134 | ): | |
131 | 135 | """Get request object from a handler function or method. Used internally by |
132 | 136 | ``use_args`` and ``use_kwargs``. |
133 | 137 | """ |
136 | 140 | if isinstance(arg, web.Request): |
137 | 141 | req = arg |
138 | 142 | break |
139 | elif isinstance(arg, web.View): | |
143 | if isinstance(arg, web.View): | |
140 | 144 | req = arg.request |
141 | 145 | 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") | |
143 | 148 | return req |
144 | 149 | |
145 | 150 | def handle_error( |
146 | 151 | self, |
147 | 152 | error: ValidationError, |
148 | req: Request, | |
153 | req, | |
149 | 154 | 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: | |
153 | 159 | """Handle ValidationErrors and return a JSON response of error messages |
154 | 160 | to the client. |
155 | 161 | """ |
157 | 163 | error_status_code or self.DEFAULT_VALIDATION_STATUS |
158 | 164 | ) |
159 | 165 | 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}") | |
161 | 167 | headers = error_headers |
162 | 168 | raise error_class( |
163 | 169 | body=json.dumps(error.messages).encode("utf-8"), |
165 | 171 | content_type="application/json", |
166 | 172 | ) |
167 | 173 | |
168 | def handle_invalid_json_error( | |
174 | def _handle_invalid_json_error( | |
169 | 175 | self, |
170 | 176 | error: typing.Union[json.JSONDecodeError, UnicodeDecodeError], |
171 | req: Request, | |
177 | req, | |
172 | 178 | *args, |
173 | 179 | **kwargs |
174 | ) -> "typing.NoReturn": | |
180 | ) -> typing.NoReturn: | |
175 | 181 | error_class = exception_map[400] |
176 | 182 | messages = {"json": ["Invalid JSON body."]} |
177 | 183 | raise error_class( |
0 | """Asynchronous request parser. Compatible with Python>=3.5.""" | |
0 | """Asynchronous request parser.""" | |
1 | 1 | import asyncio |
2 | 2 | import functools |
3 | 3 | import inspect |
5 | 5 | from collections.abc import Mapping |
6 | 6 | |
7 | 7 | from marshmallow import Schema, ValidationError |
8 | from marshmallow.fields import Field | |
9 | 8 | import marshmallow as ma |
10 | from marshmallow.utils import missing | |
11 | 9 | |
12 | 10 | from webargs import core |
13 | 11 | |
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]] | |
17 | 13 | |
18 | 14 | |
19 | 15 | class AsyncParser(core.Parser): |
21 | 17 | either coroutines or regular methods. |
22 | 18 | """ |
23 | 19 | |
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 | ||
65 | 20 | # TODO: Lots of duplication from core.Parser here. Rethink. |
66 | 21 | async def parse( |
67 | 22 | 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]: | |
75 | 32 | """Coroutine variant of `webargs.core.Parser`. |
76 | 33 | |
77 | 34 | Receives the same arguments as `webargs.core.Parser.parse`. |
78 | 35 | """ |
79 | self.clear_cache() # in case someone used `parse_*()` | |
80 | 36 | 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") | |
82 | 52 | data = None |
83 | 53 | validators = core._ensure_list_of_callables(validate) |
84 | 54 | schema = self._get_schema(argmap, req) |
85 | 55 | 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) | |
91 | 60 | self._validate_arguments(data, validators) |
92 | 61 | 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, | |
95 | 69 | ) |
96 | 70 | return data |
97 | 71 | |
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( | |
99 | 92 | self, |
100 | 93 | error: ValidationError, |
101 | req: Request, | |
94 | req: core.Request, | |
102 | 95 | 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} | |
106 | 107 | 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 | ) | |
108 | 131 | |
109 | 132 | def use_args( |
110 | 133 | 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, | |
114 | 139 | as_kwargs: bool = False, |
115 | validate: Validate = None, | |
140 | validate: core.ValidateArg = None, | |
116 | 141 | 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 | |
118 | 143 | ) -> typing.Callable[..., typing.Callable]: |
119 | 144 | """Decorator that injects parsed arguments into a view function or method. |
120 | 145 | |
121 | 146 | Receives the same arguments as `webargs.core.Parser.use_args`. |
122 | 147 | """ |
123 | locations = locations or self.locations | |
148 | location = location or self.location | |
124 | 149 | request_obj = req |
125 | 150 | # Optimization: If argmap is passed as a dictionary, we only need |
126 | 151 | # to generate a Schema once |
127 | 152 | if isinstance(argmap, Mapping): |
128 | argmap = core.dict2schema(argmap, self.schema_class)() | |
153 | argmap = self.schema_class.from_dict(argmap)() | |
129 | 154 | |
130 | 155 | def decorator(func: typing.Callable) -> typing.Callable: |
131 | 156 | req_ = request_obj |
142 | 167 | parsed_args = await self.parse( |
143 | 168 | argmap, |
144 | 169 | req=req_obj, |
145 | locations=locations, | |
170 | location=location, | |
171 | unknown=unknown, | |
146 | 172 | validate=validate, |
147 | 173 | error_status_code=error_status_code, |
148 | 174 | error_headers=error_headers, |
149 | 175 | ) |
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) | |
157 | 180 | |
158 | 181 | else: |
159 | 182 | |
167 | 190 | parsed_args = yield from self.parse( # type: ignore |
168 | 191 | argmap, |
169 | 192 | req=req_obj, |
170 | locations=locations, | |
193 | location=location, | |
194 | unknown=unknown, | |
171 | 195 | validate=validate, |
172 | 196 | error_status_code=error_status_code, |
173 | 197 | error_headers=error_headers, |
174 | 198 | ) |
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) | |
182 | 203 | |
183 | 204 | return wrapper |
184 | 205 | |
185 | 206 | 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 -*- | |
1 | 0 | """Bottle request argument parsing module. |
2 | 1 | |
3 | 2 | Example: :: |
19 | 18 | import bottle |
20 | 19 | |
21 | 20 | from webargs import core |
22 | from webargs.core import json | |
21 | from webargs.multidictproxy import MultiDictProxy | |
23 | 22 | |
24 | 23 | |
25 | 24 | class BottleParser(core.Parser): |
26 | 25 | """Bottle.py request argument parser.""" |
27 | 26 | |
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 | ) | |
31 | 31 | |
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.""" | |
34 | 55 | # For consistency with other parsers' behavior, don't attempt to |
35 | 56 | # parse if content-type is mismatched. |
36 | 57 | # TODO: Make this check more specific |
37 | 58 | if core.is_json(req.content_type): |
38 | 59 | return core.missing |
39 | return core.get_value(req.forms, name, field) | |
60 | return MultiDictProxy(req.forms, schema) | |
40 | 61 | |
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) | |
56 | 65 | |
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 | |
60 | 69 | |
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) | |
64 | 73 | |
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): | |
74 | 75 | """Handles errors during parsing. Aborts the current request with a |
75 | 76 | 400 error. |
76 | 77 | """ |
82 | 83 | exception=error, |
83 | 84 | ) |
84 | 85 | |
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 | ||
90 | 86 | def get_default_request(self): |
91 | 87 | """Override to use bottle's thread-local request object by default.""" |
92 | 88 | return bottle.request |
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 | ||
3 | 0 | import functools |
4 | import inspect | |
1 | import typing | |
5 | 2 | 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 | |
13 | 5 | |
14 | 6 | import marshmallow as ma |
15 | 7 | 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 | ||
20 | 10 | from webargs.fields import DelimitedList |
21 | 11 | |
22 | 12 | logger = logging.getLogger(__name__) |
24 | 14 | |
25 | 15 | __all__ = [ |
26 | 16 | "ValidationError", |
27 | "dict2schema", | |
28 | 17 | "is_multiple", |
29 | 18 | "Parser", |
30 | "get_value", | |
31 | 19 | "missing", |
32 | 20 | "parse_json", |
33 | 21 | ] |
34 | 22 | |
35 | 23 | |
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]: | |
40 | 51 | """Makes sure an object is callable if it is not ``None``. If not |
41 | 52 | callable, a ValueError is raised. |
42 | 53 | """ |
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: | |
50 | 60 | """Return whether or not `field` handles repeated/multi-value arguments.""" |
51 | 61 | return isinstance(field, ma.fields.List) and not isinstance(field, DelimitedList) |
52 | 62 | |
53 | 63 | |
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() | |
56 | 66 | |
57 | 67 | |
58 | 68 | # Adapted from werkzeug: |
59 | 69 | # https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/wrappers.py |
60 | def is_json(mimetype): | |
70 | def is_json(mimetype: typing.Optional[str]) -> bool: | |
61 | 71 | """Indicates if this mimetype is JSON or not. By default a request |
62 | 72 | is considered to include JSON data if the mimetype is |
63 | 73 | ``application/json`` or ``application/*+json``. |
73 | 83 | return False |
74 | 84 | |
75 | 85 | |
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: | |
114 | 90 | try: |
115 | s = s.decode(encoding) | |
116 | except UnicodeDecodeError as e: | |
91 | decoded = s.decode(encoding) | |
92 | except UnicodeDecodeError as exc: | |
117 | 93 | 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, | |
121 | 97 | ) |
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: | |
126 | 102 | if obj: |
127 | 103 | if isinstance(obj, (list, tuple)): |
128 | validators = obj | |
104 | validators = typing.cast(CallableList, list(obj)) | |
129 | 105 | elif callable(obj): |
130 | 106 | validators = [obj] |
131 | 107 | 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.") | |
135 | 109 | else: |
136 | 110 | validators = [] |
137 | 111 | return validators |
138 | 112 | |
139 | 113 | |
140 | class Parser(object): | |
114 | class Parser: | |
141 | 115 | """Base parser class that provides high-level implementation for parsing |
142 | 116 | a request. |
143 | 117 | |
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. | |
148 | 127 | :param callable error_handler: Custom error handler function. |
149 | 128 | """ |
150 | 129 | |
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 | } | |
153 | 144 | #: The marshmallow Schema class to use when creating new schemas |
154 | DEFAULT_SCHEMA_CLASS = ma.Schema | |
145 | DEFAULT_SCHEMA_CLASS: typing.Type = ma.Schema | |
155 | 146 | #: Default status code to return for validation errors |
156 | DEFAULT_VALIDATION_STATUS = DEFAULT_VALIDATION_STATUS | |
147 | DEFAULT_VALIDATION_STATUS: int = DEFAULT_VALIDATION_STATUS | |
157 | 148 | #: Default error message for validation errors |
158 | DEFAULT_VALIDATION_MESSAGE = "Invalid value." | |
149 | DEFAULT_VALIDATION_MESSAGE: str = "Invalid value." | |
159 | 150 | |
160 | 151 | #: 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", | |
169 | 161 | } |
170 | 162 | |
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 | ) | |
174 | 175 | 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 | """ | |
185 | 183 | 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 | ||
194 | 187 | # Parsing function to call |
195 | 188 | # 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 | |
273 | 210 | |
274 | 211 | 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 | |
281 | 239 | for validator in validators: |
282 | 240 | if validator(data) is False: |
283 | 241 | msg = self.DEFAULT_VALIDATION_MESSAGE |
284 | 242 | raise ValidationError(msg, data=data) |
285 | 243 | |
286 | def _get_schema(self, argmap, req): | |
244 | def _get_schema(self, argmap: ArgMap, req: Request) -> ma.Schema: | |
287 | 245 | """Return a `marshmallow.Schema` for the given argmap and request. |
288 | 246 | |
289 | 247 | :param argmap: Either a `marshmallow.Schema`, `dict` |
299 | 257 | elif callable(argmap): |
300 | 258 | schema = argmap(req) |
301 | 259 | 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)() | |
309 | 261 | return schema |
310 | ||
311 | def _clone(self): | |
312 | clone = copy(self) | |
313 | clone.clear_cache() | |
314 | return clone | |
315 | 262 | |
316 | 263 | def parse( |
317 | 264 | 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 | |
324 | 273 | ): |
325 | 274 | """Main request parsing method. |
326 | 275 | |
328 | 277 | of argname -> `marshmallow.fields.Field` pairs, or a callable |
329 | 278 | which accepts a request and returns a `marshmallow.Schema`. |
330 | 279 | :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. | |
334 | 288 | :param callable validate: Validation function or list of validation functions |
335 | 289 | that receives the dictionary of parsed arguments. Validator either returns a |
336 | 290 | boolean or raises a :exc:`ValidationError`. |
341 | 295 | |
342 | 296 | :return: A dictionary of parsed arguments |
343 | 297 | """ |
344 | self.clear_cache() # in case someone used `parse_*()` | |
345 | 298 | 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") | |
347 | 315 | data = None |
348 | 316 | validators = _ensure_list_of_callables(validate) |
349 | parser = self._clone() | |
350 | 317 | schema = self._get_schema(argmap, req) |
351 | 318 | 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 | |
354 | 321 | ) |
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) | |
358 | 324 | 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, | |
361 | 332 | ) |
333 | raise ValueError( | |
334 | "_on_validation_error hook did not raise an exception" | |
335 | ) from error | |
362 | 336 | return data |
363 | 337 | |
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]: | |
376 | 339 | """Optional override. Provides a hook for frameworks that use thread-local |
377 | 340 | request objects. |
378 | 341 | """ |
379 | 342 | return None |
380 | 343 | |
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]: | |
382 | 350 | """Optional override. Returns the request object to be parsed, given a view |
383 | 351 | function's args and kwargs. |
384 | 352 | |
392 | 360 | """ |
393 | 361 | return None |
394 | 362 | |
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 | ||
395 | 378 | def use_args( |
396 | 379 | 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]: | |
405 | 390 | """Decorator that injects parsed arguments into a view function or method. |
406 | 391 | |
407 | 392 | Example usage with Flask: :: |
408 | 393 | |
409 | 394 | @app.route('/echo', methods=['get', 'post']) |
410 | @parser.use_args({'name': fields.Str()}) | |
395 | @parser.use_args({'name': fields.Str()}, location="querystring") | |
411 | 396 | def greet(args): |
412 | 397 | return 'Hello ' + args['name'] |
413 | 398 | |
414 | 399 | :param argmap: Either a `marshmallow.Schema`, a `dict` |
415 | 400 | of argname -> `marshmallow.fields.Field` pairs, or a callable |
416 | 401 | 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. | |
418 | 405 | :param bool as_kwargs: Whether to insert arguments as keyword arguments. |
419 | 406 | :param callable validate: Validation function that receives the dictionary |
420 | 407 | of parsed arguments. If the function returns ``False``, the parser |
424 | 411 | :param dict error_headers: Headers passed to error handler functions when a |
425 | 412 | a `ValidationError` is raised. |
426 | 413 | """ |
427 | locations = locations or self.locations | |
414 | location = location or self.location | |
428 | 415 | request_obj = req |
429 | 416 | # Optimization: If argmap is passed as a dictionary, we only need |
430 | 417 | # to generate a Schema once |
431 | 418 | if isinstance(argmap, Mapping): |
432 | argmap = dict2schema(argmap, self.schema_class)() | |
419 | argmap = self.schema_class.from_dict(argmap)() | |
433 | 420 | |
434 | 421 | def decorator(func): |
435 | 422 | req_ = request_obj |
440 | 427 | |
441 | 428 | if not req_obj: |
442 | 429 | req_obj = self.get_request_from_view_args(func, args, kwargs) |
430 | ||
443 | 431 | # NOTE: At this point, argmap may be a Schema, or a callable |
444 | 432 | parsed_args = self.parse( |
445 | 433 | argmap, |
446 | 434 | req=req_obj, |
447 | locations=locations, | |
435 | location=location, | |
436 | unknown=unknown, | |
448 | 437 | validate=validate, |
449 | 438 | error_status_code=error_status_code, |
450 | 439 | error_headers=error_headers, |
451 | 440 | ) |
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) | |
459 | 445 | |
460 | 446 | wrapper.__wrapped__ = func |
461 | 447 | return wrapper |
462 | 448 | |
463 | 449 | return decorator |
464 | 450 | |
465 | def use_kwargs(self, *args, **kwargs): | |
451 | def use_kwargs(self, *args, **kwargs) -> typing.Callable: | |
466 | 452 | """Decorator that injects parsed arguments into a view function or method |
467 | 453 | as keyword arguments. |
468 | 454 | |
480 | 466 | kwargs["as_kwargs"] = True |
481 | 467 | return self.use_args(*args, **kwargs) |
482 | 468 | |
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. | |
487 | 477 | |
488 | 478 | Example: :: |
489 | 479 | |
490 | 480 | from webargs import core |
491 | 481 | parser = core.Parser() |
492 | 482 | |
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 | |
496 | 486 | |
497 | 487 | :param str name: The name of the location to register. |
498 | 488 | """ |
503 | 493 | |
504 | 494 | return decorator |
505 | 495 | |
506 | def error_handler(self, func): | |
496 | def error_handler(self, func: ErrorHandler) -> ErrorHandler: | |
507 | 497 | """Decorator that registers a custom error handling function. The |
508 | 498 | function should receive the raised error, request object, |
509 | 499 | `marshmallow.Schema` instance used to parse the request, error status code, |
522 | 512 | |
523 | 513 | |
524 | 514 | @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): | |
526 | 516 | raise CustomError(error.messages) |
527 | 517 | |
528 | 518 | :param callable func: The error callback to register. |
530 | 520 | self.error_callback = func |
531 | 521 | return func |
532 | 522 | |
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 | ||
533 | 569 | # Abstract Methods |
534 | 570 | |
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. | |
538 | 584 | """ |
539 | 585 | return missing |
540 | 586 | |
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. | |
544 | 590 | """ |
545 | 591 | return missing |
546 | 592 | |
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. | |
550 | 596 | """ |
551 | 597 | return missing |
552 | 598 | |
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.""" | |
557 | 601 | return missing |
558 | 602 | |
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. | |
562 | 606 | """ |
563 | 607 | return missing |
564 | 608 | |
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. | |
568 | 612 | """ |
569 | 613 | return missing |
570 | 614 | |
571 | 615 | 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: | |
574 | 624 | """Called if an error occurs while parsing args. By default, just logs and |
575 | 625 | raises ``error``. |
576 | 626 | """ |
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 -*- | |
1 | 0 | """Django request argument parsing. |
2 | 1 | |
3 | 2 | Example usage: :: |
18 | 17 | return HttpResponse('Hello ' + args['name']) |
19 | 18 | """ |
20 | 19 | 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) | |
22 | 25 | |
23 | 26 | |
24 | 27 | class DjangoParser(core.Parser): |
32 | 35 | the parser and returning the appropriate `HTTPResponse`. |
33 | 36 | """ |
34 | 37 | |
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 | |
38 | 40 | |
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 | |
42 | 45 | |
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) | |
49 | 47 | |
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) | |
60 | 51 | |
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) | |
64 | 55 | |
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 | |
69 | 59 | |
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) | |
73 | 69 | |
74 | 70 | def get_request_from_view_args(self, view, args, kwargs): |
75 | 71 | # The first argument is either `self` or `request` |
78 | 74 | except AttributeError: # first arg is request |
79 | 75 | return args[0] |
80 | 76 | |
81 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
82 | raise error | |
83 | ||
84 | 77 | |
85 | 78 | parser = DjangoParser() |
86 | 79 | use_args = parser.use_args |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """Falcon request argument parsing module. |
2 | 1 | """ |
3 | 2 | import falcon |
4 | 3 | from falcon.util.uri import parse_query_string |
5 | 4 | |
5 | import marshmallow as ma | |
6 | ||
6 | 7 | from webargs import core |
7 | from webargs.core import json | |
8 | from webargs.multidictproxy import MultiDictProxy | |
8 | 9 | |
9 | 10 | HTTP_422 = "422 Unprocessable Entity" |
10 | 11 | |
29 | 30 | return content_type and core.is_json(content_type) |
30 | 31 | |
31 | 32 | |
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 | ||
49 | 33 | # NOTE: Adapted from falcon.request.Request._parse_form_urlencoded |
50 | 34 | def parse_form_body(req): |
51 | 35 | if ( |
52 | 36 | req.content_type is not None |
53 | 37 | and "application/x-www-form-urlencoded" in req.content_type |
54 | 38 | ): |
55 | body = req.stream.read() | |
39 | body = req.stream.read(req.content_length or 0) | |
56 | 40 | try: |
57 | 41 | body = body.decode("ascii") |
58 | 42 | except UnicodeDecodeError: |
65 | 49 | ) |
66 | 50 | |
67 | 51 | 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 | |
72 | 55 | |
73 | 56 | |
74 | 57 | 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.""" | |
77 | 59 | |
78 | 60 | def __init__(self, status, errors, *args, **kwargs): |
79 | 61 | self.errors = errors |
80 | super(HTTPError, self).__init__(status, *args, **kwargs) | |
62 | super().__init__(status, *args, **kwargs) | |
81 | 63 | |
82 | 64 | def to_dict(self, *args, **kwargs): |
83 | 65 | """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) | |
85 | 67 | if self.errors is not None: |
86 | 68 | ret["errors"] = self.errors |
87 | 69 | return ret |
88 | 70 | |
89 | 71 | |
90 | 72 | class FalconParser(core.Parser): |
91 | """Falcon request argument parser.""" | |
73 | """Falcon request argument parser. | |
92 | 74 | |
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.""" | |
96 | 77 | |
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 | |
99 | 103 | |
100 | 104 | .. note:: |
101 | 105 | |
102 | 106 | The request stream will be read and left at EOF. |
103 | 107 | """ |
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) | |
108 | 112 | |
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_ | |
111 | 122 | |
112 | 123 | .. note:: |
113 | 124 | |
114 | 125 | The request stream will be read and left at EOF. |
115 | 126 | """ |
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 | |
123 | 131 | |
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 | |
128 | 134 | |
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 | |
135 | 156 | |
136 | 157 | def get_request_from_view_args(self, view, args, kwargs): |
137 | 158 | """Get request from a resource method's arguments. Assumes that |
138 | 159 | request is the second argument. |
139 | 160 | """ |
140 | 161 | 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") | |
142 | 164 | return req |
143 | 165 | |
144 | def parse_files(self, req, name, field): | |
166 | def load_files(self, req, schema): | |
145 | 167 | 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__}" | |
147 | 169 | ) |
148 | 170 | |
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): | |
150 | 172 | """Handles errors during parsing.""" |
151 | 173 | status = status_map.get(error_status_code or self.DEFAULT_VALIDATION_STATUS) |
152 | 174 | 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") | |
154 | 176 | raise HTTPError(status, errors=error.messages, headers=error_headers) |
155 | 177 | |
156 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
178 | def _handle_invalid_json_error(self, error, req, *args, **kwargs): | |
157 | 179 | status = status_map[400] |
158 | 180 | messages = {"json": ["Invalid JSON body."]} |
159 | 181 | raise HTTPError(status, errors=messages) |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """Field classes. |
2 | 1 | |
3 | 2 | Includes all fields from `marshmallow.fields` in addition to a custom |
9 | 8 | .. code-block:: python |
10 | 9 | |
11 | 10 | args = { |
12 | "active": fields.Bool(location='query'), | |
11 | "active": fields.Bool(location="query"), | |
13 | 12 | "content_type": fields.Str(data_key="Content-Type", location="headers"), |
14 | 13 | } |
14 | """ | |
15 | import typing | |
15 | 16 | |
16 | Note: `data_key` replaced `load_from` in marshmallow 3. | |
17 | When using marshmallow 2, use `load_from`. | |
18 | """ | |
19 | 17 | import marshmallow as ma |
20 | 18 | |
21 | 19 | # Expose all fields from marshmallow.fields. |
22 | 20 | from marshmallow.fields import * # noqa: F40 |
23 | from webargs.compat import MARSHMALLOW_VERSION_INFO | |
24 | from webargs.dict2schema import dict2schema | |
25 | 21 | |
26 | 22 | __all__ = ["DelimitedList"] + ma.fields.__all__ |
27 | 23 | |
28 | 24 | |
29 | class Nested(ma.fields.Nested): | |
25 | class Nested(ma.fields.Nested): # type: ignore[no-redef] | |
30 | 26 | """Same as `marshmallow.fields.Nested`, except can be passed a dictionary as |
31 | 27 | the first argument, which will be converted to a `marshmallow.Schema`. |
32 | 28 | |
39 | 35 | |
40 | 36 | def __init__(self, nested, *args, **kwargs): |
41 | 37 | 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) | |
44 | 40 | |
45 | 41 | |
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. | |
49 | 78 | |
50 | 79 | :param Field cls_or_instance: A field class or instance. |
51 | 80 | :param str delimiter: Delimiter between values. |
52 | :param bool as_string: Dump values to string. | |
53 | 81 | """ |
54 | 82 | |
55 | delimiter = "," | |
83 | default_error_messages = {"invalid": "Not a valid delimited list."} | |
56 | 84 | |
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 | ): | |
58 | 92 | 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) | |
61 | 94 | |
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 | |
67 | 95 | |
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 -*- | |
1 | 0 | """Flask request argument parsing module. |
2 | 1 | |
3 | 2 | Example: :: |
9 | 8 | |
10 | 9 | app = Flask(__name__) |
11 | 10 | |
12 | hello_args = { | |
13 | 'name': fields.Str(required=True) | |
11 | user_detail_args = { | |
12 | 'per_page': fields.Int() | |
14 | 13 | } |
15 | 14 | |
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 | ) | |
20 | 21 | """ |
21 | 22 | import flask |
22 | 23 | from werkzeug.exceptions import HTTPException |
23 | 24 | |
25 | import marshmallow as ma | |
26 | ||
24 | 27 | from webargs import core |
25 | from webargs.core import json | |
28 | from webargs.multidictproxy import MultiDictProxy | |
26 | 29 | |
27 | 30 | |
28 | 31 | def abort(http_status_code, exc=None, **kwargs): |
46 | 49 | class FlaskParser(core.Parser): |
47 | 50 | """Flask request argument parser.""" |
48 | 51 | |
52 | DEFAULT_UNKNOWN_BY_LOCATION = { | |
53 | "view_args": ma.RAISE, | |
54 | "path": ma.RAISE, | |
55 | **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION, | |
56 | } | |
49 | 57 | __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__, | |
53 | 61 | ) |
54 | 62 | |
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 | |
58 | 65 | |
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 | |
65 | 70 | |
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)) | |
78 | 72 | |
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."]}) | |
82 | 75 | |
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 | |
90 | 79 | |
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) | |
94 | 83 | |
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) | |
98 | 87 | |
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) | |
102 | 91 | |
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): | |
104 | 101 | """Handles errors during parsing. Aborts the current HTTP request and |
105 | 102 | responds with a 422 error. |
106 | 103 | """ |
113 | 110 | headers=error_headers, |
114 | 111 | ) |
115 | 112 | |
116 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
117 | abort(400, exc=error, messages={"json": ["Invalid JSON body."]}) | |
118 | ||
119 | 113 | 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""" | |
121 | 115 | return flask.request |
122 | 116 | |
123 | 117 |
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 |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """Pyramid request argument parsing. |
2 | 1 | |
3 | 2 | Example usage: :: |
24 | 23 | server = make_server('0.0.0.0', 6543, app) |
25 | 24 | server.serve_forever() |
26 | 25 | """ |
27 | import collections | |
28 | 26 | import functools |
27 | from collections.abc import Mapping | |
29 | 28 | |
30 | 29 | from webob.multidict import MultiDict |
31 | 30 | from pyramid.httpexceptions import exception_response |
32 | 31 | |
32 | import marshmallow as ma | |
33 | ||
33 | 34 | from webargs import core |
34 | 35 | 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")) | |
36 | 41 | |
37 | 42 | |
38 | 43 | class PyramidParser(core.Parser): |
39 | 44 | """Pyramid request argument parser.""" |
40 | 45 | |
46 | DEFAULT_UNKNOWN_BY_LOCATION = { | |
47 | "matchdict": ma.RAISE, | |
48 | "path": ma.RAISE, | |
49 | **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION, | |
50 | } | |
41 | 51 | __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__, | |
45 | 55 | ) |
46 | 56 | |
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 | |
50 | 59 | |
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 | |
54 | 64 | |
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) | |
61 | 66 | |
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) | |
72 | 70 | |
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) | |
76 | 74 | |
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) | |
80 | 78 | |
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.""" | |
83 | 85 | 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) | |
85 | 87 | |
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) | |
89 | 91 | |
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): | |
91 | 93 | """Handles errors during parsing. Aborts the current HTTP request and |
92 | 94 | responds with a 400 error. |
93 | 95 | """ |
94 | 96 | status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS |
95 | 97 | response = exception_response( |
96 | 98 | status_code, |
97 | detail=text_type(error), | |
99 | detail=str(error), | |
98 | 100 | headers=error_headers, |
99 | 101 | content_type="application/json", |
100 | 102 | ) |
101 | 103 | 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 | |
103 | 105 | raise response |
104 | 106 | |
105 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
107 | def _handle_invalid_json_error(self, error, req, *args, **kwargs): | |
106 | 108 | messages = {"json": ["Invalid JSON body."]} |
107 | 109 | response = exception_response( |
108 | 400, detail=text_type(messages), content_type="application/json" | |
110 | 400, detail=str(messages), content_type="application/json" | |
109 | 111 | ) |
110 | 112 | 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 | |
112 | 114 | raise response |
113 | 115 | |
114 | 116 | def use_args( |
115 | 117 | self, |
116 | 118 | argmap, |
117 | 119 | req=None, |
118 | locations=core.Parser.DEFAULT_LOCATIONS, | |
120 | *, | |
121 | location=core.Parser.DEFAULT_LOCATION, | |
122 | unknown=None, | |
119 | 123 | as_kwargs=False, |
120 | 124 | validate=None, |
121 | 125 | error_status_code=None, |
122 | error_headers=None, | |
126 | error_headers=None | |
123 | 127 | ): |
124 | 128 | """Decorator that injects parsed arguments into a view callable. |
125 | 129 | Supports the *Class-based View* pattern where `request` is saved as an instance |
129 | 133 | of argname -> `marshmallow.fields.Field` pairs, or a callable |
130 | 134 | which accepts a request and returns a `marshmallow.Schema`. |
131 | 135 | :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. | |
133 | 139 | :param bool as_kwargs: Whether to insert arguments as keyword arguments. |
134 | 140 | :param callable validate: Validation function that receives the dictionary |
135 | 141 | of parsed arguments. If the function returns ``False``, the parser |
139 | 145 | :param dict error_headers: Headers passed to error handler functions when a |
140 | 146 | a `ValidationError` is raised. |
141 | 147 | """ |
142 | locations = locations or self.locations | |
148 | location = location or self.location | |
143 | 149 | # Optimization: If argmap is passed as a dictionary, we only need |
144 | 150 | # 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)() | |
147 | 153 | |
148 | 154 | def decorator(func): |
149 | 155 | @functools.wraps(func) |
157 | 163 | parsed_args = self.parse( |
158 | 164 | argmap, |
159 | 165 | req=request, |
160 | locations=locations, | |
166 | location=location, | |
167 | unknown=unknown, | |
161 | 168 | validate=validate, |
162 | 169 | error_status_code=error_status_code, |
163 | 170 | error_headers=error_headers, |
164 | 171 | ) |
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) | |
170 | 176 | |
171 | 177 | wrapper.__wrapped__ = func |
172 | 178 | return wrapper |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """Utilities for testing. Includes a base test class |
2 | 1 | for testing parsers. |
3 | 2 | |
12 | 11 | from webargs.core import json |
13 | 12 | |
14 | 13 | |
15 | class CommonTestCase(object): | |
14 | class CommonTestCase: | |
16 | 15 | """Base test class that defines test methods for common functionality across all |
17 | 16 | parsers. Subclasses must define `create_app`, which returns a WSGI-like app. |
18 | 17 | """ |
39 | 38 | def test_parse_querystring_args(self, testapp): |
40 | 39 | assert testapp.get("/echo?name=Fred").json == {"name": "Fred"} |
41 | 40 | |
42 | def test_parse_querystring_with_query_location_specified(self, testapp): | |
43 | assert testapp.get("/echo_query?name=Steve").json == {"name": "Steve"} | |
44 | ||
45 | 41 | 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"} | |
47 | 43 | |
48 | 44 | 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"} | |
50 | 60 | |
51 | 61 | def test_parse_querystring_default(self, testapp): |
52 | 62 | assert testapp.get("/echo").json == {"name": "World"} |
53 | 63 | |
54 | def test_parse_json_default(self, testapp): | |
55 | assert testapp.post_json("/echo", {}).json == {"name": "World"} | |
56 | ||
57 | 64 | def test_parse_json_with_charset(self, testapp): |
58 | 65 | res = testapp.post( |
59 | "/echo", | |
66 | "/echo_json", | |
60 | 67 | json.dumps({"name": "Steve"}), |
61 | 68 | content_type="application/json;charset=UTF-8", |
62 | 69 | ) |
64 | 71 | |
65 | 72 | def test_parse_json_with_vendor_media_type(self, testapp): |
66 | 73 | res = testapp.post( |
67 | "/echo", | |
74 | "/echo_json", | |
68 | 75 | json.dumps({"name": "Steve"}), |
69 | 76 | content_type="application/vnd.api+json;charset=UTF-8", |
70 | 77 | ) |
71 | 78 | assert res.json == {"name": "Steve"} |
72 | 79 | |
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 | |
84 | 95 | |
85 | 96 | def test_parse_json_many_schema_invalid_input(self, testapp): |
86 | 97 | res = testapp.post_json( |
92 | 103 | res = testapp.post_json("/echo_many_schema", [{"name": "Steve"}]).json |
93 | 104 | assert res == [{"name": "Steve"}] |
94 | 105 | |
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 | |
97 | 111 | |
98 | 112 | def test_parsing_form_default(self, testapp): |
99 | assert testapp.post("/echo", {}).json == {"name": "World"} | |
113 | assert testapp.post("/echo_form", {}).json == {"name": "World"} | |
100 | 114 | |
101 | 115 | def test_parse_querystring_multiple(self, testapp): |
102 | 116 | expected = {"name": ["steve", "Loria"]} |
103 | 117 | assert testapp.get("/echo_multi?name=steve&name=Loria").json == expected |
104 | 118 | |
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 | ||
105 | 127 | def test_parse_form_multiple(self, testapp): |
106 | 128 | expected = {"name": ["steve", "Loria"]} |
107 | 129 | assert ( |
108 | testapp.post("/echo_multi", {"name": ["steve", "Loria"]}).json == expected | |
130 | testapp.post("/echo_multi_form", {"name": ["steve", "Loria"]}).json | |
131 | == expected | |
109 | 132 | ) |
110 | 133 | |
111 | 134 | def test_parse_json_list(self, testapp): |
112 | 135 | 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 | |
114 | 145 | |
115 | 146 | 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} | |
118 | 149 | |
119 | 150 | # https://github.com/marshmallow-code/webargs/issues/427 |
120 | 151 | def test_parse_json_with_nonutf8_chars(self, testapp): |
121 | 152 | res = testapp.post( |
122 | "/echo", | |
153 | "/echo_json", | |
123 | 154 | b"\xfe", |
124 | 155 | headers={"Accept": "application/json", "Content-Type": "application/json"}, |
125 | 156 | expect_errors=True, |
129 | 160 | assert res.json == {"json": ["Invalid JSON body."]} |
130 | 161 | |
131 | 162 | 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) | |
133 | 164 | assert res.status_code == 422 |
134 | 165 | |
135 | 166 | def test_user_validation_error_returns_422_response_by_default(self, testapp): |
186 | 217 | res = testapp.post_json("/echo_nested_many", in_data) |
187 | 218 | assert res.json == {} |
188 | 219 | |
189 | def test_parse_json_if_no_json(self, testapp): | |
190 | res = testapp.post("/echo") | |
191 | assert res.json == {"name": "World"} | |
192 | ||
193 | 220 | def test_parse_files(self, testapp): |
194 | 221 | res = testapp.post( |
195 | 222 | "/echo_file", {"myfile": webtest.Upload("README.rst", b"data")} |
198 | 225 | |
199 | 226 | # https://github.com/sloria/webargs/pull/297 |
200 | 227 | 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", | |
203 | 236 | "", |
204 | 237 | headers={"Accept": "application/json", "Content-Type": "application/json"}, |
205 | 238 | ) |
209 | 242 | # https://github.com/sloria/webargs/issues/329 |
210 | 243 | def test_invalid_json(self, testapp): |
211 | 244 | res = testapp.post( |
212 | "/echo", | |
245 | "/echo_json", | |
213 | 246 | '{"foo": "bar", }', |
214 | 247 | headers={"Accept": "application/json", "Content-Type": "application/json"}, |
215 | 248 | expect_errors=True, |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """Tornado request argument parsing module. |
2 | 1 | |
3 | 2 | Example: :: |
14 | 13 | self.write(response) |
15 | 14 | """ |
16 | 15 | import tornado.web |
16 | import tornado.concurrent | |
17 | 17 | from tornado.escape import _unicode |
18 | 18 | |
19 | 19 | from webargs import core |
20 | from webargs.compat import basestring | |
21 | from webargs.core import json | |
20 | from webargs.multidictproxy import MultiDictProxy | |
22 | 21 | |
23 | 22 | |
24 | 23 | class HTTPError(tornado.web.HTTPError): |
27 | 26 | def __init__(self, *args, **kwargs): |
28 | 27 | self.messages = kwargs.pop("messages", {}) |
29 | 28 | self.headers = kwargs.pop("headers", None) |
30 | super(HTTPError, self).__init__(*args, **kwargs) | |
29 | super().__init__(*args, **kwargs) | |
31 | 30 | |
32 | 31 | |
33 | def parse_json_body(req): | |
34 | """Return the decoded JSON body from the request.""" | |
32 | def is_json_request(req): | |
35 | 33 | 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) | |
47 | 35 | |
48 | 36 | |
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): | |
52 | 38 | """ |
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])) | |
57 | 61 | |
58 | 62 | |
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 | """ | |
61 | 69 | |
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 | |
79 | 77 | |
80 | 78 | |
81 | 79 | class TornadoParser(core.Parser): |
82 | 80 | """Tornado request argument parser.""" |
83 | 81 | |
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 | |
95 | 84 | |
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 | |
99 | 89 | |
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 | |
103 | 94 | |
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) | |
107 | 96 | |
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) | |
111 | 100 | |
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) | |
116 | 104 | |
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) | |
120 | 108 | |
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): | |
122 | 120 | """Handles errors during parsing. Raises a `tornado.web.HTTPError` |
123 | 121 | with a 400 error. |
124 | 122 | """ |
135 | 133 | headers=error_headers, |
136 | 134 | ) |
137 | 135 | |
138 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
136 | def _handle_invalid_json_error(self, error, req, *args, **kwargs): | |
139 | 137 | raise HTTPError( |
140 | 138 | 400, |
141 | 139 | log_message="Invalid JSON body.", |
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 | ||
2 | 0 | import aiohttp |
3 | 1 | from aiohttp.web import json_response |
4 | from aiohttp import web | |
5 | 2 | import marshmallow as ma |
6 | 3 | |
7 | 4 | from webargs import fields |
8 | 5 | from webargs.aiohttpparser import parser, use_args, use_kwargs |
9 | from webargs.core import MARSHMALLOW_VERSION_INFO, json | |
6 | from webargs.core import json | |
10 | 7 | |
11 | 8 | hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)} |
12 | 9 | hello_multiple = {"name": fields.List(fields.Str())} |
15 | 12 | class HelloSchema(ma.Schema): |
16 | 13 | name = fields.Str(missing="World", validate=lambda n: len(n) >= 3) |
17 | 14 | |
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 | ||
26 | 21 | |
27 | 22 | ##### Handlers ##### |
28 | 23 | |
29 | 24 | |
30 | 25 | 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): | |
31 | 36 | try: |
32 | parsed = await parser.parse(hello_args, request) | |
37 | parsed = await parser.parse(hello_args, request, location="json") | |
33 | 38 | except json.JSONDecodeError: |
34 | raise web.HTTPBadRequest( | |
39 | raise aiohttp.web.HTTPBadRequest( | |
35 | 40 | body=json.dumps(["Invalid JSON."]).encode("utf-8"), |
36 | 41 | content_type="application/json", |
37 | 42 | ) |
38 | 43 | return json_response(parsed) |
39 | 44 | |
40 | 45 | |
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") | |
57 | 58 | async def echo_use_args(request, args): |
58 | 59 | return json_response(args) |
59 | 60 | |
60 | 61 | |
61 | @use_kwargs(hello_args) | |
62 | @use_kwargs(hello_args, location="query") | |
62 | 63 | async def echo_use_kwargs(request, name): |
63 | 64 | return json_response({"name": name}) |
64 | 65 | |
65 | 66 | |
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 | ) | |
67 | 70 | async def echo_use_args_validated(request, args): |
68 | 71 | return json_response(args) |
69 | 72 | |
70 | 73 | |
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 | ||
71 | 80 | 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): | |
72 | 91 | parsed = await parser.parse(hello_multiple, request) |
73 | 92 | return json_response(parsed) |
74 | 93 | |
75 | 94 | |
76 | 95 | 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") | |
82 | 101 | async def echo_use_args_with_path_param(request, args): |
83 | 102 | return json_response(args) |
84 | 103 | |
85 | 104 | |
86 | @use_kwargs({"value": fields.Int()}) | |
105 | @use_kwargs({"value": fields.Int()}, location="query") | |
87 | 106 | async def echo_use_kwargs_with_path_param(request, value): |
88 | 107 | return json_response({"value": value}) |
89 | 108 | |
90 | 109 | |
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()}) | |
93 | 112 | async def echo_use_args_multiple(request, query_parsed, json_parsed): |
94 | 113 | return json_response({"query_parsed": query_parsed, "json_parsed": json_parsed}) |
95 | 114 | |
104 | 123 | |
105 | 124 | |
106 | 125 | 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") | |
108 | 127 | return json_response(parsed) |
109 | 128 | |
110 | 129 | |
111 | 130 | 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") | |
113 | 132 | return json_response(parsed) |
114 | 133 | |
115 | 134 | |
134 | 153 | |
135 | 154 | |
136 | 155 | 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") | |
139 | 158 | } |
140 | args = {"x_field": fields.Nested({"id": fields.Int()}, many=True, **data_key_kwarg)} | |
141 | 159 | parsed = await parser.parse(args, request) |
142 | 160 | return json_response(parsed) |
143 | 161 | |
144 | 162 | |
145 | 163 | 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 | ) | |
147 | 167 | return json_response(parsed) |
148 | 168 | |
149 | 169 | |
150 | 170 | class EchoHandler: |
151 | @use_args(hello_args) | |
171 | @use_args(hello_args, location="query") | |
152 | 172 | async def get(self, request, args): |
153 | 173 | return json_response(args) |
154 | 174 | |
155 | 175 | |
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): | |
160 | 179 | return json_response(args) |
161 | 180 | |
162 | 181 | |
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): | |
166 | 184 | return json_response({"name": name}) |
167 | 185 | |
168 | 186 | |
177 | 195 | def create_app(): |
178 | 196 | app = aiohttp.web.Application() |
179 | 197 | |
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) | |
182 | 200 | 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) | |
188 | 209 | add_route(app, ["GET", "POST"], "/echo_many_schema", echo_many_schema) |
189 | 210 | add_route( |
190 | 211 | app, |
0 | from webargs.core import json | |
1 | 0 | from bottle import Bottle, HTTPResponse, debug, request, response |
2 | 1 | |
3 | 2 | import marshmallow as ma |
4 | 3 | from webargs import fields |
5 | 4 | from webargs.bottleparser import parser, use_args, use_kwargs |
6 | from webargs.core import MARSHMALLOW_VERSION_INFO | |
5 | from webargs.core import json | |
6 | ||
7 | 7 | |
8 | 8 | hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)} |
9 | 9 | hello_multiple = {"name": fields.List(fields.Str())} |
13 | 13 | name = fields.Str(missing="World", validate=lambda n: len(n) >= 3) |
14 | 14 | |
15 | 15 | |
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) | |
18 | 20 | |
19 | 21 | |
20 | 22 | app = Bottle() |
21 | 23 | debug(True) |
22 | 24 | |
23 | 25 | |
24 | @app.route("/echo", method=["GET", "POST"]) | |
26 | @app.route("/echo", method=["GET"]) | |
25 | 27 | def echo(): |
26 | return parser.parse(hello_args, request) | |
28 | return parser.parse(hello_args, request, location="query") | |
27 | 29 | |
28 | 30 | |
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") | |
32 | 34 | |
33 | 35 | |
34 | 36 | @app.route("/echo_json", method=["POST"]) |
35 | 37 | def echo_json(): |
36 | return parser.parse(hello_args, request, locations=("json",)) | |
38 | return parser.parse(hello_args, location="json") | |
37 | 39 | |
38 | 40 | |
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") | |
42 | 44 | |
43 | 45 | |
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") | |
46 | 48 | def echo_use_args(args): |
47 | 49 | return args |
48 | 50 | |
49 | 51 | |
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 | ||
55 | 52 | @app.route( |
56 | 53 | "/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 | ), | |
59 | 60 | ) |
60 | 61 | def echo_use_args_validated(args): |
61 | 62 | return args |
62 | 63 | |
63 | 64 | |
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) | |
67 | 68 | |
68 | 69 | |
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"]) | |
70 | 93 | def echo_many_schema(): |
71 | arguments = parser.parse(hello_many_schema, request, locations=("json",)) | |
94 | arguments = parser.parse(hello_many_schema, request) | |
72 | 95 | return HTTPResponse(body=json.dumps(arguments), content_type="application/json") |
73 | 96 | |
74 | 97 | |
75 | 98 | @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"), | |
77 | 101 | ) |
78 | 102 | def echo_use_args_with_path_param(args, name): |
79 | 103 | return args |
80 | 104 | |
81 | 105 | |
82 | 106 | @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"), | |
84 | 109 | ) |
85 | 110 | def echo_use_kwargs_with_path_param(name, value): |
86 | 111 | return {"value": value} |
97 | 122 | |
98 | 123 | @app.route("/echo_headers") |
99 | 124 | def echo_headers(): |
100 | return parser.parse(hello_args, request, locations=("headers",)) | |
125 | return parser.parse(hello_args, request, location="headers") | |
101 | 126 | |
102 | 127 | |
103 | 128 | @app.route("/echo_cookie") |
104 | 129 | def echo_cookie(): |
105 | return parser.parse(hello_args, request, locations=("cookies",)) | |
130 | return parser.parse(hello_args, request, location="cookies") | |
106 | 131 | |
107 | 132 | |
108 | 133 | @app.route("/echo_file", method=["POST"]) |
109 | 134 | def echo_file(): |
110 | 135 | args = {"myfile": fields.Field()} |
111 | result = parser.parse(args, locations=("files",)) | |
136 | result = parser.parse(args, location="files") | |
112 | 137 | myfile = result["myfile"] |
113 | 138 | content = myfile.file.read().decode("utf8") |
114 | 139 | return {"myfile": content} |
6 | 6 | |
7 | 7 | TEMPLATE_DEBUG = True |
8 | 8 | |
9 | ALLOWED_HOSTS = [] | |
9 | ALLOWED_HOSTS = ["*"] | |
10 | 10 | # Application definition |
11 | 11 | |
12 | 12 | INSTALLED_APPS = ("django.contrib.contenttypes",) |
1 | 1 | |
2 | 2 | from tests.apps.django_app.echo import views |
3 | 3 | |
4 | ||
4 | 5 | urlpatterns = [ |
5 | 6 | url(r"^echo$", views.echo), |
6 | url(r"^echo_query$", views.echo_query), | |
7 | url(r"^echo_form$", views.echo_form), | |
7 | 8 | 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), | |
9 | 10 | 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), | |
10 | 13 | url(r"^echo_use_kwargs$", views.echo_use_kwargs), |
11 | 14 | 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), | |
12 | 17 | url(r"^echo_many_schema$", views.echo_many_schema), |
13 | 18 | url( |
14 | 19 | r"^echo_use_args_with_path_param/(?P<name>\w+)$", |
0 | from webargs.core import json | |
1 | 0 | from django.http import HttpResponse |
2 | 1 | from django.views.generic import View |
2 | import marshmallow as ma | |
3 | 3 | |
4 | import marshmallow as ma | |
5 | 4 | from webargs import fields |
6 | 5 | from webargs.djangoparser import parser, use_args, use_kwargs |
7 | from webargs.core import MARSHMALLOW_VERSION_INFO | |
6 | from webargs.core import json | |
7 | ||
8 | 8 | |
9 | 9 | hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)} |
10 | 10 | hello_multiple = {"name": fields.List(fields.Str())} |
14 | 14 | name = fields.Str(missing="World", validate=lambda n: len(n) >= 3) |
15 | 15 | |
16 | 16 | |
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) | |
19 | 21 | |
20 | 22 | |
21 | 23 | def json_response(data, **kwargs): |
22 | 24 | return HttpResponse(json.dumps(data), content_type="application/json", **kwargs) |
23 | 25 | |
24 | 26 | |
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 | |
33 | 37 | |
34 | 38 | |
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")) | |
37 | 42 | |
38 | 43 | |
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")) | |
41 | 47 | |
42 | 48 | |
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")) | |
45 | 52 | |
46 | 53 | |
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") | |
48 | 61 | def echo_use_args(request, args): |
49 | 62 | return json_response(args) |
50 | 63 | |
51 | 64 | |
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") | |
53 | 80 | def echo_use_kwargs(request, name): |
54 | 81 | return json_response({"name": name}) |
55 | 82 | |
56 | 83 | |
84 | @handle_view_errors | |
57 | 85 | 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): | |
58 | 96 | return json_response(parser.parse(hello_multiple, request)) |
59 | 97 | |
60 | 98 | |
99 | @handle_view_errors | |
61 | 100 | 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)) | |
68 | 102 | |
69 | 103 | |
70 | @use_args({"value": fields.Int()}) | |
104 | @handle_view_errors | |
105 | @use_args({"value": fields.Int()}, location="query") | |
71 | 106 | def echo_use_args_with_path_param(request, args, name): |
72 | 107 | return json_response(args) |
73 | 108 | |
74 | 109 | |
75 | @use_kwargs({"value": fields.Int()}) | |
110 | @handle_view_errors | |
111 | @use_kwargs({"value": fields.Int()}, location="query") | |
76 | 112 | def echo_use_kwargs_with_path_param(request, value, name): |
77 | 113 | return json_response({"value": value}) |
78 | 114 | |
79 | 115 | |
116 | @handle_view_errors | |
80 | 117 | def always_error(request): |
81 | 118 | def always_fail(value): |
82 | 119 | raise ma.ValidationError("something went wrong") |
83 | 120 | |
84 | 121 | 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) | |
89 | 123 | |
90 | 124 | |
125 | @handle_view_errors | |
91 | 126 | 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")) | |
93 | 128 | |
94 | 129 | |
130 | @handle_view_errors | |
95 | 131 | 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")) | |
97 | 133 | |
98 | 134 | |
135 | @handle_view_errors | |
99 | 136 | def echo_file(request): |
100 | 137 | args = {"myfile": fields.Field()} |
101 | result = parser.parse(args, request, locations=("files",)) | |
138 | result = parser.parse(args, request, location="files") | |
102 | 139 | myfile = result["myfile"] |
103 | 140 | content = myfile.read().decode("utf8") |
104 | 141 | return json_response({"myfile": content}) |
105 | 142 | |
106 | 143 | |
144 | @handle_view_errors | |
107 | 145 | def echo_nested(request): |
108 | 146 | argmap = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})} |
109 | 147 | return json_response(parser.parse(argmap, request)) |
110 | 148 | |
111 | 149 | |
150 | @handle_view_errors | |
112 | 151 | def echo_nested_many(request): |
113 | 152 | argmap = { |
114 | 153 | "users": fields.Nested({"id": fields.Int(), "name": fields.Str()}, many=True) |
117 | 156 | |
118 | 157 | |
119 | 158 | class EchoCBV(View): |
159 | @handle_view_errors | |
120 | 160 | 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)) | |
126 | 163 | |
127 | 164 | post = get |
128 | 165 | |
129 | 166 | |
130 | 167 | class EchoUseArgsCBV(View): |
131 | @use_args(hello_args) | |
168 | @handle_view_errors | |
169 | @use_args(hello_args, location="query") | |
132 | 170 | def get(self, request, args): |
133 | 171 | return json_response(args) |
134 | 172 | |
135 | post = get | |
173 | @handle_view_errors | |
174 | @use_args(hello_args) | |
175 | def post(self, request, args): | |
176 | return json_response(args) | |
136 | 177 | |
137 | 178 | |
138 | 179 | class EchoUseArgsWithParamCBV(View): |
139 | @use_args(hello_args) | |
180 | @handle_view_errors | |
181 | @use_args(hello_args, location="query") | |
140 | 182 | def get(self, request, args, pid): |
141 | 183 | return json_response(args) |
142 | 184 | |
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 | ||
2 | 0 | import falcon |
3 | 1 | import marshmallow as ma |
2 | ||
4 | 3 | from webargs import fields |
4 | from webargs.core import json | |
5 | 5 | from webargs.falconparser import parser, use_args, use_kwargs |
6 | from webargs.core import MARSHMALLOW_VERSION_INFO | |
7 | 6 | |
8 | 7 | hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)} |
9 | 8 | hello_multiple = {"name": fields.List(fields.Str())} |
13 | 12 | name = fields.Str(missing="World", validate=lambda n: len(n) >= 3) |
14 | 13 | |
15 | 14 | |
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) | |
18 | 19 | |
19 | 20 | |
20 | class Echo(object): | |
21 | class Echo: | |
21 | 22 | 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") | |
36 | 24 | resp.body = json.dumps(parsed) |
37 | 25 | |
38 | 26 | |
39 | class EchoJSON(object): | |
27 | class EchoForm: | |
40 | 28 | def on_post(self, req, resp): |
41 | parsed = parser.parse(hello_args, req, locations=("json",)) | |
29 | parsed = parser.parse(hello_args, req, location="form") | |
42 | 30 | resp.body = json.dumps(parsed) |
43 | 31 | |
44 | 32 | |
45 | class EchoForm(object): | |
33 | class EchoJSON: | |
46 | 34 | def on_post(self, req, resp): |
47 | parsed = parser.parse(hello_args, req, locations=("form",)) | |
35 | parsed = parser.parse(hello_args, req, location="json") | |
48 | 36 | resp.body = json.dumps(parsed) |
49 | 37 | |
50 | 38 | |
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") | |
53 | 53 | def on_get(self, req, resp, args): |
54 | 54 | resp.body = json.dumps(args) |
55 | 55 | |
56 | on_post = on_get | |
57 | 56 | |
58 | ||
59 | class EchoUseKwargs(object): | |
60 | @use_kwargs(hello_args) | |
57 | class EchoUseKwargs: | |
58 | @use_kwargs(hello_args, location="query") | |
61 | 59 | def on_get(self, req, resp, name): |
62 | 60 | resp.body = json.dumps({"name": name}) |
63 | 61 | |
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) | |
65 | 71 | |
66 | 72 | |
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)) | |
73 | 76 | |
74 | 77 | |
75 | class EchoMulti(object): | |
78 | class EchoMulti: | |
76 | 79 | 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): | |
77 | 90 | resp.body = json.dumps(parser.parse(hello_multiple, req)) |
78 | 91 | |
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)) | |
80 | 96 | |
81 | 97 | |
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") | |
93 | 100 | def on_get(self, req, resp, args, name): |
94 | 101 | resp.body = json.dumps(args) |
95 | 102 | |
96 | 103 | |
97 | class EchoUseKwargsWithPathParam(object): | |
98 | @use_kwargs({"value": fields.Int()}) | |
104 | class EchoUseKwargsWithPathParam: | |
105 | @use_kwargs({"value": fields.Int()}, location="query") | |
99 | 106 | def on_get(self, req, resp, value, name): |
100 | 107 | resp.body = json.dumps({"value": value}) |
101 | 108 | |
102 | 109 | |
103 | class AlwaysError(object): | |
110 | class AlwaysError: | |
104 | 111 | def on_get(self, req, resp): |
105 | 112 | def always_fail(value): |
106 | 113 | raise ma.ValidationError("something went wrong") |
111 | 118 | on_post = on_get |
112 | 119 | |
113 | 120 | |
114 | class EchoHeaders(object): | |
121 | class EchoHeaders: | |
115 | 122 | 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")) | |
117 | 127 | |
118 | 128 | |
119 | class EchoCookie(object): | |
129 | class EchoCookie: | |
120 | 130 | 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")) | |
122 | 132 | |
123 | 133 | |
124 | class EchoNested(object): | |
134 | class EchoNested: | |
125 | 135 | def on_post(self, req, resp): |
126 | 136 | args = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})} |
127 | 137 | resp.body = json.dumps(parser.parse(args, req)) |
128 | 138 | |
129 | 139 | |
130 | class EchoNestedMany(object): | |
140 | class EchoNestedMany: | |
131 | 141 | def on_post(self, req, resp): |
132 | 142 | args = { |
133 | 143 | "users": fields.Nested( |
138 | 148 | |
139 | 149 | |
140 | 150 | def use_args_hook(args, context_key="args", **kwargs): |
141 | def hook(req, resp, params): | |
151 | def hook(req, resp, resource, params): | |
142 | 152 | parsed_args = parser.parse(args, req=req, **kwargs) |
143 | 153 | req.context[context_key] = parsed_args |
144 | 154 | |
145 | 155 | return hook |
146 | 156 | |
147 | 157 | |
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: | |
150 | 160 | def on_get(self, req, resp): |
151 | 161 | resp.body = json.dumps(req.context["args"]) |
152 | 162 | |
154 | 164 | def create_app(): |
155 | 165 | app = falcon.API() |
156 | 166 | app.add_route("/echo", Echo()) |
157 | app.add_route("/echo_query", EchoQuery()) | |
167 | app.add_route("/echo_form", EchoForm()) | |
158 | 168 | 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()) | |
160 | 171 | app.add_route("/echo_use_args", EchoUseArgs()) |
161 | 172 | app.add_route("/echo_use_kwargs", EchoUseKwargs()) |
162 | 173 | app.add_route("/echo_use_args_validated", EchoUseArgsValidated()) |
174 | app.add_route("/echo_ignoring_extra_data", EchoJSONIgnoreExtraData()) | |
163 | 175 | app.add_route("/echo_multi", EchoMulti()) |
176 | app.add_route("/echo_multi_form", EchoMultiForm()) | |
177 | app.add_route("/echo_multi_json", EchoMultiJSON()) | |
164 | 178 | app.add_route("/echo_many_schema", EchoManySchema()) |
165 | 179 | app.add_route("/echo_use_args_with_path_param/{name}", EchoUseArgsWithPathParam()) |
166 | 180 | app.add_route( |
0 | from webargs.core import json | |
1 | 0 | from flask import Flask, jsonify as J, Response, request |
2 | 1 | from flask.views import MethodView |
3 | ||
4 | 2 | import marshmallow as ma |
3 | ||
5 | 4 | from webargs import fields |
6 | 5 | from webargs.flaskparser import parser, use_args, use_kwargs |
7 | from webargs.core import MARSHMALLOW_VERSION_INFO | |
6 | from webargs.core import json | |
8 | 7 | |
9 | 8 | |
10 | 9 | class TestAppConfig: |
19 | 18 | name = fields.Str(missing="World", validate=lambda n: len(n) >= 3) |
20 | 19 | |
21 | 20 | |
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) | |
24 | 22 | |
25 | 23 | app = Flask(__name__) |
26 | 24 | app.config.from_object(TestAppConfig) |
27 | 25 | |
28 | 26 | |
29 | @app.route("/echo", methods=["GET", "POST"]) | |
27 | @app.route("/echo", methods=["GET"]) | |
30 | 28 | 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")) | |
37 | 35 | |
38 | 36 | |
39 | 37 | @app.route("/echo_json", methods=["POST"]) |
40 | 38 | 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") | |
51 | 49 | def echo_use_args(args): |
52 | 50 | return J(args) |
53 | 51 | |
54 | 52 | |
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 | ) | |
57 | 57 | def echo_use_args_validated(args): |
58 | 58 | return J(args) |
59 | 59 | |
60 | 60 | |
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") | |
63 | 68 | def echo_use_kwargs(name): |
64 | 69 | return J({"name": name}) |
65 | 70 | |
66 | 71 | |
67 | @app.route("/echo_multi", methods=["GET", "POST"]) | |
72 | @app.route("/echo_multi", methods=["GET"]) | |
68 | 73 | 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(): | |
69 | 84 | return J(parser.parse(hello_multiple)) |
70 | 85 | |
71 | 86 | |
72 | 87 | @app.route("/echo_many_schema", methods=["GET", "POST"]) |
73 | 88 | def many_nested(): |
74 | arguments = parser.parse(hello_many_schema, locations=("json",)) | |
89 | arguments = parser.parse(hello_many_schema) | |
75 | 90 | return Response(json.dumps(arguments), content_type="application/json") |
76 | 91 | |
77 | 92 | |
78 | 93 | @app.route("/echo_use_args_with_path_param/<name>") |
79 | @use_args({"value": fields.Int()}) | |
94 | @use_args({"value": fields.Int()}, location="query") | |
80 | 95 | def echo_use_args_with_path(args, name): |
81 | 96 | return J(args) |
82 | 97 | |
83 | 98 | |
84 | 99 | @app.route("/echo_use_kwargs_with_path_param/<name>") |
85 | @use_kwargs({"value": fields.Int()}) | |
100 | @use_kwargs({"value": fields.Int()}, location="query") | |
86 | 101 | def echo_use_kwargs_with_path(name, value): |
87 | 102 | return J({"value": value}) |
88 | 103 | |
98 | 113 | |
99 | 114 | @app.route("/echo_headers") |
100 | 115 | 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) | |
102 | 125 | |
103 | 126 | |
104 | 127 | @app.route("/echo_cookie") |
105 | 128 | def echo_cookie(): |
106 | return J(parser.parse(hello_args, request, locations=("cookies",))) | |
129 | return J(parser.parse(hello_args, request, location="cookies")) | |
107 | 130 | |
108 | 131 | |
109 | 132 | @app.route("/echo_file", methods=["POST"]) |
110 | 133 | def echo_file(): |
111 | 134 | args = {"myfile": fields.Field()} |
112 | result = parser.parse(args, locations=("files",)) | |
135 | result = parser.parse(args, location="files") | |
113 | 136 | fp = result["myfile"] |
114 | 137 | content = fp.read().decode("utf8") |
115 | 138 | return J({"myfile": content}) |
117 | 140 | |
118 | 141 | @app.route("/echo_view_arg/<view_arg>") |
119 | 142 | 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")) | |
121 | 144 | |
122 | 145 | |
123 | 146 | @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") | |
125 | 148 | def echo_view_arg_with_use_args(args, **kwargs): |
126 | 149 | return J(args) |
127 | 150 | |
142 | 165 | |
143 | 166 | @app.route("/echo_nested_many_data_key", methods=["POST"]) |
144 | 167 | 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") | |
147 | 170 | } |
148 | args = {"x_field": fields.Nested({"id": fields.Int()}, many=True, **data_key_kwarg)} | |
149 | 171 | return J(parser.parse(args)) |
150 | 172 | |
151 | 173 | |
186 | 208 | def handle_error(err): |
187 | 209 | if err.code == 422: |
188 | 210 | assert isinstance(err.data["schema"], ma.Schema) |
211 | ||
189 | 212 | return J(err.data["messages"]), err.code |
0 | from webargs.core import json | |
1 | ||
2 | 0 | from pyramid.config import Configurator |
3 | 1 | from pyramid.httpexceptions import HTTPBadRequest |
4 | 2 | import marshmallow as ma |
5 | 3 | |
6 | 4 | from webargs import fields |
7 | 5 | from webargs.pyramidparser import parser, use_args, use_kwargs |
8 | from webargs.core import MARSHMALLOW_VERSION_INFO | |
6 | from webargs.core import json | |
9 | 7 | |
10 | 8 | hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)} |
11 | 9 | hello_multiple = {"name": fields.List(fields.Str())} |
15 | 13 | name = fields.Str(missing="World", validate=lambda n: len(n) >= 3) |
16 | 14 | |
17 | 15 | |
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) | |
20 | 20 | |
21 | 21 | |
22 | 22 | 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): | |
23 | 31 | 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) | |
25 | 53 | except json.JSONDecodeError: |
26 | 54 | error = HTTPBadRequest() |
27 | 55 | error.body = json.dumps(["Invalid JSON."]).encode("utf-8") |
30 | 58 | |
31 | 59 | |
32 | 60 | def echo_query(request): |
33 | return parser.parse(hello_args, request, locations=("query",)) | |
61 | return parser.parse(hello_args, request, location="query") | |
34 | 62 | |
35 | 63 | |
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") | |
45 | 65 | def echo_use_args(request, args): |
46 | 66 | return args |
47 | 67 | |
48 | 68 | |
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 | ) | |
50 | 72 | def echo_use_args_validated(request, args): |
51 | 73 | return args |
52 | 74 | |
53 | 75 | |
54 | @use_kwargs(hello_args) | |
76 | @use_kwargs(hello_args, location="query") | |
55 | 77 | def echo_use_kwargs(request, name): |
56 | 78 | return {"name": name} |
57 | 79 | |
58 | 80 | |
59 | 81 | 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): | |
60 | 90 | return parser.parse(hello_multiple, request) |
61 | 91 | |
62 | 92 | |
63 | 93 | def echo_many_schema(request): |
64 | return parser.parse(hello_many_schema, request, locations=("json",)) | |
94 | return parser.parse(hello_many_schema, request) | |
65 | 95 | |
66 | 96 | |
67 | @use_args({"value": fields.Int()}) | |
97 | @use_args({"value": fields.Int()}, location="query") | |
68 | 98 | def echo_use_args_with_path_param(request, args): |
69 | 99 | return args |
70 | 100 | |
71 | 101 | |
72 | @use_kwargs({"value": fields.Int()}) | |
102 | @use_kwargs({"value": fields.Int()}, location="query") | |
73 | 103 | def echo_use_kwargs_with_path_param(request, value): |
74 | 104 | return {"value": value} |
75 | 105 | |
83 | 113 | |
84 | 114 | |
85 | 115 | def echo_headers(request): |
86 | return parser.parse(hello_args, request, locations=("headers",)) | |
116 | return parser.parse(hello_args, request, location="headers") | |
87 | 117 | |
88 | 118 | |
89 | 119 | def echo_cookie(request): |
90 | return parser.parse(hello_args, request, locations=("cookies",)) | |
120 | return parser.parse(hello_args, request, location="cookies") | |
91 | 121 | |
92 | 122 | |
93 | 123 | def echo_file(request): |
94 | 124 | args = {"myfile": fields.Field()} |
95 | result = parser.parse(args, request, locations=("files",)) | |
125 | result = parser.parse(args, request, location="files") | |
96 | 126 | myfile = result["myfile"] |
97 | 127 | content = myfile.file.read().decode("utf8") |
98 | 128 | return {"myfile": content} |
111 | 141 | |
112 | 142 | |
113 | 143 | def echo_matchdict(request): |
114 | return parser.parse({"mymatch": fields.Int()}, request, locations=("matchdict",)) | |
144 | return parser.parse({"mymatch": fields.Int()}, request, location="matchdict") | |
115 | 145 | |
116 | 146 | |
117 | class EchoCallable(object): | |
147 | class EchoCallable: | |
118 | 148 | def __init__(self, request): |
119 | 149 | self.request = request |
120 | 150 | |
121 | @use_args({"value": fields.Int()}) | |
151 | @use_args({"value": fields.Int()}, location="query") | |
122 | 152 | def __call__(self, args): |
123 | 153 | return args |
124 | 154 | |
134 | 164 | config = Configurator() |
135 | 165 | |
136 | 166 | 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) | |
137 | 170 | 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) | |
140 | 172 | add_route(config, "/echo_use_args", echo_use_args) |
141 | 173 | add_route(config, "/echo_use_args_validated", echo_use_args_validated) |
142 | 174 | add_route(config, "/echo_use_kwargs", echo_use_kwargs) |
143 | 175 | 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) | |
144 | 178 | add_route(config, "/echo_many_schema", echo_many_schema) |
145 | 179 | add_route( |
146 | 180 | config, "/echo_use_args_with_path_param/{name}", echo_use_args_with_path_param |
0 | # -*- coding: utf-8 -*- | |
1 | # flake8: noqa | |
2 | import sys | |
3 | ||
4 | PY2 = int(sys.version[0]) == 2 | |
5 | ||
6 | if PY2: | |
7 | text_type = unicode | |
8 | binary_type = str | |
9 | string_types = (str, unicode) | |
10 | basestring = basestring | |
11 | else: | |
12 | text_type = str | |
13 | binary_type = bytes | |
14 | string_types = (str,) | |
15 | basestring = (str, bytes) |
0 | 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 | |
3 | 0 | import datetime |
1 | from unittest import mock | |
4 | 2 | |
5 | 3 | 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 | ) | |
7 | 13 | from werkzeug.datastructures import MultiDict as WerkMultiDict |
8 | 14 | from django.utils.datastructures import MultiValueDict as DjMultiDict |
9 | 15 | from bottle import MultiDict as BotMultiDict |
10 | 16 | |
11 | from webargs import fields, missing, ValidationError | |
17 | from webargs import fields, ValidationError | |
12 | 18 | from webargs.core import ( |
13 | 19 | Parser, |
14 | get_value, | |
15 | dict2schema, | |
16 | 20 | is_json, |
17 | 21 | get_mimetype, |
18 | MARSHMALLOW_VERSION_INFO, | |
19 | 22 | ) |
20 | ||
21 | ||
22 | strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {} | |
23 | from webargs.multidictproxy import MultiDictProxy | |
23 | 24 | |
24 | 25 | |
25 | 26 | class MockHTTPError(Exception): |
26 | 27 | def __init__(self, status_code, headers): |
27 | 28 | self.status_code = status_code |
28 | 29 | self.headers = headers |
29 | super(MockHTTPError, self).__init__(self, "HTTP Error occurred") | |
30 | super().__init__(self, "HTTP Error occurred") | |
30 | 31 | |
31 | 32 | |
32 | 33 | class MockRequestParser(Parser): |
33 | 34 | """A minimal parser implementation that parses mock requests.""" |
34 | 35 | |
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 | |
43 | 44 | |
44 | 45 | |
45 | 46 | @pytest.yield_fixture(scope="function") |
58 | 59 | # Parser tests |
59 | 60 | |
60 | 61 | |
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} | |
64 | 66 | 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} | |
116 | 98 | argmap = {"username": fields.Field(), "password": fields.Field()} |
117 | p = Parser() | |
118 | ret = p.parse(argmap, web_request) | |
99 | ret = parser.parse(argmap, web_request) | |
119 | 100 | 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 | |
120 | 214 | |
121 | 215 | |
122 | 216 | def test_parse_required_arg_raises_validation_error(parser, web_request): |
140 | 234 | assert result == {"first": "Steve", "last": None} |
141 | 235 | |
142 | 236 | |
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} | |
150 | 241 | |
151 | 242 | |
152 | 243 | def test_parse_required_list(parser, web_request): |
154 | 245 | args = {"foo": fields.List(fields.Field(), required=True)} |
155 | 246 | with pytest.raises(ValidationError) as excinfo: |
156 | 247 | 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 | ) | |
158 | 251 | |
159 | 252 | |
160 | 253 | # Regression test for https://github.com/marshmallow-code/webargs/issues/107 |
169 | 262 | args = {"foo": fields.List(fields.Field(), allow_none=False)} |
170 | 263 | with pytest.raises(ValidationError) as excinfo: |
171 | 264 | 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." | |
173 | 266 | |
174 | 267 | |
175 | 268 | def test_parse_empty_list(parser, web_request): |
184 | 277 | assert parser.parse(args, web_request) == {} |
185 | 278 | |
186 | 279 | |
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" | |
189 | 282 | |
190 | 283 | |
191 | 284 | def test_missing_with_default(parser, web_request): |
192 | 285 | web_request.json = {} |
193 | 286 | args = {"val": fields.Field(missing="pizza")} |
194 | result = parser.parse(args, web_request, locations=("json",)) | |
287 | result = parser.parse(args, web_request) | |
195 | 288 | assert result["val"] == "pizza" |
196 | 289 | |
197 | 290 | |
198 | 291 | def test_default_can_be_none(parser, web_request): |
199 | 292 | web_request.json = {} |
200 | 293 | 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) | |
202 | 295 | assert result["val"] is None |
203 | 296 | |
204 | 297 | |
209 | 302 | "p": fields.Int( |
210 | 303 | missing=1, |
211 | 304 | validate=lambda p: p > 0, |
212 | error=u"La page demandée n'existe pas", | |
305 | error="La page demandée n'existe pas", | |
213 | 306 | location="querystring", |
214 | 307 | ) |
215 | 308 | } |
216 | 309 | assert parser.parse(args, web_request) == {"p": 1} |
217 | 310 | |
218 | 311 | |
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): | |
220 | 313 | 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 | ||
221 | 326 | 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) | |
247 | 333 | assert handle_error.call_count == 2 |
248 | 334 | |
249 | 335 | |
250 | 336 | def test_handle_error_reraises_errors(web_request): |
251 | 337 | p = Parser() |
252 | 338 | 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 = {} | |
259 | 352 | 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): | |
272 | 357 | class CustomError(Exception): |
273 | 358 | pass |
274 | 359 | |
275 | def error_handler(error, req, schema, status_code, headers): | |
360 | def error_handler(error, req, schema, *, error_status_code, error_headers): | |
276 | 361 | assert isinstance(schema, Schema) |
277 | 362 | raise CustomError(error) |
278 | 363 | |
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 | ||
280 | 373 | p = Parser(error_handler=error_handler) |
281 | 374 | 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): | |
287 | 379 | class CustomError(Exception): |
288 | 380 | pass |
289 | 381 | |
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") | |
292 | 385 | parser = Parser() |
293 | 386 | |
294 | 387 | @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): | |
296 | 389 | assert isinstance(schema, Schema) |
297 | 390 | raise CustomError(error) |
298 | 391 | |
299 | 392 | 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): | |
304 | 416 | web_request.data = {"foo": 42} |
305 | 417 | |
306 | 418 | parser = Parser() |
307 | 419 | |
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") | |
313 | 425 | assert result["foo"] == 42 |
314 | 426 | |
315 | 427 | |
316 | def test_custom_location_handler_with_data_key(web_request): | |
428 | def test_custom_location_loader_with_data_key(web_request): | |
317 | 429 | web_request.data = {"X-Foo": 42} |
318 | 430 | parser = Parser() |
319 | 431 | |
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 | ||
327 | 436 | 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" | |
329 | 438 | ) |
330 | 439 | assert result["x_foo"] == 42 |
331 | 440 | |
332 | 441 | |
333 | def test_full_input_validation(web_request): | |
442 | def test_full_input_validation(parser, web_request): | |
334 | 443 | |
335 | 444 | web_request.json = {"foo": 41, "bar": 42} |
336 | 445 | |
337 | parser = MockRequestParser() | |
338 | 446 | args = {"foo": fields.Int(), "bar": fields.Int()} |
339 | 447 | with pytest.raises(ValidationError): |
340 | 448 | # 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"]) | |
347 | 450 | |
348 | 451 | |
349 | 452 | def test_full_input_validation_with_multiple_validators(web_request, parser): |
359 | 462 | web_request.json = {"a": 2, "b": 1} |
360 | 463 | validators = [validate1, validate2] |
361 | 464 | 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) | |
363 | 466 | |
364 | 467 | web_request.json = {"a": 1, "b": 2} |
365 | 468 | 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 = {} | |
372 | 474 | args = { |
373 | 475 | "foo": fields.Str(required=True, error_messages={"required": "We need foo"}) |
374 | 476 | } |
375 | 477 | with pytest.raises(ValidationError) as excinfo: |
376 | 478 | # 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): | |
385 | 485 | web_request.json = {"foo": ""} |
386 | parser = MockRequestParser() | |
387 | 486 | args = { |
388 | 487 | "foo": fields.Str( |
389 | 488 | required="We need foo", |
393 | 492 | } |
394 | 493 | with pytest.raises(ValidationError) as excinfo: |
395 | 494 | # Test that `validate` receives dictionary of args |
396 | parser.parse(args, web_request, locations=("json",)) | |
495 | parser.parse(args, web_request) | |
397 | 496 | |
398 | 497 | 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 | |
401 | 498 | |
402 | 499 | |
403 | 500 | def test_full_input_validator_receives_nonascii_input(web_request): |
404 | 501 | def validate(val): |
405 | 502 | return False |
406 | 503 | |
407 | text = u"øœ∑∆∑" | |
504 | text = "øœ∑∆∑" | |
408 | 505 | web_request.json = {"text": text} |
409 | 506 | parser = MockRequestParser() |
410 | 507 | args = {"text": fields.Str()} |
411 | 508 | 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."]} | |
414 | 511 | |
415 | 512 | |
416 | 513 | def test_invalid_argument_for_validate(web_request, parser): |
417 | 514 | with pytest.raises(ValueError) as excinfo: |
418 | 515 | parser.parse({}, web_request, validate="notcallable") |
419 | 516 | 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 | |
428 | 517 | |
429 | 518 | |
430 | 519 | def create_bottle_multi_dict(): |
442 | 531 | |
443 | 532 | |
444 | 533 | @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") | |
448 | 552 | |
449 | 553 | |
450 | 554 | def test_parse_with_data_key(web_request): |
451 | 555 | web_request.json = {"Content-Type": "application/json"} |
452 | 556 | |
453 | 557 | 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) | |
473 | 560 | assert parsed == {"content_type": "application/json"} |
474 | 561 | |
475 | 562 | |
477 | 564 | web_request.json = {"Content-Type": 12345} |
478 | 565 | |
479 | 566 | 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")} | |
484 | 568 | 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."] | |
488 | 573 | |
489 | 574 | |
490 | 575 | def test_parse_nested_with_data_key(web_request): |
491 | 576 | parser = MockRequestParser() |
492 | 577 | 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) | |
499 | 581 | assert parsed == {"nested_arg": {"right": "OK"}} |
500 | 582 | |
501 | 583 | |
503 | 585 | parser = MockRequestParser() |
504 | 586 | |
505 | 587 | web_request.json = {"nested_arg": {}} |
506 | data_key_kwargs = { | |
507 | "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "miss" | |
508 | } | |
509 | 588 | args = { |
510 | 589 | "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")} | |
512 | 591 | ) |
513 | 592 | } |
514 | 593 | |
515 | parsed = parser.parse(args, web_request, locations=("json",)) | |
594 | parsed = parser.parse(args, web_request) | |
516 | 595 | assert parsed == {"nested_arg": {"found": None}} |
517 | 596 | |
518 | 597 | |
522 | 601 | web_request.json = {"nested_arg": {}} |
523 | 602 | args = {"nested_arg": fields.Nested({"miss": fields.Field(missing="<foo>")})} |
524 | 603 | |
525 | parsed = parser.parse(args, web_request, locations=("json",)) | |
604 | parsed = parser.parse(args, web_request) | |
526 | 605 | assert parsed == {"nested_arg": {"miss": "<foo>"}} |
527 | 606 | |
528 | 607 | |
553 | 632 | web_request.json = {"username": "foo"} |
554 | 633 | web_request.query = {"page": 42} |
555 | 634 | |
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) | |
558 | 637 | def viewfunc(query_parsed, json_parsed): |
559 | 638 | return {"json": json_parsed, "query": query_parsed} |
560 | 639 | |
569 | 648 | web_request.json = {"username": "foo"} |
570 | 649 | web_request.query = {"page": 42} |
571 | 650 | |
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) | |
574 | 653 | def viewfunc(page, username): |
575 | 654 | return {"json": {"username": username}, "query": {"page": page}} |
576 | 655 | |
591 | 670 | |
592 | 671 | def test_list_allowed_missing(web_request, parser): |
593 | 672 | args = {"name": fields.List(fields.Str())} |
594 | web_request.json = {"fakedata": True} | |
673 | web_request.json = {} | |
595 | 674 | result = parser.parse(args, web_request) |
596 | 675 | assert result == {} |
597 | 676 | |
598 | 677 | |
599 | 678 | def test_int_list_allowed_missing(web_request, parser): |
600 | 679 | args = {"name": fields.List(fields.Int())} |
601 | web_request.json = {"fakedata": True} | |
680 | web_request.json = {} | |
602 | 681 | result = parser.parse(args, web_request) |
603 | 682 | assert result == {} |
604 | 683 | |
605 | 684 | |
606 | 685 | def test_multiple_arg_required_with_int_conversion(web_request, parser): |
607 | 686 | args = {"ids": fields.List(fields.Int(), required=True)} |
608 | web_request.json = {"fakedata": True} | |
687 | web_request.json = {} | |
609 | 688 | with pytest.raises(ValidationError) as excinfo: |
610 | 689 | 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 | } | |
612 | 693 | |
613 | 694 | |
614 | 695 | def test_parse_with_callable(web_request, parser): |
617 | 698 | |
618 | 699 | class MySchema(Schema): |
619 | 700 | foo = fields.Field() |
620 | ||
621 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
622 | ||
623 | class Meta: | |
624 | strict = True | |
625 | 701 | |
626 | 702 | def make_schema(req): |
627 | 703 | assert req is web_request |
635 | 711 | def test_use_args_callable(web_request, parser): |
636 | 712 | class HelloSchema(Schema): |
637 | 713 | name = fields.Str() |
638 | ||
639 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
640 | ||
641 | class Meta: | |
642 | strict = True | |
643 | 714 | |
644 | 715 | @post_load |
645 | 716 | def request_data(self, item, **kwargs): |
665 | 736 | id = fields.Int(dump_only=True) |
666 | 737 | email = fields.Email() |
667 | 738 | password = fields.Str(load_only=True) |
668 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
669 | ||
670 | class Meta: | |
671 | strict = True | |
672 | 739 | |
673 | 740 | def test_passing_schema_to_parse(self, parser, web_request): |
674 | 741 | web_request.json = {"email": "[email protected]", "password": "bar"} |
675 | 742 | |
676 | result = parser.parse(self.UserSchema(**strict_kwargs), web_request) | |
743 | result = parser.parse(self.UserSchema(), web_request) | |
677 | 744 | |
678 | 745 | assert result == {"email": "[email protected]", "password": "bar"} |
679 | 746 | |
681 | 748 | |
682 | 749 | web_request.json = {"email": "[email protected]", "password": "bar"} |
683 | 750 | |
684 | @parser.use_args(self.UserSchema(**strict_kwargs), web_request) | |
751 | @parser.use_args(self.UserSchema(), web_request) | |
685 | 752 | def viewfunc(args): |
686 | 753 | return args |
687 | 754 | |
692 | 759 | |
693 | 760 | def factory(req): |
694 | 761 | assert req is web_request |
695 | return self.UserSchema(context={"request": req}, **strict_kwargs) | |
762 | return self.UserSchema(context={"request": req}) | |
696 | 763 | |
697 | 764 | result = parser.parse(factory, web_request) |
698 | 765 | |
703 | 770 | |
704 | 771 | def factory(req): |
705 | 772 | assert req is web_request |
706 | return self.UserSchema(context={"request": req}, **strict_kwargs) | |
773 | return self.UserSchema(context={"request": req}) | |
707 | 774 | |
708 | 775 | @parser.use_args(factory, web_request) |
709 | 776 | def viewfunc(args): |
715 | 782 | |
716 | 783 | web_request.json = {"email": "[email protected]", "password": "bar"} |
717 | 784 | |
718 | @parser.use_kwargs(self.UserSchema(**strict_kwargs), web_request) | |
785 | @parser.use_kwargs(self.UserSchema(), web_request) | |
719 | 786 | def viewfunc(email, password): |
720 | 787 | return {"email": email, "password": password} |
721 | 788 | |
726 | 793 | |
727 | 794 | def factory(req): |
728 | 795 | assert req is web_request |
729 | return self.UserSchema(context={"request": req}, **strict_kwargs) | |
796 | return self.UserSchema(context={"request": req}) | |
730 | 797 | |
731 | 798 | @parser.use_kwargs(factory, web_request) |
732 | 799 | def viewfunc(email, password): |
734 | 801 | |
735 | 802 | assert viewfunc() == {"email": "[email protected]", "password": "bar"} |
736 | 803 | |
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 | ||
748 | 804 | def test_use_kwargs_stacked(self, web_request, parser): |
749 | 805 | web_request.json = {"email": "[email protected]", "password": "bar", "page": 42} |
750 | 806 | |
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) | |
753 | 809 | def viewfunc(email, password, page): |
754 | 810 | return {"email": email, "password": password, "page": page} |
755 | 811 | |
762 | 818 | class UserSchema(Schema): |
763 | 819 | name = fields.Str() |
764 | 820 | location = fields.Field(required=False) |
765 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
766 | ||
767 | class Meta: | |
768 | strict = True | |
769 | 821 | |
770 | 822 | @validates_schema(pass_original=True) |
771 | 823 | def validate_schema(self, data, original_data, **kwargs): |
773 | 825 | return True |
774 | 826 | |
775 | 827 | web_request.json = {"name": "Eric Cartman"} |
776 | res = parser.parse(UserSchema, web_request, locations=("json",)) | |
828 | res = parser.parse(UserSchema, web_request) | |
777 | 829 | assert res == {"name": "Eric Cartman"} |
778 | 830 | |
779 | 831 | |
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): | |
781 | 833 | custom_args = {"foo": fields.Str()} |
782 | 834 | 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"} | |
788 | 840 | |
789 | 841 | @parser.use_args(custom_args, web_request) |
790 | 842 | def viewfunc(args): |
818 | 870 | |
819 | 871 | def test_delimited_list_default_delimiter(web_request, parser): |
820 | 872 | 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())}) | |
822 | 874 | schema = schema_cls() |
823 | 875 | |
824 | 876 | parsed = parser.parse(schema, web_request) |
825 | 877 | assert parsed["ids"] == [1, 2, 3] |
826 | 878 | |
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 | } | |
836 | 896 | ) |
837 | 897 | schema = schema_cls() |
838 | 898 | |
839 | 899 | 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 | """ | |
848 | 924 | 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"))} | |
855 | 927 | ) |
856 | 928 | schema = schema_cls() |
857 | 929 | |
861 | 933 | datetime.datetime(2018, 11, 2), |
862 | 934 | ] |
863 | 935 | |
864 | dumped = schema.dump(parsed) | |
865 | data = dumped.data if MARSHMALLOW_VERSION_INFO[0] < 3 else dumped | |
936 | data = schema.dump(parsed) | |
866 | 937 | assert data["dates"] == "2018-11-01,2018-11-02" |
867 | 938 | |
868 | 939 | |
869 | 940 | def test_delimited_list_custom_delimiter(web_request, parser): |
870 | 941 | 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 | ) | |
872 | 945 | schema = schema_cls() |
873 | 946 | |
874 | 947 | parsed = parser.parse(schema, web_request) |
875 | 948 | assert parsed["ids"] == [1, 2, 3] |
876 | 949 | |
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): | |
879 | 969 | 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())}) | |
881 | 971 | schema = schema_cls() |
882 | 972 | |
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."] | |
885 | 994 | |
886 | 995 | |
887 | 996 | # Regresion test for https://github.com/marshmallow-code/webargs/issues/149 |
888 | 997 | def test_delimited_list_passed_invalid_type(web_request, parser): |
889 | 998 | 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())}) | |
891 | 1000 | schema = schema_cls() |
892 | 1001 | |
893 | 1002 | with pytest.raises(ValidationError) as excinfo: |
894 | 1003 | 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."]}} | |
896 | 1015 | |
897 | 1016 | |
898 | 1017 | def test_missing_list_argument_not_in_parsed_result(web_request, parser): |
910 | 1029 | msg = "Missing data for required field." |
911 | 1030 | with pytest.raises(ValidationError, match=msg): |
912 | 1031 | 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 | |
923 | 1032 | |
924 | 1033 | |
925 | 1034 | def test_validation_errors_in_validator_are_passed_to_handle_error(parser, web_request): |
950 | 1059 | parser.parse(args, web_request) |
951 | 1060 | |
952 | 1061 | |
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 | |
999 | 1064 | argmap = {"nest": fields.Nested({"foo": fields.Field()})} |
1000 | schema_cls = dict2schema(argmap) | |
1065 | schema_cls = Schema.from_dict(argmap) | |
1001 | 1066 | assert issubclass(schema_cls, Schema) |
1002 | 1067 | schema = schema_cls() |
1003 | 1068 | assert "nest" in schema.fields |
1015 | 1080 | def test_get_mimetype(): |
1016 | 1081 | assert get_mimetype("application/json") == "application/json" |
1017 | 1082 | assert get_mimetype("application/json;charset=utf8") == "application/json" |
1018 | assert get_mimetype(None) is None | |
1019 | 1083 | |
1020 | 1084 | |
1021 | 1085 | 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): | |
1025 | 1087 | assert isinstance(error, ValidationError) |
1026 | 1088 | assert isinstance(schema, Schema) |
1027 | 1089 | raise MockHTTPError(error_status_code, error_headers) |
1040 | 1102 | assert error.headers == {"X-Foo": "bar"} |
1041 | 1103 | |
1042 | 1104 | |
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): | |
1045 | 1107 | class CustomSchema(Schema): |
1046 | 1108 | @pre_load |
1047 | 1109 | def pre_load(self, data, **kwargs): |
1048 | 1110 | data["value"] += " world" |
1049 | 1111 | return data |
1050 | 1112 | |
1051 | parse_json.return_value = "hello" | |
1113 | load_json.return_value = {"value": "hello"} | |
1052 | 1114 | argmap = {"value": fields.Str()} |
1053 | 1115 | p = Parser(schema_class=CustomSchema) |
1054 | 1116 | ret = p.parse(argmap, web_request) |
1055 | 1117 | assert ret == {"value": "hello world"} |
1056 | 1118 | |
1057 | 1119 | |
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): | |
1060 | 1122 | class CustomSchema(Schema): |
1061 | 1123 | @pre_load |
1062 | 1124 | def pre_load(self, data, **kwargs): |
1066 | 1128 | class CustomParser(Parser): |
1067 | 1129 | DEFAULT_SCHEMA_CLASS = CustomSchema |
1068 | 1130 | |
1069 | parse_json.return_value = "hello" | |
1131 | load_json.return_value = {"value": "hello"} | |
1070 | 1132 | argmap = {"value": fields.Str()} |
1071 | 1133 | p = CustomParser() |
1072 | 1134 | ret = p.parse(argmap, web_request) |
0 | # -*- coding: utf-8 -*- | |
1 | from __future__ import unicode_literals | |
2 | ||
3 | 0 | import pytest |
4 | 1 | from tests.apps.django_app.base.wsgi import application |
5 | 2 | |
16 | 13 | def test_use_args_with_validation(self): |
17 | 14 | pass |
18 | 15 | |
19 | @pytest.mark.skip(reason="headers location not supported by DjangoParser") | |
20 | def test_parsing_headers(self, testapp): | |
21 | pass | |
22 | ||
23 | 16 | def test_parsing_in_class_based_view(self, testapp): |
24 | 17 | 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"} | |
26 | 19 | |
27 | 20 | def test_use_args_in_class_based_view(self, testapp): |
28 | 21 | res = testapp.get("/echo_use_args_cbv?name=Fred") |
29 | 22 | 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"}) | |
31 | 24 | assert res.json == {"name": "Fred"} |
32 | 25 | |
33 | 26 | def test_use_args_in_class_based_view_with_path_param(self, testapp): |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | import pytest |
1 | import falcon.testing | |
2 | 2 | |
3 | 3 | from webargs.testing import CommonTestCase |
4 | 4 | from tests.apps.falcon_app import create_app |
15 | 15 | def test_use_args_hook(self, testapp): |
16 | 16 | assert testapp.get("/echo_use_args_hook?name=Fred").json == {"name": "Fred"} |
17 | 17 | |
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 | ||
18 | 33 | # 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): | |
20 | 36 | res = testapp.post( |
21 | "/echo", | |
37 | path, | |
22 | 38 | b"\xfe", |
23 | 39 | headers={"Accept": "application/json", "Content-Type": "application/json"}, |
24 | 40 | expect_errors=True, |
25 | 41 | ) |
26 | 42 | |
27 | 43 | 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."]} | |
29 | 46 | |
30 | 47 | # 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): | |
32 | 50 | res = testapp.post( |
33 | "/echo", | |
51 | path, | |
34 | 52 | '{"foo": "bar", }', |
35 | 53 | headers={"Accept": "application/json", "Content-Type": "application/json"}, |
36 | 54 | expect_errors=True, |
37 | 55 | ) |
38 | 56 | 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 | |
3 | 1 | |
4 | from werkzeug.exceptions import HTTPException | |
5 | import mock | |
2 | from werkzeug.exceptions import HTTPException, BadRequest | |
6 | 3 | import pytest |
7 | 4 | |
5 | from marshmallow import Schema | |
8 | 6 | from flask import Flask |
9 | 7 | from webargs import fields, ValidationError, missing |
10 | 8 | from webargs.flaskparser import parser, abort |
11 | from webargs.core import MARSHMALLOW_VERSION_INFO, json | |
9 | from webargs.core import json | |
12 | 10 | |
13 | 11 | from .apps.flask_app import app |
14 | 12 | from webargs.testing import CommonTestCase |
25 | 23 | def test_parsing_invalid_view_arg(self, testapp): |
26 | 24 | res = testapp.get("/echo_view_arg/foo", expect_errors=True) |
27 | 25 | 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."]}} | |
29 | 27 | |
30 | 28 | def test_use_args_with_view_args_parsing(self, testapp): |
31 | 29 | res = testapp.get("/echo_view_arg_use_args/42") |
32 | 30 | assert res.json == {"view_arg": 42} |
33 | 31 | |
34 | 32 | 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}) | |
36 | 34 | assert res.json == {"val": 42} |
37 | 35 | |
38 | 36 | 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}) | |
40 | 38 | assert res.json == {"val": 42} |
41 | 39 | |
42 | 40 | 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"}) | |
44 | 42 | assert res.json == {"username": "foo"} |
45 | 43 | |
46 | 44 | # regression test for https://github.com/marshmallow-code/webargs/issues/145 |
47 | 45 | 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 | |
52 | 52 | |
53 | 53 | res = testapp.post_json("/echo_nested_many_data_key", {"X-Field": [{"id": 24}]}) |
54 | 54 | assert res.json == {"x_field": [{"id": 24}]} |
56 | 56 | res = testapp.post_json("/echo_nested_many_data_key", {}) |
57 | 57 | assert res.json == {} |
58 | 58 | |
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 | ||
59 | 69 | |
60 | 70 | @mock.patch("webargs.flaskparser.abort") |
61 | 71 | 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 | ||
62 | 75 | app = Flask("testapp") |
63 | 76 | |
64 | 77 | def validate(x): |
71 | 84 | data=json.dumps({"value": 41}), |
72 | 85 | content_type="application/json", |
73 | 86 | ): |
74 | parser.parse(argmap) | |
87 | with pytest.raises(HTTPException): | |
88 | parser.parse(argmap) | |
75 | 89 | mock_abort.assert_called() |
76 | 90 | abort_args, abort_kwargs = mock_abort.call_args |
77 | 91 | assert abort_args[0] == 422 |
78 | 92 | expected_msg = "Invalid value." |
79 | assert abort_kwargs["messages"]["value"] == [expected_msg] | |
93 | assert abort_kwargs["messages"]["json"]["value"] == [expected_msg] | |
80 | 94 | assert type(abort_kwargs["exc"]) == ValidationError |
81 | 95 | |
82 | 96 | |
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): | |
84 | 99 | 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 | |
87 | 104 | |
88 | 105 | |
89 | 106 | def test_abort_with_message(): |
110 | 127 | error = json.loads(serialized_error) |
111 | 128 | assert isinstance(error, dict) |
112 | 129 | assert error["message"] == "custom error message" |
113 | ||
114 | ||
115 | def test_json_cache_race_condition(): | |
116 | app = Flask("testapp") | |
117 | lock = threading.Lock() | |
118 | lock.acquire() | |
119 | ||
120 | class MyField(fields.Field): | |
121 | def _deserialize(self, value, attr, data, **kwargs): | |
122 | with lock: | |
123 | return value | |
124 | ||
125 | argmap = {"value": MyField()} | |
126 | results = {} | |
127 | ||
128 | def thread_fn(value): | |
129 | with app.test_request_context( | |
130 | "/foo", | |
131 | method="post", | |
132 | data=json.dumps({"value": value}), | |
133 | content_type="application/json", | |
134 | ): | |
135 | results[value] = parser.parse(argmap)["value"] | |
136 | ||
137 | t1 = threading.Thread(target=thread_fn, args=(42,)) | |
138 | t2 = threading.Thread(target=thread_fn, args=(23,)) | |
139 | t1.start() | |
140 | t2.start() | |
141 | lock.release() | |
142 | t1.join() | |
143 | t2.join() | |
144 | # ensure we didn't get contaminated by a parallel request | |
145 | assert results[42] == 42 | |
146 | assert results[23] == 23 |
0 | # -*- coding: utf-8 -*- | |
1 | ||
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 | 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 -*- | |
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 | |
10 | 4 | 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 | |
14 | 10 | import tornado.web |
15 | import tornado.httputil | |
16 | import tornado.httpserver | |
17 | import tornado.http1connection | |
18 | import tornado.concurrent | |
19 | import tornado.ioloop | |
20 | 11 | from tornado.testing import AsyncHTTPTestCase |
21 | ||
22 | 12 | 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 | ||
25 | 21 | |
26 | 22 | name = "name" |
27 | 23 | value = "value" |
28 | 24 | |
29 | 25 | |
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: | |
43 | 46 | def test_it_should_get_single_values(self): |
44 | query = [(name, value)] | |
45 | field = fields.Field() | |
47 | query = [("name", "Aeschylus")] | |
46 | 48 | 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" | |
51 | 51 | |
52 | 52 | 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")] | |
55 | 54 | 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"] | |
60 | 57 | |
61 | 58 | def test_it_should_return_missing_if_not_present(self): |
62 | 59 | query = [] |
63 | field = fields.Field() | |
64 | field2 = fields.List(fields.Int()) | |
65 | 60 | 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 | |
81 | 64 | |
82 | 65 | |
83 | 66 | class TestFormArgs: |
84 | def setup_method(self, method): | |
85 | parser.clear_cache() | |
86 | ||
87 | 67 | def test_it_should_get_single_values(self): |
88 | query = [(name, value)] | |
89 | field = fields.Field() | |
68 | query = [("name", "Aristophanes")] | |
90 | 69 | 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" | |
95 | 72 | |
96 | 73 | 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")] | |
99 | 75 | 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"] | |
104 | 78 | |
105 | 79 | def test_it_should_return_missing_if_not_present(self): |
106 | 80 | query = [] |
107 | field = fields.Field() | |
108 | 81 | 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: | |
128 | 88 | def test_it_should_get_single_values(self): |
129 | query = {name: value} | |
130 | field = fields.Field() | |
89 | query = {"name": "Euripides"} | |
131 | 90 | 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" | |
135 | 93 | |
136 | 94 | def test_parsing_request_with_vendor_content_type(self): |
137 | query = {name: value} | |
138 | field = fields.Field() | |
95 | query = {"name": "Euripides"} | |
139 | 96 | request = make_json_request( |
140 | 97 | query, content_type="application/vnd.api+json; charset=UTF-8" |
141 | 98 | ) |
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" | |
145 | 101 | |
146 | 102 | def test_it_should_get_multiple_values(self): |
147 | query = {name: [value, value]} | |
148 | field = fields.List(fields.Field()) | |
103 | query = {"works": ["Medea", "Electra"]} | |
149 | 104 | 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"] | |
153 | 107 | |
154 | 108 | 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 | } | |
159 | 122 | 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"] | |
162 | 165 | |
163 | 166 | 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: | |
200 | 174 | 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" | |
208 | 179 | |
209 | 180 | 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")] | |
243 | 182 | 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"] | |
257 | 185 | |
258 | 186 | def test_it_should_return_missing_if_not_present(self): |
259 | 187 | query = [] |
260 | field = fields.Field() | |
261 | 188 | 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: | |
278 | 195 | def test_it_should_raise_httperror_on_failed_validation(self): |
279 | 196 | args = {"foo": fields.Field(validate=lambda x: False)} |
280 | 197 | with pytest.raises(tornado.web.HTTPError): |
281 | 198 | parser.parse(args, make_json_request({"foo": 42})) |
282 | 199 | |
283 | 200 | |
284 | class TestParse(object): | |
285 | def setup_method(self, method): | |
286 | parser.clear_cache() | |
287 | ||
201 | class TestParse: | |
288 | 202 | def test_it_should_parse_query_arguments(self): |
289 | 203 | attrs = {"string": fields.Field(), "integer": fields.List(fields.Int())} |
290 | 204 | |
292 | 206 | [("string", "value"), ("integer", "1"), ("integer", "2")] |
293 | 207 | ) |
294 | 208 | |
295 | parsed = parser.parse(attrs, request) | |
209 | parsed = parser.parse(attrs, request, location="query") | |
296 | 210 | |
297 | 211 | assert parsed["integer"] == [1, 2] |
298 | 212 | assert parsed["string"] == value |
304 | 218 | [("string", "value"), ("integer", "1"), ("integer", "2")] |
305 | 219 | ) |
306 | 220 | |
307 | parsed = parser.parse(attrs, request) | |
221 | parsed = parser.parse(attrs, request, location="form") | |
308 | 222 | |
309 | 223 | assert parsed["integer"] == [1, 2] |
310 | 224 | assert parsed["string"] == value |
336 | 250 | |
337 | 251 | request = make_request(headers={"string": "value", "integer": ["1", "2"]}) |
338 | 252 | |
339 | parsed = parser.parse(attrs, request, locations=["headers"]) | |
253 | parsed = parser.parse(attrs, request, location="headers") | |
340 | 254 | |
341 | 255 | assert parsed["string"] == value |
342 | 256 | assert parsed["integer"] == [1, 2] |
348 | 262 | [("string", "value"), ("integer", "1"), ("integer", "2")] |
349 | 263 | ) |
350 | 264 | |
351 | parsed = parser.parse(attrs, request, locations=["cookies"]) | |
265 | parsed = parser.parse(attrs, request, location="cookies") | |
352 | 266 | |
353 | 267 | assert parsed["string"] == value |
354 | 268 | assert parsed["integer"] == [2] |
360 | 274 | [("string", "value"), ("integer", "1"), ("integer", "2")] |
361 | 275 | ) |
362 | 276 | |
363 | parsed = parser.parse(attrs, request, locations=["files"]) | |
277 | parsed = parser.parse(attrs, request, location="files") | |
364 | 278 | |
365 | 279 | assert parsed["string"] == value |
366 | 280 | assert parsed["integer"] == [1, 2] |
382 | 296 | parser.parse(args, request) |
383 | 297 | |
384 | 298 | |
385 | class TestUseArgs(object): | |
386 | def setup_method(self, method): | |
387 | parser.clear_cache() | |
388 | ||
299 | class TestUseArgs: | |
389 | 300 | def test_it_should_pass_parsed_as_first_argument(self): |
390 | class Handler(object): | |
301 | class Handler: | |
391 | 302 | request = make_json_request({"key": "value"}) |
392 | 303 | |
393 | 304 | @use_args({"key": fields.Field()}) |
402 | 313 | assert result is True |
403 | 314 | |
404 | 315 | def test_it_should_pass_parsed_as_kwargs_arguments(self): |
405 | class Handler(object): | |
316 | class Handler: | |
406 | 317 | request = make_json_request({"key": "value"}) |
407 | 318 | |
408 | 319 | @use_kwargs({"key": fields.Field()}) |
417 | 328 | assert result is True |
418 | 329 | |
419 | 330 | def test_it_should_be_validate_arguments_when_validator_is_passed(self): |
420 | class Handler(object): | |
331 | class Handler: | |
421 | 332 | request = make_json_request({"foo": 41}) |
422 | 333 | |
423 | 334 | @use_kwargs({"foo": fields.Int()}, validate=lambda args: args["foo"] > 42) |
475 | 386 | |
476 | 387 | |
477 | 388 | 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 "" | |
480 | 391 | method = "POST" if body else "GET" |
481 | 392 | # Need to make a mock connection right now because Tornado 4.0 requires a |
482 | 393 | # remote_ip in the context attribute. 4.1 addresses this, and this |
485 | 396 | mock_connection = mock.Mock(spec=tornado.http1connection.HTTP1Connection) |
486 | 397 | mock_connection.context = mock.Mock() |
487 | 398 | 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 "" | |
489 | 400 | request = tornado.httputil.HTTPServerRequest( |
490 | 401 | method=method, |
491 | 402 | uri=uri, |
508 | 419 | class EchoHandler(tornado.web.RequestHandler): |
509 | 420 | ARGS = {"name": fields.Str()} |
510 | 421 | |
511 | @use_args(ARGS) | |
422 | @use_args(ARGS, location="query") | |
512 | 423 | def get(self, args): |
513 | 424 | 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()} | |
514 | 437 | |
515 | 438 | @use_args(ARGS) |
516 | 439 | def post(self, args): |
520 | 443 | class EchoWithParamHandler(tornado.web.RequestHandler): |
521 | 444 | ARGS = {"name": fields.Str()} |
522 | 445 | |
523 | @use_args(ARGS) | |
446 | @use_args(ARGS, location="query") | |
524 | 447 | def get(self, id, args): |
525 | 448 | self.write(args) |
526 | 449 | |
527 | 450 | |
528 | 451 | 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 | ] | |
530 | 458 | ) |
531 | 459 | |
532 | 460 | |
536 | 464 | |
537 | 465 | def test_post(self): |
538 | 466 | res = self.fetch( |
539 | "/echo", | |
467 | "/echo_json", | |
540 | 468 | method="POST", |
541 | 469 | headers={"Content-Type": "application/json"}, |
542 | 470 | body=json.dumps({"name": "Steve"}), |
544 | 472 | json_body = parse_json(res.body) |
545 | 473 | assert json_body["name"] == "Steve" |
546 | 474 | res = self.fetch( |
547 | "/echo", | |
475 | "/echo_json", | |
548 | 476 | method="POST", |
549 | 477 | headers={"Content-Type": "application/json"}, |
550 | 478 | body=json.dumps({}), |
576 | 504 | def post(self, args): |
577 | 505 | self.write(args) |
578 | 506 | |
579 | @use_kwargs(ARGS) | |
507 | @use_kwargs(ARGS, location="query") | |
580 | 508 | def get(self, name): |
581 | 509 | self.write({"status": "success"}) |
582 | 510 |
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 |
0 | 0 | [tox] |
1 | 1 | envlist= |
2 | 2 | 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 | |
6 | 6 | docs |
7 | 7 | |
8 | 8 | [testenv] |
9 | 9 | extras = tests |
10 | 10 | 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 | |
13 | 12 | 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} | |
17 | 21 | |
18 | 22 | [testenv:lint] |
19 | deps = pre-commit~=1.17 | |
23 | deps = pre-commit~=2.4 | |
20 | 24 | skip_install = true |
21 | 25 | 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/ | |
22 | 33 | |
23 | 34 | [testenv:docs] |
24 | 35 | extras = docs |
30 | 41 | deps = |
31 | 42 | sphinx-autobuild |
32 | 43 | 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 | |
34 | 45 | |
35 | 46 | [testenv:watch-readme] |
36 | 47 | deps = restview |