Codebase list python-webargs / cdefc4f
New upstream version 6.1.0 Sophie Brun 4 years ago
77 changed file(s) with 4608 addition(s) and 3285 deletion(s). Raw diff Collapse all Expand all
0 open_collective: "marshmallow"
1 tidelift: "pypi/webargs"
00 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
38 hooks:
49 - id: black
5 language_version: python3.6
10 args: ["--target-version", "py35"]
11 language_version: python3
612 - repo: https://gitlab.com/pycqa/flake8
7 rev: 3.7.4
13 rev: 3.7.9
814 hooks:
915 - id: flake8
10 additional_dependencies: ["flake8-bugbear==18.8.0"]
16 additional_dependencies: [flake8-bugbear==20.1.0]
1117 - repo: https://github.com/asottile/blacken-docs
12 rev: v0.3.0
18 rev: v1.5.0-1
1319 hooks:
1420 - id: blacken-docs
15 additional_dependencies: [black==18.9b0]
21 additional_dependencies: [black==19.10b0]
22 args: ["--target-version", "py35"]
1623 - repo: https://github.com/pre-commit/mirrors-mypy
17 rev: v0.660
24 rev: v0.761
1825 hooks:
1926 - 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
24 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
-51
.travis.yml less more
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=
44 Lead
55 ----
66
7 * Steven Loria <[email protected]>
8 * Jérôme Lafréchoux <https://github.com/lafrech>
7 * Steven Loria `@sloria <https://github.com/sloria>`_
8 * Jérôme Lafréchoux `@lafrech <https://github.com/lafrech>`_
99
1010 Contributors (chronological)
1111 ----------------------------
1212
13 * @venuatu <https://github.com/venuatu>
14 * Javier Santacruz @jvrsantacruz <[email protected]>
15 * Josh Carp <https://github.com/jmcarp>
16 * @philtay <https://github.com/philtay>
17 * Andriy Yurchuk <https://github.com/Ch00k>
18 * Stas Sușcov <https://github.com/stas>
19 * Josh Johnston <https://github.com/Trii>
20 * Rory Hart <https://github.com/hartror>
21 * Jace Browning <https://github.com/jacebrowning>
22 * @marcellarius <https://github.com/marcellarius>
23 * Damian Heard <https://github.com/DamianHeard>
24 * Daniel Imhoff <https://github.com/dwieeb>
25 * immerrr <https://github.com/immerrr>
26 * Brett Higgins <https://github.com/brettdh>
27 * Vlad Frolov <https://github.com/frol>
28 * Tuukka Mustonen <https://github.com/tuukkamustonen>
29 * Francois-Xavier Darveau <https://github.com/EFF>
30 * Jérôme Lafréchoux <https://github.com/lafrech>
31 * @DmitriyS <https://github.com/DmitriyS>
32 * Svetlozar Argirov <https://github.com/zaro>
33 * Florian S. <https://github.com/nebularazer>
34 * @daniel98321 <https://github.com/daniel98321>
35 * @Itayazolay <https://github.com/Itayazolay>
36 * @Reskov <https://github.com/Reskov>
37 * @cedzz <https://github.com/cedzz>
38 * F. Moukayed (כוכב) <https://github.com/kochab>
39 * Xiaoyu Lee <https://github.com/lee3164>
40 * Jonathan Angelo <https://github.com/jangelo>
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>`_
00 Changelog
11 ---------
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.
2240
3241 5.1.2 (2019-02-03)
4242 ******************
1818 Contributing Code
1919 -----------------
2020
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
3421 Integration with a Another Web Framework…
35 *****************************************
22 +++++++++++++++++++++++++++++++++++++++++
3623
3724 …should be released as a separate package.
3825
4431 the `GitHub wiki <https://github.com/marshmallow-code/webargs/wiki/Ecosystem>`_ .
4532
4633 Setting Up for Local Development
47 ********************************
34 ++++++++++++++++++++++++++++++++
4835
49 1. Fork webargs_ on GitHub.
36 1. Fork webargs_ on GitHub.
5037
5138 ::
5239
6249 # After activating your virtualenv
6350 $ pip install -e '.[dev]'
6451
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.
6653
6754 ::
6855
7158
7259 .. note::
7360
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.
7563
7664 Git Branch Structure
77 ********************
65 ++++++++++++++++++++
7866
7967 Webargs abides by the following branching model:
8068
8876 **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.
8977
9078 Pull Requests
91 **************
79 ++++++++++++++
9280
9381 1. Create a new local branch.
9482
112100 - If the pull request adds functionality, it is tested and the docs are updated.
113101 - You've added yourself to ``AUTHORS.rst``.
114102
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.
116105
117106 Running Tests
118 *************
107 +++++++++++++
119108
120109 To run all tests: ::
121110
125114
126115 $ tox -e lint
127116
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): ::
129118
130119 $ tox
131120
132121 Documentation
133 *************
122 +++++++++++++
134123
135124 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_.
136125
141130 Changes in the `docs/` directory will automatically trigger a rebuild.
142131
143132 Contributing Examples
144 *********************
133 +++++++++++++++++++++
145134
146135 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.
147136
0 Copyright 2014-2019 Steven Loria and contributors
0 Copyright 2014-2020 Steven Loria and contributors
11
22 Permission is hereby granted, free of charge, to any person obtaining a copy
33 of this software and associated documentation files (the "Software"), to deal
0 graft tests
01 include LICENSE
12 include *.rst
3 include tox.ini
55 :target: https://pypi.org/project/webargs/
66 :alt: PyPI version
77
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
1111
1212 .. image:: https://readthedocs.org/projects/webargs/badge/
1313 :target: https://webargs.readthedocs.io/
3333
3434 app = Flask(__name__)
3535
36 hello_args = {"name": fields.Str(required=True)}
37
3836
3937 @app.route("/")
40 @use_args(hello_args)
38 @use_args({"name": fields.Str(required=True)}, location="query")
4139 def index(args):
4240 return "Hello " + args["name"]
4341
5553
5654 pip install -U webargs
5755
58 webargs supports Python >= 2.7 or >= 3.5.
56 webargs supports Python >= 3.5.
5957
6058
6159 Documentation
104102
105103 - Docs: https://webargs.readthedocs.io/
106104 - Changelog: https://webargs.readthedocs.io/en/latest/changelog.html
105 - Contributing Guidelines: https://webargs.readthedocs.io/en/latest/contributing.html
107106 - PyPI: https://pypi.python.org/pypi/webargs
108107 - Issues: https://github.com/marshmallow-code/webargs/issues
109108
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
55 Custom Location Handlers
66 ------------------------
77
8 To add your own custom location handler, write a function that receives a request, an argument name, and a :class:`Field <marshmallow.fields.Field>`, then decorate that function with :func:`Parser.location_handler <webargs.core.Parser.location_handler>`.
8 To add your own custom location handler, write a function that receives a request, and a :class:`Schema <marshmallow.Schema>`, then decorate that function with :func:`Parser.location_loader <webargs.core.Parser.location_loader>`.
99
1010
1111 .. code-block:: python
1414 from webargs.flaskparser import parser
1515
1616
17 @parser.location_handler("data")
18 def parse_data(request, name, field):
19 return request.data.get(name)
17 @parser.location_loader("data")
18 def load_data(request, schema):
19 return request.data
2020
2121
2222 # Now 'data' can be specified as a location
23 @parser.use_args({"per_page": fields.Int()}, locations=("data",))
23 @parser.use_args({"per_page": fields.Int()}, location="data")
2424 def posts(args):
2525 return "displaying {} posts".format(args["per_page"])
2626
2727
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
2990 -----------------------
3091
3192 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>`.
45106 last_name = fields.Str(missing="")
46107 date_registered = fields.DateTime(dump_only=True)
47108
48 class Meta:
49 strict = True
109 # NOTE: Uncomment below two lines if you're using marshmallow 2
110 # class Meta:
111 # strict = True
50112
51113
52114 @use_args(UserSchema())
62124
63125
64126 # 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")
66128 @use_args(UserSchema())
67129 def profile_posts(args, posts_per_page):
68130 username = args["username"]
71133 .. warning::
72134 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.
73135
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.
76172
77173
78174 Schema Factories
87183
88184 .. code-block:: python
89185
186 from flask import Flask
90187 from marshmallow import Schema, fields
91188 from webargs.flaskparser import use_args
189
190 app = Flask(__name__)
92191
93192
94193 class UserSchema(Schema):
99198 last_name = fields.Str(missing="")
100199 date_registered = fields.DateTime(dump_only=True)
101200
102 class Meta:
103 strict = True
104
105201
106202 def make_user_schema(request):
107203 # 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
109206 # Respect partial updates for PATCH requests
110207 partial = request.method == "PATCH"
111208 # Add current request to the schema's context
113210
114211
115212 # Pass the factory to .parse, .use_args, or .use_kwargs
213 @app.route("/profile/", methods=["GET", "POST", "PATCH"])
116214 @use_args(make_user_schema)
117215 def profile_view(args):
118 username = args["username"]
119 # ...
216 username = args.get("username")
217 # ...
218
120219
121220
122221 Reducing Boilerplate
137236 only = request.args.get("fields", None)
138237 # Respect partial updates for PATCH requests
139238 partial = request.method == "PATCH"
140 # Add current request to the schema's context
141 # and ensure we're always using strict mode
142239 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
148241 )
149242
150243 return use_args(factory, **kwargs)
178271 cube = args["cube"]
179272 # ...
180273
181 .. _custom-parsers:
274 .. _custom-loaders:
182275
183276 Custom Parsers
184277 --------------
185278
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.
187280
188281
189282 .. code-block:: python
212305 }
213306 """
214307
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)
217310
218311
219312 def _structure_dict(dict_):
274367 path = fields.Str(required=True)
275368 value = fields.Str(required=True)
276369
277 class Meta:
278 strict = True
279
280370
281371 @app.route("/profile/", methods=["patch"])
282 @use_args(PatchSchema(many=True), locations=("json",))
372 @use_args(PatchSchema(many=True))
283373 def patch_blog(args):
284374 """Implements JSON Patch for the user profile
285375
294384 Mixing Locations
295385 ----------------
296386
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
301392 @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")
339395 def viewfunc(query_parsed, json_parsed):
340396 page = query_parsed["page"]
341397 name = json_parsed["name"]
347403
348404 import functools
349405
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()})
356412 def viewfunc(query_parsed, json_parsed):
357413 page = query_parsed["page"]
358414 name = json_parsed["name"]
1414
1515 .. automodule:: webargs.fields
1616 :members: Nested, DelimitedList
17
18
19 webargs.multidictproxy
20 ----------------------
21
22 .. automodule:: webargs.multidictproxy
23 :members:
24
1725
1826 webargs.asyncparser
1927 -------------------
0 # -*- coding: utf-8 -*-
10 import datetime as dt
21 import sys
32 import os
65 # If extensions (or modules to document with autodoc) are in another directory,
76 # add these directories to sys.path here. If the directory is relative to the
87 # 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")))
109 import webargs # noqa
1110
1211 extensions = [
3635
3736 html_domain_indices = False
3837 source_suffix = ".rst"
39 project = u"webargs"
40 copyright = u"2014-{0:%Y}, Steven Loria and contributors".format(dt.datetime.utcnow())
38 project = "webargs"
39 copyright = "2014-{:%Y}, Steven Loria and contributors".format(dt.datetime.utcnow())
4140 version = release = webargs.__version__
4241 templates_path = ["_templates"]
4342 exclude_patterns = ["_build"]
2121
2222
2323 @app.route("/user/<int:uid>")
24 @use_args({"per_page": fields.Int()})
24 @use_args({"per_page": fields.Int()}, location="query")
2525 def user_detail(args, uid):
26 return ("The user page for user {uid}, " "showing {per_page} posts.").format(
26 return ("The user page for user {uid}, showing {per_page} posts.").format(
2727 uid=uid, per_page=args["per_page"]
2828 )
2929
6363
6464
6565 @app.route("/greeting/<name>/")
66 @use_args({"name": fields.Str(location="view_args")})
66 @use_args({"name": fields.Str()}, location="view_args")
6767 def greeting(args, **kwargs):
6868 return "Hello {}".format(args["name"])
6969
9494 }
9595
9696
97 @use_args(account_args)
97 @use_args(account_args, location="form")
9898 def login_user(request, args):
9999 if request.method == "POST":
100100 login(args["username"], args["password"])
113113
114114
115115 class BlogPostView(View):
116 @use_args(blog_args)
116 @use_args(blog_args, location="query")
117117 def get(self, request, args):
118118 blog_post = Post.objects.get(title__iexact=args["title"], author=args["author"])
119119 return render_to_response("post_template.html", {"post": blog_post})
238238 from webargs.pyramidparser import use_args
239239
240240
241 @use_args({"uid": fields.Str(), "per_page": fields.Int()})
241 @use_args({"uid": fields.Str(), "per_page": fields.Int()}, location="query")
242242 def user_detail(request, args):
243243 uid = args["uid"]
244244 return Response(
260260 from webargs.pyramidparser import use_args
261261
262262
263 @use_args({"mymatch": fields.Int()}, locations=("matchdict",))
263 @use_args({"mymatch": fields.Int()}, location="matchdict")
264264 def matched(request, args):
265265 return Response("The value for mymatch is {}".format(args["mymatch"]))
266266
309309
310310
311311 def add_args(argmap, **kwargs):
312 def hook(req, resp, params):
312 def hook(req, resp, resource, params):
313313 parsed_args = parser.parse(argmap, req=req, **kwargs)
314314 req.context["args"] = parsed_args
315315
316316 return hook
317317
318318
319 @falcon.before(add_args({"page": fields.Int(location="query")}))
319 @falcon.before(add_args({"page": fields.Int()}, location="query"))
320320 class AuthorResource:
321321 def on_get(self, req, resp):
322322 args = req.context["args"]
413413 from webargs.aiohttpparser import use_args
414414
415415
416 @parser.use_args({"slug": fields.Str(location="match_info")})
416 @parser.use_args({"slug": fields.Str()}, location="match_info")
417417 def article_detail(request, args):
418418 return web.Response(body="Slug: {}".format(args["slug"]).encode("utf-8"))
419419
55
66 webargs is a Python library for parsing and validating HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, webapp2, Falcon, and aiohttp.
77
8 Upgrading from an older version?
9 --------------------------------
10
11 See the :doc:`Upgrading to Newer Releases <upgrading>` page for notes on getting your code up-to-date with the latest version.
12
13
14 Usage and Simple Examples
15 -------------------------
816
917 .. code-block:: python
1018
1422
1523 app = Flask(__name__)
1624
17 hello_args = {"name": fields.Str(required=True)}
18
1925
2026 @app.route("/")
21 @use_args(hello_args)
27 @use_args({"name": fields.Str(required=True)}, location="query")
2228 def index(args):
2329 return "Hello " + args["name"]
2430
2935 # curl http://localhost:5000/\?name\='World'
3036 # Hello World
3137
32 Webargs will automatically parse:
38 By default Webargs will automatically parse JSON request bodies. But it also
39 has support for:
3340
3441 **Query Parameters**
3542 ::
43 $ curl http://localhost:5000/\?name\='Freddie'
44 Hello Freddie
3645
37 $ curl http://localhost:5000/\?name\='Freddie'
38 Hello Freddie
46 # pass location="query" to use_args
3947
4048 **Form Data**
4149 ::
4351 $ curl -d 'name=Brian' http://localhost:5000/
4452 Hello Brian
4553
54 # pass location="form" to use_args
55
4656 **JSON Data**
4757 ::
4858
4959 $ curl -X POST -H "Content-Type: application/json" -d '{"name":"Roger"}' http://localhost:5000/
5060 Hello Roger
61
62 # pass location="json" (or omit location) to use_args
5163
5264 and, optionally:
5365
104116
105117 license
106118 changelog
119 upgrading
107120 authors
108121 contributing
00 Install
11 =======
22
3 **webargs** requires Python >= 2.7 or >= 3.5. It depends on `marshmallow <https://marshmallow.readthedocs.io/en/latest/>`_ >= 2.7.0.
3 **webargs** requires Python >= 3.5. It depends on `marshmallow <https://marshmallow.readthedocs.io/en/latest/>`_ >= 2.7.0.
44
55 From the PyPI
66 -------------
2222 "nickname": fields.List(fields.Str()),
2323 # Delimited list, e.g. "/?languages=python,javascript"
2424 "languages": fields.DelimitedList(fields.Str()),
25 # When you know where an argument should be parsed from
26 "active": fields.Bool(location="query"),
2725 # When value is keyed on a variable-unsafe name
2826 # or you want to rename a key
29 "content_type": fields.Str(load_from="Content-Type", location="headers"),
27 "user_type": fields.Str(load_from="user-type"),
3028 # OR, on marshmallow 3
31 # "content_type": fields.Str(data_key="Content-Type", location="headers"),
32 # File uploads
33 "profile_image": fields.Field(
34 location="files", validate=lambda f: f.mimetype in ["image/jpeg", "image/png"]
35 ),
29 # "user_type": fields.Str(data_key="user-type"),
3630 }
3731
3832 .. note::
10498 Request "Locations"
10599 -------------------
106100
107 By default, webargs will search for arguments from the URL query string (e.g. ``"/?name=foo"``), form data, and JSON data (in that order). You can explicitly specify which locations to search, like so:
101 By default, webargs will search for arguments from the request body as JSON. You can specify a different location from which to load data like so:
108102
109103 .. code-block:: python
110104
111105 @app.route("/register")
112 @use_args(user_args, locations=("json", "form"))
106 @use_args(user_args, location="form")
113107 def register(args):
114108 return "registration page"
115109
146140 raise ValidationError("User does not exist")
147141
148142
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 }
158162
159163 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>`.
160164
191195
192196
193197 @parser.error_handler
194 def handle_error(error, req, schema, status_code, headers):
198 def handle_error(error, req, schema, *, status_code, headers):
195199 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())}
196221
197222 Nesting Fields
198223 --------------
211236
212237 .. note::
213238
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.
215240
216241 Next Steps
217242 ----------
+0
-4
docs/requirements.txt less more
0 -e '.[frameworks]'
1 Sphinx==1.8.3
2 sphinx-issues==1.2.0
3 sphinx-typlog-theme==0.7.1
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 ...
00 """Example of using Python 3 function annotations to define
11 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
214 """
3 import datetime as dt
15 import random
416 import functools
517
6 from flask import Flask, jsonify, request
18 from flask import Flask, request
719 from marshmallow import Schema
820 from webargs import fields
921 from webargs.flaskparser import parser
1022
23
1124 app = Flask(__name__)
1225
26 ##### Routing wrapper ####
1327
14 def route(*args, response_formatter=jsonify, **kwargs):
28
29 def route(*args, **kwargs):
1530 """Combines `Flask.route` and webargs parsing. Allows arguments to be specified
1631 as function annotations. An output schema can optionally be specified by a
1732 return annotation.
2843 if isinstance(value, fields.Field) and name != "return"
2944 }
3045 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)
3249 kw.update(parsed)
3350 response_data = func(*a, **kw)
3451 if response_schema:
35 return response_formatter(response_schema.dump(response_data).data)
52 return response_schema.dump(response_data)
3653 else:
37 return response_formatter(func(*a, **kw))
54 return func(*a, **kw)
3855
3956 return wrapped_view
4057
4158 return decorator
4259
4360
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
4497 @route("/", methods=["GET"])
45 def index(name: fields.Str(missing="Friend")): # noqa: E252
98 def index(name: fields.Str(missing="Friend")):
4699 return {"message": "Hello, {}!".format(name)}
47100
48101
49102 @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)):
51104 return {"result": x + y}
52105
53106
54107 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()
58112
59113
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)
70121 return user
71122
72123
77128 headers = err.data.get("headers", None)
78129 messages = err.data.get("messages", ["Invalid request."])
79130 if headers:
80 return jsonify({"errors": messages}), err.code, headers
131 return {"errors": messages}, err.code, headers
81132 else:
82 return jsonify({"errors": messages}), err.code
133 return {"errors": messages}, err.code
83134
84135
85136 if __name__ == "__main__":
137 User.insert(
138 db=db, id=42, username="fred", first_name="Freddie", last_name="Mercury"
139 )
86140 app.run(port=5001, debug=True)
2626 ### Middleware and hooks ###
2727
2828
29 class JSONTranslator(object):
29 class JSONTranslator:
3030 def process_response(self, req, resp, resource):
3131 if "result" not in req.context:
3232 return
4343 ### Resources ###
4444
4545
46 class HelloResource(object):
46 class HelloResource:
4747 """A welcome page."""
4848
4949 hello_args = {"name": fields.Str(missing="Friend", location="query")}
5353 req.context["result"] = {"message": "Welcome, {}!".format(args["name"])}
5454
5555
56 class AdderResource(object):
56 class AdderResource:
5757 """An addition endpoint."""
5858
5959 adder_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)}
6363 req.context["result"] = {"result": x + y}
6464
6565
66 class DateAddResource(object):
66 class DateAddResource:
6767 """A datetime adder endpoint."""
6868
6969 dateadd_args = {
0 # -*- coding: utf-8 -*-
10 """A simple number and datetime addition JSON API.
21 Run the app:
32
6968
7069 # This error handler is necessary for usage with Flask-RESTful
7170 @parser.error_handler
72 def handle_request_parsing_error(err, req, schema, error_status_code, error_headers):
71 def handle_request_parsing_error(err, req, schema, *, error_status_code, error_headers):
7372 """webargs error handler that uses Flask-RESTful's abort function to return
7473 a JSON error response to the client.
7574 """
0 python-dateutil==2.7.5
0 python-dateutil==2.8.1
11 Flask
22 bottle
33 tornado
88 $ pip install httpie
99 $ http GET :5001/users/
1010 $ 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
1212 $ http PATCH :5001/users/42 username=freddie
1313 $ http GET :5001/users/ limit==1
1414 """
1515 import functools
16 from flask import Flask, request, jsonify
16 from flask import Flask, request
1717 import random
1818
1919 from marshmallow import Schema, fields, post_dump
2121
2222 app = Flask(__name__)
2323
24 ##### Fake database and models #####
24 ##### Fake database and model #####
2525
2626
2727 class Model:
5858 ##### use_schema #####
5959
6060
61 def use_schema(schema, list_view=False, locations=None):
61 def use_schema(schema_cls, list_view=False, locations=None):
6262 """View decorator for using a marshmallow schema to
6363 (1) parse a request's input and
6464 (2) serializing the view's output to a JSON response.
6767 def decorator(func):
6868 @functools.wraps(func)
6969 def wrapped(*args, **kwargs):
70 partial = request.method != "POST"
71 schema = schema_cls(partial=partial)
7072 use_args_wrapper = parser.use_args(schema, locations=locations)
7173 # Function wrapped with use_args
7274 func_with_args = use_args_wrapper(func)
7375 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)
7677
7778 return wrapped
7879
8485
8586 class UserSchema(Schema):
8687 id = fields.Int(dump_only=True)
87 username = fields.Str()
88 username = fields.Str(required=True)
8889 first_name = fields.Str()
8990 last_name = fields.Str()
9091
91 class Meta:
92 strict = True
93
9492 @post_dump(pass_many=True)
95 def wrap_with_envelope(self, data, many):
93 def wrap_with_envelope(self, data, many, **kwargs):
9694 return {"data": data}
9795
9896
10098
10199
102100 @app.route("/users/<int:user_id>", methods=["GET", "PATCH"])
103 @use_schema(UserSchema())
101 @use_schema(UserSchema)
104102 def user_detail(reqargs, user_id):
105103 user = db["users"].get(user_id)
106104 if not user:
107 return jsonify({"message": "User not found"}), 404
105 return {"message": "User not found"}, 404
108106 if request.method == "PATCH" and reqargs:
109107 user.update(**reqargs)
110108 return user
113111 # You can add additional arguments with use_kwargs
114112 @app.route("/users/", methods=["GET", "POST"])
115113 @use_kwargs({"limit": fields.Int(missing=10, location="query")})
116 @use_schema(UserSchema(), list_view=True)
114 @use_schema(UserSchema, list_view=True)
117115 def user_list(reqargs, limit):
118116 users = db["users"].values()
119117 if request.method == "POST":
133131 headers = None
134132 messages = ["Invalid request."]
135133 if headers:
136 return jsonify({"errors": messages}), err.code, headers
134 return {"errors": messages}, err.code, headers
137135 else:
138 return jsonify({"errors": messages}), err.code
136 return {"errors": messages}, err.code
139137
140138
141139 if __name__ == "__main__":
00 #!/usr/bin/env python
1 # -*- coding: utf-8 -*-
21 """A Hello, World! example using Webapp2 in a Google App Engine environment
32
43 Run the app:
11 license_files = LICENSE
22
33 [bdist_wheel]
4 # We build separate wheels for
5 # Python 2 and 3 because of the conditional
6 # dependency on simplejson
7 universal = 0
4 universal = 1
85
96 [flake8]
107 ignore = E203, E266, E501, W503
0 # -*- coding: utf-8 -*-
1 import sys
20 import re
31 from setuptools import setup, find_packages
4
5 INSTALL_REQUIRES = ["marshmallow>=2.15.2"]
6 if sys.version_info[0] < 3:
7 INSTALL_REQUIRES.append("simplejson>=2.1.0")
82
93 FRAMEWORKS = [
104 "Flask>=0.12.2",
137 "tornado>=4.5.2",
148 "pyramid>=1.9.1",
159 "webapp2>=3.0.0b1",
16 "falcon>=1.4.0",
17 'aiohttp>=3.0.0; python_version >= "3.5"',
10 "falcon>=2.0.0",
11 "aiohttp>=3.0.0",
1812 ]
1913 EXTRAS_REQUIRE = {
2014 "frameworks": FRAMEWORKS,
2115 "tests": [
2216 "pytest",
23 "mock",
24 "webtest==2.0.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",
2721 ]
2822 + FRAMEWORKS,
2923 "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",
3428 ],
29 "docs": ["Sphinx==3.0.3", "sphinx-issues==1.2.0", "sphinx-typlog-theme==0.8.0"]
30 + FRAMEWORKS,
3531 }
3632 EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"]
3733
6157
6258 setup(
6359 name="webargs",
64 version=find_version("webargs/__init__.py"),
60 version=find_version("src/webargs/__init__.py"),
6561 description=(
6662 "Declarative parsing and validation of HTTP request objects, "
6763 "with built-in support for popular web frameworks, including "
7167 author="Steven Loria",
7268 author_email="[email protected]",
7369 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"],
7773 extras_require=EXTRAS_REQUIRE,
7874 license="MIT",
7975 zip_safe=False,
9490 "api",
9591 "marshmallow",
9692 ),
93 python_requires=">=3.5",
9794 classifiers=[
9895 "Development Status :: 5 - Production/Stable",
9996 "Intended Audience :: Developers",
10097 "License :: OSI Approved :: MIT License",
10198 "Natural Language :: English",
102 "Programming Language :: Python :: 2",
103 "Programming Language :: Python :: 2.7",
10499 "Programming Language :: Python :: 3",
105100 "Programming Language :: Python :: 3.5",
106101 "Programming Language :: Python :: 3.6",
107102 "Programming Language :: Python :: 3.7",
103 "Programming Language :: Python :: 3.8",
104 "Programming Language :: Python :: 3 :: Only",
108105 "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
109106 "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
110107 ],
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 -*-
11
22 import aiohttp
33 from aiohttp.web import json_response
4 from aiohttp import web
54 import marshmallow as ma
65
76 from webargs import fields
2423 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
2524 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
2625
26 # variant which ignores unknown fields
27 exclude_kwargs = (
28 {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE}
29 )
30 hello_exclude_schema = HelloSchema(**exclude_kwargs)
31
32
2733 ##### Handlers #####
2834
2935
3036 async def echo(request):
37 parsed = await parser.parse(hello_args, request, location="query")
38 return json_response(parsed)
39
40
41 async def echo_form(request):
42 parsed = await parser.parse(hello_args, request, location="form")
43 return json_response(parsed)
44
45
46 async def echo_json(request):
3147 try:
32 parsed = await parser.parse(hello_args, request)
48 parsed = await parser.parse(hello_args, request, location="json")
3349 except json.JSONDecodeError:
34 raise web.HTTPBadRequest(
50 raise aiohttp.web.HTTPBadRequest(
3551 body=json.dumps(["Invalid JSON."]).encode("utf-8"),
3652 content_type="application/json",
3753 )
3854 return json_response(parsed)
3955
4056
41 async def echo_query(request):
42 parsed = await parser.parse(hello_args, request, locations=("query",))
43 return json_response(parsed)
44
45
46 @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")
4769 async def echo_use_args(request, args):
4870 return json_response(args)
4971
5072
51 @use_kwargs(hello_args)
73 @use_kwargs(hello_args, location="query")
5274 async def echo_use_kwargs(request, name):
5375 return json_response({"name": name})
5476
5577
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 )
5781 async def echo_use_args_validated(request, args):
5882 return json_response(args)
5983
6084
85 async def echo_ignoring_extra_data(request):
86 return json_response(await parser.parse(hello_exclude_schema, request))
87
88
6189 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):
62100 parsed = await parser.parse(hello_multiple, request)
63101 return json_response(parsed)
64102
65103
66104 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")
72110 async def echo_use_args_with_path_param(request, args):
73111 return json_response(args)
74112
75113
76 @use_kwargs({"value": fields.Int()})
114 @use_kwargs({"value": fields.Int()}, location="query")
77115 async def echo_use_kwargs_with_path_param(request, value):
78116 return json_response({"value": value})
79117
80118
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()})
83121 async def echo_use_args_multiple(request, query_parsed, json_parsed):
84122 return json_response({"query_parsed": query_parsed, "json_parsed": json_parsed})
85123
94132
95133
96134 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")
98136 return json_response(parsed)
99137
100138
101139 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")
103141 return json_response(parsed)
104142
105143
133171
134172
135173 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 )
137177 return json_response(parsed)
138178
139179
140180 class EchoHandler:
141 @use_args(hello_args)
181 @use_args(hello_args, location="query")
142182 async def get(self, request, args):
143183 return json_response(args)
144184
145185
146 class EchoHandlerView(web.View):
186 class EchoHandlerView(aiohttp.web.View):
147187 @asyncio.coroutine
148 @use_args(hello_args)
188 @use_args(hello_args, location="query")
149189 def get(self, args):
150190 return json_response(args)
151191
152192
153193 @asyncio.coroutine
154 @use_args(HelloSchema, as_kwargs=True)
194 @use_args(HelloSchema, as_kwargs=True, location="query")
155195 def echo_use_schema_as_kwargs(request, name):
156196 return json_response({"name": name})
157197
167207 def create_app():
168208 app = aiohttp.web.Application()
169209
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)
176221 add_route(app, ["GET", "POST"], "/echo_many_schema", echo_many_schema)
177222 add_route(
178223 app,
0 from webargs.core import json
10 from bottle import Bottle, HTTPResponse, debug, request, response
21
32 import marshmallow as ma
43 from webargs import fields
54 from webargs.bottleparser import parser, use_args, use_kwargs
6 from webargs.core import MARSHMALLOW_VERSION_INFO
5 from webargs.core import json, MARSHMALLOW_VERSION_INFO
6
77
88 hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)}
99 hello_multiple = {"name": fields.List(fields.Str())}
1616 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
1717 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
1818
19 # variant which ignores unknown fields
20 exclude_kwargs = (
21 {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE}
22 )
23 hello_exclude_schema = HelloSchema(**exclude_kwargs)
24
1925
2026 app = Bottle()
2127 debug(True)
2228
2329
24 @app.route("/echo", method=["GET", "POST"])
30 @app.route("/echo", method=["GET"])
2531 def echo():
26 return parser.parse(hello_args, request)
32 return parser.parse(hello_args, request, location="query")
2733
2834
29 @app.route("/echo_query")
30 def echo_query():
31 return parser.parse(hello_args, request, locations=("query",))
35 @app.route("/echo_form", method=["POST"])
36 def echo_form():
37 return parser.parse(hello_args, location="form")
3238
3339
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")
3652 def echo_use_args(args):
3753 return args
3854
3955
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
4556 @app.route(
4657 "/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 ),
4964 )
5065 def echo_use_args_validated(args):
5166 return args
5267
5368
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)
5772
5873
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"])
6097 def echo_many_schema():
61 arguments = parser.parse(hello_many_schema, request, locations=("json",))
98 arguments = parser.parse(hello_many_schema, request)
6299 return HTTPResponse(body=json.dumps(arguments), content_type="application/json")
63100
64101
65102 @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"),
67105 )
68106 def echo_use_args_with_path_param(args, name):
69107 return args
70108
71109
72110 @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"),
74113 )
75114 def echo_use_kwargs_with_path_param(name, value):
76115 return {"value": value}
87126
88127 @app.route("/echo_headers")
89128 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")
91132
92133
93134 @app.route("/echo_cookie")
94135 def echo_cookie():
95 return parser.parse(hello_args, request, locations=("cookies",))
136 return parser.parse(hello_args, request, location="cookies")
96137
97138
98139 @app.route("/echo_file", method=["POST"])
99140 def echo_file():
100141 args = {"myfile": fields.Field()}
101 result = parser.parse(args, locations=("files",))
142 result = parser.parse(args, location="files")
102143 myfile = result["myfile"]
103144 content = myfile.file.read().decode("utf8")
104145 return {"myfile": content}
66
77 TEMPLATE_DEBUG = True
88
9 ALLOWED_HOSTS = []
9 ALLOWED_HOSTS = ["*"]
1010 # Application definition
1111
1212 INSTALLED_APPS = ("django.contrib.contenttypes",)
11
22 from tests.apps.django_app.echo import views
33
4
45 urlpatterns = [
56 url(r"^echo$", views.echo),
6 url(r"^echo_query$", views.echo_query),
7 url(r"^echo_form$", views.echo_form),
8 url(r"^echo_json$", views.echo_json),
9 url(r"^echo_json_or_form$", views.echo_json_or_form),
710 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),
813 url(r"^echo_use_kwargs$", views.echo_use_kwargs),
914 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),
1017 url(r"^echo_many_schema$", views.echo_many_schema),
1118 url(
1219 r"^echo_use_args_with_path_param/(?P<name>\w+)$",
0 from webargs.core import json
10 from django.http import HttpResponse
21 from django.views.generic import View
2 import marshmallow as ma
33
4 import marshmallow as ma
54 from webargs import fields
65 from webargs.djangoparser import parser, use_args, use_kwargs
7 from webargs.core import MARSHMALLOW_VERSION_INFO
6 from webargs.core import json, MARSHMALLOW_VERSION_INFO
7
88
99 hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)}
1010 hello_multiple = {"name": fields.List(fields.Str())}
1717 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
1818 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
1919
20 # variant which ignores unknown fields
21 exclude_kwargs = (
22 {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE}
23 )
24 hello_exclude_schema = HelloSchema(**exclude_kwargs)
25
2026
2127 def json_response(data, **kwargs):
2228 return HttpResponse(json.dumps(data), content_type="application/json", **kwargs)
2329
2430
25 def echo(request):
26 try:
27 args = parser.parse(hello_args, request)
28 except ma.ValidationError as err:
29 return json_response(err.messages, status=parser.DEFAULT_VALIDATION_STATUS)
30 except json.JSONDecodeError:
31 return json_response({"json": ["Invalid JSON body."]}, status=400)
32 return json_response(args)
31 def handle_view_errors(f):
32 def wrapped(*args, **kwargs):
33 try:
34 return f(*args, **kwargs)
35 except ma.ValidationError as err:
36 return json_response(err.messages, status=422)
37 except json.JSONDecodeError:
38 return json_response({"json": ["Invalid JSON body."]}, status=400)
39
40 return wrapped
3341
3442
35 def echo_query(request):
36 return json_response(parser.parse(hello_args, request, locations=("query",)))
43 @handle_view_errors
44 def echo(request):
45 return json_response(parser.parse(hello_args, request, location="query"))
3746
3847
39 @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")
4065 def echo_use_args(request, args):
4166 return json_response(args)
4267
4368
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")
4584 def echo_use_kwargs(request, name):
4685 return json_response({"name": name})
4786
4887
88 @handle_view_errors
4989 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):
50100 return json_response(parser.parse(hello_multiple, request))
51101
52102
103 @handle_view_errors
53104 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))
60106
61107
62 @use_args({"value": fields.Int()})
108 @handle_view_errors
109 @use_args({"value": fields.Int()}, location="query")
63110 def echo_use_args_with_path_param(request, args, name):
64111 return json_response(args)
65112
66113
67 @use_kwargs({"value": fields.Int()})
114 @handle_view_errors
115 @use_kwargs({"value": fields.Int()}, location="query")
68116 def echo_use_kwargs_with_path_param(request, value, name):
69117 return json_response({"value": value})
70118
71119
120 @handle_view_errors
72121 def always_error(request):
73122 def always_fail(value):
74123 raise ma.ValidationError("something went wrong")
75124
76125 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)
81127
82128
129 @handle_view_errors
83130 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 )
85134
86135
136 @handle_view_errors
87137 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"))
89139
90140
141 @handle_view_errors
91142 def echo_file(request):
92143 args = {"myfile": fields.Field()}
93 result = parser.parse(args, request, locations=("files",))
144 result = parser.parse(args, request, location="files")
94145 myfile = result["myfile"]
95146 content = myfile.read().decode("utf8")
96147 return json_response({"myfile": content})
97148
98149
150 @handle_view_errors
99151 def echo_nested(request):
100152 argmap = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})}
101153 return json_response(parser.parse(argmap, request))
102154
103155
156 @handle_view_errors
104157 def echo_nested_many(request):
105158 argmap = {
106159 "users": fields.Nested({"id": fields.Int(), "name": fields.Str()}, many=True)
109162
110163
111164 class EchoCBV(View):
165 @handle_view_errors
112166 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))
118169
119170 post = get
120171
121172
122173 class EchoUseArgsCBV(View):
123 @use_args(hello_args)
174 @handle_view_errors
175 @use_args(hello_args, location="query")
124176 def get(self, request, args):
125177 return json_response(args)
126178
127 post = get
179 @handle_view_errors
180 @use_args(hello_args)
181 def post(self, request, args):
182 return json_response(args)
128183
129184
130185 class EchoUseArgsWithParamCBV(View):
131 @use_args(hello_args)
186 @handle_view_errors
187 @use_args(hello_args, location="query")
132188 def get(self, request, args, pid):
133189 return json_response(args)
134190
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
20 import falcon
31 import marshmallow as ma
2
43 from webargs import fields
4 from webargs.core import MARSHMALLOW_VERSION_INFO, json
55 from webargs.falconparser import parser, use_args, use_kwargs
6 from webargs.core import MARSHMALLOW_VERSION_INFO
76
87 hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)}
98 hello_multiple = {"name": fields.List(fields.Str())}
1615 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
1716 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
1817
19
20 class Echo(object):
21 def on_get(self, req, resp):
22 try:
23 parsed = parser.parse(hello_args, req)
24 except json.JSONDecodeError:
25 resp.body = json.dumps(["Invalid JSON."])
26 resp.status = falcon.HTTP_400
27 else:
28 resp.body = json.dumps(parsed)
29
30 on_post = on_get
18 # variant which ignores unknown fields
19 exclude_kwargs = (
20 {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE}
21 )
22 hello_exclude_schema = HelloSchema(**exclude_kwargs)
3123
3224
33 class EchoQuery(object):
25 class Echo:
3426 def on_get(self, req, resp):
35 parsed = parser.parse(hello_args, req, locations=("query",))
27 parsed = parser.parse(hello_args, req, location="query")
3628 resp.body = json.dumps(parsed)
3729
3830
39 class 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")
4151 def on_get(self, req, resp, args):
4252 resp.body = json.dumps(args)
4353
44 on_post = on_get
4554
46
47 class EchoUseKwargs(object):
48 @use_kwargs(hello_args)
55 class EchoUseKwargs:
56 @use_kwargs(hello_args, location="query")
4957 def on_get(self, req, resp, name):
5058 resp.body = json.dumps({"name": name})
5159
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)
5369
5470
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))
6174
6275
63 class EchoMulti(object):
76 class EchoMulti:
6477 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):
6588 resp.body = json.dumps(parser.parse(hello_multiple, req))
6689
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))
6894
6995
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")
8198 def on_get(self, req, resp, args, name):
8299 resp.body = json.dumps(args)
83100
84101
85 class EchoUseKwargsWithPathParam(object):
86 @use_kwargs({"value": fields.Int()})
102 class EchoUseKwargsWithPathParam:
103 @use_kwargs({"value": fields.Int()}, location="query")
87104 def on_get(self, req, resp, value, name):
88105 resp.body = json.dumps({"value": value})
89106
90107
91 class AlwaysError(object):
108 class AlwaysError:
92109 def on_get(self, req, resp):
93110 def always_fail(value):
94111 raise ma.ValidationError("something went wrong")
99116 on_post = on_get
100117
101118
102 class EchoHeaders(object):
119 class EchoHeaders:
103120 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 )
105127
106128
107 class EchoCookie(object):
129 class EchoCookie:
108130 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"))
110132
111133
112 class EchoNested(object):
134 class EchoNested:
113135 def on_post(self, req, resp):
114136 args = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})}
115137 resp.body = json.dumps(parser.parse(args, req))
116138
117139
118 class EchoNestedMany(object):
140 class EchoNestedMany:
119141 def on_post(self, req, resp):
120142 args = {
121143 "users": fields.Nested(
126148
127149
128150 def use_args_hook(args, context_key="args", **kwargs):
129 def hook(req, resp, params):
151 def hook(req, resp, resource, params):
130152 parsed_args = parser.parse(args, req=req, **kwargs)
131153 req.context[context_key] = parsed_args
132154
133155 return hook
134156
135157
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:
138160 def on_get(self, req, resp):
139161 resp.body = json.dumps(req.context["args"])
140162
142164 def create_app():
143165 app = falcon.API()
144166 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())
146170 app.add_route("/echo_use_args", EchoUseArgs())
147171 app.add_route("/echo_use_kwargs", EchoUseKwargs())
148172 app.add_route("/echo_use_args_validated", EchoUseArgsValidated())
173 app.add_route("/echo_ignoring_extra_data", EchoJSONIgnoreExtraData())
149174 app.add_route("/echo_multi", EchoMulti())
175 app.add_route("/echo_multi_form", EchoMultiForm())
176 app.add_route("/echo_multi_json", EchoMultiJSON())
150177 app.add_route("/echo_many_schema", EchoManySchema())
151178 app.add_route("/echo_use_args_with_path_param/{name}", EchoUseArgsWithPathParam())
152179 app.add_route(
0 from webargs.core import json
10 from flask import Flask, jsonify as J, Response, request
21 from flask.views import MethodView
3
42 import marshmallow as ma
3
54 from webargs import fields
65 from webargs.flaskparser import parser, use_args, use_kwargs
7 from webargs.core import MARSHMALLOW_VERSION_INFO
6 from webargs.core import json, MARSHMALLOW_VERSION_INFO
87
98
109 class TestAppConfig:
2221 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
2322 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
2423
24 # variant which ignores unknown fields
25 exclude_kwargs = (
26 {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE}
27 )
28 hello_exclude_schema = HelloSchema(**exclude_kwargs)
29
2530 app = Flask(__name__)
2631 app.config.from_object(TestAppConfig)
2732
2833
29 @app.route("/echo", methods=["GET", "POST"])
34 @app.route("/echo", methods=["GET"])
3035 def echo():
31 return J(parser.parse(hello_args))
32
33
34 @app.route("/echo_query")
35 def echo_query():
36 return J(parser.parse(hello_args, request, locations=("query",)))
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")
4156 def echo_use_args(args):
4257 return J(args)
4358
4459
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 )
4764 def echo_use_args_validated(args):
4865 return J(args)
4966
5067
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")
5375 def echo_use_kwargs(name):
5476 return J({"name": name})
5577
5678
57 @app.route("/echo_multi", methods=["GET", "POST"])
79 @app.route("/echo_multi", methods=["GET"])
5880 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():
5991 return J(parser.parse(hello_multiple))
6092
6193
6294 @app.route("/echo_many_schema", methods=["GET", "POST"])
6395 def many_nested():
64 arguments = parser.parse(hello_many_schema, locations=("json",))
96 arguments = parser.parse(hello_many_schema)
6597 return Response(json.dumps(arguments), content_type="application/json")
6698
6799
68100 @app.route("/echo_use_args_with_path_param/<name>")
69 @use_args({"value": fields.Int()})
101 @use_args({"value": fields.Int()}, location="query")
70102 def echo_use_args_with_path(args, name):
71103 return J(args)
72104
73105
74106 @app.route("/echo_use_kwargs_with_path_param/<name>")
75 @use_kwargs({"value": fields.Int()})
107 @use_kwargs({"value": fields.Int()}, location="query")
76108 def echo_use_kwargs_with_path(name, value):
77109 return J({"value": value})
78110
88120
89121 @app.route("/echo_headers")
90122 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"))
92126
93127
94128 @app.route("/echo_cookie")
95129 def echo_cookie():
96 return J(parser.parse(hello_args, request, locations=("cookies",)))
130 return J(parser.parse(hello_args, request, location="cookies"))
97131
98132
99133 @app.route("/echo_file", methods=["POST"])
100134 def echo_file():
101135 args = {"myfile": fields.Field()}
102 result = parser.parse(args, locations=("files",))
136 result = parser.parse(args, location="files")
103137 fp = result["myfile"]
104138 content = fp.read().decode("utf8")
105139 return J({"myfile": content})
107141
108142 @app.route("/echo_view_arg/<view_arg>")
109143 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"))
111145
112146
113147 @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")
115149 def echo_view_arg_with_use_args(args, **kwargs):
116150 return J(args)
117151
176210 def handle_error(err):
177211 if err.code == 422:
178212 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
20 from pyramid.config import Configurator
31 from pyramid.httpexceptions import HTTPBadRequest
42 import marshmallow as ma
53
64 from webargs import fields
75 from webargs.pyramidparser import parser, use_args, use_kwargs
8 from webargs.core import MARSHMALLOW_VERSION_INFO
6 from webargs.core import json, MARSHMALLOW_VERSION_INFO
97
108 hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)}
119 hello_multiple = {"name": fields.List(fields.Str())}
1816 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
1917 hello_many_schema = HelloSchema(many=True, **strict_kwargs)
2018
19 # variant which ignores unknown fields
20 exclude_kwargs = (
21 {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE}
22 )
23 hello_exclude_schema = HelloSchema(**exclude_kwargs)
24
2125
2226 def echo(request):
27 return parser.parse(hello_args, request, location="query")
28
29
30 def echo_form(request):
31 return parser.parse(hello_args, request, location="form")
32
33
34 def echo_json(request):
2335 try:
24 return parser.parse(hello_args, request)
36 return parser.parse(hello_args, request, location="json")
2537 except json.JSONDecodeError:
2638 error = HTTPBadRequest()
2739 error.body = json.dumps(["Invalid JSON."]).encode("utf-8")
2941 raise error
3042
3143
44 def echo_json_or_form(request):
45 try:
46 return parser.parse(hello_args, request, location="json_or_form")
47 except json.JSONDecodeError:
48 error = HTTPBadRequest()
49 error.body = json.dumps(["Invalid JSON."]).encode("utf-8")
50 error.content_type = "application/json"
51 raise error
52
53
54 def echo_json_ignore_extra_data(request):
55 try:
56 return parser.parse(hello_exclude_schema, request)
57 except json.JSONDecodeError:
58 error = HTTPBadRequest()
59 error.body = json.dumps(["Invalid JSON."]).encode("utf-8")
60 error.content_type = "application/json"
61 raise error
62
63
3264 def echo_query(request):
33 return parser.parse(hello_args, request, locations=("query",))
34
35
36 @use_args(hello_args)
65 return parser.parse(hello_args, request, location="query")
66
67
68 @use_args(hello_args, location="query")
3769 def echo_use_args(request, args):
3870 return args
3971
4072
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 )
4276 def echo_use_args_validated(request, args):
4377 return args
4478
4579
46 @use_kwargs(hello_args)
80 @use_kwargs(hello_args, location="query")
4781 def echo_use_kwargs(request, name):
4882 return {"name": name}
4983
5084
5185 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):
5294 return parser.parse(hello_multiple, request)
5395
5496
5597 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")
60102 def echo_use_args_with_path_param(request, args):
61103 return args
62104
63105
64 @use_kwargs({"value": fields.Int()})
106 @use_kwargs({"value": fields.Int()}, location="query")
65107 def echo_use_kwargs_with_path_param(request, value):
66108 return {"value": value}
67109
75117
76118
77119 def echo_headers(request):
78 return parser.parse(hello_args, request, locations=("headers",))
120 return parser.parse(hello_exclude_schema, request, location="headers")
79121
80122
81123 def echo_cookie(request):
82 return parser.parse(hello_args, request, locations=("cookies",))
124 return parser.parse(hello_args, request, location="cookies")
83125
84126
85127 def echo_file(request):
86128 args = {"myfile": fields.Field()}
87 result = parser.parse(args, request, locations=("files",))
129 result = parser.parse(args, request, location="files")
88130 myfile = result["myfile"]
89131 content = myfile.file.read().decode("utf8")
90132 return {"myfile": content}
103145
104146
105147 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:
110152 def __init__(self, request):
111153 self.request = request
112154
113 @use_args({"value": fields.Int()})
155 @use_args({"value": fields.Int()}, location="query")
114156 def __call__(self, args):
115157 return args
116158
126168 config = Configurator()
127169
128170 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)
129174 add_route(config, "/echo_query", echo_query)
175 add_route(config, "/echo_ignoring_extra_data", echo_json_ignore_extra_data)
130176 add_route(config, "/echo_use_args", echo_use_args)
131177 add_route(config, "/echo_use_args_validated", echo_use_args_validated)
132178 add_route(config, "/echo_use_kwargs", echo_use_kwargs)
133179 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)
134182 add_route(config, "/echo_many_schema", echo_many_schema)
135183 add_route(
136184 config, "/echo_use_args_with_path_param/{name}", echo_use_args_with_path_param
+0
-16
tests/compat.py less more
0 # -*- coding: utf-8 -*-
1 # flake8: noqa
2 import sys
3
4 PY2 = int(sys.version[0]) == 2
5
6 if PY2:
7 text_type = unicode
8 binary_type = str
9 string_types = (str, unicode)
10 basestring = basestring
11 else:
12 text_type = str
13 binary_type = bytes
14 string_types = (str,)
15 basestring = (str, bytes)
0 # -*- coding: utf-8 -*-
10 import itertools
2 import mock
3 import sys
41 import datetime
52
63 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
85 from werkzeug.datastructures import MultiDict as WerkMultiDict
96 from django.utils.datastructures import MultiValueDict as DjMultiDict
107 from bottle import MultiDict as BotMultiDict
118
12 from webargs import fields, missing, ValidationError
9 from webargs import fields, ValidationError
1310 from webargs.core import (
1411 Parser,
15 get_value,
1612 dict2schema,
1713 is_json,
1814 get_mimetype,
1915 MARSHMALLOW_VERSION_INFO,
2016 )
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
2125
2226
2327 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
2731 def __init__(self, status_code, headers):
2832 self.status_code = status_code
2933 self.headers = headers
30 super(MockHTTPError, self).__init__(self, "HTTP Error occurred")
34 super().__init__(self, "HTTP Error occurred")
3135
3236
3337 class MockRequestParser(Parser):
3438 """A minimal parser implementation that parses mock requests."""
3539
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
4448
4549
4650 @pytest.yield_fixture(scope="function")
5963 # Parser tests
6064
6165
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}
6570 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}
117102 argmap = {"username": fields.Field(), "password": fields.Field()}
118 p = Parser()
119 ret = p.parse(argmap, web_request)
103 ret = parser.parse(argmap, web_request)
120104 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
121133
122134
123135 def test_parse_required_arg_raises_validation_error(parser, web_request):
141153 assert result == {"first": "Steve", "last": None}
142154
143155
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}
151160
152161
153162 def test_parse_required_list(parser, web_request):
155164 args = {"foo": fields.List(fields.Field(), required=True)}
156165 with pytest.raises(ValidationError) as excinfo:
157166 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 )
159170
160171
161172 # Regression test for https://github.com/marshmallow-code/webargs/issues/107
170181 args = {"foo": fields.List(fields.Field(), allow_none=False)}
171182 with pytest.raises(ValidationError) as excinfo:
172183 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."
174185
175186
176187 def test_parse_empty_list(parser, web_request):
185196 assert parser.parse(args, web_request) == {}
186197
187198
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"
190201
191202
192203 def test_missing_with_default(parser, web_request):
193204 web_request.json = {}
194205 args = {"val": fields.Field(missing="pizza")}
195 result = parser.parse(args, web_request, locations=("json",))
206 result = parser.parse(args, web_request)
196207 assert result["val"] == "pizza"
197208
198209
199210 def test_default_can_be_none(parser, web_request):
200211 web_request.json = {}
201212 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)
203214 assert result["val"] is None
204215
205216
210221 "p": fields.Int(
211222 missing=1,
212223 validate=lambda p: p > 0,
213 error=u"La page demandée n'existe pas",
224 error="La page demandée n'existe pas",
214225 location="querystring",
215226 )
216227 }
217228 assert parser.parse(args, web_request) == {"p": 1}
218229
219230
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):
221232 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
222242 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)
250247 assert handle_error.call_count == 2
251248
252249
253250 def test_handle_error_reraises_errors(web_request):
254251 p = Parser()
255252 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 = {}
262266 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):
275271 class CustomError(Exception):
276272 pass
277273
278 def error_handler(error, req, schema, status_code, headers):
274 def error_handler(error, req, schema, *, error_status_code, error_headers):
279275 assert isinstance(schema, Schema)
280276 raise CustomError(error)
281277
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
283287 p = Parser(error_handler=error_handler)
284288 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):
290293 class CustomError(Exception):
291294 pass
292295
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")
295299 parser = Parser()
296300
297301 @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):
299303 assert isinstance(schema, Schema)
300304 raise CustomError(error)
301305
302306 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):
307311 web_request.data = {"foo": 42}
308312
309313 parser = Parser()
310314
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")
316320 assert result["foo"] == 42
317321
318322
319 def test_custom_location_handler_with_data_key(web_request):
323 def test_custom_location_loader_with_data_key(web_request):
320324 web_request.data = {"X-Foo": 42}
321325 parser = Parser()
322326
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
326330
327331 data_key_kwarg = {
328332 "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "X-Foo"
329333 }
330334 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"
332336 )
333337 assert result["x_foo"] == 42
334338
335339
336 def test_full_input_validation(web_request):
340 def test_full_input_validation(parser, web_request):
337341
338342 web_request.json = {"foo": 41, "bar": 42}
339343
340 parser = MockRequestParser()
341344 args = {"foo": fields.Int(), "bar": fields.Int()}
342345 with pytest.raises(ValidationError):
343346 # 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"])
350348
351349
352350 def test_full_input_validation_with_multiple_validators(web_request, parser):
361359 args = {"a": fields.Int(), "b": fields.Int()}
362360 web_request.json = {"a": 2, "b": 1}
363361 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)
367364
368365 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 = {}
377372 args = {
378373 "foo": fields.Str(required=True, error_messages={"required": "We need foo"})
379374 }
380375 with pytest.raises(ValidationError) as excinfo:
381376 # 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"]
385380 if MARSHMALLOW_VERSION_INFO[0] < 3:
386381 assert "foo" in excinfo.value.field_names
387382
388383
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):
390385 web_request.json = {"foo": ""}
391 parser = MockRequestParser()
392386 args = {
393387 "foo": fields.Str(
394388 required="We need foo",
398392 }
399393 with pytest.raises(ValidationError) as excinfo:
400394 # Test that `validate` receives dictionary of args
401 parser.parse(args, web_request, locations=("json",))
395 parser.parse(args, web_request)
402396
403397 assert "foo required length is 3" in excinfo.value.args[0]["foo"]
404398 if MARSHMALLOW_VERSION_INFO[0] < 3:
409403 def validate(val):
410404 return False
411405
412 text = u"øœ∑∆∑"
406 text = "øœ∑∆∑"
413407 web_request.json = {"text": text}
414408 parser = MockRequestParser()
415409 args = {"text": fields.Str()}
416410 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."]}
419413
420414
421415 def test_invalid_argument_for_validate(web_request, parser):
422416 with pytest.raises(ValueError) as excinfo:
423417 parser.parse({}, web_request, validate="notcallable")
424418 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
433419
434420
435421 def create_bottle_multi_dict():
447433
448434
449435 @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")
453454
454455
455456 def test_parse_with_data_key(web_request):
460461 "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "Content-Type"
461462 }
462463 args = {"content_type": fields.Field(**data_key_kwargs)}
463 parsed = parser.parse(args, web_request, locations=("json",))
464 parsed = parser.parse(args, web_request)
464465 assert parsed == {"content_type": "application/json"}
465466
466467
474475
475476 parser = MockRequestParser()
476477 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)
478479 assert parsed == {"content_type": "application/json"}
479480
480481
487488 }
488489 args = {"content_type": fields.Str(**data_key_kwargs)}
489490 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."]
493495
494496
495497 def test_parse_nested_with_data_key(web_request):
500502 }
501503 args = {"nested_arg": fields.Nested({"right": fields.Field(**data_key_kwarg)})}
502504
503 parsed = parser.parse(args, web_request, locations=("json",))
505 parsed = parser.parse(args, web_request)
504506 assert parsed == {"nested_arg": {"right": "OK"}}
505507
506508
517519 )
518520 }
519521
520 parsed = parser.parse(args, web_request, locations=("json",))
522 parsed = parser.parse(args, web_request)
521523 assert parsed == {"nested_arg": {"found": None}}
522524
523525
527529 web_request.json = {"nested_arg": {}}
528530 args = {"nested_arg": fields.Nested({"miss": fields.Field(missing="<foo>")})}
529531
530 parsed = parser.parse(args, web_request, locations=("json",))
532 parsed = parser.parse(args, web_request)
531533 assert parsed == {"nested_arg": {"miss": "<foo>"}}
532534
533535
558560 web_request.json = {"username": "foo"}
559561 web_request.query = {"page": 42}
560562
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)
563565 def viewfunc(query_parsed, json_parsed):
564566 return {"json": json_parsed, "query": query_parsed}
565567
574576 web_request.json = {"username": "foo"}
575577 web_request.query = {"page": 42}
576578
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)
579581 def viewfunc(page, username):
580582 return {"json": {"username": username}, "query": {"page": page}}
581583
596598
597599 def test_list_allowed_missing(web_request, parser):
598600 args = {"name": fields.List(fields.Str())}
599 web_request.json = {"fakedata": True}
601 web_request.json = {}
600602 result = parser.parse(args, web_request)
601603 assert result == {}
602604
603605
604606 def test_int_list_allowed_missing(web_request, parser):
605607 args = {"name": fields.List(fields.Int())}
606 web_request.json = {"fakedata": True}
608 web_request.json = {}
607609 result = parser.parse(args, web_request)
608610 assert result == {}
609611
610612
611613 def test_multiple_arg_required_with_int_conversion(web_request, parser):
612614 args = {"ids": fields.List(fields.Int(), required=True)}
613 web_request.json = {"fakedata": True}
615 web_request.json = {}
614616 with pytest.raises(ValidationError) as excinfo:
615617 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 }
617621
618622
619623 def test_parse_with_callable(web_request, parser):
647651 strict = True
648652
649653 @post_load
650 def request_data(self, item):
654 def request_data(self, item, **kwargs):
651655 item["data"] = self.context["request"].data
652656 return item
653657
739743
740744 assert viewfunc() == {"email": "[email protected]", "password": "bar"}
741745
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 )
748746 @pytest.mark.skipif(
749747 MARSHMALLOW_VERSION_INFO[0] >= 3,
750748 reason='"strict" parameter is removed in marshmallow 3',
757755 assert "strict=True" in str(warning.message)
758756
759757 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
760770 web_request.json = {"email": "[email protected]", "password": "bar", "page": 42}
761771
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)
764774 def viewfunc(email, password, page):
765775 return {"email": email, "password": password, "page": page}
766776
779789 strict = True
780790
781791 @validates_schema(pass_original=True)
782 def validate_schema(self, data, original_data):
792 def validate_schema(self, data, original_data, **kwargs):
783793 assert "location" not in original_data
784794 return True
785795
786796 web_request.json = {"name": "Eric Cartman"}
787 res = parser.parse(UserSchema, web_request, locations=("json",))
797 res = parser.parse(UserSchema, web_request)
788798 assert res == {"name": "Eric Cartman"}
789799
790800
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):
792802 custom_args = {"foo": fields.Str()}
793803 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"}
799809
800810 @parser.use_args(custom_args, web_request)
801811 def viewfunc(args):
837847
838848 dumped = schema.dump(parsed)
839849 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"}
845863 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 }
847869 )
848870 schema = schema_cls()
849871
850872 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 """
859900 web_request.json = {"dates": "2018-11-01,2018-11-02"}
860901 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"))}
866903 )
867904 schema = schema_cls()
868905
885922 parsed = parser.parse(schema, web_request)
886923 assert parsed["ids"] == [1, 2, 3]
887924
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):
890948 web_request.json = {"ids": [1, 2, 3]}
891949 schema_cls = dict2schema({"ids": fields.DelimitedList(fields.Int())})
892950 schema = schema_cls()
893951
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."]
896974
897975
898976 # Regresion test for https://github.com/marshmallow-code/webargs/issues/149
903981
904982 with pytest.raises(ValidationError) as excinfo:
905983 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."]}}
907998
908999
9091000 def test_missing_list_argument_not_in_parsed_result(web_request, parser):
9181009 def test_type_conversion_with_multiple_required(web_request, parser):
9191010 web_request.json = {}
9201011 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):
9221014 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
9341015
9351016
9361017 def test_validation_errors_in_validator_are_passed_to_handle_error(parser, web_request):
9821063 assert schema.fields["id"].required
9831064 if MARSHMALLOW_VERSION_INFO[0] < 3:
9841065 assert schema.opts.strict is True
985 else:
986 assert schema.opts.register is False
9871066
9881067
9891068 # Regression test for https://github.com/marshmallow-code/webargs/issues/101
10321111
10331112
10341113 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):
10381115 assert isinstance(error, ValidationError)
10391116 assert isinstance(schema, Schema)
10401117 raise MockHTTPError(error_status_code, error_headers)
10511128 error = excinfo.value
10521129 assert error.status_code == 418
10531130 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
30 import pytest
41 from tests.apps.django_app.base.wsgi import application
52
2219
2320 def test_parsing_in_class_based_view(self, testapp):
2421 assert testapp.get("/echo_cbv?name=Fred").json == {"name": "Fred"}
25 assert testapp.post("/echo_cbv", {"name": "Fred"}).json == {"name": "Fred"}
22 assert testapp.post_json("/echo_cbv", {"name": "Fred"}).json == {"name": "Fred"}
2623
2724 def test_use_args_in_class_based_view(self, testapp):
2825 res = testapp.get("/echo_use_args_cbv?name=Fred")
2926 assert res.json == {"name": "Fred"}
30 res = testapp.post("/echo_use_args_cbv", {"name": "Fred"})
27 res = testapp.post_json("/echo_use_args_cbv", {"name": "Fred"})
3128 assert res.json == {"name": "Fred"}
3229
3330 def test_use_args_in_class_based_view_with_path_param(self, testapp):
0 # -*- coding: utf-8 -*-
10 import pytest
1 import falcon.testing
22
33 from webargs.testing import CommonTestCase
44 from tests.apps.falcon_app import create_app
1515 def test_use_args_hook(self, testapp):
1616 assert testapp.get("/echo_use_args_hook?name=Fred").json == {"name": "Fred"}
1717
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
1830 # https://github.com/sloria/webargs/issues/329
1931 def test_invalid_json(self, testapp):
2032 res = testapp.post(
21 "/echo",
33 "/echo_json",
2234 '{"foo": "bar", }',
2335 headers={"Accept": "application/json", "Content-Type": "application/json"},
2436 expect_errors=True,
2537 )
2638 assert res.status_code == 400
2739 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
40 from werkzeug.exceptions import HTTPException
51 import pytest
62
73 from flask import Flask
8 from webargs import fields, ValidationError, missing
4 from webargs import fields, ValidationError, missing, dict2schema
95 from webargs.flaskparser import parser, abort
106 from webargs.core import MARSHMALLOW_VERSION_INFO, json
117
128 from .apps.flask_app import app
139 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
1417
1518
1619 class TestFlaskParser(CommonTestCase):
2427 def test_parsing_invalid_view_arg(self, testapp):
2528 res = testapp.get("/echo_view_arg/foo", expect_errors=True)
2629 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."]}}
2831
2932 def test_use_args_with_view_args_parsing(self, testapp):
3033 res = testapp.get("/echo_view_arg_use_args/42")
3134 assert res.json == {"view_arg": 42}
3235
3336 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})
3538 assert res.json == {"val": 42}
3639
3740 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})
3942 assert res.json == {"val": 42}
4043
4144 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"})
4346 assert res.json == {"username": "foo"}
4447
4548 # regression test for https://github.com/marshmallow-code/webargs/issues/145
4649 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
4955 if MARSHMALLOW_VERSION_INFO[0] < 3:
56 res = testapp.post_json(*post_with_raw_fieldname_args)
5057 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
5162
5263 res = testapp.post_json("/echo_nested_many_data_key", {"X-Field": [{"id": 24}]})
5364 assert res.json == {"x_field": [{"id": 24}]}
7182 content_type="application/json",
7283 ):
7384 parser.parse(argmap)
74 mock_abort.assert_called
85 mock_abort.assert_called()
7586 abort_args, abort_kwargs = mock_abort.call_args
7687 assert abort_args[0] == 422
7788 expected_msg = "Invalid value."
78 assert abort_kwargs["messages"]["value"] == [expected_msg]
89 assert abort_kwargs["messages"]["json"]["value"] == [expected_msg]
7990 assert type(abort_kwargs["exc"]) == ValidationError
8091
8192
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):
8395 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
86100
87101
88102 def test_abort_with_message():
0 # -*- coding: utf-8 -*-
1
20 import asyncio
31 import webtest
42 import webtest_aiohttp
3735
3836 # regression test for https://github.com/marshmallow-code/webargs/issues/165
3937 def test_multiple_args(self, testapp):
40 res = testapp.post_json(
41 "/echo_multiple_args", {"first": "1", "last": "2", "_ignore": 0}
42 )
38 res = testapp.post_json("/echo_multiple_args", {"first": "1", "last": "2"})
4339 assert res.json == {"first": "1", "last": "2"}
4440
4541 # regression test for https://github.com/marshmallow-code/webargs/issues/145
4642 def test_nested_many_with_data_key(self, testapp):
47 res = testapp.post_json("/echo_nested_many_data_key", {"x_field": [{"id": 42}]})
4843 # https://github.com/marshmallow-code/marshmallow/pull/714
44 # on marshmallow 2, the field name can also be used
4945 if MARSHMALLOW_VERSION_INFO[0] < 3:
46 res = testapp.post_json(
47 "/echo_nested_many_data_key", {"x_field": [{"id": 42}]}
48 )
5049 assert res.json == {"x_field": [{"id": 42}]}
5150
5251 res = testapp.post_json("/echo_nested_many_data_key", {"X-Field": [{"id": 24}]})
1010
1111
1212 async def echo_parse(request):
13 parsed = await parser.parse(hello_args, request)
13 parsed = await parser.parse(hello_args, request, location="query")
1414 return json_response(parsed)
1515
1616
17 @use_args(hello_args)
17 @use_args(hello_args, location="query")
1818 async def echo_use_args(request, args):
1919 return json_response(args)
2020
2121
22 @use_kwargs(hello_args)
22 @use_kwargs(hello_args, location="query")
2323 async def echo_use_kwargs(request, name):
2424 return json_response({"name": name})
2525
0 # -*- coding: utf-8 -*-
10 from webargs.testing import CommonTestCase
21
32
0 # -*- coding: utf-8 -*-
1
2 from webargs.core import json
0 import marshmallow as ma
1 import pytest
2 import tornado.concurrent
3 import tornado.http1connection
4 import tornado.httpserver
5 import tornado.httputil
6 import tornado.ioloop
7 import tornado.web
8 from tornado.testing import AsyncHTTPTestCase
9 from webargs import fields, missing
10 from webargs.core import MARSHMALLOW_VERSION_INFO, json, parse_json
11 from webargs.tornadoparser import (
12 WebArgsTornadoMultiDictProxy,
13 parser,
14 use_args,
15 use_kwargs,
16 )
17
18 from urllib.parse import urlencode
319
420 try:
5 from urllib.parse import urlencode
6 except ImportError: # PY2
7 from urllib import urlencode # type: ignore
8
9 import mock
10 import pytest
11
12 import marshmallow as ma
13
14 import tornado.web
15 import tornado.httputil
16 import tornado.httpserver
17 import tornado.http1connection
18 import tornado.concurrent
19 import tornado.ioloop
20 from tornado.testing import AsyncHTTPTestCase
21
22 from webargs import fields, missing
23 from webargs.tornadoparser import parser, use_args, use_kwargs, get_value
24 from webargs.core import parse_json
21 # Python 3.5
22 import mock
23 except ImportError:
24 # Python 3.6+
25 from unittest import mock
26
2527
2628 name = "name"
2729 value = "value"
2830
2931
30 def test_get_value_basic():
31 field, multifield = fields.Field(), fields.List(fields.Str())
32 assert get_value({"foo": 42}, "foo", field) == 42
33 assert get_value({"foo": 42}, "bar", field) is missing
34 assert get_value({"foos": ["a", "b"]}, "foos", multifield) == ["a", "b"]
35 # https://github.com/marshmallow-code/webargs/pull/30
36 assert get_value({"foos": ["a", "b"]}, "bar", multifield) is missing
37
38
39 class TestQueryArgs(object):
40 def setup_method(self, method):
41 parser.clear_cache()
42
32 class AuthorSchema(ma.Schema):
33 name = fields.Str(missing="World", validate=lambda n: len(n) >= 3)
34 works = fields.List(fields.Str())
35
36
37 strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {}
38 author_schema = AuthorSchema(**strict_kwargs)
39
40
41 def test_tornado_multidictproxy():
42 for dictval, fieldname, expected in (
43 ({"name": "Sophocles"}, "name", "Sophocles"),
44 ({"name": "Sophocles"}, "works", missing),
45 ({"works": ["Antigone", "Oedipus Rex"]}, "works", ["Antigone", "Oedipus Rex"]),
46 ({"works": ["Antigone", "Oedipus at Colonus"]}, "name", missing),
47 ):
48 proxy = WebArgsTornadoMultiDictProxy(dictval, author_schema)
49 assert proxy.get(fieldname) == expected
50
51
52 class TestQueryArgs:
4353 def test_it_should_get_single_values(self):
44 query = [(name, value)]
45 field = fields.Field()
54 query = [("name", "Aeschylus")]
4655 request = make_get_request(query)
47
48 result = parser.parse_querystring(request, name, field)
49
50 assert result == value
56 result = parser.load_querystring(request, author_schema)
57 assert result["name"] == "Aeschylus"
5158
5259 def test_it_should_get_multiple_values(self):
53 query = [(name, value), (name, value)]
54 field = fields.List(fields.Field())
60 query = [("works", "Agamemnon"), ("works", "Nereids")]
5561 request = make_get_request(query)
56
57 result = parser.parse_querystring(request, name, field)
58
59 assert result == [value, value]
62 result = parser.load_querystring(request, author_schema)
63 assert result["works"] == ["Agamemnon", "Nereids"]
6064
6165 def test_it_should_return_missing_if_not_present(self):
6266 query = []
63 field = fields.Field()
64 field2 = fields.List(fields.Int())
6567 request = make_get_request(query)
66
67 result = parser.parse_querystring(request, name, field)
68 result2 = parser.parse_querystring(request, name, field2)
69
70 assert result is missing
71 assert result2 is missing
72
73 def test_it_should_return_empty_list_if_multiple_and_not_present(self):
74 query = []
75 field = fields.List(fields.Field())
76 request = make_get_request(query)
77
78 result = parser.parse_querystring(request, name, field)
79
80 assert result is missing
68 result = parser.load_querystring(request, author_schema)
69 assert result["name"] is missing
70 assert result["works"] is missing
8171
8272
8373 class TestFormArgs:
84 def setup_method(self, method):
85 parser.clear_cache()
86
8774 def test_it_should_get_single_values(self):
88 query = [(name, value)]
89 field = fields.Field()
75 query = [("name", "Aristophanes")]
9076 request = make_form_request(query)
91
92 result = parser.parse_form(request, name, field)
93
94 assert result == value
77 result = parser.load_form(request, author_schema)
78 assert result["name"] == "Aristophanes"
9579
9680 def test_it_should_get_multiple_values(self):
97 query = [(name, value), (name, value)]
98 field = fields.List(fields.Field())
81 query = [("works", "The Wasps"), ("works", "The Frogs")]
9982 request = make_form_request(query)
100
101 result = parser.parse_form(request, name, field)
102
103 assert result == [value, value]
83 result = parser.load_form(request, author_schema)
84 assert result["works"] == ["The Wasps", "The Frogs"]
10485
10586 def test_it_should_return_missing_if_not_present(self):
10687 query = []
107 field = fields.Field()
10888 request = make_form_request(query)
109
110 result = parser.parse_form(request, name, field)
111
112 assert result is missing
113
114 def test_it_should_return_empty_list_if_multiple_and_not_present(self):
115 query = []
116 field = fields.List(fields.Field())
117 request = make_form_request(query)
118
119 result = parser.parse_form(request, name, field)
120
121 assert result is missing
122
123
124 class TestJSONArgs(object):
125 def setup_method(self, method):
126 parser.clear_cache()
127
89 result = parser.load_form(request, author_schema)
90 assert result["name"] is missing
91 assert result["works"] is missing
92
93
94 class TestJSONArgs:
12895 def test_it_should_get_single_values(self):
129 query = {name: value}
130 field = fields.Field()
96 query = {"name": "Euripides"}
13197 request = make_json_request(query)
132 result = parser.parse_json(request, name, field)
133
134 assert result == value
98 result = parser.load_json(request, author_schema)
99 assert result["name"] == "Euripides"
135100
136101 def test_parsing_request_with_vendor_content_type(self):
137 query = {name: value}
138 field = fields.Field()
102 query = {"name": "Euripides"}
139103 request = make_json_request(
140104 query, content_type="application/vnd.api+json; charset=UTF-8"
141105 )
142 result = parser.parse_json(request, name, field)
143
144 assert result == value
106 result = parser.load_json(request, author_schema)
107 assert result["name"] == "Euripides"
145108
146109 def test_it_should_get_multiple_values(self):
147 query = {name: [value, value]}
148 field = fields.List(fields.Field())
110 query = {"works": ["Medea", "Electra"]}
149111 request = make_json_request(query)
150 result = parser.parse_json(request, name, field)
151
152 assert result == [value, value]
112 result = parser.load_json(request, author_schema)
113 assert result["works"] == ["Medea", "Electra"]
153114
154115 def test_it_should_get_multiple_nested_values(self):
155 query = {name: [{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}]}
156 field = fields.List(
157 fields.Nested({"id": fields.Field(), "name": fields.Field()})
158 )
116 class CustomSchema(ma.Schema):
117 works = fields.List(
118 fields.Nested({"author": fields.Str(), "workname": fields.Str()})
119 )
120
121 custom_schema = CustomSchema(**strict_kwargs)
122
123 query = {
124 "works": [
125 {"author": "Euripides", "workname": "Hecuba"},
126 {"author": "Aristophanes", "workname": "The Birds"},
127 ]
128 }
159129 request = make_json_request(query)
160 result = parser.parse_json(request, name, field)
161 assert result == [{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}]
130 result = parser.load_json(request, custom_schema)
131 assert result["works"] == [
132 {"author": "Euripides", "workname": "Hecuba"},
133 {"author": "Aristophanes", "workname": "The Birds"},
134 ]
135
136 def test_it_should_not_include_fieldnames_if_not_present(self):
137 query = {}
138 request = make_json_request(query)
139 result = parser.load_json(request, author_schema)
140 assert result == {}
141
142 def test_it_should_handle_type_error_on_load_json(self):
143 # but this is different from the test above where the payload was valid
144 # and empty -- missing vs {}
145 request = make_request(
146 body=tornado.concurrent.Future(),
147 headers={"Content-Type": "application/json"},
148 )
149 result = parser.load_json(request, author_schema)
150 assert result is missing
151
152 def test_it_should_handle_value_error_on_parse_json(self):
153 request = make_request("this is json not")
154 result = parser.load_json(request, author_schema)
155 assert result is missing
156
157
158 class TestHeadersArgs:
159 def test_it_should_get_single_values(self):
160 query = {"name": "Euphorion"}
161 request = make_request(headers=query)
162 result = parser.load_headers(request, author_schema)
163 assert result["name"] == "Euphorion"
164
165 def test_it_should_get_multiple_values(self):
166 query = {"works": ["Prometheus Bound", "Prometheus Unbound"]}
167 request = make_request(headers=query)
168 result = parser.load_headers(request, author_schema)
169 assert result["works"] == ["Prometheus Bound", "Prometheus Unbound"]
162170
163171 def test_it_should_return_missing_if_not_present(self):
164 query = {}
165 field = fields.Field()
166 request = make_json_request(query)
167 result = parser.parse_json(request, name, field)
168
169 assert result is missing
170
171 def test_it_should_return_empty_list_if_multiple_and_not_present(self):
172 query = {}
173 field = fields.List(fields.Field())
174 request = make_json_request(query)
175 result = parser.parse_json(request, name, field)
176
177 assert result is missing
178
179 def test_it_should_handle_type_error_on_parse_json(self):
180 field = fields.Field()
181 request = make_request(
182 body=tornado.concurrent.Future, headers={"Content-Type": "application/json"}
183 )
184 result = parser.parse_json(request, name, field)
185 assert parser._cache["json"] == {}
186 assert result is missing
187
188 def test_it_should_handle_value_error_on_parse_json(self):
189 field = fields.Field()
190 request = make_request("this is json not")
191 result = parser.parse_json(request, name, field)
192 assert parser._cache["json"] == {}
193 assert result is missing
194
195
196 class TestHeadersArgs(object):
197 def setup_method(self, method):
198 parser.clear_cache()
199
172 request = make_request()
173 result = parser.load_headers(request, author_schema)
174 assert result["name"] is missing
175 assert result["works"] is missing
176
177
178 class TestFilesArgs:
200179 def test_it_should_get_single_values(self):
201 query = {name: value}
202 field = fields.Field()
203 request = make_request(headers=query)
204
205 result = parser.parse_headers(request, name, field)
206
207 assert result == value
180 query = [("name", "Sappho")]
181 request = make_files_request(query)
182 result = parser.load_files(request, author_schema)
183 assert result["name"] == "Sappho"
208184
209185 def test_it_should_get_multiple_values(self):
210 query = {name: [value, value]}
211 field = fields.List(fields.Field())
212 request = make_request(headers=query)
213
214 result = parser.parse_headers(request, name, field)
215
216 assert result == [value, value]
217
218 def test_it_should_return_missing_if_not_present(self):
219 field = fields.Field(multiple=False)
220 request = make_request()
221
222 result = parser.parse_headers(request, name, field)
223
224 assert result is missing
225
226 def test_it_should_return_empty_list_if_multiple_and_not_present(self):
227 query = {}
228 field = fields.List(fields.Field())
229 request = make_request(headers=query)
230
231 result = parser.parse_headers(request, name, field)
232
233 assert result is missing
234
235
236 class TestFilesArgs(object):
237 def setup_method(self, method):
238 parser.clear_cache()
239
240 def test_it_should_get_single_values(self):
241 query = [(name, value)]
242 field = fields.Field()
186 query = [("works", "Sappho 31"), ("works", "Ode to Aphrodite")]
243187 request = make_files_request(query)
244
245 result = parser.parse_files(request, name, field)
246
247 assert result == value
248
249 def test_it_should_get_multiple_values(self):
250 query = [(name, value), (name, value)]
251 field = fields.List(fields.Field())
252 request = make_files_request(query)
253
254 result = parser.parse_files(request, name, field)
255
256 assert result == [value, value]
188 result = parser.load_files(request, author_schema)
189 assert result["works"] == ["Sappho 31", "Ode to Aphrodite"]
257190
258191 def test_it_should_return_missing_if_not_present(self):
259192 query = []
260 field = fields.Field()
261193 request = make_files_request(query)
262
263 result = parser.parse_files(request, name, field)
264
265 assert result is missing
266
267 def test_it_should_return_empty_list_if_multiple_and_not_present(self):
268 query = []
269 field = fields.List(fields.Field())
270 request = make_files_request(query)
271
272 result = parser.parse_files(request, name, field)
273
274 assert result is missing
275
276
277 class TestErrorHandler(object):
194 result = parser.load_files(request, author_schema)
195 assert result["name"] is missing
196 assert result["works"] is missing
197
198
199 class TestErrorHandler:
278200 def test_it_should_raise_httperror_on_failed_validation(self):
279201 args = {"foo": fields.Field(validate=lambda x: False)}
280202 with pytest.raises(tornado.web.HTTPError):
281203 parser.parse(args, make_json_request({"foo": 42}))
282204
283205
284 class TestParse(object):
285 def setup_method(self, method):
286 parser.clear_cache()
287
206 class TestParse:
288207 def test_it_should_parse_query_arguments(self):
289208 attrs = {"string": fields.Field(), "integer": fields.List(fields.Int())}
290209
292211 [("string", "value"), ("integer", "1"), ("integer", "2")]
293212 )
294213
295 parsed = parser.parse(attrs, request)
214 parsed = parser.parse(attrs, request, location="query")
296215
297216 assert parsed["integer"] == [1, 2]
298217 assert parsed["string"] == value
299218
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
311219 def test_it_should_parse_form_arguments(self):
312220 attrs = {"string": fields.Field(), "integer": fields.List(fields.Int())}
313221
315223 [("string", "value"), ("integer", "1"), ("integer", "2")]
316224 )
317225
318 parsed = parser.parse(attrs, request)
226 parsed = parser.parse(attrs, request, location="form")
319227
320228 assert parsed["integer"] == [1, 2]
321229 assert parsed["string"] == value
347255
348256 request = make_request(headers={"string": "value", "integer": ["1", "2"]})
349257
350 parsed = parser.parse(attrs, request, locations=["headers"])
258 parsed = parser.parse(attrs, request, location="headers")
351259
352260 assert parsed["string"] == value
353261 assert parsed["integer"] == [1, 2]
359267 [("string", "value"), ("integer", "1"), ("integer", "2")]
360268 )
361269
362 parsed = parser.parse(attrs, request, locations=["cookies"])
270 parsed = parser.parse(attrs, request, location="cookies")
363271
364272 assert parsed["string"] == value
365273 assert parsed["integer"] == [2]
371279 [("string", "value"), ("integer", "1"), ("integer", "2")]
372280 )
373281
374 parsed = parser.parse(attrs, request, locations=["files"])
282 parsed = parser.parse(attrs, request, location="files")
375283
376284 assert parsed["string"] == value
377285 assert parsed["integer"] == [1, 2]
381289
382290 request = make_json_request({})
383291
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):
385294 parser.parse(args, request)
386 assert "Missing data for required field." in str(excinfo)
387295
388296 def test_it_should_parse_multiple_arg_required(self):
389297 args = {"foo": fields.List(fields.Int(), required=True)}
390298 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):
392301 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:
400305 def test_it_should_pass_parsed_as_first_argument(self):
401 class Handler(object):
306 class Handler:
402307 request = make_json_request({"key": "value"})
403308
404309 @use_args({"key": fields.Field()})
413318 assert result is True
414319
415320 def test_it_should_pass_parsed_as_kwargs_arguments(self):
416 class Handler(object):
321 class Handler:
417322 request = make_json_request({"key": "value"})
418323
419324 @use_kwargs({"key": fields.Field()})
428333 assert result is True
429334
430335 def test_it_should_be_validate_arguments_when_validator_is_passed(self):
431 class Handler(object):
336 class Handler:
432337 request = make_json_request({"foo": 41})
433338
434339 @use_kwargs({"foo": fields.Int()}, validate=lambda args: args["foo"] > 42)
486391
487392
488393 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 ""
491396 method = "POST" if body else "GET"
492397 # Need to make a mock connection right now because Tornado 4.0 requires a
493398 # remote_ip in the context attribute. 4.1 addresses this, and this
496401 mock_connection = mock.Mock(spec=tornado.http1connection.HTTP1Connection)
497402 mock_connection.context = mock.Mock()
498403 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 ""
500405 request = tornado.httputil.HTTPServerRequest(
501406 method=method,
502407 uri=uri,
519424 class EchoHandler(tornado.web.RequestHandler):
520425 ARGS = {"name": fields.Str()}
521426
522 @use_args(ARGS)
427 @use_args(ARGS, location="query")
523428 def get(self, args):
524429 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()}
525442
526443 @use_args(ARGS)
527444 def post(self, args):
531448 class EchoWithParamHandler(tornado.web.RequestHandler):
532449 ARGS = {"name": fields.Str()}
533450
534 @use_args(ARGS)
451 @use_args(ARGS, location="query")
535452 def get(self, id, args):
536453 self.write(args)
537454
538455
539456 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 ]
541463 )
542464
543465
547469
548470 def test_post(self):
549471 res = self.fetch(
550 "/echo",
472 "/echo_json",
551473 method="POST",
552474 headers={"Content-Type": "application/json"},
553475 body=json.dumps({"name": "Steve"}),
555477 json_body = parse_json(res.body)
556478 assert json_body["name"] == "Steve"
557479 res = self.fetch(
558 "/echo",
480 "/echo_json",
559481 method="POST",
560482 headers={"Content-Type": "application/json"},
561483 body=json.dumps({}),
587509 def post(self, args):
588510 self.write(args)
589511
590 @use_kwargs(ARGS)
512 @use_kwargs(ARGS, location="query")
591513 def get(self, name):
592514 self.write({"status": "success"})
593515
0 # -*- coding: utf-8 -*-
10 """Tests for the webapp2 parser"""
2 try:
3 from urllib.parse import urlencode
4 except ImportError: # PY2
5 from urllib import urlencode # type: ignore
1 from urllib.parse import urlencode
62 from webargs.core import json
73
84 import pytest
5 import marshmallow as ma
96 from marshmallow import fields, ValidationError
107
118 import webtest
129 import webapp2
1310 from webargs.webapp2parser import parser
11 from webargs.core import MARSHMALLOW_VERSION_INFO
1412
1513 hello_args = {"name": fields.Str(missing="World")}
1614
2422 }
2523
2624
25 class HelloSchema(ma.Schema):
26 name = fields.Str(missing="World", validate=lambda n: len(n) >= 3)
27
28
29 # variant which ignores unknown fields
30 exclude_kwargs = (
31 {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE}
32 )
33 hello_exclude_schema = HelloSchema(**exclude_kwargs)
34
35
2736 def test_parse_querystring_args():
2837 request = webapp2.Request.blank("/echo?name=Fred")
29 assert parser.parse(hello_args, req=request) == {"name": "Fred"}
38 assert parser.parse(hello_args, req=request, location="query") == {"name": "Fred"}
3039
3140
3241 def test_parse_querystring_multiple():
3342 expected = {"name": ["steve", "Loria"]}
3443 request = webapp2.Request.blank("/echomulti?name=steve&name=Loria")
35 assert parser.parse(hello_multiple, req=request) == expected
44 assert parser.parse(hello_multiple, req=request, location="query") == expected
3645
3746
3847 def test_parse_form():
3948 expected = {"name": "Joe"}
4049 request = webapp2.Request.blank("/echo", POST=expected)
41 assert parser.parse(hello_args, req=request) == expected
50 assert parser.parse(hello_args, req=request, location="form") == expected
4251
4352
4453 def test_parse_form_multiple():
4554 expected = {"name": ["steve", "Loria"]}
4655 request = webapp2.Request.blank("/echo", POST=urlencode(expected, doseq=True))
47 assert parser.parse(hello_multiple, req=request) == expected
56 assert parser.parse(hello_multiple, req=request, location="form") == expected
4857
4958
5059 def test_parsing_form_default():
5160 request = webapp2.Request.blank("/echo", POST="")
52 assert parser.parse(hello_args, req=request) == {"name": "World"}
61 assert parser.parse(hello_args, req=request, location="form") == {"name": "World"}
5362
5463
5564 def test_parse_json():
5867 "/echo", POST=json.dumps(expected), headers={"content-type": "application/json"}
5968 )
6069 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"}
6179
6280
6381 def test_parse_invalid_json():
94112 request = webapp2.Request.blank(
95113 "/", headers={"Cookie": response.headers["Set-Cookie"]}
96114 )
97 assert parser.parse(hello_args, req=request, locations=("cookies",)) == expected
115 assert parser.parse(hello_args, req=request, location="cookies") == expected
98116
99117
100118 def test_parsing_headers():
101119 expected = {"name": "Fred"}
102120 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 )
104124
105125
106126 def test_parse_files():
109129 """
110130
111131 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")
113133 def post(self, args):
114134 self.response.content_type = "application/json"
115135
116136 def _value(f):
117137 return f.getvalue().decode("utf-8")
118138
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"]}
120140 self.response.write(json.dumps(data))
121141
122142 app = webapp2.WSGIApplication([("/", Handler)])
129149 def test_exception_on_validation_error():
130150 request = webapp2.Request.blank("/", POST={"num": "3"})
131151 with pytest.raises(ValidationError):
132 parser.parse(hello_validate, req=request)
152 parser.parse(hello_validate, req=request, location="form")
133153
134154
135155 def test_validation_error_with_message():
136156 request = webapp2.Request.blank("/", POST={"num": "3"})
137157 with pytest.raises(ValidationError) as exc:
138 parser.parse(hello_validate, req=request)
158 parser.parse(hello_validate, req=request, location="form")
139159 assert "Houston, we've had a problem." in exc.value
140160
141161
147167 request = webapp2.Request.blank("/echo", POST=expected)
148168 app = webapp2.WSGIApplication([])
149169 app.set_globals(app, request)
150 assert parser.parse(hello_args) == expected
170 assert parser.parse(hello_args, location="form") == expected
00 [tox]
11 envlist=
22 lint
3 py{27,35,36,37}-marshmallow{2,3}
3 py{35,36,37,38}-marshmallow2
4 py{35,36,37,38}-marshmallow3
5 py38-marshmallowdev
46 docs
57
68 [testenv]
810 deps =
911 marshmallow2: marshmallow==2.15.2
1012 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}
1415
1516 [testenv:lint]
16 deps = pre-commit~=1.14
17 deps = pre-commit~=1.20
1718 skip_install = true
1819 commands = pre-commit run --all-files
1920
2021 [testenv:docs]
21 deps = -rdocs/requirements.txt
22 extras =
22 extras = docs
2323 commands = sphinx-build docs/ docs/_build {posargs}
2424
2525 ; Below tasks are for development only (not run in CI)
2626
2727 [testenv:watch-docs]
2828 deps =
29 -rdocs/requirements.txt
3029 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
3332
3433 [testenv:watch-readme]
3534 deps = restview
+0
-15
webargs/__init__.py less more
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
-176
webargs/aiohttpparser.py less more
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
-221
webargs/asyncparser.py less more
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
-90
webargs/bottleparser.py less more
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
-576
webargs/core.py less more
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
-85
webargs/djangoparser.py less more
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
-165
webargs/falconparser.py less more
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
-74
webargs/fields.py less more
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
-120
webargs/flaskparser.py less more
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
-173
webargs/pyramidparser.py less more
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
-207
webargs/testing.py less more
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
-157
webargs/tornadoparser.py less more
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
-80
webargs/webapp2parser.py less more
0 # -*- coding: utf-8 -*-
1 """Webapp2 request argument parsing module.
2
3 Example: ::
4
5 import webapp2
6
7 from marshmallow import fields
8 from webargs.webobparser import use_args
9
10 hello_args = {
11 'name': fields.Str(missing='World')
12 }
13
14 class MainPage(webapp2.RequestHandler):
15
16 @use_args(hello_args)
17 def get_args(self, args):
18 self.response.write('Hello, {name}!'.format(name=args['name']))
19
20 @use_kwargs(hello_args)
21 def get_kwargs(self, name=None):
22 self.response.write('Hello, {name}!'.format(name=name))
23
24 app = webapp2.WSGIApplication([
25 webapp2.Route(r'/hello', MainPage, handler_method='get_args'),
26 webapp2.Route(r'/hello_dict', MainPage, handler_method='get_kwargs'),
27 ], debug=True)
28 """
29 import webapp2
30 import webob.multidict
31
32 from webargs import core
33 from webargs.core import json
34
35
36 class Webapp2Parser(core.Parser):
37 """webapp2 request argument parser."""
38
39 def parse_json(self, req, name, field):
40 """Pull a json value from the request."""
41 json_data = self._cache.get("json")
42 if json_data is None:
43 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