New upstream version 6.1.0
Sophie Brun
4 years ago
0 | 0 | repos: |
1 | - repo: https://github.com/ambv/black | |
2 | rev: 18.9b0 | |
1 | - repo: https://github.com/asottile/pyupgrade | |
2 | rev: v1.26.0 | |
3 | hooks: | |
4 | - id: pyupgrade | |
5 | args: ["--py3-plus"] | |
6 | - repo: https://github.com/psf/black | |
7 | rev: 19.10b0 | |
3 | 8 | hooks: |
4 | 9 | - id: black |
5 | language_version: python3.6 | |
10 | args: ["--target-version", "py35"] | |
11 | language_version: python3 | |
6 | 12 | - repo: https://gitlab.com/pycqa/flake8 |
7 | rev: 3.7.4 | |
13 | rev: 3.7.9 | |
8 | 14 | hooks: |
9 | 15 | - id: flake8 |
10 | additional_dependencies: ["flake8-bugbear==18.8.0"] | |
16 | additional_dependencies: [flake8-bugbear==20.1.0] | |
11 | 17 | - repo: https://github.com/asottile/blacken-docs |
12 | rev: v0.3.0 | |
18 | rev: v1.5.0-1 | |
13 | 19 | hooks: |
14 | 20 | - id: blacken-docs |
15 | additional_dependencies: [black==18.9b0] | |
21 | additional_dependencies: [black==19.10b0] | |
22 | args: ["--target-version", "py35"] | |
16 | 23 | - repo: https://github.com/pre-commit/mirrors-mypy |
17 | rev: v0.660 | |
24 | rev: v0.761 | |
18 | 25 | hooks: |
19 | 26 | - id: mypy |
20 | language_version: python3.6 | |
21 | files: ^webargs/ | |
27 | language_version: python3 | |
28 | files: ^src/webargs/ |
0 | build: | |
1 | image: latest | |
0 | version: 2 | |
1 | sphinx: | |
2 | configuration: docs/conf.py | |
3 | formats: all | |
2 | 4 | python: |
3 | version: 3.6 | |
4 | pip_install: true | |
5 | version: 3.7 | |
6 | install: | |
7 | - method: pip | |
8 | path: . | |
9 | extra_requirements: | |
10 | - docs |
0 | language: python | |
1 | cache: pip | |
2 | install: travis_retry pip install -U tox | |
3 | script: tox | |
4 | jobs: | |
5 | fast_finish: true | |
6 | ||
7 | include: | |
8 | - { python: '3.6', env: TOXENV=lint } | |
9 | ||
10 | - { python: '2.7', env: TOXENV=py27-marshmallow2 } | |
11 | - { python: '2.7', env: TOXENV=py27-marshmallow3 } | |
12 | ||
13 | - { python: '3.5', env: TOXENV=py35-marshmallow2 } | |
14 | - { python: '3.5', env: TOXENV=py35-marshmallow3 } | |
15 | ||
16 | - { python: '3.6', env: TOXENV=py36-marshmallow2 } | |
17 | - { python: '3.6', env: TOXENV=py36-marshmallow3 } | |
18 | ||
19 | - { python: '3.7', env: TOXENV=py37-marshmallow2, dist: xenial } | |
20 | - { python: '3.7', env: TOXENV=py37-marshmallow3, dist: xenial } | |
21 | ||
22 | - { python: '3.6', env: TOXENV=docs } | |
23 | ||
24 | - stage: PyPI Release | |
25 | if: tag IS present | |
26 | python: "3.6" | |
27 | install: skip | |
28 | script: skip | |
29 | deploy: | |
30 | provider: pypi | |
31 | user: sloria | |
32 | on: | |
33 | tags: true | |
34 | distributions: sdist bdist_wheel | |
35 | password: | |
36 | secure: TMeTi5OPl2cYsl5hNP4w1xESd2vQUOy8NgZ0c3KbrVSSeBuUCGOKyYJZNGzD9KDMucCvYFuxCwYiDxP8tB5iT85z3rhdVkzppZTy3/3kXMODjlhMzqTnCdJSOoZZ+D5/Y3Zqb8QxU78NggPutfX4bbUU/wNsVbdODXWHe5y2q3k= | |
37 | ||
38 | - stage: PyPI Release | |
39 | if: tag IS present | |
40 | python: "2.7" | |
41 | install: skip | |
42 | script: skip | |
43 | deploy: | |
44 | provider: pypi | |
45 | user: sloria | |
46 | on: | |
47 | tags: true | |
48 | distributions: bdist_wheel | |
49 | password: | |
50 | secure: TMeTi5OPl2cYsl5hNP4w1xESd2vQUOy8NgZ0c3KbrVSSeBuUCGOKyYJZNGzD9KDMucCvYFuxCwYiDxP8tB5iT85z3rhdVkzppZTy3/3kXMODjlhMzqTnCdJSOoZZ+D5/Y3Zqb8QxU78NggPutfX4bbUU/wNsVbdODXWHe5y2q3k= |
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> | |
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>`_ |
0 | 0 | Changelog |
1 | 1 | --------- |
2 | ||
3 | 6.1.0 (2020-04-05) | |
4 | ****************** | |
5 | ||
6 | Features: | |
7 | ||
8 | * Add ``fields.DelimitedTuple`` when using marshmallow 3. This behaves as a | |
9 | combination of ``fields.DelimitedList`` and ``marshmallow.fields.Tuple``. It | |
10 | takes an iterable of fields, plus a delimiter (defaults to ``,``), and parses | |
11 | delimiter-separated strings into tuples. (:pr:`509`) | |
12 | ||
13 | * Add ``__str__`` and ``__repr__`` to MultiDictProxy to make it easier to work | |
14 | with (:pr:`488`) | |
15 | ||
16 | Support: | |
17 | ||
18 | * Various docs updates (:pr:`482`, :pr:`486`, :pr:`489`, :pr:`498`, :pr:`508`). | |
19 | Thanks :user:`lefterisjp`, :user:`timgates42`, and :user:`ugultopu` for the PRs. | |
20 | ||
21 | ||
22 | 6.0.0 (2020-02-27) | |
23 | ****************** | |
24 | ||
25 | Features: | |
26 | ||
27 | * ``FalconParser``: Pass request content length to ``req.stream.read`` to | |
28 | provide compatibility with ``falcon.testing`` (:pr:`477`). | |
29 | Thanks :user:`suola` for the PR. | |
30 | ||
31 | * *Backwards-incompatible*: Factorize the ``use_args`` / ``use_kwargs`` branch | |
32 | in all parsers. When ``as_kwargs`` is ``False``, arguments are now | |
33 | consistently appended to the arguments list by the ``use_args`` decorator. | |
34 | Before this change, the ``PyramidParser`` would prepend the argument list on | |
35 | each call to ``use_args``. Pyramid view functions must reverse the order of | |
36 | their arguments. (:pr:`478`) | |
37 | ||
38 | 6.0.0b8 (2020-02-16) | |
39 | ******************** | |
40 | ||
41 | Refactoring: | |
42 | ||
43 | * *Backwards-incompatible*: Use keyword-only arguments (:pr:`472`). | |
44 | ||
45 | 6.0.0b7 (2020-02-14) | |
46 | ******************** | |
47 | ||
48 | Features: | |
49 | ||
50 | * *Backwards-incompatible*: webargs will rewrite the error messages in | |
51 | ValidationErrors to be namespaced under the location which raised the error. | |
52 | The `messages` field on errors will therefore be one layer deeper with a | |
53 | single top-level key. | |
54 | ||
55 | 6.0.0b6 (2020-01-31) | |
56 | ******************** | |
57 | ||
58 | Refactoring: | |
59 | ||
60 | * Remove the cache attached to webargs parsers. Due to changes between webargs | |
61 | v5 and v6, the cache is no longer considered useful. | |
62 | ||
63 | Other changes: | |
64 | ||
65 | * Import ``Mapping`` from ``collections.abc`` in pyramidparser.py (:pr:`471`). | |
66 | Thanks :user:`tirkarthi` for the PR. | |
67 | ||
68 | 6.0.0b5 (2020-01-30) | |
69 | ******************** | |
70 | ||
71 | Refactoring: | |
72 | ||
73 | * *Backwards-incompatible*: `DelimitedList` now requires that its input be a | |
74 | string and always serializes as a string. It can still serialize and deserialize | |
75 | using another field, e.g. `DelimitedList(Int())` is still valid and requires | |
76 | that the values in the list parse as ints. | |
77 | ||
78 | 6.0.0b4 (2020-01-28) | |
79 | ******************** | |
80 | ||
81 | Bug fixes: | |
82 | ||
83 | * :cve:`CVE-2020-7965`: Don't attempt to parse JSON if request's content type is mismatched | |
84 | (bugfix from 5.5.3). | |
85 | ||
86 | 6.0.0b3 (2020-01-21) | |
87 | ******************** | |
88 | ||
89 | Features: | |
90 | ||
91 | * *Backwards-incompatible*: Support Falcon 2.0. Drop support for Falcon 1.x | |
92 | (:pr:`459`). Thanks :user:`dodumosu` and :user:`Nateyo` for the PR. | |
93 | ||
94 | 6.0.0b2 (2020-01-07) | |
95 | ******************** | |
96 | ||
97 | Other changes: | |
98 | ||
99 | * *Backwards-incompatible*: Drop support for Python 2 (:issue:`440`). | |
100 | Thanks :user:`hugovk` for the PR. | |
101 | ||
102 | 6.0.0b1 (2020-01-06) | |
103 | ******************** | |
104 | ||
105 | Features: | |
106 | ||
107 | * *Backwards-incompatible*: Schemas will now load all data from a location, not | |
108 | only data specified by fields. As a result, schemas with validators which | |
109 | examine the full input data may change in behavior. The `unknown` parameter | |
110 | on schemas may be used to alter this. For example, | |
111 | `unknown=marshmallow.EXCLUDE` will produce a behavior similar to webargs v5. | |
112 | ||
113 | Bug fixes: | |
114 | ||
115 | * *Backwards-incompatible*: All parsers now require the Content-Type to be set | |
116 | correctly when processing JSON request bodies. This impacts ``DjangoParser``, | |
117 | ``FalconParser``, ``FlaskParser``, and ``PyramidParser`` | |
118 | ||
119 | Refactoring: | |
120 | ||
121 | * *Backwards-incompatible*: Schema fields may not specify a location any | |
122 | longer, and `Parser.use_args` and `Parser.use_kwargs` now accept `location` | |
123 | (singular) instead of `locations` (plural). Instead of using a single field or | |
124 | schema with multiple `locations`, users are recommended to make multiple | |
125 | calls to `use_args` or `use_kwargs` with a distinct schema per location. For | |
126 | example, code should be rewritten like this: | |
127 | ||
128 | .. code-block:: python | |
129 | ||
130 | # webargs 5.x and older | |
131 | @parser.use_args( | |
132 | { | |
133 | "q1": ma.fields.Int(location="query"), | |
134 | "q2": ma.fields.Int(location="query"), | |
135 | "h1": ma.fields.Int(location="headers"), | |
136 | }, | |
137 | locations=("query", "headers"), | |
138 | ) | |
139 | def foo(q1, q2, h1): | |
140 | ... | |
141 | ||
142 | ||
143 | # webargs 6.x | |
144 | @parser.use_args({"q1": ma.fields.Int(), "q2": ma.fields.Int()}, location="query") | |
145 | @parser.use_args({"h1": ma.fields.Int()}, location="headers") | |
146 | def foo(q1, q2, h1): | |
147 | ... | |
148 | ||
149 | * The `location_handler` decorator has been removed and replaced with | |
150 | `location_loader`. `location_loader` serves the same purpose (letting you | |
151 | write custom hooks for loading data) but its expected method signature is | |
152 | different. See the docs on `location_loader` for proper usage. | |
153 | ||
154 | Thanks :user:`sirosen` for the PR! | |
155 | ||
156 | 5.5.3 (2020-01-28) | |
157 | ****************** | |
158 | ||
159 | Bug fixes: | |
160 | ||
161 | * :cve:`CVE-2020-7965`: Don't attempt to parse JSON if request's content type is mismatched. | |
162 | ||
163 | 5.5.2 (2019-10-06) | |
164 | ****************** | |
165 | ||
166 | Bug fixes: | |
167 | ||
168 | * Handle ``UnicodeDecodeError`` when parsing JSON payloads (:issue:`427`). | |
169 | Thanks :user:`lindycoder` for the catch and patch. | |
170 | ||
171 | 5.5.1 (2019-09-15) | |
172 | ****************** | |
173 | ||
174 | Bug fixes: | |
175 | ||
176 | * Remove usage of deprecated ``Field.fail`` when using marshmallow 3. | |
177 | ||
178 | 5.5.0 (2019-09-07) | |
179 | ****************** | |
180 | ||
181 | Support: | |
182 | ||
183 | * Various docs updates (:pr:`414`, :pr:`421`). | |
184 | ||
185 | Refactoring: | |
186 | ||
187 | * Don't mutate ``globals()`` in ``webargs.fields`` (:pr:`411`). | |
188 | * Use marshmallow 3's ``Schema.from_dict`` if available (:pr:`415`). | |
189 | ||
190 | 5.4.0 (2019-07-23) | |
191 | ****************** | |
192 | ||
193 | Changes: | |
194 | ||
195 | * Use explicit type check for `fields.DelimitedList` when deciding to | |
196 | parse value with `getlist()` (`#406 (comment) <https://github.com/marshmallow-code/webargs/issues/406#issuecomment-514446228>`_ ). | |
197 | ||
198 | Support: | |
199 | ||
200 | * Add "Parsing Lists in Query Strings" section to docs (:issue:`406`). | |
201 | ||
202 | 5.3.2 (2019-06-19) | |
203 | ****************** | |
204 | ||
205 | Bug fixes: | |
206 | ||
207 | * marshmallow 3.0.0rc7 compatibility (:pr:`395`). | |
208 | ||
209 | 5.3.1 (2019-05-05) | |
210 | ****************** | |
211 | ||
212 | Bug fixes: | |
213 | ||
214 | * marshmallow 3.0.0rc6 compatibility (:pr:`384`). | |
215 | ||
216 | 5.3.0 (2019-04-08) | |
217 | ****************** | |
218 | ||
219 | Features: | |
220 | ||
221 | * Add `"path"` location to ``AIOHTTPParser``, ``FlaskParser``, and | |
222 | ``PyramidParser`` (:pr:`379`). Thanks :user:`zhenhua32` for the PR. | |
223 | * Add ``webargs.__version_info__``. | |
224 | ||
225 | 5.2.0 (2019-03-16) | |
226 | ****************** | |
227 | ||
228 | Features: | |
229 | ||
230 | * Make the schema class used when generating a schema from a | |
231 | dict overridable (:issue:`375`). Thanks :user:`ThiefMaster`. | |
232 | ||
233 | 5.1.3 (2019-03-11) | |
234 | ****************** | |
235 | ||
236 | Bug fixes: | |
237 | ||
238 | * :cve:`CVE-2019-9710`: Fix race condition between parallel requests when the cache is used | |
239 | (:issue:`371`). Thanks :user:`ThiefMaster` for reporting and fixing. | |
2 | 240 | |
3 | 241 | 5.1.2 (2019-02-03) |
4 | 242 | ****************** |
18 | 18 | Contributing Code |
19 | 19 | ----------------- |
20 | 20 | |
21 | In General | |
22 | ++++++++++ | |
23 | ||
24 | - `PEP 8`_, when sensible. | |
25 | - Test ruthlessly. Write docs for new features. | |
26 | - Even more important than Test-Driven Development--*Human-Driven Development*. | |
27 | ||
28 | .. _`PEP 8`: http://www.python.org/dev/peps/pep-0008/ | |
29 | ||
30 | In Particular | |
31 | +++++++++++++ | |
32 | ||
33 | ||
34 | 21 | Integration with a Another Web Framework… |
35 | ***************************************** | |
22 | +++++++++++++++++++++++++++++++++++++++++ | |
36 | 23 | |
37 | 24 | …should be released as a separate package. |
38 | 25 | |
44 | 31 | the `GitHub wiki <https://github.com/marshmallow-code/webargs/wiki/Ecosystem>`_ . |
45 | 32 | |
46 | 33 | Setting Up for Local Development |
47 | ******************************** | |
34 | ++++++++++++++++++++++++++++++++ | |
48 | 35 | |
49 | 1. Fork webargs_ on GitHub. | |
36 | 1. Fork webargs_ on GitHub. | |
50 | 37 | |
51 | 38 | :: |
52 | 39 | |
62 | 49 | # After activating your virtualenv |
63 | 50 | $ pip install -e '.[dev]' |
64 | 51 | |
65 | 3. (Optional, but recommended) Install the pre-commit hooks, which will format and lint your git staged files. | |
52 | 3. (Optional, but recommended) Install the pre-commit hooks, which will format and lint your git staged files. | |
66 | 53 | |
67 | 54 | :: |
68 | 55 | |
71 | 58 | |
72 | 59 | .. note:: |
73 | 60 | |
74 | webargs uses `black <https://github.com/ambv/black>`_ for code formatting, which is only compatible with Python>=3.6. Therefore, the ``pre-commit install`` command will only work if you have the ``python3.6`` interpreter installed. | |
61 | webargs uses `black <https://github.com/ambv/black>`_ for code formatting, which is only compatible with Python>=3.6. | |
62 | Therefore, the pre-commit hooks require a minimum Python version of 3.6. | |
75 | 63 | |
76 | 64 | Git Branch Structure |
77 | ******************** | |
65 | ++++++++++++++++++++ | |
78 | 66 | |
79 | 67 | Webargs abides by the following branching model: |
80 | 68 | |
88 | 76 | **Always make a new branch for your work**, no matter how small. Also, **do not put unrelated changes in the same branch or pull request**. This makes it more difficult to merge your changes. |
89 | 77 | |
90 | 78 | Pull Requests |
91 | ************** | |
79 | ++++++++++++++ | |
92 | 80 | |
93 | 81 | 1. Create a new local branch. |
94 | 82 | |
112 | 100 | - If the pull request adds functionality, it is tested and the docs are updated. |
113 | 101 | - You've added yourself to ``AUTHORS.rst``. |
114 | 102 | |
115 | 4. Submit a pull request to ``marshmallow-code:dev`` or the appropriate maintenance branch. The `Travis CI <https://travis-ci.org/marshmallow-code/webargs>`_ build must be passing before your pull request is merged. | |
103 | 4. Submit a pull request to ``marshmallow-code:dev`` or the appropriate maintenance branch. | |
104 | The `CI <https://dev.azure.com/sloria/sloria/_build/latest?definitionId=6&branchName=dev>`_ build must be passing before your pull request is merged. | |
116 | 105 | |
117 | 106 | Running Tests |
118 | ************* | |
107 | +++++++++++++ | |
119 | 108 | |
120 | 109 | To run all tests: :: |
121 | 110 | |
125 | 114 | |
126 | 115 | $ tox -e lint |
127 | 116 | |
128 | (Optional) To run tests on Python 2.7, 3.5, 3.6, and 3.7 virtual environments (must have each interpreter installed): :: | |
117 | (Optional) To run tests in all supported Python versions in their own virtual environments (must have each interpreter installed): :: | |
129 | 118 | |
130 | 119 | $ tox |
131 | 120 | |
132 | 121 | Documentation |
133 | ************* | |
122 | +++++++++++++ | |
134 | 123 | |
135 | 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_. |
136 | 125 | |
141 | 130 | Changes in the `docs/` directory will automatically trigger a rebuild. |
142 | 131 | |
143 | 132 | Contributing Examples |
144 | ********************* | |
133 | +++++++++++++++++++++ | |
145 | 134 | |
146 | 135 | Have a usage example you'd like to share? Feel free to add it to the `examples <https://github.com/marshmallow-code/webargs/tree/dev/examples>`_ directory and send a pull request. |
147 | 136 |
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 |
5 | 5 | :target: https://pypi.org/project/webargs/ |
6 | 6 | :alt: PyPI version |
7 | 7 | |
8 | .. image:: https://badgen.net/travis/marshmallow-code/webargs | |
9 | :target: https://travis-ci.org/marshmallow-code/webargs | |
10 | :alt: TravisCI build status | |
8 | .. image:: https://dev.azure.com/sloria/sloria/_apis/build/status/marshmallow-code.webargs?branchName=dev | |
9 | :target: https://dev.azure.com/sloria/sloria/_build/latest?definitionId=6&branchName=dev | |
10 | :alt: Build status | |
11 | 11 | |
12 | 12 | .. image:: https://readthedocs.org/projects/webargs/badge/ |
13 | 13 | :target: https://webargs.readthedocs.io/ |
33 | 33 | |
34 | 34 | app = Flask(__name__) |
35 | 35 | |
36 | hello_args = {"name": fields.Str(required=True)} | |
37 | ||
38 | 36 | |
39 | 37 | @app.route("/") |
40 | @use_args(hello_args) | |
38 | @use_args({"name": fields.Str(required=True)}, location="query") | |
41 | 39 | def index(args): |
42 | 40 | return "Hello " + args["name"] |
43 | 41 | |
55 | 53 | |
56 | 54 | pip install -U webargs |
57 | 55 | |
58 | webargs supports Python >= 2.7 or >= 3.5. | |
56 | webargs supports Python >= 3.5. | |
59 | 57 | |
60 | 58 | |
61 | 59 | Documentation |
104 | 102 | |
105 | 103 | - Docs: https://webargs.readthedocs.io/ |
106 | 104 | - Changelog: https://webargs.readthedocs.io/en/latest/changelog.html |
105 | - Contributing Guidelines: https://webargs.readthedocs.io/en/latest/contributing.html | |
107 | 106 | - PyPI: https://pypi.python.org/pypi/webargs |
108 | 107 | - Issues: https://github.com/marshmallow-code/webargs/issues |
109 | 108 |
0 | trigger: | |
1 | branches: | |
2 | include: [dev, test-me-*] | |
3 | tags: | |
4 | include: ['*'] | |
5 | ||
6 | # Run builds nightly to catch incompatibilities with new marshmallow releases | |
7 | schedules: | |
8 | - cron: "0 0 * * *" | |
9 | displayName: Daily midnight build | |
10 | branches: | |
11 | include: | |
12 | - dev | |
13 | always: "true" | |
14 | ||
15 | resources: | |
16 | repositories: | |
17 | - repository: sloria | |
18 | type: github | |
19 | endpoint: github | |
20 | name: sloria/azure-pipeline-templates | |
21 | ref: refs/heads/sloria | |
22 | ||
23 | jobs: | |
24 | - template: job--python-tox.yml@sloria | |
25 | parameters: | |
26 | toxenvs: | |
27 | - lint | |
28 | ||
29 | - py35-marshmallow2 | |
30 | - py35-marshmallow3 | |
31 | ||
32 | - py36-marshmallow3 | |
33 | ||
34 | - py37-marshmallow3 | |
35 | ||
36 | - py38-marshmallow2 | |
37 | - py38-marshmallow3 | |
38 | ||
39 | - py38-marshmallowdev | |
40 | ||
41 | - docs | |
42 | os: linux | |
43 | # Build wheels | |
44 | - template: job--pypi-release.yml@sloria | |
45 | parameters: | |
46 | python: "3.7" | |
47 | distributions: "sdist bdist_wheel" | |
48 | dependsOn: | |
49 | - 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 | 27 | |
28 | Marshmallow Integration | |
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"]) | |
88 | ||
89 | marshmallow Integration | |
29 | 90 | ----------------------- |
30 | 91 | |
31 | 92 | When you need more flexibility in defining input schemas, you can pass a marshmallow `Schema <marshmallow.Schema>` instead of a dictionary to `Parser.parse <webargs.core.Parser.parse>`, `Parser.use_args <webargs.core.Parser.use_args>`, and `Parser.use_kwargs <webargs.core.Parser.use_kwargs>`. |
45 | 106 | last_name = fields.Str(missing="") |
46 | 107 | date_registered = fields.DateTime(dump_only=True) |
47 | 108 | |
48 | class Meta: | |
49 | strict = True | |
109 | # NOTE: Uncomment below two lines if you're using marshmallow 2 | |
110 | # class Meta: | |
111 | # strict = True | |
50 | 112 | |
51 | 113 | |
52 | 114 | @use_args(UserSchema()) |
62 | 124 | |
63 | 125 | |
64 | 126 | # You can add additional parameters |
65 | @use_kwargs({"posts_per_page": fields.Int(missing=10, location="query")}) | |
127 | @use_kwargs({"posts_per_page": fields.Int(missing=10)}, location="query") | |
66 | 128 | @use_args(UserSchema()) |
67 | 129 | def profile_posts(args, posts_per_page): |
68 | 130 | username = args["username"] |
71 | 133 | .. warning:: |
72 | 134 | 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. |
73 | 135 | |
74 | .. warning:: | |
75 | Any `Schema <marshmallow.Schema>` passed to `use_kwargs <webargs.core.Parser.use_kwargs>` MUST deserialize to a dictionary of data. Keep this in mind when writing `post_load <marshmallow.decorators.post_load>` methods. | |
136 | ||
137 | When to avoid `use_kwargs` | |
138 | -------------------------- | |
139 | ||
140 | Any `Schema <marshmallow.Schema>` passed to `use_kwargs <webargs.core.Parser.use_kwargs>` MUST deserialize to a dictionary of data. | |
141 | If your schema has a `post_load <marshmallow.decorators.post_load>` method | |
142 | that returns a non-dictionary, | |
143 | you should use `use_args <webargs.core.Parser.use_args>` instead. | |
144 | ||
145 | .. code-block:: python | |
146 | ||
147 | from marshmallow import Schema, fields, post_load | |
148 | from webargs.flaskparser import use_args | |
149 | ||
150 | ||
151 | class Rectangle: | |
152 | def __init__(self, length, width): | |
153 | self.length = length | |
154 | self.width = width | |
155 | ||
156 | ||
157 | class RectangleSchema(Schema): | |
158 | length = fields.Float() | |
159 | width = fields.Float() | |
160 | ||
161 | @post_load | |
162 | def make_object(self, data, **kwargs): | |
163 | return Rectangle(**data) | |
164 | ||
165 | ||
166 | @use_args(RectangleSchema) | |
167 | def post(self, rect: Rectangle): | |
168 | return f"Area: {rect.length * rect.width}" | |
169 | ||
170 | Packages such as `marshmallow-sqlalchemy <https://github.com/marshmallow-code/marshmallow-sqlalchemy>`_ and `marshmallow-dataclass <https://github.com/lovasoa/marshmallow_dataclass>`_ generate schemas that deserialize to non-dictionary objects. | |
171 | Therefore, `use_args <webargs.core.Parser.use_args>` should be used with those schemas. | |
76 | 172 | |
77 | 173 | |
78 | 174 | Schema Factories |
87 | 183 | |
88 | 184 | .. code-block:: python |
89 | 185 | |
186 | from flask import Flask | |
90 | 187 | from marshmallow import Schema, fields |
91 | 188 | from webargs.flaskparser import use_args |
189 | ||
190 | app = Flask(__name__) | |
92 | 191 | |
93 | 192 | |
94 | 193 | class UserSchema(Schema): |
99 | 198 | last_name = fields.Str(missing="") |
100 | 199 | date_registered = fields.DateTime(dump_only=True) |
101 | 200 | |
102 | class Meta: | |
103 | strict = True | |
104 | ||
105 | 201 | |
106 | 202 | def make_user_schema(request): |
107 | 203 | # Filter based on 'fields' query parameter |
108 | only = request.args.get("fields", None) | |
204 | fields = request.args.get("fields", None) | |
205 | only = fields.split(",") if fields else None | |
109 | 206 | # Respect partial updates for PATCH requests |
110 | 207 | partial = request.method == "PATCH" |
111 | 208 | # Add current request to the schema's context |
113 | 210 | |
114 | 211 | |
115 | 212 | # Pass the factory to .parse, .use_args, or .use_kwargs |
213 | @app.route("/profile/", methods=["GET", "POST", "PATCH"]) | |
116 | 214 | @use_args(make_user_schema) |
117 | 215 | def profile_view(args): |
118 | username = args["username"] | |
119 | # ... | |
216 | username = args.get("username") | |
217 | # ... | |
218 | ||
120 | 219 | |
121 | 220 | |
122 | 221 | Reducing Boilerplate |
137 | 236 | only = request.args.get("fields", None) |
138 | 237 | # Respect partial updates for PATCH requests |
139 | 238 | partial = request.method == "PATCH" |
140 | # Add current request to the schema's context | |
141 | # and ensure we're always using strict mode | |
142 | 239 | return schema_cls( |
143 | only=only, | |
144 | partial=partial, | |
145 | strict=True, | |
146 | context={"request": request}, | |
147 | **schema_kwargs | |
240 | only=only, partial=partial, context={"request": request}, **schema_kwargs | |
148 | 241 | ) |
149 | 242 | |
150 | 243 | return use_args(factory, **kwargs) |
178 | 271 | cube = args["cube"] |
179 | 272 | # ... |
180 | 273 | |
181 | .. _custom-parsers: | |
274 | .. _custom-loaders: | |
182 | 275 | |
183 | 276 | Custom Parsers |
184 | 277 | -------------- |
185 | 278 | |
186 | To add your own parser, extend :class:`Parser <webargs.core.Parser>` and implement the `parse_*` method(s) you need to override. For example, here is a custom Flask parser that handles nested query string arguments. | |
279 | To add your own parser, extend :class:`Parser <webargs.core.Parser>` and implement the `load_*` method(s) you need to override. For example, here is a custom Flask parser that handles nested query string arguments. | |
187 | 280 | |
188 | 281 | |
189 | 282 | .. code-block:: python |
212 | 305 | } |
213 | 306 | """ |
214 | 307 | |
215 | def parse_querystring(self, req, name, field): | |
216 | return core.get_value(_structure_dict(req.args), name, field) | |
308 | def load_querystring(self, req, schema): | |
309 | return _structure_dict(req.args) | |
217 | 310 | |
218 | 311 | |
219 | 312 | def _structure_dict(dict_): |
274 | 367 | path = fields.Str(required=True) |
275 | 368 | value = fields.Str(required=True) |
276 | 369 | |
277 | class Meta: | |
278 | strict = True | |
279 | ||
280 | 370 | |
281 | 371 | @app.route("/profile/", methods=["patch"]) |
282 | @use_args(PatchSchema(many=True), locations=("json",)) | |
372 | @use_args(PatchSchema(many=True)) | |
283 | 373 | def patch_blog(args): |
284 | 374 | """Implements JSON Patch for the user profile |
285 | 375 | |
294 | 384 | Mixing Locations |
295 | 385 | ---------------- |
296 | 386 | |
297 | Arguments for different locations can be specified by passing ``location`` to each field individually: | |
298 | ||
299 | .. code-block:: python | |
300 | ||
387 | Arguments for different locations can be specified by passing ``location`` to each `use_args <webargs.core.Parser.use_args>` call: | |
388 | ||
389 | .. code-block:: python | |
390 | ||
391 | # "json" is the default, used explicitly below | |
301 | 392 | @app.route("/stacked", methods=["POST"]) |
302 | @use_args( | |
303 | { | |
304 | "page": fields.Int(location="query"), | |
305 | "q": fields.Str(location="query"), | |
306 | "name": fields.Str(location="json"), | |
307 | } | |
308 | ) | |
309 | def viewfunc(args): | |
310 | page = args["page"] | |
311 | # ... | |
312 | ||
313 | Alternatively, you can pass multiple locations to `use_args <webargs.core.Parser.use_args>`: | |
314 | ||
315 | .. code-block:: python | |
316 | ||
317 | @app.route("/stacked", methods=["POST"]) | |
318 | @use_args( | |
319 | {"page": fields.Int(), "q": fields.Str(), "name": fields.Str()}, | |
320 | locations=("query", "json"), | |
321 | ) | |
322 | def viewfunc(args): | |
323 | page = args["page"] | |
324 | # ... | |
325 | ||
326 | However, this allows ``page`` and ``q`` to be passed in the request body and ``name`` to be passed as a query parameter. | |
327 | ||
328 | 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: | |
329 | ||
330 | .. code-block:: python | |
331 | ||
332 | query_args = {"page": fields.Int(), "q": fields.Int()} | |
333 | json_args = {"name": fields.Str()} | |
334 | ||
335 | ||
336 | @app.route("/stacked", methods=["POST"]) | |
337 | @use_args(query_args, locations=("query",)) | |
338 | @use_args(json_args, locations=("json",)) | |
393 | @use_args({"page": fields.Int(), "q": fields.Str()}, location="query") | |
394 | @use_args({"name": fields.Str()}, location="json") | |
339 | 395 | def viewfunc(query_parsed, json_parsed): |
340 | 396 | page = query_parsed["page"] |
341 | 397 | name = json_parsed["name"] |
347 | 403 | |
348 | 404 | import functools |
349 | 405 | |
350 | query = functools.partial(use_args, locations=("query",)) | |
351 | body = functools.partial(use_args, locations=("json",)) | |
352 | ||
353 | ||
354 | @query(query_args) | |
355 | @body(json_args) | |
406 | query = functools.partial(use_args, location="query") | |
407 | body = functools.partial(use_args, location="json") | |
408 | ||
409 | ||
410 | @query({"page": fields.Int(), "q": fields.Int()}) | |
411 | @body({"name": fields.Str()}) | |
356 | 412 | def viewfunc(query_parsed, json_parsed): |
357 | 413 | page = query_parsed["page"] |
358 | 414 | 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 | ------------------- |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | import datetime as dt |
2 | 1 | import sys |
3 | 2 | import os |
6 | 5 | # If extensions (or modules to document with autodoc) are in another directory, |
7 | 6 | # add these directories to sys.path here. If the directory is relative to the |
8 | 7 | # documentation root, use os.path.abspath to make it absolute, like shown here. |
9 | sys.path.insert(0, os.path.abspath("..")) | |
8 | sys.path.insert(0, os.path.abspath(os.path.join("..", "src"))) | |
10 | 9 | import webargs # noqa |
11 | 10 | |
12 | 11 | extensions = [ |
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 = "2014-{:%Y}, Steven Loria and contributors".format(dt.datetime.utcnow()) | |
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 |
5 | 5 | |
6 | 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. |
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 | |
14 | 22 | |
15 | 23 | app = Flask(__name__) |
16 | 24 | |
17 | hello_args = {"name": fields.Str(required=True)} | |
18 | ||
19 | 25 | |
20 | 26 | @app.route("/") |
21 | @use_args(hello_args) | |
27 | @use_args({"name": fields.Str(required=True)}, location="query") | |
22 | 28 | def index(args): |
23 | 29 | return "Hello " + args["name"] |
24 | 30 | |
29 | 35 | # curl http://localhost:5000/\?name\='World' |
30 | 36 | # Hello World |
31 | 37 | |
32 | Webargs will automatically parse: | |
38 | By default Webargs will automatically parse JSON request bodies. But it also | |
39 | has support for: | |
33 | 40 | |
34 | 41 | **Query Parameters** |
35 | 42 | :: |
43 | $ curl http://localhost:5000/\?name\='Freddie' | |
44 | Hello Freddie | |
36 | 45 | |
37 | $ curl http://localhost:5000/\?name\='Freddie' | |
38 | Hello Freddie | |
46 | # pass location="query" to use_args | |
39 | 47 | |
40 | 48 | **Form Data** |
41 | 49 | :: |
43 | 51 | $ curl -d 'name=Brian' http://localhost:5000/ |
44 | 52 | Hello Brian |
45 | 53 | |
54 | # pass location="form" to use_args | |
55 | ||
46 | 56 | **JSON Data** |
47 | 57 | :: |
48 | 58 | |
49 | 59 | $ curl -X POST -H "Content-Type: application/json" -d '{"name":"Roger"}' http://localhost:5000/ |
50 | 60 | Hello Roger |
61 | ||
62 | # pass location="json" (or omit location) to use_args | |
51 | 63 | |
52 | 64 | and, optionally: |
53 | 65 | |
104 | 116 | |
105 | 117 | license |
106 | 118 | changelog |
119 | upgrading | |
107 | 120 | authors |
108 | 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.5. It depends on `marshmallow <https://marshmallow.readthedocs.io/en/latest/>`_ >= 2.7.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"), | |
27 | "user_type": fields.Str(load_from="user-type"), | |
30 | 28 | # OR, on marshmallow 3 |
31 | # "content_type": fields.Str(data_key="Content-Type", location="headers"), | |
32 | # File uploads | |
33 | "profile_image": fields.Field( | |
34 | location="files", validate=lambda f: f.mimetype in ["image/jpeg", "image/png"] | |
35 | ), | |
29 | # "user_type": fields.Str(data_key="user-type"), | |
36 | 30 | } |
37 | 31 | |
38 | 32 | .. note:: |
104 | 98 | Request "Locations" |
105 | 99 | ------------------- |
106 | 100 | |
107 | By default, webargs will search for arguments from the URL query string (e.g. ``"/?name=foo"``), form data, and JSON data (in that order). You can explicitly specify which locations to search, like so: | |
101 | By default, webargs will search for arguments from the request body as JSON. You can specify a different location from which to load data like so: | |
108 | 102 | |
109 | 103 | .. code-block:: python |
110 | 104 | |
111 | 105 | @app.route("/register") |
112 | @use_args(user_args, locations=("json", "form")) | |
106 | @use_args(user_args, location="form") | |
113 | 107 | def register(args): |
114 | 108 | return "registration page" |
115 | 109 | |
146 | 140 | raise ValidationError("User does not exist") |
147 | 141 | |
148 | 142 | |
149 | argmap = {"id": fields.Int(validate=must_exist_in_db)} | |
150 | ||
151 | .. note:: | |
152 | ||
153 | If a validator returns ``None``, validation will pass. A validator must return ``False`` or raise a `ValidationError <webargs.core.ValidationError>` for validation to fail. | |
154 | ||
155 | .. note:: | |
156 | ||
157 | You may pass a list of validators to the ``validate`` parameter. | |
143 | args = {"id": fields.Int(validate=must_exist_in_db)} | |
144 | ||
145 | .. note:: | |
146 | ||
147 | If a validator returns ``None``, validation will pass. A validator must return ``False`` or raise a `ValidationError <webargs.core.ValidationError>` | |
148 | for validation to fail. | |
149 | ||
150 | ||
151 | There are a number of built-in validators from `marshmallow.validate <marshmallow.validate>` | |
152 | (re-exported as `webargs.validate`). | |
153 | ||
154 | .. code-block:: python | |
155 | ||
156 | from webargs import fields, validate | |
157 | ||
158 | args = { | |
159 | "name": fields.Str(required=True, validate=[validate.Length(min=1, max=9999)]), | |
160 | "age": fields.Int(validate=[validate.Range(min=1, max=999)]), | |
161 | } | |
158 | 162 | |
159 | 163 | The full arguments dictionary can also be validated by passing ``validate`` to :meth:`Parser.parse <webargs.core.Parser.parse>`, :meth:`Parser.use_args <webargs.core.Parser.use_args>`, :meth:`Parser.use_kwargs <webargs.core.Parser.use_kwargs>`. |
160 | 164 | |
191 | 195 | |
192 | 196 | |
193 | 197 | @parser.error_handler |
194 | def handle_error(error, req, schema, status_code, headers): | |
198 | def handle_error(error, req, schema, *, status_code, headers): | |
195 | 199 | raise CustomError(error.messages) |
200 | ||
201 | Parsing Lists in Query Strings | |
202 | ------------------------------ | |
203 | ||
204 | Use `fields.DelimitedList <webargs.fields.DelimitedList>` to parse comma-separated | |
205 | lists in query parameters, e.g. ``/?permissions=read,write`` | |
206 | ||
207 | .. code-block:: python | |
208 | ||
209 | from webargs import fields | |
210 | ||
211 | args = {"permissions": fields.DelimitedList(fields.Str())} | |
212 | ||
213 | If you expect repeated query parameters, e.g. ``/?repo=webargs&repo=marshmallow``, use | |
214 | `fields.List <marshmallow.fields.List>` instead. | |
215 | ||
216 | .. code-block:: python | |
217 | ||
218 | from webargs import fields | |
219 | ||
220 | args = {"repo": fields.List(fields.Str())} | |
196 | 221 | |
197 | 222 | Nesting Fields |
198 | 223 | -------------- |
211 | 236 | |
212 | 237 | .. note:: |
213 | 238 | |
214 | By default, webargs only parses nested fields using the ``json`` request location. You can, however, :ref:`implement your own parser <custom-parsers>` to add nested field functionality to the other locations. | |
239 | Of the default supported locations in webargs, only the ``json`` request location supports nested datastructures. You can, however, :ref:`implement your own data loader <custom-loaders>` to add nested field functionality to the other locations. | |
215 | 240 | |
216 | 241 | Next Steps |
217 | 242 | ---------- |
0 | Upgrading to Newer Releases | |
1 | =========================== | |
2 | ||
3 | This section documents migration paths to new releases. | |
4 | ||
5 | Upgrading to 6.0 | |
6 | ++++++++++++++++ | |
7 | ||
8 | Multiple Locations Are No Longer Supported In A Single Call | |
9 | ----------------------------------------------------------- | |
10 | ||
11 | The default location is JSON/body. | |
12 | ||
13 | Under webargs 5.x, code often did not have to specify a location. | |
14 | ||
15 | Because webargs would parse data from multiple locations automatically, users | |
16 | did not need to specify where a parameter, call it `q`, was passed. | |
17 | `q` could be in a query parameter or in a JSON or form-post body. | |
18 | ||
19 | Now, webargs requires that users specify only one location for data loading per | |
20 | `use_args` call, and `"json"` is the default. If `q` is intended to be a query | |
21 | parameter, the developer must be explicit and rewrite like so: | |
22 | ||
23 | .. code-block:: python | |
24 | ||
25 | # webargs 5.x | |
26 | @parser.use_args({"q": ma.fields.String()}) | |
27 | def foo(args): | |
28 | return some_function(user_query=args.get("q")) | |
29 | ||
30 | ||
31 | # webargs 6.x | |
32 | @parser.use_args({"q": ma.fields.String()}, location="query") | |
33 | def foo(args): | |
34 | return some_function(user_query=args.get("q")) | |
35 | ||
36 | This also means that another usage from 5.x is not supported. Code with | |
37 | multiple locations in a single `use_args`, `use_kwargs`, or `parse` call | |
38 | must be rewritten in multiple separate `use_args` or `use_kwargs` invocations, | |
39 | like so: | |
40 | ||
41 | .. code-block:: python | |
42 | ||
43 | # webargs 5.x | |
44 | @parser.use_kwargs( | |
45 | { | |
46 | "q1": ma.fields.Int(location="query"), | |
47 | "q2": ma.fields.Int(location="query"), | |
48 | "h1": ma.fields.Int(location="headers"), | |
49 | }, | |
50 | locations=("query", "headers"), | |
51 | ) | |
52 | def foo(q1, q2, h1): | |
53 | ... | |
54 | ||
55 | ||
56 | # webargs 6.x | |
57 | @parser.use_kwargs({"q1": ma.fields.Int(), "q2": ma.fields.Int()}, location="query") | |
58 | @parser.use_kwargs({"h1": ma.fields.Int()}, location="headers") | |
59 | def foo(q1, q2, h1): | |
60 | ... | |
61 | ||
62 | ||
63 | Fields No Longer Support location=... | |
64 | ------------------------------------- | |
65 | ||
66 | Because a single `parser.use_args`, `parser.use_kwargs`, or `parser.parse` call | |
67 | cannot specify multiple locations, it is not necessary for a field to be able | |
68 | to specify its location. Rewrite code like so: | |
69 | ||
70 | .. code-block:: python | |
71 | ||
72 | # webargs 5.x | |
73 | @parser.use_args({"q": ma.fields.String(location="query")}) | |
74 | def foo(args): | |
75 | return some_function(user_query=args.get("q")) | |
76 | ||
77 | ||
78 | # webargs 6.x | |
79 | @parser.use_args({"q": ma.fields.String()}, location="query") | |
80 | def foo(args): | |
81 | return some_function(user_query=args.get("q")) | |
82 | ||
83 | location_handler Has Been Replaced With location_loader | |
84 | ------------------------------------------------------- | |
85 | ||
86 | This is not just a name change. The expected signature of a `location_loader` | |
87 | is slightly different from the signature for a `location_handler`. | |
88 | ||
89 | Where previously a `location_handler` code took the incoming request data and | |
90 | details of a single field being loaded, a `location_loader` takes the request | |
91 | and the schema as a pair. It does not return a specific field's data, but data | |
92 | for the whole location. | |
93 | ||
94 | Rewrite code like this: | |
95 | ||
96 | .. code-block:: python | |
97 | ||
98 | # webargs 5.x | |
99 | @parser.location_handler("data") | |
100 | def load_data(request, name, field): | |
101 | return request.data.get(name) | |
102 | ||
103 | ||
104 | # webargs 6.x | |
105 | @parser.location_loader("data") | |
106 | def load_data(request, schema): | |
107 | return request.data | |
108 | ||
109 | Data Is Not Filtered Before Being Passed To Schemas, And It May Be Proxified | |
110 | ---------------------------------------------------------------------------- | |
111 | ||
112 | In webargs 5.x, the deserialization schema was used to pull data out of the | |
113 | request object. That data was compiled into a dictionary which was then passed | |
114 | to the schema. | |
115 | ||
116 | One of the major changes in webargs 6.x allows the use of `unknown` parameter | |
117 | on schemas. This lets a schema decide what to do with fields not specified in | |
118 | the schema. In order to achieve this, webargs now passes the full data from | |
119 | the specified location to the schema. | |
120 | ||
121 | Therefore, users should specify `unknown=marshmallow.EXCLUDE` on their schemas in | |
122 | order to filter out unknown fields. Like so: | |
123 | ||
124 | .. code-block:: python | |
125 | ||
126 | # webargs 5.x | |
127 | # this can assume that "q" is the only parameter passed, and all other | |
128 | # parameters will be ignored | |
129 | @parser.use_kwargs({"q": ma.fields.String()}, locations=("query",)) | |
130 | def foo(q): | |
131 | ... | |
132 | ||
133 | ||
134 | # webargs 6.x, Solution 1: declare a schema with Meta.unknown set | |
135 | class QuerySchema(ma.Schema): | |
136 | q = ma.fields.String() | |
137 | ||
138 | class Meta: | |
139 | unknown = ma.EXCLUDE | |
140 | ||
141 | ||
142 | @parser.use_kwargs(QuerySchema, location="query") | |
143 | def foo(q): | |
144 | ... | |
145 | ||
146 | ||
147 | # webargs 6.x, Solution 2: instantiate a schema with unknown set | |
148 | class QuerySchema(ma.Schema): | |
149 | q = ma.fields.String() | |
150 | ||
151 | ||
152 | @parser.use_kwargs(QuerySchema(unknown=ma.EXCLUDE), location="query") | |
153 | def foo(q): | |
154 | ... | |
155 | ||
156 | ||
157 | This also allows usage which passes the unknown parameters through, like so: | |
158 | ||
159 | .. code-block:: python | |
160 | ||
161 | # webargs 6.x only! cannot be done in 5.x | |
162 | class QuerySchema(ma.Schema): | |
163 | q = ma.fields.String() | |
164 | ||
165 | ||
166 | # will pass *all* query params through as "kwargs" | |
167 | @parser.use_kwargs(QuerySchema(unknown=ma.INCLUDE), location="query") | |
168 | def foo(q, **kwargs): | |
169 | ... | |
170 | ||
171 | ||
172 | However, many types of request data are so-called "multidicts" -- dictionary-like | |
173 | types which can return one or multiple values. To handle `marshmallow.fields.List` | |
174 | and `webargs.fields.DelimitedList` fields correctly, passing list data, webargs | |
175 | must combine schema information with the raw request data. This is done in the | |
176 | :class:`MultiDictProxy <webargs.multidictproxy.MultiDictProxy>` type, which | |
177 | will often be passed to schemas. | |
178 | ||
179 | This means that if a schema has a `pre_load` hook which interacts with the data, | |
180 | it may need modifications. For example, a `flask` query string will be parsed | |
181 | into an `ImmutableMultiDict` type, which will break pre-load hooks which modify | |
182 | the data in-place. Such usages need rewrites like so: | |
183 | ||
184 | .. code-block:: python | |
185 | ||
186 | # webargs 5.x | |
187 | # flask query params is just an example -- applies to several types | |
188 | from webargs.flaskparser import use_kwargs | |
189 | ||
190 | ||
191 | class QuerySchema(ma.Schema): | |
192 | q = ma.fields.String() | |
193 | ||
194 | @ma.pre_load | |
195 | def convert_nil_to_none(self, obj, **kwargs): | |
196 | if obj.get("q") == "nil": | |
197 | obj["q"] = None | |
198 | return obj | |
199 | ||
200 | ||
201 | @use_kwargs(QuerySchema, locations=("query",)) | |
202 | def foo(q): | |
203 | ... | |
204 | ||
205 | ||
206 | # webargs 6.x | |
207 | class QuerySchema(ma.Schema): | |
208 | q = ma.fields.String() | |
209 | ||
210 | # unlike under 5.x, we cannot modify 'obj' in-place because writing | |
211 | # to the MultiDictProxy will try to write to the underlying | |
212 | # ImmutableMultiDict, which is not allowed | |
213 | @ma.pre_load | |
214 | def convert_nil_to_none(self, obj, **kwargs): | |
215 | # creating a dict from a MultiDictProxy works well because it | |
216 | # "unwraps" lists and delimited lists correctly | |
217 | data = dict(obj) | |
218 | if data.get("q") == "nil": | |
219 | data["q"] = None | |
220 | return data | |
221 | ||
222 | ||
223 | @parser.use_kwargs(QuerySchema, location="query") | |
224 | def foo(q): | |
225 | ... | |
226 | ||
227 | ||
228 | DelimitedList Now Only Takes A String Input | |
229 | ------------------------------------------- | |
230 | ||
231 | Combining `List` and string parsing functionality in a single type had some | |
232 | messy corner cases. For the most part, this should not require rewrites. But | |
233 | for APIs which need to allow both usages, rewrites are possible like so: | |
234 | ||
235 | .. code-block:: python | |
236 | ||
237 | # webargs 5.x | |
238 | # this allows ...?x=1&x=2&x=3 | |
239 | # as well as ...?x=1,2,3 | |
240 | @use_kwargs({"x": webargs.fields.DelimitedList(ma.fields.Int)}, locations=("query",)) | |
241 | def foo(x): | |
242 | ... | |
243 | ||
244 | ||
245 | # webargs 6.x | |
246 | # this accepts x=1,2,3 but NOT x=1&x=2&x=3 | |
247 | @use_kwargs({"x": webargs.fields.DelimitedList(ma.fields.Int)}, location="query") | |
248 | def foo(x): | |
249 | ... | |
250 | ||
251 | ||
252 | # webargs 6.x | |
253 | # this accepts x=1,2,3 ; x=1&x=2&x=3 ; x=1,2&x=3 | |
254 | # to do this, it needs a post_load hook which will flatten out the list data | |
255 | class UnpackingDelimitedListSchema(ma.Schema): | |
256 | x = ma.fields.List(webargs.fields.DelimitedList(ma.fields.Int)) | |
257 | ||
258 | @ma.post_load | |
259 | def flatten_lists(self, data, **kwargs): | |
260 | new_x = [] | |
261 | for x in data["x"]: | |
262 | new_x.extend(x) | |
263 | data["x"] = new_x | |
264 | return data | |
265 | ||
266 | ||
267 | @parser.use_kwargs(UnpackingDelimitedListSchema, location="query") | |
268 | def foo(x): | |
269 | ... | |
270 | ||
271 | ||
272 | ValidationError Messages Are Namespaced Under The Location | |
273 | ---------------------------------------------------------- | |
274 | ||
275 | Code parsing ValidationError messages will notice a change in the messages | |
276 | produced by webargs. | |
277 | What would previously have come back with messages like `{"foo":["Not a valid integer."]}` | |
278 | will now have messages nested one layer deeper, like | |
279 | `{"json":{"foo":["Not a valid integer."]}}`. | |
280 | ||
281 | To rewrite code which was handling these errors, the handler will need to be | |
282 | prepared to traverse messages by one additional level. For example: | |
283 | ||
284 | .. code-block:: python | |
285 | ||
286 | import logging | |
287 | ||
288 | log = logging.getLogger(__name__) | |
289 | ||
290 | ||
291 | # webargs 5.x | |
292 | # logs debug messages like | |
293 | # bad value for 'foo': ["Not a valid integer."] | |
294 | # bad value for 'bar': ["Not a valid boolean."] | |
295 | def log_invalid_parameters(validation_error): | |
296 | for field, messages in validation_error.messages.items(): | |
297 | log.debug("bad value for '{}': {}".format(field, messages)) | |
298 | ||
299 | ||
300 | # webargs 6.x | |
301 | # logs debug messages like | |
302 | # bad value for 'foo' [query]: ["Not a valid integer."] | |
303 | # bad value for 'bar' [json]: ["Not a valid boolean."] | |
304 | def log_invalid_parameters(validation_error): | |
305 | for location, fielddata in validation_error.messages.items(): | |
306 | for field, messages in fielddata.items(): | |
307 | log.debug("bad value for '{}' [{}]: {}".format(field, location, messages)) | |
308 | ||
309 | ||
310 | Some Functions Take Keyword-Only Arguments Now | |
311 | ---------------------------------------------- | |
312 | ||
313 | The signature of several methods has changed to have keyword-only arguments. | |
314 | For the most part, this should not require any changes, but here's a list of | |
315 | the changes. | |
316 | ||
317 | `parser.error_handler` methods: | |
318 | ||
319 | .. code-block:: python | |
320 | ||
321 | # webargs 5.x | |
322 | def handle_error(error, req, schema, status_code, headers): | |
323 | ... | |
324 | ||
325 | ||
326 | # webargs 6.x | |
327 | def handle_error(error, req, schema, *, status_code, headers): | |
328 | ... | |
329 | ||
330 | `parser.__init__` methods: | |
331 | ||
332 | .. code-block:: python | |
333 | ||
334 | # webargs 5.x | |
335 | def __init__(self, location=None, error_handler=None, schema_class=None): | |
336 | ... | |
337 | ||
338 | ||
339 | # webargs 6.x | |
340 | def __init__(self, location=None, *, error_handler=None, schema_class=None): | |
341 | ... | |
342 | ||
343 | `parser.parse`, `parser.use_args`, and `parser.use_kwargs` methods: | |
344 | ||
345 | ||
346 | .. code-block:: python | |
347 | ||
348 | # webargs 5.x | |
349 | def parse( | |
350 | self, | |
351 | argmap, | |
352 | req=None, | |
353 | location=None, | |
354 | validate=None, | |
355 | error_status_code=None, | |
356 | error_headers=None, | |
357 | ): | |
358 | ... | |
359 | ||
360 | ||
361 | # webargs 6.x | |
362 | def parse( | |
363 | self, | |
364 | argmap, | |
365 | req=None, | |
366 | *, | |
367 | location=None, | |
368 | validate=None, | |
369 | error_status_code=None, | |
370 | error_headers=None | |
371 | ): | |
372 | ... | |
373 | ||
374 | ||
375 | # webargs 5.x | |
376 | def use_args( | |
377 | self, | |
378 | argmap, | |
379 | req=None, | |
380 | location=None, | |
381 | as_kwargs=False, | |
382 | validate=None, | |
383 | error_status_code=None, | |
384 | error_headers=None, | |
385 | ): | |
386 | ... | |
387 | ||
388 | ||
389 | # webargs 6.x | |
390 | def use_args( | |
391 | self, | |
392 | argmap, | |
393 | req=None, | |
394 | *, | |
395 | location=None, | |
396 | as_kwargs=False, | |
397 | validate=None, | |
398 | error_status_code=None, | |
399 | error_headers=None | |
400 | ): | |
401 | ... | |
402 | ||
403 | ||
404 | # use_kwargs is just an alias for use_args with as_kwargs=True | |
405 | ||
406 | and finally, the `dict2schema` function: | |
407 | ||
408 | .. code-block:: python | |
409 | ||
410 | # webargs 5.x | |
411 | def dict2schema(dct, schema_class=ma.Schema): | |
412 | ... | |
413 | ||
414 | ||
415 | # webargs 6.x | |
416 | def dict2schema(dct, *, schema_class=ma.Schema): | |
417 | ... | |
418 | ||
419 | ||
420 | PyramidParser Now Appends Arguments (Used To Prepend) | |
421 | ----------------------------------------------------- | |
422 | ||
423 | `PyramidParser.use_args` was not conformant with the other parsers in webargs. | |
424 | While all other parsers added new arguments to the end of the argument list of | |
425 | a decorated view function, the Pyramid implementation added them to the front | |
426 | of the argument list. | |
427 | ||
428 | This has been corrected, but as a result pyramid views with `use_args` may need | |
429 | to be rewritten. The `request` object is always passed first in both versions, | |
430 | so the issue is only apparent with view functions taking other positional | |
431 | arguments. | |
432 | ||
433 | For example, imagine code with a decorator for passing user information, | |
434 | `pass_userinfo`, like so: | |
435 | ||
436 | .. code-block:: python | |
437 | ||
438 | # a decorator which gets information about the authenticated user | |
439 | def pass_userinfo(f): | |
440 | def decorator(request, *args, **kwargs): | |
441 | return f(request, get_userinfo(), *args, **kwargs) | |
442 | ||
443 | return decorator | |
444 | ||
445 | You will see a behavioral change if `pass_userinfo` is called on a function | |
446 | decorated with `use_args`. The difference between the two versions will be like | |
447 | so: | |
448 | ||
449 | .. code-block:: python | |
450 | ||
451 | from webargs.pyramidparser import use_args | |
452 | ||
453 | # webargs 5.x | |
454 | # pass_userinfo is called first, webargs sees positional arguments of | |
455 | # (userinfo,) | |
456 | # and changes it to | |
457 | # (request, args, userinfo) | |
458 | @pass_userinfo | |
459 | @use_args({"q": ma.fields.String()}, locations=("query",)) | |
460 | def viewfunc(request, args, userinfo): | |
461 | q = args.get("q") | |
462 | ... | |
463 | ||
464 | ||
465 | # webargs 6.x | |
466 | # pass_userinfo is called first, webargs sees positional arguments of | |
467 | # (userinfo,) | |
468 | # and changes it to | |
469 | # (request, userinfo, args) | |
470 | @pass_userinfo | |
471 | @use_args({"q": ma.fields.String()}, location="query") | |
472 | def viewfunc(request, userinfo, args): | |
473 | q = args.get("q") | |
474 | ... |
0 | 0 | """Example of using Python 3 function annotations to define |
1 | 1 | request arguments and output schemas. |
2 | ||
3 | Run the app: | |
4 | ||
5 | $ python examples/annotations_example.py | |
6 | ||
7 | Try the following with httpie (a cURL-like utility, http://httpie.org): | |
8 | ||
9 | $ pip install httpie | |
10 | $ http GET :5001/ | |
11 | $ http GET :5001/ name==Ada | |
12 | $ http POST :5001/add x=40 y=2 | |
13 | $ http GET :5001/users/42 | |
2 | 14 | """ |
3 | import datetime as dt | |
15 | import random | |
4 | 16 | import functools |
5 | 17 | |
6 | from flask import Flask, jsonify, request | |
18 | from flask import Flask, request | |
7 | 19 | from marshmallow import Schema |
8 | 20 | from webargs import fields |
9 | 21 | from webargs.flaskparser import parser |
10 | 22 | |
23 | ||
11 | 24 | app = Flask(__name__) |
12 | 25 | |
26 | ##### Routing wrapper #### | |
13 | 27 | |
14 | def route(*args, response_formatter=jsonify, **kwargs): | |
28 | ||
29 | def route(*args, **kwargs): | |
15 | 30 | """Combines `Flask.route` and webargs parsing. Allows arguments to be specified |
16 | 31 | as function annotations. An output schema can optionally be specified by a |
17 | 32 | return annotation. |
28 | 43 | if isinstance(value, fields.Field) and name != "return" |
29 | 44 | } |
30 | 45 | response_schema = annotations.get("return") |
31 | parsed = parser.parse(reqargs, request) | |
46 | schema_cls = Schema.from_dict(reqargs) | |
47 | partial = request.method != "POST" | |
48 | parsed = parser.parse(schema_cls(partial=partial), request) | |
32 | 49 | kw.update(parsed) |
33 | 50 | response_data = func(*a, **kw) |
34 | 51 | if response_schema: |
35 | return response_formatter(response_schema.dump(response_data).data) | |
52 | return response_schema.dump(response_data) | |
36 | 53 | else: |
37 | return response_formatter(func(*a, **kw)) | |
54 | return func(*a, **kw) | |
38 | 55 | |
39 | 56 | return wrapped_view |
40 | 57 | |
41 | 58 | return decorator |
42 | 59 | |
43 | 60 | |
61 | ##### Fake database and model ##### | |
62 | ||
63 | ||
64 | class Model: | |
65 | def __init__(self, **kwargs): | |
66 | self.__dict__.update(kwargs) | |
67 | ||
68 | def update(self, **kwargs): | |
69 | self.__dict__.update(kwargs) | |
70 | ||
71 | @classmethod | |
72 | def insert(cls, db, **kwargs): | |
73 | collection = db[cls.collection] | |
74 | new_id = None | |
75 | if "id" in kwargs: # for setting up fixtures | |
76 | new_id = kwargs.pop("id") | |
77 | else: # find a new id | |
78 | found_id = False | |
79 | while not found_id: | |
80 | new_id = random.randint(1, 9999) | |
81 | if new_id not in collection: | |
82 | found_id = True | |
83 | new_record = cls(id=new_id, **kwargs) | |
84 | collection[new_id] = new_record | |
85 | return new_record | |
86 | ||
87 | ||
88 | class User(Model): | |
89 | collection = "users" | |
90 | ||
91 | ||
92 | db = {"users": {}} | |
93 | ||
94 | ##### Views ##### | |
95 | ||
96 | ||
44 | 97 | @route("/", methods=["GET"]) |
45 | def index(name: fields.Str(missing="Friend")): # noqa: E252 | |
98 | def index(name: fields.Str(missing="Friend")): | |
46 | 99 | return {"message": "Hello, {}!".format(name)} |
47 | 100 | |
48 | 101 | |
49 | 102 | @route("/add", methods=["POST"]) |
50 | def add(x: fields.Float(required=True), y: fields.Float(required=True)): # noqa: E252 | |
103 | def add(x: fields.Float(required=True), y: fields.Float(required=True)): | |
51 | 104 | return {"result": x + y} |
52 | 105 | |
53 | 106 | |
54 | 107 | class UserSchema(Schema): |
55 | id = fields.Int(required=True) | |
56 | name = fields.Str(required=True) | |
57 | date_created = fields.DateTime(dump_only=True) | |
108 | id = fields.Int(dump_only=True) | |
109 | username = fields.Str(required=True) | |
110 | first_name = fields.Str() | |
111 | last_name = fields.Str() | |
58 | 112 | |
59 | 113 | |
60 | class User: | |
61 | def __init__(self, id, name): | |
62 | self.id = id | |
63 | self.name = name | |
64 | self.date_created = dt.datetime.utcnow() | |
65 | ||
66 | ||
67 | @route("/users/<int:user_id>", methods=["POST"]) | |
68 | def user_detail(user_id, name: fields.Str(required=True)) -> UserSchema(): # noqa: E252 | |
69 | user = User(id=user_id, name=name) | |
114 | @route("/users/<int:user_id>", methods=["GET", "PATCH"]) | |
115 | def user_detail(user_id, username: fields.Str(required=True) = None) -> UserSchema(): | |
116 | user = db["users"].get(user_id) | |
117 | if not user: | |
118 | return {"message": "User not found"}, 404 | |
119 | if request.method == "PATCH": | |
120 | user.update(username=username) | |
70 | 121 | return user |
71 | 122 | |
72 | 123 | |
77 | 128 | headers = err.data.get("headers", None) |
78 | 129 | messages = err.data.get("messages", ["Invalid request."]) |
79 | 130 | if headers: |
80 | return jsonify({"errors": messages}), err.code, headers | |
131 | return {"errors": messages}, err.code, headers | |
81 | 132 | else: |
82 | return jsonify({"errors": messages}), err.code | |
133 | return {"errors": messages}, err.code | |
83 | 134 | |
84 | 135 | |
85 | 136 | if __name__ == "__main__": |
137 | User.insert( | |
138 | db=db, id=42, username="fred", first_name="Freddie", last_name="Mercury" | |
139 | ) | |
86 | 140 | app.run(port=5001, debug=True) |
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 = { |
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 | """ |
8 | 8 | $ pip install httpie |
9 | 9 | $ http GET :5001/users/ |
10 | 10 | $ http GET :5001/users/42 |
11 | $ http POST :5001/users/ usename=brian first_name=Brian last_name=May | |
11 | $ http POST :5001/users/ username=brian first_name=Brian last_name=May | |
12 | 12 | $ http PATCH :5001/users/42 username=freddie |
13 | 13 | $ http GET :5001/users/ limit==1 |
14 | 14 | """ |
15 | 15 | import functools |
16 | from flask import Flask, request, jsonify | |
16 | from flask import Flask, request | |
17 | 17 | import random |
18 | 18 | |
19 | 19 | from marshmallow import Schema, fields, post_dump |
21 | 21 | |
22 | 22 | app = Flask(__name__) |
23 | 23 | |
24 | ##### Fake database and models ##### | |
24 | ##### Fake database and model ##### | |
25 | 25 | |
26 | 26 | |
27 | 27 | class Model: |
58 | 58 | ##### use_schema ##### |
59 | 59 | |
60 | 60 | |
61 | def use_schema(schema, list_view=False, locations=None): | |
61 | def use_schema(schema_cls, list_view=False, locations=None): | |
62 | 62 | """View decorator for using a marshmallow schema to |
63 | 63 | (1) parse a request's input and |
64 | 64 | (2) serializing the view's output to a JSON response. |
67 | 67 | def decorator(func): |
68 | 68 | @functools.wraps(func) |
69 | 69 | def wrapped(*args, **kwargs): |
70 | partial = request.method != "POST" | |
71 | schema = schema_cls(partial=partial) | |
70 | 72 | use_args_wrapper = parser.use_args(schema, locations=locations) |
71 | 73 | # Function wrapped with use_args |
72 | 74 | func_with_args = use_args_wrapper(func) |
73 | 75 | ret = func_with_args(*args, **kwargs) |
74 | # Serialize and jsonify the return value | |
75 | return jsonify(schema.dump(ret, many=list_view).data) | |
76 | return schema.dump(ret, many=list_view) | |
76 | 77 | |
77 | 78 | return wrapped |
78 | 79 | |
84 | 85 | |
85 | 86 | class UserSchema(Schema): |
86 | 87 | id = fields.Int(dump_only=True) |
87 | username = fields.Str() | |
88 | username = fields.Str(required=True) | |
88 | 89 | first_name = fields.Str() |
89 | 90 | last_name = fields.Str() |
90 | 91 | |
91 | class Meta: | |
92 | strict = True | |
93 | ||
94 | 92 | @post_dump(pass_many=True) |
95 | def wrap_with_envelope(self, data, many): | |
93 | def wrap_with_envelope(self, data, many, **kwargs): | |
96 | 94 | return {"data": data} |
97 | 95 | |
98 | 96 | |
100 | 98 | |
101 | 99 | |
102 | 100 | @app.route("/users/<int:user_id>", methods=["GET", "PATCH"]) |
103 | @use_schema(UserSchema()) | |
101 | @use_schema(UserSchema) | |
104 | 102 | def user_detail(reqargs, user_id): |
105 | 103 | user = db["users"].get(user_id) |
106 | 104 | if not user: |
107 | return jsonify({"message": "User not found"}), 404 | |
105 | return {"message": "User not found"}, 404 | |
108 | 106 | if request.method == "PATCH" and reqargs: |
109 | 107 | user.update(**reqargs) |
110 | 108 | return user |
113 | 111 | # You can add additional arguments with use_kwargs |
114 | 112 | @app.route("/users/", methods=["GET", "POST"]) |
115 | 113 | @use_kwargs({"limit": fields.Int(missing=10, location="query")}) |
116 | @use_schema(UserSchema(), list_view=True) | |
114 | @use_schema(UserSchema, list_view=True) | |
117 | 115 | def user_list(reqargs, limit): |
118 | 116 | users = db["users"].values() |
119 | 117 | if request.method == "POST": |
133 | 131 | headers = None |
134 | 132 | messages = ["Invalid request."] |
135 | 133 | if headers: |
136 | return jsonify({"errors": messages}), err.code, headers | |
134 | return {"errors": messages}, err.code, headers | |
137 | 135 | else: |
138 | return jsonify({"errors": messages}), err.code | |
136 | return {"errors": messages}, err.code | |
139 | 137 | |
140 | 138 | |
141 | 139 | if __name__ == "__main__": |
0 | 0 | #!/usr/bin/env python |
1 | # -*- coding: utf-8 -*- | |
2 | 1 | """A Hello, World! example using Webapp2 in a Google App Engine environment |
3 | 2 | |
4 | 3 | Run the app: |
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 | ||
5 | INSTALL_REQUIRES = ["marshmallow>=2.15.2"] | |
6 | if sys.version_info[0] < 3: | |
7 | INSTALL_REQUIRES.append("simplejson>=2.1.0") | |
8 | 2 | |
9 | 3 | FRAMEWORKS = [ |
10 | 4 | "Flask>=0.12.2", |
13 | 7 | "tornado>=4.5.2", |
14 | 8 | "pyramid>=1.9.1", |
15 | 9 | "webapp2>=3.0.0b1", |
16 | "falcon>=1.4.0", | |
17 | 'aiohttp>=3.0.0; python_version >= "3.5"', | |
10 | "falcon>=2.0.0", | |
11 | "aiohttp>=3.0.0", | |
18 | 12 | ] |
19 | 13 | EXTRAS_REQUIRE = { |
20 | 14 | "frameworks": FRAMEWORKS, |
21 | 15 | "tests": [ |
22 | 16 | "pytest", |
23 | "mock", | |
24 | "webtest==2.0.32", | |
25 | 'webtest-aiohttp==2.0.0; python_version >= "3.5"', | |
26 | 'pytest-aiohttp>=0.3.0; python_version >= "3.5"', | |
17 | 'mock; python_version == "3.5"', | |
18 | "webtest==2.0.35", | |
19 | "webtest-aiohttp==2.0.0", | |
20 | "pytest-aiohttp>=0.3.0", | |
27 | 21 | ] |
28 | 22 | + FRAMEWORKS, |
29 | 23 | "lint": [ |
30 | 'mypy==0.650; python_version >= "3.5"', | |
31 | "flake8==3.6.0", | |
32 | 'flake8-bugbear==18.8.0; python_version >= "3.5"', | |
33 | "pre-commit==1.13.0", | |
24 | "mypy==0.770", | |
25 | "flake8==3.7.9", | |
26 | "flake8-bugbear==20.1.4", | |
27 | "pre-commit>=1.20,<3.0", | |
34 | 28 | ], |
29 | "docs": ["Sphinx==3.0.3", "sphinx-issues==1.2.0", "sphinx-typlog-theme==0.8.0"] | |
30 | + FRAMEWORKS, | |
35 | 31 | } |
36 | 32 | EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"] |
37 | 33 | |
61 | 57 | |
62 | 58 | setup( |
63 | 59 | name="webargs", |
64 | version=find_version("webargs/__init__.py"), | |
60 | version=find_version("src/webargs/__init__.py"), | |
65 | 61 | description=( |
66 | 62 | "Declarative parsing and validation of HTTP request objects, " |
67 | 63 | "with built-in support for popular web frameworks, including " |
71 | 67 | author="Steven Loria", |
72 | 68 | author_email="[email protected]", |
73 | 69 | url="https://github.com/marshmallow-code/webargs", |
74 | packages=find_packages(exclude=("test*", "examples")), | |
75 | package_dir={"webargs": "webargs"}, | |
76 | install_requires=INSTALL_REQUIRES, | |
70 | packages=find_packages("src"), | |
71 | package_dir={"": "src"}, | |
72 | install_requires=["marshmallow>=2.15.2"], | |
77 | 73 | extras_require=EXTRAS_REQUIRE, |
78 | 74 | license="MIT", |
79 | 75 | zip_safe=False, |
94 | 90 | "api", |
95 | 91 | "marshmallow", |
96 | 92 | ), |
93 | python_requires=">=3.5", | |
97 | 94 | classifiers=[ |
98 | 95 | "Development Status :: 5 - Production/Stable", |
99 | 96 | "Intended Audience :: Developers", |
100 | 97 | "License :: OSI Approved :: MIT License", |
101 | 98 | "Natural Language :: English", |
102 | "Programming Language :: Python :: 2", | |
103 | "Programming Language :: Python :: 2.7", | |
104 | 99 | "Programming Language :: Python :: 3", |
105 | 100 | "Programming Language :: Python :: 3.5", |
106 | 101 | "Programming Language :: Python :: 3.6", |
107 | 102 | "Programming Language :: Python :: 3.7", |
103 | "Programming Language :: Python :: 3.8", | |
104 | "Programming Language :: Python :: 3 :: Only", | |
108 | 105 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", |
109 | 106 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", |
110 | 107 | ], |
0 | from distutils.version import LooseVersion | |
1 | from marshmallow.utils import missing | |
2 | ||
3 | # Make marshmallow's validation functions importable from webargs | |
4 | from marshmallow import validate | |
5 | ||
6 | from webargs.core import ValidationError | |
7 | from webargs.dict2schema import dict2schema | |
8 | from webargs import fields | |
9 | ||
10 | __version__ = "6.1.0" | |
11 | __version_info__ = tuple(LooseVersion(__version__).version) | |
12 | __all__ = ("dict2schema", "ValidationError", "fields", "missing", "validate") |
0 | """aiohttp request argument parsing module. | |
1 | ||
2 | Example: :: | |
3 | ||
4 | import asyncio | |
5 | from aiohttp import web | |
6 | ||
7 | from webargs import fields | |
8 | from webargs.aiohttpparser import use_args | |
9 | ||
10 | ||
11 | hello_args = { | |
12 | 'name': fields.Str(required=True) | |
13 | } | |
14 | @asyncio.coroutine | |
15 | @use_args(hello_args) | |
16 | def index(request, args): | |
17 | return web.Response( | |
18 | body='Hello {}'.format(args['name']).encode('utf-8') | |
19 | ) | |
20 | ||
21 | app = web.Application() | |
22 | app.router.add_route('GET', '/', index) | |
23 | """ | |
24 | import typing | |
25 | ||
26 | from aiohttp import web | |
27 | from aiohttp.web import Request | |
28 | from aiohttp import web_exceptions | |
29 | from marshmallow import Schema, ValidationError | |
30 | ||
31 | from webargs import core | |
32 | from webargs.core import json | |
33 | from webargs.asyncparser import AsyncParser | |
34 | from webargs.multidictproxy import MultiDictProxy | |
35 | ||
36 | ||
37 | def is_json_request(req: Request) -> bool: | |
38 | content_type = req.content_type | |
39 | return core.is_json(content_type) | |
40 | ||
41 | ||
42 | class HTTPUnprocessableEntity(web.HTTPClientError): | |
43 | status_code = 422 | |
44 | ||
45 | ||
46 | # Mapping of status codes to exception classes | |
47 | # Adapted from werkzeug | |
48 | exception_map = {422: HTTPUnprocessableEntity} | |
49 | ||
50 | ||
51 | def _find_exceptions() -> None: | |
52 | for name in web_exceptions.__all__: | |
53 | obj = getattr(web_exceptions, name) | |
54 | try: | |
55 | is_http_exception = issubclass(obj, web_exceptions.HTTPException) | |
56 | except TypeError: | |
57 | is_http_exception = False | |
58 | if not is_http_exception or obj.status_code is None: | |
59 | continue | |
60 | old_obj = exception_map.get(obj.status_code, None) | |
61 | if old_obj is not None and issubclass(obj, old_obj): | |
62 | continue | |
63 | exception_map[obj.status_code] = obj | |
64 | ||
65 | ||
66 | # Collect all exceptions from aiohttp.web_exceptions | |
67 | _find_exceptions() | |
68 | del _find_exceptions | |
69 | ||
70 | ||
71 | class AIOHTTPParser(AsyncParser): | |
72 | """aiohttp request argument parser.""" | |
73 | ||
74 | __location_map__ = dict( | |
75 | match_info="load_match_info", | |
76 | path="load_match_info", | |
77 | **core.Parser.__location_map__, | |
78 | ) | |
79 | ||
80 | def load_querystring(self, req: Request, schema: Schema) -> MultiDictProxy: | |
81 | """Return query params from the request as a MultiDictProxy.""" | |
82 | return MultiDictProxy(req.query, schema) | |
83 | ||
84 | async def load_form(self, req: Request, schema: Schema) -> MultiDictProxy: | |
85 | """Return form values from the request as a MultiDictProxy.""" | |
86 | post_data = await req.post() | |
87 | return MultiDictProxy(post_data, schema) | |
88 | ||
89 | async def load_json_or_form( | |
90 | self, req: Request, schema: Schema | |
91 | ) -> typing.Union[typing.Dict, MultiDictProxy]: | |
92 | data = await self.load_json(req, schema) | |
93 | if data is not core.missing: | |
94 | return data | |
95 | return await self.load_form(req, schema) | |
96 | ||
97 | async def load_json(self, req: Request, schema: Schema) -> typing.Dict: | |
98 | """Return a parsed json payload from the request.""" | |
99 | if not (req.body_exists and is_json_request(req)): | |
100 | return core.missing | |
101 | try: | |
102 | return await req.json(loads=json.loads) | |
103 | except json.JSONDecodeError as exc: | |
104 | if exc.doc == "": | |
105 | return core.missing | |
106 | return self._handle_invalid_json_error(exc, req) | |
107 | except UnicodeDecodeError as exc: | |
108 | return self._handle_invalid_json_error(exc, req) | |
109 | ||
110 | def load_headers(self, req: Request, schema: Schema) -> MultiDictProxy: | |
111 | """Return headers from the request as a MultiDictProxy.""" | |
112 | return MultiDictProxy(req.headers, schema) | |
113 | ||
114 | def load_cookies(self, req: Request, schema: Schema) -> MultiDictProxy: | |
115 | """Return cookies from the request as a MultiDictProxy.""" | |
116 | return MultiDictProxy(req.cookies, schema) | |
117 | ||
118 | def load_files(self, req: Request, schema: Schema) -> "typing.NoReturn": | |
119 | raise NotImplementedError( | |
120 | "load_files is not implemented. You may be able to use load_form for " | |
121 | "parsing upload data." | |
122 | ) | |
123 | ||
124 | def load_match_info(self, req: Request, schema: Schema) -> typing.Mapping: | |
125 | """Load the request's ``match_info``.""" | |
126 | return req.match_info | |
127 | ||
128 | def get_request_from_view_args( | |
129 | self, view: typing.Callable, args: typing.Iterable, kwargs: typing.Mapping | |
130 | ) -> Request: | |
131 | """Get request object from a handler function or method. Used internally by | |
132 | ``use_args`` and ``use_kwargs``. | |
133 | """ | |
134 | req = None | |
135 | for arg in args: | |
136 | if isinstance(arg, web.Request): | |
137 | req = arg | |
138 | break | |
139 | if isinstance(arg, web.View): | |
140 | req = arg.request | |
141 | break | |
142 | if not isinstance(req, web.Request): | |
143 | raise ValueError("Request argument not found for handler") | |
144 | return req | |
145 | ||
146 | def handle_error( | |
147 | self, | |
148 | error: ValidationError, | |
149 | req: Request, | |
150 | schema: Schema, | |
151 | *, | |
152 | error_status_code: typing.Union[int, None], | |
153 | error_headers: typing.Union[typing.Mapping[str, str], None] | |
154 | ) -> "typing.NoReturn": | |
155 | """Handle ValidationErrors and return a JSON response of error messages | |
156 | to the client. | |
157 | """ | |
158 | error_class = exception_map.get( | |
159 | error_status_code or self.DEFAULT_VALIDATION_STATUS | |
160 | ) | |
161 | if not error_class: | |
162 | raise LookupError("No exception for {}".format(error_status_code)) | |
163 | headers = error_headers | |
164 | raise error_class( | |
165 | body=json.dumps(error.messages).encode("utf-8"), | |
166 | headers=headers, | |
167 | content_type="application/json", | |
168 | ) | |
169 | ||
170 | def _handle_invalid_json_error( | |
171 | self, | |
172 | error: typing.Union[json.JSONDecodeError, UnicodeDecodeError], | |
173 | req: Request, | |
174 | *args, | |
175 | **kwargs | |
176 | ) -> "typing.NoReturn": | |
177 | error_class = exception_map[400] | |
178 | messages = {"json": ["Invalid JSON body."]} | |
179 | raise error_class( | |
180 | body=json.dumps(messages).encode("utf-8"), content_type="application/json" | |
181 | ) | |
182 | ||
183 | ||
184 | parser = AIOHTTPParser() | |
185 | use_args = parser.use_args # type: typing.Callable | |
186 | use_kwargs = parser.use_kwargs # type: typing.Callable |
0 | """Asynchronous request parser. Compatible with Python>=3.5.""" | |
1 | import asyncio | |
2 | import functools | |
3 | import inspect | |
4 | import typing | |
5 | from collections.abc import Mapping | |
6 | ||
7 | from marshmallow import Schema, ValidationError | |
8 | from marshmallow.fields import Field | |
9 | import marshmallow as ma | |
10 | ||
11 | from webargs import core | |
12 | ||
13 | Request = typing.TypeVar("Request") | |
14 | ArgMap = typing.Union[Schema, typing.Mapping[str, Field]] | |
15 | Validate = typing.Union[typing.Callable, typing.Iterable[typing.Callable]] | |
16 | ||
17 | ||
18 | class AsyncParser(core.Parser): | |
19 | """Asynchronous variant of `webargs.core.Parser`, where parsing methods may be | |
20 | either coroutines or regular methods. | |
21 | """ | |
22 | ||
23 | # TODO: Lots of duplication from core.Parser here. Rethink. | |
24 | async def parse( | |
25 | self, | |
26 | argmap: ArgMap, | |
27 | req: Request = None, | |
28 | *, | |
29 | location: str = None, | |
30 | validate: Validate = None, | |
31 | error_status_code: typing.Union[int, None] = None, | |
32 | error_headers: typing.Union[typing.Mapping[str, str], None] = None | |
33 | ) -> typing.Union[typing.Mapping, None]: | |
34 | """Coroutine variant of `webargs.core.Parser`. | |
35 | ||
36 | Receives the same arguments as `webargs.core.Parser.parse`. | |
37 | """ | |
38 | req = req if req is not None else self.get_default_request() | |
39 | location = location or self.location | |
40 | if req is None: | |
41 | raise ValueError("Must pass req object") | |
42 | data = None | |
43 | validators = core._ensure_list_of_callables(validate) | |
44 | schema = self._get_schema(argmap, req) | |
45 | try: | |
46 | location_data = await self._load_location_data( | |
47 | schema=schema, req=req, location=location | |
48 | ) | |
49 | result = schema.load(location_data) | |
50 | data = result.data if core.MARSHMALLOW_VERSION_INFO[0] < 3 else result | |
51 | self._validate_arguments(data, validators) | |
52 | except ma.exceptions.ValidationError as error: | |
53 | await self._on_validation_error( | |
54 | error, | |
55 | req, | |
56 | schema, | |
57 | location, | |
58 | error_status_code=error_status_code, | |
59 | error_headers=error_headers, | |
60 | ) | |
61 | return data | |
62 | ||
63 | async def _load_location_data(self, schema, req, location): | |
64 | """Return a dictionary-like object for the location on the given request. | |
65 | ||
66 | Needs to have the schema in hand in order to correctly handle loading | |
67 | lists from multidict objects and `many=True` schemas. | |
68 | """ | |
69 | loader_func = self._get_loader(location) | |
70 | if asyncio.iscoroutinefunction(loader_func): | |
71 | data = await loader_func(req, schema) | |
72 | else: | |
73 | data = loader_func(req, schema) | |
74 | ||
75 | # when the desired location is empty (no data), provide an empty | |
76 | # dict as the default so that optional arguments in a location | |
77 | # (e.g. optional JSON body) work smoothly | |
78 | if data is core.missing: | |
79 | data = {} | |
80 | return data | |
81 | ||
82 | async def _on_validation_error( | |
83 | self, | |
84 | error: ValidationError, | |
85 | req: Request, | |
86 | schema: Schema, | |
87 | location: str, | |
88 | *, | |
89 | error_status_code: typing.Union[int, None], | |
90 | error_headers: typing.Union[typing.Mapping[str, str], None] | |
91 | ) -> None: | |
92 | # rewrite messages to be namespaced under the location which created | |
93 | # them | |
94 | # e.g. {"json":{"foo":["Not a valid integer."]}} | |
95 | # instead of | |
96 | # {"foo":["Not a valid integer."]} | |
97 | error.messages = {location: error.messages} | |
98 | error_handler = self.error_callback or self.handle_error | |
99 | await error_handler( | |
100 | error, | |
101 | req, | |
102 | schema, | |
103 | error_status_code=error_status_code, | |
104 | error_headers=error_headers, | |
105 | ) | |
106 | ||
107 | def use_args( | |
108 | self, | |
109 | argmap: ArgMap, | |
110 | req: typing.Optional[Request] = None, | |
111 | *, | |
112 | location: str = None, | |
113 | as_kwargs: bool = False, | |
114 | validate: Validate = None, | |
115 | error_status_code: typing.Optional[int] = None, | |
116 | error_headers: typing.Union[typing.Mapping[str, str], None] = None | |
117 | ) -> typing.Callable[..., typing.Callable]: | |
118 | """Decorator that injects parsed arguments into a view function or method. | |
119 | ||
120 | Receives the same arguments as `webargs.core.Parser.use_args`. | |
121 | """ | |
122 | location = location or self.location | |
123 | request_obj = req | |
124 | # Optimization: If argmap is passed as a dictionary, we only need | |
125 | # to generate a Schema once | |
126 | if isinstance(argmap, Mapping): | |
127 | argmap = core.dict2schema(argmap, schema_class=self.schema_class)() | |
128 | ||
129 | def decorator(func: typing.Callable) -> typing.Callable: | |
130 | req_ = request_obj | |
131 | ||
132 | if inspect.iscoroutinefunction(func): | |
133 | ||
134 | @functools.wraps(func) | |
135 | async def wrapper(*args, **kwargs): | |
136 | req_obj = req_ | |
137 | ||
138 | if not req_obj: | |
139 | req_obj = self.get_request_from_view_args(func, args, kwargs) | |
140 | # NOTE: At this point, argmap may be a Schema, callable, or dict | |
141 | parsed_args = await self.parse( | |
142 | argmap, | |
143 | req=req_obj, | |
144 | location=location, | |
145 | validate=validate, | |
146 | error_status_code=error_status_code, | |
147 | error_headers=error_headers, | |
148 | ) | |
149 | args, kwargs = self._update_args_kwargs( | |
150 | args, kwargs, parsed_args, as_kwargs | |
151 | ) | |
152 | return await func(*args, **kwargs) | |
153 | ||
154 | else: | |
155 | ||
156 | @functools.wraps(func) # type: ignore | |
157 | def wrapper(*args, **kwargs): | |
158 | req_obj = req_ | |
159 | ||
160 | if not req_obj: | |
161 | req_obj = self.get_request_from_view_args(func, args, kwargs) | |
162 | # NOTE: At this point, argmap may be a Schema, callable, or dict | |
163 | parsed_args = yield from self.parse( # type: ignore | |
164 | argmap, | |
165 | req=req_obj, | |
166 | location=location, | |
167 | validate=validate, | |
168 | error_status_code=error_status_code, | |
169 | error_headers=error_headers, | |
170 | ) | |
171 | args, kwargs = self._update_args_kwargs( | |
172 | args, kwargs, parsed_args, as_kwargs | |
173 | ) | |
174 | return func(*args, **kwargs) | |
175 | ||
176 | return wrapper | |
177 | ||
178 | return decorator |
0 | """Bottle request argument parsing module. | |
1 | ||
2 | Example: :: | |
3 | ||
4 | from bottle import route, run | |
5 | from marshmallow import fields | |
6 | from webargs.bottleparser import use_args | |
7 | ||
8 | hello_args = { | |
9 | 'name': fields.Str(missing='World') | |
10 | } | |
11 | @route('/', method='GET', apply=use_args(hello_args)) | |
12 | def index(args): | |
13 | return 'Hello ' + args['name'] | |
14 | ||
15 | if __name__ == '__main__': | |
16 | run(debug=True) | |
17 | """ | |
18 | import bottle | |
19 | ||
20 | from webargs import core | |
21 | from webargs.multidictproxy import MultiDictProxy | |
22 | ||
23 | ||
24 | class BottleParser(core.Parser): | |
25 | """Bottle.py request argument parser.""" | |
26 | ||
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 | ||
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.""" | |
55 | # For consistency with other parsers' behavior, don't attempt to | |
56 | # parse if content-type is mismatched. | |
57 | # TODO: Make this check more specific | |
58 | if core.is_json(req.content_type): | |
59 | return core.missing | |
60 | return MultiDictProxy(req.forms, schema) | |
61 | ||
62 | def load_headers(self, req, schema): | |
63 | """Return headers from the request as a MultiDictProxy.""" | |
64 | return MultiDictProxy(req.headers, schema) | |
65 | ||
66 | def load_cookies(self, req, schema): | |
67 | """Return cookies from the request.""" | |
68 | return req.cookies | |
69 | ||
70 | def load_files(self, req, schema): | |
71 | """Return files from the request as a MultiDictProxy.""" | |
72 | return MultiDictProxy(req.files, schema) | |
73 | ||
74 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): | |
75 | """Handles errors during parsing. Aborts the current request with a | |
76 | 400 error. | |
77 | """ | |
78 | status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS | |
79 | raise bottle.HTTPError( | |
80 | status=status_code, | |
81 | body=error.messages, | |
82 | headers=error_headers, | |
83 | exception=error, | |
84 | ) | |
85 | ||
86 | def get_default_request(self): | |
87 | """Override to use bottle's thread-local request object by default.""" | |
88 | return bottle.request | |
89 | ||
90 | ||
91 | parser = BottleParser() | |
92 | use_args = parser.use_args | |
93 | use_kwargs = parser.use_kwargs |
0 | # flake8: noqa | |
1 | from distutils.version import LooseVersion | |
2 | ||
3 | import marshmallow as ma | |
4 | ||
5 | MARSHMALLOW_VERSION_INFO = tuple(LooseVersion(ma.__version__).version) # type: tuple |
0 | import functools | |
1 | import inspect | |
2 | import typing | |
3 | import logging | |
4 | import warnings | |
5 | from collections.abc import Mapping | |
6 | import json | |
7 | ||
8 | import marshmallow as ma | |
9 | from marshmallow import ValidationError | |
10 | from marshmallow.utils import missing | |
11 | ||
12 | from webargs.compat import MARSHMALLOW_VERSION_INFO | |
13 | from webargs.dict2schema import dict2schema | |
14 | from webargs.fields import DelimitedList | |
15 | ||
16 | logger = logging.getLogger(__name__) | |
17 | ||
18 | ||
19 | __all__ = [ | |
20 | "ValidationError", | |
21 | "dict2schema", | |
22 | "is_multiple", | |
23 | "Parser", | |
24 | "missing", | |
25 | "parse_json", | |
26 | ] | |
27 | ||
28 | ||
29 | DEFAULT_VALIDATION_STATUS = 422 # type: int | |
30 | ||
31 | ||
32 | def _callable_or_raise(obj): | |
33 | """Makes sure an object is callable if it is not ``None``. If not | |
34 | callable, a ValueError is raised. | |
35 | """ | |
36 | if obj and not callable(obj): | |
37 | raise ValueError("{!r} is not callable.".format(obj)) | |
38 | return obj | |
39 | ||
40 | ||
41 | def is_multiple(field): | |
42 | """Return whether or not `field` handles repeated/multi-value arguments.""" | |
43 | return isinstance(field, ma.fields.List) and not isinstance(field, DelimitedList) | |
44 | ||
45 | ||
46 | def get_mimetype(content_type): | |
47 | return content_type.split(";")[0].strip() if content_type else None | |
48 | ||
49 | ||
50 | # Adapted from werkzeug: | |
51 | # https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/wrappers.py | |
52 | def is_json(mimetype): | |
53 | """Indicates if this mimetype is JSON or not. By default a request | |
54 | is considered to include JSON data if the mimetype is | |
55 | ``application/json`` or ``application/*+json``. | |
56 | """ | |
57 | if not mimetype: | |
58 | return False | |
59 | if ";" in mimetype: # Allow Content-Type header to be passed | |
60 | mimetype = get_mimetype(mimetype) | |
61 | if mimetype == "application/json": | |
62 | return True | |
63 | if mimetype.startswith("application/") and mimetype.endswith("+json"): | |
64 | return True | |
65 | return False | |
66 | ||
67 | ||
68 | def parse_json(string, *, encoding="utf-8"): | |
69 | if isinstance(string, bytes): | |
70 | try: | |
71 | string = string.decode(encoding) | |
72 | except UnicodeDecodeError as exc: | |
73 | raise json.JSONDecodeError( | |
74 | "Bytes decoding error : {}".format(exc.reason), | |
75 | doc=str(exc.object), | |
76 | pos=exc.start, | |
77 | ) | |
78 | return json.loads(string) | |
79 | ||
80 | ||
81 | def _ensure_list_of_callables(obj): | |
82 | if obj: | |
83 | if isinstance(obj, (list, tuple)): | |
84 | validators = obj | |
85 | elif callable(obj): | |
86 | validators = [obj] | |
87 | else: | |
88 | raise ValueError("{!r} is not a callable or list of callables.".format(obj)) | |
89 | else: | |
90 | validators = [] | |
91 | return validators | |
92 | ||
93 | ||
94 | class Parser: | |
95 | """Base parser class that provides high-level implementation for parsing | |
96 | a request. | |
97 | ||
98 | Descendant classes must provide lower-level implementations for reading | |
99 | data from different locations, e.g. ``load_json``, ``load_querystring``, | |
100 | etc. | |
101 | ||
102 | :param str location: Default location to use for data | |
103 | :param callable error_handler: Custom error handler function. | |
104 | """ | |
105 | ||
106 | #: Default location to check for data | |
107 | DEFAULT_LOCATION = "json" | |
108 | #: The marshmallow Schema class to use when creating new schemas | |
109 | DEFAULT_SCHEMA_CLASS = ma.Schema | |
110 | #: Default status code to return for validation errors | |
111 | DEFAULT_VALIDATION_STATUS = DEFAULT_VALIDATION_STATUS | |
112 | #: Default error message for validation errors | |
113 | DEFAULT_VALIDATION_MESSAGE = "Invalid value." | |
114 | ||
115 | #: Maps location => method name | |
116 | __location_map__ = { | |
117 | "json": "load_json", | |
118 | "querystring": "load_querystring", | |
119 | "query": "load_querystring", | |
120 | "form": "load_form", | |
121 | "headers": "load_headers", | |
122 | "cookies": "load_cookies", | |
123 | "files": "load_files", | |
124 | "json_or_form": "load_json_or_form", | |
125 | } | |
126 | ||
127 | def __init__(self, location=None, *, error_handler=None, schema_class=None): | |
128 | self.location = location or self.DEFAULT_LOCATION | |
129 | self.error_callback = _callable_or_raise(error_handler) | |
130 | self.schema_class = schema_class or self.DEFAULT_SCHEMA_CLASS | |
131 | ||
132 | def _get_loader(self, location): | |
133 | """Get the loader function for the given location. | |
134 | ||
135 | :raises: ValueError if a given location is invalid. | |
136 | """ | |
137 | valid_locations = set(self.__location_map__.keys()) | |
138 | if location not in valid_locations: | |
139 | msg = "Invalid location argument: {}".format(location) | |
140 | raise ValueError(msg) | |
141 | ||
142 | # Parsing function to call | |
143 | # May be a method name (str) or a function | |
144 | func = self.__location_map__.get(location) | |
145 | if func: | |
146 | if inspect.isfunction(func): | |
147 | function = func | |
148 | else: | |
149 | function = getattr(self, func) | |
150 | else: | |
151 | raise ValueError('Invalid location: "{}"'.format(location)) | |
152 | return function | |
153 | ||
154 | def _load_location_data(self, *, schema, req, location): | |
155 | """Return a dictionary-like object for the location on the given request. | |
156 | ||
157 | Needs to have the schema in hand in order to correctly handle loading | |
158 | lists from multidict objects and `many=True` schemas. | |
159 | """ | |
160 | loader_func = self._get_loader(location) | |
161 | data = loader_func(req, schema) | |
162 | # when the desired location is empty (no data), provide an empty | |
163 | # dict as the default so that optional arguments in a location | |
164 | # (e.g. optional JSON body) work smoothly | |
165 | if data is missing: | |
166 | data = {} | |
167 | return data | |
168 | ||
169 | def _on_validation_error( | |
170 | self, error, req, schema, location, *, error_status_code, error_headers | |
171 | ): | |
172 | # rewrite messages to be namespaced under the location which created | |
173 | # them | |
174 | # e.g. {"json":{"foo":["Not a valid integer."]}} | |
175 | # instead of | |
176 | # {"foo":["Not a valid integer."]} | |
177 | error.messages = {location: error.messages} | |
178 | error_handler = self.error_callback or self.handle_error | |
179 | error_handler( | |
180 | error, | |
181 | req, | |
182 | schema, | |
183 | error_status_code=error_status_code, | |
184 | error_headers=error_headers, | |
185 | ) | |
186 | ||
187 | def _validate_arguments(self, data, validators): | |
188 | for validator in validators: | |
189 | if validator(data) is False: | |
190 | msg = self.DEFAULT_VALIDATION_MESSAGE | |
191 | raise ValidationError(msg, data=data) | |
192 | ||
193 | def _get_schema(self, argmap, req): | |
194 | """Return a `marshmallow.Schema` for the given argmap and request. | |
195 | ||
196 | :param argmap: Either a `marshmallow.Schema`, `dict` | |
197 | of argname -> `marshmallow.fields.Field` pairs, or a callable that returns | |
198 | a `marshmallow.Schema` instance. | |
199 | :param req: The request object being parsed. | |
200 | :rtype: marshmallow.Schema | |
201 | """ | |
202 | if isinstance(argmap, ma.Schema): | |
203 | schema = argmap | |
204 | elif isinstance(argmap, type) and issubclass(argmap, ma.Schema): | |
205 | schema = argmap() | |
206 | elif callable(argmap): | |
207 | schema = argmap(req) | |
208 | else: | |
209 | schema = dict2schema(argmap, schema_class=self.schema_class)() | |
210 | if MARSHMALLOW_VERSION_INFO[0] < 3 and not schema.strict: | |
211 | warnings.warn( | |
212 | "It is highly recommended that you set strict=True on your schema " | |
213 | "so that the parser's error handler will be invoked when expected.", | |
214 | UserWarning, | |
215 | ) | |
216 | return schema | |
217 | ||
218 | def parse( | |
219 | self, | |
220 | argmap, | |
221 | req=None, | |
222 | *, | |
223 | location=None, | |
224 | validate=None, | |
225 | error_status_code=None, | |
226 | error_headers=None | |
227 | ): | |
228 | """Main request parsing method. | |
229 | ||
230 | :param argmap: Either a `marshmallow.Schema`, a `dict` | |
231 | of argname -> `marshmallow.fields.Field` pairs, or a callable | |
232 | which accepts a request and returns a `marshmallow.Schema`. | |
233 | :param req: The request object to parse. | |
234 | :param str location: Where on the request to load values. | |
235 | Can be any of the values in :py:attr:`~__location_map__`. By | |
236 | default, that means one of ``('json', 'query', 'querystring', | |
237 | 'form', 'headers', 'cookies', 'files', 'json_or_form')``. | |
238 | :param callable validate: Validation function or list of validation functions | |
239 | that receives the dictionary of parsed arguments. Validator either returns a | |
240 | boolean or raises a :exc:`ValidationError`. | |
241 | :param int error_status_code: Status code passed to error handler functions when | |
242 | a `ValidationError` is raised. | |
243 | :param dict error_headers: Headers passed to error handler functions when a | |
244 | a `ValidationError` is raised. | |
245 | ||
246 | :return: A dictionary of parsed arguments | |
247 | """ | |
248 | req = req if req is not None else self.get_default_request() | |
249 | location = location or self.location | |
250 | if req is None: | |
251 | raise ValueError("Must pass req object") | |
252 | data = None | |
253 | validators = _ensure_list_of_callables(validate) | |
254 | schema = self._get_schema(argmap, req) | |
255 | try: | |
256 | location_data = self._load_location_data( | |
257 | schema=schema, req=req, location=location | |
258 | ) | |
259 | result = schema.load(location_data) | |
260 | data = result.data if MARSHMALLOW_VERSION_INFO[0] < 3 else result | |
261 | self._validate_arguments(data, validators) | |
262 | except ma.exceptions.ValidationError as error: | |
263 | self._on_validation_error( | |
264 | error, | |
265 | req, | |
266 | schema, | |
267 | location, | |
268 | error_status_code=error_status_code, | |
269 | error_headers=error_headers, | |
270 | ) | |
271 | return data | |
272 | ||
273 | def get_default_request(self): | |
274 | """Optional override. Provides a hook for frameworks that use thread-local | |
275 | request objects. | |
276 | """ | |
277 | return None | |
278 | ||
279 | def get_request_from_view_args(self, view, args, kwargs): | |
280 | """Optional override. Returns the request object to be parsed, given a view | |
281 | function's args and kwargs. | |
282 | ||
283 | Used by the `use_args` and `use_kwargs` to get a request object from a | |
284 | view's arguments. | |
285 | ||
286 | :param callable view: The view function or method being decorated by | |
287 | `use_args` or `use_kwargs` | |
288 | :param tuple args: Positional arguments passed to ``view``. | |
289 | :param dict kwargs: Keyword arguments passed to ``view``. | |
290 | """ | |
291 | return None | |
292 | ||
293 | @staticmethod | |
294 | def _update_args_kwargs(args, kwargs, parsed_args, as_kwargs): | |
295 | """Update args or kwargs with parsed_args depending on as_kwargs""" | |
296 | if as_kwargs: | |
297 | kwargs.update(parsed_args) | |
298 | else: | |
299 | # Add parsed_args after other positional arguments | |
300 | args += (parsed_args,) | |
301 | return args, kwargs | |
302 | ||
303 | def use_args( | |
304 | self, | |
305 | argmap, | |
306 | req=None, | |
307 | *, | |
308 | location=None, | |
309 | as_kwargs=False, | |
310 | validate=None, | |
311 | error_status_code=None, | |
312 | error_headers=None | |
313 | ): | |
314 | """Decorator that injects parsed arguments into a view function or method. | |
315 | ||
316 | Example usage with Flask: :: | |
317 | ||
318 | @app.route('/echo', methods=['get', 'post']) | |
319 | @parser.use_args({'name': fields.Str()}, location="querystring") | |
320 | def greet(args): | |
321 | return 'Hello ' + args['name'] | |
322 | ||
323 | :param argmap: Either a `marshmallow.Schema`, a `dict` | |
324 | of argname -> `marshmallow.fields.Field` pairs, or a callable | |
325 | which accepts a request and returns a `marshmallow.Schema`. | |
326 | :param str location: Where on the request to load values. | |
327 | :param bool as_kwargs: Whether to insert arguments as keyword arguments. | |
328 | :param callable validate: Validation function that receives the dictionary | |
329 | of parsed arguments. If the function returns ``False``, the parser | |
330 | will raise a :exc:`ValidationError`. | |
331 | :param int error_status_code: Status code passed to error handler functions when | |
332 | a `ValidationError` is raised. | |
333 | :param dict error_headers: Headers passed to error handler functions when a | |
334 | a `ValidationError` is raised. | |
335 | """ | |
336 | location = location or self.location | |
337 | request_obj = req | |
338 | # Optimization: If argmap is passed as a dictionary, we only need | |
339 | # to generate a Schema once | |
340 | if isinstance(argmap, Mapping): | |
341 | argmap = dict2schema(argmap, schema_class=self.schema_class)() | |
342 | ||
343 | def decorator(func): | |
344 | req_ = request_obj | |
345 | ||
346 | @functools.wraps(func) | |
347 | def wrapper(*args, **kwargs): | |
348 | req_obj = req_ | |
349 | ||
350 | if not req_obj: | |
351 | req_obj = self.get_request_from_view_args(func, args, kwargs) | |
352 | ||
353 | # NOTE: At this point, argmap may be a Schema, or a callable | |
354 | parsed_args = self.parse( | |
355 | argmap, | |
356 | req=req_obj, | |
357 | location=location, | |
358 | validate=validate, | |
359 | error_status_code=error_status_code, | |
360 | error_headers=error_headers, | |
361 | ) | |
362 | args, kwargs = self._update_args_kwargs( | |
363 | args, kwargs, parsed_args, as_kwargs | |
364 | ) | |
365 | return func(*args, **kwargs) | |
366 | ||
367 | wrapper.__wrapped__ = func | |
368 | return wrapper | |
369 | ||
370 | return decorator | |
371 | ||
372 | def use_kwargs(self, *args, **kwargs) -> typing.Callable: | |
373 | """Decorator that injects parsed arguments into a view function or method | |
374 | as keyword arguments. | |
375 | ||
376 | This is a shortcut to :meth:`use_args` with ``as_kwargs=True``. | |
377 | ||
378 | Example usage with Flask: :: | |
379 | ||
380 | @app.route('/echo', methods=['get', 'post']) | |
381 | @parser.use_kwargs({'name': fields.Str()}) | |
382 | def greet(name): | |
383 | return 'Hello ' + name | |
384 | ||
385 | Receives the same ``args`` and ``kwargs`` as :meth:`use_args`. | |
386 | """ | |
387 | kwargs["as_kwargs"] = True | |
388 | return self.use_args(*args, **kwargs) | |
389 | ||
390 | def location_loader(self, name): | |
391 | """Decorator that registers a function for loading a request location. | |
392 | The wrapped function receives a schema and a request. | |
393 | ||
394 | The schema will usually not be relevant, but it's important in some | |
395 | cases -- most notably in order to correctly load multidict values into | |
396 | list fields. Without the schema, there would be no way to know whether | |
397 | to simply `.get()` or `.getall()` from a multidict for a given value. | |
398 | ||
399 | Example: :: | |
400 | ||
401 | from webargs import core | |
402 | parser = core.Parser() | |
403 | ||
404 | @parser.location_loader("name") | |
405 | def load_data(request, schema): | |
406 | return request.data | |
407 | ||
408 | :param str name: The name of the location to register. | |
409 | """ | |
410 | ||
411 | def decorator(func): | |
412 | self.__location_map__[name] = func | |
413 | return func | |
414 | ||
415 | return decorator | |
416 | ||
417 | def error_handler(self, func): | |
418 | """Decorator that registers a custom error handling function. The | |
419 | function should receive the raised error, request object, | |
420 | `marshmallow.Schema` instance used to parse the request, error status code, | |
421 | and headers to use for the error response. Overrides | |
422 | the parser's ``handle_error`` method. | |
423 | ||
424 | Example: :: | |
425 | ||
426 | from webargs import flaskparser | |
427 | ||
428 | parser = flaskparser.FlaskParser() | |
429 | ||
430 | ||
431 | class CustomError(Exception): | |
432 | pass | |
433 | ||
434 | ||
435 | @parser.error_handler | |
436 | def handle_error(error, req, schema, *, status_code, headers): | |
437 | raise CustomError(error.messages) | |
438 | ||
439 | :param callable func: The error callback to register. | |
440 | """ | |
441 | self.error_callback = func | |
442 | return func | |
443 | ||
444 | def _handle_invalid_json_error(self, error, req, *args, **kwargs): | |
445 | """Internal hook for overriding treatment of JSONDecodeErrors. | |
446 | ||
447 | Invoked by default `load_json` implementation. | |
448 | ||
449 | External parsers can just implement their own behavior for load_json , | |
450 | so this is not part of the public parser API. | |
451 | """ | |
452 | raise error | |
453 | ||
454 | def load_json(self, req, schema): | |
455 | """Load JSON from a request object or return `missing` if no value can | |
456 | be found. | |
457 | """ | |
458 | # NOTE: although this implementation is real/concrete and used by | |
459 | # several of the parsers in webargs, it relies on the internal hooks | |
460 | # `_handle_invalid_json_error` and `_raw_load_json` | |
461 | # these methods are not part of the public API and are used to simplify | |
462 | # code sharing amongst the built-in webargs parsers | |
463 | try: | |
464 | return self._raw_load_json(req) | |
465 | except json.JSONDecodeError as exc: | |
466 | if exc.doc == "": | |
467 | return missing | |
468 | return self._handle_invalid_json_error(exc, req) | |
469 | except UnicodeDecodeError as exc: | |
470 | return self._handle_invalid_json_error(exc, req) | |
471 | ||
472 | def load_json_or_form(self, req, schema): | |
473 | """Load data from a request, accepting either JSON or form-encoded | |
474 | data. | |
475 | ||
476 | The data will first be loaded as JSON, and, if that fails, it will be | |
477 | loaded as a form post. | |
478 | """ | |
479 | data = self.load_json(req, schema) | |
480 | if data is not missing: | |
481 | return data | |
482 | return self.load_form(req, schema) | |
483 | ||
484 | # Abstract Methods | |
485 | ||
486 | def _raw_load_json(self, req): | |
487 | """Internal hook method for implementing load_json() | |
488 | ||
489 | Get a request body for feeding in to `load_json`, and parse it either | |
490 | using core.parse_json() or similar utilities which raise | |
491 | JSONDecodeErrors. | |
492 | Ensure consistent behavior when encountering decoding errors. | |
493 | ||
494 | The default implementation here simply returns `missing`, and the default | |
495 | implementation of `load_json` above will pass that value through. | |
496 | However, by implementing a "mostly concrete" version of load_json with | |
497 | this as a hook for getting data, we consolidate the logic for handling | |
498 | those JSONDecodeErrors. | |
499 | """ | |
500 | return missing | |
501 | ||
502 | def load_querystring(self, req, schema): | |
503 | """Load the query string of a request object or return `missing` if no | |
504 | value can be found. | |
505 | """ | |
506 | return missing | |
507 | ||
508 | def load_form(self, req, schema): | |
509 | """Load the form data of a request object or return `missing` if no | |
510 | value can be found. | |
511 | """ | |
512 | return missing | |
513 | ||
514 | def load_headers(self, req, schema): | |
515 | """Load the headers or return `missing` if no value can be found. | |
516 | """ | |
517 | return missing | |
518 | ||
519 | def load_cookies(self, req, schema): | |
520 | """Load the cookies from the request or return `missing` if no value | |
521 | can be found. | |
522 | """ | |
523 | return missing | |
524 | ||
525 | def load_files(self, req, schema): | |
526 | """Load files from the request or return `missing` if no values can be | |
527 | found. | |
528 | """ | |
529 | return missing | |
530 | ||
531 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): | |
532 | """Called if an error occurs while parsing args. By default, just logs and | |
533 | raises ``error``. | |
534 | """ | |
535 | logger.error(error) | |
536 | raise error |
0 | import marshmallow as ma | |
1 | ||
2 | ||
3 | def dict2schema(dct, *, schema_class=ma.Schema): | |
4 | """Generate a `marshmallow.Schema` class given a dictionary of | |
5 | `Fields <marshmallow.fields.Field>`. | |
6 | """ | |
7 | if hasattr(schema_class, "from_dict"): # marshmallow 3 | |
8 | return schema_class.from_dict(dct) | |
9 | attrs = dct.copy() | |
10 | ||
11 | class Meta: | |
12 | strict = True | |
13 | ||
14 | attrs["Meta"] = Meta | |
15 | return type("", (schema_class,), attrs) |
0 | """Django request argument parsing. | |
1 | ||
2 | Example usage: :: | |
3 | ||
4 | from django.views.generic import View | |
5 | from django.http import HttpResponse | |
6 | from marshmallow import fields | |
7 | from webargs.djangoparser import use_args | |
8 | ||
9 | hello_args = { | |
10 | 'name': fields.Str(missing='World') | |
11 | } | |
12 | ||
13 | class MyView(View): | |
14 | ||
15 | @use_args(hello_args) | |
16 | def get(self, args, request): | |
17 | return HttpResponse('Hello ' + args['name']) | |
18 | """ | |
19 | from webargs import core | |
20 | from webargs.multidictproxy import MultiDictProxy | |
21 | ||
22 | ||
23 | def is_json_request(req): | |
24 | return core.is_json(req.content_type) | |
25 | ||
26 | ||
27 | class DjangoParser(core.Parser): | |
28 | """Django request argument parser. | |
29 | ||
30 | .. warning:: | |
31 | ||
32 | :class:`DjangoParser` does not override | |
33 | :meth:`handle_error <webargs.core.Parser.handle_error>`, so your Django | |
34 | views are responsible for catching any :exc:`ValidationErrors` raised by | |
35 | the parser and returning the appropriate `HTTPResponse`. | |
36 | """ | |
37 | ||
38 | def _raw_load_json(self, req): | |
39 | """Read a json payload from the request for the core parser's load_json | |
40 | ||
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 | |
45 | ||
46 | return core.parse_json(req.body) | |
47 | ||
48 | def load_querystring(self, req, schema): | |
49 | """Return query params from the request as a MultiDictProxy.""" | |
50 | return MultiDictProxy(req.GET, schema) | |
51 | ||
52 | def load_form(self, req, schema): | |
53 | """Return form values from the request as a MultiDictProxy.""" | |
54 | return MultiDictProxy(req.POST, schema) | |
55 | ||
56 | def load_cookies(self, req, schema): | |
57 | """Return cookies from the request.""" | |
58 | return req.COOKIES | |
59 | ||
60 | def load_headers(self, req, schema): | |
61 | raise NotImplementedError( | |
62 | "Header parsing not supported by {}".format(self.__class__.__name__) | |
63 | ) | |
64 | ||
65 | def load_files(self, req, schema): | |
66 | """Return files from the request as a MultiDictProxy.""" | |
67 | return MultiDictProxy(req.FILES, schema) | |
68 | ||
69 | def get_request_from_view_args(self, view, args, kwargs): | |
70 | # The first argument is either `self` or `request` | |
71 | try: # self.request | |
72 | return args[0].request | |
73 | except AttributeError: # first arg is request | |
74 | return args[0] | |
75 | ||
76 | ||
77 | parser = DjangoParser() | |
78 | use_args = parser.use_args | |
79 | use_kwargs = parser.use_kwargs |
0 | """Falcon request argument parsing module. | |
1 | """ | |
2 | import falcon | |
3 | from falcon.util.uri import parse_query_string | |
4 | ||
5 | from webargs import core | |
6 | from webargs.multidictproxy import MultiDictProxy | |
7 | ||
8 | HTTP_422 = "422 Unprocessable Entity" | |
9 | ||
10 | # Mapping of int status codes to string status | |
11 | status_map = {422: HTTP_422} | |
12 | ||
13 | ||
14 | # Collect all exceptions from falcon.status_codes | |
15 | def _find_exceptions(): | |
16 | for name in filter(lambda n: n.startswith("HTTP"), dir(falcon.status_codes)): | |
17 | status = getattr(falcon.status_codes, name) | |
18 | status_code = int(status.split(" ")[0]) | |
19 | status_map[status_code] = status | |
20 | ||
21 | ||
22 | _find_exceptions() | |
23 | del _find_exceptions | |
24 | ||
25 | ||
26 | def is_json_request(req): | |
27 | content_type = req.get_header("Content-Type") | |
28 | return content_type and core.is_json(content_type) | |
29 | ||
30 | ||
31 | # NOTE: Adapted from falcon.request.Request._parse_form_urlencoded | |
32 | def parse_form_body(req): | |
33 | if ( | |
34 | req.content_type is not None | |
35 | and "application/x-www-form-urlencoded" in req.content_type | |
36 | ): | |
37 | body = req.stream.read(req.content_length or 0) | |
38 | try: | |
39 | body = body.decode("ascii") | |
40 | except UnicodeDecodeError: | |
41 | body = None | |
42 | req.log_error( | |
43 | "Non-ASCII characters found in form body " | |
44 | "with Content-Type of " | |
45 | "application/x-www-form-urlencoded. Body " | |
46 | "will be ignored." | |
47 | ) | |
48 | ||
49 | if body: | |
50 | return parse_query_string(body, keep_blank=req.options.keep_blank_qs_values) | |
51 | ||
52 | return core.missing | |
53 | ||
54 | ||
55 | class HTTPError(falcon.HTTPError): | |
56 | """HTTPError that stores a dictionary of validation error messages. | |
57 | """ | |
58 | ||
59 | def __init__(self, status, errors, *args, **kwargs): | |
60 | self.errors = errors | |
61 | super().__init__(status, *args, **kwargs) | |
62 | ||
63 | def to_dict(self, *args, **kwargs): | |
64 | """Override `falcon.HTTPError` to include error messages in responses.""" | |
65 | ret = super().to_dict(*args, **kwargs) | |
66 | if self.errors is not None: | |
67 | ret["errors"] = self.errors | |
68 | return ret | |
69 | ||
70 | ||
71 | class FalconParser(core.Parser): | |
72 | """Falcon request argument parser.""" | |
73 | ||
74 | # Note on the use of MultiDictProxy throughout: | |
75 | # Falcon parses query strings and form values into ordinary dicts, but with | |
76 | # the values listified where appropriate | |
77 | # it is still therefore necessary in these cases to wrap them in | |
78 | # MultiDictProxy because we need to use the schema to determine when single | |
79 | # values should be wrapped in lists due to the type of the destination | |
80 | # field | |
81 | ||
82 | def load_querystring(self, req, schema): | |
83 | """Return query params from the request as a MultiDictProxy.""" | |
84 | return MultiDictProxy(req.params, schema) | |
85 | ||
86 | def load_form(self, req, schema): | |
87 | """Return form values from the request as a MultiDictProxy | |
88 | ||
89 | .. note:: | |
90 | ||
91 | The request stream will be read and left at EOF. | |
92 | """ | |
93 | form = parse_form_body(req) | |
94 | if form is core.missing: | |
95 | return form | |
96 | return MultiDictProxy(form, schema) | |
97 | ||
98 | def _raw_load_json(self, req): | |
99 | """Return a json payload from the request for the core parser's load_json | |
100 | ||
101 | Checks the input mimetype and may return 'missing' if the mimetype is | |
102 | non-json, even if the request body is parseable as json.""" | |
103 | if not is_json_request(req) or req.content_length in (None, 0): | |
104 | return core.missing | |
105 | body = req.stream.read(req.content_length) | |
106 | if body: | |
107 | return core.parse_json(body) | |
108 | return core.missing | |
109 | ||
110 | def load_headers(self, req, schema): | |
111 | """Return headers from the request.""" | |
112 | # Falcon only exposes headers as a dict (not multidict) | |
113 | return req.headers | |
114 | ||
115 | def load_cookies(self, req, schema): | |
116 | """Return cookies from the request.""" | |
117 | # Cookies are expressed in Falcon as a dict, but the possibility of | |
118 | # multiple values for a cookie is preserved internally -- if desired in | |
119 | # the future, webargs could add a MultiDict type for Cookies here built | |
120 | # from (req, schema), but Falcon does not provide one out of the box | |
121 | return req.cookies | |
122 | ||
123 | def get_request_from_view_args(self, view, args, kwargs): | |
124 | """Get request from a resource method's arguments. Assumes that | |
125 | request is the second argument. | |
126 | """ | |
127 | req = args[1] | |
128 | if not isinstance(req, falcon.Request): | |
129 | raise TypeError("Argument is not a falcon.Request") | |
130 | return req | |
131 | ||
132 | def load_files(self, req, schema): | |
133 | raise NotImplementedError( | |
134 | "Parsing files not yet supported by {}".format(self.__class__.__name__) | |
135 | ) | |
136 | ||
137 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): | |
138 | """Handles errors during parsing.""" | |
139 | status = status_map.get(error_status_code or self.DEFAULT_VALIDATION_STATUS) | |
140 | if status is None: | |
141 | raise LookupError("Status code {} not supported".format(error_status_code)) | |
142 | raise HTTPError(status, errors=error.messages, headers=error_headers) | |
143 | ||
144 | def _handle_invalid_json_error(self, error, req, *args, **kwargs): | |
145 | status = status_map[400] | |
146 | messages = {"json": ["Invalid JSON body."]} | |
147 | raise HTTPError(status, errors=messages) | |
148 | ||
149 | ||
150 | parser = FalconParser() | |
151 | use_args = parser.use_args | |
152 | use_kwargs = parser.use_kwargs |
0 | """Field classes. | |
1 | ||
2 | Includes all fields from `marshmallow.fields` in addition to a custom | |
3 | `Nested` field and `DelimitedList`. | |
4 | ||
5 | All fields can optionally take a special `location` keyword argument, which | |
6 | tells webargs where to parse the request argument from. | |
7 | ||
8 | .. code-block:: python | |
9 | ||
10 | args = { | |
11 | "active": fields.Bool(location="query"), | |
12 | "content_type": fields.Str(data_key="Content-Type", location="headers"), | |
13 | } | |
14 | ||
15 | Note: `data_key` replaced `load_from` in marshmallow 3. | |
16 | When using marshmallow 2, use `load_from`. | |
17 | """ | |
18 | import marshmallow as ma | |
19 | ||
20 | # Expose all fields from marshmallow.fields. | |
21 | from marshmallow.fields import * # noqa: F40 | |
22 | from webargs.compat import MARSHMALLOW_VERSION_INFO | |
23 | from webargs.dict2schema import dict2schema | |
24 | ||
25 | __all__ = ["DelimitedList"] + ma.fields.__all__ | |
26 | ||
27 | ||
28 | class Nested(ma.fields.Nested): | |
29 | """Same as `marshmallow.fields.Nested`, except can be passed a dictionary as | |
30 | the first argument, which will be converted to a `marshmallow.Schema`. | |
31 | ||
32 | .. note:: | |
33 | ||
34 | The schema class here will always be `marshmallow.Schema`, regardless | |
35 | of whether a custom schema class is set on the parser. Pass an explicit schema | |
36 | class if necessary. | |
37 | """ | |
38 | ||
39 | def __init__(self, nested, *args, **kwargs): | |
40 | if isinstance(nested, dict): | |
41 | nested = dict2schema(nested) | |
42 | super().__init__(nested, *args, **kwargs) | |
43 | ||
44 | ||
45 | class DelimitedFieldMixin: | |
46 | """ | |
47 | This is a mixin class for subclasses of ma.fields.List and ma.fields.Tuple | |
48 | which split on a pre-specified delimiter. By default, the delimiter will be "," | |
49 | ||
50 | Because we want the MRO to reach this class before the List or Tuple class, | |
51 | it must be listed first in the superclasses | |
52 | ||
53 | For example, a DelimitedList-like type can be defined like so: | |
54 | ||
55 | >>> class MyDelimitedList(DelimitedFieldMixin, ma.fields.List): | |
56 | >>> pass | |
57 | """ | |
58 | ||
59 | delimiter = "," | |
60 | ||
61 | def _serialize(self, value, attr, obj): | |
62 | # serializing will start with parent-class serialization, so that we correctly | |
63 | # output lists of non-primitive types, e.g. DelimitedList(DateTime) | |
64 | return self.delimiter.join( | |
65 | format(each) for each in super()._serialize(value, attr, obj) | |
66 | ) | |
67 | ||
68 | def _deserialize(self, value, attr, data, **kwargs): | |
69 | # attempting to deserialize from a non-string source is an error | |
70 | if not isinstance(value, (str, bytes)): | |
71 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
72 | self.fail("invalid") | |
73 | else: | |
74 | raise self.make_error("invalid") | |
75 | return super()._deserialize(value.split(self.delimiter), attr, data, **kwargs) | |
76 | ||
77 | ||
78 | class DelimitedList(DelimitedFieldMixin, ma.fields.List): | |
79 | """A field which is similar to a List, but takes its input as a delimited | |
80 | string (e.g. "foo,bar,baz"). | |
81 | ||
82 | Like List, it can be given a nested field type which it will use to | |
83 | de/serialize each element of the list. | |
84 | ||
85 | :param Field cls_or_instance: A field class or instance. | |
86 | :param str delimiter: Delimiter between values. | |
87 | """ | |
88 | ||
89 | default_error_messages = {"invalid": "Not a valid delimited list."} | |
90 | delimiter = "," | |
91 | ||
92 | def __init__(self, cls_or_instance, *, delimiter=None, **kwargs): | |
93 | self.delimiter = delimiter or self.delimiter | |
94 | super().__init__(cls_or_instance, **kwargs) | |
95 | ||
96 | ||
97 | # DelimitedTuple can only be defined when using marshmallow3, when Tuple was | |
98 | # added | |
99 | if MARSHMALLOW_VERSION_INFO[0] >= 3: | |
100 | ||
101 | class DelimitedTuple(DelimitedFieldMixin, ma.fields.Tuple): | |
102 | """A field which is similar to a Tuple, but takes its input as a delimited | |
103 | string (e.g. "foo,bar,baz"). | |
104 | ||
105 | Like Tuple, it can be given a tuple of nested field types which it will use to | |
106 | de/serialize each element of the tuple. | |
107 | ||
108 | :param Iterable[Field] tuple_fields: An iterable of field classes or instances. | |
109 | :param str delimiter: Delimiter between values. | |
110 | """ | |
111 | ||
112 | default_error_messages = {"invalid": "Not a valid delimited tuple."} | |
113 | delimiter = "," | |
114 | ||
115 | def __init__(self, tuple_fields, *, delimiter=None, **kwargs): | |
116 | self.delimiter = delimiter or self.delimiter | |
117 | super().__init__(tuple_fields, **kwargs) |
0 | """Flask request argument parsing module. | |
1 | ||
2 | Example: :: | |
3 | ||
4 | from flask import Flask | |
5 | ||
6 | from webargs import fields | |
7 | from webargs.flaskparser import use_args | |
8 | ||
9 | app = Flask(__name__) | |
10 | ||
11 | user_detail_args = { | |
12 | 'per_page': fields.Int() | |
13 | } | |
14 | ||
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 | ) | |
21 | """ | |
22 | import flask | |
23 | from werkzeug.exceptions import HTTPException | |
24 | ||
25 | from webargs import core | |
26 | from webargs.compat import MARSHMALLOW_VERSION_INFO | |
27 | from webargs.multidictproxy import MultiDictProxy | |
28 | ||
29 | ||
30 | def abort(http_status_code, exc=None, **kwargs): | |
31 | """Raise a HTTPException for the given http_status_code. Attach any keyword | |
32 | arguments to the exception for later processing. | |
33 | ||
34 | From Flask-Restful. See NOTICE file for license information. | |
35 | """ | |
36 | try: | |
37 | flask.abort(http_status_code) | |
38 | except HTTPException as err: | |
39 | err.data = kwargs | |
40 | err.exc = exc | |
41 | raise err | |
42 | ||
43 | ||
44 | def is_json_request(req): | |
45 | return core.is_json(req.mimetype) | |
46 | ||
47 | ||
48 | class FlaskParser(core.Parser): | |
49 | """Flask request argument parser.""" | |
50 | ||
51 | __location_map__ = dict( | |
52 | view_args="load_view_args", | |
53 | path="load_view_args", | |
54 | **core.Parser.__location_map__, | |
55 | ) | |
56 | ||
57 | def _raw_load_json(self, req): | |
58 | """Return a json payload from the request for the core parser's load_json | |
59 | ||
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 | |
64 | ||
65 | return core.parse_json(req.get_data(cache=True)) | |
66 | ||
67 | def _handle_invalid_json_error(self, error, req, *args, **kwargs): | |
68 | abort(400, exc=error, messages={"json": ["Invalid JSON body."]}) | |
69 | ||
70 | def load_view_args(self, req, schema): | |
71 | """Return the request's ``view_args`` or ``missing`` if there are none.""" | |
72 | return req.view_args or core.missing | |
73 | ||
74 | def load_querystring(self, req, schema): | |
75 | """Return query params from the request as a MultiDictProxy.""" | |
76 | return MultiDictProxy(req.args, schema) | |
77 | ||
78 | def load_form(self, req, schema): | |
79 | """Return form values from the request as a MultiDictProxy.""" | |
80 | return MultiDictProxy(req.form, schema) | |
81 | ||
82 | def load_headers(self, req, schema): | |
83 | """Return headers from the request as a MultiDictProxy.""" | |
84 | return MultiDictProxy(req.headers, schema) | |
85 | ||
86 | def load_cookies(self, req, schema): | |
87 | """Return cookies from the request.""" | |
88 | return req.cookies | |
89 | ||
90 | def load_files(self, req, schema): | |
91 | """Return files from the request as a MultiDictProxy.""" | |
92 | return MultiDictProxy(req.files, schema) | |
93 | ||
94 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): | |
95 | """Handles errors during parsing. Aborts the current HTTP request and | |
96 | responds with a 422 error. | |
97 | """ | |
98 | status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS | |
99 | # on marshmallow 2, a many schema receiving a non-list value will | |
100 | # produce this specific error back -- reformat it to match the | |
101 | # marshmallow 3 message so that Flask can properly encode it | |
102 | messages = error.messages | |
103 | if ( | |
104 | MARSHMALLOW_VERSION_INFO[0] < 3 | |
105 | and schema.many | |
106 | and messages == {0: {}, "_schema": ["Invalid input type."]} | |
107 | ): | |
108 | messages.pop(0) | |
109 | abort( | |
110 | status_code, | |
111 | exc=error, | |
112 | messages=error.messages, | |
113 | schema=schema, | |
114 | headers=error_headers, | |
115 | ) | |
116 | ||
117 | def get_default_request(self): | |
118 | """Override to use Flask's thread-local request object by default""" | |
119 | return flask.request | |
120 | ||
121 | ||
122 | parser = FlaskParser() | |
123 | use_args = parser.use_args | |
124 | use_kwargs = parser.use_kwargs |
0 | from collections.abc import Mapping | |
1 | ||
2 | from webargs.compat import MARSHMALLOW_VERSION_INFO | |
3 | from webargs.core import missing, is_multiple | |
4 | ||
5 | ||
6 | class MultiDictProxy(Mapping): | |
7 | """ | |
8 | A proxy object which wraps multidict types along with a matching schema | |
9 | Whenever a value is looked up, it is checked against the schema to see if | |
10 | there is a matching field where `is_multiple` is True. If there is, then | |
11 | the data should be loaded as a list or tuple. | |
12 | ||
13 | In all other cases, __getitem__ proxies directly to the input multidict. | |
14 | """ | |
15 | ||
16 | def __init__(self, multidict, schema): | |
17 | self.data = multidict | |
18 | self.multiple_keys = self._collect_multiple_keys(schema) | |
19 | ||
20 | @staticmethod | |
21 | def _collect_multiple_keys(schema): | |
22 | result = set() | |
23 | for name, field in schema.fields.items(): | |
24 | if not is_multiple(field): | |
25 | continue | |
26 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
27 | result.add(field.load_from if field.load_from is not None else name) | |
28 | else: | |
29 | result.add(field.data_key if field.data_key is not None else name) | |
30 | return result | |
31 | ||
32 | def __getitem__(self, key): | |
33 | val = self.data.get(key, missing) | |
34 | if val is missing or key not in self.multiple_keys: | |
35 | return val | |
36 | if hasattr(self.data, "getlist"): | |
37 | return self.data.getlist(key) | |
38 | if hasattr(self.data, "getall"): | |
39 | return self.data.getall(key) | |
40 | if isinstance(val, (list, tuple)): | |
41 | return val | |
42 | if val is None: | |
43 | return None | |
44 | return [val] | |
45 | ||
46 | def __str__(self): # str(proxy) proxies to str(proxy.data) | |
47 | return str(self.data) | |
48 | ||
49 | def __repr__(self): | |
50 | return "MultiDictProxy(data={!r}, multiple_keys={!r})".format( | |
51 | self.data, self.multiple_keys | |
52 | ) | |
53 | ||
54 | def __delitem__(self, key): | |
55 | del self.data[key] | |
56 | ||
57 | def __setitem__(self, key, value): | |
58 | self.data[key] = value | |
59 | ||
60 | def __getattr__(self, name): | |
61 | return getattr(self.data, name) | |
62 | ||
63 | def __iter__(self): | |
64 | return iter(self.data) | |
65 | ||
66 | def __contains__(self, x): | |
67 | return x in self.data | |
68 | ||
69 | def __len__(self): | |
70 | return len(self.data) | |
71 | ||
72 | def __eq__(self, other): | |
73 | return self.data == other | |
74 | ||
75 | def __ne__(self, other): | |
76 | return self.data != other |
0 | """Pyramid request argument parsing. | |
1 | ||
2 | Example usage: :: | |
3 | ||
4 | from wsgiref.simple_server import make_server | |
5 | from pyramid.config import Configurator | |
6 | from pyramid.response import Response | |
7 | from marshmallow import fields | |
8 | from webargs.pyramidparser import use_args | |
9 | ||
10 | hello_args = { | |
11 | 'name': fields.Str(missing='World') | |
12 | } | |
13 | ||
14 | @use_args(hello_args) | |
15 | def hello_world(request, args): | |
16 | return Response('Hello ' + args['name']) | |
17 | ||
18 | if __name__ == '__main__': | |
19 | config = Configurator() | |
20 | config.add_route('hello', '/') | |
21 | config.add_view(hello_world, route_name='hello') | |
22 | app = config.make_wsgi_app() | |
23 | server = make_server('0.0.0.0', 6543, app) | |
24 | server.serve_forever() | |
25 | """ | |
26 | import functools | |
27 | from collections.abc import Mapping | |
28 | ||
29 | from webob.multidict import MultiDict | |
30 | from pyramid.httpexceptions import exception_response | |
31 | ||
32 | from webargs import core | |
33 | from webargs.core import json | |
34 | from webargs.multidictproxy import MultiDictProxy | |
35 | ||
36 | ||
37 | def is_json_request(req): | |
38 | return core.is_json(req.headers.get("content-type")) | |
39 | ||
40 | ||
41 | class PyramidParser(core.Parser): | |
42 | """Pyramid request argument parser.""" | |
43 | ||
44 | __location_map__ = dict( | |
45 | matchdict="load_matchdict", | |
46 | path="load_matchdict", | |
47 | **core.Parser.__location_map__, | |
48 | ) | |
49 | ||
50 | def _raw_load_json(self, req): | |
51 | """Return a json payload from the request for the core parser's load_json | |
52 | ||
53 | Checks the input mimetype and may return 'missing' if the mimetype is | |
54 | non-json, even if the request body is parseable as json.""" | |
55 | if not is_json_request(req): | |
56 | return core.missing | |
57 | ||
58 | return core.parse_json(req.body, encoding=req.charset) | |
59 | ||
60 | def load_querystring(self, req, schema): | |
61 | """Return query params from the request as a MultiDictProxy.""" | |
62 | return MultiDictProxy(req.GET, schema) | |
63 | ||
64 | def load_form(self, req, schema): | |
65 | """Return form values from the request as a MultiDictProxy.""" | |
66 | return MultiDictProxy(req.POST, schema) | |
67 | ||
68 | def load_cookies(self, req, schema): | |
69 | """Return cookies from the request as a MultiDictProxy.""" | |
70 | return MultiDictProxy(req.cookies, schema) | |
71 | ||
72 | def load_headers(self, req, schema): | |
73 | """Return headers from the request as a MultiDictProxy.""" | |
74 | return MultiDictProxy(req.headers, schema) | |
75 | ||
76 | def load_files(self, req, schema): | |
77 | """Return files from the request as a MultiDictProxy.""" | |
78 | files = ((k, v) for k, v in req.POST.items() if hasattr(v, "file")) | |
79 | return MultiDictProxy(MultiDict(files), schema) | |
80 | ||
81 | def load_matchdict(self, req, schema): | |
82 | """Return the request's ``matchdict`` as a MultiDictProxy.""" | |
83 | return MultiDictProxy(req.matchdict, schema) | |
84 | ||
85 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): | |
86 | """Handles errors during parsing. Aborts the current HTTP request and | |
87 | responds with a 400 error. | |
88 | """ | |
89 | status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS | |
90 | response = exception_response( | |
91 | status_code, | |
92 | detail=str(error), | |
93 | headers=error_headers, | |
94 | content_type="application/json", | |
95 | ) | |
96 | body = json.dumps(error.messages) | |
97 | response.body = body.encode("utf-8") if isinstance(body, str) else body | |
98 | raise response | |
99 | ||
100 | def _handle_invalid_json_error(self, error, req, *args, **kwargs): | |
101 | messages = {"json": ["Invalid JSON body."]} | |
102 | response = exception_response( | |
103 | 400, detail=str(messages), content_type="application/json" | |
104 | ) | |
105 | body = json.dumps(messages) | |
106 | response.body = body.encode("utf-8") if isinstance(body, str) else body | |
107 | raise response | |
108 | ||
109 | def use_args( | |
110 | self, | |
111 | argmap, | |
112 | req=None, | |
113 | *, | |
114 | location=core.Parser.DEFAULT_LOCATION, | |
115 | as_kwargs=False, | |
116 | validate=None, | |
117 | error_status_code=None, | |
118 | error_headers=None | |
119 | ): | |
120 | """Decorator that injects parsed arguments into a view callable. | |
121 | Supports the *Class-based View* pattern where `request` is saved as an instance | |
122 | attribute on a view class. | |
123 | ||
124 | :param dict argmap: Either a `marshmallow.Schema`, a `dict` | |
125 | of argname -> `marshmallow.fields.Field` pairs, or a callable | |
126 | which accepts a request and returns a `marshmallow.Schema`. | |
127 | :param req: The request object to parse. Pulled off of the view by default. | |
128 | :param str location: Where on the request to load values. | |
129 | :param bool as_kwargs: Whether to insert arguments as keyword arguments. | |
130 | :param callable validate: Validation function that receives the dictionary | |
131 | of parsed arguments. If the function returns ``False``, the parser | |
132 | will raise a :exc:`ValidationError`. | |
133 | :param int error_status_code: Status code passed to error handler functions when | |
134 | a `ValidationError` is raised. | |
135 | :param dict error_headers: Headers passed to error handler functions when a | |
136 | a `ValidationError` is raised. | |
137 | """ | |
138 | location = location or self.location | |
139 | # Optimization: If argmap is passed as a dictionary, we only need | |
140 | # to generate a Schema once | |
141 | if isinstance(argmap, Mapping): | |
142 | argmap = core.dict2schema(argmap, schema_class=self.schema_class)() | |
143 | ||
144 | def decorator(func): | |
145 | @functools.wraps(func) | |
146 | def wrapper(obj, *args, **kwargs): | |
147 | # The first argument is either `self` or `request` | |
148 | try: # get self.request | |
149 | request = req or obj.request | |
150 | except AttributeError: # first arg is request | |
151 | request = obj | |
152 | # NOTE: At this point, argmap may be a Schema, callable, or dict | |
153 | parsed_args = self.parse( | |
154 | argmap, | |
155 | req=request, | |
156 | location=location, | |
157 | validate=validate, | |
158 | error_status_code=error_status_code, | |
159 | error_headers=error_headers, | |
160 | ) | |
161 | args, kwargs = self._update_args_kwargs( | |
162 | args, kwargs, parsed_args, as_kwargs | |
163 | ) | |
164 | return func(obj, *args, **kwargs) | |
165 | ||
166 | wrapper.__wrapped__ = func | |
167 | return wrapper | |
168 | ||
169 | return decorator | |
170 | ||
171 | ||
172 | parser = PyramidParser() | |
173 | use_args = parser.use_args | |
174 | use_kwargs = parser.use_kwargs |
0 | """Utilities for testing. Includes a base test class | |
1 | for testing parsers. | |
2 | ||
3 | .. warning:: | |
4 | ||
5 | Methods and functions in this module may change without | |
6 | warning and without a major version change. | |
7 | """ | |
8 | import pytest | |
9 | import webtest | |
10 | ||
11 | from webargs.core import json | |
12 | ||
13 | ||
14 | class CommonTestCase: | |
15 | """Base test class that defines test methods for common functionality across all | |
16 | parsers. Subclasses must define `create_app`, which returns a WSGI-like app. | |
17 | """ | |
18 | ||
19 | def create_app(self): | |
20 | """Return a WSGI app""" | |
21 | raise NotImplementedError("Must define create_app()") | |
22 | ||
23 | def create_testapp(self, app): | |
24 | return webtest.TestApp(app) | |
25 | ||
26 | def before_create_app(self): | |
27 | pass | |
28 | ||
29 | def after_create_app(self): | |
30 | pass | |
31 | ||
32 | @pytest.fixture(scope="class") | |
33 | def testapp(self): | |
34 | self.before_create_app() | |
35 | yield self.create_testapp(self.create_app()) | |
36 | self.after_create_app() | |
37 | ||
38 | def test_parse_querystring_args(self, testapp): | |
39 | assert testapp.get("/echo?name=Fred").json == {"name": "Fred"} | |
40 | ||
41 | def test_parse_form(self, testapp): | |
42 | assert testapp.post("/echo_form", {"name": "Joe"}).json == {"name": "Joe"} | |
43 | ||
44 | def test_parse_json(self, testapp): | |
45 | assert testapp.post_json("/echo_json", {"name": "Fred"}).json == { | |
46 | "name": "Fred" | |
47 | } | |
48 | ||
49 | def test_parse_json_missing(self, testapp): | |
50 | assert testapp.post("/echo_json", "").json == {"name": "World"} | |
51 | ||
52 | def test_parse_json_or_form(self, testapp): | |
53 | assert testapp.post_json("/echo_json_or_form", {"name": "Fred"}).json == { | |
54 | "name": "Fred" | |
55 | } | |
56 | assert testapp.post("/echo_json_or_form", {"name": "Joe"}).json == { | |
57 | "name": "Joe" | |
58 | } | |
59 | assert testapp.post("/echo_json_or_form", "").json == {"name": "World"} | |
60 | ||
61 | def test_parse_querystring_default(self, testapp): | |
62 | assert testapp.get("/echo").json == {"name": "World"} | |
63 | ||
64 | def test_parse_json_default(self, testapp): | |
65 | assert testapp.post_json("/echo_json", {}).json == {"name": "World"} | |
66 | ||
67 | def test_parse_json_with_charset(self, testapp): | |
68 | res = testapp.post( | |
69 | "/echo_json", | |
70 | json.dumps({"name": "Steve"}), | |
71 | content_type="application/json;charset=UTF-8", | |
72 | ) | |
73 | assert res.json == {"name": "Steve"} | |
74 | ||
75 | def test_parse_json_with_vendor_media_type(self, testapp): | |
76 | res = testapp.post( | |
77 | "/echo_json", | |
78 | json.dumps({"name": "Steve"}), | |
79 | content_type="application/vnd.api+json;charset=UTF-8", | |
80 | ) | |
81 | assert res.json == {"name": "Steve"} | |
82 | ||
83 | def test_parse_ignore_extra_data(self, testapp): | |
84 | assert testapp.post_json( | |
85 | "/echo_ignoring_extra_data", {"extra": "data"} | |
86 | ).json == {"name": "World"} | |
87 | ||
88 | def test_parse_json_empty(self, testapp): | |
89 | assert testapp.post_json("/echo_json", {}).json == {"name": "World"} | |
90 | ||
91 | def test_parse_json_error_unexpected_int(self, testapp): | |
92 | res = testapp.post_json("/echo_json", 1, expect_errors=True) | |
93 | assert res.status_code == 422 | |
94 | ||
95 | def test_parse_json_error_unexpected_list(self, testapp): | |
96 | res = testapp.post_json("/echo_json", [{"extra": "data"}], expect_errors=True) | |
97 | assert res.status_code == 422 | |
98 | ||
99 | def test_parse_json_many_schema_invalid_input(self, testapp): | |
100 | res = testapp.post_json( | |
101 | "/echo_many_schema", [{"name": "a"}], expect_errors=True | |
102 | ) | |
103 | assert res.status_code == 422 | |
104 | ||
105 | def test_parse_json_many_schema(self, testapp): | |
106 | res = testapp.post_json("/echo_many_schema", [{"name": "Steve"}]).json | |
107 | assert res == [{"name": "Steve"}] | |
108 | ||
109 | def test_parse_json_many_schema_error_malformed_data(self, testapp): | |
110 | res = testapp.post_json( | |
111 | "/echo_many_schema", {"extra": "data"}, expect_errors=True | |
112 | ) | |
113 | assert res.status_code == 422 | |
114 | ||
115 | def test_parsing_form_default(self, testapp): | |
116 | assert testapp.post("/echo_form", {}).json == {"name": "World"} | |
117 | ||
118 | def test_parse_querystring_multiple(self, testapp): | |
119 | expected = {"name": ["steve", "Loria"]} | |
120 | assert testapp.get("/echo_multi?name=steve&name=Loria").json == expected | |
121 | ||
122 | # test that passing a single value parses correctly | |
123 | # on parsers like falconparser, where there is no native MultiDict type, | |
124 | # this verifies the usage of MultiDictProxy to ensure that single values | |
125 | # are "listified" | |
126 | def test_parse_querystring_multiple_single_value(self, testapp): | |
127 | expected = {"name": ["steve"]} | |
128 | assert testapp.get("/echo_multi?name=steve").json == expected | |
129 | ||
130 | def test_parse_form_multiple(self, testapp): | |
131 | expected = {"name": ["steve", "Loria"]} | |
132 | assert ( | |
133 | testapp.post("/echo_multi_form", {"name": ["steve", "Loria"]}).json | |
134 | == expected | |
135 | ) | |
136 | ||
137 | def test_parse_json_list(self, testapp): | |
138 | expected = {"name": ["Steve"]} | |
139 | assert ( | |
140 | testapp.post_json("/echo_multi_json", {"name": ["Steve"]}).json == expected | |
141 | ) | |
142 | ||
143 | def test_parse_json_list_error_malformed_data(self, testapp): | |
144 | res = testapp.post_json( | |
145 | "/echo_multi_json", {"name": "Steve"}, expect_errors=True | |
146 | ) | |
147 | assert res.status_code == 422 | |
148 | ||
149 | def test_parse_json_with_nonascii_chars(self, testapp): | |
150 | text = "øˆƒ£ºº∆ƒˆ∆" | |
151 | assert testapp.post_json("/echo_json", {"name": text}).json == {"name": text} | |
152 | ||
153 | # https://github.com/marshmallow-code/webargs/issues/427 | |
154 | def test_parse_json_with_nonutf8_chars(self, testapp): | |
155 | res = testapp.post( | |
156 | "/echo_json", | |
157 | b"\xfe", | |
158 | headers={"Accept": "application/json", "Content-Type": "application/json"}, | |
159 | expect_errors=True, | |
160 | ) | |
161 | ||
162 | assert res.status_code == 400 | |
163 | assert res.json == {"json": ["Invalid JSON body."]} | |
164 | ||
165 | def test_validation_error_returns_422_response(self, testapp): | |
166 | res = testapp.post_json("/echo_json", {"name": "b"}, expect_errors=True) | |
167 | assert res.status_code == 422 | |
168 | ||
169 | def test_user_validation_error_returns_422_response_by_default(self, testapp): | |
170 | res = testapp.post_json("/error", {"text": "foo"}, expect_errors=True) | |
171 | assert res.status_code == 422 | |
172 | ||
173 | def test_use_args_decorator(self, testapp): | |
174 | assert testapp.get("/echo_use_args?name=Fred").json == {"name": "Fred"} | |
175 | ||
176 | def test_use_args_with_path_param(self, testapp): | |
177 | url = "/echo_use_args_with_path_param/foo" | |
178 | res = testapp.get(url + "?value=42") | |
179 | assert res.json == {"value": 42} | |
180 | ||
181 | def test_use_args_with_validation(self, testapp): | |
182 | result = testapp.post("/echo_use_args_validated", {"value": 43}) | |
183 | assert result.status_code == 200 | |
184 | result = testapp.post( | |
185 | "/echo_use_args_validated", {"value": 41}, expect_errors=True | |
186 | ) | |
187 | assert result.status_code == 422 | |
188 | ||
189 | def test_use_kwargs_decorator(self, testapp): | |
190 | assert testapp.get("/echo_use_kwargs?name=Fred").json == {"name": "Fred"} | |
191 | ||
192 | def test_use_kwargs_with_path_param(self, testapp): | |
193 | url = "/echo_use_kwargs_with_path_param/foo" | |
194 | res = testapp.get(url + "?value=42") | |
195 | assert res.json == {"value": 42} | |
196 | ||
197 | def test_parsing_headers(self, testapp): | |
198 | res = testapp.get("/echo_headers", headers={"name": "Fred"}) | |
199 | assert res.json == {"name": "Fred"} | |
200 | ||
201 | def test_parsing_cookies(self, testapp): | |
202 | testapp.set_cookie("name", "Steve") | |
203 | res = testapp.get("/echo_cookie") | |
204 | assert res.json == {"name": "Steve"} | |
205 | ||
206 | def test_parse_nested_json(self, testapp): | |
207 | res = testapp.post_json( | |
208 | "/echo_nested", {"name": {"first": "Steve", "last": "Loria"}} | |
209 | ) | |
210 | assert res.json == {"name": {"first": "Steve", "last": "Loria"}} | |
211 | ||
212 | def test_parse_nested_many_json(self, testapp): | |
213 | in_data = {"users": [{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}]} | |
214 | res = testapp.post_json("/echo_nested_many", in_data) | |
215 | assert res.json == in_data | |
216 | ||
217 | # Regression test for https://github.com/marshmallow-code/webargs/issues/120 | |
218 | def test_parse_nested_many_missing(self, testapp): | |
219 | in_data = {} | |
220 | res = testapp.post_json("/echo_nested_many", in_data) | |
221 | assert res.json == {} | |
222 | ||
223 | def test_parse_files(self, testapp): | |
224 | res = testapp.post( | |
225 | "/echo_file", {"myfile": webtest.Upload("README.rst", b"data")} | |
226 | ) | |
227 | assert res.json == {"myfile": "data"} | |
228 | ||
229 | # https://github.com/sloria/webargs/pull/297 | |
230 | def test_empty_json(self, testapp): | |
231 | res = testapp.post("/echo_json") | |
232 | assert res.status_code == 200 | |
233 | assert res.json == {"name": "World"} | |
234 | ||
235 | # https://github.com/sloria/webargs/pull/297 | |
236 | def test_empty_json_with_headers(self, testapp): | |
237 | res = testapp.post( | |
238 | "/echo_json", | |
239 | "", | |
240 | headers={"Accept": "application/json", "Content-Type": "application/json"}, | |
241 | ) | |
242 | assert res.status_code == 200 | |
243 | assert res.json == {"name": "World"} | |
244 | ||
245 | # https://github.com/sloria/webargs/issues/329 | |
246 | def test_invalid_json(self, testapp): | |
247 | res = testapp.post( | |
248 | "/echo_json", | |
249 | '{"foo": "bar", }', | |
250 | headers={"Accept": "application/json", "Content-Type": "application/json"}, | |
251 | expect_errors=True, | |
252 | ) | |
253 | assert res.status_code == 400 | |
254 | assert res.json == {"json": ["Invalid JSON body."]} | |
255 | ||
256 | @pytest.mark.parametrize( | |
257 | ("path", "payload", "content_type"), | |
258 | [ | |
259 | ( | |
260 | "/echo_json", | |
261 | json.dumps({"name": "foo"}), | |
262 | "application/x-www-form-urlencoded", | |
263 | ), | |
264 | ("/echo_form", {"name": "foo"}, "application/json"), | |
265 | ], | |
266 | ) | |
267 | def test_content_type_mismatch(self, testapp, path, payload, content_type): | |
268 | res = testapp.post(path, payload, headers={"Content-Type": content_type}) | |
269 | assert res.json == {"name": "World"} |
0 | """Tornado request argument parsing module. | |
1 | ||
2 | Example: :: | |
3 | ||
4 | import tornado.web | |
5 | from marshmallow import fields | |
6 | from webargs.tornadoparser import use_args | |
7 | ||
8 | class HelloHandler(tornado.web.RequestHandler): | |
9 | ||
10 | @use_args({'name': fields.Str(missing='World')}) | |
11 | def get(self, args): | |
12 | response = {'message': 'Hello {}'.format(args['name'])} | |
13 | self.write(response) | |
14 | """ | |
15 | import tornado.web | |
16 | import tornado.concurrent | |
17 | from tornado.escape import _unicode | |
18 | ||
19 | from webargs import core | |
20 | from webargs.multidictproxy import MultiDictProxy | |
21 | ||
22 | ||
23 | class HTTPError(tornado.web.HTTPError): | |
24 | """`tornado.web.HTTPError` that stores validation errors.""" | |
25 | ||
26 | def __init__(self, *args, **kwargs): | |
27 | self.messages = kwargs.pop("messages", {}) | |
28 | self.headers = kwargs.pop("headers", None) | |
29 | super().__init__(*args, **kwargs) | |
30 | ||
31 | ||
32 | def is_json_request(req): | |
33 | content_type = req.headers.get("Content-Type") | |
34 | return content_type is not None and core.is_json(content_type) | |
35 | ||
36 | ||
37 | class WebArgsTornadoMultiDictProxy(MultiDictProxy): | |
38 | """ | |
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])) | |
61 | ||
62 | ||
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 | """ | |
69 | ||
70 | def __getitem__(self, key): | |
71 | cookie = self.data.get(key, core.missing) | |
72 | if cookie is core.missing: | |
73 | return core.missing | |
74 | if key in self.multiple_keys: | |
75 | return [cookie.value] | |
76 | return cookie.value | |
77 | ||
78 | ||
79 | class TornadoParser(core.Parser): | |
80 | """Tornado request argument parser.""" | |
81 | ||
82 | def _raw_load_json(self, req): | |
83 | """Return a json payload from the request for the core parser's load_json | |
84 | ||
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 | |
89 | ||
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 | |
94 | ||
95 | return core.parse_json(req.body) | |
96 | ||
97 | def load_querystring(self, req, schema): | |
98 | """Return query params from the request as a MultiDictProxy.""" | |
99 | return WebArgsTornadoMultiDictProxy(req.query_arguments, schema) | |
100 | ||
101 | def load_form(self, req, schema): | |
102 | """Return form values from the request as a MultiDictProxy.""" | |
103 | return WebArgsTornadoMultiDictProxy(req.body_arguments, schema) | |
104 | ||
105 | def load_headers(self, req, schema): | |
106 | """Return headers from the request as a MultiDictProxy.""" | |
107 | return WebArgsTornadoMultiDictProxy(req.headers, schema) | |
108 | ||
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): | |
120 | """Handles errors during parsing. Raises a `tornado.web.HTTPError` | |
121 | with a 400 error. | |
122 | """ | |
123 | status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS | |
124 | if status_code == 422: | |
125 | reason = "Unprocessable Entity" | |
126 | else: | |
127 | reason = None | |
128 | raise HTTPError( | |
129 | status_code, | |
130 | log_message=str(error.messages), | |
131 | reason=reason, | |
132 | messages=error.messages, | |
133 | headers=error_headers, | |
134 | ) | |
135 | ||
136 | def _handle_invalid_json_error(self, error, req, *args, **kwargs): | |
137 | raise HTTPError( | |
138 | 400, | |
139 | log_message="Invalid JSON body.", | |
140 | reason="Bad Request", | |
141 | messages={"json": ["Invalid JSON body."]}, | |
142 | ) | |
143 | ||
144 | def get_request_from_view_args(self, view, args, kwargs): | |
145 | return args[0].request | |
146 | ||
147 | ||
148 | parser = TornadoParser() | |
149 | use_args = parser.use_args | |
150 | use_kwargs = parser.use_kwargs |
0 | """Webapp2 request argument parsing module. | |
1 | ||
2 | Example: :: | |
3 | ||
4 | import webapp2 | |
5 | ||
6 | from marshmallow import fields | |
7 | from webargs.webobparser import use_args | |
8 | ||
9 | hello_args = { | |
10 | 'name': fields.Str(missing='World') | |
11 | } | |
12 | ||
13 | class MainPage(webapp2.RequestHandler): | |
14 | ||
15 | @use_args(hello_args) | |
16 | def get_args(self, args): | |
17 | self.response.write('Hello, {name}!'.format(name=args['name'])) | |
18 | ||
19 | @use_kwargs(hello_args) | |
20 | def get_kwargs(self, name=None): | |
21 | self.response.write('Hello, {name}!'.format(name=name)) | |
22 | ||
23 | app = webapp2.WSGIApplication([ | |
24 | webapp2.Route(r'/hello', MainPage, handler_method='get_args'), | |
25 | webapp2.Route(r'/hello_dict', MainPage, handler_method='get_kwargs'), | |
26 | ], debug=True) | |
27 | """ | |
28 | import webapp2 | |
29 | import webob.multidict | |
30 | ||
31 | from webargs import core | |
32 | from webargs.multidictproxy import MultiDictProxy | |
33 | ||
34 | ||
35 | class Webapp2Parser(core.Parser): | |
36 | """webapp2 request argument parser.""" | |
37 | ||
38 | def _raw_load_json(self, req): | |
39 | """Return a json payload from the request for the core parser's load_json.""" | |
40 | if not core.is_json(req.content_type): | |
41 | return core.missing | |
42 | return core.parse_json(req.body) | |
43 | ||
44 | def load_querystring(self, req, schema): | |
45 | """Return query params from the request as a MultiDictProxy.""" | |
46 | return MultiDictProxy(req.GET, schema) | |
47 | ||
48 | def load_form(self, req, schema): | |
49 | """Return form values from the request as a MultiDictProxy.""" | |
50 | return MultiDictProxy(req.POST, schema) | |
51 | ||
52 | def load_cookies(self, req, schema): | |
53 | """Return cookies from the request as a MultiDictProxy.""" | |
54 | return MultiDictProxy(req.cookies, schema) | |
55 | ||
56 | def load_headers(self, req, schema): | |
57 | """Return headers from the request as a MultiDictProxy.""" | |
58 | return MultiDictProxy(req.headers, schema) | |
59 | ||
60 | def load_files(self, req, schema): | |
61 | """Return files from the request as a MultiDictProxy.""" | |
62 | files = ((k, v) for k, v in req.POST.items() if hasattr(v, "file")) | |
63 | return MultiDictProxy(webob.multidict.MultiDict(files), schema) | |
64 | ||
65 | def get_default_request(self): | |
66 | return webapp2.get_request() | |
67 | ||
68 | ||
69 | parser = Webapp2Parser() | |
70 | use_args = parser.use_args | |
71 | use_kwargs = parser.use_kwargs |
0 | # -*- coding: utf-8 -*- |
1 | 1 | |
2 | 2 | import aiohttp |
3 | 3 | from aiohttp.web import json_response |
4 | from aiohttp import web | |
5 | 4 | import marshmallow as ma |
6 | 5 | |
7 | 6 | from webargs import fields |
24 | 23 | strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {} |
25 | 24 | hello_many_schema = HelloSchema(many=True, **strict_kwargs) |
26 | 25 | |
26 | # variant which ignores unknown fields | |
27 | exclude_kwargs = ( | |
28 | {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE} | |
29 | ) | |
30 | hello_exclude_schema = HelloSchema(**exclude_kwargs) | |
31 | ||
32 | ||
27 | 33 | ##### Handlers ##### |
28 | 34 | |
29 | 35 | |
30 | 36 | async def echo(request): |
37 | parsed = await parser.parse(hello_args, request, location="query") | |
38 | return json_response(parsed) | |
39 | ||
40 | ||
41 | async def echo_form(request): | |
42 | parsed = await parser.parse(hello_args, request, location="form") | |
43 | return json_response(parsed) | |
44 | ||
45 | ||
46 | async def echo_json(request): | |
31 | 47 | try: |
32 | parsed = await parser.parse(hello_args, request) | |
48 | parsed = await parser.parse(hello_args, request, location="json") | |
33 | 49 | except json.JSONDecodeError: |
34 | raise web.HTTPBadRequest( | |
50 | raise aiohttp.web.HTTPBadRequest( | |
35 | 51 | body=json.dumps(["Invalid JSON."]).encode("utf-8"), |
36 | 52 | content_type="application/json", |
37 | 53 | ) |
38 | 54 | return json_response(parsed) |
39 | 55 | |
40 | 56 | |
41 | async def echo_query(request): | |
42 | parsed = await parser.parse(hello_args, request, locations=("query",)) | |
43 | return json_response(parsed) | |
44 | ||
45 | ||
46 | @use_args(hello_args) | |
57 | async def echo_json_or_form(request): | |
58 | try: | |
59 | parsed = await parser.parse(hello_args, request, location="json_or_form") | |
60 | except json.JSONDecodeError: | |
61 | raise aiohttp.web.HTTPBadRequest( | |
62 | body=json.dumps(["Invalid JSON."]).encode("utf-8"), | |
63 | content_type="application/json", | |
64 | ) | |
65 | return json_response(parsed) | |
66 | ||
67 | ||
68 | @use_args(hello_args, location="query") | |
47 | 69 | async def echo_use_args(request, args): |
48 | 70 | return json_response(args) |
49 | 71 | |
50 | 72 | |
51 | @use_kwargs(hello_args) | |
73 | @use_kwargs(hello_args, location="query") | |
52 | 74 | async def echo_use_kwargs(request, name): |
53 | 75 | return json_response({"name": name}) |
54 | 76 | |
55 | 77 | |
56 | @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42) | |
78 | @use_args( | |
79 | {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form" | |
80 | ) | |
57 | 81 | async def echo_use_args_validated(request, args): |
58 | 82 | return json_response(args) |
59 | 83 | |
60 | 84 | |
85 | async def echo_ignoring_extra_data(request): | |
86 | return json_response(await parser.parse(hello_exclude_schema, request)) | |
87 | ||
88 | ||
61 | 89 | async def echo_multi(request): |
90 | parsed = await parser.parse(hello_multiple, request, location="query") | |
91 | return json_response(parsed) | |
92 | ||
93 | ||
94 | async def echo_multi_form(request): | |
95 | parsed = await parser.parse(hello_multiple, request, location="form") | |
96 | return json_response(parsed) | |
97 | ||
98 | ||
99 | async def echo_multi_json(request): | |
62 | 100 | parsed = await parser.parse(hello_multiple, request) |
63 | 101 | return json_response(parsed) |
64 | 102 | |
65 | 103 | |
66 | 104 | async def echo_many_schema(request): |
67 | parsed = await parser.parse(hello_many_schema, request, locations=("json",)) | |
68 | return json_response(parsed) | |
69 | ||
70 | ||
71 | @use_args({"value": fields.Int()}) | |
105 | parsed = await parser.parse(hello_many_schema, request) | |
106 | return json_response(parsed) | |
107 | ||
108 | ||
109 | @use_args({"value": fields.Int()}, location="query") | |
72 | 110 | async def echo_use_args_with_path_param(request, args): |
73 | 111 | return json_response(args) |
74 | 112 | |
75 | 113 | |
76 | @use_kwargs({"value": fields.Int()}) | |
114 | @use_kwargs({"value": fields.Int()}, location="query") | |
77 | 115 | async def echo_use_kwargs_with_path_param(request, value): |
78 | 116 | return json_response({"value": value}) |
79 | 117 | |
80 | 118 | |
81 | @use_args({"page": fields.Int(), "q": fields.Int()}, locations=("query",)) | |
82 | @use_args({"name": fields.Str()}, locations=("json",)) | |
119 | @use_args({"page": fields.Int(), "q": fields.Int()}, location="query") | |
120 | @use_args({"name": fields.Str()}) | |
83 | 121 | async def echo_use_args_multiple(request, query_parsed, json_parsed): |
84 | 122 | return json_response({"query_parsed": query_parsed, "json_parsed": json_parsed}) |
85 | 123 | |
94 | 132 | |
95 | 133 | |
96 | 134 | async def echo_headers(request): |
97 | parsed = await parser.parse(hello_args, request, locations=("headers",)) | |
135 | parsed = await parser.parse(hello_exclude_schema, request, location="headers") | |
98 | 136 | return json_response(parsed) |
99 | 137 | |
100 | 138 | |
101 | 139 | async def echo_cookie(request): |
102 | parsed = await parser.parse(hello_args, request, locations=("cookies",)) | |
140 | parsed = await parser.parse(hello_args, request, location="cookies") | |
103 | 141 | return json_response(parsed) |
104 | 142 | |
105 | 143 | |
133 | 171 | |
134 | 172 | |
135 | 173 | async def echo_match_info(request): |
136 | parsed = await parser.parse({"mymatch": fields.Int(location="match_info")}, request) | |
174 | parsed = await parser.parse( | |
175 | {"mymatch": fields.Int()}, request, location="match_info" | |
176 | ) | |
137 | 177 | return json_response(parsed) |
138 | 178 | |
139 | 179 | |
140 | 180 | class EchoHandler: |
141 | @use_args(hello_args) | |
181 | @use_args(hello_args, location="query") | |
142 | 182 | async def get(self, request, args): |
143 | 183 | return json_response(args) |
144 | 184 | |
145 | 185 | |
146 | class EchoHandlerView(web.View): | |
186 | class EchoHandlerView(aiohttp.web.View): | |
147 | 187 | @asyncio.coroutine |
148 | @use_args(hello_args) | |
188 | @use_args(hello_args, location="query") | |
149 | 189 | def get(self, args): |
150 | 190 | return json_response(args) |
151 | 191 | |
152 | 192 | |
153 | 193 | @asyncio.coroutine |
154 | @use_args(HelloSchema, as_kwargs=True) | |
194 | @use_args(HelloSchema, as_kwargs=True, location="query") | |
155 | 195 | def echo_use_schema_as_kwargs(request, name): |
156 | 196 | return json_response({"name": name}) |
157 | 197 | |
167 | 207 | def create_app(): |
168 | 208 | app = aiohttp.web.Application() |
169 | 209 | |
170 | add_route(app, ["GET", "POST"], "/echo", echo) | |
171 | add_route(app, ["GET"], "/echo_query", echo_query) | |
172 | add_route(app, ["GET", "POST"], "/echo_use_args", echo_use_args) | |
173 | add_route(app, ["GET", "POST"], "/echo_use_kwargs", echo_use_kwargs) | |
174 | add_route(app, ["GET", "POST"], "/echo_use_args_validated", echo_use_args_validated) | |
175 | add_route(app, ["GET", "POST"], "/echo_multi", echo_multi) | |
210 | add_route(app, ["GET"], "/echo", echo) | |
211 | add_route(app, ["POST"], "/echo_form", echo_form) | |
212 | add_route(app, ["POST"], "/echo_json", echo_json) | |
213 | add_route(app, ["POST"], "/echo_json_or_form", echo_json_or_form) | |
214 | add_route(app, ["GET"], "/echo_use_args", echo_use_args) | |
215 | add_route(app, ["GET"], "/echo_use_kwargs", echo_use_kwargs) | |
216 | add_route(app, ["POST"], "/echo_use_args_validated", echo_use_args_validated) | |
217 | add_route(app, ["POST"], "/echo_ignoring_extra_data", echo_ignoring_extra_data) | |
218 | add_route(app, ["GET"], "/echo_multi", echo_multi) | |
219 | add_route(app, ["POST"], "/echo_multi_form", echo_multi_form) | |
220 | add_route(app, ["POST"], "/echo_multi_json", echo_multi_json) | |
176 | 221 | add_route(app, ["GET", "POST"], "/echo_many_schema", echo_many_schema) |
177 | 222 | add_route( |
178 | 223 | 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, MARSHMALLOW_VERSION_INFO | |
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())} |
16 | 16 | strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {} |
17 | 17 | hello_many_schema = HelloSchema(many=True, **strict_kwargs) |
18 | 18 | |
19 | # variant which ignores unknown fields | |
20 | exclude_kwargs = ( | |
21 | {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE} | |
22 | ) | |
23 | hello_exclude_schema = HelloSchema(**exclude_kwargs) | |
24 | ||
19 | 25 | |
20 | 26 | app = Bottle() |
21 | 27 | debug(True) |
22 | 28 | |
23 | 29 | |
24 | @app.route("/echo", method=["GET", "POST"]) | |
30 | @app.route("/echo", method=["GET"]) | |
25 | 31 | def echo(): |
26 | return parser.parse(hello_args, request) | |
32 | return parser.parse(hello_args, request, location="query") | |
27 | 33 | |
28 | 34 | |
29 | @app.route("/echo_query") | |
30 | def echo_query(): | |
31 | return parser.parse(hello_args, request, locations=("query",)) | |
35 | @app.route("/echo_form", method=["POST"]) | |
36 | def echo_form(): | |
37 | return parser.parse(hello_args, location="form") | |
32 | 38 | |
33 | 39 | |
34 | @app.route("/echo_use_args", method=["GET", "POST"]) | |
35 | @use_args(hello_args) | |
40 | @app.route("/echo_json", method=["POST"]) | |
41 | def echo_json(): | |
42 | return parser.parse(hello_args, location="json") | |
43 | ||
44 | ||
45 | @app.route("/echo_json_or_form", method=["POST"]) | |
46 | def echo_json_or_form(): | |
47 | return parser.parse(hello_args, location="json_or_form") | |
48 | ||
49 | ||
50 | @app.route("/echo_use_args", method=["GET"]) | |
51 | @use_args(hello_args, location="query") | |
36 | 52 | def echo_use_args(args): |
37 | 53 | return args |
38 | 54 | |
39 | 55 | |
40 | @app.route("/echo_use_kwargs", method=["GET", "POST"], apply=use_kwargs(hello_args)) | |
41 | def echo_use_kwargs(name): | |
42 | return {"name": name} | |
43 | ||
44 | ||
45 | 56 | @app.route( |
46 | 57 | "/echo_use_args_validated", |
47 | method=["GET", "POST"], | |
48 | apply=use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42), | |
58 | method=["POST"], | |
59 | apply=use_args( | |
60 | {"value": fields.Int()}, | |
61 | validate=lambda args: args["value"] > 42, | |
62 | location="form", | |
63 | ), | |
49 | 64 | ) |
50 | 65 | def echo_use_args_validated(args): |
51 | 66 | return args |
52 | 67 | |
53 | 68 | |
54 | @app.route("/echo_multi", method=["GET", "POST"]) | |
55 | def echo_multi(): | |
56 | return parser.parse(hello_multiple, request) | |
69 | @app.route("/echo_ignoring_extra_data", method=["POST"]) | |
70 | def echo_json_ignore_extra_data(): | |
71 | return parser.parse(hello_exclude_schema) | |
57 | 72 | |
58 | 73 | |
59 | @app.route("/echo_many_schema", method=["GET", "POST"]) | |
74 | @app.route( | |
75 | "/echo_use_kwargs", method=["GET"], apply=use_kwargs(hello_args, location="query") | |
76 | ) | |
77 | def echo_use_kwargs(name): | |
78 | return {"name": name} | |
79 | ||
80 | ||
81 | @app.route("/echo_multi", method=["GET"]) | |
82 | def echo_multi(): | |
83 | return parser.parse(hello_multiple, request, location="query") | |
84 | ||
85 | ||
86 | @app.route("/echo_multi_form", method=["POST"]) | |
87 | def multi_form(): | |
88 | return parser.parse(hello_multiple, location="form") | |
89 | ||
90 | ||
91 | @app.route("/echo_multi_json", method=["POST"]) | |
92 | def multi_json(): | |
93 | return parser.parse(hello_multiple) | |
94 | ||
95 | ||
96 | @app.route("/echo_many_schema", method=["POST"]) | |
60 | 97 | def echo_many_schema(): |
61 | arguments = parser.parse(hello_many_schema, request, locations=("json",)) | |
98 | arguments = parser.parse(hello_many_schema, request) | |
62 | 99 | return HTTPResponse(body=json.dumps(arguments), content_type="application/json") |
63 | 100 | |
64 | 101 | |
65 | 102 | @app.route( |
66 | "/echo_use_args_with_path_param/<name>", apply=use_args({"value": fields.Int()}) | |
103 | "/echo_use_args_with_path_param/<name>", | |
104 | apply=use_args({"value": fields.Int()}, location="query"), | |
67 | 105 | ) |
68 | 106 | def echo_use_args_with_path_param(args, name): |
69 | 107 | return args |
70 | 108 | |
71 | 109 | |
72 | 110 | @app.route( |
73 | "/echo_use_kwargs_with_path_param/<name>", apply=use_kwargs({"value": fields.Int()}) | |
111 | "/echo_use_kwargs_with_path_param/<name>", | |
112 | apply=use_kwargs({"value": fields.Int()}, location="query"), | |
74 | 113 | ) |
75 | 114 | def echo_use_kwargs_with_path_param(name, value): |
76 | 115 | return {"value": value} |
87 | 126 | |
88 | 127 | @app.route("/echo_headers") |
89 | 128 | def echo_headers(): |
90 | return parser.parse(hello_args, request, locations=("headers",)) | |
129 | # the "exclude schema" must be used in this case because WSGI headers may | |
130 | # be populated with many fields not sent by the caller | |
131 | return parser.parse(hello_exclude_schema, request, location="headers") | |
91 | 132 | |
92 | 133 | |
93 | 134 | @app.route("/echo_cookie") |
94 | 135 | def echo_cookie(): |
95 | return parser.parse(hello_args, request, locations=("cookies",)) | |
136 | return parser.parse(hello_args, request, location="cookies") | |
96 | 137 | |
97 | 138 | |
98 | 139 | @app.route("/echo_file", method=["POST"]) |
99 | 140 | def echo_file(): |
100 | 141 | args = {"myfile": fields.Field()} |
101 | result = parser.parse(args, locations=("files",)) | |
142 | result = parser.parse(args, location="files") | |
102 | 143 | myfile = result["myfile"] |
103 | 144 | content = myfile.file.read().decode("utf8") |
104 | 145 | 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), | |
8 | url(r"^echo_json$", views.echo_json), | |
9 | url(r"^echo_json_or_form$", views.echo_json_or_form), | |
7 | 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), | |
8 | 13 | url(r"^echo_use_kwargs$", views.echo_use_kwargs), |
9 | 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), | |
10 | 17 | url(r"^echo_many_schema$", views.echo_many_schema), |
11 | 18 | url( |
12 | 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, MARSHMALLOW_VERSION_INFO | |
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())} |
17 | 17 | strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {} |
18 | 18 | hello_many_schema = HelloSchema(many=True, **strict_kwargs) |
19 | 19 | |
20 | # variant which ignores unknown fields | |
21 | exclude_kwargs = ( | |
22 | {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE} | |
23 | ) | |
24 | hello_exclude_schema = HelloSchema(**exclude_kwargs) | |
25 | ||
20 | 26 | |
21 | 27 | def json_response(data, **kwargs): |
22 | 28 | return HttpResponse(json.dumps(data), content_type="application/json", **kwargs) |
23 | 29 | |
24 | 30 | |
25 | def echo(request): | |
26 | try: | |
27 | args = parser.parse(hello_args, request) | |
28 | except ma.ValidationError as err: | |
29 | return json_response(err.messages, status=parser.DEFAULT_VALIDATION_STATUS) | |
30 | except json.JSONDecodeError: | |
31 | return json_response({"json": ["Invalid JSON body."]}, status=400) | |
32 | return json_response(args) | |
31 | def handle_view_errors(f): | |
32 | def wrapped(*args, **kwargs): | |
33 | try: | |
34 | return f(*args, **kwargs) | |
35 | except ma.ValidationError as err: | |
36 | return json_response(err.messages, status=422) | |
37 | except json.JSONDecodeError: | |
38 | return json_response({"json": ["Invalid JSON body."]}, status=400) | |
39 | ||
40 | return wrapped | |
33 | 41 | |
34 | 42 | |
35 | def echo_query(request): | |
36 | return json_response(parser.parse(hello_args, request, locations=("query",))) | |
43 | @handle_view_errors | |
44 | def echo(request): | |
45 | return json_response(parser.parse(hello_args, request, location="query")) | |
37 | 46 | |
38 | 47 | |
39 | @use_args(hello_args) | |
48 | @handle_view_errors | |
49 | def echo_form(request): | |
50 | return json_response(parser.parse(hello_args, request, location="form")) | |
51 | ||
52 | ||
53 | @handle_view_errors | |
54 | def echo_json(request): | |
55 | return json_response(parser.parse(hello_args, request, location="json")) | |
56 | ||
57 | ||
58 | @handle_view_errors | |
59 | def echo_json_or_form(request): | |
60 | return json_response(parser.parse(hello_args, request, location="json_or_form")) | |
61 | ||
62 | ||
63 | @handle_view_errors | |
64 | @use_args(hello_args, location="query") | |
40 | 65 | def echo_use_args(request, args): |
41 | 66 | return json_response(args) |
42 | 67 | |
43 | 68 | |
44 | @use_kwargs(hello_args) | |
69 | @handle_view_errors | |
70 | @use_args( | |
71 | {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form" | |
72 | ) | |
73 | def echo_use_args_validated(args): | |
74 | return json_response(args) | |
75 | ||
76 | ||
77 | @handle_view_errors | |
78 | def echo_ignoring_extra_data(request): | |
79 | return json_response(parser.parse(hello_exclude_schema, request)) | |
80 | ||
81 | ||
82 | @handle_view_errors | |
83 | @use_kwargs(hello_args, location="query") | |
45 | 84 | def echo_use_kwargs(request, name): |
46 | 85 | return json_response({"name": name}) |
47 | 86 | |
48 | 87 | |
88 | @handle_view_errors | |
49 | 89 | def echo_multi(request): |
90 | return json_response(parser.parse(hello_multiple, request, location="query")) | |
91 | ||
92 | ||
93 | @handle_view_errors | |
94 | def echo_multi_form(request): | |
95 | return json_response(parser.parse(hello_multiple, request, location="form")) | |
96 | ||
97 | ||
98 | @handle_view_errors | |
99 | def echo_multi_json(request): | |
50 | 100 | return json_response(parser.parse(hello_multiple, request)) |
51 | 101 | |
52 | 102 | |
103 | @handle_view_errors | |
53 | 104 | def echo_many_schema(request): |
54 | try: | |
55 | return json_response( | |
56 | parser.parse(hello_many_schema, request, locations=("json",)) | |
57 | ) | |
58 | except ma.ValidationError as err: | |
59 | return json_response(err.messages, status=parser.DEFAULT_VALIDATION_STATUS) | |
105 | return json_response(parser.parse(hello_many_schema, request)) | |
60 | 106 | |
61 | 107 | |
62 | @use_args({"value": fields.Int()}) | |
108 | @handle_view_errors | |
109 | @use_args({"value": fields.Int()}, location="query") | |
63 | 110 | def echo_use_args_with_path_param(request, args, name): |
64 | 111 | return json_response(args) |
65 | 112 | |
66 | 113 | |
67 | @use_kwargs({"value": fields.Int()}) | |
114 | @handle_view_errors | |
115 | @use_kwargs({"value": fields.Int()}, location="query") | |
68 | 116 | def echo_use_kwargs_with_path_param(request, value, name): |
69 | 117 | return json_response({"value": value}) |
70 | 118 | |
71 | 119 | |
120 | @handle_view_errors | |
72 | 121 | def always_error(request): |
73 | 122 | def always_fail(value): |
74 | 123 | raise ma.ValidationError("something went wrong") |
75 | 124 | |
76 | 125 | argmap = {"text": fields.Str(validate=always_fail)} |
77 | try: | |
78 | return parser.parse(argmap, request) | |
79 | except ma.ValidationError as err: | |
80 | return json_response(err.messages, status=parser.DEFAULT_VALIDATION_STATUS) | |
126 | return parser.parse(argmap, request) | |
81 | 127 | |
82 | 128 | |
129 | @handle_view_errors | |
83 | 130 | def echo_headers(request): |
84 | return json_response(parser.parse(hello_args, request, locations=("headers",))) | |
131 | return json_response( | |
132 | parser.parse(hello_exclude_schema, request, location="headers") | |
133 | ) | |
85 | 134 | |
86 | 135 | |
136 | @handle_view_errors | |
87 | 137 | def echo_cookie(request): |
88 | return json_response(parser.parse(hello_args, request, locations=("cookies",))) | |
138 | return json_response(parser.parse(hello_args, request, location="cookies")) | |
89 | 139 | |
90 | 140 | |
141 | @handle_view_errors | |
91 | 142 | def echo_file(request): |
92 | 143 | args = {"myfile": fields.Field()} |
93 | result = parser.parse(args, request, locations=("files",)) | |
144 | result = parser.parse(args, request, location="files") | |
94 | 145 | myfile = result["myfile"] |
95 | 146 | content = myfile.read().decode("utf8") |
96 | 147 | return json_response({"myfile": content}) |
97 | 148 | |
98 | 149 | |
150 | @handle_view_errors | |
99 | 151 | def echo_nested(request): |
100 | 152 | argmap = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})} |
101 | 153 | return json_response(parser.parse(argmap, request)) |
102 | 154 | |
103 | 155 | |
156 | @handle_view_errors | |
104 | 157 | def echo_nested_many(request): |
105 | 158 | argmap = { |
106 | 159 | "users": fields.Nested({"id": fields.Int(), "name": fields.Str()}, many=True) |
109 | 162 | |
110 | 163 | |
111 | 164 | class EchoCBV(View): |
165 | @handle_view_errors | |
112 | 166 | def get(self, request): |
113 | try: | |
114 | args = parser.parse(hello_args, self.request) | |
115 | except ma.ValidationError as err: | |
116 | return json_response(err.messages, status=parser.DEFAULT_VALIDATION_STATUS) | |
117 | return json_response(args) | |
167 | location_kwarg = {} if request.method == "POST" else {"location": "query"} | |
168 | return json_response(parser.parse(hello_args, self.request, **location_kwarg)) | |
118 | 169 | |
119 | 170 | post = get |
120 | 171 | |
121 | 172 | |
122 | 173 | class EchoUseArgsCBV(View): |
123 | @use_args(hello_args) | |
174 | @handle_view_errors | |
175 | @use_args(hello_args, location="query") | |
124 | 176 | def get(self, request, args): |
125 | 177 | return json_response(args) |
126 | 178 | |
127 | post = get | |
179 | @handle_view_errors | |
180 | @use_args(hello_args) | |
181 | def post(self, request, args): | |
182 | return json_response(args) | |
128 | 183 | |
129 | 184 | |
130 | 185 | class EchoUseArgsWithParamCBV(View): |
131 | @use_args(hello_args) | |
186 | @handle_view_errors | |
187 | @use_args(hello_args, location="query") | |
132 | 188 | def get(self, request, args, pid): |
133 | 189 | return json_response(args) |
134 | 190 | |
135 | post = get | |
191 | @handle_view_errors | |
192 | @use_args(hello_args) | |
193 | def post(self, request, args, pid): | |
194 | return json_response(args) |
0 | from webargs.core import json | |
1 | ||
2 | 0 | import falcon |
3 | 1 | import marshmallow as ma |
2 | ||
4 | 3 | from webargs import fields |
4 | from webargs.core import MARSHMALLOW_VERSION_INFO, 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())} |
16 | 15 | strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {} |
17 | 16 | hello_many_schema = HelloSchema(many=True, **strict_kwargs) |
18 | 17 | |
19 | ||
20 | class Echo(object): | |
21 | def on_get(self, req, resp): | |
22 | try: | |
23 | parsed = parser.parse(hello_args, req) | |
24 | except json.JSONDecodeError: | |
25 | resp.body = json.dumps(["Invalid JSON."]) | |
26 | resp.status = falcon.HTTP_400 | |
27 | else: | |
28 | resp.body = json.dumps(parsed) | |
29 | ||
30 | on_post = on_get | |
18 | # variant which ignores unknown fields | |
19 | exclude_kwargs = ( | |
20 | {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE} | |
21 | ) | |
22 | hello_exclude_schema = HelloSchema(**exclude_kwargs) | |
31 | 23 | |
32 | 24 | |
33 | class EchoQuery(object): | |
25 | class Echo: | |
34 | 26 | def on_get(self, req, resp): |
35 | parsed = parser.parse(hello_args, req, locations=("query",)) | |
27 | parsed = parser.parse(hello_args, req, location="query") | |
36 | 28 | resp.body = json.dumps(parsed) |
37 | 29 | |
38 | 30 | |
39 | class EchoUseArgs(object): | |
40 | @use_args(hello_args) | |
31 | class EchoForm: | |
32 | def on_post(self, req, resp): | |
33 | parsed = parser.parse(hello_args, req, location="form") | |
34 | resp.body = json.dumps(parsed) | |
35 | ||
36 | ||
37 | class EchoJSON: | |
38 | def on_post(self, req, resp): | |
39 | parsed = parser.parse(hello_args, req, location="json") | |
40 | resp.body = json.dumps(parsed) | |
41 | ||
42 | ||
43 | class EchoJSONOrForm: | |
44 | def on_post(self, req, resp): | |
45 | parsed = parser.parse(hello_args, req, location="json_or_form") | |
46 | resp.body = json.dumps(parsed) | |
47 | ||
48 | ||
49 | class EchoUseArgs: | |
50 | @use_args(hello_args, location="query") | |
41 | 51 | def on_get(self, req, resp, args): |
42 | 52 | resp.body = json.dumps(args) |
43 | 53 | |
44 | on_post = on_get | |
45 | 54 | |
46 | ||
47 | class EchoUseKwargs(object): | |
48 | @use_kwargs(hello_args) | |
55 | class EchoUseKwargs: | |
56 | @use_kwargs(hello_args, location="query") | |
49 | 57 | def on_get(self, req, resp, name): |
50 | 58 | resp.body = json.dumps({"name": name}) |
51 | 59 | |
52 | on_post = on_get | |
60 | ||
61 | class EchoUseArgsValidated: | |
62 | @use_args( | |
63 | {"value": fields.Int()}, | |
64 | validate=lambda args: args["value"] > 42, | |
65 | location="form", | |
66 | ) | |
67 | def on_post(self, req, resp, args): | |
68 | resp.body = json.dumps(args) | |
53 | 69 | |
54 | 70 | |
55 | class EchoUseArgsValidated(object): | |
56 | @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42) | |
57 | def on_get(self, req, resp, args): | |
58 | resp.body = json.dumps(args) | |
59 | ||
60 | on_post = on_get | |
71 | class EchoJSONIgnoreExtraData: | |
72 | def on_post(self, req, resp): | |
73 | resp.body = json.dumps(parser.parse(hello_exclude_schema, req)) | |
61 | 74 | |
62 | 75 | |
63 | class EchoMulti(object): | |
76 | class EchoMulti: | |
64 | 77 | def on_get(self, req, resp): |
78 | resp.body = json.dumps(parser.parse(hello_multiple, req, location="query")) | |
79 | ||
80 | ||
81 | class EchoMultiForm: | |
82 | def on_post(self, req, resp): | |
83 | resp.body = json.dumps(parser.parse(hello_multiple, req, location="form")) | |
84 | ||
85 | ||
86 | class EchoMultiJSON: | |
87 | def on_post(self, req, resp): | |
65 | 88 | resp.body = json.dumps(parser.parse(hello_multiple, req)) |
66 | 89 | |
67 | on_post = on_get | |
90 | ||
91 | class EchoManySchema: | |
92 | def on_post(self, req, resp): | |
93 | resp.body = json.dumps(parser.parse(hello_many_schema, req)) | |
68 | 94 | |
69 | 95 | |
70 | class EchoManySchema(object): | |
71 | def on_get(self, req, resp): | |
72 | resp.body = json.dumps( | |
73 | parser.parse(hello_many_schema, req, locations=("json",)) | |
74 | ) | |
75 | ||
76 | on_post = on_get | |
77 | ||
78 | ||
79 | class EchoUseArgsWithPathParam(object): | |
80 | @use_args({"value": fields.Int()}) | |
96 | class EchoUseArgsWithPathParam: | |
97 | @use_args({"value": fields.Int()}, location="query") | |
81 | 98 | def on_get(self, req, resp, args, name): |
82 | 99 | resp.body = json.dumps(args) |
83 | 100 | |
84 | 101 | |
85 | class EchoUseKwargsWithPathParam(object): | |
86 | @use_kwargs({"value": fields.Int()}) | |
102 | class EchoUseKwargsWithPathParam: | |
103 | @use_kwargs({"value": fields.Int()}, location="query") | |
87 | 104 | def on_get(self, req, resp, value, name): |
88 | 105 | resp.body = json.dumps({"value": value}) |
89 | 106 | |
90 | 107 | |
91 | class AlwaysError(object): | |
108 | class AlwaysError: | |
92 | 109 | def on_get(self, req, resp): |
93 | 110 | def always_fail(value): |
94 | 111 | raise ma.ValidationError("something went wrong") |
99 | 116 | on_post = on_get |
100 | 117 | |
101 | 118 | |
102 | class EchoHeaders(object): | |
119 | class EchoHeaders: | |
103 | 120 | def on_get(self, req, resp): |
104 | resp.body = json.dumps(parser.parse(hello_args, req, locations=("headers",))) | |
121 | class HeaderSchema(ma.Schema): | |
122 | NAME = fields.Str(missing="World") | |
123 | ||
124 | resp.body = json.dumps( | |
125 | parser.parse(HeaderSchema(**exclude_kwargs), req, location="headers") | |
126 | ) | |
105 | 127 | |
106 | 128 | |
107 | class EchoCookie(object): | |
129 | class EchoCookie: | |
108 | 130 | def on_get(self, req, resp): |
109 | resp.body = json.dumps(parser.parse(hello_args, req, locations=("cookies",))) | |
131 | resp.body = json.dumps(parser.parse(hello_args, req, location="cookies")) | |
110 | 132 | |
111 | 133 | |
112 | class EchoNested(object): | |
134 | class EchoNested: | |
113 | 135 | def on_post(self, req, resp): |
114 | 136 | args = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})} |
115 | 137 | resp.body = json.dumps(parser.parse(args, req)) |
116 | 138 | |
117 | 139 | |
118 | class EchoNestedMany(object): | |
140 | class EchoNestedMany: | |
119 | 141 | def on_post(self, req, resp): |
120 | 142 | args = { |
121 | 143 | "users": fields.Nested( |
126 | 148 | |
127 | 149 | |
128 | 150 | def use_args_hook(args, context_key="args", **kwargs): |
129 | def hook(req, resp, params): | |
151 | def hook(req, resp, resource, params): | |
130 | 152 | parsed_args = parser.parse(args, req=req, **kwargs) |
131 | 153 | req.context[context_key] = parsed_args |
132 | 154 | |
133 | 155 | return hook |
134 | 156 | |
135 | 157 | |
136 | @falcon.before(use_args_hook(hello_args)) | |
137 | class EchoUseArgsHook(object): | |
158 | @falcon.before(use_args_hook(hello_args, location="query")) | |
159 | class EchoUseArgsHook: | |
138 | 160 | def on_get(self, req, resp): |
139 | 161 | resp.body = json.dumps(req.context["args"]) |
140 | 162 | |
142 | 164 | def create_app(): |
143 | 165 | app = falcon.API() |
144 | 166 | app.add_route("/echo", Echo()) |
145 | app.add_route("/echo_query", EchoQuery()) | |
167 | app.add_route("/echo_form", EchoForm()) | |
168 | app.add_route("/echo_json", EchoJSON()) | |
169 | app.add_route("/echo_json_or_form", EchoJSONOrForm()) | |
146 | 170 | app.add_route("/echo_use_args", EchoUseArgs()) |
147 | 171 | app.add_route("/echo_use_kwargs", EchoUseKwargs()) |
148 | 172 | app.add_route("/echo_use_args_validated", EchoUseArgsValidated()) |
173 | app.add_route("/echo_ignoring_extra_data", EchoJSONIgnoreExtraData()) | |
149 | 174 | app.add_route("/echo_multi", EchoMulti()) |
175 | app.add_route("/echo_multi_form", EchoMultiForm()) | |
176 | app.add_route("/echo_multi_json", EchoMultiJSON()) | |
150 | 177 | app.add_route("/echo_many_schema", EchoManySchema()) |
151 | 178 | app.add_route("/echo_use_args_with_path_param/{name}", EchoUseArgsWithPathParam()) |
152 | 179 | 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, MARSHMALLOW_VERSION_INFO | |
8 | 7 | |
9 | 8 | |
10 | 9 | class TestAppConfig: |
22 | 21 | strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {} |
23 | 22 | hello_many_schema = HelloSchema(many=True, **strict_kwargs) |
24 | 23 | |
24 | # variant which ignores unknown fields | |
25 | exclude_kwargs = ( | |
26 | {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE} | |
27 | ) | |
28 | hello_exclude_schema = HelloSchema(**exclude_kwargs) | |
29 | ||
25 | 30 | app = Flask(__name__) |
26 | 31 | app.config.from_object(TestAppConfig) |
27 | 32 | |
28 | 33 | |
29 | @app.route("/echo", methods=["GET", "POST"]) | |
34 | @app.route("/echo", methods=["GET"]) | |
30 | 35 | 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",))) | |
37 | ||
38 | ||
39 | @app.route("/echo_use_args", methods=["GET", "POST"]) | |
40 | @use_args(hello_args) | |
36 | return J(parser.parse(hello_args, location="query")) | |
37 | ||
38 | ||
39 | @app.route("/echo_form", methods=["POST"]) | |
40 | def echo_form(): | |
41 | return J(parser.parse(hello_args, location="form")) | |
42 | ||
43 | ||
44 | @app.route("/echo_json", methods=["POST"]) | |
45 | def echo_json(): | |
46 | return J(parser.parse(hello_args, location="json")) | |
47 | ||
48 | ||
49 | @app.route("/echo_json_or_form", methods=["POST"]) | |
50 | def echo_json_or_form(): | |
51 | return J(parser.parse(hello_args, location="json_or_form")) | |
52 | ||
53 | ||
54 | @app.route("/echo_use_args", methods=["GET"]) | |
55 | @use_args(hello_args, location="query") | |
41 | 56 | def echo_use_args(args): |
42 | 57 | return J(args) |
43 | 58 | |
44 | 59 | |
45 | @app.route("/echo_use_args_validated", methods=["GET", "POST"]) | |
46 | @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42) | |
60 | @app.route("/echo_use_args_validated", methods=["POST"]) | |
61 | @use_args( | |
62 | {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form" | |
63 | ) | |
47 | 64 | def echo_use_args_validated(args): |
48 | 65 | return J(args) |
49 | 66 | |
50 | 67 | |
51 | @app.route("/echo_use_kwargs", methods=["GET", "POST"]) | |
52 | @use_kwargs(hello_args) | |
68 | @app.route("/echo_ignoring_extra_data", methods=["POST"]) | |
69 | def echo_json_ignore_extra_data(): | |
70 | return J(parser.parse(hello_exclude_schema)) | |
71 | ||
72 | ||
73 | @app.route("/echo_use_kwargs", methods=["GET"]) | |
74 | @use_kwargs(hello_args, location="query") | |
53 | 75 | def echo_use_kwargs(name): |
54 | 76 | return J({"name": name}) |
55 | 77 | |
56 | 78 | |
57 | @app.route("/echo_multi", methods=["GET", "POST"]) | |
79 | @app.route("/echo_multi", methods=["GET"]) | |
58 | 80 | def multi(): |
81 | return J(parser.parse(hello_multiple, location="query")) | |
82 | ||
83 | ||
84 | @app.route("/echo_multi_form", methods=["POST"]) | |
85 | def multi_form(): | |
86 | return J(parser.parse(hello_multiple, location="form")) | |
87 | ||
88 | ||
89 | @app.route("/echo_multi_json", methods=["POST"]) | |
90 | def multi_json(): | |
59 | 91 | return J(parser.parse(hello_multiple)) |
60 | 92 | |
61 | 93 | |
62 | 94 | @app.route("/echo_many_schema", methods=["GET", "POST"]) |
63 | 95 | def many_nested(): |
64 | arguments = parser.parse(hello_many_schema, locations=("json",)) | |
96 | arguments = parser.parse(hello_many_schema) | |
65 | 97 | return Response(json.dumps(arguments), content_type="application/json") |
66 | 98 | |
67 | 99 | |
68 | 100 | @app.route("/echo_use_args_with_path_param/<name>") |
69 | @use_args({"value": fields.Int()}) | |
101 | @use_args({"value": fields.Int()}, location="query") | |
70 | 102 | def echo_use_args_with_path(args, name): |
71 | 103 | return J(args) |
72 | 104 | |
73 | 105 | |
74 | 106 | @app.route("/echo_use_kwargs_with_path_param/<name>") |
75 | @use_kwargs({"value": fields.Int()}) | |
107 | @use_kwargs({"value": fields.Int()}, location="query") | |
76 | 108 | def echo_use_kwargs_with_path(name, value): |
77 | 109 | return J({"value": value}) |
78 | 110 | |
88 | 120 | |
89 | 121 | @app.route("/echo_headers") |
90 | 122 | def echo_headers(): |
91 | return J(parser.parse(hello_args, locations=("headers",))) | |
123 | # the "exclude schema" must be used in this case because WSGI headers may | |
124 | # be populated with many fields not sent by the caller | |
125 | return J(parser.parse(hello_exclude_schema, location="headers")) | |
92 | 126 | |
93 | 127 | |
94 | 128 | @app.route("/echo_cookie") |
95 | 129 | def echo_cookie(): |
96 | return J(parser.parse(hello_args, request, locations=("cookies",))) | |
130 | return J(parser.parse(hello_args, request, location="cookies")) | |
97 | 131 | |
98 | 132 | |
99 | 133 | @app.route("/echo_file", methods=["POST"]) |
100 | 134 | def echo_file(): |
101 | 135 | args = {"myfile": fields.Field()} |
102 | result = parser.parse(args, locations=("files",)) | |
136 | result = parser.parse(args, location="files") | |
103 | 137 | fp = result["myfile"] |
104 | 138 | content = fp.read().decode("utf8") |
105 | 139 | return J({"myfile": content}) |
107 | 141 | |
108 | 142 | @app.route("/echo_view_arg/<view_arg>") |
109 | 143 | def echo_view_arg(view_arg): |
110 | return J(parser.parse({"view_arg": fields.Int()}, locations=("view_args",))) | |
144 | return J(parser.parse({"view_arg": fields.Int()}, location="view_args")) | |
111 | 145 | |
112 | 146 | |
113 | 147 | @app.route("/echo_view_arg_use_args/<view_arg>") |
114 | @use_args({"view_arg": fields.Int(location="view_args")}) | |
148 | @use_args({"view_arg": fields.Int()}, location="view_args") | |
115 | 149 | def echo_view_arg_with_use_args(args, **kwargs): |
116 | 150 | return J(args) |
117 | 151 | |
176 | 210 | def handle_error(err): |
177 | 211 | if err.code == 422: |
178 | 212 | assert isinstance(err.data["schema"], ma.Schema) |
179 | return J(err.data["messages"]), err.code | |
213 | ||
214 | if MARSHMALLOW_VERSION_INFO[0] >= 3: | |
215 | return J(err.data["messages"]), err.code | |
216 | ||
217 | # on marshmallow2, validation errors for nested schemas can fail to encode: | |
218 | # https://github.com/marshmallow-code/marshmallow/issues/493 | |
219 | # to workaround this, convert integer keys to strings | |
220 | def tweak_data(value): | |
221 | if not isinstance(value, dict): | |
222 | return value | |
223 | return {str(k): v for k, v in value.items()} | |
224 | ||
225 | return J({k: tweak_data(v) for k, v in err.data["messages"].items()}), err.code |
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, MARSHMALLOW_VERSION_INFO | |
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())} |
18 | 16 | strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {} |
19 | 17 | hello_many_schema = HelloSchema(many=True, **strict_kwargs) |
20 | 18 | |
19 | # variant which ignores unknown fields | |
20 | exclude_kwargs = ( | |
21 | {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE} | |
22 | ) | |
23 | hello_exclude_schema = HelloSchema(**exclude_kwargs) | |
24 | ||
21 | 25 | |
22 | 26 | def echo(request): |
27 | return parser.parse(hello_args, request, location="query") | |
28 | ||
29 | ||
30 | def echo_form(request): | |
31 | return parser.parse(hello_args, request, location="form") | |
32 | ||
33 | ||
34 | def echo_json(request): | |
23 | 35 | try: |
24 | return parser.parse(hello_args, request) | |
36 | return parser.parse(hello_args, request, location="json") | |
25 | 37 | except json.JSONDecodeError: |
26 | 38 | error = HTTPBadRequest() |
27 | 39 | error.body = json.dumps(["Invalid JSON."]).encode("utf-8") |
29 | 41 | raise error |
30 | 42 | |
31 | 43 | |
44 | def echo_json_or_form(request): | |
45 | try: | |
46 | return parser.parse(hello_args, request, location="json_or_form") | |
47 | except json.JSONDecodeError: | |
48 | error = HTTPBadRequest() | |
49 | error.body = json.dumps(["Invalid JSON."]).encode("utf-8") | |
50 | error.content_type = "application/json" | |
51 | raise error | |
52 | ||
53 | ||
54 | def echo_json_ignore_extra_data(request): | |
55 | try: | |
56 | return parser.parse(hello_exclude_schema, request) | |
57 | except json.JSONDecodeError: | |
58 | error = HTTPBadRequest() | |
59 | error.body = json.dumps(["Invalid JSON."]).encode("utf-8") | |
60 | error.content_type = "application/json" | |
61 | raise error | |
62 | ||
63 | ||
32 | 64 | def echo_query(request): |
33 | return parser.parse(hello_args, request, locations=("query",)) | |
34 | ||
35 | ||
36 | @use_args(hello_args) | |
65 | return parser.parse(hello_args, request, location="query") | |
66 | ||
67 | ||
68 | @use_args(hello_args, location="query") | |
37 | 69 | def echo_use_args(request, args): |
38 | 70 | return args |
39 | 71 | |
40 | 72 | |
41 | @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42) | |
73 | @use_args( | |
74 | {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form" | |
75 | ) | |
42 | 76 | def echo_use_args_validated(request, args): |
43 | 77 | return args |
44 | 78 | |
45 | 79 | |
46 | @use_kwargs(hello_args) | |
80 | @use_kwargs(hello_args, location="query") | |
47 | 81 | def echo_use_kwargs(request, name): |
48 | 82 | return {"name": name} |
49 | 83 | |
50 | 84 | |
51 | 85 | def echo_multi(request): |
86 | return parser.parse(hello_multiple, request, location="query") | |
87 | ||
88 | ||
89 | def echo_multi_form(request): | |
90 | return parser.parse(hello_multiple, request, location="form") | |
91 | ||
92 | ||
93 | def echo_multi_json(request): | |
52 | 94 | return parser.parse(hello_multiple, request) |
53 | 95 | |
54 | 96 | |
55 | 97 | def echo_many_schema(request): |
56 | return parser.parse(hello_many_schema, request, locations=("json",)) | |
57 | ||
58 | ||
59 | @use_args({"value": fields.Int()}) | |
98 | return parser.parse(hello_many_schema, request) | |
99 | ||
100 | ||
101 | @use_args({"value": fields.Int()}, location="query") | |
60 | 102 | def echo_use_args_with_path_param(request, args): |
61 | 103 | return args |
62 | 104 | |
63 | 105 | |
64 | @use_kwargs({"value": fields.Int()}) | |
106 | @use_kwargs({"value": fields.Int()}, location="query") | |
65 | 107 | def echo_use_kwargs_with_path_param(request, value): |
66 | 108 | return {"value": value} |
67 | 109 | |
75 | 117 | |
76 | 118 | |
77 | 119 | def echo_headers(request): |
78 | return parser.parse(hello_args, request, locations=("headers",)) | |
120 | return parser.parse(hello_exclude_schema, request, location="headers") | |
79 | 121 | |
80 | 122 | |
81 | 123 | def echo_cookie(request): |
82 | return parser.parse(hello_args, request, locations=("cookies",)) | |
124 | return parser.parse(hello_args, request, location="cookies") | |
83 | 125 | |
84 | 126 | |
85 | 127 | def echo_file(request): |
86 | 128 | args = {"myfile": fields.Field()} |
87 | result = parser.parse(args, request, locations=("files",)) | |
129 | result = parser.parse(args, request, location="files") | |
88 | 130 | myfile = result["myfile"] |
89 | 131 | content = myfile.file.read().decode("utf8") |
90 | 132 | return {"myfile": content} |
103 | 145 | |
104 | 146 | |
105 | 147 | def echo_matchdict(request): |
106 | return parser.parse({"mymatch": fields.Int()}, request, locations=("matchdict",)) | |
107 | ||
108 | ||
109 | class EchoCallable(object): | |
148 | return parser.parse({"mymatch": fields.Int()}, request, location="matchdict") | |
149 | ||
150 | ||
151 | class EchoCallable: | |
110 | 152 | def __init__(self, request): |
111 | 153 | self.request = request |
112 | 154 | |
113 | @use_args({"value": fields.Int()}) | |
155 | @use_args({"value": fields.Int()}, location="query") | |
114 | 156 | def __call__(self, args): |
115 | 157 | return args |
116 | 158 | |
126 | 168 | config = Configurator() |
127 | 169 | |
128 | 170 | add_route(config, "/echo", echo) |
171 | add_route(config, "/echo_form", echo_form) | |
172 | add_route(config, "/echo_json", echo_json) | |
173 | add_route(config, "/echo_json_or_form", echo_json_or_form) | |
129 | 174 | add_route(config, "/echo_query", echo_query) |
175 | add_route(config, "/echo_ignoring_extra_data", echo_json_ignore_extra_data) | |
130 | 176 | add_route(config, "/echo_use_args", echo_use_args) |
131 | 177 | add_route(config, "/echo_use_args_validated", echo_use_args_validated) |
132 | 178 | add_route(config, "/echo_use_kwargs", echo_use_kwargs) |
133 | 179 | add_route(config, "/echo_multi", echo_multi) |
180 | add_route(config, "/echo_multi_form", echo_multi_form) | |
181 | add_route(config, "/echo_multi_json", echo_multi_json) | |
134 | 182 | add_route(config, "/echo_many_schema", echo_many_schema) |
135 | 183 | add_route( |
136 | 184 | config, "/echo_use_args_with_path_param/{name}", echo_use_args_with_path_param |
0 | # -*- coding: utf-8 -*- | |
1 | # flake8: noqa | |
2 | import sys | |
3 | ||
4 | PY2 = int(sys.version[0]) == 2 | |
5 | ||
6 | if PY2: | |
7 | text_type = unicode | |
8 | binary_type = str | |
9 | string_types = (str, unicode) | |
10 | basestring = basestring | |
11 | else: | |
12 | text_type = str | |
13 | binary_type = bytes | |
14 | string_types = (str,) | |
15 | basestring = (str, bytes) |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | import itertools |
2 | import mock | |
3 | import sys | |
4 | 1 | import datetime |
5 | 2 | |
6 | 3 | import pytest |
7 | from marshmallow import Schema, post_load, class_registry, validates_schema | |
4 | from marshmallow import Schema, post_load, pre_load, class_registry, validates_schema | |
8 | 5 | from werkzeug.datastructures import MultiDict as WerkMultiDict |
9 | 6 | from django.utils.datastructures import MultiValueDict as DjMultiDict |
10 | 7 | from bottle import MultiDict as BotMultiDict |
11 | 8 | |
12 | from webargs import fields, missing, ValidationError | |
9 | from webargs import fields, ValidationError | |
13 | 10 | from webargs.core import ( |
14 | 11 | Parser, |
15 | get_value, | |
16 | 12 | dict2schema, |
17 | 13 | is_json, |
18 | 14 | get_mimetype, |
19 | 15 | MARSHMALLOW_VERSION_INFO, |
20 | 16 | ) |
17 | from webargs.multidictproxy import MultiDictProxy | |
18 | ||
19 | try: | |
20 | # Python 3.5 | |
21 | import mock | |
22 | except ImportError: | |
23 | # Python 3.6+ | |
24 | from unittest import mock | |
21 | 25 | |
22 | 26 | |
23 | 27 | strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {} |
27 | 31 | def __init__(self, status_code, headers): |
28 | 32 | self.status_code = status_code |
29 | 33 | self.headers = headers |
30 | super(MockHTTPError, self).__init__(self, "HTTP Error occurred") | |
34 | super().__init__(self, "HTTP Error occurred") | |
31 | 35 | |
32 | 36 | |
33 | 37 | class MockRequestParser(Parser): |
34 | 38 | """A minimal parser implementation that parses mock requests.""" |
35 | 39 | |
36 | def parse_querystring(self, req, name, field): | |
37 | return get_value(req.query, name, field) | |
38 | ||
39 | def parse_json(self, req, name, field): | |
40 | return get_value(req.json, name, field) | |
41 | ||
42 | def parse_cookies(self, req, name, field): | |
43 | return get_value(req.cookies, name, field) | |
40 | def load_querystring(self, req, schema): | |
41 | return MultiDictProxy(req.query, schema) | |
42 | ||
43 | def load_json(self, req, schema): | |
44 | return req.json | |
45 | ||
46 | def load_cookies(self, req, schema): | |
47 | return req.cookies | |
44 | 48 | |
45 | 49 | |
46 | 50 | @pytest.yield_fixture(scope="function") |
59 | 63 | # Parser tests |
60 | 64 | |
61 | 65 | |
62 | @mock.patch("webargs.core.Parser.parse_json") | |
63 | def test_parse_json_called_by_parse_arg(parse_json, web_request): | |
64 | field = fields.Field() | |
66 | @mock.patch("webargs.core.Parser.load_json") | |
67 | def test_load_json_called_by_parse_default(load_json, web_request): | |
68 | schema = dict2schema({"foo": fields.Field()})() | |
69 | load_json.return_value = {"foo": 1} | |
65 | 70 | p = Parser() |
66 | p.parse_arg("foo", field, web_request) | |
67 | parse_json.assert_called_with(web_request, "foo", field) | |
68 | ||
69 | ||
70 | @mock.patch("webargs.core.Parser.parse_querystring") | |
71 | def test_parse_querystring_called_by_parse_arg(parse_querystring, web_request): | |
72 | field = fields.Field() | |
73 | p = Parser() | |
74 | p.parse_arg("foo", field, web_request) | |
75 | assert parse_querystring.called_once() | |
76 | ||
77 | ||
78 | @mock.patch("webargs.core.Parser.parse_form") | |
79 | def test_parse_form_called_by_parse_arg(parse_form, web_request): | |
80 | field = fields.Field() | |
81 | p = Parser() | |
82 | p.parse_arg("foo", field, web_request) | |
83 | assert parse_form.called_once() | |
84 | ||
85 | ||
86 | @mock.patch("webargs.core.Parser.parse_json") | |
87 | def test_parse_json_not_called_when_json_not_a_location(parse_json, web_request): | |
88 | field = fields.Field() | |
89 | p = Parser() | |
90 | p.parse_arg("foo", field, web_request, locations=("form", "querystring")) | |
91 | assert parse_json.call_count == 0 | |
92 | ||
93 | ||
94 | @mock.patch("webargs.core.Parser.parse_headers") | |
95 | def test_parse_headers_called_when_headers_is_a_location(parse_headers, web_request): | |
96 | field = fields.Field() | |
97 | p = Parser() | |
98 | p.parse_arg("foo", field, web_request) | |
99 | assert parse_headers.call_count == 0 | |
100 | p.parse_arg("foo", field, web_request, locations=("headers",)) | |
101 | parse_headers.assert_called() | |
102 | ||
103 | ||
104 | @mock.patch("webargs.core.Parser.parse_cookies") | |
105 | def test_parse_cookies_called_when_cookies_is_a_location(parse_cookies, web_request): | |
106 | field = fields.Field() | |
107 | p = Parser() | |
108 | p.parse_arg("foo", field, web_request) | |
109 | assert parse_cookies.call_count == 0 | |
110 | p.parse_arg("foo", field, web_request, locations=("cookies",)) | |
111 | parse_cookies.assert_called() | |
112 | ||
113 | ||
114 | @mock.patch("webargs.core.Parser.parse_json") | |
115 | def test_parse(parse_json, web_request): | |
116 | parse_json.return_value = 42 | |
71 | p.parse(schema, web_request) | |
72 | load_json.assert_called_with(web_request, schema) | |
73 | ||
74 | ||
75 | @pytest.mark.parametrize( | |
76 | "location", ["querystring", "form", "headers", "cookies", "files"] | |
77 | ) | |
78 | def test_load_nondefault_called_by_parse_with_location(location, web_request): | |
79 | with mock.patch( | |
80 | "webargs.core.Parser.load_{}".format(location) | |
81 | ) as mock_loadfunc, mock.patch("webargs.core.Parser.load_json") as load_json: | |
82 | mock_loadfunc.return_value = {} | |
83 | load_json.return_value = {} | |
84 | p = Parser() | |
85 | ||
86 | # ensure that without location=..., the loader is not called (json is | |
87 | # called) | |
88 | p.parse({"foo": fields.Field()}, web_request) | |
89 | assert mock_loadfunc.call_count == 0 | |
90 | assert load_json.call_count == 1 | |
91 | ||
92 | # but when location=... is given, the loader *is* called and json is | |
93 | # not called | |
94 | p.parse({"foo": fields.Field()}, web_request, location=location) | |
95 | assert mock_loadfunc.call_count == 1 | |
96 | # it was already 1, should not go up | |
97 | assert load_json.call_count == 1 | |
98 | ||
99 | ||
100 | def test_parse(parser, web_request): | |
101 | web_request.json = {"username": 42, "password": 42} | |
117 | 102 | argmap = {"username": fields.Field(), "password": fields.Field()} |
118 | p = Parser() | |
119 | ret = p.parse(argmap, web_request) | |
103 | ret = parser.parse(argmap, web_request) | |
120 | 104 | assert {"username": 42, "password": 42} == ret |
105 | ||
106 | ||
107 | @pytest.mark.skipif( | |
108 | MARSHMALLOW_VERSION_INFO[0] < 3, reason="unknown=... added in marshmallow3" | |
109 | ) | |
110 | def test_parse_with_unknown_behavior_specified(parser, web_request): | |
111 | # This is new in webargs 6.x ; it's the way you can "get back" the behavior | |
112 | # of webargs 5.x in which extra args are ignored | |
113 | from marshmallow import EXCLUDE, INCLUDE, RAISE | |
114 | ||
115 | web_request.json = {"username": 42, "password": 42, "fjords": 42} | |
116 | ||
117 | class CustomSchema(Schema): | |
118 | username = fields.Field() | |
119 | password = fields.Field() | |
120 | ||
121 | # with no unknown setting or unknown=RAISE, it blows up | |
122 | with pytest.raises(ValidationError, match="Unknown field."): | |
123 | parser.parse(CustomSchema(), web_request) | |
124 | with pytest.raises(ValidationError, match="Unknown field."): | |
125 | parser.parse(CustomSchema(unknown=RAISE), web_request) | |
126 | ||
127 | # with unknown=EXCLUDE the data is omitted | |
128 | ret = parser.parse(CustomSchema(unknown=EXCLUDE), web_request) | |
129 | assert {"username": 42, "password": 42} == ret | |
130 | # with unknown=INCLUDE it is added even though it isn't part of the schema | |
131 | ret = parser.parse(CustomSchema(unknown=INCLUDE), web_request) | |
132 | assert {"username": 42, "password": 42, "fjords": 42} == ret | |
121 | 133 | |
122 | 134 | |
123 | 135 | def test_parse_required_arg_raises_validation_error(parser, web_request): |
141 | 153 | assert result == {"first": "Steve", "last": None} |
142 | 154 | |
143 | 155 | |
144 | @mock.patch("webargs.core.Parser.parse_json") | |
145 | def test_parse_required_arg(parse_json, web_request): | |
146 | arg = fields.Field(required=True) | |
147 | parse_json.return_value = 42 | |
148 | p = Parser() | |
149 | result = p.parse_arg("foo", arg, web_request, locations=("json",)) | |
150 | assert result == 42 | |
156 | def test_parse_required_arg(parser, web_request): | |
157 | web_request.json = {"foo": 42} | |
158 | result = parser.parse({"foo": fields.Field(required=True)}, web_request) | |
159 | assert result == {"foo": 42} | |
151 | 160 | |
152 | 161 | |
153 | 162 | def test_parse_required_list(parser, web_request): |
155 | 164 | args = {"foo": fields.List(fields.Field(), required=True)} |
156 | 165 | with pytest.raises(ValidationError) as excinfo: |
157 | 166 | parser.parse(args, web_request) |
158 | assert excinfo.value.messages["foo"][0] == "Missing data for required field." | |
167 | assert ( | |
168 | excinfo.value.messages["json"]["foo"][0] == "Missing data for required field." | |
169 | ) | |
159 | 170 | |
160 | 171 | |
161 | 172 | # Regression test for https://github.com/marshmallow-code/webargs/issues/107 |
170 | 181 | args = {"foo": fields.List(fields.Field(), allow_none=False)} |
171 | 182 | with pytest.raises(ValidationError) as excinfo: |
172 | 183 | parser.parse(args, web_request) |
173 | assert excinfo.value.messages["foo"][0] == "Field may not be null." | |
184 | assert excinfo.value.messages["json"]["foo"][0] == "Field may not be null." | |
174 | 185 | |
175 | 186 | |
176 | 187 | def test_parse_empty_list(parser, web_request): |
185 | 196 | assert parser.parse(args, web_request) == {} |
186 | 197 | |
187 | 198 | |
188 | def test_default_locations(): | |
189 | assert set(Parser.DEFAULT_LOCATIONS) == set(["json", "querystring", "form"]) | |
199 | def test_default_location(): | |
200 | assert Parser.DEFAULT_LOCATION == "json" | |
190 | 201 | |
191 | 202 | |
192 | 203 | def test_missing_with_default(parser, web_request): |
193 | 204 | web_request.json = {} |
194 | 205 | args = {"val": fields.Field(missing="pizza")} |
195 | result = parser.parse(args, web_request, locations=("json",)) | |
206 | result = parser.parse(args, web_request) | |
196 | 207 | assert result["val"] == "pizza" |
197 | 208 | |
198 | 209 | |
199 | 210 | def test_default_can_be_none(parser, web_request): |
200 | 211 | web_request.json = {} |
201 | 212 | args = {"val": fields.Field(missing=None, allow_none=True)} |
202 | result = parser.parse(args, web_request, locations=("json",)) | |
213 | result = parser.parse(args, web_request) | |
203 | 214 | assert result["val"] is None |
204 | 215 | |
205 | 216 | |
210 | 221 | "p": fields.Int( |
211 | 222 | missing=1, |
212 | 223 | validate=lambda p: p > 0, |
213 | error=u"La page demandée n'existe pas", | |
224 | error="La page demandée n'existe pas", | |
214 | 225 | location="querystring", |
215 | 226 | ) |
216 | 227 | } |
217 | 228 | assert parser.parse(args, web_request) == {"p": 1} |
218 | 229 | |
219 | 230 | |
220 | def test_value_error_raised_if_parse_arg_called_with_invalid_location(web_request): | |
231 | def test_value_error_raised_if_parse_called_with_invalid_location(parser, web_request): | |
221 | 232 | field = fields.Field() |
233 | with pytest.raises(ValueError, match="Invalid location argument: invalidlocation"): | |
234 | parser.parse({"foo": field}, web_request, location="invalidlocation") | |
235 | ||
236 | ||
237 | @mock.patch("webargs.core.Parser.handle_error") | |
238 | def test_handle_error_called_when_parsing_raises_error(handle_error, web_request): | |
239 | def always_fail(*args, **kwargs): | |
240 | raise ValidationError("error occurred") | |
241 | ||
222 | 242 | p = Parser() |
223 | with pytest.raises(ValueError) as excinfo: | |
224 | p.parse_arg("foo", field, web_request, locations=("invalidlocation", "headers")) | |
225 | assert "Invalid locations arguments: {0}".format(["invalidlocation"]) in str( | |
226 | excinfo | |
227 | ) | |
228 | ||
229 | ||
230 | def test_value_error_raised_if_invalid_location_on_field(web_request, parser): | |
231 | with pytest.raises(ValueError) as excinfo: | |
232 | parser.parse({"foo": fields.Field(location="invalidlocation")}, web_request) | |
233 | assert "Invalid locations arguments: {0}".format(["invalidlocation"]) in str( | |
234 | excinfo | |
235 | ) | |
236 | ||
237 | ||
238 | @mock.patch("webargs.core.Parser.handle_error") | |
239 | @mock.patch("webargs.core.Parser.parse_json") | |
240 | def test_handle_error_called_when_parsing_raises_error( | |
241 | parse_json, handle_error, web_request | |
242 | ): | |
243 | val_err = ValidationError("error occurred") | |
244 | parse_json.side_effect = val_err | |
245 | p = Parser() | |
246 | p.parse({"foo": fields.Field()}, web_request, locations=("json",)) | |
247 | handle_error.assert_called | |
248 | parse_json.side_effect = ValidationError("another exception") | |
249 | p.parse({"foo": fields.Field()}, web_request, locations=("json",)) | |
243 | assert handle_error.call_count == 0 | |
244 | p.parse({"foo": fields.Field()}, web_request, validate=always_fail) | |
245 | assert handle_error.call_count == 1 | |
246 | p.parse({"foo": fields.Field()}, web_request, validate=always_fail) | |
250 | 247 | assert handle_error.call_count == 2 |
251 | 248 | |
252 | 249 | |
253 | 250 | def test_handle_error_reraises_errors(web_request): |
254 | 251 | p = Parser() |
255 | 252 | with pytest.raises(ValidationError): |
256 | p.handle_error(ValidationError("error raised"), web_request, Schema()) | |
257 | ||
258 | ||
259 | @mock.patch("webargs.core.Parser.parse_headers") | |
260 | def test_locations_as_init_arguments(parse_headers, web_request): | |
261 | p = Parser(locations=("headers",)) | |
253 | p.handle_error( | |
254 | ValidationError("error raised"), | |
255 | web_request, | |
256 | Schema(), | |
257 | error_status_code=422, | |
258 | error_headers={}, | |
259 | ) | |
260 | ||
261 | ||
262 | @mock.patch("webargs.core.Parser.load_headers") | |
263 | def test_location_as_init_argument(load_headers, web_request): | |
264 | p = Parser(location="headers") | |
265 | load_headers.return_value = {} | |
262 | 266 | p.parse({"foo": fields.Field()}, web_request) |
263 | assert parse_headers.called | |
264 | ||
265 | ||
266 | @mock.patch("webargs.core.Parser.parse_files") | |
267 | def test_parse_files(parse_files, web_request): | |
268 | p = Parser() | |
269 | p.parse({"foo": fields.Field()}, web_request, locations=("files",)) | |
270 | assert parse_files.called | |
271 | ||
272 | ||
273 | @mock.patch("webargs.core.Parser.parse_json") | |
274 | def test_custom_error_handler(parse_json, web_request): | |
267 | assert load_headers.called | |
268 | ||
269 | ||
270 | def test_custom_error_handler(web_request): | |
275 | 271 | class CustomError(Exception): |
276 | 272 | pass |
277 | 273 | |
278 | def error_handler(error, req, schema, status_code, headers): | |
274 | def error_handler(error, req, schema, *, error_status_code, error_headers): | |
279 | 275 | assert isinstance(schema, Schema) |
280 | 276 | raise CustomError(error) |
281 | 277 | |
282 | parse_json.side_effect = ValidationError("parse_json failed") | |
278 | def failing_validate_func(args): | |
279 | raise ValidationError("parsing failed") | |
280 | ||
281 | class MySchema(Schema): | |
282 | foo = fields.Int() | |
283 | ||
284 | myschema = MySchema(**strict_kwargs) | |
285 | web_request.json = {"foo": "hello world"} | |
286 | ||
283 | 287 | p = Parser(error_handler=error_handler) |
284 | 288 | with pytest.raises(CustomError): |
285 | p.parse({"foo": fields.Field()}, web_request) | |
286 | ||
287 | ||
288 | @mock.patch("webargs.core.Parser.parse_json") | |
289 | def test_custom_error_handler_decorator(parse_json, web_request): | |
289 | p.parse(myschema, web_request, validate=failing_validate_func) | |
290 | ||
291 | ||
292 | def test_custom_error_handler_decorator(web_request): | |
290 | 293 | class CustomError(Exception): |
291 | 294 | pass |
292 | 295 | |
293 | parse_json.side_effect = ValidationError("parse_json failed") | |
294 | ||
296 | mock_schema = mock.Mock(spec=Schema) | |
297 | mock_schema.strict = True | |
298 | mock_schema.load.side_effect = ValidationError("parsing json failed") | |
295 | 299 | parser = Parser() |
296 | 300 | |
297 | 301 | @parser.error_handler |
298 | def handle_error(error, req, schema, status_code, headers): | |
302 | def handle_error(error, req, schema, *, error_status_code, error_headers): | |
299 | 303 | assert isinstance(schema, Schema) |
300 | 304 | raise CustomError(error) |
301 | 305 | |
302 | 306 | with pytest.raises(CustomError): |
303 | parser.parse({"foo": fields.Field()}, web_request) | |
304 | ||
305 | ||
306 | def test_custom_location_handler(web_request): | |
307 | parser.parse(mock_schema, web_request) | |
308 | ||
309 | ||
310 | def test_custom_location_loader(web_request): | |
307 | 311 | web_request.data = {"foo": 42} |
308 | 312 | |
309 | 313 | parser = Parser() |
310 | 314 | |
311 | @parser.location_handler("data") | |
312 | def parse_data(req, name, arg): | |
313 | return req.data.get(name, missing) | |
314 | ||
315 | result = parser.parse({"foo": fields.Int()}, web_request, locations=("data",)) | |
315 | @parser.location_loader("data") | |
316 | def load_data(req, schema): | |
317 | return req.data | |
318 | ||
319 | result = parser.parse({"foo": fields.Int()}, web_request, location="data") | |
316 | 320 | assert result["foo"] == 42 |
317 | 321 | |
318 | 322 | |
319 | def test_custom_location_handler_with_data_key(web_request): | |
323 | def test_custom_location_loader_with_data_key(web_request): | |
320 | 324 | web_request.data = {"X-Foo": 42} |
321 | 325 | parser = Parser() |
322 | 326 | |
323 | @parser.location_handler("data") | |
324 | def parse_data(req, name, arg): | |
325 | return req.data.get(name, missing) | |
327 | @parser.location_loader("data") | |
328 | def load_data(req, schema): | |
329 | return req.data | |
326 | 330 | |
327 | 331 | data_key_kwarg = { |
328 | 332 | "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "X-Foo" |
329 | 333 | } |
330 | 334 | result = parser.parse( |
331 | {"x_foo": fields.Int(**data_key_kwarg)}, web_request, locations=("data",) | |
335 | {"x_foo": fields.Int(**data_key_kwarg)}, web_request, location="data" | |
332 | 336 | ) |
333 | 337 | assert result["x_foo"] == 42 |
334 | 338 | |
335 | 339 | |
336 | def test_full_input_validation(web_request): | |
340 | def test_full_input_validation(parser, web_request): | |
337 | 341 | |
338 | 342 | web_request.json = {"foo": 41, "bar": 42} |
339 | 343 | |
340 | parser = MockRequestParser() | |
341 | 344 | args = {"foo": fields.Int(), "bar": fields.Int()} |
342 | 345 | with pytest.raises(ValidationError): |
343 | 346 | # Test that `validate` receives dictionary of args |
344 | parser.parse( | |
345 | args, | |
346 | web_request, | |
347 | locations=("json",), | |
348 | validate=lambda args: args["foo"] > args["bar"], | |
349 | ) | |
347 | parser.parse(args, web_request, validate=lambda args: args["foo"] > args["bar"]) | |
350 | 348 | |
351 | 349 | |
352 | 350 | def test_full_input_validation_with_multiple_validators(web_request, parser): |
361 | 359 | args = {"a": fields.Int(), "b": fields.Int()} |
362 | 360 | web_request.json = {"a": 2, "b": 1} |
363 | 361 | validators = [validate1, validate2] |
364 | with pytest.raises(ValidationError) as excinfo: | |
365 | parser.parse(args, web_request, locations=("json",), validate=validators) | |
366 | assert "b must be > a" in str(excinfo) | |
362 | with pytest.raises(ValidationError, match="b must be > a"): | |
363 | parser.parse(args, web_request, validate=validators) | |
367 | 364 | |
368 | 365 | web_request.json = {"a": 1, "b": 2} |
369 | with pytest.raises(ValidationError) as excinfo: | |
370 | parser.parse(args, web_request, locations=("json",), validate=validators) | |
371 | assert "a must be > b" in str(excinfo) | |
372 | ||
373 | ||
374 | def test_required_with_custom_error(web_request): | |
375 | web_request.json = {} | |
376 | parser = MockRequestParser() | |
366 | with pytest.raises(ValidationError, match="a must be > b"): | |
367 | parser.parse(args, web_request, validate=validators) | |
368 | ||
369 | ||
370 | def test_required_with_custom_error(parser, web_request): | |
371 | web_request.json = {} | |
377 | 372 | args = { |
378 | 373 | "foo": fields.Str(required=True, error_messages={"required": "We need foo"}) |
379 | 374 | } |
380 | 375 | with pytest.raises(ValidationError) as excinfo: |
381 | 376 | # Test that `validate` receives dictionary of args |
382 | parser.parse(args, web_request, locations=("json",)) | |
383 | ||
384 | assert "We need foo" in excinfo.value.messages["foo"] | |
377 | parser.parse(args, web_request) | |
378 | ||
379 | assert "We need foo" in excinfo.value.messages["json"]["foo"] | |
385 | 380 | if MARSHMALLOW_VERSION_INFO[0] < 3: |
386 | 381 | assert "foo" in excinfo.value.field_names |
387 | 382 | |
388 | 383 | |
389 | def test_required_with_custom_error_and_validation_error(web_request): | |
384 | def test_required_with_custom_error_and_validation_error(parser, web_request): | |
390 | 385 | web_request.json = {"foo": ""} |
391 | parser = MockRequestParser() | |
392 | 386 | args = { |
393 | 387 | "foo": fields.Str( |
394 | 388 | required="We need foo", |
398 | 392 | } |
399 | 393 | with pytest.raises(ValidationError) as excinfo: |
400 | 394 | # Test that `validate` receives dictionary of args |
401 | parser.parse(args, web_request, locations=("json",)) | |
395 | parser.parse(args, web_request) | |
402 | 396 | |
403 | 397 | assert "foo required length is 3" in excinfo.value.args[0]["foo"] |
404 | 398 | if MARSHMALLOW_VERSION_INFO[0] < 3: |
409 | 403 | def validate(val): |
410 | 404 | return False |
411 | 405 | |
412 | text = u"øœ∑∆∑" | |
406 | text = "øœ∑∆∑" | |
413 | 407 | web_request.json = {"text": text} |
414 | 408 | parser = MockRequestParser() |
415 | 409 | args = {"text": fields.Str()} |
416 | 410 | with pytest.raises(ValidationError) as excinfo: |
417 | parser.parse(args, web_request, locations=("json",), validate=validate) | |
418 | assert excinfo.value.messages == ["Invalid value."] | |
411 | parser.parse(args, web_request, validate=validate) | |
412 | assert excinfo.value.messages == {"json": ["Invalid value."]} | |
419 | 413 | |
420 | 414 | |
421 | 415 | def test_invalid_argument_for_validate(web_request, parser): |
422 | 416 | with pytest.raises(ValueError) as excinfo: |
423 | 417 | parser.parse({}, web_request, validate="notcallable") |
424 | 418 | assert "not a callable or list of callables." in excinfo.value.args[0] |
425 | ||
426 | ||
427 | def test_get_value_basic(): | |
428 | assert get_value({"foo": 42}, "foo", False) == 42 | |
429 | assert get_value({"foo": 42}, "bar", False) is missing | |
430 | assert get_value({"foos": ["a", "b"]}, "foos", True) == ["a", "b"] | |
431 | # https://github.com/marshmallow-code/webargs/pull/30 | |
432 | assert get_value({"foos": ["a", "b"]}, "bar", True) is missing | |
433 | 419 | |
434 | 420 | |
435 | 421 | def create_bottle_multi_dict(): |
447 | 433 | |
448 | 434 | |
449 | 435 | @pytest.mark.parametrize("input_dict", multidicts) |
450 | def test_get_value_multidict(input_dict): | |
451 | field = fields.List(fields.Str()) | |
452 | assert get_value(input_dict, "foos", field) == ["a", "b"] | |
436 | def test_multidict_proxy(input_dict): | |
437 | class ListSchema(Schema): | |
438 | foos = fields.List(fields.Str()) | |
439 | ||
440 | class StrSchema(Schema): | |
441 | foos = fields.Str() | |
442 | ||
443 | # this MultiDictProxy is aware that "foos" is a list field and will | |
444 | # therefore produce a list with __getitem__ | |
445 | list_wrapped_multidict = MultiDictProxy(input_dict, ListSchema()) | |
446 | ||
447 | # this MultiDictProxy is under the impression that "foos" is just a string | |
448 | # and it should return "a" or "b" | |
449 | # the decision between "a" and "b" in this case belongs to the framework | |
450 | str_wrapped_multidict = MultiDictProxy(input_dict, StrSchema()) | |
451 | ||
452 | assert list_wrapped_multidict["foos"] == ["a", "b"] | |
453 | assert str_wrapped_multidict["foos"] in ("a", "b") | |
453 | 454 | |
454 | 455 | |
455 | 456 | def test_parse_with_data_key(web_request): |
460 | 461 | "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "Content-Type" |
461 | 462 | } |
462 | 463 | args = {"content_type": fields.Field(**data_key_kwargs)} |
463 | parsed = parser.parse(args, web_request, locations=("json",)) | |
464 | parsed = parser.parse(args, web_request) | |
464 | 465 | assert parsed == {"content_type": "application/json"} |
465 | 466 | |
466 | 467 | |
474 | 475 | |
475 | 476 | parser = MockRequestParser() |
476 | 477 | args = {"content_type": fields.Field(load_from="Content-Type")} |
477 | parsed = parser.parse(args, web_request, locations=("json",)) | |
478 | parsed = parser.parse(args, web_request) | |
478 | 479 | assert parsed == {"content_type": "application/json"} |
479 | 480 | |
480 | 481 | |
487 | 488 | } |
488 | 489 | args = {"content_type": fields.Str(**data_key_kwargs)} |
489 | 490 | with pytest.raises(ValidationError) as excinfo: |
490 | parser.parse(args, web_request, locations=("json",)) | |
491 | assert "Content-Type" in excinfo.value.messages | |
492 | assert excinfo.value.messages["Content-Type"] == ["Not a valid string."] | |
491 | parser.parse(args, web_request) | |
492 | assert "json" in excinfo.value.messages | |
493 | assert "Content-Type" in excinfo.value.messages["json"] | |
494 | assert excinfo.value.messages["json"]["Content-Type"] == ["Not a valid string."] | |
493 | 495 | |
494 | 496 | |
495 | 497 | def test_parse_nested_with_data_key(web_request): |
500 | 502 | } |
501 | 503 | args = {"nested_arg": fields.Nested({"right": fields.Field(**data_key_kwarg)})} |
502 | 504 | |
503 | parsed = parser.parse(args, web_request, locations=("json",)) | |
505 | parsed = parser.parse(args, web_request) | |
504 | 506 | assert parsed == {"nested_arg": {"right": "OK"}} |
505 | 507 | |
506 | 508 | |
517 | 519 | ) |
518 | 520 | } |
519 | 521 | |
520 | parsed = parser.parse(args, web_request, locations=("json",)) | |
522 | parsed = parser.parse(args, web_request) | |
521 | 523 | assert parsed == {"nested_arg": {"found": None}} |
522 | 524 | |
523 | 525 | |
527 | 529 | web_request.json = {"nested_arg": {}} |
528 | 530 | args = {"nested_arg": fields.Nested({"miss": fields.Field(missing="<foo>")})} |
529 | 531 | |
530 | parsed = parser.parse(args, web_request, locations=("json",)) | |
532 | parsed = parser.parse(args, web_request) | |
531 | 533 | assert parsed == {"nested_arg": {"miss": "<foo>"}} |
532 | 534 | |
533 | 535 | |
558 | 560 | web_request.json = {"username": "foo"} |
559 | 561 | web_request.query = {"page": 42} |
560 | 562 | |
561 | @parser.use_args(query_args, web_request, locations=("query",)) | |
562 | @parser.use_args(json_args, web_request, locations=("json",)) | |
563 | @parser.use_args(query_args, web_request, location="query") | |
564 | @parser.use_args(json_args, web_request) | |
563 | 565 | def viewfunc(query_parsed, json_parsed): |
564 | 566 | return {"json": json_parsed, "query": query_parsed} |
565 | 567 | |
574 | 576 | web_request.json = {"username": "foo"} |
575 | 577 | web_request.query = {"page": 42} |
576 | 578 | |
577 | @parser.use_kwargs(query_args, web_request, locations=("query",)) | |
578 | @parser.use_kwargs(json_args, web_request, locations=("json",)) | |
579 | @parser.use_kwargs(query_args, web_request, location="query") | |
580 | @parser.use_kwargs(json_args, web_request) | |
579 | 581 | def viewfunc(page, username): |
580 | 582 | return {"json": {"username": username}, "query": {"page": page}} |
581 | 583 | |
596 | 598 | |
597 | 599 | def test_list_allowed_missing(web_request, parser): |
598 | 600 | args = {"name": fields.List(fields.Str())} |
599 | web_request.json = {"fakedata": True} | |
601 | web_request.json = {} | |
600 | 602 | result = parser.parse(args, web_request) |
601 | 603 | assert result == {} |
602 | 604 | |
603 | 605 | |
604 | 606 | def test_int_list_allowed_missing(web_request, parser): |
605 | 607 | args = {"name": fields.List(fields.Int())} |
606 | web_request.json = {"fakedata": True} | |
608 | web_request.json = {} | |
607 | 609 | result = parser.parse(args, web_request) |
608 | 610 | assert result == {} |
609 | 611 | |
610 | 612 | |
611 | 613 | def test_multiple_arg_required_with_int_conversion(web_request, parser): |
612 | 614 | args = {"ids": fields.List(fields.Int(), required=True)} |
613 | web_request.json = {"fakedata": True} | |
615 | web_request.json = {} | |
614 | 616 | with pytest.raises(ValidationError) as excinfo: |
615 | 617 | parser.parse(args, web_request) |
616 | assert excinfo.value.messages == {"ids": ["Missing data for required field."]} | |
618 | assert excinfo.value.messages == { | |
619 | "json": {"ids": ["Missing data for required field."]} | |
620 | } | |
617 | 621 | |
618 | 622 | |
619 | 623 | def test_parse_with_callable(web_request, parser): |
647 | 651 | strict = True |
648 | 652 | |
649 | 653 | @post_load |
650 | def request_data(self, item): | |
654 | def request_data(self, item, **kwargs): | |
651 | 655 | item["data"] = self.context["request"].data |
652 | 656 | return item |
653 | 657 | |
739 | 743 | |
740 | 744 | assert viewfunc() == {"email": "[email protected]", "password": "bar"} |
741 | 745 | |
742 | # Must skip on older versions of python due to | |
743 | # https://github.com/pytest-dev/pytest/issues/840 | |
744 | @pytest.mark.skipif( | |
745 | sys.version_info < (3, 4), | |
746 | reason="Skipping due to a bug in pytest's warning recording", | |
747 | ) | |
748 | 746 | @pytest.mark.skipif( |
749 | 747 | MARSHMALLOW_VERSION_INFO[0] >= 3, |
750 | 748 | reason='"strict" parameter is removed in marshmallow 3', |
757 | 755 | assert "strict=True" in str(warning.message) |
758 | 756 | |
759 | 757 | def test_use_kwargs_stacked(self, web_request, parser): |
758 | if MARSHMALLOW_VERSION_INFO[0] >= 3: | |
759 | from marshmallow import EXCLUDE | |
760 | ||
761 | class PageSchema(Schema): | |
762 | page = fields.Int() | |
763 | ||
764 | pageschema = PageSchema(unknown=EXCLUDE) | |
765 | userschema = self.UserSchema(unknown=EXCLUDE) | |
766 | else: | |
767 | pageschema = {"page": fields.Int()} | |
768 | userschema = self.UserSchema(**strict_kwargs) | |
769 | ||
760 | 770 | web_request.json = {"email": "[email protected]", "password": "bar", "page": 42} |
761 | 771 | |
762 | @parser.use_kwargs({"page": fields.Int()}, web_request) | |
763 | @parser.use_kwargs(self.UserSchema(**strict_kwargs), web_request) | |
772 | @parser.use_kwargs(pageschema, web_request) | |
773 | @parser.use_kwargs(userschema, web_request) | |
764 | 774 | def viewfunc(email, password, page): |
765 | 775 | return {"email": email, "password": password, "page": page} |
766 | 776 | |
779 | 789 | strict = True |
780 | 790 | |
781 | 791 | @validates_schema(pass_original=True) |
782 | def validate_schema(self, data, original_data): | |
792 | def validate_schema(self, data, original_data, **kwargs): | |
783 | 793 | assert "location" not in original_data |
784 | 794 | return True |
785 | 795 | |
786 | 796 | web_request.json = {"name": "Eric Cartman"} |
787 | res = parser.parse(UserSchema, web_request, locations=("json",)) | |
797 | res = parser.parse(UserSchema, web_request) | |
788 | 798 | assert res == {"name": "Eric Cartman"} |
789 | 799 | |
790 | 800 | |
791 | def test_use_args_with_custom_locations_in_parser(web_request, parser): | |
801 | def test_use_args_with_custom_location_in_parser(web_request, parser): | |
792 | 802 | custom_args = {"foo": fields.Str()} |
793 | 803 | web_request.json = {} |
794 | parser.locations = ("custom",) | |
795 | ||
796 | @parser.location_handler("custom") | |
797 | def parse_custom(req, name, arg): | |
798 | return "bar" | |
804 | parser.location = "custom" | |
805 | ||
806 | @parser.location_loader("custom") | |
807 | def load_custom(schema, req): | |
808 | return {"foo": "bar"} | |
799 | 809 | |
800 | 810 | @parser.use_args(custom_args, web_request) |
801 | 811 | def viewfunc(args): |
837 | 847 | |
838 | 848 | dumped = schema.dump(parsed) |
839 | 849 | data = dumped.data if MARSHMALLOW_VERSION_INFO[0] < 3 else dumped |
840 | assert data["ids"] == [1, 2, 3] | |
841 | ||
842 | ||
843 | def test_delimited_list_as_string(web_request, parser): | |
844 | web_request.json = {"ids": "1,2,3"} | |
850 | assert data["ids"] == "1,2,3" | |
851 | ||
852 | ||
853 | @pytest.mark.skipif( | |
854 | MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3" | |
855 | ) | |
856 | def test_delimited_tuple_default_delimiter(web_request, parser): | |
857 | """ | |
858 | Test load and dump from DelimitedTuple, including the use of a datetime | |
859 | type (similar to a DelimitedList test below) which confirms that we aren't | |
860 | relying on __str__, but are properly de/serializing the included fields | |
861 | """ | |
862 | web_request.json = {"ids": "1,2,2020-05-04"} | |
845 | 863 | schema_cls = dict2schema( |
846 | {"ids": fields.DelimitedList(fields.Int(), as_string=True)} | |
864 | { | |
865 | "ids": fields.DelimitedTuple( | |
866 | (fields.Int, fields.Int, fields.DateTime(format="%Y-%m-%d")) | |
867 | ) | |
868 | } | |
847 | 869 | ) |
848 | 870 | schema = schema_cls() |
849 | 871 | |
850 | 872 | parsed = parser.parse(schema, web_request) |
851 | assert parsed["ids"] == [1, 2, 3] | |
852 | ||
853 | dumped = schema.dump(parsed) | |
854 | data = dumped.data if MARSHMALLOW_VERSION_INFO[0] < 3 else dumped | |
855 | assert data["ids"] == "1,2,3" | |
856 | ||
857 | ||
858 | def test_delimited_list_as_string_v2(web_request, parser): | |
873 | assert parsed["ids"] == (1, 2, datetime.datetime(2020, 5, 4)) | |
874 | ||
875 | data = schema.dump(parsed) | |
876 | assert data["ids"] == "1,2,2020-05-04" | |
877 | ||
878 | ||
879 | @pytest.mark.skipif( | |
880 | MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3" | |
881 | ) | |
882 | def test_delimited_tuple_incorrect_arity(web_request, parser): | |
883 | web_request.json = {"ids": "1,2"} | |
884 | schema_cls = dict2schema( | |
885 | {"ids": fields.DelimitedTuple((fields.Int, fields.Int, fields.Int))} | |
886 | ) | |
887 | schema = schema_cls() | |
888 | ||
889 | with pytest.raises(ValidationError): | |
890 | parser.parse(schema, web_request) | |
891 | ||
892 | ||
893 | def test_delimited_list_with_datetime(web_request, parser): | |
894 | """ | |
895 | Test that DelimitedList(DateTime(format=...)) correctly parses and dumps | |
896 | dates to and from strings -- indicates that we're doing proper | |
897 | serialization of values in dump() and not just relying on __str__ producing | |
898 | correct results | |
899 | """ | |
859 | 900 | web_request.json = {"dates": "2018-11-01,2018-11-02"} |
860 | 901 | schema_cls = dict2schema( |
861 | { | |
862 | "dates": fields.DelimitedList( | |
863 | fields.DateTime(format="%Y-%m-%d"), as_string=True | |
864 | ) | |
865 | } | |
902 | {"dates": fields.DelimitedList(fields.DateTime(format="%Y-%m-%d"))} | |
866 | 903 | ) |
867 | 904 | schema = schema_cls() |
868 | 905 | |
885 | 922 | parsed = parser.parse(schema, web_request) |
886 | 923 | assert parsed["ids"] == [1, 2, 3] |
887 | 924 | |
888 | ||
889 | def test_delimited_list_load_list(web_request, parser): | |
925 | dumped = schema.dump(parsed) | |
926 | data = dumped.data if MARSHMALLOW_VERSION_INFO[0] < 3 else dumped | |
927 | assert data["ids"] == "1|2|3" | |
928 | ||
929 | ||
930 | @pytest.mark.skipif( | |
931 | MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3" | |
932 | ) | |
933 | def test_delimited_tuple_custom_delimiter(web_request, parser): | |
934 | web_request.json = {"ids": "1|2"} | |
935 | schema_cls = dict2schema( | |
936 | {"ids": fields.DelimitedTuple((fields.Int, fields.Int), delimiter="|")} | |
937 | ) | |
938 | schema = schema_cls() | |
939 | ||
940 | parsed = parser.parse(schema, web_request) | |
941 | assert parsed["ids"] == (1, 2) | |
942 | ||
943 | data = schema.dump(parsed) | |
944 | assert data["ids"] == "1|2" | |
945 | ||
946 | ||
947 | def test_delimited_list_load_list_errors(web_request, parser): | |
890 | 948 | web_request.json = {"ids": [1, 2, 3]} |
891 | 949 | schema_cls = dict2schema({"ids": fields.DelimitedList(fields.Int())}) |
892 | 950 | schema = schema_cls() |
893 | 951 | |
894 | parsed = parser.parse(schema, web_request) | |
895 | assert parsed["ids"] == [1, 2, 3] | |
952 | with pytest.raises(ValidationError) as excinfo: | |
953 | parser.parse(schema, web_request) | |
954 | exc = excinfo.value | |
955 | assert isinstance(exc, ValidationError) | |
956 | errors = exc.args[0] | |
957 | assert errors["ids"] == ["Not a valid delimited list."] | |
958 | ||
959 | ||
960 | @pytest.mark.skipif( | |
961 | MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3" | |
962 | ) | |
963 | def test_delimited_tuple_load_list_errors(web_request, parser): | |
964 | web_request.json = {"ids": [1, 2]} | |
965 | schema_cls = dict2schema({"ids": fields.DelimitedTuple((fields.Int, fields.Int))}) | |
966 | schema = schema_cls() | |
967 | ||
968 | with pytest.raises(ValidationError) as excinfo: | |
969 | parser.parse(schema, web_request) | |
970 | exc = excinfo.value | |
971 | assert isinstance(exc, ValidationError) | |
972 | errors = exc.args[0] | |
973 | assert errors["ids"] == ["Not a valid delimited tuple."] | |
896 | 974 | |
897 | 975 | |
898 | 976 | # Regresion test for https://github.com/marshmallow-code/webargs/issues/149 |
903 | 981 | |
904 | 982 | with pytest.raises(ValidationError) as excinfo: |
905 | 983 | parser.parse(schema, web_request) |
906 | assert excinfo.value.messages == {"ids": ["Not a valid list."]} | |
984 | assert excinfo.value.messages == {"json": {"ids": ["Not a valid delimited list."]}} | |
985 | ||
986 | ||
987 | @pytest.mark.skipif( | |
988 | MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3" | |
989 | ) | |
990 | def test_delimited_tuple_passed_invalid_type(web_request, parser): | |
991 | web_request.json = {"ids": 1} | |
992 | schema_cls = dict2schema({"ids": fields.DelimitedTuple((fields.Int,))}) | |
993 | schema = schema_cls() | |
994 | ||
995 | with pytest.raises(ValidationError) as excinfo: | |
996 | parser.parse(schema, web_request) | |
997 | assert excinfo.value.messages == {"json": {"ids": ["Not a valid delimited tuple."]}} | |
907 | 998 | |
908 | 999 | |
909 | 1000 | def test_missing_list_argument_not_in_parsed_result(web_request, parser): |
918 | 1009 | def test_type_conversion_with_multiple_required(web_request, parser): |
919 | 1010 | web_request.json = {} |
920 | 1011 | args = {"ids": fields.List(fields.Int(), required=True)} |
921 | with pytest.raises(ValidationError) as excinfo: | |
1012 | msg = "Missing data for required field." | |
1013 | with pytest.raises(ValidationError, match=msg): | |
922 | 1014 | parser.parse(args, web_request) |
923 | assert "Missing data for required field." in str(excinfo) | |
924 | ||
925 | ||
926 | def test_arg_location_param(web_request, parser): | |
927 | web_request.json = {"foo": 24} | |
928 | web_request.cookies = {"foo": 42} | |
929 | args = {"foo": fields.Field(location="cookies")} | |
930 | ||
931 | parsed = parser.parse(args, web_request) | |
932 | ||
933 | assert parsed["foo"] == 42 | |
934 | 1015 | |
935 | 1016 | |
936 | 1017 | def test_validation_errors_in_validator_are_passed_to_handle_error(parser, web_request): |
982 | 1063 | assert schema.fields["id"].required |
983 | 1064 | if MARSHMALLOW_VERSION_INFO[0] < 3: |
984 | 1065 | assert schema.opts.strict is True |
985 | else: | |
986 | assert schema.opts.register is False | |
987 | 1066 | |
988 | 1067 | |
989 | 1068 | # Regression test for https://github.com/marshmallow-code/webargs/issues/101 |
1032 | 1111 | |
1033 | 1112 | |
1034 | 1113 | class MockRequestParserWithErrorHandler(MockRequestParser): |
1035 | def handle_error( | |
1036 | self, error, req, schema, error_status_code=None, error_headers=None | |
1037 | ): | |
1114 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): | |
1038 | 1115 | assert isinstance(error, ValidationError) |
1039 | 1116 | assert isinstance(schema, Schema) |
1040 | 1117 | raise MockHTTPError(error_status_code, error_headers) |
1051 | 1128 | error = excinfo.value |
1052 | 1129 | assert error.status_code == 418 |
1053 | 1130 | assert error.headers == {"X-Foo": "bar"} |
1131 | ||
1132 | ||
1133 | @mock.patch("webargs.core.Parser.load_json") | |
1134 | def test_custom_schema_class(load_json, web_request): | |
1135 | class CustomSchema(Schema): | |
1136 | @pre_load | |
1137 | def pre_load(self, data, **kwargs): | |
1138 | data["value"] += " world" | |
1139 | return data | |
1140 | ||
1141 | load_json.return_value = {"value": "hello"} | |
1142 | argmap = {"value": fields.Str()} | |
1143 | p = Parser(schema_class=CustomSchema) | |
1144 | ret = p.parse(argmap, web_request) | |
1145 | assert ret == {"value": "hello world"} | |
1146 | ||
1147 | ||
1148 | @mock.patch("webargs.core.Parser.load_json") | |
1149 | def test_custom_default_schema_class(load_json, web_request): | |
1150 | class CustomSchema(Schema): | |
1151 | @pre_load | |
1152 | def pre_load(self, data, **kwargs): | |
1153 | data["value"] += " world" | |
1154 | return data | |
1155 | ||
1156 | class CustomParser(Parser): | |
1157 | DEFAULT_SCHEMA_CLASS = CustomSchema | |
1158 | ||
1159 | load_json.return_value = {"value": "hello"} | |
1160 | argmap = {"value": fields.Str()} | |
1161 | p = CustomParser() | |
1162 | ret = p.parse(argmap, web_request) | |
1163 | assert ret == {"value": "hello world"} |
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 | |
22 | 19 | |
23 | 20 | def test_parsing_in_class_based_view(self, testapp): |
24 | 21 | assert testapp.get("/echo_cbv?name=Fred").json == {"name": "Fred"} |
25 | assert testapp.post("/echo_cbv", {"name": "Fred"}).json == {"name": "Fred"} | |
22 | assert testapp.post_json("/echo_cbv", {"name": "Fred"}).json == {"name": "Fred"} | |
26 | 23 | |
27 | 24 | def test_use_args_in_class_based_view(self, testapp): |
28 | 25 | res = testapp.get("/echo_use_args_cbv?name=Fred") |
29 | 26 | assert res.json == {"name": "Fred"} |
30 | res = testapp.post("/echo_use_args_cbv", {"name": "Fred"}) | |
27 | res = testapp.post_json("/echo_use_args_cbv", {"name": "Fred"}) | |
31 | 28 | assert res.json == {"name": "Fred"} |
32 | 29 | |
33 | 30 | 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 | # https://github.com/marshmallow-code/webargs/issues/427 | |
19 | def test_parse_json_with_nonutf8_chars(self, testapp): | |
20 | res = testapp.post( | |
21 | "/echo_json", | |
22 | b"\xfe", | |
23 | headers={"Accept": "application/json", "Content-Type": "application/json"}, | |
24 | expect_errors=True, | |
25 | ) | |
26 | ||
27 | assert res.status_code == 400 | |
28 | assert res.json["errors"] == {"json": ["Invalid JSON body."]} | |
29 | ||
18 | 30 | # https://github.com/sloria/webargs/issues/329 |
19 | 31 | def test_invalid_json(self, testapp): |
20 | 32 | res = testapp.post( |
21 | "/echo", | |
33 | "/echo_json", | |
22 | 34 | '{"foo": "bar", }', |
23 | 35 | headers={"Accept": "application/json", "Content-Type": "application/json"}, |
24 | 36 | expect_errors=True, |
25 | 37 | ) |
26 | 38 | assert res.status_code == 400 |
27 | 39 | assert res.json["errors"] == {"json": ["Invalid JSON body."]} |
40 | ||
41 | # Falcon converts headers to all-caps | |
42 | def test_parsing_headers(self, testapp): | |
43 | res = testapp.get("/echo_headers", headers={"name": "Fred"}) | |
44 | assert res.json == {"NAME": "Fred"} | |
45 | ||
46 | # `falcon.testing.TestClient.simulate_request` parses request with `wsgiref` | |
47 | def test_body_parsing_works_with_simulate(self): | |
48 | app = self.create_app() | |
49 | client = falcon.testing.TestClient(app) | |
50 | res = client.simulate_post("/echo_json", json={"name": "Fred"},) | |
51 | assert res.json == {"name": "Fred"} |
0 | # -*- coding: utf-8 -*- | |
1 | from __future__ import unicode_literals | |
2 | import mock | |
3 | ||
4 | 0 | from werkzeug.exceptions import HTTPException |
5 | 1 | import pytest |
6 | 2 | |
7 | 3 | from flask import Flask |
8 | from webargs import fields, ValidationError, missing | |
4 | from webargs import fields, ValidationError, missing, dict2schema | |
9 | 5 | from webargs.flaskparser import parser, abort |
10 | 6 | from webargs.core import MARSHMALLOW_VERSION_INFO, json |
11 | 7 | |
12 | 8 | from .apps.flask_app import app |
13 | 9 | from webargs.testing import CommonTestCase |
10 | ||
11 | try: | |
12 | # Python 3.5 | |
13 | import mock | |
14 | except ImportError: | |
15 | # Python 3.6+ | |
16 | from unittest import mock | |
14 | 17 | |
15 | 18 | |
16 | 19 | class TestFlaskParser(CommonTestCase): |
24 | 27 | def test_parsing_invalid_view_arg(self, testapp): |
25 | 28 | res = testapp.get("/echo_view_arg/foo", expect_errors=True) |
26 | 29 | assert res.status_code == 422 |
27 | assert res.json == {"view_arg": ["Not a valid integer."]} | |
30 | assert res.json == {"view_args": {"view_arg": ["Not a valid integer."]}} | |
28 | 31 | |
29 | 32 | def test_use_args_with_view_args_parsing(self, testapp): |
30 | 33 | res = testapp.get("/echo_view_arg_use_args/42") |
31 | 34 | assert res.json == {"view_arg": 42} |
32 | 35 | |
33 | 36 | def test_use_args_on_a_method_view(self, testapp): |
34 | res = testapp.post("/echo_method_view_use_args", {"val": 42}) | |
37 | res = testapp.post_json("/echo_method_view_use_args", {"val": 42}) | |
35 | 38 | assert res.json == {"val": 42} |
36 | 39 | |
37 | 40 | def test_use_kwargs_on_a_method_view(self, testapp): |
38 | res = testapp.post("/echo_method_view_use_kwargs", {"val": 42}) | |
41 | res = testapp.post_json("/echo_method_view_use_kwargs", {"val": 42}) | |
39 | 42 | assert res.json == {"val": 42} |
40 | 43 | |
41 | 44 | def test_use_kwargs_with_missing_data(self, testapp): |
42 | res = testapp.post("/echo_use_kwargs_missing", {"username": "foo"}) | |
45 | res = testapp.post_json("/echo_use_kwargs_missing", {"username": "foo"}) | |
43 | 46 | assert res.json == {"username": "foo"} |
44 | 47 | |
45 | 48 | # regression test for https://github.com/marshmallow-code/webargs/issues/145 |
46 | 49 | 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 | |
50 | post_with_raw_fieldname_args = ( | |
51 | "/echo_nested_many_data_key", | |
52 | {"x_field": [{"id": 42}]}, | |
53 | ) | |
54 | # under marshmallow 2 this is allowed and works | |
49 | 55 | if MARSHMALLOW_VERSION_INFO[0] < 3: |
56 | res = testapp.post_json(*post_with_raw_fieldname_args) | |
50 | 57 | assert res.json == {"x_field": [{"id": 42}]} |
58 | # but under marshmallow3 , only data_key is checked, field name is ignored | |
59 | else: | |
60 | res = testapp.post_json(*post_with_raw_fieldname_args, expect_errors=True) | |
61 | assert res.status_code == 422 | |
51 | 62 | |
52 | 63 | res = testapp.post_json("/echo_nested_many_data_key", {"X-Field": [{"id": 24}]}) |
53 | 64 | assert res.json == {"x_field": [{"id": 24}]} |
71 | 82 | content_type="application/json", |
72 | 83 | ): |
73 | 84 | parser.parse(argmap) |
74 | mock_abort.assert_called | |
85 | mock_abort.assert_called() | |
75 | 86 | abort_args, abort_kwargs = mock_abort.call_args |
76 | 87 | assert abort_args[0] == 422 |
77 | 88 | expected_msg = "Invalid value." |
78 | assert abort_kwargs["messages"]["value"] == [expected_msg] | |
89 | assert abort_kwargs["messages"]["json"]["value"] == [expected_msg] | |
79 | 90 | assert type(abort_kwargs["exc"]) == ValidationError |
80 | 91 | |
81 | 92 | |
82 | def test_parse_form_returns_missing_if_no_form(): | |
93 | @pytest.mark.parametrize("mimetype", [None, "application/json"]) | |
94 | def test_load_json_returns_missing_if_no_data(mimetype): | |
83 | 95 | req = mock.Mock() |
84 | req.form.get.side_effect = AttributeError("no form") | |
85 | assert parser.parse_form(req, "foo", fields.Field()) is missing | |
96 | req.mimetype = mimetype | |
97 | req.get_data.return_value = "" | |
98 | schema = dict2schema({"foo": fields.Field()})() | |
99 | assert parser.load_json(req, schema) is missing | |
86 | 100 | |
87 | 101 | |
88 | 102 | def test_abort_with_message(): |
0 | # -*- coding: utf-8 -*- | |
1 | ||
2 | 0 | import asyncio |
3 | 1 | import webtest |
4 | 2 | import webtest_aiohttp |
37 | 35 | |
38 | 36 | # regression test for https://github.com/marshmallow-code/webargs/issues/165 |
39 | 37 | def test_multiple_args(self, testapp): |
40 | res = testapp.post_json( | |
41 | "/echo_multiple_args", {"first": "1", "last": "2", "_ignore": 0} | |
42 | ) | |
38 | res = testapp.post_json("/echo_multiple_args", {"first": "1", "last": "2"}) | |
43 | 39 | assert res.json == {"first": "1", "last": "2"} |
44 | 40 | |
45 | 41 | # regression test for https://github.com/marshmallow-code/webargs/issues/145 |
46 | 42 | def test_nested_many_with_data_key(self, testapp): |
47 | res = testapp.post_json("/echo_nested_many_data_key", {"x_field": [{"id": 42}]}) | |
48 | 43 | # https://github.com/marshmallow-code/marshmallow/pull/714 |
44 | # on marshmallow 2, the field name can also be used | |
49 | 45 | if MARSHMALLOW_VERSION_INFO[0] < 3: |
46 | res = testapp.post_json( | |
47 | "/echo_nested_many_data_key", {"x_field": [{"id": 42}]} | |
48 | ) | |
50 | 49 | assert res.json == {"x_field": [{"id": 42}]} |
51 | 50 | |
52 | 51 | res = testapp.post_json("/echo_nested_many_data_key", {"X-Field": [{"id": 24}]}) |
10 | 10 | |
11 | 11 | |
12 | 12 | async def echo_parse(request): |
13 | parsed = await parser.parse(hello_args, request) | |
13 | parsed = await parser.parse(hello_args, request, location="query") | |
14 | 14 | return json_response(parsed) |
15 | 15 | |
16 | 16 | |
17 | @use_args(hello_args) | |
17 | @use_args(hello_args, location="query") | |
18 | 18 | async def echo_use_args(request, args): |
19 | 19 | return json_response(args) |
20 | 20 | |
21 | 21 | |
22 | @use_kwargs(hello_args) | |
22 | @use_kwargs(hello_args, location="query") | |
23 | 23 | async def echo_use_kwargs(request, name): |
24 | 24 | return json_response({"name": name}) |
25 | 25 |
0 | # -*- coding: utf-8 -*- | |
1 | ||
2 | from webargs.core import json | |
0 | import marshmallow as ma | |
1 | import pytest | |
2 | import tornado.concurrent | |
3 | import tornado.http1connection | |
4 | import tornado.httpserver | |
5 | import tornado.httputil | |
6 | import tornado.ioloop | |
7 | import tornado.web | |
8 | from tornado.testing import AsyncHTTPTestCase | |
9 | from webargs import fields, missing | |
10 | from webargs.core import MARSHMALLOW_VERSION_INFO, json, parse_json | |
11 | from webargs.tornadoparser import ( | |
12 | WebArgsTornadoMultiDictProxy, | |
13 | parser, | |
14 | use_args, | |
15 | use_kwargs, | |
16 | ) | |
17 | ||
18 | from urllib.parse import urlencode | |
3 | 19 | |
4 | 20 | try: |
5 | from urllib.parse import urlencode | |
6 | except ImportError: # PY2 | |
7 | from urllib import urlencode # type: ignore | |
8 | ||
9 | import mock | |
10 | import pytest | |
11 | ||
12 | import marshmallow as ma | |
13 | ||
14 | import tornado.web | |
15 | import tornado.httputil | |
16 | import tornado.httpserver | |
17 | import tornado.http1connection | |
18 | import tornado.concurrent | |
19 | import tornado.ioloop | |
20 | from tornado.testing import AsyncHTTPTestCase | |
21 | ||
22 | from webargs import fields, missing | |
23 | from webargs.tornadoparser import parser, use_args, use_kwargs, get_value | |
24 | from webargs.core import parse_json | |
21 | # Python 3.5 | |
22 | import mock | |
23 | except ImportError: | |
24 | # Python 3.6+ | |
25 | from unittest import mock | |
26 | ||
25 | 27 | |
26 | 28 | name = "name" |
27 | 29 | value = "value" |
28 | 30 | |
29 | 31 | |
30 | def test_get_value_basic(): | |
31 | field, multifield = fields.Field(), fields.List(fields.Str()) | |
32 | assert get_value({"foo": 42}, "foo", field) == 42 | |
33 | assert get_value({"foo": 42}, "bar", field) is missing | |
34 | assert get_value({"foos": ["a", "b"]}, "foos", multifield) == ["a", "b"] | |
35 | # https://github.com/marshmallow-code/webargs/pull/30 | |
36 | assert get_value({"foos": ["a", "b"]}, "bar", multifield) is missing | |
37 | ||
38 | ||
39 | class TestQueryArgs(object): | |
40 | def setup_method(self, method): | |
41 | parser.clear_cache() | |
42 | ||
32 | class AuthorSchema(ma.Schema): | |
33 | name = fields.Str(missing="World", validate=lambda n: len(n) >= 3) | |
34 | works = fields.List(fields.Str()) | |
35 | ||
36 | ||
37 | strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {} | |
38 | author_schema = AuthorSchema(**strict_kwargs) | |
39 | ||
40 | ||
41 | def test_tornado_multidictproxy(): | |
42 | for dictval, fieldname, expected in ( | |
43 | ({"name": "Sophocles"}, "name", "Sophocles"), | |
44 | ({"name": "Sophocles"}, "works", missing), | |
45 | ({"works": ["Antigone", "Oedipus Rex"]}, "works", ["Antigone", "Oedipus Rex"]), | |
46 | ({"works": ["Antigone", "Oedipus at Colonus"]}, "name", missing), | |
47 | ): | |
48 | proxy = WebArgsTornadoMultiDictProxy(dictval, author_schema) | |
49 | assert proxy.get(fieldname) == expected | |
50 | ||
51 | ||
52 | class TestQueryArgs: | |
43 | 53 | def test_it_should_get_single_values(self): |
44 | query = [(name, value)] | |
45 | field = fields.Field() | |
54 | query = [("name", "Aeschylus")] | |
46 | 55 | request = make_get_request(query) |
47 | ||
48 | result = parser.parse_querystring(request, name, field) | |
49 | ||
50 | assert result == value | |
56 | result = parser.load_querystring(request, author_schema) | |
57 | assert result["name"] == "Aeschylus" | |
51 | 58 | |
52 | 59 | def test_it_should_get_multiple_values(self): |
53 | query = [(name, value), (name, value)] | |
54 | field = fields.List(fields.Field()) | |
60 | query = [("works", "Agamemnon"), ("works", "Nereids")] | |
55 | 61 | request = make_get_request(query) |
56 | ||
57 | result = parser.parse_querystring(request, name, field) | |
58 | ||
59 | assert result == [value, value] | |
62 | result = parser.load_querystring(request, author_schema) | |
63 | assert result["works"] == ["Agamemnon", "Nereids"] | |
60 | 64 | |
61 | 65 | def test_it_should_return_missing_if_not_present(self): |
62 | 66 | query = [] |
63 | field = fields.Field() | |
64 | field2 = fields.List(fields.Int()) | |
65 | 67 | request = make_get_request(query) |
66 | ||
67 | result = parser.parse_querystring(request, name, field) | |
68 | result2 = parser.parse_querystring(request, name, field2) | |
69 | ||
70 | assert result is missing | |
71 | assert result2 is missing | |
72 | ||
73 | def test_it_should_return_empty_list_if_multiple_and_not_present(self): | |
74 | query = [] | |
75 | field = fields.List(fields.Field()) | |
76 | request = make_get_request(query) | |
77 | ||
78 | result = parser.parse_querystring(request, name, field) | |
79 | ||
80 | assert result is missing | |
68 | result = parser.load_querystring(request, author_schema) | |
69 | assert result["name"] is missing | |
70 | assert result["works"] is missing | |
81 | 71 | |
82 | 72 | |
83 | 73 | class TestFormArgs: |
84 | def setup_method(self, method): | |
85 | parser.clear_cache() | |
86 | ||
87 | 74 | def test_it_should_get_single_values(self): |
88 | query = [(name, value)] | |
89 | field = fields.Field() | |
75 | query = [("name", "Aristophanes")] | |
90 | 76 | request = make_form_request(query) |
91 | ||
92 | result = parser.parse_form(request, name, field) | |
93 | ||
94 | assert result == value | |
77 | result = parser.load_form(request, author_schema) | |
78 | assert result["name"] == "Aristophanes" | |
95 | 79 | |
96 | 80 | def test_it_should_get_multiple_values(self): |
97 | query = [(name, value), (name, value)] | |
98 | field = fields.List(fields.Field()) | |
81 | query = [("works", "The Wasps"), ("works", "The Frogs")] | |
99 | 82 | request = make_form_request(query) |
100 | ||
101 | result = parser.parse_form(request, name, field) | |
102 | ||
103 | assert result == [value, value] | |
83 | result = parser.load_form(request, author_schema) | |
84 | assert result["works"] == ["The Wasps", "The Frogs"] | |
104 | 85 | |
105 | 86 | def test_it_should_return_missing_if_not_present(self): |
106 | 87 | query = [] |
107 | field = fields.Field() | |
108 | 88 | request = make_form_request(query) |
109 | ||
110 | result = parser.parse_form(request, name, field) | |
111 | ||
112 | assert result is missing | |
113 | ||
114 | def test_it_should_return_empty_list_if_multiple_and_not_present(self): | |
115 | query = [] | |
116 | field = fields.List(fields.Field()) | |
117 | request = make_form_request(query) | |
118 | ||
119 | result = parser.parse_form(request, name, field) | |
120 | ||
121 | assert result is missing | |
122 | ||
123 | ||
124 | class TestJSONArgs(object): | |
125 | def setup_method(self, method): | |
126 | parser.clear_cache() | |
127 | ||
89 | result = parser.load_form(request, author_schema) | |
90 | assert result["name"] is missing | |
91 | assert result["works"] is missing | |
92 | ||
93 | ||
94 | class TestJSONArgs: | |
128 | 95 | def test_it_should_get_single_values(self): |
129 | query = {name: value} | |
130 | field = fields.Field() | |
96 | query = {"name": "Euripides"} | |
131 | 97 | request = make_json_request(query) |
132 | result = parser.parse_json(request, name, field) | |
133 | ||
134 | assert result == value | |
98 | result = parser.load_json(request, author_schema) | |
99 | assert result["name"] == "Euripides" | |
135 | 100 | |
136 | 101 | def test_parsing_request_with_vendor_content_type(self): |
137 | query = {name: value} | |
138 | field = fields.Field() | |
102 | query = {"name": "Euripides"} | |
139 | 103 | request = make_json_request( |
140 | 104 | query, content_type="application/vnd.api+json; charset=UTF-8" |
141 | 105 | ) |
142 | result = parser.parse_json(request, name, field) | |
143 | ||
144 | assert result == value | |
106 | result = parser.load_json(request, author_schema) | |
107 | assert result["name"] == "Euripides" | |
145 | 108 | |
146 | 109 | def test_it_should_get_multiple_values(self): |
147 | query = {name: [value, value]} | |
148 | field = fields.List(fields.Field()) | |
110 | query = {"works": ["Medea", "Electra"]} | |
149 | 111 | request = make_json_request(query) |
150 | result = parser.parse_json(request, name, field) | |
151 | ||
152 | assert result == [value, value] | |
112 | result = parser.load_json(request, author_schema) | |
113 | assert result["works"] == ["Medea", "Electra"] | |
153 | 114 | |
154 | 115 | def test_it_should_get_multiple_nested_values(self): |
155 | query = {name: [{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}]} | |
156 | field = fields.List( | |
157 | fields.Nested({"id": fields.Field(), "name": fields.Field()}) | |
158 | ) | |
116 | class CustomSchema(ma.Schema): | |
117 | works = fields.List( | |
118 | fields.Nested({"author": fields.Str(), "workname": fields.Str()}) | |
119 | ) | |
120 | ||
121 | custom_schema = CustomSchema(**strict_kwargs) | |
122 | ||
123 | query = { | |
124 | "works": [ | |
125 | {"author": "Euripides", "workname": "Hecuba"}, | |
126 | {"author": "Aristophanes", "workname": "The Birds"}, | |
127 | ] | |
128 | } | |
159 | 129 | request = make_json_request(query) |
160 | result = parser.parse_json(request, name, field) | |
161 | assert result == [{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}] | |
130 | result = parser.load_json(request, custom_schema) | |
131 | assert result["works"] == [ | |
132 | {"author": "Euripides", "workname": "Hecuba"}, | |
133 | {"author": "Aristophanes", "workname": "The Birds"}, | |
134 | ] | |
135 | ||
136 | def test_it_should_not_include_fieldnames_if_not_present(self): | |
137 | query = {} | |
138 | request = make_json_request(query) | |
139 | result = parser.load_json(request, author_schema) | |
140 | assert result == {} | |
141 | ||
142 | def test_it_should_handle_type_error_on_load_json(self): | |
143 | # but this is different from the test above where the payload was valid | |
144 | # and empty -- missing vs {} | |
145 | request = make_request( | |
146 | body=tornado.concurrent.Future(), | |
147 | headers={"Content-Type": "application/json"}, | |
148 | ) | |
149 | result = parser.load_json(request, author_schema) | |
150 | assert result is missing | |
151 | ||
152 | def test_it_should_handle_value_error_on_parse_json(self): | |
153 | request = make_request("this is json not") | |
154 | result = parser.load_json(request, author_schema) | |
155 | assert result is missing | |
156 | ||
157 | ||
158 | class TestHeadersArgs: | |
159 | def test_it_should_get_single_values(self): | |
160 | query = {"name": "Euphorion"} | |
161 | request = make_request(headers=query) | |
162 | result = parser.load_headers(request, author_schema) | |
163 | assert result["name"] == "Euphorion" | |
164 | ||
165 | def test_it_should_get_multiple_values(self): | |
166 | query = {"works": ["Prometheus Bound", "Prometheus Unbound"]} | |
167 | request = make_request(headers=query) | |
168 | result = parser.load_headers(request, author_schema) | |
169 | assert result["works"] == ["Prometheus Bound", "Prometheus Unbound"] | |
162 | 170 | |
163 | 171 | def test_it_should_return_missing_if_not_present(self): |
164 | query = {} | |
165 | field = fields.Field() | |
166 | request = make_json_request(query) | |
167 | result = parser.parse_json(request, name, field) | |
168 | ||
169 | assert result is missing | |
170 | ||
171 | def test_it_should_return_empty_list_if_multiple_and_not_present(self): | |
172 | query = {} | |
173 | field = fields.List(fields.Field()) | |
174 | request = make_json_request(query) | |
175 | result = parser.parse_json(request, name, field) | |
176 | ||
177 | assert result is missing | |
178 | ||
179 | def test_it_should_handle_type_error_on_parse_json(self): | |
180 | field = fields.Field() | |
181 | request = make_request( | |
182 | body=tornado.concurrent.Future, headers={"Content-Type": "application/json"} | |
183 | ) | |
184 | result = parser.parse_json(request, name, field) | |
185 | assert parser._cache["json"] == {} | |
186 | assert result is missing | |
187 | ||
188 | def test_it_should_handle_value_error_on_parse_json(self): | |
189 | field = fields.Field() | |
190 | request = make_request("this is json not") | |
191 | result = parser.parse_json(request, name, field) | |
192 | assert parser._cache["json"] == {} | |
193 | assert result is missing | |
194 | ||
195 | ||
196 | class TestHeadersArgs(object): | |
197 | def setup_method(self, method): | |
198 | parser.clear_cache() | |
199 | ||
172 | request = make_request() | |
173 | result = parser.load_headers(request, author_schema) | |
174 | assert result["name"] is missing | |
175 | assert result["works"] is missing | |
176 | ||
177 | ||
178 | class TestFilesArgs: | |
200 | 179 | def test_it_should_get_single_values(self): |
201 | query = {name: value} | |
202 | field = fields.Field() | |
203 | request = make_request(headers=query) | |
204 | ||
205 | result = parser.parse_headers(request, name, field) | |
206 | ||
207 | assert result == value | |
180 | query = [("name", "Sappho")] | |
181 | request = make_files_request(query) | |
182 | result = parser.load_files(request, author_schema) | |
183 | assert result["name"] == "Sappho" | |
208 | 184 | |
209 | 185 | def test_it_should_get_multiple_values(self): |
210 | query = {name: [value, value]} | |
211 | field = fields.List(fields.Field()) | |
212 | request = make_request(headers=query) | |
213 | ||
214 | result = parser.parse_headers(request, name, field) | |
215 | ||
216 | assert result == [value, value] | |
217 | ||
218 | def test_it_should_return_missing_if_not_present(self): | |
219 | field = fields.Field(multiple=False) | |
220 | request = make_request() | |
221 | ||
222 | result = parser.parse_headers(request, name, field) | |
223 | ||
224 | assert result is missing | |
225 | ||
226 | def test_it_should_return_empty_list_if_multiple_and_not_present(self): | |
227 | query = {} | |
228 | field = fields.List(fields.Field()) | |
229 | request = make_request(headers=query) | |
230 | ||
231 | result = parser.parse_headers(request, name, field) | |
232 | ||
233 | assert result is missing | |
234 | ||
235 | ||
236 | class TestFilesArgs(object): | |
237 | def setup_method(self, method): | |
238 | parser.clear_cache() | |
239 | ||
240 | def test_it_should_get_single_values(self): | |
241 | query = [(name, value)] | |
242 | field = fields.Field() | |
186 | query = [("works", "Sappho 31"), ("works", "Ode to Aphrodite")] | |
243 | 187 | request = make_files_request(query) |
244 | ||
245 | result = parser.parse_files(request, name, field) | |
246 | ||
247 | assert result == value | |
248 | ||
249 | def test_it_should_get_multiple_values(self): | |
250 | query = [(name, value), (name, value)] | |
251 | field = fields.List(fields.Field()) | |
252 | request = make_files_request(query) | |
253 | ||
254 | result = parser.parse_files(request, name, field) | |
255 | ||
256 | assert result == [value, value] | |
188 | result = parser.load_files(request, author_schema) | |
189 | assert result["works"] == ["Sappho 31", "Ode to Aphrodite"] | |
257 | 190 | |
258 | 191 | def test_it_should_return_missing_if_not_present(self): |
259 | 192 | query = [] |
260 | field = fields.Field() | |
261 | 193 | request = make_files_request(query) |
262 | ||
263 | result = parser.parse_files(request, name, field) | |
264 | ||
265 | assert result is missing | |
266 | ||
267 | def test_it_should_return_empty_list_if_multiple_and_not_present(self): | |
268 | query = [] | |
269 | field = fields.List(fields.Field()) | |
270 | request = make_files_request(query) | |
271 | ||
272 | result = parser.parse_files(request, name, field) | |
273 | ||
274 | assert result is missing | |
275 | ||
276 | ||
277 | class TestErrorHandler(object): | |
194 | result = parser.load_files(request, author_schema) | |
195 | assert result["name"] is missing | |
196 | assert result["works"] is missing | |
197 | ||
198 | ||
199 | class TestErrorHandler: | |
278 | 200 | def test_it_should_raise_httperror_on_failed_validation(self): |
279 | 201 | args = {"foo": fields.Field(validate=lambda x: False)} |
280 | 202 | with pytest.raises(tornado.web.HTTPError): |
281 | 203 | parser.parse(args, make_json_request({"foo": 42})) |
282 | 204 | |
283 | 205 | |
284 | class TestParse(object): | |
285 | def setup_method(self, method): | |
286 | parser.clear_cache() | |
287 | ||
206 | class TestParse: | |
288 | 207 | def test_it_should_parse_query_arguments(self): |
289 | 208 | attrs = {"string": fields.Field(), "integer": fields.List(fields.Int())} |
290 | 209 | |
292 | 211 | [("string", "value"), ("integer", "1"), ("integer", "2")] |
293 | 212 | ) |
294 | 213 | |
295 | parsed = parser.parse(attrs, request) | |
214 | parsed = parser.parse(attrs, request, location="query") | |
296 | 215 | |
297 | 216 | assert parsed["integer"] == [1, 2] |
298 | 217 | assert parsed["string"] == value |
299 | 218 | |
300 | def test_parsing_clears_cache(self): | |
301 | request = make_json_request({"string": "value", "integer": [1, 2]}) | |
302 | string_result = parser.parse_json(request, "string", fields.Str()) | |
303 | assert string_result == "value" | |
304 | assert "json" in parser._cache | |
305 | assert "string" in parser._cache["json"] | |
306 | assert "integer" in parser._cache["json"] | |
307 | attrs = {"string": fields.Str(), "integer": fields.List(fields.Int())} | |
308 | parser.parse(attrs, request) | |
309 | assert parser._cache == {} | |
310 | ||
311 | 219 | def test_it_should_parse_form_arguments(self): |
312 | 220 | attrs = {"string": fields.Field(), "integer": fields.List(fields.Int())} |
313 | 221 | |
315 | 223 | [("string", "value"), ("integer", "1"), ("integer", "2")] |
316 | 224 | ) |
317 | 225 | |
318 | parsed = parser.parse(attrs, request) | |
226 | parsed = parser.parse(attrs, request, location="form") | |
319 | 227 | |
320 | 228 | assert parsed["integer"] == [1, 2] |
321 | 229 | assert parsed["string"] == value |
347 | 255 | |
348 | 256 | request = make_request(headers={"string": "value", "integer": ["1", "2"]}) |
349 | 257 | |
350 | parsed = parser.parse(attrs, request, locations=["headers"]) | |
258 | parsed = parser.parse(attrs, request, location="headers") | |
351 | 259 | |
352 | 260 | assert parsed["string"] == value |
353 | 261 | assert parsed["integer"] == [1, 2] |
359 | 267 | [("string", "value"), ("integer", "1"), ("integer", "2")] |
360 | 268 | ) |
361 | 269 | |
362 | parsed = parser.parse(attrs, request, locations=["cookies"]) | |
270 | parsed = parser.parse(attrs, request, location="cookies") | |
363 | 271 | |
364 | 272 | assert parsed["string"] == value |
365 | 273 | assert parsed["integer"] == [2] |
371 | 279 | [("string", "value"), ("integer", "1"), ("integer", "2")] |
372 | 280 | ) |
373 | 281 | |
374 | parsed = parser.parse(attrs, request, locations=["files"]) | |
282 | parsed = parser.parse(attrs, request, location="files") | |
375 | 283 | |
376 | 284 | assert parsed["string"] == value |
377 | 285 | assert parsed["integer"] == [1, 2] |
381 | 289 | |
382 | 290 | request = make_json_request({}) |
383 | 291 | |
384 | with pytest.raises(tornado.web.HTTPError) as excinfo: | |
292 | msg = "Missing data for required field." | |
293 | with pytest.raises(tornado.web.HTTPError, match=msg): | |
385 | 294 | parser.parse(args, request) |
386 | assert "Missing data for required field." in str(excinfo) | |
387 | 295 | |
388 | 296 | def test_it_should_parse_multiple_arg_required(self): |
389 | 297 | args = {"foo": fields.List(fields.Int(), required=True)} |
390 | 298 | request = make_json_request({}) |
391 | with pytest.raises(tornado.web.HTTPError) as excinfo: | |
299 | msg = "Missing data for required field." | |
300 | with pytest.raises(tornado.web.HTTPError, match=msg): | |
392 | 301 | parser.parse(args, request) |
393 | assert "Missing data for required field." in str(excinfo) | |
394 | ||
395 | ||
396 | class TestUseArgs(object): | |
397 | def setup_method(self, method): | |
398 | parser.clear_cache() | |
399 | ||
302 | ||
303 | ||
304 | class TestUseArgs: | |
400 | 305 | def test_it_should_pass_parsed_as_first_argument(self): |
401 | class Handler(object): | |
306 | class Handler: | |
402 | 307 | request = make_json_request({"key": "value"}) |
403 | 308 | |
404 | 309 | @use_args({"key": fields.Field()}) |
413 | 318 | assert result is True |
414 | 319 | |
415 | 320 | def test_it_should_pass_parsed_as_kwargs_arguments(self): |
416 | class Handler(object): | |
321 | class Handler: | |
417 | 322 | request = make_json_request({"key": "value"}) |
418 | 323 | |
419 | 324 | @use_kwargs({"key": fields.Field()}) |
428 | 333 | assert result is True |
429 | 334 | |
430 | 335 | def test_it_should_be_validate_arguments_when_validator_is_passed(self): |
431 | class Handler(object): | |
336 | class Handler: | |
432 | 337 | request = make_json_request({"foo": 41}) |
433 | 338 | |
434 | 339 | @use_kwargs({"foo": fields.Int()}, validate=lambda args: args["foo"] > 42) |
486 | 391 | |
487 | 392 | |
488 | 393 | def make_request(uri=None, body=None, headers=None, files=None): |
489 | uri = uri if uri is not None else u"" | |
490 | body = body if body is not None else u"" | |
394 | uri = uri if uri is not None else "" | |
395 | body = body if body is not None else "" | |
491 | 396 | method = "POST" if body else "GET" |
492 | 397 | # Need to make a mock connection right now because Tornado 4.0 requires a |
493 | 398 | # remote_ip in the context attribute. 4.1 addresses this, and this |
496 | 401 | mock_connection = mock.Mock(spec=tornado.http1connection.HTTP1Connection) |
497 | 402 | mock_connection.context = mock.Mock() |
498 | 403 | mock_connection.remote_ip = None |
499 | content_type = headers.get("Content-Type", u"") if headers else u"" | |
404 | content_type = headers.get("Content-Type", "") if headers else "" | |
500 | 405 | request = tornado.httputil.HTTPServerRequest( |
501 | 406 | method=method, |
502 | 407 | uri=uri, |
519 | 424 | class EchoHandler(tornado.web.RequestHandler): |
520 | 425 | ARGS = {"name": fields.Str()} |
521 | 426 | |
522 | @use_args(ARGS) | |
427 | @use_args(ARGS, location="query") | |
523 | 428 | def get(self, args): |
524 | 429 | self.write(args) |
430 | ||
431 | ||
432 | class EchoFormHandler(tornado.web.RequestHandler): | |
433 | ARGS = {"name": fields.Str()} | |
434 | ||
435 | @use_args(ARGS, location="form") | |
436 | def post(self, args): | |
437 | self.write(args) | |
438 | ||
439 | ||
440 | class EchoJSONHandler(tornado.web.RequestHandler): | |
441 | ARGS = {"name": fields.Str()} | |
525 | 442 | |
526 | 443 | @use_args(ARGS) |
527 | 444 | def post(self, args): |
531 | 448 | class EchoWithParamHandler(tornado.web.RequestHandler): |
532 | 449 | ARGS = {"name": fields.Str()} |
533 | 450 | |
534 | @use_args(ARGS) | |
451 | @use_args(ARGS, location="query") | |
535 | 452 | def get(self, id, args): |
536 | 453 | self.write(args) |
537 | 454 | |
538 | 455 | |
539 | 456 | echo_app = tornado.web.Application( |
540 | [(r"/echo", EchoHandler), (r"/echo_with_param/(\d+)", EchoWithParamHandler)] | |
457 | [ | |
458 | (r"/echo", EchoHandler), | |
459 | (r"/echo_form", EchoFormHandler), | |
460 | (r"/echo_json", EchoJSONHandler), | |
461 | (r"/echo_with_param/(\d+)", EchoWithParamHandler), | |
462 | ] | |
541 | 463 | ) |
542 | 464 | |
543 | 465 | |
547 | 469 | |
548 | 470 | def test_post(self): |
549 | 471 | res = self.fetch( |
550 | "/echo", | |
472 | "/echo_json", | |
551 | 473 | method="POST", |
552 | 474 | headers={"Content-Type": "application/json"}, |
553 | 475 | body=json.dumps({"name": "Steve"}), |
555 | 477 | json_body = parse_json(res.body) |
556 | 478 | assert json_body["name"] == "Steve" |
557 | 479 | res = self.fetch( |
558 | "/echo", | |
480 | "/echo_json", | |
559 | 481 | method="POST", |
560 | 482 | headers={"Content-Type": "application/json"}, |
561 | 483 | body=json.dumps({}), |
587 | 509 | def post(self, args): |
588 | 510 | self.write(args) |
589 | 511 | |
590 | @use_kwargs(ARGS) | |
512 | @use_kwargs(ARGS, location="query") | |
591 | 513 | def get(self, name): |
592 | 514 | self.write({"status": "success"}) |
593 | 515 |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """Tests for the webapp2 parser""" |
2 | try: | |
3 | from urllib.parse import urlencode | |
4 | except ImportError: # PY2 | |
5 | from urllib import urlencode # type: ignore | |
1 | from urllib.parse import urlencode | |
6 | 2 | from webargs.core import json |
7 | 3 | |
8 | 4 | import pytest |
5 | import marshmallow as ma | |
9 | 6 | from marshmallow import fields, ValidationError |
10 | 7 | |
11 | 8 | import webtest |
12 | 9 | import webapp2 |
13 | 10 | from webargs.webapp2parser import parser |
11 | from webargs.core import MARSHMALLOW_VERSION_INFO | |
14 | 12 | |
15 | 13 | hello_args = {"name": fields.Str(missing="World")} |
16 | 14 | |
24 | 22 | } |
25 | 23 | |
26 | 24 | |
25 | class HelloSchema(ma.Schema): | |
26 | name = fields.Str(missing="World", validate=lambda n: len(n) >= 3) | |
27 | ||
28 | ||
29 | # variant which ignores unknown fields | |
30 | exclude_kwargs = ( | |
31 | {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE} | |
32 | ) | |
33 | hello_exclude_schema = HelloSchema(**exclude_kwargs) | |
34 | ||
35 | ||
27 | 36 | def test_parse_querystring_args(): |
28 | 37 | request = webapp2.Request.blank("/echo?name=Fred") |
29 | assert parser.parse(hello_args, req=request) == {"name": "Fred"} | |
38 | assert parser.parse(hello_args, req=request, location="query") == {"name": "Fred"} | |
30 | 39 | |
31 | 40 | |
32 | 41 | def test_parse_querystring_multiple(): |
33 | 42 | expected = {"name": ["steve", "Loria"]} |
34 | 43 | request = webapp2.Request.blank("/echomulti?name=steve&name=Loria") |
35 | assert parser.parse(hello_multiple, req=request) == expected | |
44 | assert parser.parse(hello_multiple, req=request, location="query") == expected | |
36 | 45 | |
37 | 46 | |
38 | 47 | def test_parse_form(): |
39 | 48 | expected = {"name": "Joe"} |
40 | 49 | request = webapp2.Request.blank("/echo", POST=expected) |
41 | assert parser.parse(hello_args, req=request) == expected | |
50 | assert parser.parse(hello_args, req=request, location="form") == expected | |
42 | 51 | |
43 | 52 | |
44 | 53 | def test_parse_form_multiple(): |
45 | 54 | expected = {"name": ["steve", "Loria"]} |
46 | 55 | request = webapp2.Request.blank("/echo", POST=urlencode(expected, doseq=True)) |
47 | assert parser.parse(hello_multiple, req=request) == expected | |
56 | assert parser.parse(hello_multiple, req=request, location="form") == expected | |
48 | 57 | |
49 | 58 | |
50 | 59 | def test_parsing_form_default(): |
51 | 60 | request = webapp2.Request.blank("/echo", POST="") |
52 | assert parser.parse(hello_args, req=request) == {"name": "World"} | |
61 | assert parser.parse(hello_args, req=request, location="form") == {"name": "World"} | |
53 | 62 | |
54 | 63 | |
55 | 64 | def test_parse_json(): |
58 | 67 | "/echo", POST=json.dumps(expected), headers={"content-type": "application/json"} |
59 | 68 | ) |
60 | 69 | assert parser.parse(hello_args, req=request) == expected |
70 | ||
71 | ||
72 | def test_parse_json_content_type_mismatch(): | |
73 | request = webapp2.Request.blank( | |
74 | "/echo_json", | |
75 | POST=json.dumps({"name": "foo"}), | |
76 | headers={"content-type": "application/x-www-form-urlencoded"}, | |
77 | ) | |
78 | assert parser.parse(hello_args, req=request) == {"name": "World"} | |
61 | 79 | |
62 | 80 | |
63 | 81 | def test_parse_invalid_json(): |
94 | 112 | request = webapp2.Request.blank( |
95 | 113 | "/", headers={"Cookie": response.headers["Set-Cookie"]} |
96 | 114 | ) |
97 | assert parser.parse(hello_args, req=request, locations=("cookies",)) == expected | |
115 | assert parser.parse(hello_args, req=request, location="cookies") == expected | |
98 | 116 | |
99 | 117 | |
100 | 118 | def test_parsing_headers(): |
101 | 119 | expected = {"name": "Fred"} |
102 | 120 | request = webapp2.Request.blank("/", headers=expected) |
103 | assert parser.parse(hello_args, req=request, locations=("headers",)) == expected | |
121 | assert ( | |
122 | parser.parse(hello_exclude_schema, req=request, location="headers") == expected | |
123 | ) | |
104 | 124 | |
105 | 125 | |
106 | 126 | def test_parse_files(): |
109 | 129 | """ |
110 | 130 | |
111 | 131 | class Handler(webapp2.RequestHandler): |
112 | @parser.use_args({"myfile": fields.List(fields.Field())}, locations=("files",)) | |
132 | @parser.use_args({"myfile": fields.List(fields.Field())}, location="files") | |
113 | 133 | def post(self, args): |
114 | 134 | self.response.content_type = "application/json" |
115 | 135 | |
116 | 136 | def _value(f): |
117 | 137 | return f.getvalue().decode("utf-8") |
118 | 138 | |
119 | data = dict((i.filename, _value(i.file)) for i in args["myfile"]) | |
139 | data = {i.filename: _value(i.file) for i in args["myfile"]} | |
120 | 140 | self.response.write(json.dumps(data)) |
121 | 141 | |
122 | 142 | app = webapp2.WSGIApplication([("/", Handler)]) |
129 | 149 | def test_exception_on_validation_error(): |
130 | 150 | request = webapp2.Request.blank("/", POST={"num": "3"}) |
131 | 151 | with pytest.raises(ValidationError): |
132 | parser.parse(hello_validate, req=request) | |
152 | parser.parse(hello_validate, req=request, location="form") | |
133 | 153 | |
134 | 154 | |
135 | 155 | def test_validation_error_with_message(): |
136 | 156 | request = webapp2.Request.blank("/", POST={"num": "3"}) |
137 | 157 | with pytest.raises(ValidationError) as exc: |
138 | parser.parse(hello_validate, req=request) | |
158 | parser.parse(hello_validate, req=request, location="form") | |
139 | 159 | assert "Houston, we've had a problem." in exc.value |
140 | 160 | |
141 | 161 | |
147 | 167 | request = webapp2.Request.blank("/echo", POST=expected) |
148 | 168 | app = webapp2.WSGIApplication([]) |
149 | 169 | app.set_globals(app, request) |
150 | assert parser.parse(hello_args) == expected | |
170 | assert parser.parse(hello_args, location="form") == expected |
0 | 0 | [tox] |
1 | 1 | envlist= |
2 | 2 | lint |
3 | py{27,35,36,37}-marshmallow{2,3} | |
3 | py{35,36,37,38}-marshmallow2 | |
4 | py{35,36,37,38}-marshmallow3 | |
5 | py38-marshmallowdev | |
4 | 6 | docs |
5 | 7 | |
6 | 8 | [testenv] |
8 | 10 | deps = |
9 | 11 | marshmallow2: marshmallow==2.15.2 |
10 | 12 | marshmallow3: marshmallow>=3.0.0rc2,<4.0.0 |
11 | commands = | |
12 | py27: pytest --ignore=tests/test_py3/ {posargs} | |
13 | py{35,36,37}: pytest {posargs} | |
13 | marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz | |
14 | commands = pytest {posargs} | |
14 | 15 | |
15 | 16 | [testenv:lint] |
16 | deps = pre-commit~=1.14 | |
17 | deps = pre-commit~=1.20 | |
17 | 18 | skip_install = true |
18 | 19 | commands = pre-commit run --all-files |
19 | 20 | |
20 | 21 | [testenv:docs] |
21 | deps = -rdocs/requirements.txt | |
22 | extras = | |
22 | extras = docs | |
23 | 23 | commands = sphinx-build docs/ docs/_build {posargs} |
24 | 24 | |
25 | 25 | ; Below tasks are for development only (not run in CI) |
26 | 26 | |
27 | 27 | [testenv:watch-docs] |
28 | 28 | deps = |
29 | -rdocs/requirements.txt | |
30 | 29 | sphinx-autobuild |
31 | extras = | |
32 | commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} -z webargs | |
30 | extras = docs | |
31 | commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} -z src/webargs -s 2 | |
33 | 32 | |
34 | 33 | [testenv:watch-readme] |
35 | 34 | deps = restview |
0 | # -*- coding: utf-8 -*- | |
1 | from marshmallow.utils import missing | |
2 | ||
3 | # Make marshmallow's validation functions importable from webargs | |
4 | from marshmallow import validate | |
5 | ||
6 | from webargs.core import dict2schema, ValidationError | |
7 | from webargs import fields | |
8 | ||
9 | __version__ = "5.1.2" | |
10 | __author__ = "Steven Loria" | |
11 | __license__ = "MIT" | |
12 | ||
13 | ||
14 | __all__ = ("dict2schema", "ValidationError", "fields", "missing", "validate") |
0 | """aiohttp request argument parsing module. | |
1 | ||
2 | Example: :: | |
3 | ||
4 | import asyncio | |
5 | from aiohttp import web | |
6 | ||
7 | from webargs import fields | |
8 | from webargs.aiohttpparser import use_args | |
9 | ||
10 | ||
11 | hello_args = { | |
12 | 'name': fields.Str(required=True) | |
13 | } | |
14 | @asyncio.coroutine | |
15 | @use_args(hello_args) | |
16 | def index(request, args): | |
17 | return web.Response( | |
18 | body='Hello {}'.format(args['name']).encode('utf-8') | |
19 | ) | |
20 | ||
21 | app = web.Application() | |
22 | app.router.add_route('GET', '/', index) | |
23 | """ | |
24 | import typing | |
25 | ||
26 | from aiohttp import web | |
27 | from aiohttp.web import Request | |
28 | from aiohttp import web_exceptions | |
29 | from marshmallow import Schema, ValidationError | |
30 | from marshmallow.fields import Field | |
31 | ||
32 | from webargs import core | |
33 | from webargs.core import json | |
34 | from webargs.asyncparser import AsyncParser | |
35 | ||
36 | ||
37 | def is_json_request(req: Request) -> bool: | |
38 | content_type = req.content_type | |
39 | return core.is_json(content_type) | |
40 | ||
41 | ||
42 | class HTTPUnprocessableEntity(web.HTTPClientError): | |
43 | status_code = 422 | |
44 | ||
45 | ||
46 | # Mapping of status codes to exception classes | |
47 | # Adapted from werkzeug | |
48 | exception_map = {422: HTTPUnprocessableEntity} | |
49 | ||
50 | ||
51 | def _find_exceptions() -> None: | |
52 | for name in web_exceptions.__all__: | |
53 | obj = getattr(web_exceptions, name) | |
54 | try: | |
55 | is_http_exception = issubclass(obj, web_exceptions.HTTPException) | |
56 | except TypeError: | |
57 | is_http_exception = False | |
58 | if not is_http_exception or obj.status_code is None: | |
59 | continue | |
60 | old_obj = exception_map.get(obj.status_code, None) | |
61 | if old_obj is not None and issubclass(obj, old_obj): | |
62 | continue | |
63 | exception_map[obj.status_code] = obj | |
64 | ||
65 | ||
66 | # Collect all exceptions from aiohttp.web_exceptions | |
67 | _find_exceptions() | |
68 | del _find_exceptions | |
69 | ||
70 | ||
71 | class AIOHTTPParser(AsyncParser): | |
72 | """aiohttp request argument parser.""" | |
73 | ||
74 | __location_map__ = dict( | |
75 | match_info="parse_match_info", **core.Parser.__location_map__ | |
76 | ) | |
77 | ||
78 | def parse_querystring(self, req: Request, name: str, field: Field) -> typing.Any: | |
79 | """Pull a querystring value from the request.""" | |
80 | return core.get_value(req.query, name, field) | |
81 | ||
82 | async def parse_form(self, req: Request, name: str, field: Field) -> typing.Any: | |
83 | """Pull a form value from the request.""" | |
84 | post_data = self._cache.get("post") | |
85 | if post_data is None: | |
86 | self._cache["post"] = await req.post() | |
87 | return core.get_value(self._cache["post"], name, field) | |
88 | ||
89 | async def parse_json(self, req: Request, name: str, field: Field) -> typing.Any: | |
90 | """Pull a json value from the request.""" | |
91 | json_data = self._cache.get("json") | |
92 | if json_data is None: | |
93 | if not (req.body_exists and is_json_request(req)): | |
94 | return core.missing | |
95 | try: | |
96 | json_data = await req.json(loads=json.loads) | |
97 | except json.JSONDecodeError as e: | |
98 | if e.doc == "": | |
99 | return core.missing | |
100 | else: | |
101 | return self.handle_invalid_json_error(e, req) | |
102 | self._cache["json"] = json_data | |
103 | return core.get_value(json_data, name, field, allow_many_nested=True) | |
104 | ||
105 | def parse_headers(self, req: Request, name: str, field: Field) -> typing.Any: | |
106 | """Pull a value from the header data.""" | |
107 | return core.get_value(req.headers, name, field) | |
108 | ||
109 | def parse_cookies(self, req: Request, name: str, field: Field) -> typing.Any: | |
110 | """Pull a value from the cookiejar.""" | |
111 | return core.get_value(req.cookies, name, field) | |
112 | ||
113 | def parse_files(self, req: Request, name: str, field: Field) -> None: | |
114 | raise NotImplementedError( | |
115 | "parse_files is not implemented. You may be able to use parse_form for " | |
116 | "parsing upload data." | |
117 | ) | |
118 | ||
119 | def parse_match_info(self, req: Request, name: str, field: Field) -> typing.Any: | |
120 | """Pull a value from the request's ``match_info``.""" | |
121 | return core.get_value(req.match_info, name, field) | |
122 | ||
123 | def get_request_from_view_args( | |
124 | self, view: typing.Callable, args: typing.Iterable, kwargs: typing.Mapping | |
125 | ) -> Request: | |
126 | """Get request object from a handler function or method. Used internally by | |
127 | ``use_args`` and ``use_kwargs``. | |
128 | """ | |
129 | req = None | |
130 | for arg in args: | |
131 | if isinstance(arg, web.Request): | |
132 | req = arg | |
133 | break | |
134 | elif isinstance(arg, web.View): | |
135 | req = arg.request | |
136 | break | |
137 | assert isinstance(req, web.Request), "Request argument not found for handler" | |
138 | return req | |
139 | ||
140 | def handle_error( | |
141 | self, | |
142 | error: ValidationError, | |
143 | req: Request, | |
144 | schema: Schema, | |
145 | error_status_code: typing.Union[int, None] = None, | |
146 | error_headers: typing.Union[typing.Mapping[str, str], None] = None, | |
147 | ) -> "typing.NoReturn": | |
148 | """Handle ValidationErrors and return a JSON response of error messages | |
149 | to the client. | |
150 | """ | |
151 | error_class = exception_map.get( | |
152 | error_status_code or self.DEFAULT_VALIDATION_STATUS | |
153 | ) | |
154 | if not error_class: | |
155 | raise LookupError("No exception for {0}".format(error_status_code)) | |
156 | headers = error_headers | |
157 | raise error_class( | |
158 | body=json.dumps(error.messages).encode("utf-8"), | |
159 | headers=headers, | |
160 | content_type="application/json", | |
161 | ) | |
162 | ||
163 | def handle_invalid_json_error( | |
164 | self, error: json.JSONDecodeError, req: Request, *args, **kwargs | |
165 | ) -> "typing.NoReturn": | |
166 | error_class = exception_map[400] | |
167 | messages = {"json": ["Invalid JSON body."]} | |
168 | raise error_class( | |
169 | body=json.dumps(messages).encode("utf-8"), content_type="application/json" | |
170 | ) | |
171 | ||
172 | ||
173 | parser = AIOHTTPParser() | |
174 | use_args = parser.use_args # type: typing.Callable | |
175 | use_kwargs = parser.use_kwargs # type: typing.Callable |
0 | """Asynchronous request parser. Compatible with Python>=3.5.""" | |
1 | import asyncio | |
2 | import functools | |
3 | import inspect | |
4 | import typing | |
5 | from collections.abc import Mapping | |
6 | ||
7 | from marshmallow import Schema, ValidationError | |
8 | from marshmallow.fields import Field | |
9 | import marshmallow as ma | |
10 | from marshmallow.utils import missing | |
11 | ||
12 | from webargs import core | |
13 | ||
14 | Request = typing.TypeVar("Request") | |
15 | ArgMap = typing.Union[Schema, typing.Mapping[str, Field]] | |
16 | Validate = typing.Union[typing.Callable, typing.Iterable[typing.Callable]] | |
17 | ||
18 | ||
19 | class AsyncParser(core.Parser): | |
20 | """Asynchronous variant of `webargs.core.Parser`, where parsing methods may be | |
21 | either coroutines or regular methods. | |
22 | """ | |
23 | ||
24 | async def _parse_request( | |
25 | self, schema: Schema, req: Request, locations: typing.Iterable | |
26 | ) -> typing.Union[dict, list]: | |
27 | if schema.many: | |
28 | assert ( | |
29 | "json" in locations | |
30 | ), "schema.many=True is only supported for JSON location" | |
31 | # The ad hoc Nested field is more like a workaround or a helper, | |
32 | # and it servers its purpose fine. However, if somebody has a desire | |
33 | # to re-design the support of bulk-type arguments, go ahead. | |
34 | parsed = await self.parse_arg( | |
35 | name="json", | |
36 | field=ma.fields.Nested(schema, many=True), | |
37 | req=req, | |
38 | locations=locations, | |
39 | ) | |
40 | if parsed is missing: | |
41 | parsed = [] | |
42 | else: | |
43 | argdict = schema.fields | |
44 | parsed = {} | |
45 | for argname, field_obj in argdict.items(): | |
46 | if core.MARSHMALLOW_VERSION_INFO[0] < 3: | |
47 | parsed_value = await self.parse_arg( | |
48 | argname, field_obj, req, locations | |
49 | ) | |
50 | # If load_from is specified on the field, try to parse from that key | |
51 | if parsed_value is missing and field_obj.load_from: | |
52 | parsed_value = await self.parse_arg( | |
53 | field_obj.load_from, field_obj, req, locations | |
54 | ) | |
55 | argname = field_obj.load_from | |
56 | else: | |
57 | argname = field_obj.data_key or argname | |
58 | parsed_value = await self.parse_arg( | |
59 | argname, field_obj, req, locations | |
60 | ) | |
61 | if parsed_value is not missing: | |
62 | parsed[argname] = parsed_value | |
63 | return parsed | |
64 | ||
65 | # TODO: Lots of duplication from core.Parser here. Rethink. | |
66 | async def parse( | |
67 | 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]: | |
75 | """Coroutine variant of `webargs.core.Parser`. | |
76 | ||
77 | Receives the same arguments as `webargs.core.Parser.parse`. | |
78 | """ | |
79 | req = req if req is not None else self.get_default_request() | |
80 | assert req is not None, "Must pass req object" | |
81 | data = None | |
82 | validators = core._ensure_list_of_callables(validate) | |
83 | schema = self._get_schema(argmap, req) | |
84 | try: | |
85 | parsed = await self._parse_request( | |
86 | schema=schema, req=req, locations=locations or self.locations | |
87 | ) | |
88 | result = schema.load(parsed) | |
89 | data = result.data if core.MARSHMALLOW_VERSION_INFO[0] < 3 else result | |
90 | self._validate_arguments(data, validators) | |
91 | except ma.exceptions.ValidationError as error: | |
92 | await self._on_validation_error( | |
93 | error, req, schema, error_status_code, error_headers | |
94 | ) | |
95 | finally: | |
96 | self.clear_cache() | |
97 | return data | |
98 | ||
99 | async def _on_validation_error( | |
100 | self, | |
101 | error: ValidationError, | |
102 | req: Request, | |
103 | schema: Schema, | |
104 | error_status_code: typing.Union[int, None], | |
105 | error_headers: typing.Union[typing.Mapping[str, str], None] = None, | |
106 | ) -> None: | |
107 | error_handler = self.error_callback or self.handle_error | |
108 | await error_handler(error, req, schema, error_status_code, error_headers) | |
109 | ||
110 | def use_args( | |
111 | self, | |
112 | argmap: ArgMap, | |
113 | req: typing.Optional[Request] = None, | |
114 | locations: typing.Iterable = None, | |
115 | as_kwargs: bool = False, | |
116 | validate: Validate = None, | |
117 | error_status_code: typing.Optional[int] = None, | |
118 | error_headers: typing.Union[typing.Mapping[str, str], None] = None, | |
119 | ) -> typing.Callable[..., typing.Callable]: | |
120 | """Decorator that injects parsed arguments into a view function or method. | |
121 | ||
122 | Receives the same arguments as `webargs.core.Parser.use_args`. | |
123 | """ | |
124 | locations = locations or self.locations | |
125 | request_obj = req | |
126 | # Optimization: If argmap is passed as a dictionary, we only need | |
127 | # to generate a Schema once | |
128 | if isinstance(argmap, Mapping): | |
129 | argmap = core.dict2schema(argmap)() | |
130 | ||
131 | def decorator(func: typing.Callable) -> typing.Callable: | |
132 | req_ = request_obj | |
133 | ||
134 | if inspect.iscoroutinefunction(func): | |
135 | ||
136 | @functools.wraps(func) | |
137 | async def wrapper(*args, **kwargs): | |
138 | req_obj = req_ | |
139 | ||
140 | if not req_obj: | |
141 | req_obj = self.get_request_from_view_args(func, args, kwargs) | |
142 | # NOTE: At this point, argmap may be a Schema, callable, or dict | |
143 | parsed_args = await self.parse( | |
144 | argmap, | |
145 | req=req_obj, | |
146 | locations=locations, | |
147 | validate=validate, | |
148 | error_status_code=error_status_code, | |
149 | error_headers=error_headers, | |
150 | ) | |
151 | if as_kwargs: | |
152 | kwargs.update(parsed_args or {}) | |
153 | return await func(*args, **kwargs) | |
154 | else: | |
155 | # Add parsed_args after other positional arguments | |
156 | new_args = args + (parsed_args,) | |
157 | return await func(*new_args, **kwargs) | |
158 | ||
159 | else: | |
160 | ||
161 | @functools.wraps(func) # type: ignore | |
162 | def wrapper(*args, **kwargs): | |
163 | req_obj = req_ | |
164 | ||
165 | if not req_obj: | |
166 | req_obj = self.get_request_from_view_args(func, args, kwargs) | |
167 | # NOTE: At this point, argmap may be a Schema, callable, or dict | |
168 | parsed_args = yield from self.parse( # type: ignore | |
169 | argmap, | |
170 | req=req_obj, | |
171 | locations=locations, | |
172 | validate=validate, | |
173 | error_status_code=error_status_code, | |
174 | error_headers=error_headers, | |
175 | ) | |
176 | if as_kwargs: | |
177 | kwargs.update(parsed_args) | |
178 | return func(*args, **kwargs) # noqa: B901 | |
179 | else: | |
180 | # Add parsed_args after other positional arguments | |
181 | new_args = args + (parsed_args,) | |
182 | return func(*new_args, **kwargs) | |
183 | ||
184 | return wrapper | |
185 | ||
186 | return decorator | |
187 | ||
188 | def use_kwargs(self, *args, **kwargs) -> typing.Callable: | |
189 | """Decorator that injects parsed arguments into a view function or method. | |
190 | ||
191 | Receives the same arguments as `webargs.core.Parser.use_kwargs`. | |
192 | ||
193 | """ | |
194 | return super().use_kwargs(*args, **kwargs) | |
195 | ||
196 | async def parse_arg( | |
197 | self, name: str, field: Field, req: Request, locations: typing.Iterable = None | |
198 | ) -> typing.Any: | |
199 | location = field.metadata.get("location") | |
200 | if location: | |
201 | locations_to_check = self._validated_locations([location]) | |
202 | else: | |
203 | locations_to_check = self._validated_locations(locations or self.locations) | |
204 | ||
205 | for location in locations_to_check: | |
206 | value = await self._get_value(name, field, req=req, location=location) | |
207 | # Found the value; validate and return it | |
208 | if value is not core.missing: | |
209 | return value | |
210 | return core.missing | |
211 | ||
212 | async def _get_value( | |
213 | self, name: str, argobj: Field, req: Request, location: str | |
214 | ) -> typing.Any: | |
215 | function = self._get_handler(location) | |
216 | if asyncio.iscoroutinefunction(function): | |
217 | value = await function(req, name, argobj) | |
218 | else: | |
219 | value = function(req, name, argobj) | |
220 | return value |
0 | # -*- coding: utf-8 -*- | |
1 | """Bottle request argument parsing module. | |
2 | ||
3 | Example: :: | |
4 | ||
5 | from bottle import route, run | |
6 | from marshmallow import fields | |
7 | from webargs.bottleparser import use_args | |
8 | ||
9 | hello_args = { | |
10 | 'name': fields.Str(missing='World') | |
11 | } | |
12 | @route('/', method='GET', apply=use_args(hello_args)) | |
13 | def index(args): | |
14 | return 'Hello ' + args['name'] | |
15 | ||
16 | if __name__ == '__main__': | |
17 | run(debug=True) | |
18 | """ | |
19 | import bottle | |
20 | ||
21 | from webargs import core | |
22 | from webargs.core import json | |
23 | ||
24 | ||
25 | class BottleParser(core.Parser): | |
26 | """Bottle.py request argument parser.""" | |
27 | ||
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) | |
31 | ||
32 | def parse_form(self, req, name, field): | |
33 | """Pull a form value from the request.""" | |
34 | return core.get_value(req.forms, name, field) | |
35 | ||
36 | def parse_json(self, req, name, field): | |
37 | """Pull a json value from the request.""" | |
38 | json_data = self._cache.get("json") | |
39 | if json_data is None: | |
40 | try: | |
41 | self._cache["json"] = json_data = req.json | |
42 | except AttributeError: | |
43 | return core.missing | |
44 | except json.JSONDecodeError as e: | |
45 | if e.doc == "": | |
46 | return core.missing | |
47 | else: | |
48 | return self.handle_invalid_json_error(e, req) | |
49 | if json_data is None: | |
50 | return core.missing | |
51 | return core.get_value(json_data, name, field, allow_many_nested=True) | |
52 | ||
53 | def parse_headers(self, req, name, field): | |
54 | """Pull a value from the header data.""" | |
55 | return core.get_value(req.headers, name, field) | |
56 | ||
57 | def parse_cookies(self, req, name, field): | |
58 | """Pull a value from the cookiejar.""" | |
59 | return req.get_cookie(name) | |
60 | ||
61 | def parse_files(self, req, name, field): | |
62 | """Pull a file from the request.""" | |
63 | return core.get_value(req.files, name, field) | |
64 | ||
65 | def handle_error(self, error, req, schema, error_status_code, error_headers): | |
66 | """Handles errors during parsing. Aborts the current request with a | |
67 | 400 error. | |
68 | """ | |
69 | status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS | |
70 | raise bottle.HTTPError( | |
71 | status=status_code, | |
72 | body=error.messages, | |
73 | headers=error_headers, | |
74 | exception=error, | |
75 | ) | |
76 | ||
77 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
78 | raise bottle.HTTPError( | |
79 | status=400, body={"json": ["Invalid JSON body."]}, exception=error | |
80 | ) | |
81 | ||
82 | def get_default_request(self): | |
83 | """Override to use bottle's thread-local request object by default.""" | |
84 | return bottle.request | |
85 | ||
86 | ||
87 | parser = BottleParser() | |
88 | use_args = parser.use_args | |
89 | use_kwargs = parser.use_kwargs |
0 | # -*- coding: utf-8 -*- | |
1 | from __future__ import unicode_literals | |
2 | ||
3 | import functools | |
4 | import inspect | |
5 | import logging | |
6 | import warnings | |
7 | from distutils.version import LooseVersion | |
8 | ||
9 | try: | |
10 | from collections.abc import Mapping | |
11 | except ImportError: | |
12 | from collections import Mapping | |
13 | ||
14 | try: | |
15 | import simplejson as json | |
16 | except ImportError: | |
17 | import json # type: ignore | |
18 | ||
19 | import marshmallow as ma | |
20 | from marshmallow import ValidationError | |
21 | from marshmallow.compat import iteritems | |
22 | from marshmallow.utils import missing, is_collection | |
23 | ||
24 | logger = logging.getLogger(__name__) | |
25 | ||
26 | ||
27 | __all__ = [ | |
28 | "ValidationError", | |
29 | "dict2schema", | |
30 | "is_multiple", | |
31 | "Parser", | |
32 | "get_value", | |
33 | "missing", | |
34 | "parse_json", | |
35 | ] | |
36 | ||
37 | MARSHMALLOW_VERSION_INFO = tuple(LooseVersion(ma.__version__).version) # type: tuple | |
38 | ||
39 | DEFAULT_VALIDATION_STATUS = 422 # type: int | |
40 | ||
41 | ||
42 | def _callable_or_raise(obj): | |
43 | """Makes sure an object is callable if it is not ``None``. If not | |
44 | callable, a ValueError is raised. | |
45 | """ | |
46 | if obj and not callable(obj): | |
47 | raise ValueError("{0!r} is not callable.".format(obj)) | |
48 | else: | |
49 | return obj | |
50 | ||
51 | ||
52 | def dict2schema(dct): | |
53 | """Generate a `marshmallow.Schema` class given a dictionary of | |
54 | `Fields <marshmallow.fields.Field>`. | |
55 | """ | |
56 | attrs = dct.copy() | |
57 | ||
58 | class Meta(object): | |
59 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
60 | strict = True | |
61 | else: | |
62 | register = False | |
63 | ||
64 | attrs["Meta"] = Meta | |
65 | return type(str(""), (ma.Schema,), attrs) | |
66 | ||
67 | ||
68 | def is_multiple(field): | |
69 | """Return whether or not `field` handles repeated/multi-value arguments.""" | |
70 | return isinstance(field, ma.fields.List) and not hasattr(field, "delimiter") | |
71 | ||
72 | ||
73 | def get_mimetype(content_type): | |
74 | return content_type.split(";")[0].strip() if content_type else None | |
75 | ||
76 | ||
77 | # Adapted from werkzeug: | |
78 | # https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/wrappers.py | |
79 | def is_json(mimetype): | |
80 | """Indicates if this mimetype is JSON or not. By default a request | |
81 | is considered to include JSON data if the mimetype is | |
82 | ``application/json`` or ``application/*+json``. | |
83 | """ | |
84 | if not mimetype: | |
85 | return False | |
86 | if ";" in mimetype: # Allow Content-Type header to be passed | |
87 | mimetype = get_mimetype(mimetype) | |
88 | if mimetype == "application/json": | |
89 | return True | |
90 | if mimetype.startswith("application/") and mimetype.endswith("+json"): | |
91 | return True | |
92 | return False | |
93 | ||
94 | ||
95 | def get_value(data, name, field, allow_many_nested=False): | |
96 | """Get a value from a dictionary. Handles ``MultiDict`` types when | |
97 | ``multiple=True``. If the value is not found, return `missing`. | |
98 | ||
99 | :param object data: Mapping (e.g. `dict`) or list-like instance to | |
100 | pull the value from. | |
101 | :param str name: Name of the key. | |
102 | :param bool multiple: Whether to handle multiple values. | |
103 | :param bool allow_many_nested: Whether to allow a list of nested objects | |
104 | (it is valid only for JSON format, so it is set to True in ``parse_json`` | |
105 | methods). | |
106 | """ | |
107 | missing_value = missing | |
108 | if allow_many_nested and isinstance(field, ma.fields.Nested) and field.many: | |
109 | if is_collection(data): | |
110 | return data | |
111 | ||
112 | if not hasattr(data, "get"): | |
113 | return missing_value | |
114 | ||
115 | multiple = is_multiple(field) | |
116 | val = data.get(name, missing_value) | |
117 | if multiple and val is not missing: | |
118 | if hasattr(data, "getlist"): | |
119 | return data.getlist(name) | |
120 | elif hasattr(data, "getall"): | |
121 | return data.getall(name) | |
122 | elif isinstance(val, (list, tuple)): | |
123 | return val | |
124 | if val is None: | |
125 | return None | |
126 | else: | |
127 | return [val] | |
128 | return val | |
129 | ||
130 | ||
131 | def parse_json(s, encoding="utf-8"): | |
132 | if isinstance(s, bytes): | |
133 | s = s.decode(encoding) | |
134 | return json.loads(s) | |
135 | ||
136 | ||
137 | def _ensure_list_of_callables(obj): | |
138 | if obj: | |
139 | if isinstance(obj, (list, tuple)): | |
140 | validators = obj | |
141 | elif callable(obj): | |
142 | validators = [obj] | |
143 | else: | |
144 | raise ValueError( | |
145 | "{0!r} is not a callable or list of callables.".format(obj) | |
146 | ) | |
147 | else: | |
148 | validators = [] | |
149 | return validators | |
150 | ||
151 | ||
152 | class Parser(object): | |
153 | """Base parser class that provides high-level implementation for parsing | |
154 | a request. | |
155 | ||
156 | Descendant classes must provide lower-level implementations for parsing | |
157 | different locations, e.g. ``parse_json``, ``parse_querystring``, etc. | |
158 | ||
159 | :param tuple locations: Default locations to parse. | |
160 | :param callable error_handler: Custom error handler function. | |
161 | """ | |
162 | ||
163 | DEFAULT_LOCATIONS = ("querystring", "form", "json") | |
164 | #: Default status code to return for validation errors | |
165 | DEFAULT_VALIDATION_STATUS = DEFAULT_VALIDATION_STATUS | |
166 | #: Default error message for validation errors | |
167 | DEFAULT_VALIDATION_MESSAGE = "Invalid value." | |
168 | ||
169 | #: Maps location => method name | |
170 | __location_map__ = { | |
171 | "json": "parse_json", | |
172 | "querystring": "parse_querystring", | |
173 | "query": "parse_querystring", | |
174 | "form": "parse_form", | |
175 | "headers": "parse_headers", | |
176 | "cookies": "parse_cookies", | |
177 | "files": "parse_files", | |
178 | } | |
179 | ||
180 | def __init__(self, locations=None, error_handler=None): | |
181 | self.locations = locations or self.DEFAULT_LOCATIONS | |
182 | self.error_callback = _callable_or_raise(error_handler) | |
183 | #: A short-lived cache to store results from processing request bodies. | |
184 | self._cache = {} | |
185 | ||
186 | def _validated_locations(self, locations): | |
187 | """Ensure that the given locations argument is valid. | |
188 | ||
189 | :raises: ValueError if a given locations includes an invalid location. | |
190 | """ | |
191 | # The set difference between the given locations and the available locations | |
192 | # will be the set of invalid locations | |
193 | valid_locations = set(self.__location_map__.keys()) | |
194 | given = set(locations) | |
195 | invalid_locations = given - valid_locations | |
196 | if len(invalid_locations): | |
197 | msg = "Invalid locations arguments: {0}".format(list(invalid_locations)) | |
198 | raise ValueError(msg) | |
199 | return locations | |
200 | ||
201 | def _get_handler(self, location): | |
202 | # Parsing function to call | |
203 | # May be a method name (str) or a function | |
204 | func = self.__location_map__.get(location) | |
205 | if func: | |
206 | if inspect.isfunction(func): | |
207 | function = func | |
208 | else: | |
209 | function = getattr(self, func) | |
210 | else: | |
211 | raise ValueError('Invalid location: "{0}"'.format(location)) | |
212 | return function | |
213 | ||
214 | def _get_value(self, name, argobj, req, location): | |
215 | function = self._get_handler(location) | |
216 | return function(req, name, argobj) | |
217 | ||
218 | def parse_arg(self, name, field, req, locations=None): | |
219 | """Parse a single argument from a request. | |
220 | ||
221 | .. note:: | |
222 | This method does not perform validation on the argument. | |
223 | ||
224 | :param str name: The name of the value. | |
225 | :param marshmallow.fields.Field field: The marshmallow `Field` for the request | |
226 | parameter. | |
227 | :param req: The request object to parse. | |
228 | :param tuple locations: The locations ('json', 'querystring', etc.) where | |
229 | to search for the value. | |
230 | :return: The unvalidated argument value or `missing` if the value cannot | |
231 | be found on the request. | |
232 | """ | |
233 | location = field.metadata.get("location") | |
234 | if location: | |
235 | locations_to_check = self._validated_locations([location]) | |
236 | else: | |
237 | locations_to_check = self._validated_locations(locations or self.locations) | |
238 | ||
239 | for location in locations_to_check: | |
240 | value = self._get_value(name, field, req=req, location=location) | |
241 | # Found the value; validate and return it | |
242 | if value is not missing: | |
243 | return value | |
244 | return missing | |
245 | ||
246 | def _parse_request(self, schema, req, locations): | |
247 | """Return a parsed arguments dictionary for the current request.""" | |
248 | if schema.many: | |
249 | assert ( | |
250 | "json" in locations | |
251 | ), "schema.many=True is only supported for JSON location" | |
252 | # The ad hoc Nested field is more like a workaround or a helper, | |
253 | # and it servers its purpose fine. However, if somebody has a desire | |
254 | # to re-design the support of bulk-type arguments, go ahead. | |
255 | parsed = self.parse_arg( | |
256 | name="json", | |
257 | field=ma.fields.Nested(schema, many=True), | |
258 | req=req, | |
259 | locations=locations, | |
260 | ) | |
261 | if parsed is missing: | |
262 | parsed = [] | |
263 | else: | |
264 | argdict = schema.fields | |
265 | parsed = {} | |
266 | for argname, field_obj in iteritems(argdict): | |
267 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
268 | parsed_value = self.parse_arg(argname, field_obj, req, locations) | |
269 | # If load_from is specified on the field, try to parse from that key | |
270 | if parsed_value is missing and field_obj.load_from: | |
271 | parsed_value = self.parse_arg( | |
272 | field_obj.load_from, field_obj, req, locations | |
273 | ) | |
274 | argname = field_obj.load_from | |
275 | else: | |
276 | argname = field_obj.data_key or argname | |
277 | parsed_value = self.parse_arg(argname, field_obj, req, locations) | |
278 | if parsed_value is not missing: | |
279 | parsed[argname] = parsed_value | |
280 | return parsed | |
281 | ||
282 | def _on_validation_error( | |
283 | self, error, req, schema, error_status_code, error_headers | |
284 | ): | |
285 | error_handler = self.error_callback or self.handle_error | |
286 | error_handler(error, req, schema, error_status_code, error_headers) | |
287 | ||
288 | def _validate_arguments(self, data, validators): | |
289 | for validator in validators: | |
290 | if validator(data) is False: | |
291 | msg = self.DEFAULT_VALIDATION_MESSAGE | |
292 | raise ValidationError(msg, data=data) | |
293 | ||
294 | def _get_schema(self, argmap, req): | |
295 | """Return a `marshmallow.Schema` for the given argmap and request. | |
296 | ||
297 | :param argmap: Either a `marshmallow.Schema`, `dict` | |
298 | of argname -> `marshmallow.fields.Field` pairs, or a callable that returns | |
299 | a `marshmallow.Schema` instance. | |
300 | :param req: The request object being parsed. | |
301 | :rtype: marshmallow.Schema | |
302 | """ | |
303 | if isinstance(argmap, ma.Schema): | |
304 | schema = argmap | |
305 | elif isinstance(argmap, type) and issubclass(argmap, ma.Schema): | |
306 | schema = argmap() | |
307 | elif callable(argmap): | |
308 | schema = argmap(req) | |
309 | else: | |
310 | schema = dict2schema(argmap)() | |
311 | if MARSHMALLOW_VERSION_INFO[0] < 3 and not schema.strict: | |
312 | warnings.warn( | |
313 | "It is highly recommended that you set strict=True on your schema " | |
314 | "so that the parser's error handler will be invoked when expected.", | |
315 | UserWarning, | |
316 | ) | |
317 | return schema | |
318 | ||
319 | def parse( | |
320 | self, | |
321 | argmap, | |
322 | req=None, | |
323 | locations=None, | |
324 | validate=None, | |
325 | error_status_code=None, | |
326 | error_headers=None, | |
327 | ): | |
328 | """Main request parsing method. | |
329 | ||
330 | :param argmap: Either a `marshmallow.Schema`, a `dict` | |
331 | of argname -> `marshmallow.fields.Field` pairs, or a callable | |
332 | which accepts a request and returns a `marshmallow.Schema`. | |
333 | :param req: The request object to parse. | |
334 | :param tuple locations: Where on the request to search for values. | |
335 | Can include one or more of ``('json', 'querystring', 'form', | |
336 | 'headers', 'cookies', 'files')``. | |
337 | :param callable validate: Validation function or list of validation functions | |
338 | that receives the dictionary of parsed arguments. Validator either returns a | |
339 | boolean or raises a :exc:`ValidationError`. | |
340 | :param int error_status_code: Status code passed to error handler functions when | |
341 | a `ValidationError` is raised. | |
342 | :param dict error_headers: Headers passed to error handler functions when a | |
343 | a `ValidationError` is raised. | |
344 | ||
345 | :return: A dictionary of parsed arguments | |
346 | """ | |
347 | req = req if req is not None else self.get_default_request() | |
348 | assert req is not None, "Must pass req object" | |
349 | data = None | |
350 | validators = _ensure_list_of_callables(validate) | |
351 | schema = self._get_schema(argmap, req) | |
352 | try: | |
353 | parsed = self._parse_request( | |
354 | schema=schema, req=req, locations=locations or self.locations | |
355 | ) | |
356 | result = schema.load(parsed) | |
357 | data = result.data if MARSHMALLOW_VERSION_INFO[0] < 3 else result | |
358 | self._validate_arguments(data, validators) | |
359 | except ma.exceptions.ValidationError as error: | |
360 | self._on_validation_error( | |
361 | error, req, schema, error_status_code, error_headers | |
362 | ) | |
363 | finally: | |
364 | self.clear_cache() | |
365 | return data | |
366 | ||
367 | def clear_cache(self): | |
368 | """Invalidate the parser's cache.""" | |
369 | self._cache = {} | |
370 | return None | |
371 | ||
372 | def get_default_request(self): | |
373 | """Optional override. Provides a hook for frameworks that use thread-local | |
374 | request objects. | |
375 | """ | |
376 | return None | |
377 | ||
378 | def get_request_from_view_args(self, view, args, kwargs): | |
379 | """Optional override. Returns the request object to be parsed, given a view | |
380 | function's args and kwargs. | |
381 | ||
382 | Used by the `use_args` and `use_kwargs` to get a request object from a | |
383 | view's arguments. | |
384 | ||
385 | :param callable view: The view function or method being decorated by | |
386 | `use_args` or `use_kwargs` | |
387 | :param tuple args: Positional arguments passed to ``view``. | |
388 | :param dict kwargs: Keyword arguments passed to ``view``. | |
389 | """ | |
390 | return None | |
391 | ||
392 | def use_args( | |
393 | self, | |
394 | argmap, | |
395 | req=None, | |
396 | locations=None, | |
397 | as_kwargs=False, | |
398 | validate=None, | |
399 | error_status_code=None, | |
400 | error_headers=None, | |
401 | ): | |
402 | """Decorator that injects parsed arguments into a view function or method. | |
403 | ||
404 | Example usage with Flask: :: | |
405 | ||
406 | @app.route('/echo', methods=['get', 'post']) | |
407 | @parser.use_args({'name': fields.Str()}) | |
408 | def greet(args): | |
409 | return 'Hello ' + args['name'] | |
410 | ||
411 | :param argmap: Either a `marshmallow.Schema`, a `dict` | |
412 | of argname -> `marshmallow.fields.Field` pairs, or a callable | |
413 | which accepts a request and returns a `marshmallow.Schema`. | |
414 | :param tuple locations: Where on the request to search for values. | |
415 | :param bool as_kwargs: Whether to insert arguments as keyword arguments. | |
416 | :param callable validate: Validation function that receives the dictionary | |
417 | of parsed arguments. If the function returns ``False``, the parser | |
418 | will raise a :exc:`ValidationError`. | |
419 | :param int error_status_code: Status code passed to error handler functions when | |
420 | a `ValidationError` is raised. | |
421 | :param dict error_headers: Headers passed to error handler functions when a | |
422 | a `ValidationError` is raised. | |
423 | """ | |
424 | locations = locations or self.locations | |
425 | request_obj = req | |
426 | # Optimization: If argmap is passed as a dictionary, we only need | |
427 | # to generate a Schema once | |
428 | if isinstance(argmap, Mapping): | |
429 | argmap = dict2schema(argmap)() | |
430 | ||
431 | def decorator(func): | |
432 | req_ = request_obj | |
433 | ||
434 | @functools.wraps(func) | |
435 | def wrapper(*args, **kwargs): | |
436 | req_obj = req_ | |
437 | ||
438 | if not req_obj: | |
439 | req_obj = self.get_request_from_view_args(func, args, kwargs) | |
440 | # NOTE: At this point, argmap may be a Schema, or a callable | |
441 | parsed_args = self.parse( | |
442 | argmap, | |
443 | req=req_obj, | |
444 | locations=locations, | |
445 | validate=validate, | |
446 | error_status_code=error_status_code, | |
447 | error_headers=error_headers, | |
448 | ) | |
449 | if as_kwargs: | |
450 | kwargs.update(parsed_args) | |
451 | return func(*args, **kwargs) | |
452 | else: | |
453 | # Add parsed_args after other positional arguments | |
454 | new_args = args + (parsed_args,) | |
455 | return func(*new_args, **kwargs) | |
456 | ||
457 | wrapper.__wrapped__ = func | |
458 | return wrapper | |
459 | ||
460 | return decorator | |
461 | ||
462 | def use_kwargs(self, *args, **kwargs): | |
463 | """Decorator that injects parsed arguments into a view function or method | |
464 | as keyword arguments. | |
465 | ||
466 | This is a shortcut to :meth:`use_args` with ``as_kwargs=True``. | |
467 | ||
468 | Example usage with Flask: :: | |
469 | ||
470 | @app.route('/echo', methods=['get', 'post']) | |
471 | @parser.use_kwargs({'name': fields.Str()}) | |
472 | def greet(name): | |
473 | return 'Hello ' + name | |
474 | ||
475 | Receives the same ``args`` and ``kwargs`` as :meth:`use_args`. | |
476 | """ | |
477 | kwargs["as_kwargs"] = True | |
478 | return self.use_args(*args, **kwargs) | |
479 | ||
480 | def location_handler(self, name): | |
481 | """Decorator that registers a function for parsing a request location. | |
482 | The wrapped function receives a request, the name of the argument, and | |
483 | the corresponding `Field <marshmallow.fields.Field>` object. | |
484 | ||
485 | Example: :: | |
486 | ||
487 | from webargs import core | |
488 | parser = core.Parser() | |
489 | ||
490 | @parser.location_handler("name") | |
491 | def parse_data(request, name, field): | |
492 | return request.data.get(name) | |
493 | ||
494 | :param str name: The name of the location to register. | |
495 | """ | |
496 | ||
497 | def decorator(func): | |
498 | self.__location_map__[name] = func | |
499 | return func | |
500 | ||
501 | return decorator | |
502 | ||
503 | def error_handler(self, func): | |
504 | """Decorator that registers a custom error handling function. The | |
505 | function should receive the raised error, request object, | |
506 | `marshmallow.Schema` instance used to parse the request, error status code, | |
507 | and headers to use for the error response. Overrides | |
508 | the parser's ``handle_error`` method. | |
509 | ||
510 | Example: :: | |
511 | ||
512 | from webargs import flaskparser | |
513 | ||
514 | parser = flaskparser.FlaskParser() | |
515 | ||
516 | ||
517 | class CustomError(Exception): | |
518 | pass | |
519 | ||
520 | ||
521 | @parser.error_handler | |
522 | def handle_error(error, req, schema, status_code, headers): | |
523 | raise CustomError(error.messages) | |
524 | ||
525 | :param callable func: The error callback to register. | |
526 | """ | |
527 | self.error_callback = func | |
528 | return func | |
529 | ||
530 | # Abstract Methods | |
531 | ||
532 | def parse_json(self, req, name, arg): | |
533 | """Pull a JSON value from a request object or return `missing` if the | |
534 | value cannot be found. | |
535 | """ | |
536 | return missing | |
537 | ||
538 | def parse_querystring(self, req, name, arg): | |
539 | """Pull a value from the query string of a request object or return `missing` if | |
540 | the value cannot be found. | |
541 | """ | |
542 | return missing | |
543 | ||
544 | def parse_form(self, req, name, arg): | |
545 | """Pull a value from the form data of a request object or return | |
546 | `missing` if the value cannot be found. | |
547 | """ | |
548 | return missing | |
549 | ||
550 | def parse_headers(self, req, name, arg): | |
551 | """Pull a value from the headers or return `missing` if the value | |
552 | cannot be found. | |
553 | """ | |
554 | return missing | |
555 | ||
556 | def parse_cookies(self, req, name, arg): | |
557 | """Pull a cookie value from the request or return `missing` if the value | |
558 | cannot be found. | |
559 | """ | |
560 | return missing | |
561 | ||
562 | def parse_files(self, req, name, arg): | |
563 | """Pull a file from the request or return `missing` if the value file | |
564 | cannot be found. | |
565 | """ | |
566 | return missing | |
567 | ||
568 | def handle_error( | |
569 | self, error, req, schema, error_status_code=None, error_headers=None | |
570 | ): | |
571 | """Called if an error occurs while parsing args. By default, just logs and | |
572 | raises ``error``. | |
573 | """ | |
574 | logger.error(error) | |
575 | raise error |
0 | # -*- coding: utf-8 -*- | |
1 | """Django request argument parsing. | |
2 | ||
3 | Example usage: :: | |
4 | ||
5 | from django.views.generic import View | |
6 | from django.http import HttpResponse | |
7 | from marshmallow import fields | |
8 | from webargs.djangoparser import use_args | |
9 | ||
10 | hello_args = { | |
11 | 'name': fields.Str(missing='World') | |
12 | } | |
13 | ||
14 | class MyView(View): | |
15 | ||
16 | @use_args(hello_args) | |
17 | def get(self, args, request): | |
18 | return HttpResponse('Hello ' + args['name']) | |
19 | """ | |
20 | from webargs import core | |
21 | from webargs.core import json | |
22 | ||
23 | ||
24 | class DjangoParser(core.Parser): | |
25 | """Django request argument parser. | |
26 | ||
27 | .. warning:: | |
28 | ||
29 | :class:`DjangoParser` does not override | |
30 | :meth:`handle_error <webargs.core.Parser.handle_error>`, so your Django | |
31 | views are responsible for catching any :exc:`ValidationErrors` raised by | |
32 | the parser and returning the appropriate `HTTPResponse`. | |
33 | """ | |
34 | ||
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 | ||
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) | |
42 | ||
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 | try: | |
48 | self._cache["json"] = json_data = core.parse_json(req.body) | |
49 | except AttributeError: | |
50 | return core.missing | |
51 | except json.JSONDecodeError as e: | |
52 | if e.doc == "": | |
53 | return core.missing | |
54 | else: | |
55 | return self.handle_invalid_json_error(e, req) | |
56 | return core.get_value(json_data, name, field, allow_many_nested=True) | |
57 | ||
58 | def parse_cookies(self, req, name, field): | |
59 | """Pull the value from the cookiejar.""" | |
60 | return core.get_value(req.COOKIES, name, field) | |
61 | ||
62 | def parse_headers(self, req, name, field): | |
63 | raise NotImplementedError( | |
64 | "Header parsing not supported by {0}".format(self.__class__.__name__) | |
65 | ) | |
66 | ||
67 | def parse_files(self, req, name, field): | |
68 | """Pull a file from the request.""" | |
69 | return core.get_value(req.FILES, name, field) | |
70 | ||
71 | def get_request_from_view_args(self, view, args, kwargs): | |
72 | # The first argument is either `self` or `request` | |
73 | try: # self.request | |
74 | return args[0].request | |
75 | except AttributeError: # first arg is request | |
76 | return args[0] | |
77 | ||
78 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
79 | raise error | |
80 | ||
81 | ||
82 | parser = DjangoParser() | |
83 | use_args = parser.use_args | |
84 | use_kwargs = parser.use_kwargs |
0 | # -*- coding: utf-8 -*- | |
1 | """Falcon request argument parsing module. | |
2 | """ | |
3 | import falcon | |
4 | from falcon.util.uri import parse_query_string | |
5 | ||
6 | from webargs import core | |
7 | from webargs.core import json | |
8 | ||
9 | HTTP_422 = "422 Unprocessable Entity" | |
10 | ||
11 | # Mapping of int status codes to string status | |
12 | status_map = {422: HTTP_422} | |
13 | ||
14 | ||
15 | # Collect all exceptions from falcon.status_codes | |
16 | def _find_exceptions(): | |
17 | for name in filter(lambda n: n.startswith("HTTP"), dir(falcon.status_codes)): | |
18 | status = getattr(falcon.status_codes, name) | |
19 | status_code = int(status.split(" ")[0]) | |
20 | status_map[status_code] = status | |
21 | ||
22 | ||
23 | _find_exceptions() | |
24 | del _find_exceptions | |
25 | ||
26 | ||
27 | def is_json_request(req): | |
28 | content_type = req.get_header("Content-Type") | |
29 | return content_type and core.is_json(content_type) | |
30 | ||
31 | ||
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 | # NOTE: Adapted from falcon.request.Request._parse_form_urlencoded | |
50 | def parse_form_body(req): | |
51 | if ( | |
52 | req.content_type is not None | |
53 | and "application/x-www-form-urlencoded" in req.content_type | |
54 | ): | |
55 | body = req.stream.read() | |
56 | try: | |
57 | body = body.decode("ascii") | |
58 | except UnicodeDecodeError: | |
59 | body = None | |
60 | req.log_error( | |
61 | "Non-ASCII characters found in form body " | |
62 | "with Content-Type of " | |
63 | "application/x-www-form-urlencoded. Body " | |
64 | "will be ignored." | |
65 | ) | |
66 | ||
67 | if body: | |
68 | return parse_query_string( | |
69 | body, keep_blank_qs_values=req.options.keep_blank_qs_values | |
70 | ) | |
71 | return {} | |
72 | ||
73 | ||
74 | class HTTPError(falcon.HTTPError): | |
75 | """HTTPError that stores a dictionary of validation error messages. | |
76 | """ | |
77 | ||
78 | def __init__(self, status, errors, *args, **kwargs): | |
79 | self.errors = errors | |
80 | super(HTTPError, self).__init__(status, *args, **kwargs) | |
81 | ||
82 | def to_dict(self, *args, **kwargs): | |
83 | """Override `falcon.HTTPError` to include error messages in responses.""" | |
84 | ret = super(HTTPError, self).to_dict(*args, **kwargs) | |
85 | if self.errors is not None: | |
86 | ret["errors"] = self.errors | |
87 | return ret | |
88 | ||
89 | ||
90 | class FalconParser(core.Parser): | |
91 | """Falcon request argument parser.""" | |
92 | ||
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) | |
96 | ||
97 | def parse_form(self, req, name, field): | |
98 | """Pull a form value from the request. | |
99 | ||
100 | .. note:: | |
101 | ||
102 | The request stream will be read and left at EOF. | |
103 | """ | |
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 | ||
109 | def parse_json(self, req, name, field): | |
110 | """Pull a JSON body value from the request. | |
111 | ||
112 | .. note:: | |
113 | ||
114 | The request stream will be read and left at EOF. | |
115 | """ | |
116 | json_data = self._cache.get("json_data") | |
117 | if json_data is None: | |
118 | try: | |
119 | self._cache["json_data"] = json_data = parse_json_body(req) | |
120 | except json.JSONDecodeError as e: | |
121 | return self.handle_invalid_json_error(e, req) | |
122 | return core.get_value(json_data, name, field, allow_many_nested=True) | |
123 | ||
124 | def parse_headers(self, req, name, field): | |
125 | """Pull a header value from the request.""" | |
126 | # Use req.get_headers rather than req.headers for performance | |
127 | return req.get_header(name, required=False) or core.missing | |
128 | ||
129 | def parse_cookies(self, req, name, field): | |
130 | """Pull a cookie value from the request.""" | |
131 | cookies = self._cache.get("cookies") | |
132 | if cookies is None: | |
133 | self._cache["cookies"] = cookies = req.cookies | |
134 | return core.get_value(cookies, name, field) | |
135 | ||
136 | def get_request_from_view_args(self, view, args, kwargs): | |
137 | """Get request from a resource method's arguments. Assumes that | |
138 | request is the second argument. | |
139 | """ | |
140 | req = args[1] | |
141 | assert isinstance(req, falcon.Request), "Argument is not a falcon.Request" | |
142 | return req | |
143 | ||
144 | def parse_files(self, req, name, field): | |
145 | raise NotImplementedError( | |
146 | "Parsing files not yet supported by {0}".format(self.__class__.__name__) | |
147 | ) | |
148 | ||
149 | def handle_error(self, error, req, schema, error_status_code, error_headers): | |
150 | """Handles errors during parsing.""" | |
151 | status = status_map.get(error_status_code or self.DEFAULT_VALIDATION_STATUS) | |
152 | if status is None: | |
153 | raise LookupError("Status code {0} not supported".format(error_status_code)) | |
154 | raise HTTPError(status, errors=error.messages, headers=error_headers) | |
155 | ||
156 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
157 | status = status_map[400] | |
158 | messages = {"json": ["Invalid JSON body."]} | |
159 | raise HTTPError(status, errors=messages) | |
160 | ||
161 | ||
162 | parser = FalconParser() | |
163 | use_args = parser.use_args | |
164 | use_kwargs = parser.use_kwargs |
0 | # -*- coding: utf-8 -*- | |
1 | """Field classes. | |
2 | ||
3 | Includes all fields from `marshmallow.fields` in addition to a custom | |
4 | `Nested` field and `DelimitedList`. | |
5 | ||
6 | All fields can optionally take a special `location` keyword argument, which | |
7 | tells webargs where to parse the request argument from. :: | |
8 | ||
9 | args = { | |
10 | 'active': fields.Bool(location='query') | |
11 | 'content_type': fields.Str(data_key='Content-Type', | |
12 | location='headers') | |
13 | } | |
14 | ||
15 | Note: `data_key` replaced `load_from` in marshmallow 3. | |
16 | When using marshmallow 2, use `load_from`. | |
17 | """ | |
18 | import marshmallow as ma | |
19 | ||
20 | from webargs.core import dict2schema | |
21 | ||
22 | __all__ = ["Nested", "DelimitedList"] | |
23 | # Expose all fields from marshmallow.fields. | |
24 | # We do this instead of 'from marshmallow.fields import *' because webargs | |
25 | # has its own subclass of Nested | |
26 | for each in (field_name for field_name in ma.fields.__all__ if field_name != "Nested"): | |
27 | __all__.append(each) | |
28 | globals()[each] = getattr(ma.fields, each) | |
29 | ||
30 | ||
31 | class Nested(ma.fields.Nested): | |
32 | """Same as `marshmallow.fields.Nested`, except can be passed a dictionary as | |
33 | the first argument, which will be converted to a `marshmallow.Schema`. | |
34 | """ | |
35 | ||
36 | def __init__(self, nested, *args, **kwargs): | |
37 | if isinstance(nested, dict): | |
38 | nested = dict2schema(nested) | |
39 | super(Nested, self).__init__(nested, *args, **kwargs) | |
40 | ||
41 | ||
42 | class DelimitedList(ma.fields.List): | |
43 | """Same as `marshmallow.fields.List`, except can load from either a list or | |
44 | a delimited string (e.g. "foo,bar,baz"). | |
45 | ||
46 | :param Field cls_or_instance: A field class or instance. | |
47 | :param str delimiter: Delimiter between values. | |
48 | :param bool as_string: Dump values to string. | |
49 | """ | |
50 | ||
51 | delimiter = "," | |
52 | ||
53 | def __init__(self, cls_or_instance, delimiter=None, as_string=False, **kwargs): | |
54 | self.delimiter = delimiter or self.delimiter | |
55 | self.as_string = as_string | |
56 | super(DelimitedList, self).__init__(cls_or_instance, **kwargs) | |
57 | ||
58 | def _serialize(self, value, attr, obj): | |
59 | ret = super(DelimitedList, self)._serialize(value, attr, obj) | |
60 | if self.as_string: | |
61 | return self.delimiter.join(format(each) for each in ret) | |
62 | return ret | |
63 | ||
64 | def _deserialize(self, value, attr, data): | |
65 | try: | |
66 | ret = ( | |
67 | value | |
68 | if ma.utils.is_iterable_but_not_string(value) | |
69 | else value.split(self.delimiter) | |
70 | ) | |
71 | except AttributeError: | |
72 | self.fail("invalid") | |
73 | return super(DelimitedList, self)._deserialize(ret, attr, data) |
0 | # -*- coding: utf-8 -*- | |
1 | """Flask request argument parsing module. | |
2 | ||
3 | Example: :: | |
4 | ||
5 | from flask import Flask | |
6 | ||
7 | from webargs import fields | |
8 | from webargs.flaskparser import use_args | |
9 | ||
10 | app = Flask(__name__) | |
11 | ||
12 | hello_args = { | |
13 | 'name': fields.Str(required=True) | |
14 | } | |
15 | ||
16 | @app.route('/') | |
17 | @use_args(hello_args) | |
18 | def index(args): | |
19 | return 'Hello ' + args['name'] | |
20 | """ | |
21 | import flask | |
22 | from werkzeug.exceptions import HTTPException | |
23 | ||
24 | from webargs import core | |
25 | from webargs.core import json | |
26 | ||
27 | ||
28 | def abort(http_status_code, exc=None, **kwargs): | |
29 | """Raise a HTTPException for the given http_status_code. Attach any keyword | |
30 | arguments to the exception for later processing. | |
31 | ||
32 | From Flask-Restful. See NOTICE file for license information. | |
33 | """ | |
34 | try: | |
35 | flask.abort(http_status_code) | |
36 | except HTTPException as err: | |
37 | err.data = kwargs | |
38 | err.exc = exc | |
39 | raise err | |
40 | ||
41 | ||
42 | def is_json_request(req): | |
43 | return core.is_json(req.mimetype) | |
44 | ||
45 | ||
46 | class FlaskParser(core.Parser): | |
47 | """Flask request argument parser.""" | |
48 | ||
49 | __location_map__ = dict(view_args="parse_view_args", **core.Parser.__location_map__) | |
50 | ||
51 | def parse_view_args(self, req, name, field): | |
52 | """Pull a value from the request's ``view_args``.""" | |
53 | return core.get_value(req.view_args, name, field) | |
54 | ||
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 | # We decode the json manually here instead of | |
60 | # using req.get_json() so that we can handle | |
61 | # JSONDecodeErrors consistently | |
62 | data = req.get_data(cache=True) | |
63 | try: | |
64 | self._cache["json"] = json_data = core.parse_json(data) | |
65 | except json.JSONDecodeError as e: | |
66 | if e.doc == "": | |
67 | return core.missing | |
68 | else: | |
69 | return self.handle_invalid_json_error(e, req) | |
70 | return core.get_value(json_data, name, field, allow_many_nested=True) | |
71 | ||
72 | def parse_querystring(self, req, name, field): | |
73 | """Pull a querystring value from the request.""" | |
74 | return core.get_value(req.args, name, field) | |
75 | ||
76 | def parse_form(self, req, name, field): | |
77 | """Pull a form value from the request.""" | |
78 | try: | |
79 | return core.get_value(req.form, name, field) | |
80 | except AttributeError: | |
81 | pass | |
82 | return core.missing | |
83 | ||
84 | def parse_headers(self, req, name, field): | |
85 | """Pull a value from the header data.""" | |
86 | return core.get_value(req.headers, name, field) | |
87 | ||
88 | def parse_cookies(self, req, name, field): | |
89 | """Pull a value from the cookiejar.""" | |
90 | return core.get_value(req.cookies, name, field) | |
91 | ||
92 | def parse_files(self, req, name, field): | |
93 | """Pull a file from the request.""" | |
94 | return core.get_value(req.files, name, field) | |
95 | ||
96 | def handle_error(self, error, req, schema, error_status_code, error_headers): | |
97 | """Handles errors during parsing. Aborts the current HTTP request and | |
98 | responds with a 422 error. | |
99 | """ | |
100 | status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS | |
101 | abort( | |
102 | status_code, | |
103 | exc=error, | |
104 | messages=error.messages, | |
105 | schema=schema, | |
106 | headers=error_headers, | |
107 | ) | |
108 | ||
109 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
110 | abort(400, exc=error, messages={"json": ["Invalid JSON body."]}) | |
111 | ||
112 | def get_default_request(self): | |
113 | """Override to use Flask's thread-local request objec by default""" | |
114 | return flask.request | |
115 | ||
116 | ||
117 | parser = FlaskParser() | |
118 | use_args = parser.use_args | |
119 | use_kwargs = parser.use_kwargs |
0 | # -*- coding: utf-8 -*- | |
1 | """Pyramid request argument parsing. | |
2 | ||
3 | Example usage: :: | |
4 | ||
5 | from wsgiref.simple_server import make_server | |
6 | from pyramid.config import Configurator | |
7 | from pyramid.response import Response | |
8 | from marshmallow import fields | |
9 | from webargs.pyramidparser import use_args | |
10 | ||
11 | hello_args = { | |
12 | 'name': fields.Str(missing='World') | |
13 | } | |
14 | ||
15 | @use_args(hello_args) | |
16 | def hello_world(request, args): | |
17 | return Response('Hello ' + args['name']) | |
18 | ||
19 | if __name__ == '__main__': | |
20 | config = Configurator() | |
21 | config.add_route('hello', '/') | |
22 | config.add_view(hello_world, route_name='hello') | |
23 | app = config.make_wsgi_app() | |
24 | server = make_server('0.0.0.0', 6543, app) | |
25 | server.serve_forever() | |
26 | """ | |
27 | import collections | |
28 | import functools | |
29 | ||
30 | from webob.multidict import MultiDict | |
31 | from pyramid.httpexceptions import exception_response | |
32 | ||
33 | from marshmallow.compat import text_type | |
34 | from webargs import core | |
35 | from webargs.core import json | |
36 | ||
37 | ||
38 | class PyramidParser(core.Parser): | |
39 | """Pyramid request argument parser.""" | |
40 | ||
41 | __location_map__ = dict(matchdict="parse_matchdict", **core.Parser.__location_map__) | |
42 | ||
43 | def parse_querystring(self, req, name, field): | |
44 | """Pull a querystring value from the request.""" | |
45 | return core.get_value(req.GET, name, field) | |
46 | ||
47 | def parse_form(self, req, name, field): | |
48 | """Pull a form value from the request.""" | |
49 | return core.get_value(req.POST, name, field) | |
50 | ||
51 | def parse_json(self, req, name, field): | |
52 | """Pull a json value from the request.""" | |
53 | json_data = self._cache.get("json") | |
54 | if json_data is None: | |
55 | try: | |
56 | self._cache["json"] = json_data = core.parse_json(req.body, req.charset) | |
57 | except json.JSONDecodeError as e: | |
58 | if e.doc == "": | |
59 | return core.missing | |
60 | else: | |
61 | return self.handle_invalid_json_error(e, req) | |
62 | if json_data is None: | |
63 | return core.missing | |
64 | return core.get_value(json_data, name, field, allow_many_nested=True) | |
65 | ||
66 | def parse_cookies(self, req, name, field): | |
67 | """Pull the value from the cookiejar.""" | |
68 | return core.get_value(req.cookies, name, field) | |
69 | ||
70 | def parse_headers(self, req, name, field): | |
71 | """Pull a value from the header data.""" | |
72 | return core.get_value(req.headers, name, field) | |
73 | ||
74 | def parse_files(self, req, name, field): | |
75 | """Pull a file from the request.""" | |
76 | files = ((k, v) for k, v in req.POST.items() if hasattr(v, "file")) | |
77 | return core.get_value(MultiDict(files), name, field) | |
78 | ||
79 | def parse_matchdict(self, req, name, field): | |
80 | """Pull a value from the request's `matchdict`.""" | |
81 | return core.get_value(req.matchdict, name, field) | |
82 | ||
83 | def handle_error(self, error, req, schema, error_status_code, error_headers): | |
84 | """Handles errors during parsing. Aborts the current HTTP request and | |
85 | responds with a 400 error. | |
86 | """ | |
87 | status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS | |
88 | response = exception_response( | |
89 | status_code, | |
90 | detail=text_type(error), | |
91 | headers=error_headers, | |
92 | content_type="application/json", | |
93 | ) | |
94 | body = json.dumps(error.messages) | |
95 | response.body = body.encode("utf-8") if isinstance(body, text_type) else body | |
96 | raise response | |
97 | ||
98 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
99 | messages = {"json": ["Invalid JSON body."]} | |
100 | response = exception_response( | |
101 | 400, detail=text_type(messages), content_type="application/json" | |
102 | ) | |
103 | body = json.dumps(messages) | |
104 | response.body = body.encode("utf-8") if isinstance(body, text_type) else body | |
105 | raise response | |
106 | ||
107 | def use_args( | |
108 | self, | |
109 | argmap, | |
110 | req=None, | |
111 | locations=core.Parser.DEFAULT_LOCATIONS, | |
112 | as_kwargs=False, | |
113 | validate=None, | |
114 | error_status_code=None, | |
115 | error_headers=None, | |
116 | ): | |
117 | """Decorator that injects parsed arguments into a view callable. | |
118 | Supports the *Class-based View* pattern where `request` is saved as an instance | |
119 | attribute on a view class. | |
120 | ||
121 | :param dict argmap: Either a `marshmallow.Schema`, a `dict` | |
122 | of argname -> `marshmallow.fields.Field` pairs, or a callable | |
123 | which accepts a request and returns a `marshmallow.Schema`. | |
124 | :param req: The request object to parse. Pulled off of the view by default. | |
125 | :param tuple locations: Where on the request to search for values. | |
126 | :param bool as_kwargs: Whether to insert arguments as keyword arguments. | |
127 | :param callable validate: Validation function that receives the dictionary | |
128 | of parsed arguments. If the function returns ``False``, the parser | |
129 | will raise a :exc:`ValidationError`. | |
130 | :param int error_status_code: Status code passed to error handler functions when | |
131 | a `ValidationError` is raised. | |
132 | :param dict error_headers: Headers passed to error handler functions when a | |
133 | a `ValidationError` is raised. | |
134 | """ | |
135 | locations = locations or self.locations | |
136 | # Optimization: If argmap is passed as a dictionary, we only need | |
137 | # to generate a Schema once | |
138 | if isinstance(argmap, collections.Mapping): | |
139 | argmap = core.dict2schema(argmap)() | |
140 | ||
141 | def decorator(func): | |
142 | @functools.wraps(func) | |
143 | def wrapper(obj, *args, **kwargs): | |
144 | # The first argument is either `self` or `request` | |
145 | try: # get self.request | |
146 | request = req or obj.request | |
147 | except AttributeError: # first arg is request | |
148 | request = obj | |
149 | # NOTE: At this point, argmap may be a Schema, callable, or dict | |
150 | parsed_args = self.parse( | |
151 | argmap, | |
152 | req=request, | |
153 | locations=locations, | |
154 | validate=validate, | |
155 | error_status_code=error_status_code, | |
156 | error_headers=error_headers, | |
157 | ) | |
158 | if as_kwargs: | |
159 | kwargs.update(parsed_args) | |
160 | return func(obj, *args, **kwargs) | |
161 | else: | |
162 | return func(obj, parsed_args, *args, **kwargs) | |
163 | ||
164 | wrapper.__wrapped__ = func | |
165 | return wrapper | |
166 | ||
167 | return decorator | |
168 | ||
169 | ||
170 | parser = PyramidParser() | |
171 | use_args = parser.use_args | |
172 | use_kwargs = parser.use_kwargs |
0 | # -*- coding: utf-8 -*- | |
1 | """Utilities for testing. Includes a base test class | |
2 | for testing parsers. | |
3 | ||
4 | .. warning:: | |
5 | ||
6 | Methods and functions in this module may change without | |
7 | warning and without a major version change. | |
8 | """ | |
9 | import pytest | |
10 | import webtest | |
11 | ||
12 | from webargs.core import json | |
13 | ||
14 | ||
15 | class CommonTestCase(object): | |
16 | """Base test class that defines test methods for common functionality across all | |
17 | parsers. Subclasses must define `create_app`, which returns a WSGI-like app. | |
18 | """ | |
19 | ||
20 | def create_app(self): | |
21 | """Return a WSGI app""" | |
22 | raise NotImplementedError("Must define create_app()") | |
23 | ||
24 | def create_testapp(self, app): | |
25 | return webtest.TestApp(app) | |
26 | ||
27 | def before_create_app(self): | |
28 | pass | |
29 | ||
30 | def after_create_app(self): | |
31 | pass | |
32 | ||
33 | @pytest.fixture(scope="class") | |
34 | def testapp(self): | |
35 | self.before_create_app() | |
36 | yield self.create_testapp(self.create_app()) | |
37 | self.after_create_app() | |
38 | ||
39 | def test_parse_querystring_args(self, testapp): | |
40 | assert testapp.get("/echo?name=Fred").json == {"name": "Fred"} | |
41 | ||
42 | def test_parse_querystring_with_query_location_specified(self, testapp): | |
43 | assert testapp.get("/echo_query?name=Steve").json == {"name": "Steve"} | |
44 | ||
45 | def test_parse_form(self, testapp): | |
46 | assert testapp.post("/echo", {"name": "Joe"}).json == {"name": "Joe"} | |
47 | ||
48 | def test_parse_json(self, testapp): | |
49 | assert testapp.post_json("/echo", {"name": "Fred"}).json == {"name": "Fred"} | |
50 | ||
51 | def test_parse_querystring_default(self, testapp): | |
52 | assert testapp.get("/echo").json == {"name": "World"} | |
53 | ||
54 | def test_parse_json_default(self, testapp): | |
55 | assert testapp.post_json("/echo", {}).json == {"name": "World"} | |
56 | ||
57 | def test_parse_json_with_charset(self, testapp): | |
58 | res = testapp.post( | |
59 | "/echo", | |
60 | json.dumps({"name": "Steve"}), | |
61 | content_type="application/json;charset=UTF-8", | |
62 | ) | |
63 | assert res.json == {"name": "Steve"} | |
64 | ||
65 | def test_parse_json_with_vendor_media_type(self, testapp): | |
66 | res = testapp.post( | |
67 | "/echo", | |
68 | json.dumps({"name": "Steve"}), | |
69 | content_type="application/vnd.api+json;charset=UTF-8", | |
70 | ) | |
71 | assert res.json == {"name": "Steve"} | |
72 | ||
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"} | |
84 | ||
85 | def test_parse_json_many_schema_invalid_input(self, testapp): | |
86 | res = testapp.post_json( | |
87 | "/echo_many_schema", [{"name": "a"}], expect_errors=True | |
88 | ) | |
89 | assert res.status_code == 422 | |
90 | ||
91 | def test_parse_json_many_schema(self, testapp): | |
92 | res = testapp.post_json("/echo_many_schema", [{"name": "Steve"}]).json | |
93 | assert res == [{"name": "Steve"}] | |
94 | ||
95 | def test_parse_json_many_schema_ignore_malformed_data(self, testapp): | |
96 | assert testapp.post_json("/echo_many_schema", {"extra": "data"}).json == [] | |
97 | ||
98 | def test_parsing_form_default(self, testapp): | |
99 | assert testapp.post("/echo", {}).json == {"name": "World"} | |
100 | ||
101 | def test_parse_querystring_multiple(self, testapp): | |
102 | expected = {"name": ["steve", "Loria"]} | |
103 | assert testapp.get("/echo_multi?name=steve&name=Loria").json == expected | |
104 | ||
105 | def test_parse_form_multiple(self, testapp): | |
106 | expected = {"name": ["steve", "Loria"]} | |
107 | assert ( | |
108 | testapp.post("/echo_multi", {"name": ["steve", "Loria"]}).json == expected | |
109 | ) | |
110 | ||
111 | def test_parse_json_list(self, testapp): | |
112 | expected = {"name": ["Steve"]} | |
113 | assert testapp.post_json("/echo_multi", {"name": "Steve"}).json == expected | |
114 | ||
115 | def test_parse_json_with_nonascii_chars(self, testapp): | |
116 | text = u"øˆƒ£ºº∆ƒˆ∆" | |
117 | assert testapp.post_json("/echo", {"name": text}).json == {"name": text} | |
118 | ||
119 | def test_validation_error_returns_422_response(self, testapp): | |
120 | res = testapp.post("/echo", {"name": "b"}, expect_errors=True) | |
121 | assert res.status_code == 422 | |
122 | ||
123 | def test_user_validation_error_returns_422_response_by_default(self, testapp): | |
124 | res = testapp.post_json("/error", {"text": "foo"}, expect_errors=True) | |
125 | assert res.status_code == 422 | |
126 | ||
127 | def test_use_args_decorator(self, testapp): | |
128 | assert testapp.get("/echo_use_args?name=Fred").json == {"name": "Fred"} | |
129 | ||
130 | def test_use_args_with_path_param(self, testapp): | |
131 | url = "/echo_use_args_with_path_param/foo" | |
132 | res = testapp.get(url + "?value=42") | |
133 | assert res.json == {"value": 42} | |
134 | ||
135 | def test_use_args_with_validation(self, testapp): | |
136 | result = testapp.post("/echo_use_args_validated", {"value": 43}) | |
137 | assert result.status_code == 200 | |
138 | result = testapp.post( | |
139 | "/echo_use_args_validated", {"value": 41}, expect_errors=True | |
140 | ) | |
141 | assert result.status_code == 422 | |
142 | ||
143 | def test_use_kwargs_decorator(self, testapp): | |
144 | assert testapp.get("/echo_use_kwargs?name=Fred").json == {"name": "Fred"} | |
145 | ||
146 | def test_use_kwargs_with_path_param(self, testapp): | |
147 | url = "/echo_use_kwargs_with_path_param/foo" | |
148 | res = testapp.get(url + "?value=42") | |
149 | assert res.json == {"value": 42} | |
150 | ||
151 | def test_parsing_headers(self, testapp): | |
152 | res = testapp.get("/echo_headers", headers={"name": "Fred"}) | |
153 | assert res.json == {"name": "Fred"} | |
154 | ||
155 | def test_parsing_cookies(self, testapp): | |
156 | testapp.set_cookie("name", "Steve") | |
157 | res = testapp.get("/echo_cookie") | |
158 | assert res.json == {"name": "Steve"} | |
159 | ||
160 | def test_parse_nested_json(self, testapp): | |
161 | res = testapp.post_json( | |
162 | "/echo_nested", {"name": {"first": "Steve", "last": "Loria"}} | |
163 | ) | |
164 | assert res.json == {"name": {"first": "Steve", "last": "Loria"}} | |
165 | ||
166 | def test_parse_nested_many_json(self, testapp): | |
167 | in_data = {"users": [{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}]} | |
168 | res = testapp.post_json("/echo_nested_many", in_data) | |
169 | assert res.json == in_data | |
170 | ||
171 | # Regression test for https://github.com/marshmallow-code/webargs/issues/120 | |
172 | def test_parse_nested_many_missing(self, testapp): | |
173 | in_data = {} | |
174 | res = testapp.post_json("/echo_nested_many", in_data) | |
175 | assert res.json == {} | |
176 | ||
177 | def test_parse_json_if_no_json(self, testapp): | |
178 | res = testapp.post("/echo") | |
179 | assert res.json == {"name": "World"} | |
180 | ||
181 | def test_parse_files(self, testapp): | |
182 | res = testapp.post( | |
183 | "/echo_file", {"myfile": webtest.Upload("README.rst", b"data")} | |
184 | ) | |
185 | assert res.json == {"myfile": "data"} | |
186 | ||
187 | # https://github.com/sloria/webargs/pull/297 | |
188 | def test_empty_json(self, testapp): | |
189 | res = testapp.post( | |
190 | "/echo", | |
191 | "", | |
192 | headers={"Accept": "application/json", "Content-Type": "application/json"}, | |
193 | ) | |
194 | assert res.status_code == 200 | |
195 | assert res.json == {"name": "World"} | |
196 | ||
197 | # https://github.com/sloria/webargs/issues/329 | |
198 | def test_invalid_json(self, testapp): | |
199 | res = testapp.post( | |
200 | "/echo", | |
201 | '{"foo": "bar", }', | |
202 | headers={"Accept": "application/json", "Content-Type": "application/json"}, | |
203 | expect_errors=True, | |
204 | ) | |
205 | assert res.status_code == 400 | |
206 | assert res.json == {"json": ["Invalid JSON body."]} |
0 | # -*- coding: utf-8 -*- | |
1 | """Tornado request argument parsing module. | |
2 | ||
3 | Example: :: | |
4 | ||
5 | import tornado.web | |
6 | from marshmallow import fields | |
7 | from webargs.tornadoparser import use_args | |
8 | ||
9 | class HelloHandler(tornado.web.RequestHandler): | |
10 | ||
11 | @use_args({'name': fields.Str(missing='World')}) | |
12 | def get(self, args): | |
13 | response = {'message': 'Hello {}'.format(args['name'])} | |
14 | self.write(response) | |
15 | """ | |
16 | import tornado.web | |
17 | from tornado.escape import _unicode | |
18 | ||
19 | from marshmallow.compat import basestring | |
20 | from webargs import core | |
21 | from webargs.core import json | |
22 | ||
23 | ||
24 | class HTTPError(tornado.web.HTTPError): | |
25 | """`tornado.web.HTTPError` that stores validation errors.""" | |
26 | ||
27 | def __init__(self, *args, **kwargs): | |
28 | self.messages = kwargs.pop("messages", {}) | |
29 | self.headers = kwargs.pop("headers", None) | |
30 | super(HTTPError, self).__init__(*args, **kwargs) | |
31 | ||
32 | ||
33 | def parse_json_body(req): | |
34 | """Return the decoded JSON body from the request.""" | |
35 | 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 {} | |
47 | ||
48 | ||
49 | # From tornado.web.RequestHandler.decode_argument | |
50 | def decode_argument(value, name=None): | |
51 | """Decodes an argument from the request. | |
52 | """ | |
53 | try: | |
54 | return _unicode(value) | |
55 | except UnicodeDecodeError: | |
56 | raise HTTPError(400, "Invalid unicode in %s: %r" % (name or "url", value[:40])) | |
57 | ||
58 | ||
59 | def get_value(d, name, field): | |
60 | """Handle gets from 'multidicts' made of lists | |
61 | ||
62 | It handles cases: ``{"key": [value]}`` and ``{"key": value}`` | |
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 | |
79 | ||
80 | ||
81 | class TornadoParser(core.Parser): | |
82 | """Tornado request argument parser.""" | |
83 | ||
84 | def __init__(self, *args, **kwargs): | |
85 | super(TornadoParser, self).__init__(*args, **kwargs) | |
86 | self.json = None | |
87 | ||
88 | def parse_json(self, req, name, field): | |
89 | """Pull a json value from the request.""" | |
90 | json_data = self._cache.get("json") | |
91 | if json_data is None: | |
92 | try: | |
93 | self._cache["json"] = json_data = parse_json_body(req) | |
94 | except json.JSONDecodeError as e: | |
95 | return self.handle_invalid_json_error(e, req) | |
96 | if json_data is None: | |
97 | return core.missing | |
98 | return core.get_value(json_data, name, field, allow_many_nested=True) | |
99 | ||
100 | def parse_querystring(self, req, name, field): | |
101 | """Pull a querystring value from the request.""" | |
102 | return get_value(req.query_arguments, name, field) | |
103 | ||
104 | def parse_form(self, req, name, field): | |
105 | """Pull a form value from the request.""" | |
106 | return get_value(req.body_arguments, name, field) | |
107 | ||
108 | def parse_headers(self, req, name, field): | |
109 | """Pull a value from the header data.""" | |
110 | return get_value(req.headers, name, field) | |
111 | ||
112 | def parse_cookies(self, req, name, field): | |
113 | """Pull a value from the header data.""" | |
114 | cookie = req.cookies.get(name) | |
115 | ||
116 | if cookie is not None: | |
117 | return [cookie.value] if core.is_multiple(field) else cookie.value | |
118 | else: | |
119 | return [] if core.is_multiple(field) else None | |
120 | ||
121 | def parse_files(self, req, name, field): | |
122 | """Pull a file from the request.""" | |
123 | return get_value(req.files, name, field) | |
124 | ||
125 | def handle_error(self, error, req, schema, error_status_code, error_headers): | |
126 | """Handles errors during parsing. Raises a `tornado.web.HTTPError` | |
127 | with a 400 error. | |
128 | """ | |
129 | status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS | |
130 | if status_code == 422: | |
131 | reason = "Unprocessable Entity" | |
132 | else: | |
133 | reason = None | |
134 | raise HTTPError( | |
135 | status_code, | |
136 | log_message=str(error.messages), | |
137 | reason=reason, | |
138 | messages=error.messages, | |
139 | headers=error_headers, | |
140 | ) | |
141 | ||
142 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
143 | raise HTTPError( | |
144 | 400, | |
145 | log_message="Invalid JSON body.", | |
146 | reason="Bad Request", | |
147 | messages={"json": ["Invalid JSON body."]}, | |
148 | ) | |
149 | ||
150 | def get_request_from_view_args(self, view, args, kwargs): | |
151 | return args[0].request | |
152 | ||
153 | ||
154 | parser = TornadoParser() | |
155 | use_args = parser.use_args | |
156 | use_kwargs = parser.use_kwargs |
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 | try: | |
44 | self._cache["json"] = json_data = core.parse_json(req.body) | |
45 | except json.JSONDecodeError as e: | |
46 | if e.doc == "": | |
47 | return core.missing | |
48 | else: | |
49 | raise | |
50 | return core.get_value(json_data, name, field, allow_many_nested=True) | |
51 | ||
52 | def parse_querystring(self, req, name, field): | |
53 | """Pull a querystring value from the request.""" | |
54 | return core.get_value(req.GET, name, field) | |
55 | ||
56 | def parse_form(self, req, name, field): | |
57 | """Pull a form value from the request.""" | |
58 | return core.get_value(req.POST, name, field) | |
59 | ||
60 | def parse_cookies(self, req, name, field): | |
61 | """Pull the value from the cookiejar.""" | |
62 | return core.get_value(req.cookies, name, field) | |
63 | ||
64 | def parse_headers(self, req, name, field): | |
65 | """Pull a value from the header data.""" | |
66 | return core.get_value(req.headers, name, field) | |
67 | ||
68 | def parse_files(self, req, name, field): | |
69 | """Pull a file from the request.""" | |
70 | files = ((k, v) for k, v in req.POST.items() if hasattr(v, "file")) | |
71 | return core.get_value(webob.multidict.MultiDict(files), name, field) | |
72 | ||
73 | def get_default_request(self): | |
74 | return webapp2.get_request() | |
75 | ||
76 | ||
77 | parser = Webapp2Parser() | |
78 | use_args = parser.use_args | |
79 | use_kwargs = parser.use_kwargs |