Import upstream version 6.1.1
Kali Janitor
3 years ago
0 | 0 | repos: |
1 | - repo: https://github.com/python/black | |
2 | rev: 19.3b0 | |
1 | - repo: https://github.com/asottile/pyupgrade | |
2 | rev: v2.7.2 | |
3 | hooks: | |
4 | - id: pyupgrade | |
5 | args: ["--py3-plus", "--keep-mock"] | |
6 | - repo: https://github.com/psf/black | |
7 | rev: 20.8b1 | |
3 | 8 | hooks: |
4 | 9 | - id: black |
5 | language_version: python3 | |
6 | 10 | - repo: https://gitlab.com/pycqa/flake8 |
7 | rev: 3.7.8 | |
11 | rev: 3.8.3 | |
8 | 12 | hooks: |
9 | 13 | - id: flake8 |
10 | additional_dependencies: ['flake8-bugbear==19.8.0; python_version >= "3.5"'] | |
14 | additional_dependencies: [flake8-bugbear==20.1.0] | |
11 | 15 | - repo: https://github.com/asottile/blacken-docs |
12 | rev: v1.3.0 | |
16 | rev: v1.8.0 | |
13 | 17 | hooks: |
14 | 18 | - id: blacken-docs |
15 | additional_dependencies: [black==19.3b0] | |
19 | additional_dependencies: [black==20.8b1] | |
20 | args: ["--target-version", "py35"] | |
16 | 21 | - repo: https://github.com/pre-commit/mirrors-mypy |
17 | rev: v0.730 | |
22 | rev: v0.782 | |
18 | 23 | hooks: |
19 | 24 | - id: mypy |
20 | 25 | language_version: python3 |
2 | 2 | configuration: docs/conf.py |
3 | 3 | formats: all |
4 | 4 | python: |
5 | version: 3.7 | |
5 | version: 3.8 | |
6 | 6 | install: |
7 | 7 | - method: pip |
8 | 8 | path: . |
4 | 4 | Lead |
5 | 5 | ---- |
6 | 6 | |
7 | * Steven Loria <[email protected]> | |
8 | * Jérôme Lafréchoux <https://github.com/lafrech> | |
7 | * Steven Loria `@sloria <https://github.com/sloria>`_ | |
8 | * Jérôme Lafréchoux `@lafrech <https://github.com/lafrech>`_ | |
9 | 9 | |
10 | 10 | Contributors (chronological) |
11 | 11 | ---------------------------- |
12 | 12 | |
13 | * @venuatu <https://github.com/venuatu> | |
14 | * Javier Santacruz @jvrsantacruz <[email protected]> | |
15 | * Josh Carp <https://github.com/jmcarp> | |
16 | * @philtay <https://github.com/philtay> | |
17 | * Andriy Yurchuk <https://github.com/Ch00k> | |
18 | * Stas Sușcov <https://github.com/stas> | |
19 | * Josh Johnston <https://github.com/Trii> | |
20 | * Rory Hart <https://github.com/hartror> | |
21 | * Jace Browning <https://github.com/jacebrowning> | |
22 | * @marcellarius <https://github.com/marcellarius> | |
23 | * Damian Heard <https://github.com/DamianHeard> | |
24 | * Daniel Imhoff <https://github.com/dwieeb> | |
25 | * immerrr <https://github.com/immerrr> | |
26 | * Brett Higgins <https://github.com/brettdh> | |
27 | * Vlad Frolov <https://github.com/frol> | |
28 | * Tuukka Mustonen <https://github.com/tuukkamustonen> | |
29 | * Francois-Xavier Darveau <https://github.com/EFF> | |
30 | * Jérôme Lafréchoux <https://github.com/lafrech> | |
31 | * @DmitriyS <https://github.com/DmitriyS> | |
32 | * Svetlozar Argirov <https://github.com/zaro> | |
33 | * Florian S. <https://github.com/nebularazer> | |
34 | * @daniel98321 <https://github.com/daniel98321> | |
35 | * @Itayazolay <https://github.com/Itayazolay> | |
36 | * @Reskov <https://github.com/Reskov> | |
37 | * @cedzz <https://github.com/cedzz> | |
38 | * F. Moukayed (כוכב) <https://github.com/kochab> | |
39 | * Xiaoyu Lee <https://github.com/lee3164> | |
40 | * Jonathan Angelo <https://github.com/jangelo> | |
41 | * @zhenhua32 <https://github.com/zhenhua32> | |
42 | * Martin Roy <https://github.com/lindycoder> | |
13 | * Steven Manuatu `@venuatu <https://github.com/venuatu>`_ | |
14 | * Javier Santacruz `@jvrsantacruz <https://github.com/jvrsantacruz>`_ | |
15 | * Josh Carp `@jmcarp <https://github.com/jmcarp>`_ | |
16 | * `@philtay <https://github.com/philtay>`_ | |
17 | * Andriy Yurchuk `@Ch00k <https://github.com/Ch00k>`_ | |
18 | * Stas Sușcov `@stas <https://github.com/stas>`_ | |
19 | * Josh Johnston `@Trii <https://github.com/Trii>`_ | |
20 | * Rory Hart `@hartror <https://github.com/hartror>`_ | |
21 | * Jace Browning `@jacebrowning <https://github.com/jacebrowning>`_ | |
22 | * marcellarius `@marcellarius <https://github.com/marcellarius>`_ | |
23 | * Damian Heard `@DamianHeard <https://github.com/DamianHeard>`_ | |
24 | * Daniel Imhoff `@dwieeb <https://github.com/dwieeb>`_ | |
25 | * `@immerrr <https://github.com/immerrr>`_ | |
26 | * Brett Higgins `@brettdh <https://github.com/brettdh>`_ | |
27 | * Vlad Frolov `@frol <https://github.com/frol>`_ | |
28 | * Tuukka Mustonen `@tuukkamustonen <https://github.com/tuukkamustonen>`_ | |
29 | * Francois-Xavier Darveau `@EFF <https://github.com/EFF>`_ | |
30 | * Jérôme Lafréchoux `@lafrech <https://github.com/lafrech>`_ | |
31 | * `@DmitriyS <https://github.com/DmitriyS>`_ | |
32 | * Svetlozar Argirov `@zaro <https://github.com/zaro>`_ | |
33 | * Florian S. `@nebularazer <https://github.com/nebularazer>`_ | |
34 | * `@daniel98321 <https://github.com/daniel98321>`_ | |
35 | * `@Itayazolay <https://github.com/Itayazolay>`_ | |
36 | * `@Reskov <https://github.com/Reskov>`_ | |
37 | * `@cedzz <https://github.com/cedzz>`_ | |
38 | * F. Moukayed (כוכב) `@kochab <https://github.com/kochab>`_ | |
39 | * Xiaoyu Lee `@lee3164 <https://github.com/lee3164>`_ | |
40 | * Jonathan Angelo `@jangelo <https://github.com/jangelo>`_ | |
41 | * `@zhenhua32 <https://github.com/zhenhua32>`_ | |
42 | * Martin Roy `@lindycoder <https://github.com/lindycoder>`_ | |
43 | * Kubilay Kocak `@koobs <https://github.com/koobs>`_ | |
44 | * Stephen Rosen `@sirosen <https://github.com/sirosen>`_ | |
45 | * `@dodumosu <https://github.com/dodumosu>`_ | |
46 | * Nate Dellinger `@Nateyo <https://github.com/Nateyo>`_ | |
47 | * Karthikeyan Singaravelan `@tirkarthi <https://github.com/tirkarthi>`_ | |
48 | * Sami Salonen `@suola <https://github.com/suola>`_ | |
49 | * Tim Gates `@timgates42 <https://github.com/timgates42>`_ | |
50 | * Lefteris Karapetsas `@lefterisjp <https://github.com/lefterisjp>`_ | |
51 | * Utku Gultopu `@ugultopu <https://github.com/ugultopu>`_ | |
52 | * Jason Williams `@jaswilli <https://github.com/jaswilli>`_ |
0 | 0 | Changelog |
1 | 1 | --------- |
2 | 2 | |
3 | 6.1.1 (2020-09-08) | |
4 | ****************** | |
5 | ||
6 | Bug fixes: | |
7 | ||
8 | * Failure to validate flask headers would produce error data which contained | |
9 | tuples as keys, and was therefore not JSON-serializable. (:issue:`500`) | |
10 | These errors will now extract the headername as the key correctly. | |
11 | Thanks to :user:`shughes-uk` for reporting. | |
12 | ||
13 | 6.1.0 (2020-04-05) | |
14 | ****************** | |
15 | ||
16 | Features: | |
17 | ||
18 | * Add ``fields.DelimitedTuple`` when using marshmallow 3. This behaves as a | |
19 | combination of ``fields.DelimitedList`` and ``marshmallow.fields.Tuple``. It | |
20 | takes an iterable of fields, plus a delimiter (defaults to ``,``), and parses | |
21 | delimiter-separated strings into tuples. (:pr:`509`) | |
22 | ||
23 | * Add ``__str__`` and ``__repr__`` to MultiDictProxy to make it easier to work | |
24 | with (:pr:`488`) | |
25 | ||
26 | Support: | |
27 | ||
28 | * Various docs updates (:pr:`482`, :pr:`486`, :pr:`489`, :pr:`498`, :pr:`508`). | |
29 | Thanks :user:`lefterisjp`, :user:`timgates42`, and :user:`ugultopu` for the PRs. | |
30 | ||
31 | ||
32 | 6.0.0 (2020-02-27) | |
33 | ****************** | |
34 | ||
35 | Features: | |
36 | ||
37 | * ``FalconParser``: Pass request content length to ``req.stream.read`` to | |
38 | provide compatibility with ``falcon.testing`` (:pr:`477`). | |
39 | Thanks :user:`suola` for the PR. | |
40 | ||
41 | * *Backwards-incompatible*: Factorize the ``use_args`` / ``use_kwargs`` branch | |
42 | in all parsers. When ``as_kwargs`` is ``False``, arguments are now | |
43 | consistently appended to the arguments list by the ``use_args`` decorator. | |
44 | Before this change, the ``PyramidParser`` would prepend the argument list on | |
45 | each call to ``use_args``. Pyramid view functions must reverse the order of | |
46 | their arguments. (:pr:`478`) | |
47 | ||
48 | 6.0.0b8 (2020-02-16) | |
49 | ******************** | |
50 | ||
51 | Refactoring: | |
52 | ||
53 | * *Backwards-incompatible*: Use keyword-only arguments (:pr:`472`). | |
54 | ||
55 | 6.0.0b7 (2020-02-14) | |
56 | ******************** | |
57 | ||
58 | Features: | |
59 | ||
60 | * *Backwards-incompatible*: webargs will rewrite the error messages in | |
61 | ValidationErrors to be namespaced under the location which raised the error. | |
62 | The `messages` field on errors will therefore be one layer deeper with a | |
63 | single top-level key. | |
64 | ||
65 | 6.0.0b6 (2020-01-31) | |
66 | ******************** | |
67 | ||
68 | Refactoring: | |
69 | ||
70 | * Remove the cache attached to webargs parsers. Due to changes between webargs | |
71 | v5 and v6, the cache is no longer considered useful. | |
72 | ||
73 | Other changes: | |
74 | ||
75 | * Import ``Mapping`` from ``collections.abc`` in pyramidparser.py (:pr:`471`). | |
76 | Thanks :user:`tirkarthi` for the PR. | |
77 | ||
78 | 6.0.0b5 (2020-01-30) | |
79 | ******************** | |
80 | ||
81 | Refactoring: | |
82 | ||
83 | * *Backwards-incompatible*: `DelimitedList` now requires that its input be a | |
84 | string and always serializes as a string. It can still serialize and deserialize | |
85 | using another field, e.g. `DelimitedList(Int())` is still valid and requires | |
86 | that the values in the list parse as ints. | |
87 | ||
88 | 6.0.0b4 (2020-01-28) | |
89 | ******************** | |
90 | ||
91 | Bug fixes: | |
92 | ||
93 | * :cve:`CVE-2020-7965`: Don't attempt to parse JSON if request's content type is mismatched | |
94 | (bugfix from 5.5.3). | |
95 | ||
96 | 6.0.0b3 (2020-01-21) | |
97 | ******************** | |
98 | ||
99 | Features: | |
100 | ||
101 | * *Backwards-incompatible*: Support Falcon 2.0. Drop support for Falcon 1.x | |
102 | (:pr:`459`). Thanks :user:`dodumosu` and :user:`Nateyo` for the PR. | |
103 | ||
104 | 6.0.0b2 (2020-01-07) | |
105 | ******************** | |
106 | ||
107 | Other changes: | |
108 | ||
109 | * *Backwards-incompatible*: Drop support for Python 2 (:issue:`440`). | |
110 | Thanks :user:`hugovk` for the PR. | |
111 | ||
112 | 6.0.0b1 (2020-01-06) | |
113 | ******************** | |
114 | ||
115 | Features: | |
116 | ||
117 | * *Backwards-incompatible*: Schemas will now load all data from a location, not | |
118 | only data specified by fields. As a result, schemas with validators which | |
119 | examine the full input data may change in behavior. The `unknown` parameter | |
120 | on schemas may be used to alter this. For example, | |
121 | `unknown=marshmallow.EXCLUDE` will produce a behavior similar to webargs v5. | |
122 | ||
123 | Bug fixes: | |
124 | ||
125 | * *Backwards-incompatible*: All parsers now require the Content-Type to be set | |
126 | correctly when processing JSON request bodies. This impacts ``DjangoParser``, | |
127 | ``FalconParser``, ``FlaskParser``, and ``PyramidParser`` | |
128 | ||
129 | Refactoring: | |
130 | ||
131 | * *Backwards-incompatible*: Schema fields may not specify a location any | |
132 | longer, and `Parser.use_args` and `Parser.use_kwargs` now accept `location` | |
133 | (singular) instead of `locations` (plural). Instead of using a single field or | |
134 | schema with multiple `locations`, users are recommended to make multiple | |
135 | calls to `use_args` or `use_kwargs` with a distinct schema per location. For | |
136 | example, code should be rewritten like this: | |
137 | ||
138 | .. code-block:: python | |
139 | ||
140 | # webargs 5.x and older | |
141 | @parser.use_args( | |
142 | { | |
143 | "q1": ma.fields.Int(location="query"), | |
144 | "q2": ma.fields.Int(location="query"), | |
145 | "h1": ma.fields.Int(location="headers"), | |
146 | }, | |
147 | locations=("query", "headers"), | |
148 | ) | |
149 | def foo(q1, q2, h1): | |
150 | ... | |
151 | ||
152 | ||
153 | # webargs 6.x | |
154 | @parser.use_args({"q1": ma.fields.Int(), "q2": ma.fields.Int()}, location="query") | |
155 | @parser.use_args({"h1": ma.fields.Int()}, location="headers") | |
156 | def foo(q1, q2, h1): | |
157 | ... | |
158 | ||
159 | * The `location_handler` decorator has been removed and replaced with | |
160 | `location_loader`. `location_loader` serves the same purpose (letting you | |
161 | write custom hooks for loading data) but its expected method signature is | |
162 | different. See the docs on `location_loader` for proper usage. | |
163 | ||
164 | Thanks :user:`sirosen` for the PR! | |
165 | ||
3 | 166 | 5.5.3 (2020-01-28) |
4 | 167 | ****************** |
5 | 168 | |
6 | 169 | Bug fixes: |
7 | 170 | |
8 | * :cve:`CVE-2020-7965`: Don't attempt to parse JSON if the request's Content-Type is mismatched. | |
171 | * :cve:`CVE-2020-7965`: Don't attempt to parse JSON if request's content type is mismatched. | |
9 | 172 | |
10 | 173 | 5.5.2 (2019-10-06) |
11 | 174 | ****************** |
121 | 121 | Documentation |
122 | 122 | +++++++++++++ |
123 | 123 | |
124 | Contributions to the documentation are welcome. Documentation is written in `reStructured Text`_ (rST). A quick rST reference can be found `here <http://docutils.sourceforge.net/docs/user/rst/quickref.html>`_. Builds are powered by Sphinx_. | |
124 | Contributions to the documentation are welcome. Documentation is written in `reStructuredText`_ (rST). A quick rST reference can be found `here <https://docutils.sourceforge.io/docs/user/rst/quickref.html>`_. Builds are powered by Sphinx_. | |
125 | 125 | |
126 | 126 | To build the docs in "watch" mode: :: |
127 | 127 | |
136 | 136 | |
137 | 137 | |
138 | 138 | .. _Sphinx: http://sphinx.pocoo.org/ |
139 | .. _`reStructured Text`: http://docutils.sourceforge.net/rst.html | |
139 | .. _`reStructuredText`: https://docutils.sourceforge.io/rst.html | |
140 | 140 | .. _webargs: https://github.com/marshmallow-code/webargs |
0 | Copyright 2014-2019 Steven Loria and contributors | |
0 | Copyright 2014-2020 Steven Loria and contributors | |
1 | 1 | |
2 | 2 | Permission is hereby granted, free of charge, to any person obtaining a copy |
3 | 3 | of this software and associated documentation files (the "Software"), to deal |
35 | 35 | |
36 | 36 | |
37 | 37 | @app.route("/") |
38 | @use_args({"name": fields.Str(required=True)}) | |
38 | @use_args({"name": fields.Str(required=True)}, location="query") | |
39 | 39 | def index(args): |
40 | 40 | return "Hello " + args["name"] |
41 | 41 | |
53 | 53 | |
54 | 54 | pip install -U webargs |
55 | 55 | |
56 | webargs supports Python >= 2.7 or >= 3.5. | |
56 | webargs supports Python >= 3.5. | |
57 | 57 | |
58 | 58 | |
59 | 59 | Documentation |
25 | 25 | parameters: |
26 | 26 | toxenvs: |
27 | 27 | - lint |
28 | - py27-marshmallow2 | |
29 | 28 | |
30 | 29 | - py35-marshmallow2 |
31 | 30 | - py35-marshmallow3 |
32 | 31 | |
33 | - py36-marshmallow2 | |
34 | 32 | - py36-marshmallow3 |
35 | 33 | |
36 | - py37-marshmallow2 | |
37 | 34 | - py37-marshmallow3 |
38 | 35 | |
39 | - py37-marshmallowdev | |
36 | - py38-marshmallow2 | |
37 | - py38-marshmallow3 | |
38 | ||
39 | - py38-marshmallowdev | |
40 | 40 | |
41 | 41 | - docs |
42 | 42 | os: linux |
43 | # Build separate wheels for python 2 and 3 | |
43 | # Build wheels | |
44 | 44 | - template: job--pypi-release.yml@sloria |
45 | 45 | parameters: |
46 | 46 | python: "3.7" |
47 | 47 | distributions: "sdist bdist_wheel" |
48 | name_postfix: "_py3" | |
49 | 48 | dependsOn: |
50 | 49 | - tox_linux |
51 | - template: job--pypi-release.yml@sloria | |
52 | parameters: | |
53 | python: "2.7" | |
54 | distributions: "bdist_wheel" | |
55 | name_postfix: "_py2" | |
56 | dependsOn: | |
57 | - tox_linux |
5 | 5 | Custom Location Handlers |
6 | 6 | ------------------------ |
7 | 7 | |
8 | To add your own custom location handler, write a function that receives a request, an argument name, and a :class:`Field <marshmallow.fields.Field>`, then decorate that function with :func:`Parser.location_handler <webargs.core.Parser.location_handler>`. | |
8 | To add your own custom location handler, write a function that receives a request, and a :class:`Schema <marshmallow.Schema>`, then decorate that function with :func:`Parser.location_loader <webargs.core.Parser.location_loader>`. | |
9 | 9 | |
10 | 10 | |
11 | 11 | .. code-block:: python |
14 | 14 | from webargs.flaskparser import parser |
15 | 15 | |
16 | 16 | |
17 | @parser.location_handler("data") | |
18 | def parse_data(request, name, field): | |
19 | return request.data.get(name) | |
17 | @parser.location_loader("data") | |
18 | def load_data(request, schema): | |
19 | return request.data | |
20 | 20 | |
21 | 21 | |
22 | 22 | # Now 'data' can be specified as a location |
23 | @parser.use_args({"per_page": fields.Int()}, locations=("data",)) | |
23 | @parser.use_args({"per_page": fields.Int()}, location="data") | |
24 | 24 | def posts(args): |
25 | 25 | return "displaying {} posts".format(args["per_page"]) |
26 | 26 | |
27 | ||
28 | .. NOTE:: | |
29 | ||
30 | The schema is passed so that it can be used to wrap multidict types and | |
31 | unpack List fields correctly. If you are writing a loader for a multidict | |
32 | type, consider looking at | |
33 | :class:`MultiDictProxy <webargs.multidictproxy.MultiDictProxy>` for an | |
34 | example of how to do this. | |
35 | ||
36 | "meta" Locations | |
37 | ~~~~~~~~~~~~~~~~ | |
38 | ||
39 | You can define your own locations which mix data from several existing | |
40 | locations. | |
41 | ||
42 | The `json_or_form` location does this -- first trying to load data as JSON and | |
43 | then falling back to a form body -- and its implementation is quite simple: | |
44 | ||
45 | ||
46 | .. code-block:: python | |
47 | ||
48 | def load_json_or_form(self, req, schema): | |
49 | """Load data from a request, accepting either JSON or form-encoded | |
50 | data. | |
51 | ||
52 | The data will first be loaded as JSON, and, if that fails, it will be | |
53 | loaded as a form post. | |
54 | """ | |
55 | data = self.load_json(req, schema) | |
56 | if data is not missing: | |
57 | return data | |
58 | return self.load_form(req, schema) | |
59 | ||
60 | ||
61 | You can imagine your own locations with custom behaviors like this. | |
62 | For example, to mix query parameters and form body data, you might write the | |
63 | following: | |
64 | ||
65 | .. code-block:: python | |
66 | ||
67 | from webargs import fields | |
68 | from webargs.multidictproxy import MultiDictProxy | |
69 | from webargs.flaskparser import parser | |
70 | ||
71 | ||
72 | @parser.location_loader("query_and_form") | |
73 | def load_data(request, schema): | |
74 | # relies on the Flask (werkzeug) MultiDict type's implementation of | |
75 | # these methods, but when you're extending webargs, you may know things | |
76 | # about your framework of choice | |
77 | newdata = request.args.copy() | |
78 | newdata.update(request.form) | |
79 | return MultiDictProxy(newdata, schema) | |
80 | ||
81 | ||
82 | # Now 'query_and_form' means you can send these values in either location, | |
83 | # and they will be *mixed* together into a new dict to pass to your schema | |
84 | @parser.use_args({"favorite_food": fields.String()}, location="query_and_form") | |
85 | def set_favorite_food(args): | |
86 | ... # do stuff | |
87 | return "your favorite food is now set to {}".format(args["favorite_food"]) | |
27 | 88 | |
28 | 89 | marshmallow Integration |
29 | 90 | ----------------------- |
63 | 124 | |
64 | 125 | |
65 | 126 | # You can add additional parameters |
66 | @use_kwargs({"posts_per_page": fields.Int(missing=10, location="query")}) | |
127 | @use_kwargs({"posts_per_page": fields.Int(missing=10)}, location="query") | |
67 | 128 | @use_args(UserSchema()) |
68 | 129 | def profile_posts(args, posts_per_page): |
69 | 130 | username = args["username"] |
72 | 133 | .. warning:: |
73 | 134 | If you're using marshmallow 2, you should always set ``strict=True`` (either as a ``class Meta`` option or in the Schema's constructor) when passing a schema to webargs. This will ensure that the parser's error handler is invoked when expected. |
74 | 135 | |
75 | .. warning:: | |
76 | Any `Schema <marshmallow.Schema>` passed to `use_kwargs <webargs.core.Parser.use_kwargs>` MUST deserialize to a dictionary of data. Keep this in mind when writing `post_load <marshmallow.decorators.post_load>` methods. | |
136 | ||
137 | When to avoid `use_kwargs` | |
138 | -------------------------- | |
139 | ||
140 | Any `Schema <marshmallow.Schema>` passed to `use_kwargs <webargs.core.Parser.use_kwargs>` MUST deserialize to a dictionary of data. | |
141 | If your schema has a `post_load <marshmallow.decorators.post_load>` method | |
142 | that returns a non-dictionary, | |
143 | you should use `use_args <webargs.core.Parser.use_args>` instead. | |
144 | ||
145 | .. code-block:: python | |
146 | ||
147 | from marshmallow import Schema, fields, post_load | |
148 | from webargs.flaskparser import use_args | |
149 | ||
150 | ||
151 | class Rectangle: | |
152 | def __init__(self, length, width): | |
153 | self.length = length | |
154 | self.width = width | |
155 | ||
156 | ||
157 | class RectangleSchema(Schema): | |
158 | length = fields.Float() | |
159 | width = fields.Float() | |
160 | ||
161 | @post_load | |
162 | def make_object(self, data, **kwargs): | |
163 | return Rectangle(**data) | |
164 | ||
165 | ||
166 | @use_args(RectangleSchema) | |
167 | def post(self, rect: Rectangle): | |
168 | return f"Area: {rect.length * rect.width}" | |
169 | ||
170 | Packages such as `marshmallow-sqlalchemy <https://github.com/marshmallow-code/marshmallow-sqlalchemy>`_ and `marshmallow-dataclass <https://github.com/lovasoa/marshmallow_dataclass>`_ generate schemas that deserialize to non-dictionary objects. | |
171 | Therefore, `use_args <webargs.core.Parser.use_args>` should be used with those schemas. | |
77 | 172 | |
78 | 173 | |
79 | 174 | Schema Factories |
176 | 271 | cube = args["cube"] |
177 | 272 | # ... |
178 | 273 | |
179 | .. _custom-parsers: | |
274 | .. _custom-loaders: | |
180 | 275 | |
181 | 276 | Custom Parsers |
182 | 277 | -------------- |
183 | 278 | |
184 | To add your own parser, extend :class:`Parser <webargs.core.Parser>` and implement the `parse_*` method(s) you need to override. For example, here is a custom Flask parser that handles nested query string arguments. | |
279 | To add your own parser, extend :class:`Parser <webargs.core.Parser>` and implement the `load_*` method(s) you need to override. For example, here is a custom Flask parser that handles nested query string arguments. | |
185 | 280 | |
186 | 281 | |
187 | 282 | .. code-block:: python |
210 | 305 | } |
211 | 306 | """ |
212 | 307 | |
213 | def parse_querystring(self, req, name, field): | |
214 | return core.get_value(_structure_dict(req.args), name, field) | |
308 | def load_querystring(self, req, schema): | |
309 | return _structure_dict(req.args) | |
215 | 310 | |
216 | 311 | |
217 | 312 | def _structure_dict(dict_): |
274 | 369 | |
275 | 370 | |
276 | 371 | @app.route("/profile/", methods=["patch"]) |
277 | @use_args(PatchSchema(many=True), locations=("json",)) | |
372 | @use_args(PatchSchema(many=True)) | |
278 | 373 | def patch_blog(args): |
279 | 374 | """Implements JSON Patch for the user profile |
280 | 375 | |
289 | 384 | Mixing Locations |
290 | 385 | ---------------- |
291 | 386 | |
292 | Arguments for different locations can be specified by passing ``location`` to each field individually: | |
293 | ||
294 | .. code-block:: python | |
295 | ||
387 | Arguments for different locations can be specified by passing ``location`` to each `use_args <webargs.core.Parser.use_args>` call: | |
388 | ||
389 | .. code-block:: python | |
390 | ||
391 | # "json" is the default, used explicitly below | |
296 | 392 | @app.route("/stacked", methods=["POST"]) |
297 | @use_args( | |
298 | { | |
299 | "page": fields.Int(location="query"), | |
300 | "q": fields.Str(location="query"), | |
301 | "name": fields.Str(location="json"), | |
302 | } | |
303 | ) | |
304 | def viewfunc(args): | |
305 | page = args["page"] | |
306 | # ... | |
307 | ||
308 | Alternatively, you can pass multiple locations to `use_args <webargs.core.Parser.use_args>`: | |
309 | ||
310 | .. code-block:: python | |
311 | ||
312 | @app.route("/stacked", methods=["POST"]) | |
313 | @use_args( | |
314 | {"page": fields.Int(), "q": fields.Str(), "name": fields.Str()}, | |
315 | locations=("query", "json"), | |
316 | ) | |
317 | def viewfunc(args): | |
318 | page = args["page"] | |
319 | # ... | |
320 | ||
321 | However, this allows ``page`` and ``q`` to be passed in the request body and ``name`` to be passed as a query parameter. | |
322 | ||
323 | To restrict the arguments to single locations without having to pass ``location`` to every field, you can call the `use_args <webargs.core.Parser.use_args>` multiple times: | |
324 | ||
325 | .. code-block:: python | |
326 | ||
327 | query_args = {"page": fields.Int(), "q": fields.Int()} | |
328 | json_args = {"name": fields.Str()} | |
329 | ||
330 | ||
331 | @app.route("/stacked", methods=["POST"]) | |
332 | @use_args(query_args, locations=("query",)) | |
333 | @use_args(json_args, locations=("json",)) | |
393 | @use_args({"page": fields.Int(), "q": fields.Str()}, location="query") | |
394 | @use_args({"name": fields.Str()}, location="json") | |
334 | 395 | def viewfunc(query_parsed, json_parsed): |
335 | 396 | page = query_parsed["page"] |
336 | 397 | name = json_parsed["name"] |
342 | 403 | |
343 | 404 | import functools |
344 | 405 | |
345 | query = functools.partial(use_args, locations=("query",)) | |
346 | body = functools.partial(use_args, locations=("json",)) | |
347 | ||
348 | ||
349 | @query(query_args) | |
350 | @body(json_args) | |
406 | query = functools.partial(use_args, location="query") | |
407 | body = functools.partial(use_args, location="json") | |
408 | ||
409 | ||
410 | @query({"page": fields.Int(), "q": fields.Int()}) | |
411 | @body({"name": fields.Str()}) | |
351 | 412 | def viewfunc(query_parsed, json_parsed): |
352 | 413 | page = query_parsed["page"] |
353 | 414 | name = json_parsed["name"] |
14 | 14 | |
15 | 15 | .. automodule:: webargs.fields |
16 | 16 | :members: Nested, DelimitedList |
17 | ||
18 | ||
19 | webargs.multidictproxy | |
20 | ---------------------- | |
21 | ||
22 | .. automodule:: webargs.multidictproxy | |
23 | :members: | |
24 | ||
17 | 25 | |
18 | 26 | webargs.asyncparser |
19 | 27 | ------------------- |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | import datetime as dt |
2 | 1 | import sys |
3 | 2 | import os |
36 | 35 | |
37 | 36 | html_domain_indices = False |
38 | 37 | source_suffix = ".rst" |
39 | project = u"webargs" | |
40 | copyright = u"2014-{0:%Y}, Steven Loria and contributors".format(dt.datetime.utcnow()) | |
38 | project = "webargs" | |
39 | copyright = "2014-{:%Y}, Steven Loria and contributors".format(dt.datetime.utcnow()) | |
41 | 40 | version = release = webargs.__version__ |
42 | 41 | templates_path = ["_templates"] |
43 | 42 | exclude_patterns = ["_build"] |
21 | 21 | |
22 | 22 | |
23 | 23 | @app.route("/user/<int:uid>") |
24 | @use_args({"per_page": fields.Int()}) | |
24 | @use_args({"per_page": fields.Int()}, location="query") | |
25 | 25 | def user_detail(args, uid): |
26 | return ("The user page for user {uid}, " "showing {per_page} posts.").format( | |
26 | return ("The user page for user {uid}, showing {per_page} posts.").format( | |
27 | 27 | uid=uid, per_page=args["per_page"] |
28 | 28 | ) |
29 | 29 | |
63 | 63 | |
64 | 64 | |
65 | 65 | @app.route("/greeting/<name>/") |
66 | @use_args({"name": fields.Str(location="view_args")}) | |
66 | @use_args({"name": fields.Str()}, location="view_args") | |
67 | 67 | def greeting(args, **kwargs): |
68 | 68 | return "Hello {}".format(args["name"]) |
69 | 69 | |
94 | 94 | } |
95 | 95 | |
96 | 96 | |
97 | @use_args(account_args) | |
97 | @use_args(account_args, location="form") | |
98 | 98 | def login_user(request, args): |
99 | 99 | if request.method == "POST": |
100 | 100 | login(args["username"], args["password"]) |
113 | 113 | |
114 | 114 | |
115 | 115 | class BlogPostView(View): |
116 | @use_args(blog_args) | |
116 | @use_args(blog_args, location="query") | |
117 | 117 | def get(self, request, args): |
118 | 118 | blog_post = Post.objects.get(title__iexact=args["title"], author=args["author"]) |
119 | 119 | return render_to_response("post_template.html", {"post": blog_post}) |
238 | 238 | from webargs.pyramidparser import use_args |
239 | 239 | |
240 | 240 | |
241 | @use_args({"uid": fields.Str(), "per_page": fields.Int()}) | |
241 | @use_args({"uid": fields.Str(), "per_page": fields.Int()}, location="query") | |
242 | 242 | def user_detail(request, args): |
243 | 243 | uid = args["uid"] |
244 | 244 | return Response( |
260 | 260 | from webargs.pyramidparser import use_args |
261 | 261 | |
262 | 262 | |
263 | @use_args({"mymatch": fields.Int()}, locations=("matchdict",)) | |
263 | @use_args({"mymatch": fields.Int()}, location="matchdict") | |
264 | 264 | def matched(request, args): |
265 | 265 | return Response("The value for mymatch is {}".format(args["mymatch"])) |
266 | 266 | |
309 | 309 | |
310 | 310 | |
311 | 311 | def add_args(argmap, **kwargs): |
312 | def hook(req, resp, params): | |
312 | def hook(req, resp, resource, params): | |
313 | 313 | parsed_args = parser.parse(argmap, req=req, **kwargs) |
314 | 314 | req.context["args"] = parsed_args |
315 | 315 | |
316 | 316 | return hook |
317 | 317 | |
318 | 318 | |
319 | @falcon.before(add_args({"page": fields.Int(location="query")})) | |
319 | @falcon.before(add_args({"page": fields.Int()}, location="query")) | |
320 | 320 | class AuthorResource: |
321 | 321 | def on_get(self, req, resp): |
322 | 322 | args = req.context["args"] |
413 | 413 | from webargs.aiohttpparser import use_args |
414 | 414 | |
415 | 415 | |
416 | @parser.use_args({"slug": fields.Str(location="match_info")}) | |
416 | @parser.use_args({"slug": fields.Str()}, location="match_info") | |
417 | 417 | def article_detail(request, args): |
418 | 418 | return web.Response(body="Slug: {}".format(args["slug"]).encode("utf-8")) |
419 | 419 | |
442 | 442 | |
443 | 443 | @route("/users/<_id:int>", method="GET", apply=use_args(user_args)) |
444 | 444 | def users(args, _id): |
445 | """A welcome page. | |
446 | """ | |
445 | """A welcome page.""" | |
447 | 446 | return {"message": "Welcome, {}!".format(args["name"]), "_id": _id} |
5 | 5 | |
6 | 6 | webargs is a Python library for parsing and validating HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, webapp2, Falcon, and aiohttp. |
7 | 7 | |
8 | Upgrading from an older version? | |
9 | -------------------------------- | |
10 | ||
11 | See the :doc:`Upgrading to Newer Releases <upgrading>` page for notes on getting your code up-to-date with the latest version. | |
12 | ||
13 | ||
14 | Usage and Simple Examples | |
15 | ------------------------- | |
8 | 16 | |
9 | 17 | .. code-block:: python |
10 | 18 | |
16 | 24 | |
17 | 25 | |
18 | 26 | @app.route("/") |
19 | @use_args({"name": fields.Str(required=True)}) | |
27 | @use_args({"name": fields.Str(required=True)}, location="query") | |
20 | 28 | def index(args): |
21 | 29 | return "Hello " + args["name"] |
22 | 30 | |
27 | 35 | # curl http://localhost:5000/\?name\='World' |
28 | 36 | # Hello World |
29 | 37 | |
30 | Webargs will automatically parse: | |
38 | By default Webargs will automatically parse JSON request bodies. But it also | |
39 | has support for: | |
31 | 40 | |
32 | 41 | **Query Parameters** |
33 | 42 | :: |
43 | $ curl http://localhost:5000/\?name\='Freddie' | |
44 | Hello Freddie | |
34 | 45 | |
35 | $ curl http://localhost:5000/\?name\='Freddie' | |
36 | Hello Freddie | |
46 | # pass location="query" to use_args | |
37 | 47 | |
38 | 48 | **Form Data** |
39 | 49 | :: |
41 | 51 | $ curl -d 'name=Brian' http://localhost:5000/ |
42 | 52 | Hello Brian |
43 | 53 | |
54 | # pass location="form" to use_args | |
55 | ||
44 | 56 | **JSON Data** |
45 | 57 | :: |
46 | 58 | |
47 | 59 | $ curl -X POST -H "Content-Type: application/json" -d '{"name":"Roger"}' http://localhost:5000/ |
48 | 60 | Hello Roger |
61 | ||
62 | # pass location="json" (or omit location) to use_args | |
49 | 63 | |
50 | 64 | and, optionally: |
51 | 65 | |
102 | 116 | |
103 | 117 | license |
104 | 118 | changelog |
119 | upgrading | |
105 | 120 | authors |
106 | 121 | contributing |
0 | 0 | Install |
1 | 1 | ======= |
2 | 2 | |
3 | **webargs** requires Python >= 2.7 or >= 3.5. It depends on `marshmallow <https://marshmallow.readthedocs.io/en/latest/>`_ >= 2.7.0. | |
3 | **webargs** requires Python >= 3.5. It depends on `marshmallow <https://marshmallow.readthedocs.io/en/latest/>`_ >= 2.7.0. | |
4 | 4 | |
5 | 5 | From the PyPI |
6 | 6 | ------------- |
22 | 22 | "nickname": fields.List(fields.Str()), |
23 | 23 | # Delimited list, e.g. "/?languages=python,javascript" |
24 | 24 | "languages": fields.DelimitedList(fields.Str()), |
25 | # When you know where an argument should be parsed from | |
26 | "active": fields.Bool(location="query"), | |
27 | 25 | # When value is keyed on a variable-unsafe name |
28 | 26 | # or you want to rename a key |
29 | "content_type": fields.Str(load_from="Content-Type", location="headers"), | |
27 | "user_type": fields.Str(load_from="user-type"), | |
30 | 28 | # OR, on marshmallow 3 |
31 | # "content_type": fields.Str(data_key="Content-Type", location="headers"), | |
32 | # File uploads | |
33 | "profile_image": fields.Field( | |
34 | location="files", validate=lambda f: f.mimetype in ["image/jpeg", "image/png"] | |
35 | ), | |
29 | # "user_type": fields.Str(data_key="user-type"), | |
36 | 30 | } |
37 | 31 | |
38 | 32 | .. note:: |
104 | 98 | Request "Locations" |
105 | 99 | ------------------- |
106 | 100 | |
107 | By default, webargs will search for arguments from the URL query string (e.g. ``"/?name=foo"``), form data, and JSON data (in that order). You can explicitly specify which locations to search, like so: | |
101 | By default, webargs will search for arguments from the request body as JSON. You can specify a different location from which to load data like so: | |
108 | 102 | |
109 | 103 | .. code-block:: python |
110 | 104 | |
111 | 105 | @app.route("/register") |
112 | @use_args(user_args, locations=("json", "form")) | |
106 | @use_args(user_args, location="form") | |
113 | 107 | def register(args): |
114 | 108 | return "registration page" |
115 | 109 | |
201 | 195 | |
202 | 196 | |
203 | 197 | @parser.error_handler |
204 | def handle_error(error, req, schema, status_code, headers): | |
198 | def handle_error(error, req, schema, *, error_status_code, error_headers): | |
205 | 199 | raise CustomError(error.messages) |
206 | 200 | |
207 | 201 | Parsing Lists in Query Strings |
242 | 236 | |
243 | 237 | .. note:: |
244 | 238 | |
245 | By default, webargs only parses nested fields using the ``json`` request location. You can, however, :ref:`implement your own parser <custom-parsers>` to add nested field functionality to the other locations. | |
239 | Of the default supported locations in webargs, only the ``json`` request location supports nested datastructures. You can, however, :ref:`implement your own data loader <custom-loaders>` to add nested field functionality to the other locations. | |
246 | 240 | |
247 | 241 | Next Steps |
248 | 242 | ---------- |
0 | Upgrading to Newer Releases | |
1 | =========================== | |
2 | ||
3 | This section documents migration paths to new releases. | |
4 | ||
5 | Upgrading to 6.0 | |
6 | ++++++++++++++++ | |
7 | ||
8 | Multiple Locations Are No Longer Supported In A Single Call | |
9 | ----------------------------------------------------------- | |
10 | ||
11 | The default location is JSON/body. | |
12 | ||
13 | Under webargs 5.x, code often did not have to specify a location. | |
14 | ||
15 | Because webargs would parse data from multiple locations automatically, users | |
16 | did not need to specify where a parameter, call it `q`, was passed. | |
17 | `q` could be in a query parameter or in a JSON or form-post body. | |
18 | ||
19 | Now, webargs requires that users specify only one location for data loading per | |
20 | `use_args` call, and `"json"` is the default. If `q` is intended to be a query | |
21 | parameter, the developer must be explicit and rewrite like so: | |
22 | ||
23 | .. code-block:: python | |
24 | ||
25 | # webargs 5.x | |
26 | @parser.use_args({"q": ma.fields.String()}) | |
27 | def foo(args): | |
28 | return some_function(user_query=args.get("q")) | |
29 | ||
30 | ||
31 | # webargs 6.x | |
32 | @parser.use_args({"q": ma.fields.String()}, location="query") | |
33 | def foo(args): | |
34 | return some_function(user_query=args.get("q")) | |
35 | ||
36 | This also means that another usage from 5.x is not supported. Code with | |
37 | multiple locations in a single `use_args`, `use_kwargs`, or `parse` call | |
38 | must be rewritten in multiple separate `use_args` or `use_kwargs` invocations, | |
39 | like so: | |
40 | ||
41 | .. code-block:: python | |
42 | ||
43 | # webargs 5.x | |
44 | @parser.use_kwargs( | |
45 | { | |
46 | "q1": ma.fields.Int(location="query"), | |
47 | "q2": ma.fields.Int(location="query"), | |
48 | "h1": ma.fields.Int(location="headers"), | |
49 | }, | |
50 | locations=("query", "headers"), | |
51 | ) | |
52 | def foo(q1, q2, h1): | |
53 | ... | |
54 | ||
55 | ||
56 | # webargs 6.x | |
57 | @parser.use_kwargs({"q1": ma.fields.Int(), "q2": ma.fields.Int()}, location="query") | |
58 | @parser.use_kwargs({"h1": ma.fields.Int()}, location="headers") | |
59 | def foo(q1, q2, h1): | |
60 | ... | |
61 | ||
62 | ||
63 | Fields No Longer Support location=... | |
64 | ------------------------------------- | |
65 | ||
66 | Because a single `parser.use_args`, `parser.use_kwargs`, or `parser.parse` call | |
67 | cannot specify multiple locations, it is not necessary for a field to be able | |
68 | to specify its location. Rewrite code like so: | |
69 | ||
70 | .. code-block:: python | |
71 | ||
72 | # webargs 5.x | |
73 | @parser.use_args({"q": ma.fields.String(location="query")}) | |
74 | def foo(args): | |
75 | return some_function(user_query=args.get("q")) | |
76 | ||
77 | ||
78 | # webargs 6.x | |
79 | @parser.use_args({"q": ma.fields.String()}, location="query") | |
80 | def foo(args): | |
81 | return some_function(user_query=args.get("q")) | |
82 | ||
83 | location_handler Has Been Replaced With location_loader | |
84 | ------------------------------------------------------- | |
85 | ||
86 | This is not just a name change. The expected signature of a `location_loader` | |
87 | is slightly different from the signature for a `location_handler`. | |
88 | ||
89 | Where previously a `location_handler` code took the incoming request data and | |
90 | details of a single field being loaded, a `location_loader` takes the request | |
91 | and the schema as a pair. It does not return a specific field's data, but data | |
92 | for the whole location. | |
93 | ||
94 | Rewrite code like this: | |
95 | ||
96 | .. code-block:: python | |
97 | ||
98 | # webargs 5.x | |
99 | @parser.location_handler("data") | |
100 | def load_data(request, name, field): | |
101 | return request.data.get(name) | |
102 | ||
103 | ||
104 | # webargs 6.x | |
105 | @parser.location_loader("data") | |
106 | def load_data(request, schema): | |
107 | return request.data | |
108 | ||
109 | Data Is Not Filtered Before Being Passed To Schemas, And It May Be Proxified | |
110 | ---------------------------------------------------------------------------- | |
111 | ||
112 | In webargs 5.x, the deserialization schema was used to pull data out of the | |
113 | request object. That data was compiled into a dictionary which was then passed | |
114 | to the schema. | |
115 | ||
116 | One of the major changes in webargs 6.x allows the use of `unknown` parameter | |
117 | on schemas. This lets a schema decide what to do with fields not specified in | |
118 | the schema. In order to achieve this, webargs now passes the full data from | |
119 | the specified location to the schema. | |
120 | ||
121 | Therefore, users should specify `unknown=marshmallow.EXCLUDE` on their schemas in | |
122 | order to filter out unknown fields. Like so: | |
123 | ||
124 | .. code-block:: python | |
125 | ||
126 | # webargs 5.x | |
127 | # this can assume that "q" is the only parameter passed, and all other | |
128 | # parameters will be ignored | |
129 | @parser.use_kwargs({"q": ma.fields.String()}, locations=("query",)) | |
130 | def foo(q): | |
131 | ... | |
132 | ||
133 | ||
134 | # webargs 6.x, Solution 1: declare a schema with Meta.unknown set | |
135 | class QuerySchema(ma.Schema): | |
136 | q = ma.fields.String() | |
137 | ||
138 | class Meta: | |
139 | unknown = ma.EXCLUDE | |
140 | ||
141 | ||
142 | @parser.use_kwargs(QuerySchema, location="query") | |
143 | def foo(q): | |
144 | ... | |
145 | ||
146 | ||
147 | # webargs 6.x, Solution 2: instantiate a schema with unknown set | |
148 | class QuerySchema(ma.Schema): | |
149 | q = ma.fields.String() | |
150 | ||
151 | ||
152 | @parser.use_kwargs(QuerySchema(unknown=ma.EXCLUDE), location="query") | |
153 | def foo(q): | |
154 | ... | |
155 | ||
156 | ||
157 | This also allows usage which passes the unknown parameters through, like so: | |
158 | ||
159 | .. code-block:: python | |
160 | ||
161 | # webargs 6.x only! cannot be done in 5.x | |
162 | class QuerySchema(ma.Schema): | |
163 | q = ma.fields.String() | |
164 | ||
165 | ||
166 | # will pass *all* query params through as "kwargs" | |
167 | @parser.use_kwargs(QuerySchema(unknown=ma.INCLUDE), location="query") | |
168 | def foo(q, **kwargs): | |
169 | ... | |
170 | ||
171 | ||
172 | However, many types of request data are so-called "multidicts" -- dictionary-like | |
173 | types which can return one or multiple values. To handle `marshmallow.fields.List` | |
174 | and `webargs.fields.DelimitedList` fields correctly, passing list data, webargs | |
175 | must combine schema information with the raw request data. This is done in the | |
176 | :class:`MultiDictProxy <webargs.multidictproxy.MultiDictProxy>` type, which | |
177 | will often be passed to schemas. | |
178 | ||
179 | This means that if a schema has a `pre_load` hook which interacts with the data, | |
180 | it may need modifications. For example, a `flask` query string will be parsed | |
181 | into an `ImmutableMultiDict` type, which will break pre-load hooks which modify | |
182 | the data in-place. Such usages need rewrites like so: | |
183 | ||
184 | .. code-block:: python | |
185 | ||
186 | # webargs 5.x | |
187 | # flask query params is just an example -- applies to several types | |
188 | from webargs.flaskparser import use_kwargs | |
189 | ||
190 | ||
191 | class QuerySchema(ma.Schema): | |
192 | q = ma.fields.String() | |
193 | ||
194 | @ma.pre_load | |
195 | def convert_nil_to_none(self, obj, **kwargs): | |
196 | if obj.get("q") == "nil": | |
197 | obj["q"] = None | |
198 | return obj | |
199 | ||
200 | ||
201 | @use_kwargs(QuerySchema, locations=("query",)) | |
202 | def foo(q): | |
203 | ... | |
204 | ||
205 | ||
206 | # webargs 6.x | |
207 | class QuerySchema(ma.Schema): | |
208 | q = ma.fields.String() | |
209 | ||
210 | # unlike under 5.x, we cannot modify 'obj' in-place because writing | |
211 | # to the MultiDictProxy will try to write to the underlying | |
212 | # ImmutableMultiDict, which is not allowed | |
213 | @ma.pre_load | |
214 | def convert_nil_to_none(self, obj, **kwargs): | |
215 | # creating a dict from a MultiDictProxy works well because it | |
216 | # "unwraps" lists and delimited lists correctly | |
217 | data = dict(obj) | |
218 | if data.get("q") == "nil": | |
219 | data["q"] = None | |
220 | return data | |
221 | ||
222 | ||
223 | @parser.use_kwargs(QuerySchema, location="query") | |
224 | def foo(q): | |
225 | ... | |
226 | ||
227 | ||
228 | DelimitedList Now Only Takes A String Input | |
229 | ------------------------------------------- | |
230 | ||
231 | Combining `List` and string parsing functionality in a single type had some | |
232 | messy corner cases. For the most part, this should not require rewrites. But | |
233 | for APIs which need to allow both usages, rewrites are possible like so: | |
234 | ||
235 | .. code-block:: python | |
236 | ||
237 | # webargs 5.x | |
238 | # this allows ...?x=1&x=2&x=3 | |
239 | # as well as ...?x=1,2,3 | |
240 | @use_kwargs({"x": webargs.fields.DelimitedList(ma.fields.Int)}, locations=("query",)) | |
241 | def foo(x): | |
242 | ... | |
243 | ||
244 | ||
245 | # webargs 6.x | |
246 | # this accepts x=1,2,3 but NOT x=1&x=2&x=3 | |
247 | @use_kwargs({"x": webargs.fields.DelimitedList(ma.fields.Int)}, location="query") | |
248 | def foo(x): | |
249 | ... | |
250 | ||
251 | ||
252 | # webargs 6.x | |
253 | # this accepts x=1,2,3 ; x=1&x=2&x=3 ; x=1,2&x=3 | |
254 | # to do this, it needs a post_load hook which will flatten out the list data | |
255 | class UnpackingDelimitedListSchema(ma.Schema): | |
256 | x = ma.fields.List(webargs.fields.DelimitedList(ma.fields.Int)) | |
257 | ||
258 | @ma.post_load | |
259 | def flatten_lists(self, data, **kwargs): | |
260 | new_x = [] | |
261 | for x in data["x"]: | |
262 | new_x.extend(x) | |
263 | data["x"] = new_x | |
264 | return data | |
265 | ||
266 | ||
267 | @parser.use_kwargs(UnpackingDelimitedListSchema, location="query") | |
268 | def foo(x): | |
269 | ... | |
270 | ||
271 | ||
272 | ValidationError Messages Are Namespaced Under The Location | |
273 | ---------------------------------------------------------- | |
274 | ||
275 | Code parsing ValidationError messages will notice a change in the messages | |
276 | produced by webargs. | |
277 | What would previously have come back with messages like `{"foo":["Not a valid integer."]}` | |
278 | will now have messages nested one layer deeper, like | |
279 | `{"json":{"foo":["Not a valid integer."]}}`. | |
280 | ||
281 | To rewrite code which was handling these errors, the handler will need to be | |
282 | prepared to traverse messages by one additional level. For example: | |
283 | ||
284 | .. code-block:: python | |
285 | ||
286 | import logging | |
287 | ||
288 | log = logging.getLogger(__name__) | |
289 | ||
290 | ||
291 | # webargs 5.x | |
292 | # logs debug messages like | |
293 | # bad value for 'foo': ["Not a valid integer."] | |
294 | # bad value for 'bar': ["Not a valid boolean."] | |
295 | def log_invalid_parameters(validation_error): | |
296 | for field, messages in validation_error.messages.items(): | |
297 | log.debug("bad value for '{}': {}".format(field, messages)) | |
298 | ||
299 | ||
300 | # webargs 6.x | |
301 | # logs debug messages like | |
302 | # bad value for 'foo' [query]: ["Not a valid integer."] | |
303 | # bad value for 'bar' [json]: ["Not a valid boolean."] | |
304 | def log_invalid_parameters(validation_error): | |
305 | for location, fielddata in validation_error.messages.items(): | |
306 | for field, messages in fielddata.items(): | |
307 | log.debug("bad value for '{}' [{}]: {}".format(field, location, messages)) | |
308 | ||
309 | ||
310 | Custom Error Handler Argument Names Changed | |
311 | ------------------------------------------- | |
312 | ||
313 | If you define a custom error handler via `@parser.error_handler` the function | |
314 | arguments are now keyword-only and `status_code` and `headers` have been renamed | |
315 | `error_status_code` and `error_headers`. | |
316 | ||
317 | .. code-block:: python | |
318 | ||
319 | # webargs 5.x | |
320 | @parser.error_handler | |
321 | def custom_handle_error(error, req, schema, status_code, headers): | |
322 | ... | |
323 | ||
324 | ||
325 | # webargs 6.x | |
326 | @parser.error_handler | |
327 | def custom_handle_error(error, req, schema, *, error_status_code, error_headers): | |
328 | ... | |
329 | ||
330 | ||
331 | Some Functions Take Keyword-Only Arguments Now | |
332 | ---------------------------------------------- | |
333 | ||
334 | The signature of several methods has changed to have keyword-only arguments. | |
335 | For the most part, this should not require any changes, but here's a list of | |
336 | the changes. | |
337 | ||
338 | `parser.error_handler` methods: | |
339 | ||
340 | .. code-block:: python | |
341 | ||
342 | # webargs 5.x | |
343 | def handle_error(error, req, schema, status_code, headers): | |
344 | ... | |
345 | ||
346 | ||
347 | # webargs 6.x | |
348 | def handle_error(error, req, schema, *, error_status_code, error_headers): | |
349 | ... | |
350 | ||
351 | `parser.__init__` methods: | |
352 | ||
353 | .. code-block:: python | |
354 | ||
355 | # webargs 5.x | |
356 | def __init__(self, location=None, error_handler=None, schema_class=None): | |
357 | ... | |
358 | ||
359 | ||
360 | # webargs 6.x | |
361 | def __init__(self, location=None, *, error_handler=None, schema_class=None): | |
362 | ... | |
363 | ||
364 | `parser.parse`, `parser.use_args`, and `parser.use_kwargs` methods: | |
365 | ||
366 | ||
367 | .. code-block:: python | |
368 | ||
369 | # webargs 5.x | |
370 | def parse( | |
371 | self, | |
372 | argmap, | |
373 | req=None, | |
374 | location=None, | |
375 | validate=None, | |
376 | error_status_code=None, | |
377 | error_headers=None, | |
378 | ): | |
379 | ... | |
380 | ||
381 | ||
382 | # webargs 6.x | |
383 | def parse( | |
384 | self, | |
385 | argmap, | |
386 | req=None, | |
387 | *, | |
388 | location=None, | |
389 | validate=None, | |
390 | error_status_code=None, | |
391 | error_headers=None | |
392 | ): | |
393 | ... | |
394 | ||
395 | ||
396 | # webargs 5.x | |
397 | def use_args( | |
398 | self, | |
399 | argmap, | |
400 | req=None, | |
401 | location=None, | |
402 | as_kwargs=False, | |
403 | validate=None, | |
404 | error_status_code=None, | |
405 | error_headers=None, | |
406 | ): | |
407 | ... | |
408 | ||
409 | ||
410 | # webargs 6.x | |
411 | def use_args( | |
412 | self, | |
413 | argmap, | |
414 | req=None, | |
415 | *, | |
416 | location=None, | |
417 | as_kwargs=False, | |
418 | validate=None, | |
419 | error_status_code=None, | |
420 | error_headers=None | |
421 | ): | |
422 | ... | |
423 | ||
424 | ||
425 | # use_kwargs is just an alias for use_args with as_kwargs=True | |
426 | ||
427 | and finally, the `dict2schema` function: | |
428 | ||
429 | .. code-block:: python | |
430 | ||
431 | # webargs 5.x | |
432 | def dict2schema(dct, schema_class=ma.Schema): | |
433 | ... | |
434 | ||
435 | ||
436 | # webargs 6.x | |
437 | def dict2schema(dct, *, schema_class=ma.Schema): | |
438 | ... | |
439 | ||
440 | ||
441 | PyramidParser Now Appends Arguments (Used To Prepend) | |
442 | ----------------------------------------------------- | |
443 | ||
444 | `PyramidParser.use_args` was not conformant with the other parsers in webargs. | |
445 | While all other parsers added new arguments to the end of the argument list of | |
446 | a decorated view function, the Pyramid implementation added them to the front | |
447 | of the argument list. | |
448 | ||
449 | This has been corrected, but as a result pyramid views with `use_args` may need | |
450 | to be rewritten. The `request` object is always passed first in both versions, | |
451 | so the issue is only apparent with view functions taking other positional | |
452 | arguments. | |
453 | ||
454 | For example, imagine code with a decorator for passing user information, | |
455 | `pass_userinfo`, like so: | |
456 | ||
457 | .. code-block:: python | |
458 | ||
459 | # a decorator which gets information about the authenticated user | |
460 | def pass_userinfo(f): | |
461 | def decorator(request, *args, **kwargs): | |
462 | return f(request, get_userinfo(), *args, **kwargs) | |
463 | ||
464 | return decorator | |
465 | ||
466 | You will see a behavioral change if `pass_userinfo` is called on a function | |
467 | decorated with `use_args`. The difference between the two versions will be like | |
468 | so: | |
469 | ||
470 | .. code-block:: python | |
471 | ||
472 | from webargs.pyramidparser import use_args | |
473 | ||
474 | # webargs 5.x | |
475 | # pass_userinfo is called first, webargs sees positional arguments of | |
476 | # (userinfo,) | |
477 | # and changes it to | |
478 | # (request, args, userinfo) | |
479 | @pass_userinfo | |
480 | @use_args({"q": ma.fields.String()}, locations=("query",)) | |
481 | def viewfunc(request, args, userinfo): | |
482 | q = args.get("q") | |
483 | ... | |
484 | ||
485 | ||
486 | # webargs 6.x | |
487 | # pass_userinfo is called first, webargs sees positional arguments of | |
488 | # (userinfo,) | |
489 | # and changes it to | |
490 | # (request, userinfo, args) | |
491 | @pass_userinfo | |
492 | @use_args({"q": ma.fields.String()}, location="query") | |
493 | def viewfunc(request, userinfo, args): | |
494 | q = args.get("q") | |
495 | ... |
24 | 24 | |
25 | 25 | @use_args(hello_args) |
26 | 26 | async def index(request, args): |
27 | """A welcome page. | |
28 | """ | |
27 | """A welcome page.""" | |
29 | 28 | return json_response({"message": "Welcome, {}!".format(args["name"])}) |
30 | 29 | |
31 | 30 |
95 | 95 | |
96 | 96 | |
97 | 97 | @route("/", methods=["GET"]) |
98 | def index(name: fields.Str(missing="Friend")): | |
98 | def index(name: fields.Str(missing="Friend")): # noqa: F821 | |
99 | 99 | return {"message": "Hello, {}!".format(name)} |
100 | 100 | |
101 | 101 |
23 | 23 | |
24 | 24 | @route("/", method="GET", apply=use_args(hello_args)) |
25 | 25 | def index(args): |
26 | """A welcome page. | |
27 | """ | |
26 | """A welcome page.""" | |
28 | 27 | return {"message": "Welcome, {}!".format(args["name"])} |
29 | 28 | |
30 | 29 |
26 | 26 | ### Middleware and hooks ### |
27 | 27 | |
28 | 28 | |
29 | class JSONTranslator(object): | |
29 | class JSONTranslator: | |
30 | 30 | def process_response(self, req, resp, resource): |
31 | 31 | if "result" not in req.context: |
32 | 32 | return |
43 | 43 | ### Resources ### |
44 | 44 | |
45 | 45 | |
46 | class HelloResource(object): | |
46 | class HelloResource: | |
47 | 47 | """A welcome page.""" |
48 | 48 | |
49 | 49 | hello_args = {"name": fields.Str(missing="Friend", location="query")} |
53 | 53 | req.context["result"] = {"message": "Welcome, {}!".format(args["name"])} |
54 | 54 | |
55 | 55 | |
56 | class AdderResource(object): | |
56 | class AdderResource: | |
57 | 57 | """An addition endpoint.""" |
58 | 58 | |
59 | 59 | adder_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} |
63 | 63 | req.context["result"] = {"result": x + y} |
64 | 64 | |
65 | 65 | |
66 | class DateAddResource(object): | |
66 | class DateAddResource: | |
67 | 67 | """A datetime adder endpoint.""" |
68 | 68 | |
69 | 69 | dateadd_args = { |
25 | 25 | @app.route("/", methods=["GET"]) |
26 | 26 | @use_args(hello_args) |
27 | 27 | def index(args): |
28 | """A welcome page. | |
29 | """ | |
28 | """A welcome page.""" | |
30 | 29 | return jsonify({"message": "Welcome, {}!".format(args["name"])}) |
31 | 30 | |
32 | 31 |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """A simple number and datetime addition JSON API. |
2 | 1 | Run the app: |
3 | 2 | |
69 | 68 | |
70 | 69 | # This error handler is necessary for usage with Flask-RESTful |
71 | 70 | @parser.error_handler |
72 | def handle_request_parsing_error(err, req, schema, error_status_code, error_headers): | |
71 | def handle_request_parsing_error(err, req, schema, *, error_status_code, error_headers): | |
73 | 72 | """webargs error handler that uses Flask-RESTful's abort function to return |
74 | 73 | a JSON error response to the client. |
75 | 74 | """ |
28 | 28 | @view_config(route_name="hello", request_method="GET", renderer="json") |
29 | 29 | @use_args(hello_args) |
30 | 30 | def index(request, args): |
31 | """A welcome page. | |
32 | """ | |
31 | """A welcome page.""" | |
33 | 32 | return {"message": "Welcome, {}!".format(args["name"])} |
34 | 33 | |
35 | 34 |
60 | 60 | |
61 | 61 | def use_schema(schema_cls, list_view=False, locations=None): |
62 | 62 | """View decorator for using a marshmallow schema to |
63 | (1) parse a request's input and | |
64 | (2) serializing the view's output to a JSON response. | |
63 | (1) parse a request's input and | |
64 | (2) serializing the view's output to a JSON response. | |
65 | 65 | """ |
66 | 66 | |
67 | 67 | def decorator(func): |
0 | 0 | #!/usr/bin/env python |
1 | # -*- coding: utf-8 -*- | |
2 | 1 | """A Hello, World! example using Webapp2 in a Google App Engine environment |
3 | 2 | |
4 | 3 | Run the app: |
1 | 1 | license_files = LICENSE |
2 | 2 | |
3 | 3 | [bdist_wheel] |
4 | # We build separate wheels for | |
5 | # Python 2 and 3 because of the conditional | |
6 | # dependency on simplejson | |
7 | universal = 0 | |
4 | universal = 1 | |
8 | 5 | |
9 | 6 | [flake8] |
10 | 7 | ignore = E203, E266, E501, W503 |
0 | # -*- coding: utf-8 -*- | |
1 | import sys | |
2 | 0 | import re |
3 | 1 | from setuptools import setup, find_packages |
4 | ||
5 | INSTALL_REQUIRES = ["marshmallow>=2.15.2"] | |
6 | if sys.version_info[0] < 3: | |
7 | INSTALL_REQUIRES.append("simplejson>=2.1.0") | |
8 | 2 | |
9 | 3 | FRAMEWORKS = [ |
10 | 4 | "Flask>=0.12.2", |
13 | 7 | "tornado>=4.5.2", |
14 | 8 | "pyramid>=1.9.1", |
15 | 9 | "webapp2>=3.0.0b1", |
16 | "falcon>=1.4.0,<2.0", | |
17 | 'aiohttp>=3.0.0; python_version >= "3.5"', | |
10 | "falcon>=2.0.0", | |
11 | "aiohttp>=3.0.0", | |
18 | 12 | ] |
19 | 13 | EXTRAS_REQUIRE = { |
20 | 14 | "frameworks": FRAMEWORKS, |
21 | 15 | "tests": [ |
22 | 16 | "pytest", |
23 | "mock", | |
24 | "webtest==2.0.33", | |
25 | 'webtest-aiohttp==2.0.0; python_version >= "3.5"', | |
26 | 'pytest-aiohttp>=0.3.0; python_version >= "3.5"', | |
17 | 'mock; python_version == "3.5"', | |
18 | "webtest==2.0.35", | |
19 | "webtest-aiohttp==2.0.0", | |
20 | "pytest-aiohttp>=0.3.0", | |
27 | 21 | ] |
28 | 22 | + FRAMEWORKS, |
29 | 23 | "lint": [ |
30 | 'mypy==0.730; python_version >= "3.5"', | |
31 | "flake8==3.7.8", | |
32 | 'flake8-bugbear==19.8.0; python_version >= "3.5"', | |
33 | "pre-commit~=1.17", | |
24 | "mypy==0.782", | |
25 | "flake8==3.8.3", | |
26 | "flake8-bugbear==20.1.4", | |
27 | "pre-commit~=2.4", | |
34 | 28 | ], |
35 | "docs": ["Sphinx==2.2.0", "sphinx-issues==1.2.0", "sphinx-typlog-theme==0.7.3"] | |
29 | "docs": ["Sphinx==3.2.1", "sphinx-issues==1.2.0", "sphinx-typlog-theme==0.8.0"] | |
36 | 30 | + FRAMEWORKS, |
37 | 31 | } |
38 | 32 | EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"] |
43 | 37 | Raises RuntimeError if not found. |
44 | 38 | """ |
45 | 39 | version = "" |
46 | with open(fname, "r") as fp: | |
40 | with open(fname) as fp: | |
47 | 41 | reg = re.compile(r'__version__ = [\'"]([^\'"]*)[\'"]') |
48 | 42 | for line in fp: |
49 | 43 | m = reg.match(line) |
75 | 69 | url="https://github.com/marshmallow-code/webargs", |
76 | 70 | packages=find_packages("src"), |
77 | 71 | package_dir={"": "src"}, |
78 | install_requires=INSTALL_REQUIRES, | |
72 | install_requires=["marshmallow>=2.15.2"], | |
79 | 73 | extras_require=EXTRAS_REQUIRE, |
80 | 74 | license="MIT", |
81 | 75 | zip_safe=False, |
96 | 90 | "api", |
97 | 91 | "marshmallow", |
98 | 92 | ), |
93 | python_requires=">=3.5", | |
99 | 94 | classifiers=[ |
100 | 95 | "Development Status :: 5 - Production/Stable", |
101 | 96 | "Intended Audience :: Developers", |
102 | 97 | "License :: OSI Approved :: MIT License", |
103 | 98 | "Natural Language :: English", |
104 | "Programming Language :: Python :: 2", | |
105 | "Programming Language :: Python :: 2.7", | |
106 | 99 | "Programming Language :: Python :: 3", |
107 | 100 | "Programming Language :: Python :: 3.5", |
108 | 101 | "Programming Language :: Python :: 3.6", |
109 | 102 | "Programming Language :: Python :: 3.7", |
103 | "Programming Language :: Python :: 3.8", | |
104 | "Programming Language :: Python :: 3 :: Only", | |
110 | 105 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", |
111 | 106 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", |
112 | 107 | ], |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | from distutils.version import LooseVersion |
2 | 1 | from marshmallow.utils import missing |
3 | 2 | |
8 | 7 | from webargs.dict2schema import dict2schema |
9 | 8 | from webargs import fields |
10 | 9 | |
11 | __version__ = "5.5.3" | |
10 | __version__ = "6.1.1" | |
12 | 11 | __version_info__ = tuple(LooseVersion(__version__).version) |
13 | 12 | __all__ = ("dict2schema", "ValidationError", "fields", "missing", "validate") |
27 | 27 | from aiohttp.web import Request |
28 | 28 | from aiohttp import web_exceptions |
29 | 29 | from marshmallow import Schema, ValidationError |
30 | from marshmallow.fields import Field | |
31 | 30 | |
32 | 31 | from webargs import core |
33 | 32 | from webargs.core import json |
34 | 33 | from webargs.asyncparser import AsyncParser |
34 | from webargs.multidictproxy import MultiDictProxy | |
35 | 35 | |
36 | 36 | |
37 | 37 | def is_json_request(req: Request) -> bool: |
72 | 72 | """aiohttp request argument parser.""" |
73 | 73 | |
74 | 74 | __location_map__ = dict( |
75 | match_info="parse_match_info", | |
76 | path="parse_match_info", | |
77 | **core.Parser.__location_map__ | |
75 | match_info="load_match_info", | |
76 | path="load_match_info", | |
77 | **core.Parser.__location_map__, | |
78 | 78 | ) |
79 | 79 | |
80 | def parse_querystring(self, req: Request, name: str, field: Field) -> typing.Any: | |
81 | """Pull a querystring value from the request.""" | |
82 | return core.get_value(req.query, name, field) | |
80 | def load_querystring(self, req: Request, schema: Schema) -> MultiDictProxy: | |
81 | """Return query params from the request as a MultiDictProxy.""" | |
82 | return MultiDictProxy(req.query, schema) | |
83 | 83 | |
84 | async def parse_form(self, req: Request, name: str, field: Field) -> typing.Any: | |
85 | """Pull a form value from the request.""" | |
86 | post_data = self._cache.get("post") | |
87 | if post_data is None: | |
88 | self._cache["post"] = await req.post() | |
89 | return core.get_value(self._cache["post"], name, field) | |
84 | async def load_form(self, req: Request, schema: Schema) -> MultiDictProxy: | |
85 | """Return form values from the request as a MultiDictProxy.""" | |
86 | post_data = await req.post() | |
87 | return MultiDictProxy(post_data, schema) | |
90 | 88 | |
91 | async def parse_json(self, req: Request, name: str, field: Field) -> typing.Any: | |
92 | """Pull a json value from the request.""" | |
93 | json_data = self._cache.get("json") | |
94 | if json_data is None: | |
95 | if not (req.body_exists and is_json_request(req)): | |
89 | async def load_json_or_form( | |
90 | self, req: Request, schema: Schema | |
91 | ) -> typing.Union[typing.Dict, MultiDictProxy]: | |
92 | data = await self.load_json(req, schema) | |
93 | if data is not core.missing: | |
94 | return data | |
95 | return await self.load_form(req, schema) | |
96 | ||
97 | async def load_json(self, req: Request, schema: Schema) -> typing.Dict: | |
98 | """Return a parsed json payload from the request.""" | |
99 | if not (req.body_exists and is_json_request(req)): | |
100 | return core.missing | |
101 | try: | |
102 | return await req.json(loads=json.loads) | |
103 | except json.JSONDecodeError as exc: | |
104 | if exc.doc == "": | |
96 | 105 | return core.missing |
97 | try: | |
98 | json_data = await req.json(loads=json.loads) | |
99 | except json.JSONDecodeError as e: | |
100 | if e.doc == "": | |
101 | return core.missing | |
102 | else: | |
103 | return self.handle_invalid_json_error(e, req) | |
104 | except UnicodeDecodeError as e: | |
105 | return self.handle_invalid_json_error(e, req) | |
106 | return self._handle_invalid_json_error(exc, req) | |
107 | except UnicodeDecodeError as exc: | |
108 | return self._handle_invalid_json_error(exc, req) | |
106 | 109 | |
107 | self._cache["json"] = json_data | |
108 | return core.get_value(json_data, name, field, allow_many_nested=True) | |
110 | def load_headers(self, req: Request, schema: Schema) -> MultiDictProxy: | |
111 | """Return headers from the request as a MultiDictProxy.""" | |
112 | return MultiDictProxy(req.headers, schema) | |
109 | 113 | |
110 | def parse_headers(self, req: Request, name: str, field: Field) -> typing.Any: | |
111 | """Pull a value from the header data.""" | |
112 | return core.get_value(req.headers, name, field) | |
114 | def load_cookies(self, req: Request, schema: Schema) -> MultiDictProxy: | |
115 | """Return cookies from the request as a MultiDictProxy.""" | |
116 | return MultiDictProxy(req.cookies, schema) | |
113 | 117 | |
114 | def parse_cookies(self, req: Request, name: str, field: Field) -> typing.Any: | |
115 | """Pull a value from the cookiejar.""" | |
116 | return core.get_value(req.cookies, name, field) | |
117 | ||
118 | def parse_files(self, req: Request, name: str, field: Field) -> None: | |
118 | def load_files(self, req: Request, schema: Schema) -> "typing.NoReturn": | |
119 | 119 | raise NotImplementedError( |
120 | "parse_files is not implemented. You may be able to use parse_form for " | |
120 | "load_files is not implemented. You may be able to use load_form for " | |
121 | 121 | "parsing upload data." |
122 | 122 | ) |
123 | 123 | |
124 | def parse_match_info(self, req: Request, name: str, field: Field) -> typing.Any: | |
125 | """Pull a value from the request's ``match_info``.""" | |
126 | return core.get_value(req.match_info, name, field) | |
124 | def load_match_info(self, req: Request, schema: Schema) -> typing.Mapping: | |
125 | """Load the request's ``match_info``.""" | |
126 | return req.match_info | |
127 | 127 | |
128 | 128 | def get_request_from_view_args( |
129 | 129 | self, view: typing.Callable, args: typing.Iterable, kwargs: typing.Mapping |
136 | 136 | if isinstance(arg, web.Request): |
137 | 137 | req = arg |
138 | 138 | break |
139 | elif isinstance(arg, web.View): | |
139 | if isinstance(arg, web.View): | |
140 | 140 | req = arg.request |
141 | 141 | break |
142 | assert isinstance(req, web.Request), "Request argument not found for handler" | |
142 | if not isinstance(req, web.Request): | |
143 | raise ValueError("Request argument not found for handler") | |
143 | 144 | return req |
144 | 145 | |
145 | 146 | def handle_error( |
147 | 148 | error: ValidationError, |
148 | 149 | req: Request, |
149 | 150 | schema: Schema, |
150 | error_status_code: typing.Union[int, None] = None, | |
151 | error_headers: typing.Union[typing.Mapping[str, str], None] = None, | |
151 | *, | |
152 | error_status_code: typing.Union[int, None], | |
153 | error_headers: typing.Union[typing.Mapping[str, str], None] | |
152 | 154 | ) -> "typing.NoReturn": |
153 | 155 | """Handle ValidationErrors and return a JSON response of error messages |
154 | 156 | to the client. |
157 | 159 | error_status_code or self.DEFAULT_VALIDATION_STATUS |
158 | 160 | ) |
159 | 161 | if not error_class: |
160 | raise LookupError("No exception for {0}".format(error_status_code)) | |
162 | raise LookupError("No exception for {}".format(error_status_code)) | |
161 | 163 | headers = error_headers |
162 | 164 | raise error_class( |
163 | 165 | body=json.dumps(error.messages).encode("utf-8"), |
165 | 167 | content_type="application/json", |
166 | 168 | ) |
167 | 169 | |
168 | def handle_invalid_json_error( | |
170 | def _handle_invalid_json_error( | |
169 | 171 | self, |
170 | 172 | error: typing.Union[json.JSONDecodeError, UnicodeDecodeError], |
171 | 173 | req: Request, |
7 | 7 | from marshmallow import Schema, ValidationError |
8 | 8 | from marshmallow.fields import Field |
9 | 9 | import marshmallow as ma |
10 | from marshmallow.utils import missing | |
11 | 10 | |
12 | 11 | from webargs import core |
13 | 12 | |
21 | 20 | either coroutines or regular methods. |
22 | 21 | """ |
23 | 22 | |
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 | 23 | # TODO: Lots of duplication from core.Parser here. Rethink. |
66 | 24 | async def parse( |
67 | 25 | self, |
68 | 26 | argmap: ArgMap, |
69 | 27 | req: Request = None, |
70 | locations: typing.Iterable = None, | |
28 | *, | |
29 | location: str = None, | |
71 | 30 | validate: Validate = None, |
72 | 31 | error_status_code: typing.Union[int, None] = None, |
73 | error_headers: typing.Union[typing.Mapping[str, str], None] = None, | |
32 | error_headers: typing.Union[typing.Mapping[str, str], None] = None | |
74 | 33 | ) -> typing.Union[typing.Mapping, None]: |
75 | 34 | """Coroutine variant of `webargs.core.Parser`. |
76 | 35 | |
77 | 36 | Receives the same arguments as `webargs.core.Parser.parse`. |
78 | 37 | """ |
79 | self.clear_cache() # in case someone used `parse_*()` | |
80 | 38 | req = req if req is not None else self.get_default_request() |
81 | assert req is not None, "Must pass req object" | |
39 | location = location or self.location | |
40 | if req is None: | |
41 | raise ValueError("Must pass req object") | |
82 | 42 | data = None |
83 | 43 | validators = core._ensure_list_of_callables(validate) |
84 | 44 | schema = self._get_schema(argmap, req) |
85 | 45 | try: |
86 | parsed = await self._parse_request( | |
87 | schema=schema, req=req, locations=locations or self.locations | |
46 | location_data = await self._load_location_data( | |
47 | schema=schema, req=req, location=location | |
88 | 48 | ) |
89 | result = schema.load(parsed) | |
49 | result = schema.load(location_data) | |
90 | 50 | data = result.data if core.MARSHMALLOW_VERSION_INFO[0] < 3 else result |
91 | 51 | self._validate_arguments(data, validators) |
92 | 52 | except ma.exceptions.ValidationError as error: |
93 | 53 | await self._on_validation_error( |
94 | error, req, schema, error_status_code, error_headers | |
54 | error, | |
55 | req, | |
56 | schema, | |
57 | location, | |
58 | error_status_code=error_status_code, | |
59 | error_headers=error_headers, | |
95 | 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 = {} | |
96 | 80 | return data |
97 | 81 | |
98 | 82 | async def _on_validation_error( |
100 | 84 | error: ValidationError, |
101 | 85 | req: Request, |
102 | 86 | schema: Schema, |
87 | location: str, | |
88 | *, | |
103 | 89 | error_status_code: typing.Union[int, None], |
104 | error_headers: typing.Union[typing.Mapping[str, str], None] = None, | |
90 | error_headers: typing.Union[typing.Mapping[str, str], None] | |
105 | 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} | |
106 | 98 | error_handler = self.error_callback or self.handle_error |
107 | await error_handler(error, req, schema, error_status_code, error_headers) | |
99 | await error_handler( | |
100 | error, | |
101 | req, | |
102 | schema, | |
103 | error_status_code=error_status_code, | |
104 | error_headers=error_headers, | |
105 | ) | |
108 | 106 | |
109 | 107 | def use_args( |
110 | 108 | self, |
111 | 109 | argmap: ArgMap, |
112 | 110 | req: typing.Optional[Request] = None, |
113 | locations: typing.Iterable = None, | |
111 | *, | |
112 | location: str = None, | |
114 | 113 | as_kwargs: bool = False, |
115 | 114 | validate: Validate = None, |
116 | 115 | error_status_code: typing.Optional[int] = None, |
117 | error_headers: typing.Union[typing.Mapping[str, str], None] = None, | |
116 | error_headers: typing.Union[typing.Mapping[str, str], None] = None | |
118 | 117 | ) -> typing.Callable[..., typing.Callable]: |
119 | 118 | """Decorator that injects parsed arguments into a view function or method. |
120 | 119 | |
121 | 120 | Receives the same arguments as `webargs.core.Parser.use_args`. |
122 | 121 | """ |
123 | locations = locations or self.locations | |
122 | location = location or self.location | |
124 | 123 | request_obj = req |
125 | 124 | # Optimization: If argmap is passed as a dictionary, we only need |
126 | 125 | # to generate a Schema once |
127 | 126 | if isinstance(argmap, Mapping): |
128 | argmap = core.dict2schema(argmap, self.schema_class)() | |
127 | argmap = core.dict2schema(argmap, schema_class=self.schema_class)() | |
129 | 128 | |
130 | 129 | def decorator(func: typing.Callable) -> typing.Callable: |
131 | 130 | req_ = request_obj |
142 | 141 | parsed_args = await self.parse( |
143 | 142 | argmap, |
144 | 143 | req=req_obj, |
145 | locations=locations, | |
144 | location=location, | |
146 | 145 | validate=validate, |
147 | 146 | error_status_code=error_status_code, |
148 | 147 | error_headers=error_headers, |
149 | 148 | ) |
150 | if as_kwargs: | |
151 | kwargs.update(parsed_args or {}) | |
152 | return await func(*args, **kwargs) | |
153 | else: | |
154 | # Add parsed_args after other positional arguments | |
155 | new_args = args + (parsed_args,) | |
156 | return await func(*new_args, **kwargs) | |
149 | args, kwargs = self._update_args_kwargs( | |
150 | args, kwargs, parsed_args, as_kwargs | |
151 | ) | |
152 | return await func(*args, **kwargs) | |
157 | 153 | |
158 | 154 | else: |
159 | 155 | |
167 | 163 | parsed_args = yield from self.parse( # type: ignore |
168 | 164 | argmap, |
169 | 165 | req=req_obj, |
170 | locations=locations, | |
166 | location=location, | |
171 | 167 | validate=validate, |
172 | 168 | error_status_code=error_status_code, |
173 | 169 | error_headers=error_headers, |
174 | 170 | ) |
175 | if as_kwargs: | |
176 | kwargs.update(parsed_args) | |
177 | return func(*args, **kwargs) # noqa: B901 | |
178 | else: | |
179 | # Add parsed_args after other positional arguments | |
180 | new_args = args + (parsed_args,) | |
181 | return func(*new_args, **kwargs) | |
171 | args, kwargs = self._update_args_kwargs( | |
172 | args, kwargs, parsed_args, as_kwargs | |
173 | ) | |
174 | return func(*args, **kwargs) | |
182 | 175 | |
183 | 176 | return wrapper |
184 | 177 | |
185 | 178 | return decorator |
186 | ||
187 | def use_kwargs(self, *args, **kwargs) -> typing.Callable: | |
188 | """Decorator that injects parsed arguments into a view function or method. | |
189 | ||
190 | Receives the same arguments as `webargs.core.Parser.use_kwargs`. | |
191 | ||
192 | """ | |
193 | return super().use_kwargs(*args, **kwargs) | |
194 | ||
195 | async def parse_arg( | |
196 | self, name: str, field: Field, req: Request, locations: typing.Iterable = None | |
197 | ) -> typing.Any: | |
198 | location = field.metadata.get("location") | |
199 | if location: | |
200 | locations_to_check = self._validated_locations([location]) | |
201 | else: | |
202 | locations_to_check = self._validated_locations(locations or self.locations) | |
203 | ||
204 | for location in locations_to_check: | |
205 | value = await self._get_value(name, field, req=req, location=location) | |
206 | # Found the value; validate and return it | |
207 | if value is not core.missing: | |
208 | return value | |
209 | return core.missing | |
210 | ||
211 | async def _get_value( | |
212 | self, name: str, argobj: Field, req: Request, location: str | |
213 | ) -> typing.Any: | |
214 | function = self._get_handler(location) | |
215 | if asyncio.iscoroutinefunction(function): | |
216 | value = await function(req, name, argobj) | |
217 | else: | |
218 | value = function(req, name, argobj) | |
219 | return value |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """Bottle request argument parsing module. |
2 | 1 | |
3 | 2 | Example: :: |
19 | 18 | import bottle |
20 | 19 | |
21 | 20 | from webargs import core |
22 | from webargs.core import json | |
21 | from webargs.multidictproxy import MultiDictProxy | |
23 | 22 | |
24 | 23 | |
25 | 24 | class BottleParser(core.Parser): |
26 | 25 | """Bottle.py request argument parser.""" |
27 | 26 | |
28 | def parse_querystring(self, req, name, field): | |
29 | """Pull a querystring value from the request.""" | |
30 | return core.get_value(req.query, name, field) | |
27 | def _handle_invalid_json_error(self, error, req, *args, **kwargs): | |
28 | raise bottle.HTTPError( | |
29 | status=400, body={"json": ["Invalid JSON body."]}, exception=error | |
30 | ) | |
31 | 31 | |
32 | def parse_form(self, req, name, field): | |
33 | """Pull a form value from the request.""" | |
32 | def _raw_load_json(self, req): | |
33 | """Read a json payload from the request.""" | |
34 | try: | |
35 | data = req.json | |
36 | except AttributeError: | |
37 | return core.missing | |
38 | ||
39 | # unfortunately, bottle does not distinguish between an emtpy body, "", | |
40 | # and a body containing the valid JSON value null, "null" | |
41 | # so these can't be properly disambiguated | |
42 | # as our best-effort solution, treat None as missing and ignore the | |
43 | # (admittedly unusual) "null" case | |
44 | # see: https://github.com/bottlepy/bottle/issues/1160 | |
45 | if data is None: | |
46 | return core.missing | |
47 | return data | |
48 | ||
49 | def load_querystring(self, req, schema): | |
50 | """Return query params from the request as a MultiDictProxy.""" | |
51 | return MultiDictProxy(req.query, schema) | |
52 | ||
53 | def load_form(self, req, schema): | |
54 | """Return form values from the request as a MultiDictProxy.""" | |
34 | 55 | # For consistency with other parsers' behavior, don't attempt to |
35 | 56 | # parse if content-type is mismatched. |
36 | 57 | # TODO: Make this check more specific |
37 | 58 | if core.is_json(req.content_type): |
38 | 59 | return core.missing |
39 | return core.get_value(req.forms, name, field) | |
60 | return MultiDictProxy(req.forms, schema) | |
40 | 61 | |
41 | def parse_json(self, req, name, field): | |
42 | """Pull a json value from the request.""" | |
43 | json_data = self._cache.get("json") | |
44 | if json_data is None: | |
45 | try: | |
46 | self._cache["json"] = json_data = req.json | |
47 | except AttributeError: | |
48 | return core.missing | |
49 | except json.JSONDecodeError as e: | |
50 | if e.doc == "": | |
51 | return core.missing | |
52 | else: | |
53 | return self.handle_invalid_json_error(e, req) | |
54 | except UnicodeDecodeError as e: | |
55 | return self.handle_invalid_json_error(e, req) | |
62 | def load_headers(self, req, schema): | |
63 | """Return headers from the request as a MultiDictProxy.""" | |
64 | return MultiDictProxy(req.headers, schema) | |
56 | 65 | |
57 | if json_data is None: | |
58 | return core.missing | |
59 | return core.get_value(json_data, name, field, allow_many_nested=True) | |
66 | def load_cookies(self, req, schema): | |
67 | """Return cookies from the request.""" | |
68 | return req.cookies | |
60 | 69 | |
61 | def parse_headers(self, req, name, field): | |
62 | """Pull a value from the header data.""" | |
63 | return core.get_value(req.headers, name, field) | |
70 | def load_files(self, req, schema): | |
71 | """Return files from the request as a MultiDictProxy.""" | |
72 | return MultiDictProxy(req.files, schema) | |
64 | 73 | |
65 | def parse_cookies(self, req, name, field): | |
66 | """Pull a value from the cookiejar.""" | |
67 | return req.get_cookie(name) | |
68 | ||
69 | def parse_files(self, req, name, field): | |
70 | """Pull a file from the request.""" | |
71 | return core.get_value(req.files, name, field) | |
72 | ||
73 | def handle_error(self, error, req, schema, error_status_code, error_headers): | |
74 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): | |
74 | 75 | """Handles errors during parsing. Aborts the current request with a |
75 | 76 | 400 error. |
76 | 77 | """ |
82 | 83 | exception=error, |
83 | 84 | ) |
84 | 85 | |
85 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
86 | raise bottle.HTTPError( | |
87 | status=400, body={"json": ["Invalid JSON body."]}, exception=error | |
88 | ) | |
89 | ||
90 | 86 | def get_default_request(self): |
91 | 87 | """Override to use bottle's thread-local request object by default.""" |
92 | 88 | return bottle.request |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | # flake8: noqa |
2 | import sys | |
3 | 1 | from distutils.version import LooseVersion |
4 | 2 | |
5 | 3 | import marshmallow as ma |
6 | 4 | |
7 | 5 | MARSHMALLOW_VERSION_INFO = tuple(LooseVersion(ma.__version__).version) # type: tuple |
8 | PY2 = int(sys.version_info[0]) == 2 | |
9 | ||
10 | if PY2: | |
11 | from collections import Mapping | |
12 | ||
13 | basestring = basestring | |
14 | text_type = unicode | |
15 | iteritems = lambda d: d.iteritems() | |
16 | else: | |
17 | from collections.abc import Mapping | |
18 | ||
19 | basestring = (str, bytes) | |
20 | text_type = str | |
21 | iteritems = lambda d: d.items() |
0 | # -*- coding: utf-8 -*- | |
1 | from __future__ import unicode_literals | |
2 | ||
3 | 0 | import functools |
4 | 1 | import inspect |
2 | import typing | |
5 | 3 | import logging |
6 | 4 | import warnings |
7 | from copy import copy | |
8 | ||
9 | try: | |
10 | import simplejson as json | |
11 | except ImportError: | |
12 | import json # type: ignore | |
5 | from collections.abc import Mapping | |
6 | import json | |
13 | 7 | |
14 | 8 | import marshmallow as ma |
15 | 9 | from marshmallow import ValidationError |
16 | from marshmallow.utils import missing, is_collection | |
17 | ||
18 | from webargs.compat import Mapping, iteritems, MARSHMALLOW_VERSION_INFO | |
10 | from marshmallow.utils import missing | |
11 | ||
12 | from webargs.compat import MARSHMALLOW_VERSION_INFO | |
19 | 13 | from webargs.dict2schema import dict2schema |
20 | 14 | from webargs.fields import DelimitedList |
21 | 15 | |
27 | 21 | "dict2schema", |
28 | 22 | "is_multiple", |
29 | 23 | "Parser", |
30 | "get_value", | |
31 | 24 | "missing", |
32 | 25 | "parse_json", |
33 | 26 | ] |
41 | 34 | callable, a ValueError is raised. |
42 | 35 | """ |
43 | 36 | if obj and not callable(obj): |
44 | raise ValueError("{0!r} is not callable.".format(obj)) | |
45 | else: | |
46 | return obj | |
37 | raise ValueError("{!r} is not callable.".format(obj)) | |
38 | return obj | |
47 | 39 | |
48 | 40 | |
49 | 41 | def is_multiple(field): |
73 | 65 | return False |
74 | 66 | |
75 | 67 | |
76 | def get_value(data, name, field, allow_many_nested=False): | |
77 | """Get a value from a dictionary. Handles ``MultiDict`` types when | |
78 | ``field`` handles repeated/multi-value arguments. | |
79 | If the value is not found, return `missing`. | |
80 | ||
81 | :param object data: Mapping (e.g. `dict`) or list-like instance to | |
82 | pull the value from. | |
83 | :param str name: Name of the key. | |
84 | :param bool allow_many_nested: Whether to allow a list of nested objects | |
85 | (it is valid only for JSON format, so it is set to True in ``parse_json`` | |
86 | methods). | |
87 | """ | |
88 | missing_value = missing | |
89 | if allow_many_nested and isinstance(field, ma.fields.Nested) and field.many: | |
90 | if is_collection(data): | |
91 | return data | |
92 | ||
93 | if not hasattr(data, "get"): | |
94 | return missing_value | |
95 | ||
96 | multiple = is_multiple(field) | |
97 | val = data.get(name, missing_value) | |
98 | if multiple and val is not missing: | |
99 | if hasattr(data, "getlist"): | |
100 | return data.getlist(name) | |
101 | elif hasattr(data, "getall"): | |
102 | return data.getall(name) | |
103 | elif isinstance(val, (list, tuple)): | |
104 | return val | |
105 | if val is None: | |
106 | return None | |
107 | else: | |
108 | return [val] | |
109 | return val | |
110 | ||
111 | ||
112 | def parse_json(s, encoding="utf-8"): | |
113 | if isinstance(s, bytes): | |
68 | def parse_json(string, *, encoding="utf-8"): | |
69 | if isinstance(string, bytes): | |
114 | 70 | try: |
115 | s = s.decode(encoding) | |
116 | except UnicodeDecodeError as e: | |
71 | string = string.decode(encoding) | |
72 | except UnicodeDecodeError as exc: | |
117 | 73 | raise json.JSONDecodeError( |
118 | "Bytes decoding error : {}".format(e.reason), | |
119 | doc=str(e.object), | |
120 | pos=e.start, | |
74 | "Bytes decoding error : {}".format(exc.reason), | |
75 | doc=str(exc.object), | |
76 | pos=exc.start, | |
121 | 77 | ) |
122 | return json.loads(s) | |
78 | return json.loads(string) | |
123 | 79 | |
124 | 80 | |
125 | 81 | def _ensure_list_of_callables(obj): |
129 | 85 | elif callable(obj): |
130 | 86 | validators = [obj] |
131 | 87 | else: |
132 | raise ValueError( | |
133 | "{0!r} is not a callable or list of callables.".format(obj) | |
134 | ) | |
88 | raise ValueError("{!r} is not a callable or list of callables.".format(obj)) | |
135 | 89 | else: |
136 | 90 | validators = [] |
137 | 91 | return validators |
138 | 92 | |
139 | 93 | |
140 | class Parser(object): | |
94 | class Parser: | |
141 | 95 | """Base parser class that provides high-level implementation for parsing |
142 | 96 | a request. |
143 | 97 | |
144 | Descendant classes must provide lower-level implementations for parsing | |
145 | different locations, e.g. ``parse_json``, ``parse_querystring``, etc. | |
146 | ||
147 | :param tuple locations: Default locations to parse. | |
98 | Descendant classes must provide lower-level implementations for reading | |
99 | data from different locations, e.g. ``load_json``, ``load_querystring``, | |
100 | etc. | |
101 | ||
102 | :param str location: Default location to use for data | |
148 | 103 | :param callable error_handler: Custom error handler function. |
149 | 104 | """ |
150 | 105 | |
151 | #: Default locations to check for data | |
152 | DEFAULT_LOCATIONS = ("querystring", "form", "json") | |
106 | #: Default location to check for data | |
107 | DEFAULT_LOCATION = "json" | |
153 | 108 | #: The marshmallow Schema class to use when creating new schemas |
154 | 109 | DEFAULT_SCHEMA_CLASS = ma.Schema |
155 | 110 | #: Default status code to return for validation errors |
159 | 114 | |
160 | 115 | #: Maps location => method name |
161 | 116 | __location_map__ = { |
162 | "json": "parse_json", | |
163 | "querystring": "parse_querystring", | |
164 | "query": "parse_querystring", | |
165 | "form": "parse_form", | |
166 | "headers": "parse_headers", | |
167 | "cookies": "parse_cookies", | |
168 | "files": "parse_files", | |
117 | "json": "load_json", | |
118 | "querystring": "load_querystring", | |
119 | "query": "load_querystring", | |
120 | "form": "load_form", | |
121 | "headers": "load_headers", | |
122 | "cookies": "load_cookies", | |
123 | "files": "load_files", | |
124 | "json_or_form": "load_json_or_form", | |
169 | 125 | } |
170 | 126 | |
171 | def __init__(self, locations=None, error_handler=None, schema_class=None): | |
172 | self.locations = locations or self.DEFAULT_LOCATIONS | |
127 | def __init__(self, location=None, *, error_handler=None, schema_class=None): | |
128 | self.location = location or self.DEFAULT_LOCATION | |
173 | 129 | self.error_callback = _callable_or_raise(error_handler) |
174 | 130 | self.schema_class = schema_class or self.DEFAULT_SCHEMA_CLASS |
175 | #: A short-lived cache to store results from processing request bodies. | |
176 | self._cache = {} | |
177 | ||
178 | def _validated_locations(self, locations): | |
179 | """Ensure that the given locations argument is valid. | |
180 | ||
181 | :raises: ValueError if a given locations includes an invalid location. | |
182 | """ | |
183 | # The set difference between the given locations and the available locations | |
184 | # will be the set of invalid locations | |
131 | ||
132 | def _get_loader(self, location): | |
133 | """Get the loader function for the given location. | |
134 | ||
135 | :raises: ValueError if a given location is invalid. | |
136 | """ | |
185 | 137 | valid_locations = set(self.__location_map__.keys()) |
186 | given = set(locations) | |
187 | invalid_locations = given - valid_locations | |
188 | if len(invalid_locations): | |
189 | msg = "Invalid locations arguments: {0}".format(list(invalid_locations)) | |
138 | if location not in valid_locations: | |
139 | msg = "Invalid location argument: {}".format(location) | |
190 | 140 | raise ValueError(msg) |
191 | return locations | |
192 | ||
193 | def _get_handler(self, location): | |
141 | ||
194 | 142 | # Parsing function to call |
195 | 143 | # May be a method name (str) or a function |
196 | 144 | func = self.__location_map__.get(location) |
200 | 148 | else: |
201 | 149 | function = getattr(self, func) |
202 | 150 | else: |
203 | raise ValueError('Invalid location: "{0}"'.format(location)) | |
151 | raise ValueError('Invalid location: "{}"'.format(location)) | |
204 | 152 | return function |
205 | 153 | |
206 | def _get_value(self, name, argobj, req, location): | |
207 | function = self._get_handler(location) | |
208 | return function(req, name, argobj) | |
209 | ||
210 | def parse_arg(self, name, field, req, locations=None): | |
211 | """Parse a single argument from a request. | |
212 | ||
213 | .. note:: | |
214 | This method does not perform validation on the argument. | |
215 | ||
216 | :param str name: The name of the value. | |
217 | :param marshmallow.fields.Field field: The marshmallow `Field` for the request | |
218 | parameter. | |
219 | :param req: The request object to parse. | |
220 | :param tuple locations: The locations ('json', 'querystring', etc.) where | |
221 | to search for the value. | |
222 | :return: The unvalidated argument value or `missing` if the value cannot | |
223 | be found on the request. | |
224 | """ | |
225 | location = field.metadata.get("location") | |
226 | if location: | |
227 | locations_to_check = self._validated_locations([location]) | |
228 | else: | |
229 | locations_to_check = self._validated_locations(locations or self.locations) | |
230 | ||
231 | for location in locations_to_check: | |
232 | value = self._get_value(name, field, req=req, location=location) | |
233 | # Found the value; validate and return it | |
234 | if value is not missing: | |
235 | return value | |
236 | return missing | |
237 | ||
238 | def _parse_request(self, schema, req, locations): | |
239 | """Return a parsed arguments dictionary for the current request.""" | |
240 | if schema.many: | |
241 | assert ( | |
242 | "json" in locations | |
243 | ), "schema.many=True is only supported for JSON location" | |
244 | # The ad hoc Nested field is more like a workaround or a helper, | |
245 | # and it servers its purpose fine. However, if somebody has a desire | |
246 | # to re-design the support of bulk-type arguments, go ahead. | |
247 | parsed = self.parse_arg( | |
248 | name="json", | |
249 | field=ma.fields.Nested(schema, many=True), | |
250 | req=req, | |
251 | locations=locations, | |
252 | ) | |
253 | if parsed is missing: | |
254 | parsed = [] | |
255 | else: | |
256 | argdict = schema.fields | |
257 | parsed = {} | |
258 | for argname, field_obj in iteritems(argdict): | |
259 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
260 | parsed_value = self.parse_arg(argname, field_obj, req, locations) | |
261 | # If load_from is specified on the field, try to parse from that key | |
262 | if parsed_value is missing and field_obj.load_from: | |
263 | parsed_value = self.parse_arg( | |
264 | field_obj.load_from, field_obj, req, locations | |
265 | ) | |
266 | argname = field_obj.load_from | |
267 | else: | |
268 | argname = field_obj.data_key or argname | |
269 | parsed_value = self.parse_arg(argname, field_obj, req, locations) | |
270 | if parsed_value is not missing: | |
271 | parsed[argname] = parsed_value | |
272 | return parsed | |
154 | def _load_location_data(self, *, schema, req, location): | |
155 | """Return a dictionary-like object for the location on the given request. | |
156 | ||
157 | Needs to have the schema in hand in order to correctly handle loading | |
158 | lists from multidict objects and `many=True` schemas. | |
159 | """ | |
160 | loader_func = self._get_loader(location) | |
161 | data = loader_func(req, schema) | |
162 | # when the desired location is empty (no data), provide an empty | |
163 | # dict as the default so that optional arguments in a location | |
164 | # (e.g. optional JSON body) work smoothly | |
165 | if data is missing: | |
166 | data = {} | |
167 | return data | |
273 | 168 | |
274 | 169 | def _on_validation_error( |
275 | self, error, req, schema, error_status_code, error_headers | |
170 | self, error, req, schema, location, *, error_status_code, error_headers | |
276 | 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} | |
277 | 178 | error_handler = self.error_callback or self.handle_error |
278 | error_handler(error, req, schema, error_status_code, error_headers) | |
179 | error_handler( | |
180 | error, | |
181 | req, | |
182 | schema, | |
183 | error_status_code=error_status_code, | |
184 | error_headers=error_headers, | |
185 | ) | |
279 | 186 | |
280 | 187 | def _validate_arguments(self, data, validators): |
281 | 188 | for validator in validators: |
299 | 206 | elif callable(argmap): |
300 | 207 | schema = argmap(req) |
301 | 208 | else: |
302 | schema = dict2schema(argmap, self.schema_class)() | |
209 | schema = dict2schema(argmap, schema_class=self.schema_class)() | |
303 | 210 | if MARSHMALLOW_VERSION_INFO[0] < 3 and not schema.strict: |
304 | 211 | warnings.warn( |
305 | 212 | "It is highly recommended that you set strict=True on your schema " |
308 | 215 | ) |
309 | 216 | return schema |
310 | 217 | |
311 | def _clone(self): | |
312 | clone = copy(self) | |
313 | clone.clear_cache() | |
314 | return clone | |
315 | ||
316 | 218 | def parse( |
317 | 219 | self, |
318 | 220 | argmap, |
319 | 221 | req=None, |
320 | locations=None, | |
222 | *, | |
223 | location=None, | |
321 | 224 | validate=None, |
322 | 225 | error_status_code=None, |
323 | error_headers=None, | |
226 | error_headers=None | |
324 | 227 | ): |
325 | 228 | """Main request parsing method. |
326 | 229 | |
328 | 231 | of argname -> `marshmallow.fields.Field` pairs, or a callable |
329 | 232 | which accepts a request and returns a `marshmallow.Schema`. |
330 | 233 | :param req: The request object to parse. |
331 | :param tuple locations: Where on the request to search for values. | |
332 | Can include one or more of ``('json', 'querystring', 'form', | |
333 | 'headers', 'cookies', 'files')``. | |
234 | :param str location: Where on the request to load values. | |
235 | Can be any of the values in :py:attr:`~__location_map__`. By | |
236 | default, that means one of ``('json', 'query', 'querystring', | |
237 | 'form', 'headers', 'cookies', 'files', 'json_or_form')``. | |
334 | 238 | :param callable validate: Validation function or list of validation functions |
335 | 239 | that receives the dictionary of parsed arguments. Validator either returns a |
336 | 240 | boolean or raises a :exc:`ValidationError`. |
341 | 245 | |
342 | 246 | :return: A dictionary of parsed arguments |
343 | 247 | """ |
344 | self.clear_cache() # in case someone used `parse_*()` | |
345 | 248 | req = req if req is not None else self.get_default_request() |
346 | assert req is not None, "Must pass req object" | |
249 | location = location or self.location | |
250 | if req is None: | |
251 | raise ValueError("Must pass req object") | |
347 | 252 | data = None |
348 | 253 | validators = _ensure_list_of_callables(validate) |
349 | parser = self._clone() | |
350 | 254 | schema = self._get_schema(argmap, req) |
351 | 255 | try: |
352 | parsed = parser._parse_request( | |
353 | schema=schema, req=req, locations=locations or self.locations | |
256 | location_data = self._load_location_data( | |
257 | schema=schema, req=req, location=location | |
354 | 258 | ) |
355 | result = schema.load(parsed) | |
259 | result = schema.load(location_data) | |
356 | 260 | data = result.data if MARSHMALLOW_VERSION_INFO[0] < 3 else result |
357 | parser._validate_arguments(data, validators) | |
261 | self._validate_arguments(data, validators) | |
358 | 262 | except ma.exceptions.ValidationError as error: |
359 | parser._on_validation_error( | |
360 | error, req, schema, error_status_code, error_headers | |
263 | self._on_validation_error( | |
264 | error, | |
265 | req, | |
266 | schema, | |
267 | location, | |
268 | error_status_code=error_status_code, | |
269 | error_headers=error_headers, | |
270 | ) | |
271 | warnings.warn( | |
272 | "_on_validation_error hook did not raise an exception and flow " | |
273 | "of control returned to parse(). You may get unexpected results" | |
361 | 274 | ) |
362 | 275 | return data |
363 | ||
364 | def clear_cache(self): | |
365 | """Invalidate the parser's cache. | |
366 | ||
367 | This is usually a no-op now since the Parser clone used for parsing a | |
368 | request is discarded afterwards. It can still be used when manually | |
369 | calling ``parse_*`` methods which would populate the cache on the main | |
370 | Parser instance. | |
371 | """ | |
372 | self._cache = {} | |
373 | return None | |
374 | 276 | |
375 | 277 | def get_default_request(self): |
376 | 278 | """Optional override. Provides a hook for frameworks that use thread-local |
392 | 294 | """ |
393 | 295 | return None |
394 | 296 | |
297 | @staticmethod | |
298 | def _update_args_kwargs(args, kwargs, parsed_args, as_kwargs): | |
299 | """Update args or kwargs with parsed_args depending on as_kwargs""" | |
300 | if as_kwargs: | |
301 | kwargs.update(parsed_args) | |
302 | else: | |
303 | # Add parsed_args after other positional arguments | |
304 | args += (parsed_args,) | |
305 | return args, kwargs | |
306 | ||
395 | 307 | def use_args( |
396 | 308 | self, |
397 | 309 | argmap, |
398 | 310 | req=None, |
399 | locations=None, | |
311 | *, | |
312 | location=None, | |
400 | 313 | as_kwargs=False, |
401 | 314 | validate=None, |
402 | 315 | error_status_code=None, |
403 | error_headers=None, | |
316 | error_headers=None | |
404 | 317 | ): |
405 | 318 | """Decorator that injects parsed arguments into a view function or method. |
406 | 319 | |
407 | 320 | Example usage with Flask: :: |
408 | 321 | |
409 | 322 | @app.route('/echo', methods=['get', 'post']) |
410 | @parser.use_args({'name': fields.Str()}) | |
323 | @parser.use_args({'name': fields.Str()}, location="querystring") | |
411 | 324 | def greet(args): |
412 | 325 | return 'Hello ' + args['name'] |
413 | 326 | |
414 | 327 | :param argmap: Either a `marshmallow.Schema`, a `dict` |
415 | 328 | of argname -> `marshmallow.fields.Field` pairs, or a callable |
416 | 329 | which accepts a request and returns a `marshmallow.Schema`. |
417 | :param tuple locations: Where on the request to search for values. | |
330 | :param str location: Where on the request to load values. | |
418 | 331 | :param bool as_kwargs: Whether to insert arguments as keyword arguments. |
419 | 332 | :param callable validate: Validation function that receives the dictionary |
420 | 333 | of parsed arguments. If the function returns ``False``, the parser |
424 | 337 | :param dict error_headers: Headers passed to error handler functions when a |
425 | 338 | a `ValidationError` is raised. |
426 | 339 | """ |
427 | locations = locations or self.locations | |
340 | location = location or self.location | |
428 | 341 | request_obj = req |
429 | 342 | # Optimization: If argmap is passed as a dictionary, we only need |
430 | 343 | # to generate a Schema once |
431 | 344 | if isinstance(argmap, Mapping): |
432 | argmap = dict2schema(argmap, self.schema_class)() | |
345 | argmap = dict2schema(argmap, schema_class=self.schema_class)() | |
433 | 346 | |
434 | 347 | def decorator(func): |
435 | 348 | req_ = request_obj |
440 | 353 | |
441 | 354 | if not req_obj: |
442 | 355 | req_obj = self.get_request_from_view_args(func, args, kwargs) |
356 | ||
443 | 357 | # NOTE: At this point, argmap may be a Schema, or a callable |
444 | 358 | parsed_args = self.parse( |
445 | 359 | argmap, |
446 | 360 | req=req_obj, |
447 | locations=locations, | |
361 | location=location, | |
448 | 362 | validate=validate, |
449 | 363 | error_status_code=error_status_code, |
450 | 364 | error_headers=error_headers, |
451 | 365 | ) |
452 | if as_kwargs: | |
453 | kwargs.update(parsed_args) | |
454 | return func(*args, **kwargs) | |
455 | else: | |
456 | # Add parsed_args after other positional arguments | |
457 | new_args = args + (parsed_args,) | |
458 | return func(*new_args, **kwargs) | |
366 | args, kwargs = self._update_args_kwargs( | |
367 | args, kwargs, parsed_args, as_kwargs | |
368 | ) | |
369 | return func(*args, **kwargs) | |
459 | 370 | |
460 | 371 | wrapper.__wrapped__ = func |
461 | 372 | return wrapper |
462 | 373 | |
463 | 374 | return decorator |
464 | 375 | |
465 | def use_kwargs(self, *args, **kwargs): | |
376 | def use_kwargs(self, *args, **kwargs) -> typing.Callable: | |
466 | 377 | """Decorator that injects parsed arguments into a view function or method |
467 | 378 | as keyword arguments. |
468 | 379 | |
480 | 391 | kwargs["as_kwargs"] = True |
481 | 392 | return self.use_args(*args, **kwargs) |
482 | 393 | |
483 | def location_handler(self, name): | |
484 | """Decorator that registers a function for parsing a request location. | |
485 | The wrapped function receives a request, the name of the argument, and | |
486 | the corresponding `Field <marshmallow.fields.Field>` object. | |
394 | def location_loader(self, name): | |
395 | """Decorator that registers a function for loading a request location. | |
396 | The wrapped function receives a schema and a request. | |
397 | ||
398 | The schema will usually not be relevant, but it's important in some | |
399 | cases -- most notably in order to correctly load multidict values into | |
400 | list fields. Without the schema, there would be no way to know whether | |
401 | to simply `.get()` or `.getall()` from a multidict for a given value. | |
487 | 402 | |
488 | 403 | Example: :: |
489 | 404 | |
490 | 405 | from webargs import core |
491 | 406 | parser = core.Parser() |
492 | 407 | |
493 | @parser.location_handler("name") | |
494 | def parse_data(request, name, field): | |
495 | return request.data.get(name) | |
408 | @parser.location_loader("name") | |
409 | def load_data(request, schema): | |
410 | return request.data | |
496 | 411 | |
497 | 412 | :param str name: The name of the location to register. |
498 | 413 | """ |
522 | 437 | |
523 | 438 | |
524 | 439 | @parser.error_handler |
525 | def handle_error(error, req, schema, status_code, headers): | |
440 | def handle_error(error, req, schema, *, error_status_code, error_headers): | |
526 | 441 | raise CustomError(error.messages) |
527 | 442 | |
528 | 443 | :param callable func: The error callback to register. |
530 | 445 | self.error_callback = func |
531 | 446 | return func |
532 | 447 | |
448 | def _handle_invalid_json_error(self, error, req, *args, **kwargs): | |
449 | """Internal hook for overriding treatment of JSONDecodeErrors. | |
450 | ||
451 | Invoked by default `load_json` implementation. | |
452 | ||
453 | External parsers can just implement their own behavior for load_json , | |
454 | so this is not part of the public parser API. | |
455 | """ | |
456 | raise error | |
457 | ||
458 | def load_json(self, req, schema): | |
459 | """Load JSON from a request object or return `missing` if no value can | |
460 | be found. | |
461 | """ | |
462 | # NOTE: although this implementation is real/concrete and used by | |
463 | # several of the parsers in webargs, it relies on the internal hooks | |
464 | # `_handle_invalid_json_error` and `_raw_load_json` | |
465 | # these methods are not part of the public API and are used to simplify | |
466 | # code sharing amongst the built-in webargs parsers | |
467 | try: | |
468 | return self._raw_load_json(req) | |
469 | except json.JSONDecodeError as exc: | |
470 | if exc.doc == "": | |
471 | return missing | |
472 | return self._handle_invalid_json_error(exc, req) | |
473 | except UnicodeDecodeError as exc: | |
474 | return self._handle_invalid_json_error(exc, req) | |
475 | ||
476 | def load_json_or_form(self, req, schema): | |
477 | """Load data from a request, accepting either JSON or form-encoded | |
478 | data. | |
479 | ||
480 | The data will first be loaded as JSON, and, if that fails, it will be | |
481 | loaded as a form post. | |
482 | """ | |
483 | data = self.load_json(req, schema) | |
484 | if data is not missing: | |
485 | return data | |
486 | return self.load_form(req, schema) | |
487 | ||
533 | 488 | # Abstract Methods |
534 | 489 | |
535 | def parse_json(self, req, name, arg): | |
536 | """Pull a JSON value from a request object or return `missing` if the | |
537 | value cannot be found. | |
490 | def _raw_load_json(self, req): | |
491 | """Internal hook method for implementing load_json() | |
492 | ||
493 | Get a request body for feeding in to `load_json`, and parse it either | |
494 | using core.parse_json() or similar utilities which raise | |
495 | JSONDecodeErrors. | |
496 | Ensure consistent behavior when encountering decoding errors. | |
497 | ||
498 | The default implementation here simply returns `missing`, and the default | |
499 | implementation of `load_json` above will pass that value through. | |
500 | However, by implementing a "mostly concrete" version of load_json with | |
501 | this as a hook for getting data, we consolidate the logic for handling | |
502 | those JSONDecodeErrors. | |
538 | 503 | """ |
539 | 504 | return missing |
540 | 505 | |
541 | def parse_querystring(self, req, name, arg): | |
542 | """Pull a value from the query string of a request object or return `missing` if | |
543 | the value cannot be found. | |
506 | def load_querystring(self, req, schema): | |
507 | """Load the query string of a request object or return `missing` if no | |
508 | value can be found. | |
544 | 509 | """ |
545 | 510 | return missing |
546 | 511 | |
547 | def parse_form(self, req, name, arg): | |
548 | """Pull a value from the form data of a request object or return | |
549 | `missing` if the value cannot be found. | |
512 | def load_form(self, req, schema): | |
513 | """Load the form data of a request object or return `missing` if no | |
514 | value can be found. | |
550 | 515 | """ |
551 | 516 | return missing |
552 | 517 | |
553 | def parse_headers(self, req, name, arg): | |
554 | """Pull a value from the headers or return `missing` if the value | |
555 | cannot be found. | |
556 | """ | |
518 | def load_headers(self, req, schema): | |
519 | """Load the headers or return `missing` if no value can be found.""" | |
557 | 520 | return missing |
558 | 521 | |
559 | def parse_cookies(self, req, name, arg): | |
560 | """Pull a cookie value from the request or return `missing` if the value | |
561 | cannot be found. | |
522 | def load_cookies(self, req, schema): | |
523 | """Load the cookies from the request or return `missing` if no value | |
524 | can be found. | |
562 | 525 | """ |
563 | 526 | return missing |
564 | 527 | |
565 | def parse_files(self, req, name, arg): | |
566 | """Pull a file from the request or return `missing` if the value file | |
567 | cannot be found. | |
528 | def load_files(self, req, schema): | |
529 | """Load files from the request or return `missing` if no values can be | |
530 | found. | |
568 | 531 | """ |
569 | 532 | return missing |
570 | 533 | |
571 | def handle_error( | |
572 | self, error, req, schema, error_status_code=None, error_headers=None | |
573 | ): | |
534 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): | |
574 | 535 | """Called if an error occurs while parsing args. By default, just logs and |
575 | 536 | raises ``error``. |
576 | 537 | """ |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | import marshmallow as ma |
2 | 1 | |
3 | 2 | |
4 | def dict2schema(dct, schema_class=ma.Schema): | |
3 | def dict2schema(dct, *, schema_class=ma.Schema): | |
5 | 4 | """Generate a `marshmallow.Schema` class given a dictionary of |
6 | 5 | `Fields <marshmallow.fields.Field>`. |
7 | 6 | """ |
9 | 8 | return schema_class.from_dict(dct) |
10 | 9 | attrs = dct.copy() |
11 | 10 | |
12 | class Meta(object): | |
11 | class Meta: | |
13 | 12 | strict = True |
14 | 13 | |
15 | 14 | attrs["Meta"] = Meta |
16 | return type(str(""), (schema_class,), attrs) | |
15 | return type("", (schema_class,), attrs) |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """Django request argument parsing. |
2 | 1 | |
3 | 2 | Example usage: :: |
18 | 17 | return HttpResponse('Hello ' + args['name']) |
19 | 18 | """ |
20 | 19 | from webargs import core |
21 | from webargs.core import json | |
20 | from webargs.multidictproxy import MultiDictProxy | |
21 | ||
22 | ||
23 | def is_json_request(req): | |
24 | return core.is_json(req.content_type) | |
22 | 25 | |
23 | 26 | |
24 | 27 | class DjangoParser(core.Parser): |
32 | 35 | the parser and returning the appropriate `HTTPResponse`. |
33 | 36 | """ |
34 | 37 | |
35 | def parse_querystring(self, req, name, field): | |
36 | """Pull the querystring value from the request.""" | |
37 | return core.get_value(req.GET, name, field) | |
38 | def _raw_load_json(self, req): | |
39 | """Read a json payload from the request for the core parser's load_json | |
38 | 40 | |
39 | def parse_form(self, req, name, field): | |
40 | """Pull the form value from the request.""" | |
41 | return core.get_value(req.POST, name, field) | |
41 | Checks the input mimetype and may return 'missing' if the mimetype is | |
42 | non-json, even if the request body is parseable as json.""" | |
43 | if not is_json_request(req): | |
44 | return core.missing | |
42 | 45 | |
43 | def parse_json(self, req, name, field): | |
44 | """Pull a json value from the request body.""" | |
45 | json_data = self._cache.get("json") | |
46 | if json_data is None: | |
47 | if not core.is_json(req.content_type): | |
48 | return core.missing | |
46 | return core.parse_json(req.body) | |
49 | 47 | |
50 | try: | |
51 | self._cache["json"] = json_data = core.parse_json(req.body) | |
52 | except AttributeError: | |
53 | return core.missing | |
54 | except json.JSONDecodeError as e: | |
55 | if e.doc == "": | |
56 | return core.missing | |
57 | else: | |
58 | return self.handle_invalid_json_error(e, req) | |
59 | return core.get_value(json_data, name, field, allow_many_nested=True) | |
48 | def load_querystring(self, req, schema): | |
49 | """Return query params from the request as a MultiDictProxy.""" | |
50 | return MultiDictProxy(req.GET, schema) | |
60 | 51 | |
61 | def parse_cookies(self, req, name, field): | |
62 | """Pull the value from the cookiejar.""" | |
63 | return core.get_value(req.COOKIES, name, field) | |
52 | def load_form(self, req, schema): | |
53 | """Return form values from the request as a MultiDictProxy.""" | |
54 | return MultiDictProxy(req.POST, schema) | |
64 | 55 | |
65 | def parse_headers(self, req, name, field): | |
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): | |
66 | 61 | raise NotImplementedError( |
67 | "Header parsing not supported by {0}".format(self.__class__.__name__) | |
62 | "Header parsing not supported by {}".format(self.__class__.__name__) | |
68 | 63 | ) |
69 | 64 | |
70 | def parse_files(self, req, name, field): | |
71 | """Pull a file from the request.""" | |
72 | return core.get_value(req.FILES, name, field) | |
65 | def load_files(self, req, schema): | |
66 | """Return files from the request as a MultiDictProxy.""" | |
67 | return MultiDictProxy(req.FILES, schema) | |
73 | 68 | |
74 | 69 | def get_request_from_view_args(self, view, args, kwargs): |
75 | 70 | # The first argument is either `self` or `request` |
78 | 73 | except AttributeError: # first arg is request |
79 | 74 | return args[0] |
80 | 75 | |
81 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
82 | raise error | |
83 | ||
84 | 76 | |
85 | 77 | parser = DjangoParser() |
86 | 78 | use_args = parser.use_args |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """Falcon request argument parsing module. |
2 | 1 | """ |
3 | 2 | import falcon |
4 | 3 | from falcon.util.uri import parse_query_string |
5 | 4 | |
6 | 5 | from webargs import core |
7 | from webargs.core import json | |
6 | from webargs.multidictproxy import MultiDictProxy | |
8 | 7 | |
9 | 8 | HTTP_422 = "422 Unprocessable Entity" |
10 | 9 | |
29 | 28 | return content_type and core.is_json(content_type) |
30 | 29 | |
31 | 30 | |
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 | 31 | # NOTE: Adapted from falcon.request.Request._parse_form_urlencoded |
50 | 32 | def parse_form_body(req): |
51 | 33 | if ( |
52 | 34 | req.content_type is not None |
53 | 35 | and "application/x-www-form-urlencoded" in req.content_type |
54 | 36 | ): |
55 | body = req.stream.read() | |
37 | body = req.stream.read(req.content_length or 0) | |
56 | 38 | try: |
57 | 39 | body = body.decode("ascii") |
58 | 40 | except UnicodeDecodeError: |
65 | 47 | ) |
66 | 48 | |
67 | 49 | if body: |
68 | return parse_query_string( | |
69 | body, keep_blank_qs_values=req.options.keep_blank_qs_values | |
70 | ) | |
71 | return {} | |
50 | return parse_query_string(body, keep_blank=req.options.keep_blank_qs_values) | |
51 | ||
52 | return core.missing | |
72 | 53 | |
73 | 54 | |
74 | 55 | class HTTPError(falcon.HTTPError): |
75 | """HTTPError that stores a dictionary of validation error messages. | |
76 | """ | |
56 | """HTTPError that stores a dictionary of validation error messages.""" | |
77 | 57 | |
78 | 58 | def __init__(self, status, errors, *args, **kwargs): |
79 | 59 | self.errors = errors |
80 | super(HTTPError, self).__init__(status, *args, **kwargs) | |
60 | super().__init__(status, *args, **kwargs) | |
81 | 61 | |
82 | 62 | def to_dict(self, *args, **kwargs): |
83 | 63 | """Override `falcon.HTTPError` to include error messages in responses.""" |
84 | ret = super(HTTPError, self).to_dict(*args, **kwargs) | |
64 | ret = super().to_dict(*args, **kwargs) | |
85 | 65 | if self.errors is not None: |
86 | 66 | ret["errors"] = self.errors |
87 | 67 | return ret |
90 | 70 | class FalconParser(core.Parser): |
91 | 71 | """Falcon request argument parser.""" |
92 | 72 | |
93 | def parse_querystring(self, req, name, field): | |
94 | """Pull a querystring value from the request.""" | |
95 | return core.get_value(req.params, name, field) | |
73 | # Note on the use of MultiDictProxy throughout: | |
74 | # Falcon parses query strings and form values into ordinary dicts, but with | |
75 | # the values listified where appropriate | |
76 | # it is still therefore necessary in these cases to wrap them in | |
77 | # MultiDictProxy because we need to use the schema to determine when single | |
78 | # values should be wrapped in lists due to the type of the destination | |
79 | # field | |
96 | 80 | |
97 | def parse_form(self, req, name, field): | |
98 | """Pull a form value from the request. | |
81 | def load_querystring(self, req, schema): | |
82 | """Return query params from the request as a MultiDictProxy.""" | |
83 | return MultiDictProxy(req.params, schema) | |
84 | ||
85 | def load_form(self, req, schema): | |
86 | """Return form values from the request as a MultiDictProxy | |
99 | 87 | |
100 | 88 | .. note:: |
101 | 89 | |
102 | 90 | The request stream will be read and left at EOF. |
103 | 91 | """ |
104 | form = self._cache.get("form") | |
105 | if form is None: | |
106 | self._cache["form"] = form = parse_form_body(req) | |
107 | return core.get_value(form, name, field) | |
92 | form = parse_form_body(req) | |
93 | if form is core.missing: | |
94 | return form | |
95 | return MultiDictProxy(form, schema) | |
108 | 96 | |
109 | def parse_json(self, req, name, field): | |
110 | """Pull a JSON body value from the request. | |
97 | def _raw_load_json(self, req): | |
98 | """Return a json payload from the request for the core parser's load_json | |
111 | 99 | |
112 | .. note:: | |
100 | Checks the input mimetype and may return 'missing' if the mimetype is | |
101 | non-json, even if the request body is parseable as json.""" | |
102 | if not is_json_request(req) or req.content_length in (None, 0): | |
103 | return core.missing | |
104 | body = req.stream.read(req.content_length) | |
105 | if body: | |
106 | return core.parse_json(body) | |
107 | return core.missing | |
113 | 108 | |
114 | The request stream will be read and left at EOF. | |
115 | """ | |
116 | json_data = self._cache.get("json_data") | |
117 | if json_data is None: | |
118 | try: | |
119 | self._cache["json_data"] = json_data = parse_json_body(req) | |
120 | except json.JSONDecodeError as e: | |
121 | return self.handle_invalid_json_error(e, req) | |
122 | return core.get_value(json_data, name, field, allow_many_nested=True) | |
109 | def load_headers(self, req, schema): | |
110 | """Return headers from the request.""" | |
111 | # Falcon only exposes headers as a dict (not multidict) | |
112 | return req.headers | |
123 | 113 | |
124 | def parse_headers(self, req, name, field): | |
125 | """Pull a header value from the request.""" | |
126 | # Use req.get_headers rather than req.headers for performance | |
127 | return req.get_header(name, required=False) or core.missing | |
128 | ||
129 | def parse_cookies(self, req, name, field): | |
130 | """Pull a cookie value from the request.""" | |
131 | cookies = self._cache.get("cookies") | |
132 | if cookies is None: | |
133 | self._cache["cookies"] = cookies = req.cookies | |
134 | return core.get_value(cookies, name, field) | |
114 | def load_cookies(self, req, schema): | |
115 | """Return cookies from the request.""" | |
116 | # Cookies are expressed in Falcon as a dict, but the possibility of | |
117 | # multiple values for a cookie is preserved internally -- if desired in | |
118 | # the future, webargs could add a MultiDict type for Cookies here built | |
119 | # from (req, schema), but Falcon does not provide one out of the box | |
120 | return req.cookies | |
135 | 121 | |
136 | 122 | def get_request_from_view_args(self, view, args, kwargs): |
137 | 123 | """Get request from a resource method's arguments. Assumes that |
138 | 124 | request is the second argument. |
139 | 125 | """ |
140 | 126 | req = args[1] |
141 | assert isinstance(req, falcon.Request), "Argument is not a falcon.Request" | |
127 | if not isinstance(req, falcon.Request): | |
128 | raise TypeError("Argument is not a falcon.Request") | |
142 | 129 | return req |
143 | 130 | |
144 | def parse_files(self, req, name, field): | |
131 | def load_files(self, req, schema): | |
145 | 132 | raise NotImplementedError( |
146 | "Parsing files not yet supported by {0}".format(self.__class__.__name__) | |
133 | "Parsing files not yet supported by {}".format(self.__class__.__name__) | |
147 | 134 | ) |
148 | 135 | |
149 | def handle_error(self, error, req, schema, error_status_code, error_headers): | |
136 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): | |
150 | 137 | """Handles errors during parsing.""" |
151 | 138 | status = status_map.get(error_status_code or self.DEFAULT_VALIDATION_STATUS) |
152 | 139 | if status is None: |
153 | raise LookupError("Status code {0} not supported".format(error_status_code)) | |
140 | raise LookupError("Status code {} not supported".format(error_status_code)) | |
154 | 141 | raise HTTPError(status, errors=error.messages, headers=error_headers) |
155 | 142 | |
156 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
143 | def _handle_invalid_json_error(self, error, req, *args, **kwargs): | |
157 | 144 | status = status_map[400] |
158 | 145 | messages = {"json": ["Invalid JSON body."]} |
159 | 146 | raise HTTPError(status, errors=messages) |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """Field classes. |
2 | 1 | |
3 | 2 | Includes all fields from `marshmallow.fields` in addition to a custom |
9 | 8 | .. code-block:: python |
10 | 9 | |
11 | 10 | args = { |
12 | "active": fields.Bool(location='query'), | |
11 | "active": fields.Bool(location="query"), | |
13 | 12 | "content_type": fields.Str(data_key="Content-Type", location="headers"), |
14 | 13 | } |
15 | 14 | |
40 | 39 | def __init__(self, nested, *args, **kwargs): |
41 | 40 | if isinstance(nested, dict): |
42 | 41 | nested = dict2schema(nested) |
43 | super(Nested, self).__init__(nested, *args, **kwargs) | |
42 | super().__init__(nested, *args, **kwargs) | |
44 | 43 | |
45 | 44 | |
46 | class DelimitedList(ma.fields.List): | |
47 | """Same as `marshmallow.fields.List`, except can load from either a list or | |
48 | a delimited string (e.g. "foo,bar,baz"). | |
45 | class DelimitedFieldMixin: | |
46 | """ | |
47 | This is a mixin class for subclasses of ma.fields.List and ma.fields.Tuple | |
48 | which split on a pre-specified delimiter. By default, the delimiter will be "," | |
49 | 49 | |
50 | :param Field cls_or_instance: A field class or instance. | |
51 | :param str delimiter: Delimiter between values. | |
52 | :param bool as_string: Dump values to string. | |
50 | Because we want the MRO to reach this class before the List or Tuple class, | |
51 | it must be listed first in the superclasses | |
52 | ||
53 | For example, a DelimitedList-like type can be defined like so: | |
54 | ||
55 | >>> class MyDelimitedList(DelimitedFieldMixin, ma.fields.List): | |
56 | >>> pass | |
53 | 57 | """ |
54 | 58 | |
55 | 59 | delimiter = "," |
56 | 60 | |
57 | def __init__(self, cls_or_instance, delimiter=None, as_string=False, **kwargs): | |
58 | self.delimiter = delimiter or self.delimiter | |
59 | self.as_string = as_string | |
60 | super(DelimitedList, self).__init__(cls_or_instance, **kwargs) | |
61 | ||
62 | 61 | def _serialize(self, value, attr, obj): |
63 | ret = super(DelimitedList, self)._serialize(value, attr, obj) | |
64 | if self.as_string: | |
65 | return self.delimiter.join(format(each) for each in ret) | |
66 | return ret | |
62 | # serializing will start with parent-class serialization, so that we correctly | |
63 | # output lists of non-primitive types, e.g. DelimitedList(DateTime) | |
64 | return self.delimiter.join( | |
65 | format(each) for each in super()._serialize(value, attr, obj) | |
66 | ) | |
67 | 67 | |
68 | 68 | def _deserialize(self, value, attr, data, **kwargs): |
69 | try: | |
70 | ret = ( | |
71 | value | |
72 | if ma.utils.is_iterable_but_not_string(value) | |
73 | else value.split(self.delimiter) | |
74 | ) | |
75 | except AttributeError: | |
69 | # attempting to deserialize from a non-string source is an error | |
70 | if not isinstance(value, (str, bytes)): | |
76 | 71 | if MARSHMALLOW_VERSION_INFO[0] < 3: |
77 | 72 | self.fail("invalid") |
78 | 73 | else: |
79 | 74 | raise self.make_error("invalid") |
80 | return super(DelimitedList, self)._deserialize(ret, attr, data, **kwargs) | |
75 | return super()._deserialize(value.split(self.delimiter), attr, data, **kwargs) | |
76 | ||
77 | ||
78 | class DelimitedList(DelimitedFieldMixin, ma.fields.List): | |
79 | """A field which is similar to a List, but takes its input as a delimited | |
80 | string (e.g. "foo,bar,baz"). | |
81 | ||
82 | Like List, it can be given a nested field type which it will use to | |
83 | de/serialize each element of the list. | |
84 | ||
85 | :param Field cls_or_instance: A field class or instance. | |
86 | :param str delimiter: Delimiter between values. | |
87 | """ | |
88 | ||
89 | default_error_messages = {"invalid": "Not a valid delimited list."} | |
90 | delimiter = "," | |
91 | ||
92 | def __init__(self, cls_or_instance, *, delimiter=None, **kwargs): | |
93 | self.delimiter = delimiter or self.delimiter | |
94 | super().__init__(cls_or_instance, **kwargs) | |
95 | ||
96 | ||
97 | # DelimitedTuple can only be defined when using marshmallow3, when Tuple was | |
98 | # added | |
99 | if MARSHMALLOW_VERSION_INFO[0] >= 3: | |
100 | ||
101 | class DelimitedTuple(DelimitedFieldMixin, ma.fields.Tuple): | |
102 | """A field which is similar to a Tuple, but takes its input as a delimited | |
103 | string (e.g. "foo,bar,baz"). | |
104 | ||
105 | Like Tuple, it can be given a tuple of nested field types which it will use to | |
106 | de/serialize each element of the tuple. | |
107 | ||
108 | :param Iterable[Field] tuple_fields: An iterable of field classes or instances. | |
109 | :param str delimiter: Delimiter between values. | |
110 | """ | |
111 | ||
112 | default_error_messages = {"invalid": "Not a valid delimited tuple."} | |
113 | delimiter = "," | |
114 | ||
115 | def __init__(self, tuple_fields, *, delimiter=None, **kwargs): | |
116 | self.delimiter = delimiter or self.delimiter | |
117 | super().__init__(tuple_fields, **kwargs) |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """Flask request argument parsing module. |
2 | 1 | |
3 | 2 | Example: :: |
9 | 8 | |
10 | 9 | app = Flask(__name__) |
11 | 10 | |
12 | hello_args = { | |
13 | 'name': fields.Str(required=True) | |
11 | user_detail_args = { | |
12 | 'per_page': fields.Int() | |
14 | 13 | } |
15 | 14 | |
16 | @app.route('/') | |
17 | @use_args(hello_args) | |
18 | def index(args): | |
19 | return 'Hello ' + args['name'] | |
15 | @app.route("/user/<int:uid>") | |
16 | @use_args(user_detail_args) | |
17 | def user_detail(args, uid): | |
18 | return ("The user page for user {uid}, showing {per_page} posts.").format( | |
19 | uid=uid, per_page=args["per_page"] | |
20 | ) | |
20 | 21 | """ |
21 | 22 | import flask |
22 | 23 | from werkzeug.exceptions import HTTPException |
23 | 24 | |
24 | 25 | from webargs import core |
25 | from webargs.core import json | |
26 | from webargs.compat import MARSHMALLOW_VERSION_INFO | |
27 | from webargs.multidictproxy import MultiDictProxy | |
26 | 28 | |
27 | 29 | |
28 | 30 | def abort(http_status_code, exc=None, **kwargs): |
47 | 49 | """Flask request argument parser.""" |
48 | 50 | |
49 | 51 | __location_map__ = dict( |
50 | view_args="parse_view_args", | |
51 | path="parse_view_args", | |
52 | **core.Parser.__location_map__ | |
52 | view_args="load_view_args", | |
53 | path="load_view_args", | |
54 | **core.Parser.__location_map__, | |
53 | 55 | ) |
54 | 56 | |
55 | def parse_view_args(self, req, name, field): | |
56 | """Pull a value from the request's ``view_args``.""" | |
57 | return core.get_value(req.view_args, name, field) | |
57 | def _raw_load_json(self, req): | |
58 | """Return a json payload from the request for the core parser's load_json | |
58 | 59 | |
59 | def parse_json(self, req, name, field): | |
60 | """Pull a json value from the request.""" | |
61 | json_data = self._cache.get("json") | |
62 | if json_data is None: | |
63 | if not is_json_request(req): | |
64 | return core.missing | |
60 | Checks the input mimetype and may return 'missing' if the mimetype is | |
61 | non-json, even if the request body is parseable as json.""" | |
62 | if not is_json_request(req): | |
63 | return core.missing | |
65 | 64 | |
66 | # We decode the json manually here instead of | |
67 | # using req.get_json() so that we can handle | |
68 | # JSONDecodeErrors consistently | |
69 | data = req.get_data(cache=True) | |
70 | try: | |
71 | self._cache["json"] = json_data = core.parse_json(data) | |
72 | except json.JSONDecodeError as e: | |
73 | if e.doc == "": | |
74 | return core.missing | |
75 | else: | |
76 | return self.handle_invalid_json_error(e, req) | |
77 | return core.get_value(json_data, name, field, allow_many_nested=True) | |
65 | return core.parse_json(req.get_data(cache=True)) | |
78 | 66 | |
79 | def parse_querystring(self, req, name, field): | |
80 | """Pull a querystring value from the request.""" | |
81 | return core.get_value(req.args, name, field) | |
67 | def _handle_invalid_json_error(self, error, req, *args, **kwargs): | |
68 | abort(400, exc=error, messages={"json": ["Invalid JSON body."]}) | |
82 | 69 | |
83 | def parse_form(self, req, name, field): | |
84 | """Pull a form value from the request.""" | |
85 | try: | |
86 | return core.get_value(req.form, name, field) | |
87 | except AttributeError: | |
88 | pass | |
89 | return core.missing | |
70 | def load_view_args(self, req, schema): | |
71 | """Return the request's ``view_args`` or ``missing`` if there are none.""" | |
72 | return req.view_args or core.missing | |
90 | 73 | |
91 | def parse_headers(self, req, name, field): | |
92 | """Pull a value from the header data.""" | |
93 | return core.get_value(req.headers, name, field) | |
74 | def load_querystring(self, req, schema): | |
75 | """Return query params from the request as a MultiDictProxy.""" | |
76 | return MultiDictProxy(req.args, schema) | |
94 | 77 | |
95 | def parse_cookies(self, req, name, field): | |
96 | """Pull a value from the cookiejar.""" | |
97 | return core.get_value(req.cookies, name, field) | |
78 | def load_form(self, req, schema): | |
79 | """Return form values from the request as a MultiDictProxy.""" | |
80 | return MultiDictProxy(req.form, schema) | |
98 | 81 | |
99 | def parse_files(self, req, name, field): | |
100 | """Pull a file from the request.""" | |
101 | return core.get_value(req.files, name, field) | |
82 | def load_headers(self, req, schema): | |
83 | """Return headers from the request as a MultiDictProxy.""" | |
84 | return MultiDictProxy(req.headers, schema) | |
102 | 85 | |
103 | def handle_error(self, error, req, schema, error_status_code, error_headers): | |
86 | def load_cookies(self, req, schema): | |
87 | """Return cookies from the request.""" | |
88 | return req.cookies | |
89 | ||
90 | def load_files(self, req, schema): | |
91 | """Return files from the request as a MultiDictProxy.""" | |
92 | return MultiDictProxy(req.files, schema) | |
93 | ||
94 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): | |
104 | 95 | """Handles errors during parsing. Aborts the current HTTP request and |
105 | 96 | responds with a 422 error. |
106 | 97 | """ |
107 | 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) | |
108 | 109 | abort( |
109 | 110 | status_code, |
110 | 111 | exc=error, |
113 | 114 | headers=error_headers, |
114 | 115 | ) |
115 | 116 | |
116 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
117 | abort(400, exc=error, messages={"json": ["Invalid JSON body."]}) | |
118 | ||
119 | 117 | def get_default_request(self): |
120 | """Override to use Flask's thread-local request objec by default""" | |
118 | """Override to use Flask's thread-local request object by default""" | |
121 | 119 | return flask.request |
122 | 120 | |
123 | 121 |
0 | from collections.abc import Mapping | |
1 | ||
2 | from webargs.compat import MARSHMALLOW_VERSION_INFO | |
3 | from webargs.core import missing, is_multiple | |
4 | ||
5 | ||
6 | class MultiDictProxy(Mapping): | |
7 | """ | |
8 | A proxy object which wraps multidict types along with a matching schema | |
9 | Whenever a value is looked up, it is checked against the schema to see if | |
10 | there is a matching field where `is_multiple` is True. If there is, then | |
11 | the data should be loaded as a list or tuple. | |
12 | ||
13 | In all other cases, __getitem__ proxies directly to the input multidict. | |
14 | """ | |
15 | ||
16 | def __init__(self, multidict, schema): | |
17 | self.data = multidict | |
18 | self.multiple_keys = self._collect_multiple_keys(schema) | |
19 | ||
20 | @staticmethod | |
21 | def _collect_multiple_keys(schema): | |
22 | result = set() | |
23 | for name, field in schema.fields.items(): | |
24 | if not is_multiple(field): | |
25 | continue | |
26 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
27 | result.add(field.load_from if field.load_from is not None else name) | |
28 | else: | |
29 | result.add(field.data_key if field.data_key is not None else name) | |
30 | return result | |
31 | ||
32 | def __getitem__(self, key): | |
33 | val = self.data.get(key, missing) | |
34 | if val is missing or key not in self.multiple_keys: | |
35 | return val | |
36 | if hasattr(self.data, "getlist"): | |
37 | return self.data.getlist(key) | |
38 | if hasattr(self.data, "getall"): | |
39 | return self.data.getall(key) | |
40 | if isinstance(val, (list, tuple)): | |
41 | return val | |
42 | if val is None: | |
43 | return None | |
44 | return [val] | |
45 | ||
46 | def __str__(self): # str(proxy) proxies to str(proxy.data) | |
47 | return str(self.data) | |
48 | ||
49 | def __repr__(self): | |
50 | return "MultiDictProxy(data={!r}, multiple_keys={!r})".format( | |
51 | self.data, self.multiple_keys | |
52 | ) | |
53 | ||
54 | def __delitem__(self, key): | |
55 | del self.data[key] | |
56 | ||
57 | def __setitem__(self, key, value): | |
58 | self.data[key] = value | |
59 | ||
60 | def __getattr__(self, name): | |
61 | return getattr(self.data, name) | |
62 | ||
63 | def __iter__(self): | |
64 | for x in iter(self.data): | |
65 | # special case for header dicts which produce an iterator of tuples | |
66 | # instead of an iterator of strings | |
67 | if isinstance(x, tuple): | |
68 | yield x[0] | |
69 | else: | |
70 | yield x | |
71 | ||
72 | def __contains__(self, x): | |
73 | return x in self.data | |
74 | ||
75 | def __len__(self): | |
76 | return len(self.data) | |
77 | ||
78 | def __eq__(self, other): | |
79 | return self.data == other | |
80 | ||
81 | def __ne__(self, other): | |
82 | return self.data != other |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """Pyramid request argument parsing. |
2 | 1 | |
3 | 2 | Example usage: :: |
24 | 23 | server = make_server('0.0.0.0', 6543, app) |
25 | 24 | server.serve_forever() |
26 | 25 | """ |
27 | import collections | |
28 | 26 | import functools |
27 | from collections.abc import Mapping | |
29 | 28 | |
30 | 29 | from webob.multidict import MultiDict |
31 | 30 | from pyramid.httpexceptions import exception_response |
32 | 31 | |
33 | 32 | from webargs import core |
34 | 33 | from webargs.core import json |
35 | from webargs.compat import text_type | |
34 | from webargs.multidictproxy import MultiDictProxy | |
35 | ||
36 | ||
37 | def is_json_request(req): | |
38 | return core.is_json(req.headers.get("content-type")) | |
36 | 39 | |
37 | 40 | |
38 | 41 | class PyramidParser(core.Parser): |
39 | 42 | """Pyramid request argument parser.""" |
40 | 43 | |
41 | 44 | __location_map__ = dict( |
42 | matchdict="parse_matchdict", | |
43 | path="parse_matchdict", | |
44 | **core.Parser.__location_map__ | |
45 | matchdict="load_matchdict", | |
46 | path="load_matchdict", | |
47 | **core.Parser.__location_map__, | |
45 | 48 | ) |
46 | 49 | |
47 | def parse_querystring(self, req, name, field): | |
48 | """Pull a querystring value from the request.""" | |
49 | return core.get_value(req.GET, name, field) | |
50 | def _raw_load_json(self, req): | |
51 | """Return a json payload from the request for the core parser's load_json | |
50 | 52 | |
51 | def parse_form(self, req, name, field): | |
52 | """Pull a form value from the request.""" | |
53 | return core.get_value(req.POST, name, field) | |
53 | Checks the input mimetype and may return 'missing' if the mimetype is | |
54 | non-json, even if the request body is parseable as json.""" | |
55 | if not is_json_request(req): | |
56 | return core.missing | |
54 | 57 | |
55 | def parse_json(self, req, name, field): | |
56 | """Pull a json value from the request.""" | |
57 | json_data = self._cache.get("json") | |
58 | if json_data is None: | |
59 | if not core.is_json(req.content_type): | |
60 | return core.missing | |
58 | return core.parse_json(req.body, encoding=req.charset) | |
61 | 59 | |
62 | try: | |
63 | self._cache["json"] = json_data = core.parse_json(req.body, req.charset) | |
64 | except json.JSONDecodeError as e: | |
65 | if e.doc == "": | |
66 | return core.missing | |
67 | else: | |
68 | return self.handle_invalid_json_error(e, req) | |
69 | if json_data is None: | |
70 | return core.missing | |
71 | return core.get_value(json_data, name, field, allow_many_nested=True) | |
60 | def load_querystring(self, req, schema): | |
61 | """Return query params from the request as a MultiDictProxy.""" | |
62 | return MultiDictProxy(req.GET, schema) | |
72 | 63 | |
73 | def parse_cookies(self, req, name, field): | |
74 | """Pull the value from the cookiejar.""" | |
75 | return core.get_value(req.cookies, name, field) | |
64 | def load_form(self, req, schema): | |
65 | """Return form values from the request as a MultiDictProxy.""" | |
66 | return MultiDictProxy(req.POST, schema) | |
76 | 67 | |
77 | def parse_headers(self, req, name, field): | |
78 | """Pull a value from the header data.""" | |
79 | return core.get_value(req.headers, name, field) | |
68 | def load_cookies(self, req, schema): | |
69 | """Return cookies from the request as a MultiDictProxy.""" | |
70 | return MultiDictProxy(req.cookies, schema) | |
80 | 71 | |
81 | def parse_files(self, req, name, field): | |
82 | """Pull a file from the request.""" | |
72 | def load_headers(self, req, schema): | |
73 | """Return headers from the request as a MultiDictProxy.""" | |
74 | return MultiDictProxy(req.headers, schema) | |
75 | ||
76 | def load_files(self, req, schema): | |
77 | """Return files from the request as a MultiDictProxy.""" | |
83 | 78 | files = ((k, v) for k, v in req.POST.items() if hasattr(v, "file")) |
84 | return core.get_value(MultiDict(files), name, field) | |
79 | return MultiDictProxy(MultiDict(files), schema) | |
85 | 80 | |
86 | def parse_matchdict(self, req, name, field): | |
87 | """Pull a value from the request's `matchdict`.""" | |
88 | return core.get_value(req.matchdict, name, field) | |
81 | def load_matchdict(self, req, schema): | |
82 | """Return the request's ``matchdict`` as a MultiDictProxy.""" | |
83 | return MultiDictProxy(req.matchdict, schema) | |
89 | 84 | |
90 | def handle_error(self, error, req, schema, error_status_code, error_headers): | |
85 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): | |
91 | 86 | """Handles errors during parsing. Aborts the current HTTP request and |
92 | 87 | responds with a 400 error. |
93 | 88 | """ |
94 | 89 | status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS |
95 | 90 | response = exception_response( |
96 | 91 | status_code, |
97 | detail=text_type(error), | |
92 | detail=str(error), | |
98 | 93 | headers=error_headers, |
99 | 94 | content_type="application/json", |
100 | 95 | ) |
101 | 96 | body = json.dumps(error.messages) |
102 | response.body = body.encode("utf-8") if isinstance(body, text_type) else body | |
97 | response.body = body.encode("utf-8") if isinstance(body, str) else body | |
103 | 98 | raise response |
104 | 99 | |
105 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
100 | def _handle_invalid_json_error(self, error, req, *args, **kwargs): | |
106 | 101 | messages = {"json": ["Invalid JSON body."]} |
107 | 102 | response = exception_response( |
108 | 400, detail=text_type(messages), content_type="application/json" | |
103 | 400, detail=str(messages), content_type="application/json" | |
109 | 104 | ) |
110 | 105 | body = json.dumps(messages) |
111 | response.body = body.encode("utf-8") if isinstance(body, text_type) else body | |
106 | response.body = body.encode("utf-8") if isinstance(body, str) else body | |
112 | 107 | raise response |
113 | 108 | |
114 | 109 | def use_args( |
115 | 110 | self, |
116 | 111 | argmap, |
117 | 112 | req=None, |
118 | locations=core.Parser.DEFAULT_LOCATIONS, | |
113 | *, | |
114 | location=core.Parser.DEFAULT_LOCATION, | |
119 | 115 | as_kwargs=False, |
120 | 116 | validate=None, |
121 | 117 | error_status_code=None, |
122 | error_headers=None, | |
118 | error_headers=None | |
123 | 119 | ): |
124 | 120 | """Decorator that injects parsed arguments into a view callable. |
125 | 121 | Supports the *Class-based View* pattern where `request` is saved as an instance |
129 | 125 | of argname -> `marshmallow.fields.Field` pairs, or a callable |
130 | 126 | which accepts a request and returns a `marshmallow.Schema`. |
131 | 127 | :param req: The request object to parse. Pulled off of the view by default. |
132 | :param tuple locations: Where on the request to search for values. | |
128 | :param str location: Where on the request to load values. | |
133 | 129 | :param bool as_kwargs: Whether to insert arguments as keyword arguments. |
134 | 130 | :param callable validate: Validation function that receives the dictionary |
135 | 131 | of parsed arguments. If the function returns ``False``, the parser |
139 | 135 | :param dict error_headers: Headers passed to error handler functions when a |
140 | 136 | a `ValidationError` is raised. |
141 | 137 | """ |
142 | locations = locations or self.locations | |
138 | location = location or self.location | |
143 | 139 | # Optimization: If argmap is passed as a dictionary, we only need |
144 | 140 | # to generate a Schema once |
145 | if isinstance(argmap, collections.Mapping): | |
146 | argmap = core.dict2schema(argmap, self.schema_class)() | |
141 | if isinstance(argmap, Mapping): | |
142 | argmap = core.dict2schema(argmap, schema_class=self.schema_class)() | |
147 | 143 | |
148 | 144 | def decorator(func): |
149 | 145 | @functools.wraps(func) |
157 | 153 | parsed_args = self.parse( |
158 | 154 | argmap, |
159 | 155 | req=request, |
160 | locations=locations, | |
156 | location=location, | |
161 | 157 | validate=validate, |
162 | 158 | error_status_code=error_status_code, |
163 | 159 | error_headers=error_headers, |
164 | 160 | ) |
165 | if as_kwargs: | |
166 | kwargs.update(parsed_args) | |
167 | return func(obj, *args, **kwargs) | |
168 | else: | |
169 | return func(obj, parsed_args, *args, **kwargs) | |
161 | args, kwargs = self._update_args_kwargs( | |
162 | args, kwargs, parsed_args, as_kwargs | |
163 | ) | |
164 | return func(obj, *args, **kwargs) | |
170 | 165 | |
171 | 166 | wrapper.__wrapped__ = func |
172 | 167 | return wrapper |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """Utilities for testing. Includes a base test class |
2 | 1 | for testing parsers. |
3 | 2 | |
12 | 11 | from webargs.core import json |
13 | 12 | |
14 | 13 | |
15 | class CommonTestCase(object): | |
14 | class CommonTestCase: | |
16 | 15 | """Base test class that defines test methods for common functionality across all |
17 | 16 | parsers. Subclasses must define `create_app`, which returns a WSGI-like app. |
18 | 17 | """ |
39 | 38 | def test_parse_querystring_args(self, testapp): |
40 | 39 | assert testapp.get("/echo?name=Fred").json == {"name": "Fred"} |
41 | 40 | |
42 | def test_parse_querystring_with_query_location_specified(self, testapp): | |
43 | assert testapp.get("/echo_query?name=Steve").json == {"name": "Steve"} | |
44 | ||
45 | 41 | def test_parse_form(self, testapp): |
46 | assert testapp.post("/echo", {"name": "Joe"}).json == {"name": "Joe"} | |
42 | assert testapp.post("/echo_form", {"name": "Joe"}).json == {"name": "Joe"} | |
47 | 43 | |
48 | 44 | def test_parse_json(self, testapp): |
49 | assert testapp.post_json("/echo", {"name": "Fred"}).json == {"name": "Fred"} | |
45 | assert testapp.post_json("/echo_json", {"name": "Fred"}).json == { | |
46 | "name": "Fred" | |
47 | } | |
48 | ||
49 | def test_parse_json_missing(self, testapp): | |
50 | assert testapp.post("/echo_json", "").json == {"name": "World"} | |
51 | ||
52 | def test_parse_json_or_form(self, testapp): | |
53 | assert testapp.post_json("/echo_json_or_form", {"name": "Fred"}).json == { | |
54 | "name": "Fred" | |
55 | } | |
56 | assert testapp.post("/echo_json_or_form", {"name": "Joe"}).json == { | |
57 | "name": "Joe" | |
58 | } | |
59 | assert testapp.post("/echo_json_or_form", "").json == {"name": "World"} | |
50 | 60 | |
51 | 61 | def test_parse_querystring_default(self, testapp): |
52 | 62 | assert testapp.get("/echo").json == {"name": "World"} |
53 | 63 | |
54 | 64 | def test_parse_json_default(self, testapp): |
55 | assert testapp.post_json("/echo", {}).json == {"name": "World"} | |
65 | assert testapp.post_json("/echo_json", {}).json == {"name": "World"} | |
56 | 66 | |
57 | 67 | def test_parse_json_with_charset(self, testapp): |
58 | 68 | res = testapp.post( |
59 | "/echo", | |
69 | "/echo_json", | |
60 | 70 | json.dumps({"name": "Steve"}), |
61 | 71 | content_type="application/json;charset=UTF-8", |
62 | 72 | ) |
64 | 74 | |
65 | 75 | def test_parse_json_with_vendor_media_type(self, testapp): |
66 | 76 | res = testapp.post( |
67 | "/echo", | |
77 | "/echo_json", | |
68 | 78 | json.dumps({"name": "Steve"}), |
69 | 79 | content_type="application/vnd.api+json;charset=UTF-8", |
70 | 80 | ) |
71 | 81 | assert res.json == {"name": "Steve"} |
72 | 82 | |
73 | def test_parse_json_ignores_extra_data(self, testapp): | |
74 | assert testapp.post_json("/echo", {"extra": "data"}).json == {"name": "World"} | |
75 | ||
76 | def test_parse_json_blank(self, testapp): | |
77 | assert testapp.post_json("/echo", None).json == {"name": "World"} | |
78 | ||
79 | def test_parse_json_ignore_unexpected_int(self, testapp): | |
80 | assert testapp.post_json("/echo", 1).json == {"name": "World"} | |
81 | ||
82 | def test_parse_json_ignore_unexpected_list(self, testapp): | |
83 | assert testapp.post_json("/echo", [{"extra": "data"}]).json == {"name": "World"} | |
83 | def test_parse_ignore_extra_data(self, testapp): | |
84 | assert testapp.post_json( | |
85 | "/echo_ignoring_extra_data", {"extra": "data"} | |
86 | ).json == {"name": "World"} | |
87 | ||
88 | def test_parse_json_empty(self, testapp): | |
89 | assert testapp.post_json("/echo_json", {}).json == {"name": "World"} | |
90 | ||
91 | def test_parse_json_error_unexpected_int(self, testapp): | |
92 | res = testapp.post_json("/echo_json", 1, expect_errors=True) | |
93 | assert res.status_code == 422 | |
94 | ||
95 | def test_parse_json_error_unexpected_list(self, testapp): | |
96 | res = testapp.post_json("/echo_json", [{"extra": "data"}], expect_errors=True) | |
97 | assert res.status_code == 422 | |
84 | 98 | |
85 | 99 | def test_parse_json_many_schema_invalid_input(self, testapp): |
86 | 100 | res = testapp.post_json( |
92 | 106 | res = testapp.post_json("/echo_many_schema", [{"name": "Steve"}]).json |
93 | 107 | assert res == [{"name": "Steve"}] |
94 | 108 | |
95 | def test_parse_json_many_schema_ignore_malformed_data(self, testapp): | |
96 | assert testapp.post_json("/echo_many_schema", {"extra": "data"}).json == [] | |
109 | def test_parse_json_many_schema_error_malformed_data(self, testapp): | |
110 | res = testapp.post_json( | |
111 | "/echo_many_schema", {"extra": "data"}, expect_errors=True | |
112 | ) | |
113 | assert res.status_code == 422 | |
97 | 114 | |
98 | 115 | def test_parsing_form_default(self, testapp): |
99 | assert testapp.post("/echo", {}).json == {"name": "World"} | |
116 | assert testapp.post("/echo_form", {}).json == {"name": "World"} | |
100 | 117 | |
101 | 118 | def test_parse_querystring_multiple(self, testapp): |
102 | 119 | expected = {"name": ["steve", "Loria"]} |
103 | 120 | assert testapp.get("/echo_multi?name=steve&name=Loria").json == expected |
104 | 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 | ||
105 | 130 | def test_parse_form_multiple(self, testapp): |
106 | 131 | expected = {"name": ["steve", "Loria"]} |
107 | 132 | assert ( |
108 | testapp.post("/echo_multi", {"name": ["steve", "Loria"]}).json == expected | |
133 | testapp.post("/echo_multi_form", {"name": ["steve", "Loria"]}).json | |
134 | == expected | |
109 | 135 | ) |
110 | 136 | |
111 | 137 | def test_parse_json_list(self, testapp): |
112 | 138 | expected = {"name": ["Steve"]} |
113 | assert testapp.post_json("/echo_multi", {"name": "Steve"}).json == expected | |
139 | assert ( | |
140 | testapp.post_json("/echo_multi_json", {"name": ["Steve"]}).json == expected | |
141 | ) | |
142 | ||
143 | def test_parse_json_list_error_malformed_data(self, testapp): | |
144 | res = testapp.post_json( | |
145 | "/echo_multi_json", {"name": "Steve"}, expect_errors=True | |
146 | ) | |
147 | assert res.status_code == 422 | |
114 | 148 | |
115 | 149 | def test_parse_json_with_nonascii_chars(self, testapp): |
116 | text = u"øˆƒ£ºº∆ƒˆ∆" | |
117 | assert testapp.post_json("/echo", {"name": text}).json == {"name": text} | |
150 | text = "øˆƒ£ºº∆ƒˆ∆" | |
151 | assert testapp.post_json("/echo_json", {"name": text}).json == {"name": text} | |
118 | 152 | |
119 | 153 | # https://github.com/marshmallow-code/webargs/issues/427 |
120 | 154 | def test_parse_json_with_nonutf8_chars(self, testapp): |
121 | 155 | res = testapp.post( |
122 | "/echo", | |
156 | "/echo_json", | |
123 | 157 | b"\xfe", |
124 | 158 | headers={"Accept": "application/json", "Content-Type": "application/json"}, |
125 | 159 | expect_errors=True, |
129 | 163 | assert res.json == {"json": ["Invalid JSON body."]} |
130 | 164 | |
131 | 165 | def test_validation_error_returns_422_response(self, testapp): |
132 | res = testapp.post("/echo", {"name": "b"}, expect_errors=True) | |
166 | res = testapp.post_json("/echo_json", {"name": "b"}, expect_errors=True) | |
133 | 167 | assert res.status_code == 422 |
134 | 168 | |
135 | 169 | def test_user_validation_error_returns_422_response_by_default(self, testapp): |
186 | 220 | res = testapp.post_json("/echo_nested_many", in_data) |
187 | 221 | assert res.json == {} |
188 | 222 | |
189 | def test_parse_json_if_no_json(self, testapp): | |
190 | res = testapp.post("/echo") | |
191 | assert res.json == {"name": "World"} | |
192 | ||
193 | 223 | def test_parse_files(self, testapp): |
194 | 224 | res = testapp.post( |
195 | 225 | "/echo_file", {"myfile": webtest.Upload("README.rst", b"data")} |
198 | 228 | |
199 | 229 | # https://github.com/sloria/webargs/pull/297 |
200 | 230 | def test_empty_json(self, testapp): |
201 | res = testapp.post( | |
202 | "/echo", | |
231 | res = testapp.post("/echo_json") | |
232 | assert res.status_code == 200 | |
233 | assert res.json == {"name": "World"} | |
234 | ||
235 | # https://github.com/sloria/webargs/pull/297 | |
236 | def test_empty_json_with_headers(self, testapp): | |
237 | res = testapp.post( | |
238 | "/echo_json", | |
203 | 239 | "", |
204 | 240 | headers={"Accept": "application/json", "Content-Type": "application/json"}, |
205 | 241 | ) |
209 | 245 | # https://github.com/sloria/webargs/issues/329 |
210 | 246 | def test_invalid_json(self, testapp): |
211 | 247 | res = testapp.post( |
212 | "/echo", | |
248 | "/echo_json", | |
213 | 249 | '{"foo": "bar", }', |
214 | 250 | headers={"Accept": "application/json", "Content-Type": "application/json"}, |
215 | 251 | expect_errors=True, |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """Tornado request argument parsing module. |
2 | 1 | |
3 | 2 | Example: :: |
14 | 13 | self.write(response) |
15 | 14 | """ |
16 | 15 | import tornado.web |
16 | import tornado.concurrent | |
17 | 17 | from tornado.escape import _unicode |
18 | 18 | |
19 | 19 | from webargs import core |
20 | from webargs.compat import basestring | |
21 | from webargs.core import json | |
20 | from webargs.multidictproxy import MultiDictProxy | |
22 | 21 | |
23 | 22 | |
24 | 23 | class HTTPError(tornado.web.HTTPError): |
27 | 26 | def __init__(self, *args, **kwargs): |
28 | 27 | self.messages = kwargs.pop("messages", {}) |
29 | 28 | self.headers = kwargs.pop("headers", None) |
30 | super(HTTPError, self).__init__(*args, **kwargs) | |
29 | super().__init__(*args, **kwargs) | |
31 | 30 | |
32 | 31 | |
33 | def parse_json_body(req): | |
34 | """Return the decoded JSON body from the request.""" | |
32 | def is_json_request(req): | |
35 | 33 | content_type = req.headers.get("Content-Type") |
36 | if content_type and core.is_json(content_type): | |
37 | try: | |
38 | return core.parse_json(req.body) | |
39 | except TypeError: | |
40 | pass | |
41 | except json.JSONDecodeError as e: | |
42 | if e.doc == "": | |
43 | return core.missing | |
44 | else: | |
45 | raise | |
46 | return {} | |
34 | return content_type is not None and core.is_json(content_type) | |
47 | 35 | |
48 | 36 | |
49 | # From tornado.web.RequestHandler.decode_argument | |
50 | def decode_argument(value, name=None): | |
51 | """Decodes an argument from the request. | |
37 | class WebArgsTornadoMultiDictProxy(MultiDictProxy): | |
52 | 38 | """ |
53 | try: | |
54 | return _unicode(value) | |
55 | except UnicodeDecodeError: | |
56 | raise HTTPError(400, "Invalid unicode in %s: %r" % (name or "url", value[:40])) | |
39 | Override class for Tornado multidicts, handles argument decoding | |
40 | requirements. | |
41 | """ | |
42 | ||
43 | def __getitem__(self, key): | |
44 | try: | |
45 | value = self.data.get(key, core.missing) | |
46 | if value is core.missing: | |
47 | return core.missing | |
48 | if key in self.multiple_keys: | |
49 | return [ | |
50 | _unicode(v) if isinstance(v, (str, bytes)) else v for v in value | |
51 | ] | |
52 | if value and isinstance(value, (list, tuple)): | |
53 | value = value[0] | |
54 | ||
55 | if isinstance(value, (str, bytes)): | |
56 | return _unicode(value) | |
57 | return value | |
58 | # based on tornado.web.RequestHandler.decode_argument | |
59 | except UnicodeDecodeError: | |
60 | raise HTTPError(400, "Invalid unicode in {}: {!r}".format(key, value[:40])) | |
57 | 61 | |
58 | 62 | |
59 | def get_value(d, name, field): | |
60 | """Handle gets from 'multidicts' made of lists | |
63 | class WebArgsTornadoCookiesMultiDictProxy(MultiDictProxy): | |
64 | """ | |
65 | And a special override for cookies because they come back as objects with a | |
66 | `value` attribute we need to extract. | |
67 | Also, does not use the `_unicode` decoding step | |
68 | """ | |
61 | 69 | |
62 | It handles cases: ``{"key": [value]}`` and ``{"key": value}`` | |
63 | """ | |
64 | multiple = core.is_multiple(field) | |
65 | value = d.get(name, core.missing) | |
66 | if value is core.missing: | |
67 | return core.missing | |
68 | if multiple and value is not core.missing: | |
69 | return [ | |
70 | decode_argument(v, name) if isinstance(v, basestring) else v for v in value | |
71 | ] | |
72 | ret = value | |
73 | if value and isinstance(value, (list, tuple)): | |
74 | ret = value[0] | |
75 | if isinstance(ret, basestring): | |
76 | return decode_argument(ret, name) | |
77 | else: | |
78 | return ret | |
70 | def __getitem__(self, key): | |
71 | cookie = self.data.get(key, core.missing) | |
72 | if cookie is core.missing: | |
73 | return core.missing | |
74 | if key in self.multiple_keys: | |
75 | return [cookie.value] | |
76 | return cookie.value | |
79 | 77 | |
80 | 78 | |
81 | 79 | class TornadoParser(core.Parser): |
82 | 80 | """Tornado request argument parser.""" |
83 | 81 | |
84 | def parse_json(self, req, name, field): | |
85 | """Pull a json value from the request.""" | |
86 | json_data = self._cache.get("json") | |
87 | if json_data is None: | |
88 | try: | |
89 | self._cache["json"] = json_data = parse_json_body(req) | |
90 | except json.JSONDecodeError as e: | |
91 | return self.handle_invalid_json_error(e, req) | |
92 | if json_data is None: | |
93 | return core.missing | |
94 | return core.get_value(json_data, name, field, allow_many_nested=True) | |
82 | def _raw_load_json(self, req): | |
83 | """Return a json payload from the request for the core parser's load_json | |
95 | 84 | |
96 | def parse_querystring(self, req, name, field): | |
97 | """Pull a querystring value from the request.""" | |
98 | return get_value(req.query_arguments, name, field) | |
85 | Checks the input mimetype and may return 'missing' if the mimetype is | |
86 | non-json, even if the request body is parseable as json.""" | |
87 | if not is_json_request(req): | |
88 | return core.missing | |
99 | 89 | |
100 | def parse_form(self, req, name, field): | |
101 | """Pull a form value from the request.""" | |
102 | return get_value(req.body_arguments, name, field) | |
90 | # request.body may be a concurrent.Future on streaming requests | |
91 | # this would cause a TypeError if we try to parse it | |
92 | if isinstance(req.body, tornado.concurrent.Future): | |
93 | return core.missing | |
103 | 94 | |
104 | def parse_headers(self, req, name, field): | |
105 | """Pull a value from the header data.""" | |
106 | return get_value(req.headers, name, field) | |
95 | return core.parse_json(req.body) | |
107 | 96 | |
108 | def parse_cookies(self, req, name, field): | |
109 | """Pull a value from the header data.""" | |
110 | cookie = req.cookies.get(name) | |
97 | def load_querystring(self, req, schema): | |
98 | """Return query params from the request as a MultiDictProxy.""" | |
99 | return WebArgsTornadoMultiDictProxy(req.query_arguments, schema) | |
111 | 100 | |
112 | if cookie is not None: | |
113 | return [cookie.value] if core.is_multiple(field) else cookie.value | |
114 | else: | |
115 | return [] if core.is_multiple(field) else None | |
101 | def load_form(self, req, schema): | |
102 | """Return form values from the request as a MultiDictProxy.""" | |
103 | return WebArgsTornadoMultiDictProxy(req.body_arguments, schema) | |
116 | 104 | |
117 | def parse_files(self, req, name, field): | |
118 | """Pull a file from the request.""" | |
119 | return get_value(req.files, name, field) | |
105 | def load_headers(self, req, schema): | |
106 | """Return headers from the request as a MultiDictProxy.""" | |
107 | return WebArgsTornadoMultiDictProxy(req.headers, schema) | |
120 | 108 | |
121 | def handle_error(self, error, req, schema, error_status_code, error_headers): | |
109 | def load_cookies(self, req, schema): | |
110 | """Return cookies from the request as a MultiDictProxy.""" | |
111 | # use the specialized subclass specifically for handling Tornado | |
112 | # cookies | |
113 | return WebArgsTornadoCookiesMultiDictProxy(req.cookies, schema) | |
114 | ||
115 | def load_files(self, req, schema): | |
116 | """Return files from the request as a MultiDictProxy.""" | |
117 | return WebArgsTornadoMultiDictProxy(req.files, schema) | |
118 | ||
119 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): | |
122 | 120 | """Handles errors during parsing. Raises a `tornado.web.HTTPError` |
123 | 121 | with a 400 error. |
124 | 122 | """ |
135 | 133 | headers=error_headers, |
136 | 134 | ) |
137 | 135 | |
138 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
136 | def _handle_invalid_json_error(self, error, req, *args, **kwargs): | |
139 | 137 | raise HTTPError( |
140 | 138 | 400, |
141 | 139 | log_message="Invalid JSON body.", |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """Webapp2 request argument parsing module. |
2 | 1 | |
3 | 2 | Example: :: |
30 | 29 | import webob.multidict |
31 | 30 | |
32 | 31 | from webargs import core |
33 | from webargs.core import json | |
32 | from webargs.multidictproxy import MultiDictProxy | |
34 | 33 | |
35 | 34 | |
36 | 35 | class Webapp2Parser(core.Parser): |
37 | 36 | """webapp2 request argument parser.""" |
38 | 37 | |
39 | def parse_json(self, req, name, field): | |
40 | """Pull a json value from the request.""" | |
41 | json_data = self._cache.get("json") | |
42 | if json_data is None: | |
43 | if not core.is_json(req.content_type): | |
44 | return core.missing | |
38 | def _raw_load_json(self, req): | |
39 | """Return a json payload from the request for the core parser's load_json.""" | |
40 | if not core.is_json(req.content_type): | |
41 | return core.missing | |
42 | return core.parse_json(req.body) | |
45 | 43 | |
46 | try: | |
47 | self._cache["json"] = json_data = core.parse_json(req.body) | |
48 | except json.JSONDecodeError as e: | |
49 | if e.doc == "": | |
50 | return core.missing | |
51 | else: | |
52 | raise | |
53 | return core.get_value(json_data, name, field, allow_many_nested=True) | |
44 | def load_querystring(self, req, schema): | |
45 | """Return query params from the request as a MultiDictProxy.""" | |
46 | return MultiDictProxy(req.GET, schema) | |
54 | 47 | |
55 | def parse_querystring(self, req, name, field): | |
56 | """Pull a querystring value from the request.""" | |
57 | return core.get_value(req.GET, name, field) | |
48 | def load_form(self, req, schema): | |
49 | """Return form values from the request as a MultiDictProxy.""" | |
50 | return MultiDictProxy(req.POST, schema) | |
58 | 51 | |
59 | def parse_form(self, req, name, field): | |
60 | """Pull a form value from the request.""" | |
61 | return core.get_value(req.POST, name, field) | |
52 | def load_cookies(self, req, schema): | |
53 | """Return cookies from the request as a MultiDictProxy.""" | |
54 | return MultiDictProxy(req.cookies, schema) | |
62 | 55 | |
63 | def parse_cookies(self, req, name, field): | |
64 | """Pull the value from the cookiejar.""" | |
65 | return core.get_value(req.cookies, name, field) | |
56 | def load_headers(self, req, schema): | |
57 | """Return headers from the request as a MultiDictProxy.""" | |
58 | return MultiDictProxy(req.headers, schema) | |
66 | 59 | |
67 | def parse_headers(self, req, name, field): | |
68 | """Pull a value from the header data.""" | |
69 | return core.get_value(req.headers, name, field) | |
70 | ||
71 | def parse_files(self, req, name, field): | |
72 | """Pull a file from the request.""" | |
60 | def load_files(self, req, schema): | |
61 | """Return files from the request as a MultiDictProxy.""" | |
73 | 62 | files = ((k, v) for k, v in req.POST.items() if hasattr(v, "file")) |
74 | return core.get_value(webob.multidict.MultiDict(files), name, field) | |
63 | return MultiDictProxy(webob.multidict.MultiDict(files), schema) | |
75 | 64 | |
76 | 65 | def get_default_request(self): |
77 | 66 | return webapp2.get_request() |
0 | # -*- coding: utf-8 -*- |
1 | 1 | |
2 | 2 | import aiohttp |
3 | 3 | from aiohttp.web import json_response |
4 | from aiohttp import web | |
5 | 4 | import marshmallow as ma |
6 | 5 | |
7 | 6 | from webargs import fields |
24 | 23 | strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {} |
25 | 24 | hello_many_schema = HelloSchema(many=True, **strict_kwargs) |
26 | 25 | |
26 | # variant which ignores unknown fields | |
27 | exclude_kwargs = ( | |
28 | {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE} | |
29 | ) | |
30 | hello_exclude_schema = HelloSchema(**exclude_kwargs) | |
31 | ||
32 | ||
27 | 33 | ##### Handlers ##### |
28 | 34 | |
29 | 35 | |
30 | 36 | async def echo(request): |
37 | parsed = await parser.parse(hello_args, request, location="query") | |
38 | return json_response(parsed) | |
39 | ||
40 | ||
41 | async def echo_form(request): | |
42 | parsed = await parser.parse(hello_args, request, location="form") | |
43 | return json_response(parsed) | |
44 | ||
45 | ||
46 | async def echo_json(request): | |
31 | 47 | try: |
32 | parsed = await parser.parse(hello_args, request) | |
48 | parsed = await parser.parse(hello_args, request, location="json") | |
33 | 49 | except json.JSONDecodeError: |
34 | raise web.HTTPBadRequest( | |
50 | raise aiohttp.web.HTTPBadRequest( | |
35 | 51 | body=json.dumps(["Invalid JSON."]).encode("utf-8"), |
36 | 52 | content_type="application/json", |
37 | 53 | ) |
38 | 54 | return json_response(parsed) |
39 | 55 | |
40 | 56 | |
41 | async def echo_query(request): | |
42 | parsed = await parser.parse(hello_args, request, locations=("query",)) | |
43 | return json_response(parsed) | |
44 | ||
45 | ||
46 | async def echo_json(request): | |
47 | parsed = await parser.parse(hello_args, request, locations=("json",)) | |
48 | return json_response(parsed) | |
49 | ||
50 | ||
51 | async def echo_form(request): | |
52 | parsed = await parser.parse(hello_args, request, locations=("form",)) | |
53 | return json_response(parsed) | |
54 | ||
55 | ||
56 | @use_args(hello_args) | |
57 | async def echo_json_or_form(request): | |
58 | try: | |
59 | parsed = await parser.parse(hello_args, request, location="json_or_form") | |
60 | except json.JSONDecodeError: | |
61 | raise aiohttp.web.HTTPBadRequest( | |
62 | body=json.dumps(["Invalid JSON."]).encode("utf-8"), | |
63 | content_type="application/json", | |
64 | ) | |
65 | return json_response(parsed) | |
66 | ||
67 | ||
68 | @use_args(hello_args, location="query") | |
57 | 69 | async def echo_use_args(request, args): |
58 | 70 | return json_response(args) |
59 | 71 | |
60 | 72 | |
61 | @use_kwargs(hello_args) | |
73 | @use_kwargs(hello_args, location="query") | |
62 | 74 | async def echo_use_kwargs(request, name): |
63 | 75 | return json_response({"name": name}) |
64 | 76 | |
65 | 77 | |
66 | @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42) | |
78 | @use_args( | |
79 | {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form" | |
80 | ) | |
67 | 81 | async def echo_use_args_validated(request, args): |
68 | 82 | return json_response(args) |
69 | 83 | |
70 | 84 | |
85 | async def echo_ignoring_extra_data(request): | |
86 | return json_response(await parser.parse(hello_exclude_schema, request)) | |
87 | ||
88 | ||
71 | 89 | async def echo_multi(request): |
90 | parsed = await parser.parse(hello_multiple, request, location="query") | |
91 | return json_response(parsed) | |
92 | ||
93 | ||
94 | async def echo_multi_form(request): | |
95 | parsed = await parser.parse(hello_multiple, request, location="form") | |
96 | return json_response(parsed) | |
97 | ||
98 | ||
99 | async def echo_multi_json(request): | |
72 | 100 | parsed = await parser.parse(hello_multiple, request) |
73 | 101 | return json_response(parsed) |
74 | 102 | |
75 | 103 | |
76 | 104 | async def echo_many_schema(request): |
77 | parsed = await parser.parse(hello_many_schema, request, locations=("json",)) | |
78 | return json_response(parsed) | |
79 | ||
80 | ||
81 | @use_args({"value": fields.Int()}) | |
105 | parsed = await parser.parse(hello_many_schema, request) | |
106 | return json_response(parsed) | |
107 | ||
108 | ||
109 | @use_args({"value": fields.Int()}, location="query") | |
82 | 110 | async def echo_use_args_with_path_param(request, args): |
83 | 111 | return json_response(args) |
84 | 112 | |
85 | 113 | |
86 | @use_kwargs({"value": fields.Int()}) | |
114 | @use_kwargs({"value": fields.Int()}, location="query") | |
87 | 115 | async def echo_use_kwargs_with_path_param(request, value): |
88 | 116 | return json_response({"value": value}) |
89 | 117 | |
90 | 118 | |
91 | @use_args({"page": fields.Int(), "q": fields.Int()}, locations=("query",)) | |
92 | @use_args({"name": fields.Str()}, locations=("json",)) | |
119 | @use_args({"page": fields.Int(), "q": fields.Int()}, location="query") | |
120 | @use_args({"name": fields.Str()}) | |
93 | 121 | async def echo_use_args_multiple(request, query_parsed, json_parsed): |
94 | 122 | return json_response({"query_parsed": query_parsed, "json_parsed": json_parsed}) |
95 | 123 | |
104 | 132 | |
105 | 133 | |
106 | 134 | async def echo_headers(request): |
107 | parsed = await parser.parse(hello_args, request, locations=("headers",)) | |
135 | parsed = await parser.parse(hello_exclude_schema, request, location="headers") | |
108 | 136 | return json_response(parsed) |
109 | 137 | |
110 | 138 | |
111 | 139 | async def echo_cookie(request): |
112 | parsed = await parser.parse(hello_args, request, locations=("cookies",)) | |
140 | parsed = await parser.parse(hello_args, request, location="cookies") | |
113 | 141 | return json_response(parsed) |
114 | 142 | |
115 | 143 | |
143 | 171 | |
144 | 172 | |
145 | 173 | async def echo_match_info(request): |
146 | parsed = await parser.parse({"mymatch": fields.Int(location="match_info")}, request) | |
174 | parsed = await parser.parse( | |
175 | {"mymatch": fields.Int()}, request, location="match_info" | |
176 | ) | |
147 | 177 | return json_response(parsed) |
148 | 178 | |
149 | 179 | |
150 | 180 | class EchoHandler: |
151 | @use_args(hello_args) | |
181 | @use_args(hello_args, location="query") | |
152 | 182 | async def get(self, request, args): |
153 | 183 | return json_response(args) |
154 | 184 | |
155 | 185 | |
156 | class EchoHandlerView(web.View): | |
186 | class EchoHandlerView(aiohttp.web.View): | |
157 | 187 | @asyncio.coroutine |
158 | @use_args(hello_args) | |
188 | @use_args(hello_args, location="query") | |
159 | 189 | def get(self, args): |
160 | 190 | return json_response(args) |
161 | 191 | |
162 | 192 | |
163 | 193 | @asyncio.coroutine |
164 | @use_args(HelloSchema, as_kwargs=True) | |
194 | @use_args(HelloSchema, as_kwargs=True, location="query") | |
165 | 195 | def echo_use_schema_as_kwargs(request, name): |
166 | 196 | return json_response({"name": name}) |
167 | 197 | |
177 | 207 | def create_app(): |
178 | 208 | app = aiohttp.web.Application() |
179 | 209 | |
180 | add_route(app, ["GET", "POST"], "/echo", echo) | |
181 | add_route(app, ["GET"], "/echo_query", echo_query) | |
210 | add_route(app, ["GET"], "/echo", echo) | |
211 | add_route(app, ["POST"], "/echo_form", echo_form) | |
182 | 212 | add_route(app, ["POST"], "/echo_json", echo_json) |
183 | add_route(app, ["POST"], "/echo_form", echo_form) | |
184 | add_route(app, ["GET", "POST"], "/echo_use_args", echo_use_args) | |
185 | add_route(app, ["GET", "POST"], "/echo_use_kwargs", echo_use_kwargs) | |
186 | add_route(app, ["GET", "POST"], "/echo_use_args_validated", echo_use_args_validated) | |
187 | add_route(app, ["GET", "POST"], "/echo_multi", echo_multi) | |
213 | add_route(app, ["POST"], "/echo_json_or_form", echo_json_or_form) | |
214 | add_route(app, ["GET"], "/echo_use_args", echo_use_args) | |
215 | add_route(app, ["GET"], "/echo_use_kwargs", echo_use_kwargs) | |
216 | add_route(app, ["POST"], "/echo_use_args_validated", echo_use_args_validated) | |
217 | add_route(app, ["POST"], "/echo_ignoring_extra_data", echo_ignoring_extra_data) | |
218 | add_route(app, ["GET"], "/echo_multi", echo_multi) | |
219 | add_route(app, ["POST"], "/echo_multi_form", echo_multi_form) | |
220 | add_route(app, ["POST"], "/echo_multi_json", echo_multi_json) | |
188 | 221 | add_route(app, ["GET", "POST"], "/echo_many_schema", echo_many_schema) |
189 | 222 | add_route( |
190 | 223 | app, |
0 | from webargs.core import json | |
1 | 0 | from bottle import Bottle, HTTPResponse, debug, request, response |
2 | 1 | |
3 | 2 | import marshmallow as ma |
4 | 3 | from webargs import fields |
5 | 4 | from webargs.bottleparser import parser, use_args, use_kwargs |
6 | from webargs.core import MARSHMALLOW_VERSION_INFO | |
5 | from webargs.core import json, MARSHMALLOW_VERSION_INFO | |
6 | ||
7 | 7 | |
8 | 8 | hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)} |
9 | 9 | hello_multiple = {"name": fields.List(fields.Str())} |
16 | 16 | strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {} |
17 | 17 | hello_many_schema = HelloSchema(many=True, **strict_kwargs) |
18 | 18 | |
19 | # variant which ignores unknown fields | |
20 | exclude_kwargs = ( | |
21 | {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE} | |
22 | ) | |
23 | hello_exclude_schema = HelloSchema(**exclude_kwargs) | |
24 | ||
19 | 25 | |
20 | 26 | app = Bottle() |
21 | 27 | debug(True) |
22 | 28 | |
23 | 29 | |
24 | @app.route("/echo", method=["GET", "POST"]) | |
30 | @app.route("/echo", method=["GET"]) | |
25 | 31 | def echo(): |
26 | return parser.parse(hello_args, request) | |
32 | return parser.parse(hello_args, request, location="query") | |
27 | 33 | |
28 | 34 | |
29 | @app.route("/echo_query") | |
30 | def echo_query(): | |
31 | return parser.parse(hello_args, request, locations=("query",)) | |
35 | @app.route("/echo_form", method=["POST"]) | |
36 | def echo_form(): | |
37 | return parser.parse(hello_args, location="form") | |
32 | 38 | |
33 | 39 | |
34 | 40 | @app.route("/echo_json", method=["POST"]) |
35 | 41 | def echo_json(): |
36 | return parser.parse(hello_args, request, locations=("json",)) | |
42 | return parser.parse(hello_args, location="json") | |
37 | 43 | |
38 | 44 | |
39 | @app.route("/echo_form", method=["POST"]) | |
40 | def echo_form(): | |
41 | return parser.parse(hello_args, request, locations=("form",)) | |
45 | @app.route("/echo_json_or_form", method=["POST"]) | |
46 | def echo_json_or_form(): | |
47 | return parser.parse(hello_args, location="json_or_form") | |
42 | 48 | |
43 | 49 | |
44 | @app.route("/echo_use_args", method=["GET", "POST"]) | |
45 | @use_args(hello_args) | |
50 | @app.route("/echo_use_args", method=["GET"]) | |
51 | @use_args(hello_args, location="query") | |
46 | 52 | def echo_use_args(args): |
47 | 53 | return args |
48 | 54 | |
49 | 55 | |
50 | @app.route("/echo_use_kwargs", method=["GET", "POST"], apply=use_kwargs(hello_args)) | |
51 | def echo_use_kwargs(name): | |
52 | return {"name": name} | |
53 | ||
54 | ||
55 | 56 | @app.route( |
56 | 57 | "/echo_use_args_validated", |
57 | method=["GET", "POST"], | |
58 | apply=use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42), | |
58 | method=["POST"], | |
59 | apply=use_args( | |
60 | {"value": fields.Int()}, | |
61 | validate=lambda args: args["value"] > 42, | |
62 | location="form", | |
63 | ), | |
59 | 64 | ) |
60 | 65 | def echo_use_args_validated(args): |
61 | 66 | return args |
62 | 67 | |
63 | 68 | |
64 | @app.route("/echo_multi", method=["GET", "POST"]) | |
65 | def echo_multi(): | |
66 | return parser.parse(hello_multiple, request) | |
69 | @app.route("/echo_ignoring_extra_data", method=["POST"]) | |
70 | def echo_json_ignore_extra_data(): | |
71 | return parser.parse(hello_exclude_schema) | |
67 | 72 | |
68 | 73 | |
69 | @app.route("/echo_many_schema", method=["GET", "POST"]) | |
74 | @app.route( | |
75 | "/echo_use_kwargs", method=["GET"], apply=use_kwargs(hello_args, location="query") | |
76 | ) | |
77 | def echo_use_kwargs(name): | |
78 | return {"name": name} | |
79 | ||
80 | ||
81 | @app.route("/echo_multi", method=["GET"]) | |
82 | def echo_multi(): | |
83 | return parser.parse(hello_multiple, request, location="query") | |
84 | ||
85 | ||
86 | @app.route("/echo_multi_form", method=["POST"]) | |
87 | def multi_form(): | |
88 | return parser.parse(hello_multiple, location="form") | |
89 | ||
90 | ||
91 | @app.route("/echo_multi_json", method=["POST"]) | |
92 | def multi_json(): | |
93 | return parser.parse(hello_multiple) | |
94 | ||
95 | ||
96 | @app.route("/echo_many_schema", method=["POST"]) | |
70 | 97 | def echo_many_schema(): |
71 | arguments = parser.parse(hello_many_schema, request, locations=("json",)) | |
98 | arguments = parser.parse(hello_many_schema, request) | |
72 | 99 | return HTTPResponse(body=json.dumps(arguments), content_type="application/json") |
73 | 100 | |
74 | 101 | |
75 | 102 | @app.route( |
76 | "/echo_use_args_with_path_param/<name>", apply=use_args({"value": fields.Int()}) | |
103 | "/echo_use_args_with_path_param/<name>", | |
104 | apply=use_args({"value": fields.Int()}, location="query"), | |
77 | 105 | ) |
78 | 106 | def echo_use_args_with_path_param(args, name): |
79 | 107 | return args |
80 | 108 | |
81 | 109 | |
82 | 110 | @app.route( |
83 | "/echo_use_kwargs_with_path_param/<name>", apply=use_kwargs({"value": fields.Int()}) | |
111 | "/echo_use_kwargs_with_path_param/<name>", | |
112 | apply=use_kwargs({"value": fields.Int()}, location="query"), | |
84 | 113 | ) |
85 | 114 | def echo_use_kwargs_with_path_param(name, value): |
86 | 115 | return {"value": value} |
97 | 126 | |
98 | 127 | @app.route("/echo_headers") |
99 | 128 | def echo_headers(): |
100 | return parser.parse(hello_args, request, locations=("headers",)) | |
129 | # the "exclude schema" must be used in this case because WSGI headers may | |
130 | # be populated with many fields not sent by the caller | |
131 | return parser.parse(hello_exclude_schema, request, location="headers") | |
101 | 132 | |
102 | 133 | |
103 | 134 | @app.route("/echo_cookie") |
104 | 135 | def echo_cookie(): |
105 | return parser.parse(hello_args, request, locations=("cookies",)) | |
136 | return parser.parse(hello_args, request, location="cookies") | |
106 | 137 | |
107 | 138 | |
108 | 139 | @app.route("/echo_file", method=["POST"]) |
109 | 140 | def echo_file(): |
110 | 141 | args = {"myfile": fields.Field()} |
111 | result = parser.parse(args, locations=("files",)) | |
142 | result = parser.parse(args, location="files") | |
112 | 143 | myfile = result["myfile"] |
113 | 144 | content = myfile.file.read().decode("utf8") |
114 | 145 | return {"myfile": content} |
6 | 6 | |
7 | 7 | TEMPLATE_DEBUG = True |
8 | 8 | |
9 | ALLOWED_HOSTS = [] | |
9 | ALLOWED_HOSTS = ["*"] | |
10 | 10 | # Application definition |
11 | 11 | |
12 | 12 | INSTALLED_APPS = ("django.contrib.contenttypes",) |
1 | 1 | |
2 | 2 | from tests.apps.django_app.echo import views |
3 | 3 | |
4 | ||
4 | 5 | urlpatterns = [ |
5 | 6 | url(r"^echo$", views.echo), |
6 | url(r"^echo_query$", views.echo_query), | |
7 | url(r"^echo_form$", views.echo_form), | |
7 | 8 | url(r"^echo_json$", views.echo_json), |
8 | url(r"^echo_form$", views.echo_form), | |
9 | url(r"^echo_json_or_form$", views.echo_json_or_form), | |
9 | 10 | url(r"^echo_use_args$", views.echo_use_args), |
11 | url(r"^echo_use_args_validated$", views.echo_use_args_validated), | |
12 | url(r"^echo_ignoring_extra_data$", views.echo_ignoring_extra_data), | |
10 | 13 | url(r"^echo_use_kwargs$", views.echo_use_kwargs), |
11 | 14 | url(r"^echo_multi$", views.echo_multi), |
15 | url(r"^echo_multi_form$", views.echo_multi_form), | |
16 | url(r"^echo_multi_json$", views.echo_multi_json), | |
12 | 17 | url(r"^echo_many_schema$", views.echo_many_schema), |
13 | 18 | url( |
14 | 19 | r"^echo_use_args_with_path_param/(?P<name>\w+)$", |
0 | from webargs.core import json | |
1 | 0 | from django.http import HttpResponse |
2 | 1 | from django.views.generic import View |
2 | import marshmallow as ma | |
3 | 3 | |
4 | import marshmallow as ma | |
5 | 4 | from webargs import fields |
6 | 5 | from webargs.djangoparser import parser, use_args, use_kwargs |
7 | from webargs.core import MARSHMALLOW_VERSION_INFO | |
6 | from webargs.core import json, MARSHMALLOW_VERSION_INFO | |
7 | ||
8 | 8 | |
9 | 9 | hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)} |
10 | 10 | hello_multiple = {"name": fields.List(fields.Str())} |
17 | 17 | strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {} |
18 | 18 | hello_many_schema = HelloSchema(many=True, **strict_kwargs) |
19 | 19 | |
20 | # variant which ignores unknown fields | |
21 | exclude_kwargs = ( | |
22 | {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE} | |
23 | ) | |
24 | hello_exclude_schema = HelloSchema(**exclude_kwargs) | |
25 | ||
20 | 26 | |
21 | 27 | def json_response(data, **kwargs): |
22 | 28 | return HttpResponse(json.dumps(data), content_type="application/json", **kwargs) |
23 | 29 | |
24 | 30 | |
25 | def echo(request): | |
26 | try: | |
27 | args = parser.parse(hello_args, request) | |
28 | except ma.ValidationError as err: | |
29 | return json_response(err.messages, status=parser.DEFAULT_VALIDATION_STATUS) | |
30 | except json.JSONDecodeError: | |
31 | return json_response({"json": ["Invalid JSON body."]}, status=400) | |
32 | return json_response(args) | |
31 | def handle_view_errors(f): | |
32 | def wrapped(*args, **kwargs): | |
33 | try: | |
34 | return f(*args, **kwargs) | |
35 | except ma.ValidationError as err: | |
36 | return json_response(err.messages, status=422) | |
37 | except json.JSONDecodeError: | |
38 | return json_response({"json": ["Invalid JSON body."]}, status=400) | |
39 | ||
40 | return wrapped | |
33 | 41 | |
34 | 42 | |
35 | def echo_query(request): | |
36 | return json_response(parser.parse(hello_args, request, locations=("query",))) | |
43 | @handle_view_errors | |
44 | def echo(request): | |
45 | return json_response(parser.parse(hello_args, request, location="query")) | |
37 | 46 | |
38 | 47 | |
39 | def echo_json(request): | |
40 | return json_response(parser.parse(hello_args, request, locations=("json",))) | |
48 | @handle_view_errors | |
49 | def echo_form(request): | |
50 | return json_response(parser.parse(hello_args, request, location="form")) | |
41 | 51 | |
42 | 52 | |
43 | def echo_form(request): | |
44 | return json_response(parser.parse(hello_args, request, locations=("form",))) | |
53 | @handle_view_errors | |
54 | def echo_json(request): | |
55 | return json_response(parser.parse(hello_args, request, location="json")) | |
45 | 56 | |
46 | 57 | |
47 | @use_args(hello_args) | |
58 | @handle_view_errors | |
59 | def echo_json_or_form(request): | |
60 | return json_response(parser.parse(hello_args, request, location="json_or_form")) | |
61 | ||
62 | ||
63 | @handle_view_errors | |
64 | @use_args(hello_args, location="query") | |
48 | 65 | def echo_use_args(request, args): |
49 | 66 | return json_response(args) |
50 | 67 | |
51 | 68 | |
52 | @use_kwargs(hello_args) | |
69 | @handle_view_errors | |
70 | @use_args( | |
71 | {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form" | |
72 | ) | |
73 | def echo_use_args_validated(args): | |
74 | return json_response(args) | |
75 | ||
76 | ||
77 | @handle_view_errors | |
78 | def echo_ignoring_extra_data(request): | |
79 | return json_response(parser.parse(hello_exclude_schema, request)) | |
80 | ||
81 | ||
82 | @handle_view_errors | |
83 | @use_kwargs(hello_args, location="query") | |
53 | 84 | def echo_use_kwargs(request, name): |
54 | 85 | return json_response({"name": name}) |
55 | 86 | |
56 | 87 | |
88 | @handle_view_errors | |
57 | 89 | def echo_multi(request): |
90 | return json_response(parser.parse(hello_multiple, request, location="query")) | |
91 | ||
92 | ||
93 | @handle_view_errors | |
94 | def echo_multi_form(request): | |
95 | return json_response(parser.parse(hello_multiple, request, location="form")) | |
96 | ||
97 | ||
98 | @handle_view_errors | |
99 | def echo_multi_json(request): | |
58 | 100 | return json_response(parser.parse(hello_multiple, request)) |
59 | 101 | |
60 | 102 | |
103 | @handle_view_errors | |
61 | 104 | def echo_many_schema(request): |
62 | try: | |
63 | return json_response( | |
64 | parser.parse(hello_many_schema, request, locations=("json",)) | |
65 | ) | |
66 | except ma.ValidationError as err: | |
67 | return json_response(err.messages, status=parser.DEFAULT_VALIDATION_STATUS) | |
105 | return json_response(parser.parse(hello_many_schema, request)) | |
68 | 106 | |
69 | 107 | |
70 | @use_args({"value": fields.Int()}) | |
108 | @handle_view_errors | |
109 | @use_args({"value": fields.Int()}, location="query") | |
71 | 110 | def echo_use_args_with_path_param(request, args, name): |
72 | 111 | return json_response(args) |
73 | 112 | |
74 | 113 | |
75 | @use_kwargs({"value": fields.Int()}) | |
114 | @handle_view_errors | |
115 | @use_kwargs({"value": fields.Int()}, location="query") | |
76 | 116 | def echo_use_kwargs_with_path_param(request, value, name): |
77 | 117 | return json_response({"value": value}) |
78 | 118 | |
79 | 119 | |
120 | @handle_view_errors | |
80 | 121 | def always_error(request): |
81 | 122 | def always_fail(value): |
82 | 123 | raise ma.ValidationError("something went wrong") |
83 | 124 | |
84 | 125 | argmap = {"text": fields.Str(validate=always_fail)} |
85 | try: | |
86 | return parser.parse(argmap, request) | |
87 | except ma.ValidationError as err: | |
88 | return json_response(err.messages, status=parser.DEFAULT_VALIDATION_STATUS) | |
126 | return parser.parse(argmap, request) | |
89 | 127 | |
90 | 128 | |
129 | @handle_view_errors | |
91 | 130 | def echo_headers(request): |
92 | return json_response(parser.parse(hello_args, request, locations=("headers",))) | |
131 | return json_response( | |
132 | parser.parse(hello_exclude_schema, request, location="headers") | |
133 | ) | |
93 | 134 | |
94 | 135 | |
136 | @handle_view_errors | |
95 | 137 | def echo_cookie(request): |
96 | return json_response(parser.parse(hello_args, request, locations=("cookies",))) | |
138 | return json_response(parser.parse(hello_args, request, location="cookies")) | |
97 | 139 | |
98 | 140 | |
141 | @handle_view_errors | |
99 | 142 | def echo_file(request): |
100 | 143 | args = {"myfile": fields.Field()} |
101 | result = parser.parse(args, request, locations=("files",)) | |
144 | result = parser.parse(args, request, location="files") | |
102 | 145 | myfile = result["myfile"] |
103 | 146 | content = myfile.read().decode("utf8") |
104 | 147 | return json_response({"myfile": content}) |
105 | 148 | |
106 | 149 | |
150 | @handle_view_errors | |
107 | 151 | def echo_nested(request): |
108 | 152 | argmap = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})} |
109 | 153 | return json_response(parser.parse(argmap, request)) |
110 | 154 | |
111 | 155 | |
156 | @handle_view_errors | |
112 | 157 | def echo_nested_many(request): |
113 | 158 | argmap = { |
114 | 159 | "users": fields.Nested({"id": fields.Int(), "name": fields.Str()}, many=True) |
117 | 162 | |
118 | 163 | |
119 | 164 | class EchoCBV(View): |
165 | @handle_view_errors | |
120 | 166 | def get(self, request): |
121 | try: | |
122 | args = parser.parse(hello_args, self.request) | |
123 | except ma.ValidationError as err: | |
124 | return json_response(err.messages, status=parser.DEFAULT_VALIDATION_STATUS) | |
125 | return json_response(args) | |
167 | location_kwarg = {} if request.method == "POST" else {"location": "query"} | |
168 | return json_response(parser.parse(hello_args, self.request, **location_kwarg)) | |
126 | 169 | |
127 | 170 | post = get |
128 | 171 | |
129 | 172 | |
130 | 173 | class EchoUseArgsCBV(View): |
131 | @use_args(hello_args) | |
174 | @handle_view_errors | |
175 | @use_args(hello_args, location="query") | |
132 | 176 | def get(self, request, args): |
133 | 177 | return json_response(args) |
134 | 178 | |
135 | post = get | |
179 | @handle_view_errors | |
180 | @use_args(hello_args) | |
181 | def post(self, request, args): | |
182 | return json_response(args) | |
136 | 183 | |
137 | 184 | |
138 | 185 | class EchoUseArgsWithParamCBV(View): |
139 | @use_args(hello_args) | |
186 | @handle_view_errors | |
187 | @use_args(hello_args, location="query") | |
140 | 188 | def get(self, request, args, pid): |
141 | 189 | return json_response(args) |
142 | 190 | |
143 | post = get | |
191 | @handle_view_errors | |
192 | @use_args(hello_args) | |
193 | def post(self, request, args, pid): | |
194 | return json_response(args) |
0 | from webargs.core import json | |
1 | ||
2 | 0 | import falcon |
3 | 1 | import marshmallow as ma |
2 | ||
4 | 3 | from webargs import fields |
4 | from webargs.core import MARSHMALLOW_VERSION_INFO, json | |
5 | 5 | from webargs.falconparser import parser, use_args, use_kwargs |
6 | from webargs.core import MARSHMALLOW_VERSION_INFO | |
7 | 6 | |
8 | 7 | hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)} |
9 | 8 | hello_multiple = {"name": fields.List(fields.Str())} |
16 | 15 | strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {} |
17 | 16 | hello_many_schema = HelloSchema(many=True, **strict_kwargs) |
18 | 17 | |
19 | ||
20 | class Echo(object): | |
21 | def on_get(self, req, resp): | |
22 | try: | |
23 | parsed = parser.parse(hello_args, req) | |
24 | except json.JSONDecodeError: | |
25 | resp.body = json.dumps(["Invalid JSON."]) | |
26 | resp.status = falcon.HTTP_400 | |
27 | else: | |
28 | resp.body = json.dumps(parsed) | |
29 | ||
30 | on_post = on_get | |
18 | # variant which ignores unknown fields | |
19 | exclude_kwargs = ( | |
20 | {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE} | |
21 | ) | |
22 | hello_exclude_schema = HelloSchema(**exclude_kwargs) | |
31 | 23 | |
32 | 24 | |
33 | class EchoQuery(object): | |
25 | class Echo: | |
34 | 26 | def on_get(self, req, resp): |
35 | parsed = parser.parse(hello_args, req, locations=("query",)) | |
27 | parsed = parser.parse(hello_args, req, location="query") | |
36 | 28 | resp.body = json.dumps(parsed) |
37 | 29 | |
38 | 30 | |
39 | class EchoJSON(object): | |
31 | class EchoForm: | |
40 | 32 | def on_post(self, req, resp): |
41 | parsed = parser.parse(hello_args, req, locations=("json",)) | |
33 | parsed = parser.parse(hello_args, req, location="form") | |
42 | 34 | resp.body = json.dumps(parsed) |
43 | 35 | |
44 | 36 | |
45 | class EchoForm(object): | |
37 | class EchoJSON: | |
46 | 38 | def on_post(self, req, resp): |
47 | parsed = parser.parse(hello_args, req, locations=("form",)) | |
39 | parsed = parser.parse(hello_args, req, location="json") | |
48 | 40 | resp.body = json.dumps(parsed) |
49 | 41 | |
50 | 42 | |
51 | class EchoUseArgs(object): | |
52 | @use_args(hello_args) | |
43 | class EchoJSONOrForm: | |
44 | def on_post(self, req, resp): | |
45 | parsed = parser.parse(hello_args, req, location="json_or_form") | |
46 | resp.body = json.dumps(parsed) | |
47 | ||
48 | ||
49 | class EchoUseArgs: | |
50 | @use_args(hello_args, location="query") | |
53 | 51 | def on_get(self, req, resp, args): |
54 | 52 | resp.body = json.dumps(args) |
55 | 53 | |
56 | on_post = on_get | |
57 | 54 | |
58 | ||
59 | class EchoUseKwargs(object): | |
60 | @use_kwargs(hello_args) | |
55 | class EchoUseKwargs: | |
56 | @use_kwargs(hello_args, location="query") | |
61 | 57 | def on_get(self, req, resp, name): |
62 | 58 | resp.body = json.dumps({"name": name}) |
63 | 59 | |
64 | on_post = on_get | |
60 | ||
61 | class EchoUseArgsValidated: | |
62 | @use_args( | |
63 | {"value": fields.Int()}, | |
64 | validate=lambda args: args["value"] > 42, | |
65 | location="form", | |
66 | ) | |
67 | def on_post(self, req, resp, args): | |
68 | resp.body = json.dumps(args) | |
65 | 69 | |
66 | 70 | |
67 | class EchoUseArgsValidated(object): | |
68 | @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42) | |
69 | def on_get(self, req, resp, args): | |
70 | resp.body = json.dumps(args) | |
71 | ||
72 | on_post = on_get | |
71 | class EchoJSONIgnoreExtraData: | |
72 | def on_post(self, req, resp): | |
73 | resp.body = json.dumps(parser.parse(hello_exclude_schema, req)) | |
73 | 74 | |
74 | 75 | |
75 | class EchoMulti(object): | |
76 | class EchoMulti: | |
76 | 77 | def on_get(self, req, resp): |
78 | resp.body = json.dumps(parser.parse(hello_multiple, req, location="query")) | |
79 | ||
80 | ||
81 | class EchoMultiForm: | |
82 | def on_post(self, req, resp): | |
83 | resp.body = json.dumps(parser.parse(hello_multiple, req, location="form")) | |
84 | ||
85 | ||
86 | class EchoMultiJSON: | |
87 | def on_post(self, req, resp): | |
77 | 88 | resp.body = json.dumps(parser.parse(hello_multiple, req)) |
78 | 89 | |
79 | on_post = on_get | |
90 | ||
91 | class EchoManySchema: | |
92 | def on_post(self, req, resp): | |
93 | resp.body = json.dumps(parser.parse(hello_many_schema, req)) | |
80 | 94 | |
81 | 95 | |
82 | class EchoManySchema(object): | |
83 | def on_get(self, req, resp): | |
84 | resp.body = json.dumps( | |
85 | parser.parse(hello_many_schema, req, locations=("json",)) | |
86 | ) | |
87 | ||
88 | on_post = on_get | |
89 | ||
90 | ||
91 | class EchoUseArgsWithPathParam(object): | |
92 | @use_args({"value": fields.Int()}) | |
96 | class EchoUseArgsWithPathParam: | |
97 | @use_args({"value": fields.Int()}, location="query") | |
93 | 98 | def on_get(self, req, resp, args, name): |
94 | 99 | resp.body = json.dumps(args) |
95 | 100 | |
96 | 101 | |
97 | class EchoUseKwargsWithPathParam(object): | |
98 | @use_kwargs({"value": fields.Int()}) | |
102 | class EchoUseKwargsWithPathParam: | |
103 | @use_kwargs({"value": fields.Int()}, location="query") | |
99 | 104 | def on_get(self, req, resp, value, name): |
100 | 105 | resp.body = json.dumps({"value": value}) |
101 | 106 | |
102 | 107 | |
103 | class AlwaysError(object): | |
108 | class AlwaysError: | |
104 | 109 | def on_get(self, req, resp): |
105 | 110 | def always_fail(value): |
106 | 111 | raise ma.ValidationError("something went wrong") |
111 | 116 | on_post = on_get |
112 | 117 | |
113 | 118 | |
114 | class EchoHeaders(object): | |
119 | class EchoHeaders: | |
115 | 120 | def on_get(self, req, resp): |
116 | resp.body = json.dumps(parser.parse(hello_args, req, locations=("headers",))) | |
121 | class HeaderSchema(ma.Schema): | |
122 | NAME = fields.Str(missing="World") | |
123 | ||
124 | resp.body = json.dumps( | |
125 | parser.parse(HeaderSchema(**exclude_kwargs), req, location="headers") | |
126 | ) | |
117 | 127 | |
118 | 128 | |
119 | class EchoCookie(object): | |
129 | class EchoCookie: | |
120 | 130 | def on_get(self, req, resp): |
121 | resp.body = json.dumps(parser.parse(hello_args, req, locations=("cookies",))) | |
131 | resp.body = json.dumps(parser.parse(hello_args, req, location="cookies")) | |
122 | 132 | |
123 | 133 | |
124 | class EchoNested(object): | |
134 | class EchoNested: | |
125 | 135 | def on_post(self, req, resp): |
126 | 136 | args = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})} |
127 | 137 | resp.body = json.dumps(parser.parse(args, req)) |
128 | 138 | |
129 | 139 | |
130 | class EchoNestedMany(object): | |
140 | class EchoNestedMany: | |
131 | 141 | def on_post(self, req, resp): |
132 | 142 | args = { |
133 | 143 | "users": fields.Nested( |
138 | 148 | |
139 | 149 | |
140 | 150 | def use_args_hook(args, context_key="args", **kwargs): |
141 | def hook(req, resp, params): | |
151 | def hook(req, resp, resource, params): | |
142 | 152 | parsed_args = parser.parse(args, req=req, **kwargs) |
143 | 153 | req.context[context_key] = parsed_args |
144 | 154 | |
145 | 155 | return hook |
146 | 156 | |
147 | 157 | |
148 | @falcon.before(use_args_hook(hello_args)) | |
149 | class EchoUseArgsHook(object): | |
158 | @falcon.before(use_args_hook(hello_args, location="query")) | |
159 | class EchoUseArgsHook: | |
150 | 160 | def on_get(self, req, resp): |
151 | 161 | resp.body = json.dumps(req.context["args"]) |
152 | 162 | |
154 | 164 | def create_app(): |
155 | 165 | app = falcon.API() |
156 | 166 | app.add_route("/echo", Echo()) |
157 | app.add_route("/echo_query", EchoQuery()) | |
167 | app.add_route("/echo_form", EchoForm()) | |
158 | 168 | app.add_route("/echo_json", EchoJSON()) |
159 | app.add_route("/echo_form", EchoForm()) | |
169 | app.add_route("/echo_json_or_form", EchoJSONOrForm()) | |
160 | 170 | app.add_route("/echo_use_args", EchoUseArgs()) |
161 | 171 | app.add_route("/echo_use_kwargs", EchoUseKwargs()) |
162 | 172 | app.add_route("/echo_use_args_validated", EchoUseArgsValidated()) |
173 | app.add_route("/echo_ignoring_extra_data", EchoJSONIgnoreExtraData()) | |
163 | 174 | app.add_route("/echo_multi", EchoMulti()) |
175 | app.add_route("/echo_multi_form", EchoMultiForm()) | |
176 | app.add_route("/echo_multi_json", EchoMultiJSON()) | |
164 | 177 | app.add_route("/echo_many_schema", EchoManySchema()) |
165 | 178 | app.add_route("/echo_use_args_with_path_param/{name}", EchoUseArgsWithPathParam()) |
166 | 179 | app.add_route( |
0 | from webargs.core import json | |
1 | 0 | from flask import Flask, jsonify as J, Response, request |
2 | 1 | from flask.views import MethodView |
3 | ||
4 | 2 | import marshmallow as ma |
3 | ||
5 | 4 | from webargs import fields |
6 | 5 | from webargs.flaskparser import parser, use_args, use_kwargs |
7 | from webargs.core import MARSHMALLOW_VERSION_INFO | |
6 | from webargs.core import json, MARSHMALLOW_VERSION_INFO | |
8 | 7 | |
9 | 8 | |
10 | 9 | class TestAppConfig: |
22 | 21 | strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {} |
23 | 22 | hello_many_schema = HelloSchema(many=True, **strict_kwargs) |
24 | 23 | |
24 | # variant which ignores unknown fields | |
25 | exclude_kwargs = ( | |
26 | {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE} | |
27 | ) | |
28 | hello_exclude_schema = HelloSchema(**exclude_kwargs) | |
29 | ||
25 | 30 | app = Flask(__name__) |
26 | 31 | app.config.from_object(TestAppConfig) |
27 | 32 | |
28 | 33 | |
29 | @app.route("/echo", methods=["GET", "POST"]) | |
34 | @app.route("/echo", methods=["GET"]) | |
30 | 35 | def echo(): |
31 | return J(parser.parse(hello_args)) | |
32 | ||
33 | ||
34 | @app.route("/echo_query") | |
35 | def echo_query(): | |
36 | return J(parser.parse(hello_args, request, locations=("query",))) | |
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")) | |
37 | 42 | |
38 | 43 | |
39 | 44 | @app.route("/echo_json", methods=["POST"]) |
40 | 45 | def echo_json(): |
41 | return J(parser.parse(hello_args, request, locations=("json",))) | |
42 | ||
43 | ||
44 | @app.route("/echo_form", methods=["POST"]) | |
45 | def echo_form(): | |
46 | return J(parser.parse(hello_args, request, locations=("form",))) | |
47 | ||
48 | ||
49 | @app.route("/echo_use_args", methods=["GET", "POST"]) | |
50 | @use_args(hello_args) | |
46 | return J(parser.parse(hello_args, location="json")) | |
47 | ||
48 | ||
49 | @app.route("/echo_json_or_form", methods=["POST"]) | |
50 | def echo_json_or_form(): | |
51 | return J(parser.parse(hello_args, location="json_or_form")) | |
52 | ||
53 | ||
54 | @app.route("/echo_use_args", methods=["GET"]) | |
55 | @use_args(hello_args, location="query") | |
51 | 56 | def echo_use_args(args): |
52 | 57 | return J(args) |
53 | 58 | |
54 | 59 | |
55 | @app.route("/echo_use_args_validated", methods=["GET", "POST"]) | |
56 | @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42) | |
60 | @app.route("/echo_use_args_validated", methods=["POST"]) | |
61 | @use_args( | |
62 | {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form" | |
63 | ) | |
57 | 64 | def echo_use_args_validated(args): |
58 | 65 | return J(args) |
59 | 66 | |
60 | 67 | |
61 | @app.route("/echo_use_kwargs", methods=["GET", "POST"]) | |
62 | @use_kwargs(hello_args) | |
68 | @app.route("/echo_ignoring_extra_data", methods=["POST"]) | |
69 | def echo_json_ignore_extra_data(): | |
70 | return J(parser.parse(hello_exclude_schema)) | |
71 | ||
72 | ||
73 | @app.route("/echo_use_kwargs", methods=["GET"]) | |
74 | @use_kwargs(hello_args, location="query") | |
63 | 75 | def echo_use_kwargs(name): |
64 | 76 | return J({"name": name}) |
65 | 77 | |
66 | 78 | |
67 | @app.route("/echo_multi", methods=["GET", "POST"]) | |
79 | @app.route("/echo_multi", methods=["GET"]) | |
68 | 80 | def multi(): |
81 | return J(parser.parse(hello_multiple, location="query")) | |
82 | ||
83 | ||
84 | @app.route("/echo_multi_form", methods=["POST"]) | |
85 | def multi_form(): | |
86 | return J(parser.parse(hello_multiple, location="form")) | |
87 | ||
88 | ||
89 | @app.route("/echo_multi_json", methods=["POST"]) | |
90 | def multi_json(): | |
69 | 91 | return J(parser.parse(hello_multiple)) |
70 | 92 | |
71 | 93 | |
72 | 94 | @app.route("/echo_many_schema", methods=["GET", "POST"]) |
73 | 95 | def many_nested(): |
74 | arguments = parser.parse(hello_many_schema, locations=("json",)) | |
96 | arguments = parser.parse(hello_many_schema) | |
75 | 97 | return Response(json.dumps(arguments), content_type="application/json") |
76 | 98 | |
77 | 99 | |
78 | 100 | @app.route("/echo_use_args_with_path_param/<name>") |
79 | @use_args({"value": fields.Int()}) | |
101 | @use_args({"value": fields.Int()}, location="query") | |
80 | 102 | def echo_use_args_with_path(args, name): |
81 | 103 | return J(args) |
82 | 104 | |
83 | 105 | |
84 | 106 | @app.route("/echo_use_kwargs_with_path_param/<name>") |
85 | @use_kwargs({"value": fields.Int()}) | |
107 | @use_kwargs({"value": fields.Int()}, location="query") | |
86 | 108 | def echo_use_kwargs_with_path(name, value): |
87 | 109 | return J({"value": value}) |
88 | 110 | |
98 | 120 | |
99 | 121 | @app.route("/echo_headers") |
100 | 122 | def echo_headers(): |
101 | return J(parser.parse(hello_args, locations=("headers",))) | |
123 | # the "exclude schema" must be used in this case because WSGI headers may | |
124 | # be populated with many fields not sent by the caller | |
125 | return J(parser.parse(hello_exclude_schema, location="headers")) | |
126 | ||
127 | ||
128 | @app.route("/echo_headers_raising") | |
129 | @use_args(HelloSchema(**strict_kwargs), location="headers") | |
130 | def echo_headers_raising(args): | |
131 | # as above, but in this case, don't use the exclude schema (so unexpected | |
132 | # headers will raise errors) | |
133 | return J(args) | |
102 | 134 | |
103 | 135 | |
104 | 136 | @app.route("/echo_cookie") |
105 | 137 | def echo_cookie(): |
106 | return J(parser.parse(hello_args, request, locations=("cookies",))) | |
138 | return J(parser.parse(hello_args, request, location="cookies")) | |
107 | 139 | |
108 | 140 | |
109 | 141 | @app.route("/echo_file", methods=["POST"]) |
110 | 142 | def echo_file(): |
111 | 143 | args = {"myfile": fields.Field()} |
112 | result = parser.parse(args, locations=("files",)) | |
144 | result = parser.parse(args, location="files") | |
113 | 145 | fp = result["myfile"] |
114 | 146 | content = fp.read().decode("utf8") |
115 | 147 | return J({"myfile": content}) |
117 | 149 | |
118 | 150 | @app.route("/echo_view_arg/<view_arg>") |
119 | 151 | def echo_view_arg(view_arg): |
120 | return J(parser.parse({"view_arg": fields.Int()}, locations=("view_args",))) | |
152 | return J(parser.parse({"view_arg": fields.Int()}, location="view_args")) | |
121 | 153 | |
122 | 154 | |
123 | 155 | @app.route("/echo_view_arg_use_args/<view_arg>") |
124 | @use_args({"view_arg": fields.Int(location="view_args")}) | |
156 | @use_args({"view_arg": fields.Int()}, location="view_args") | |
125 | 157 | def echo_view_arg_with_use_args(args, **kwargs): |
126 | 158 | return J(args) |
127 | 159 | |
186 | 218 | def handle_error(err): |
187 | 219 | if err.code == 422: |
188 | 220 | assert isinstance(err.data["schema"], ma.Schema) |
189 | return J(err.data["messages"]), err.code | |
221 | ||
222 | if MARSHMALLOW_VERSION_INFO[0] >= 3: | |
223 | return J(err.data["messages"]), err.code | |
224 | ||
225 | # on marshmallow2, validation errors for nested schemas can fail to encode: | |
226 | # https://github.com/marshmallow-code/marshmallow/issues/493 | |
227 | # to workaround this, convert integer keys to strings | |
228 | def tweak_data(value): | |
229 | if not isinstance(value, dict): | |
230 | return value | |
231 | return {str(k): v for k, v in value.items()} | |
232 | ||
233 | return J({k: tweak_data(v) for k, v in err.data["messages"].items()}), err.code |
0 | from webargs.core import json | |
1 | ||
2 | 0 | from pyramid.config import Configurator |
3 | 1 | from pyramid.httpexceptions import HTTPBadRequest |
4 | 2 | import marshmallow as ma |
5 | 3 | |
6 | 4 | from webargs import fields |
7 | 5 | from webargs.pyramidparser import parser, use_args, use_kwargs |
8 | from webargs.core import MARSHMALLOW_VERSION_INFO | |
6 | from webargs.core import json, MARSHMALLOW_VERSION_INFO | |
9 | 7 | |
10 | 8 | hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)} |
11 | 9 | hello_multiple = {"name": fields.List(fields.Str())} |
18 | 16 | strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {} |
19 | 17 | hello_many_schema = HelloSchema(many=True, **strict_kwargs) |
20 | 18 | |
19 | # variant which ignores unknown fields | |
20 | exclude_kwargs = ( | |
21 | {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE} | |
22 | ) | |
23 | hello_exclude_schema = HelloSchema(**exclude_kwargs) | |
24 | ||
21 | 25 | |
22 | 26 | def echo(request): |
27 | return parser.parse(hello_args, request, location="query") | |
28 | ||
29 | ||
30 | def echo_form(request): | |
31 | return parser.parse(hello_args, request, location="form") | |
32 | ||
33 | ||
34 | def echo_json(request): | |
23 | 35 | try: |
24 | return parser.parse(hello_args, request) | |
36 | return parser.parse(hello_args, request, location="json") | |
25 | 37 | except json.JSONDecodeError: |
26 | 38 | error = HTTPBadRequest() |
27 | 39 | error.body = json.dumps(["Invalid JSON."]).encode("utf-8") |
29 | 41 | raise error |
30 | 42 | |
31 | 43 | |
44 | def echo_json_or_form(request): | |
45 | try: | |
46 | return parser.parse(hello_args, request, location="json_or_form") | |
47 | except json.JSONDecodeError: | |
48 | error = HTTPBadRequest() | |
49 | error.body = json.dumps(["Invalid JSON."]).encode("utf-8") | |
50 | error.content_type = "application/json" | |
51 | raise error | |
52 | ||
53 | ||
54 | def echo_json_ignore_extra_data(request): | |
55 | try: | |
56 | return parser.parse(hello_exclude_schema, request) | |
57 | except json.JSONDecodeError: | |
58 | error = HTTPBadRequest() | |
59 | error.body = json.dumps(["Invalid JSON."]).encode("utf-8") | |
60 | error.content_type = "application/json" | |
61 | raise error | |
62 | ||
63 | ||
32 | 64 | def echo_query(request): |
33 | return parser.parse(hello_args, request, locations=("query",)) | |
34 | ||
35 | ||
36 | def echo_json(request): | |
37 | return parser.parse(hello_args, request, locations=("json",)) | |
38 | ||
39 | ||
40 | def echo_form(request): | |
41 | return parser.parse(hello_args, request, locations=("form",)) | |
42 | ||
43 | ||
44 | @use_args(hello_args) | |
65 | return parser.parse(hello_args, request, location="query") | |
66 | ||
67 | ||
68 | @use_args(hello_args, location="query") | |
45 | 69 | def echo_use_args(request, args): |
46 | 70 | return args |
47 | 71 | |
48 | 72 | |
49 | @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42) | |
73 | @use_args( | |
74 | {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form" | |
75 | ) | |
50 | 76 | def echo_use_args_validated(request, args): |
51 | 77 | return args |
52 | 78 | |
53 | 79 | |
54 | @use_kwargs(hello_args) | |
80 | @use_kwargs(hello_args, location="query") | |
55 | 81 | def echo_use_kwargs(request, name): |
56 | 82 | return {"name": name} |
57 | 83 | |
58 | 84 | |
59 | 85 | def echo_multi(request): |
86 | return parser.parse(hello_multiple, request, location="query") | |
87 | ||
88 | ||
89 | def echo_multi_form(request): | |
90 | return parser.parse(hello_multiple, request, location="form") | |
91 | ||
92 | ||
93 | def echo_multi_json(request): | |
60 | 94 | return parser.parse(hello_multiple, request) |
61 | 95 | |
62 | 96 | |
63 | 97 | def echo_many_schema(request): |
64 | return parser.parse(hello_many_schema, request, locations=("json",)) | |
65 | ||
66 | ||
67 | @use_args({"value": fields.Int()}) | |
98 | return parser.parse(hello_many_schema, request) | |
99 | ||
100 | ||
101 | @use_args({"value": fields.Int()}, location="query") | |
68 | 102 | def echo_use_args_with_path_param(request, args): |
69 | 103 | return args |
70 | 104 | |
71 | 105 | |
72 | @use_kwargs({"value": fields.Int()}) | |
106 | @use_kwargs({"value": fields.Int()}, location="query") | |
73 | 107 | def echo_use_kwargs_with_path_param(request, value): |
74 | 108 | return {"value": value} |
75 | 109 | |
83 | 117 | |
84 | 118 | |
85 | 119 | def echo_headers(request): |
86 | return parser.parse(hello_args, request, locations=("headers",)) | |
120 | return parser.parse(hello_exclude_schema, request, location="headers") | |
87 | 121 | |
88 | 122 | |
89 | 123 | def echo_cookie(request): |
90 | return parser.parse(hello_args, request, locations=("cookies",)) | |
124 | return parser.parse(hello_args, request, location="cookies") | |
91 | 125 | |
92 | 126 | |
93 | 127 | def echo_file(request): |
94 | 128 | args = {"myfile": fields.Field()} |
95 | result = parser.parse(args, request, locations=("files",)) | |
129 | result = parser.parse(args, request, location="files") | |
96 | 130 | myfile = result["myfile"] |
97 | 131 | content = myfile.file.read().decode("utf8") |
98 | 132 | return {"myfile": content} |
111 | 145 | |
112 | 146 | |
113 | 147 | def echo_matchdict(request): |
114 | return parser.parse({"mymatch": fields.Int()}, request, locations=("matchdict",)) | |
115 | ||
116 | ||
117 | class EchoCallable(object): | |
148 | return parser.parse({"mymatch": fields.Int()}, request, location="matchdict") | |
149 | ||
150 | ||
151 | class EchoCallable: | |
118 | 152 | def __init__(self, request): |
119 | 153 | self.request = request |
120 | 154 | |
121 | @use_args({"value": fields.Int()}) | |
155 | @use_args({"value": fields.Int()}, location="query") | |
122 | 156 | def __call__(self, args): |
123 | 157 | return args |
124 | 158 | |
134 | 168 | config = Configurator() |
135 | 169 | |
136 | 170 | add_route(config, "/echo", echo) |
171 | add_route(config, "/echo_form", echo_form) | |
172 | add_route(config, "/echo_json", echo_json) | |
173 | add_route(config, "/echo_json_or_form", echo_json_or_form) | |
137 | 174 | add_route(config, "/echo_query", echo_query) |
138 | add_route(config, "/echo_json", echo_json) | |
139 | add_route(config, "/echo_form", echo_form) | |
175 | add_route(config, "/echo_ignoring_extra_data", echo_json_ignore_extra_data) | |
140 | 176 | add_route(config, "/echo_use_args", echo_use_args) |
141 | 177 | add_route(config, "/echo_use_args_validated", echo_use_args_validated) |
142 | 178 | add_route(config, "/echo_use_kwargs", echo_use_kwargs) |
143 | 179 | add_route(config, "/echo_multi", echo_multi) |
180 | add_route(config, "/echo_multi_form", echo_multi_form) | |
181 | add_route(config, "/echo_multi_json", echo_multi_json) | |
144 | 182 | add_route(config, "/echo_many_schema", echo_many_schema) |
145 | 183 | add_route( |
146 | 184 | config, "/echo_use_args_with_path_param/{name}", echo_use_args_with_path_param |
0 | # -*- coding: utf-8 -*- | |
1 | # flake8: noqa | |
2 | import sys | |
3 | ||
4 | PY2 = int(sys.version[0]) == 2 | |
5 | ||
6 | if PY2: | |
7 | text_type = unicode | |
8 | binary_type = str | |
9 | string_types = (str, unicode) | |
10 | basestring = basestring | |
11 | else: | |
12 | text_type = str | |
13 | binary_type = bytes | |
14 | string_types = (str,) | |
15 | basestring = (str, bytes) |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | import itertools |
2 | import mock | |
3 | 1 | import datetime |
4 | 2 | |
5 | 3 | import pytest |
8 | 6 | from django.utils.datastructures import MultiValueDict as DjMultiDict |
9 | 7 | from bottle import MultiDict as BotMultiDict |
10 | 8 | |
11 | from webargs import fields, missing, ValidationError | |
9 | from webargs import fields, ValidationError | |
12 | 10 | from webargs.core import ( |
13 | 11 | Parser, |
14 | get_value, | |
15 | 12 | dict2schema, |
16 | 13 | is_json, |
17 | 14 | get_mimetype, |
18 | 15 | MARSHMALLOW_VERSION_INFO, |
19 | 16 | ) |
17 | from webargs.multidictproxy import MultiDictProxy | |
18 | ||
19 | try: | |
20 | # Python 3.5 | |
21 | import mock | |
22 | except ImportError: | |
23 | # Python 3.6+ | |
24 | from unittest import mock | |
20 | 25 | |
21 | 26 | |
22 | 27 | strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {} |
26 | 31 | def __init__(self, status_code, headers): |
27 | 32 | self.status_code = status_code |
28 | 33 | self.headers = headers |
29 | super(MockHTTPError, self).__init__(self, "HTTP Error occurred") | |
34 | super().__init__(self, "HTTP Error occurred") | |
30 | 35 | |
31 | 36 | |
32 | 37 | class MockRequestParser(Parser): |
33 | 38 | """A minimal parser implementation that parses mock requests.""" |
34 | 39 | |
35 | def parse_querystring(self, req, name, field): | |
36 | return get_value(req.query, name, field) | |
37 | ||
38 | def parse_json(self, req, name, field): | |
39 | return get_value(req.json, name, field) | |
40 | ||
41 | def parse_cookies(self, req, name, field): | |
42 | return get_value(req.cookies, name, field) | |
40 | def load_querystring(self, req, schema): | |
41 | return MultiDictProxy(req.query, schema) | |
42 | ||
43 | def load_json(self, req, schema): | |
44 | return req.json | |
45 | ||
46 | def load_cookies(self, req, schema): | |
47 | return req.cookies | |
43 | 48 | |
44 | 49 | |
45 | 50 | @pytest.yield_fixture(scope="function") |
58 | 63 | # Parser tests |
59 | 64 | |
60 | 65 | |
61 | @mock.patch("webargs.core.Parser.parse_json") | |
62 | def test_parse_json_called_by_parse_arg(parse_json, web_request): | |
63 | field = fields.Field() | |
66 | @mock.patch("webargs.core.Parser.load_json") | |
67 | def test_load_json_called_by_parse_default(load_json, web_request): | |
68 | schema = dict2schema({"foo": fields.Field()})() | |
69 | load_json.return_value = {"foo": 1} | |
64 | 70 | p = Parser() |
65 | p.parse_arg("foo", field, web_request) | |
66 | parse_json.assert_called_with(web_request, "foo", field) | |
67 | ||
68 | ||
69 | @mock.patch("webargs.core.Parser.parse_querystring") | |
70 | def test_parse_querystring_called_by_parse_arg(parse_querystring, web_request): | |
71 | field = fields.Field() | |
72 | p = Parser() | |
73 | p.parse_arg("foo", field, web_request) | |
74 | assert parse_querystring.called_once() | |
75 | ||
76 | ||
77 | @mock.patch("webargs.core.Parser.parse_form") | |
78 | def test_parse_form_called_by_parse_arg(parse_form, web_request): | |
79 | field = fields.Field() | |
80 | p = Parser() | |
81 | p.parse_arg("foo", field, web_request) | |
82 | assert parse_form.called_once() | |
83 | ||
84 | ||
85 | @mock.patch("webargs.core.Parser.parse_json") | |
86 | def test_parse_json_not_called_when_json_not_a_location(parse_json, web_request): | |
87 | field = fields.Field() | |
88 | p = Parser() | |
89 | p.parse_arg("foo", field, web_request, locations=("form", "querystring")) | |
90 | assert parse_json.call_count == 0 | |
91 | ||
92 | ||
93 | @mock.patch("webargs.core.Parser.parse_headers") | |
94 | def test_parse_headers_called_when_headers_is_a_location(parse_headers, web_request): | |
95 | field = fields.Field() | |
96 | p = Parser() | |
97 | p.parse_arg("foo", field, web_request) | |
98 | assert parse_headers.call_count == 0 | |
99 | p.parse_arg("foo", field, web_request, locations=("headers",)) | |
100 | parse_headers.assert_called() | |
101 | ||
102 | ||
103 | @mock.patch("webargs.core.Parser.parse_cookies") | |
104 | def test_parse_cookies_called_when_cookies_is_a_location(parse_cookies, web_request): | |
105 | field = fields.Field() | |
106 | p = Parser() | |
107 | p.parse_arg("foo", field, web_request) | |
108 | assert parse_cookies.call_count == 0 | |
109 | p.parse_arg("foo", field, web_request, locations=("cookies",)) | |
110 | parse_cookies.assert_called() | |
111 | ||
112 | ||
113 | @mock.patch("webargs.core.Parser.parse_json") | |
114 | def test_parse(parse_json, web_request): | |
115 | parse_json.return_value = 42 | |
71 | p.parse(schema, web_request) | |
72 | load_json.assert_called_with(web_request, schema) | |
73 | ||
74 | ||
75 | @pytest.mark.parametrize( | |
76 | "location", ["querystring", "form", "headers", "cookies", "files"] | |
77 | ) | |
78 | def test_load_nondefault_called_by_parse_with_location(location, web_request): | |
79 | with mock.patch( | |
80 | "webargs.core.Parser.load_{}".format(location) | |
81 | ) as mock_loadfunc, mock.patch("webargs.core.Parser.load_json") as load_json: | |
82 | mock_loadfunc.return_value = {} | |
83 | load_json.return_value = {} | |
84 | p = Parser() | |
85 | ||
86 | # ensure that without location=..., the loader is not called (json is | |
87 | # called) | |
88 | p.parse({"foo": fields.Field()}, web_request) | |
89 | assert mock_loadfunc.call_count == 0 | |
90 | assert load_json.call_count == 1 | |
91 | ||
92 | # but when location=... is given, the loader *is* called and json is | |
93 | # not called | |
94 | p.parse({"foo": fields.Field()}, web_request, location=location) | |
95 | assert mock_loadfunc.call_count == 1 | |
96 | # it was already 1, should not go up | |
97 | assert load_json.call_count == 1 | |
98 | ||
99 | ||
100 | def test_parse(parser, web_request): | |
101 | web_request.json = {"username": 42, "password": 42} | |
116 | 102 | argmap = {"username": fields.Field(), "password": fields.Field()} |
117 | p = Parser() | |
118 | ret = p.parse(argmap, web_request) | |
103 | ret = parser.parse(argmap, web_request) | |
119 | 104 | assert {"username": 42, "password": 42} == ret |
105 | ||
106 | ||
107 | @pytest.mark.skipif( | |
108 | MARSHMALLOW_VERSION_INFO[0] < 3, reason="unknown=... added in marshmallow3" | |
109 | ) | |
110 | def test_parse_with_unknown_behavior_specified(parser, web_request): | |
111 | # This is new in webargs 6.x ; it's the way you can "get back" the behavior | |
112 | # of webargs 5.x in which extra args are ignored | |
113 | from marshmallow import EXCLUDE, INCLUDE, RAISE | |
114 | ||
115 | web_request.json = {"username": 42, "password": 42, "fjords": 42} | |
116 | ||
117 | class CustomSchema(Schema): | |
118 | username = fields.Field() | |
119 | password = fields.Field() | |
120 | ||
121 | # with no unknown setting or unknown=RAISE, it blows up | |
122 | with pytest.raises(ValidationError, match="Unknown field."): | |
123 | parser.parse(CustomSchema(), web_request) | |
124 | with pytest.raises(ValidationError, match="Unknown field."): | |
125 | parser.parse(CustomSchema(unknown=RAISE), web_request) | |
126 | ||
127 | # with unknown=EXCLUDE the data is omitted | |
128 | ret = parser.parse(CustomSchema(unknown=EXCLUDE), web_request) | |
129 | assert {"username": 42, "password": 42} == ret | |
130 | # with unknown=INCLUDE it is added even though it isn't part of the schema | |
131 | ret = parser.parse(CustomSchema(unknown=INCLUDE), web_request) | |
132 | assert {"username": 42, "password": 42, "fjords": 42} == ret | |
120 | 133 | |
121 | 134 | |
122 | 135 | def test_parse_required_arg_raises_validation_error(parser, web_request): |
140 | 153 | assert result == {"first": "Steve", "last": None} |
141 | 154 | |
142 | 155 | |
143 | @mock.patch("webargs.core.Parser.parse_json") | |
144 | def test_parse_required_arg(parse_json, web_request): | |
145 | arg = fields.Field(required=True) | |
146 | parse_json.return_value = 42 | |
147 | p = Parser() | |
148 | result = p.parse_arg("foo", arg, web_request, locations=("json",)) | |
149 | assert result == 42 | |
156 | def test_parse_required_arg(parser, web_request): | |
157 | web_request.json = {"foo": 42} | |
158 | result = parser.parse({"foo": fields.Field(required=True)}, web_request) | |
159 | assert result == {"foo": 42} | |
150 | 160 | |
151 | 161 | |
152 | 162 | def test_parse_required_list(parser, web_request): |
154 | 164 | args = {"foo": fields.List(fields.Field(), required=True)} |
155 | 165 | with pytest.raises(ValidationError) as excinfo: |
156 | 166 | parser.parse(args, web_request) |
157 | assert excinfo.value.messages["foo"][0] == "Missing data for required field." | |
167 | assert ( | |
168 | excinfo.value.messages["json"]["foo"][0] == "Missing data for required field." | |
169 | ) | |
158 | 170 | |
159 | 171 | |
160 | 172 | # Regression test for https://github.com/marshmallow-code/webargs/issues/107 |
169 | 181 | args = {"foo": fields.List(fields.Field(), allow_none=False)} |
170 | 182 | with pytest.raises(ValidationError) as excinfo: |
171 | 183 | parser.parse(args, web_request) |
172 | assert excinfo.value.messages["foo"][0] == "Field may not be null." | |
184 | assert excinfo.value.messages["json"]["foo"][0] == "Field may not be null." | |
173 | 185 | |
174 | 186 | |
175 | 187 | def test_parse_empty_list(parser, web_request): |
184 | 196 | assert parser.parse(args, web_request) == {} |
185 | 197 | |
186 | 198 | |
187 | def test_default_locations(): | |
188 | assert set(Parser.DEFAULT_LOCATIONS) == set(["json", "querystring", "form"]) | |
199 | def test_default_location(): | |
200 | assert Parser.DEFAULT_LOCATION == "json" | |
189 | 201 | |
190 | 202 | |
191 | 203 | def test_missing_with_default(parser, web_request): |
192 | 204 | web_request.json = {} |
193 | 205 | args = {"val": fields.Field(missing="pizza")} |
194 | result = parser.parse(args, web_request, locations=("json",)) | |
206 | result = parser.parse(args, web_request) | |
195 | 207 | assert result["val"] == "pizza" |
196 | 208 | |
197 | 209 | |
198 | 210 | def test_default_can_be_none(parser, web_request): |
199 | 211 | web_request.json = {} |
200 | 212 | args = {"val": fields.Field(missing=None, allow_none=True)} |
201 | result = parser.parse(args, web_request, locations=("json",)) | |
213 | result = parser.parse(args, web_request) | |
202 | 214 | assert result["val"] is None |
203 | 215 | |
204 | 216 | |
209 | 221 | "p": fields.Int( |
210 | 222 | missing=1, |
211 | 223 | validate=lambda p: p > 0, |
212 | error=u"La page demandée n'existe pas", | |
224 | error="La page demandée n'existe pas", | |
213 | 225 | location="querystring", |
214 | 226 | ) |
215 | 227 | } |
216 | 228 | assert parser.parse(args, web_request) == {"p": 1} |
217 | 229 | |
218 | 230 | |
219 | def test_value_error_raised_if_parse_arg_called_with_invalid_location(web_request): | |
231 | def test_value_error_raised_if_parse_called_with_invalid_location(parser, web_request): | |
220 | 232 | field = fields.Field() |
233 | with pytest.raises(ValueError, match="Invalid location argument: invalidlocation"): | |
234 | parser.parse({"foo": field}, web_request, location="invalidlocation") | |
235 | ||
236 | ||
237 | @mock.patch("webargs.core.Parser.handle_error") | |
238 | def test_handle_error_called_when_parsing_raises_error(handle_error, web_request): | |
239 | def always_fail(*args, **kwargs): | |
240 | raise ValidationError("error occurred") | |
241 | ||
221 | 242 | p = Parser() |
222 | with pytest.raises(ValueError) as excinfo: | |
223 | p.parse_arg("foo", field, web_request, locations=("invalidlocation", "headers")) | |
224 | msg = "Invalid locations arguments: {0}".format(["invalidlocation"]) | |
225 | assert msg in str(excinfo.value) | |
226 | ||
227 | ||
228 | def test_value_error_raised_if_invalid_location_on_field(web_request, parser): | |
229 | with pytest.raises(ValueError) as excinfo: | |
230 | parser.parse({"foo": fields.Field(location="invalidlocation")}, web_request) | |
231 | msg = "Invalid locations arguments: {0}".format(["invalidlocation"]) | |
232 | assert msg in str(excinfo.value) | |
233 | ||
234 | ||
235 | @mock.patch("webargs.core.Parser.handle_error") | |
236 | @mock.patch("webargs.core.Parser.parse_json") | |
237 | def test_handle_error_called_when_parsing_raises_error( | |
238 | parse_json, handle_error, web_request | |
239 | ): | |
240 | val_err = ValidationError("error occurred") | |
241 | parse_json.side_effect = val_err | |
242 | p = Parser() | |
243 | p.parse({"foo": fields.Field()}, web_request, locations=("json",)) | |
244 | handle_error.assert_called() | |
245 | parse_json.side_effect = ValidationError("another exception") | |
246 | p.parse({"foo": fields.Field()}, web_request, locations=("json",)) | |
243 | assert handle_error.call_count == 0 | |
244 | p.parse({"foo": fields.Field()}, web_request, validate=always_fail) | |
245 | assert handle_error.call_count == 1 | |
246 | p.parse({"foo": fields.Field()}, web_request, validate=always_fail) | |
247 | 247 | assert handle_error.call_count == 2 |
248 | 248 | |
249 | 249 | |
250 | 250 | def test_handle_error_reraises_errors(web_request): |
251 | 251 | p = Parser() |
252 | 252 | with pytest.raises(ValidationError): |
253 | p.handle_error(ValidationError("error raised"), web_request, Schema()) | |
254 | ||
255 | ||
256 | @mock.patch("webargs.core.Parser.parse_headers") | |
257 | def test_locations_as_init_arguments(parse_headers, web_request): | |
258 | p = Parser(locations=("headers",)) | |
253 | p.handle_error( | |
254 | ValidationError("error raised"), | |
255 | web_request, | |
256 | Schema(), | |
257 | error_status_code=422, | |
258 | error_headers={}, | |
259 | ) | |
260 | ||
261 | ||
262 | @mock.patch("webargs.core.Parser.load_headers") | |
263 | def test_location_as_init_argument(load_headers, web_request): | |
264 | p = Parser(location="headers") | |
265 | load_headers.return_value = {} | |
259 | 266 | p.parse({"foo": fields.Field()}, web_request) |
260 | assert parse_headers.called | |
261 | ||
262 | ||
263 | @mock.patch("webargs.core.Parser.parse_files") | |
264 | def test_parse_files(parse_files, web_request): | |
265 | p = Parser() | |
266 | p.parse({"foo": fields.Field()}, web_request, locations=("files",)) | |
267 | assert parse_files.called | |
268 | ||
269 | ||
270 | @mock.patch("webargs.core.Parser.parse_json") | |
271 | def test_custom_error_handler(parse_json, web_request): | |
267 | assert load_headers.called | |
268 | ||
269 | ||
270 | def test_custom_error_handler(web_request): | |
272 | 271 | class CustomError(Exception): |
273 | 272 | pass |
274 | 273 | |
275 | def error_handler(error, req, schema, status_code, headers): | |
274 | def error_handler(error, req, schema, *, error_status_code, error_headers): | |
276 | 275 | assert isinstance(schema, Schema) |
277 | 276 | raise CustomError(error) |
278 | 277 | |
279 | parse_json.side_effect = ValidationError("parse_json failed") | |
278 | def failing_validate_func(args): | |
279 | raise ValidationError("parsing failed") | |
280 | ||
281 | class MySchema(Schema): | |
282 | foo = fields.Int() | |
283 | ||
284 | myschema = MySchema(**strict_kwargs) | |
285 | web_request.json = {"foo": "hello world"} | |
286 | ||
280 | 287 | p = Parser(error_handler=error_handler) |
281 | 288 | with pytest.raises(CustomError): |
282 | p.parse({"foo": fields.Field()}, web_request) | |
283 | ||
284 | ||
285 | @mock.patch("webargs.core.Parser.parse_json") | |
286 | def test_custom_error_handler_decorator(parse_json, web_request): | |
289 | p.parse(myschema, web_request, validate=failing_validate_func) | |
290 | ||
291 | ||
292 | def test_custom_error_handler_decorator(web_request): | |
287 | 293 | class CustomError(Exception): |
288 | 294 | pass |
289 | 295 | |
290 | parse_json.side_effect = ValidationError("parse_json failed") | |
291 | ||
296 | mock_schema = mock.Mock(spec=Schema) | |
297 | mock_schema.strict = True | |
298 | mock_schema.load.side_effect = ValidationError("parsing json failed") | |
292 | 299 | parser = Parser() |
293 | 300 | |
294 | 301 | @parser.error_handler |
295 | def handle_error(error, req, schema, status_code, headers): | |
302 | def handle_error(error, req, schema, *, error_status_code, error_headers): | |
296 | 303 | assert isinstance(schema, Schema) |
297 | 304 | raise CustomError(error) |
298 | 305 | |
299 | 306 | with pytest.raises(CustomError): |
300 | parser.parse({"foo": fields.Field()}, web_request) | |
301 | ||
302 | ||
303 | def test_custom_location_handler(web_request): | |
307 | parser.parse(mock_schema, web_request) | |
308 | ||
309 | ||
310 | def test_custom_location_loader(web_request): | |
304 | 311 | web_request.data = {"foo": 42} |
305 | 312 | |
306 | 313 | parser = Parser() |
307 | 314 | |
308 | @parser.location_handler("data") | |
309 | def parse_data(req, name, arg): | |
310 | return req.data.get(name, missing) | |
311 | ||
312 | result = parser.parse({"foo": fields.Int()}, web_request, locations=("data",)) | |
315 | @parser.location_loader("data") | |
316 | def load_data(req, schema): | |
317 | return req.data | |
318 | ||
319 | result = parser.parse({"foo": fields.Int()}, web_request, location="data") | |
313 | 320 | assert result["foo"] == 42 |
314 | 321 | |
315 | 322 | |
316 | def test_custom_location_handler_with_data_key(web_request): | |
323 | def test_custom_location_loader_with_data_key(web_request): | |
317 | 324 | web_request.data = {"X-Foo": 42} |
318 | 325 | parser = Parser() |
319 | 326 | |
320 | @parser.location_handler("data") | |
321 | def parse_data(req, name, arg): | |
322 | return req.data.get(name, missing) | |
327 | @parser.location_loader("data") | |
328 | def load_data(req, schema): | |
329 | return req.data | |
323 | 330 | |
324 | 331 | data_key_kwarg = { |
325 | 332 | "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "X-Foo" |
326 | 333 | } |
327 | 334 | result = parser.parse( |
328 | {"x_foo": fields.Int(**data_key_kwarg)}, web_request, locations=("data",) | |
335 | {"x_foo": fields.Int(**data_key_kwarg)}, web_request, location="data" | |
329 | 336 | ) |
330 | 337 | assert result["x_foo"] == 42 |
331 | 338 | |
332 | 339 | |
333 | def test_full_input_validation(web_request): | |
340 | def test_full_input_validation(parser, web_request): | |
334 | 341 | |
335 | 342 | web_request.json = {"foo": 41, "bar": 42} |
336 | 343 | |
337 | parser = MockRequestParser() | |
338 | 344 | args = {"foo": fields.Int(), "bar": fields.Int()} |
339 | 345 | with pytest.raises(ValidationError): |
340 | 346 | # Test that `validate` receives dictionary of args |
341 | parser.parse( | |
342 | args, | |
343 | web_request, | |
344 | locations=("json",), | |
345 | validate=lambda args: args["foo"] > args["bar"], | |
346 | ) | |
347 | parser.parse(args, web_request, validate=lambda args: args["foo"] > args["bar"]) | |
347 | 348 | |
348 | 349 | |
349 | 350 | def test_full_input_validation_with_multiple_validators(web_request, parser): |
359 | 360 | web_request.json = {"a": 2, "b": 1} |
360 | 361 | validators = [validate1, validate2] |
361 | 362 | with pytest.raises(ValidationError, match="b must be > a"): |
362 | parser.parse(args, web_request, locations=("json",), validate=validators) | |
363 | parser.parse(args, web_request, validate=validators) | |
363 | 364 | |
364 | 365 | web_request.json = {"a": 1, "b": 2} |
365 | 366 | with pytest.raises(ValidationError, match="a must be > b"): |
366 | parser.parse(args, web_request, locations=("json",), validate=validators) | |
367 | ||
368 | ||
369 | def test_required_with_custom_error(web_request): | |
370 | web_request.json = {} | |
371 | parser = MockRequestParser() | |
367 | parser.parse(args, web_request, validate=validators) | |
368 | ||
369 | ||
370 | def test_required_with_custom_error(parser, web_request): | |
371 | web_request.json = {} | |
372 | 372 | args = { |
373 | 373 | "foo": fields.Str(required=True, error_messages={"required": "We need foo"}) |
374 | 374 | } |
375 | 375 | with pytest.raises(ValidationError) as excinfo: |
376 | 376 | # Test that `validate` receives dictionary of args |
377 | parser.parse(args, web_request, locations=("json",)) | |
378 | ||
379 | assert "We need foo" in excinfo.value.messages["foo"] | |
377 | parser.parse(args, web_request) | |
378 | ||
379 | assert "We need foo" in excinfo.value.messages["json"]["foo"] | |
380 | 380 | if MARSHMALLOW_VERSION_INFO[0] < 3: |
381 | 381 | assert "foo" in excinfo.value.field_names |
382 | 382 | |
383 | 383 | |
384 | def test_required_with_custom_error_and_validation_error(web_request): | |
384 | def test_required_with_custom_error_and_validation_error(parser, web_request): | |
385 | 385 | web_request.json = {"foo": ""} |
386 | parser = MockRequestParser() | |
387 | 386 | args = { |
388 | 387 | "foo": fields.Str( |
389 | 388 | required="We need foo", |
393 | 392 | } |
394 | 393 | with pytest.raises(ValidationError) as excinfo: |
395 | 394 | # Test that `validate` receives dictionary of args |
396 | parser.parse(args, web_request, locations=("json",)) | |
395 | parser.parse(args, web_request) | |
397 | 396 | |
398 | 397 | assert "foo required length is 3" in excinfo.value.args[0]["foo"] |
399 | 398 | if MARSHMALLOW_VERSION_INFO[0] < 3: |
404 | 403 | def validate(val): |
405 | 404 | return False |
406 | 405 | |
407 | text = u"øœ∑∆∑" | |
406 | text = "øœ∑∆∑" | |
408 | 407 | web_request.json = {"text": text} |
409 | 408 | parser = MockRequestParser() |
410 | 409 | args = {"text": fields.Str()} |
411 | 410 | with pytest.raises(ValidationError) as excinfo: |
412 | parser.parse(args, web_request, locations=("json",), validate=validate) | |
413 | assert excinfo.value.messages == ["Invalid value."] | |
411 | parser.parse(args, web_request, validate=validate) | |
412 | assert excinfo.value.messages == {"json": ["Invalid value."]} | |
414 | 413 | |
415 | 414 | |
416 | 415 | def test_invalid_argument_for_validate(web_request, parser): |
417 | 416 | with pytest.raises(ValueError) as excinfo: |
418 | 417 | parser.parse({}, web_request, validate="notcallable") |
419 | 418 | assert "not a callable or list of callables." in excinfo.value.args[0] |
420 | ||
421 | ||
422 | def test_get_value_basic(): | |
423 | assert get_value({"foo": 42}, "foo", False) == 42 | |
424 | assert get_value({"foo": 42}, "bar", False) is missing | |
425 | assert get_value({"foos": ["a", "b"]}, "foos", True) == ["a", "b"] | |
426 | # https://github.com/marshmallow-code/webargs/pull/30 | |
427 | assert get_value({"foos": ["a", "b"]}, "bar", True) is missing | |
428 | 419 | |
429 | 420 | |
430 | 421 | def create_bottle_multi_dict(): |
442 | 433 | |
443 | 434 | |
444 | 435 | @pytest.mark.parametrize("input_dict", multidicts) |
445 | def test_get_value_multidict(input_dict): | |
446 | field = fields.List(fields.Str()) | |
447 | assert get_value(input_dict, "foos", field) == ["a", "b"] | |
436 | def test_multidict_proxy(input_dict): | |
437 | class ListSchema(Schema): | |
438 | foos = fields.List(fields.Str()) | |
439 | ||
440 | class StrSchema(Schema): | |
441 | foos = fields.Str() | |
442 | ||
443 | # this MultiDictProxy is aware that "foos" is a list field and will | |
444 | # therefore produce a list with __getitem__ | |
445 | list_wrapped_multidict = MultiDictProxy(input_dict, ListSchema()) | |
446 | ||
447 | # this MultiDictProxy is under the impression that "foos" is just a string | |
448 | # and it should return "a" or "b" | |
449 | # the decision between "a" and "b" in this case belongs to the framework | |
450 | str_wrapped_multidict = MultiDictProxy(input_dict, StrSchema()) | |
451 | ||
452 | assert list_wrapped_multidict["foos"] == ["a", "b"] | |
453 | assert str_wrapped_multidict["foos"] in ("a", "b") | |
448 | 454 | |
449 | 455 | |
450 | 456 | def test_parse_with_data_key(web_request): |
455 | 461 | "load_from" if (MARSHMALLOW_VERSION_INFO[0] < 3) else "data_key": "Content-Type" |
456 | 462 | } |
457 | 463 | args = {"content_type": fields.Field(**data_key_kwargs)} |
458 | parsed = parser.parse(args, web_request, locations=("json",)) | |
464 | parsed = parser.parse(args, web_request) | |
459 | 465 | assert parsed == {"content_type": "application/json"} |
460 | 466 | |
461 | 467 | |
469 | 475 | |
470 | 476 | parser = MockRequestParser() |
471 | 477 | args = {"content_type": fields.Field(load_from="Content-Type")} |
472 | parsed = parser.parse(args, web_request, locations=("json",)) | |
478 | parsed = parser.parse(args, web_request) | |
473 | 479 | assert parsed == {"content_type": "application/json"} |
474 | 480 | |
475 | 481 | |
482 | 488 | } |
483 | 489 | args = {"content_type": fields.Str(**data_key_kwargs)} |
484 | 490 | with pytest.raises(ValidationError) as excinfo: |
485 | parser.parse(args, web_request, locations=("json",)) | |
486 | assert "Content-Type" in excinfo.value.messages | |
487 | assert excinfo.value.messages["Content-Type"] == ["Not a valid string."] | |
491 | parser.parse(args, web_request) | |
492 | assert "json" in excinfo.value.messages | |
493 | assert "Content-Type" in excinfo.value.messages["json"] | |
494 | assert excinfo.value.messages["json"]["Content-Type"] == ["Not a valid string."] | |
488 | 495 | |
489 | 496 | |
490 | 497 | def test_parse_nested_with_data_key(web_request): |
495 | 502 | } |
496 | 503 | args = {"nested_arg": fields.Nested({"right": fields.Field(**data_key_kwarg)})} |
497 | 504 | |
498 | parsed = parser.parse(args, web_request, locations=("json",)) | |
505 | parsed = parser.parse(args, web_request) | |
499 | 506 | assert parsed == {"nested_arg": {"right": "OK"}} |
500 | 507 | |
501 | 508 | |
512 | 519 | ) |
513 | 520 | } |
514 | 521 | |
515 | parsed = parser.parse(args, web_request, locations=("json",)) | |
522 | parsed = parser.parse(args, web_request) | |
516 | 523 | assert parsed == {"nested_arg": {"found": None}} |
517 | 524 | |
518 | 525 | |
522 | 529 | web_request.json = {"nested_arg": {}} |
523 | 530 | args = {"nested_arg": fields.Nested({"miss": fields.Field(missing="<foo>")})} |
524 | 531 | |
525 | parsed = parser.parse(args, web_request, locations=("json",)) | |
532 | parsed = parser.parse(args, web_request) | |
526 | 533 | assert parsed == {"nested_arg": {"miss": "<foo>"}} |
527 | 534 | |
528 | 535 | |
553 | 560 | web_request.json = {"username": "foo"} |
554 | 561 | web_request.query = {"page": 42} |
555 | 562 | |
556 | @parser.use_args(query_args, web_request, locations=("query",)) | |
557 | @parser.use_args(json_args, web_request, locations=("json",)) | |
563 | @parser.use_args(query_args, web_request, location="query") | |
564 | @parser.use_args(json_args, web_request) | |
558 | 565 | def viewfunc(query_parsed, json_parsed): |
559 | 566 | return {"json": json_parsed, "query": query_parsed} |
560 | 567 | |
569 | 576 | web_request.json = {"username": "foo"} |
570 | 577 | web_request.query = {"page": 42} |
571 | 578 | |
572 | @parser.use_kwargs(query_args, web_request, locations=("query",)) | |
573 | @parser.use_kwargs(json_args, web_request, locations=("json",)) | |
579 | @parser.use_kwargs(query_args, web_request, location="query") | |
580 | @parser.use_kwargs(json_args, web_request) | |
574 | 581 | def viewfunc(page, username): |
575 | 582 | return {"json": {"username": username}, "query": {"page": page}} |
576 | 583 | |
591 | 598 | |
592 | 599 | def test_list_allowed_missing(web_request, parser): |
593 | 600 | args = {"name": fields.List(fields.Str())} |
594 | web_request.json = {"fakedata": True} | |
601 | web_request.json = {} | |
595 | 602 | result = parser.parse(args, web_request) |
596 | 603 | assert result == {} |
597 | 604 | |
598 | 605 | |
599 | 606 | def test_int_list_allowed_missing(web_request, parser): |
600 | 607 | args = {"name": fields.List(fields.Int())} |
601 | web_request.json = {"fakedata": True} | |
608 | web_request.json = {} | |
602 | 609 | result = parser.parse(args, web_request) |
603 | 610 | assert result == {} |
604 | 611 | |
605 | 612 | |
606 | 613 | def test_multiple_arg_required_with_int_conversion(web_request, parser): |
607 | 614 | args = {"ids": fields.List(fields.Int(), required=True)} |
608 | web_request.json = {"fakedata": True} | |
615 | web_request.json = {} | |
609 | 616 | with pytest.raises(ValidationError) as excinfo: |
610 | 617 | parser.parse(args, web_request) |
611 | assert excinfo.value.messages == {"ids": ["Missing data for required field."]} | |
618 | assert excinfo.value.messages == { | |
619 | "json": {"ids": ["Missing data for required field."]} | |
620 | } | |
612 | 621 | |
613 | 622 | |
614 | 623 | def test_parse_with_callable(web_request, parser): |
746 | 755 | assert "strict=True" in str(warning.message) |
747 | 756 | |
748 | 757 | def test_use_kwargs_stacked(self, web_request, parser): |
758 | if MARSHMALLOW_VERSION_INFO[0] >= 3: | |
759 | from marshmallow import EXCLUDE | |
760 | ||
761 | class PageSchema(Schema): | |
762 | page = fields.Int() | |
763 | ||
764 | pageschema = PageSchema(unknown=EXCLUDE) | |
765 | userschema = self.UserSchema(unknown=EXCLUDE) | |
766 | else: | |
767 | pageschema = {"page": fields.Int()} | |
768 | userschema = self.UserSchema(**strict_kwargs) | |
769 | ||
749 | 770 | web_request.json = {"email": "[email protected]", "password": "bar", "page": 42} |
750 | 771 | |
751 | @parser.use_kwargs({"page": fields.Int()}, web_request) | |
752 | @parser.use_kwargs(self.UserSchema(**strict_kwargs), web_request) | |
772 | @parser.use_kwargs(pageschema, web_request) | |
773 | @parser.use_kwargs(userschema, web_request) | |
753 | 774 | def viewfunc(email, password, page): |
754 | 775 | return {"email": email, "password": password, "page": page} |
755 | 776 | |
773 | 794 | return True |
774 | 795 | |
775 | 796 | web_request.json = {"name": "Eric Cartman"} |
776 | res = parser.parse(UserSchema, web_request, locations=("json",)) | |
797 | res = parser.parse(UserSchema, web_request) | |
777 | 798 | assert res == {"name": "Eric Cartman"} |
778 | 799 | |
779 | 800 | |
780 | def test_use_args_with_custom_locations_in_parser(web_request, parser): | |
801 | def test_use_args_with_custom_location_in_parser(web_request, parser): | |
781 | 802 | custom_args = {"foo": fields.Str()} |
782 | 803 | web_request.json = {} |
783 | parser.locations = ("custom",) | |
784 | ||
785 | @parser.location_handler("custom") | |
786 | def parse_custom(req, name, arg): | |
787 | return "bar" | |
804 | parser.location = "custom" | |
805 | ||
806 | @parser.location_loader("custom") | |
807 | def load_custom(schema, req): | |
808 | return {"foo": "bar"} | |
788 | 809 | |
789 | 810 | @parser.use_args(custom_args, web_request) |
790 | 811 | def viewfunc(args): |
826 | 847 | |
827 | 848 | dumped = schema.dump(parsed) |
828 | 849 | data = dumped.data if MARSHMALLOW_VERSION_INFO[0] < 3 else dumped |
829 | assert data["ids"] == [1, 2, 3] | |
830 | ||
831 | ||
832 | def test_delimited_list_as_string(web_request, parser): | |
833 | web_request.json = {"ids": "1,2,3"} | |
850 | assert data["ids"] == "1,2,3" | |
851 | ||
852 | ||
853 | @pytest.mark.skipif( | |
854 | MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3" | |
855 | ) | |
856 | def test_delimited_tuple_default_delimiter(web_request, parser): | |
857 | """ | |
858 | Test load and dump from DelimitedTuple, including the use of a datetime | |
859 | type (similar to a DelimitedList test below) which confirms that we aren't | |
860 | relying on __str__, but are properly de/serializing the included fields | |
861 | """ | |
862 | web_request.json = {"ids": "1,2,2020-05-04"} | |
834 | 863 | schema_cls = dict2schema( |
835 | {"ids": fields.DelimitedList(fields.Int(), as_string=True)} | |
864 | { | |
865 | "ids": fields.DelimitedTuple( | |
866 | (fields.Int, fields.Int, fields.DateTime(format="%Y-%m-%d")) | |
867 | ) | |
868 | } | |
836 | 869 | ) |
837 | 870 | schema = schema_cls() |
838 | 871 | |
839 | 872 | parsed = parser.parse(schema, web_request) |
840 | assert parsed["ids"] == [1, 2, 3] | |
841 | ||
842 | dumped = schema.dump(parsed) | |
843 | data = dumped.data if MARSHMALLOW_VERSION_INFO[0] < 3 else dumped | |
844 | assert data["ids"] == "1,2,3" | |
845 | ||
846 | ||
847 | def test_delimited_list_as_string_v2(web_request, parser): | |
873 | assert parsed["ids"] == (1, 2, datetime.datetime(2020, 5, 4)) | |
874 | ||
875 | data = schema.dump(parsed) | |
876 | assert data["ids"] == "1,2,2020-05-04" | |
877 | ||
878 | ||
879 | @pytest.mark.skipif( | |
880 | MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3" | |
881 | ) | |
882 | def test_delimited_tuple_incorrect_arity(web_request, parser): | |
883 | web_request.json = {"ids": "1,2"} | |
884 | schema_cls = dict2schema( | |
885 | {"ids": fields.DelimitedTuple((fields.Int, fields.Int, fields.Int))} | |
886 | ) | |
887 | schema = schema_cls() | |
888 | ||
889 | with pytest.raises(ValidationError): | |
890 | parser.parse(schema, web_request) | |
891 | ||
892 | ||
893 | def test_delimited_list_with_datetime(web_request, parser): | |
894 | """ | |
895 | Test that DelimitedList(DateTime(format=...)) correctly parses and dumps | |
896 | dates to and from strings -- indicates that we're doing proper | |
897 | serialization of values in dump() and not just relying on __str__ producing | |
898 | correct results | |
899 | """ | |
848 | 900 | web_request.json = {"dates": "2018-11-01,2018-11-02"} |
849 | 901 | schema_cls = dict2schema( |
850 | { | |
851 | "dates": fields.DelimitedList( | |
852 | fields.DateTime(format="%Y-%m-%d"), as_string=True | |
853 | ) | |
854 | } | |
902 | {"dates": fields.DelimitedList(fields.DateTime(format="%Y-%m-%d"))} | |
855 | 903 | ) |
856 | 904 | schema = schema_cls() |
857 | 905 | |
874 | 922 | parsed = parser.parse(schema, web_request) |
875 | 923 | assert parsed["ids"] == [1, 2, 3] |
876 | 924 | |
877 | ||
878 | def test_delimited_list_load_list(web_request, parser): | |
925 | dumped = schema.dump(parsed) | |
926 | data = dumped.data if MARSHMALLOW_VERSION_INFO[0] < 3 else dumped | |
927 | assert data["ids"] == "1|2|3" | |
928 | ||
929 | ||
930 | @pytest.mark.skipif( | |
931 | MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3" | |
932 | ) | |
933 | def test_delimited_tuple_custom_delimiter(web_request, parser): | |
934 | web_request.json = {"ids": "1|2"} | |
935 | schema_cls = dict2schema( | |
936 | {"ids": fields.DelimitedTuple((fields.Int, fields.Int), delimiter="|")} | |
937 | ) | |
938 | schema = schema_cls() | |
939 | ||
940 | parsed = parser.parse(schema, web_request) | |
941 | assert parsed["ids"] == (1, 2) | |
942 | ||
943 | data = schema.dump(parsed) | |
944 | assert data["ids"] == "1|2" | |
945 | ||
946 | ||
947 | def test_delimited_list_load_list_errors(web_request, parser): | |
879 | 948 | web_request.json = {"ids": [1, 2, 3]} |
880 | 949 | schema_cls = dict2schema({"ids": fields.DelimitedList(fields.Int())}) |
881 | 950 | schema = schema_cls() |
882 | 951 | |
883 | parsed = parser.parse(schema, web_request) | |
884 | assert parsed["ids"] == [1, 2, 3] | |
952 | with pytest.raises(ValidationError) as excinfo: | |
953 | parser.parse(schema, web_request) | |
954 | exc = excinfo.value | |
955 | assert isinstance(exc, ValidationError) | |
956 | errors = exc.args[0] | |
957 | assert errors["ids"] == ["Not a valid delimited list."] | |
958 | ||
959 | ||
960 | @pytest.mark.skipif( | |
961 | MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3" | |
962 | ) | |
963 | def test_delimited_tuple_load_list_errors(web_request, parser): | |
964 | web_request.json = {"ids": [1, 2]} | |
965 | schema_cls = dict2schema({"ids": fields.DelimitedTuple((fields.Int, fields.Int))}) | |
966 | schema = schema_cls() | |
967 | ||
968 | with pytest.raises(ValidationError) as excinfo: | |
969 | parser.parse(schema, web_request) | |
970 | exc = excinfo.value | |
971 | assert isinstance(exc, ValidationError) | |
972 | errors = exc.args[0] | |
973 | assert errors["ids"] == ["Not a valid delimited tuple."] | |
885 | 974 | |
886 | 975 | |
887 | 976 | # Regresion test for https://github.com/marshmallow-code/webargs/issues/149 |
892 | 981 | |
893 | 982 | with pytest.raises(ValidationError) as excinfo: |
894 | 983 | parser.parse(schema, web_request) |
895 | assert excinfo.value.messages == {"ids": ["Not a valid list."]} | |
984 | assert excinfo.value.messages == {"json": {"ids": ["Not a valid delimited list."]}} | |
985 | ||
986 | ||
987 | @pytest.mark.skipif( | |
988 | MARSHMALLOW_VERSION_INFO[0] < 3, reason="fields.Tuple added in marshmallow3" | |
989 | ) | |
990 | def test_delimited_tuple_passed_invalid_type(web_request, parser): | |
991 | web_request.json = {"ids": 1} | |
992 | schema_cls = dict2schema({"ids": fields.DelimitedTuple((fields.Int,))}) | |
993 | schema = schema_cls() | |
994 | ||
995 | with pytest.raises(ValidationError) as excinfo: | |
996 | parser.parse(schema, web_request) | |
997 | assert excinfo.value.messages == {"json": {"ids": ["Not a valid delimited tuple."]}} | |
896 | 998 | |
897 | 999 | |
898 | 1000 | def test_missing_list_argument_not_in_parsed_result(web_request, parser): |
910 | 1012 | msg = "Missing data for required field." |
911 | 1013 | with pytest.raises(ValidationError, match=msg): |
912 | 1014 | parser.parse(args, web_request) |
913 | ||
914 | ||
915 | def test_arg_location_param(web_request, parser): | |
916 | web_request.json = {"foo": 24} | |
917 | web_request.cookies = {"foo": 42} | |
918 | args = {"foo": fields.Field(location="cookies")} | |
919 | ||
920 | parsed = parser.parse(args, web_request) | |
921 | ||
922 | assert parsed["foo"] == 42 | |
923 | 1015 | |
924 | 1016 | |
925 | 1017 | def test_validation_errors_in_validator_are_passed_to_handle_error(parser, web_request): |
1019 | 1111 | |
1020 | 1112 | |
1021 | 1113 | class MockRequestParserWithErrorHandler(MockRequestParser): |
1022 | def handle_error( | |
1023 | self, error, req, schema, error_status_code=None, error_headers=None | |
1024 | ): | |
1114 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): | |
1025 | 1115 | assert isinstance(error, ValidationError) |
1026 | 1116 | assert isinstance(schema, Schema) |
1027 | 1117 | raise MockHTTPError(error_status_code, error_headers) |
1040 | 1130 | assert error.headers == {"X-Foo": "bar"} |
1041 | 1131 | |
1042 | 1132 | |
1043 | @mock.patch("webargs.core.Parser.parse_json") | |
1044 | def test_custom_schema_class(parse_json, web_request): | |
1133 | @mock.patch("webargs.core.Parser.load_json") | |
1134 | def test_custom_schema_class(load_json, web_request): | |
1045 | 1135 | class CustomSchema(Schema): |
1046 | 1136 | @pre_load |
1047 | 1137 | def pre_load(self, data, **kwargs): |
1048 | 1138 | data["value"] += " world" |
1049 | 1139 | return data |
1050 | 1140 | |
1051 | parse_json.return_value = "hello" | |
1141 | load_json.return_value = {"value": "hello"} | |
1052 | 1142 | argmap = {"value": fields.Str()} |
1053 | 1143 | p = Parser(schema_class=CustomSchema) |
1054 | 1144 | ret = p.parse(argmap, web_request) |
1055 | 1145 | assert ret == {"value": "hello world"} |
1056 | 1146 | |
1057 | 1147 | |
1058 | @mock.patch("webargs.core.Parser.parse_json") | |
1059 | def test_custom_default_schema_class(parse_json, web_request): | |
1148 | @mock.patch("webargs.core.Parser.load_json") | |
1149 | def test_custom_default_schema_class(load_json, web_request): | |
1060 | 1150 | class CustomSchema(Schema): |
1061 | 1151 | @pre_load |
1062 | 1152 | def pre_load(self, data, **kwargs): |
1066 | 1156 | class CustomParser(Parser): |
1067 | 1157 | DEFAULT_SCHEMA_CLASS = CustomSchema |
1068 | 1158 | |
1069 | parse_json.return_value = "hello" | |
1159 | load_json.return_value = {"value": "hello"} | |
1070 | 1160 | argmap = {"value": fields.Str()} |
1071 | 1161 | p = CustomParser() |
1072 | 1162 | ret = p.parse(argmap, web_request) |
0 | # -*- coding: utf-8 -*- | |
1 | from __future__ import unicode_literals | |
2 | ||
3 | 0 | import pytest |
4 | 1 | from tests.apps.django_app.base.wsgi import application |
5 | 2 | |
22 | 19 | |
23 | 20 | def test_parsing_in_class_based_view(self, testapp): |
24 | 21 | assert testapp.get("/echo_cbv?name=Fred").json == {"name": "Fred"} |
25 | assert testapp.post("/echo_cbv", {"name": "Fred"}).json == {"name": "Fred"} | |
22 | assert testapp.post_json("/echo_cbv", {"name": "Fred"}).json == {"name": "Fred"} | |
26 | 23 | |
27 | 24 | def test_use_args_in_class_based_view(self, testapp): |
28 | 25 | res = testapp.get("/echo_use_args_cbv?name=Fred") |
29 | 26 | assert res.json == {"name": "Fred"} |
30 | res = testapp.post("/echo_use_args_cbv", {"name": "Fred"}) | |
27 | res = testapp.post_json("/echo_use_args_cbv", {"name": "Fred"}) | |
31 | 28 | assert res.json == {"name": "Fred"} |
32 | 29 | |
33 | 30 | def test_use_args_in_class_based_view_with_path_param(self, testapp): |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | import pytest |
1 | import falcon.testing | |
2 | 2 | |
3 | 3 | from webargs.testing import CommonTestCase |
4 | 4 | from tests.apps.falcon_app import create_app |
18 | 18 | # https://github.com/marshmallow-code/webargs/issues/427 |
19 | 19 | def test_parse_json_with_nonutf8_chars(self, testapp): |
20 | 20 | res = testapp.post( |
21 | "/echo", | |
21 | "/echo_json", | |
22 | 22 | b"\xfe", |
23 | 23 | headers={"Accept": "application/json", "Content-Type": "application/json"}, |
24 | 24 | expect_errors=True, |
30 | 30 | # https://github.com/sloria/webargs/issues/329 |
31 | 31 | def test_invalid_json(self, testapp): |
32 | 32 | res = testapp.post( |
33 | "/echo", | |
33 | "/echo_json", | |
34 | 34 | '{"foo": "bar", }', |
35 | 35 | headers={"Accept": "application/json", "Content-Type": "application/json"}, |
36 | 36 | expect_errors=True, |
37 | 37 | ) |
38 | 38 | assert res.status_code == 400 |
39 | 39 | assert res.json["errors"] == {"json": ["Invalid JSON body."]} |
40 | ||
41 | # Falcon converts headers to all-caps | |
42 | def test_parsing_headers(self, testapp): | |
43 | res = testapp.get("/echo_headers", headers={"name": "Fred"}) | |
44 | assert res.json == {"NAME": "Fred"} | |
45 | ||
46 | # `falcon.testing.TestClient.simulate_request` parses request with `wsgiref` | |
47 | def test_body_parsing_works_with_simulate(self): | |
48 | app = self.create_app() | |
49 | client = falcon.testing.TestClient(app) | |
50 | res = client.simulate_post( | |
51 | "/echo_json", | |
52 | json={"name": "Fred"}, | |
53 | ) | |
54 | assert res.json == {"name": "Fred"} |
0 | # -*- coding: utf-8 -*- | |
1 | from __future__ import unicode_literals | |
2 | import threading | |
3 | ||
4 | 0 | from werkzeug.exceptions import HTTPException |
5 | import mock | |
6 | 1 | import pytest |
7 | 2 | |
8 | 3 | from flask import Flask |
9 | from webargs import fields, ValidationError, missing | |
4 | from webargs import fields, ValidationError, missing, dict2schema | |
10 | 5 | from webargs.flaskparser import parser, abort |
11 | 6 | from webargs.core import MARSHMALLOW_VERSION_INFO, json |
12 | 7 | |
13 | 8 | from .apps.flask_app import app |
14 | 9 | from webargs.testing import CommonTestCase |
10 | ||
11 | try: | |
12 | # Python 3.5 | |
13 | import mock | |
14 | except ImportError: | |
15 | # Python 3.6+ | |
16 | from unittest import mock | |
15 | 17 | |
16 | 18 | |
17 | 19 | class TestFlaskParser(CommonTestCase): |
25 | 27 | def test_parsing_invalid_view_arg(self, testapp): |
26 | 28 | res = testapp.get("/echo_view_arg/foo", expect_errors=True) |
27 | 29 | assert res.status_code == 422 |
28 | assert res.json == {"view_arg": ["Not a valid integer."]} | |
30 | assert res.json == {"view_args": {"view_arg": ["Not a valid integer."]}} | |
29 | 31 | |
30 | 32 | def test_use_args_with_view_args_parsing(self, testapp): |
31 | 33 | res = testapp.get("/echo_view_arg_use_args/42") |
32 | 34 | assert res.json == {"view_arg": 42} |
33 | 35 | |
34 | 36 | def test_use_args_on_a_method_view(self, testapp): |
35 | res = testapp.post("/echo_method_view_use_args", {"val": 42}) | |
37 | res = testapp.post_json("/echo_method_view_use_args", {"val": 42}) | |
36 | 38 | assert res.json == {"val": 42} |
37 | 39 | |
38 | 40 | def test_use_kwargs_on_a_method_view(self, testapp): |
39 | res = testapp.post("/echo_method_view_use_kwargs", {"val": 42}) | |
41 | res = testapp.post_json("/echo_method_view_use_kwargs", {"val": 42}) | |
40 | 42 | assert res.json == {"val": 42} |
41 | 43 | |
42 | 44 | def test_use_kwargs_with_missing_data(self, testapp): |
43 | res = testapp.post("/echo_use_kwargs_missing", {"username": "foo"}) | |
45 | res = testapp.post_json("/echo_use_kwargs_missing", {"username": "foo"}) | |
44 | 46 | assert res.json == {"username": "foo"} |
45 | 47 | |
46 | 48 | # regression test for https://github.com/marshmallow-code/webargs/issues/145 |
47 | 49 | def test_nested_many_with_data_key(self, testapp): |
48 | res = testapp.post_json("/echo_nested_many_data_key", {"x_field": [{"id": 42}]}) | |
49 | # https://github.com/marshmallow-code/marshmallow/pull/714 | |
50 | post_with_raw_fieldname_args = ( | |
51 | "/echo_nested_many_data_key", | |
52 | {"x_field": [{"id": 42}]}, | |
53 | ) | |
54 | # under marshmallow 2 this is allowed and works | |
50 | 55 | if MARSHMALLOW_VERSION_INFO[0] < 3: |
56 | res = testapp.post_json(*post_with_raw_fieldname_args) | |
51 | 57 | assert res.json == {"x_field": [{"id": 42}]} |
58 | # but under marshmallow3 , only data_key is checked, field name is ignored | |
59 | else: | |
60 | res = testapp.post_json(*post_with_raw_fieldname_args, expect_errors=True) | |
61 | assert res.status_code == 422 | |
52 | 62 | |
53 | 63 | res = testapp.post_json("/echo_nested_many_data_key", {"X-Field": [{"id": 24}]}) |
54 | 64 | assert res.json == {"x_field": [{"id": 24}]} |
55 | 65 | |
56 | 66 | res = testapp.post_json("/echo_nested_many_data_key", {}) |
57 | 67 | assert res.json == {} |
68 | ||
69 | # regression test for | |
70 | # https://github.com/marshmallow-code/webargs/issues/500 | |
71 | def test_parsing_unexpected_headers_when_raising(self, testapp): | |
72 | res = testapp.get( | |
73 | "/echo_headers_raising", expect_errors=True, headers={"X-Unexpected": "foo"} | |
74 | ) | |
75 | # under marshmallow 2 this is allowed and works | |
76 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
77 | assert res.json == {"name": "World"} | |
78 | # but on ma3 it's supposed to be a validation error | |
79 | else: | |
80 | assert res.status_code == 422 | |
81 | assert "headers" in res.json | |
82 | assert "X-Unexpected" in set(res.json["headers"].keys()) | |
58 | 83 | |
59 | 84 | |
60 | 85 | @mock.patch("webargs.flaskparser.abort") |
76 | 101 | abort_args, abort_kwargs = mock_abort.call_args |
77 | 102 | assert abort_args[0] == 422 |
78 | 103 | expected_msg = "Invalid value." |
79 | assert abort_kwargs["messages"]["value"] == [expected_msg] | |
104 | assert abort_kwargs["messages"]["json"]["value"] == [expected_msg] | |
80 | 105 | assert type(abort_kwargs["exc"]) == ValidationError |
81 | 106 | |
82 | 107 | |
83 | def test_parse_form_returns_missing_if_no_form(): | |
108 | @pytest.mark.parametrize("mimetype", [None, "application/json"]) | |
109 | def test_load_json_returns_missing_if_no_data(mimetype): | |
84 | 110 | req = mock.Mock() |
85 | req.form.get.side_effect = AttributeError("no form") | |
86 | assert parser.parse_form(req, "foo", fields.Field()) is missing | |
111 | req.mimetype = mimetype | |
112 | req.get_data.return_value = "" | |
113 | schema = dict2schema({"foo": fields.Field()})() | |
114 | assert parser.load_json(req, schema) is missing | |
87 | 115 | |
88 | 116 | |
89 | 117 | def test_abort_with_message(): |
110 | 138 | error = json.loads(serialized_error) |
111 | 139 | assert isinstance(error, dict) |
112 | 140 | assert error["message"] == "custom error message" |
113 | ||
114 | ||
115 | def test_json_cache_race_condition(): | |
116 | app = Flask("testapp") | |
117 | lock = threading.Lock() | |
118 | lock.acquire() | |
119 | ||
120 | class MyField(fields.Field): | |
121 | def _deserialize(self, value, attr, data, **kwargs): | |
122 | with lock: | |
123 | return value | |
124 | ||
125 | argmap = {"value": MyField()} | |
126 | results = {} | |
127 | ||
128 | def thread_fn(value): | |
129 | with app.test_request_context( | |
130 | "/foo", | |
131 | method="post", | |
132 | data=json.dumps({"value": value}), | |
133 | content_type="application/json", | |
134 | ): | |
135 | results[value] = parser.parse(argmap)["value"] | |
136 | ||
137 | t1 = threading.Thread(target=thread_fn, args=(42,)) | |
138 | t2 = threading.Thread(target=thread_fn, args=(23,)) | |
139 | t1.start() | |
140 | t2.start() | |
141 | lock.release() | |
142 | t1.join() | |
143 | t2.join() | |
144 | # ensure we didn't get contaminated by a parallel request | |
145 | assert results[42] == 42 | |
146 | assert results[23] == 23 |
0 | # -*- coding: utf-8 -*- | |
1 | ||
2 | 0 | import asyncio |
3 | 1 | import webtest |
4 | 2 | import webtest_aiohttp |
37 | 35 | |
38 | 36 | # regression test for https://github.com/marshmallow-code/webargs/issues/165 |
39 | 37 | def test_multiple_args(self, testapp): |
40 | res = testapp.post_json( | |
41 | "/echo_multiple_args", {"first": "1", "last": "2", "_ignore": 0} | |
42 | ) | |
38 | res = testapp.post_json("/echo_multiple_args", {"first": "1", "last": "2"}) | |
43 | 39 | assert res.json == {"first": "1", "last": "2"} |
44 | 40 | |
45 | 41 | # regression test for https://github.com/marshmallow-code/webargs/issues/145 |
46 | 42 | def test_nested_many_with_data_key(self, testapp): |
47 | res = testapp.post_json("/echo_nested_many_data_key", {"x_field": [{"id": 42}]}) | |
48 | 43 | # https://github.com/marshmallow-code/marshmallow/pull/714 |
44 | # on marshmallow 2, the field name can also be used | |
49 | 45 | if MARSHMALLOW_VERSION_INFO[0] < 3: |
46 | res = testapp.post_json( | |
47 | "/echo_nested_many_data_key", {"x_field": [{"id": 42}]} | |
48 | ) | |
50 | 49 | assert res.json == {"x_field": [{"id": 42}]} |
51 | 50 | |
52 | 51 | res = testapp.post_json("/echo_nested_many_data_key", {"X-Field": [{"id": 24}]}) |
10 | 10 | |
11 | 11 | |
12 | 12 | async def echo_parse(request): |
13 | parsed = await parser.parse(hello_args, request) | |
13 | parsed = await parser.parse(hello_args, request, location="query") | |
14 | 14 | return json_response(parsed) |
15 | 15 | |
16 | 16 | |
17 | @use_args(hello_args) | |
17 | @use_args(hello_args, location="query") | |
18 | 18 | async def echo_use_args(request, args): |
19 | 19 | return json_response(args) |
20 | 20 | |
21 | 21 | |
22 | @use_kwargs(hello_args) | |
22 | @use_kwargs(hello_args, location="query") | |
23 | 23 | async def echo_use_kwargs(request, name): |
24 | 24 | return json_response({"name": name}) |
25 | 25 |
0 | # -*- coding: utf-8 -*- | |
1 | ||
2 | from webargs.core import json | |
0 | import marshmallow as ma | |
1 | import pytest | |
2 | import tornado.concurrent | |
3 | import tornado.http1connection | |
4 | import tornado.httpserver | |
5 | import tornado.httputil | |
6 | import tornado.ioloop | |
7 | import tornado.web | |
8 | from tornado.testing import AsyncHTTPTestCase | |
9 | from webargs import fields, missing | |
10 | from webargs.core import MARSHMALLOW_VERSION_INFO, json, parse_json | |
11 | from webargs.tornadoparser import ( | |
12 | WebArgsTornadoMultiDictProxy, | |
13 | parser, | |
14 | use_args, | |
15 | use_kwargs, | |
16 | ) | |
17 | ||
18 | from urllib.parse import urlencode | |
3 | 19 | |
4 | 20 | try: |
5 | from urllib.parse import urlencode | |
6 | except ImportError: # PY2 | |
7 | from urllib import urlencode # type: ignore | |
8 | ||
9 | import mock | |
10 | import pytest | |
11 | ||
12 | import marshmallow as ma | |
13 | ||
14 | import tornado.web | |
15 | import tornado.httputil | |
16 | import tornado.httpserver | |
17 | import tornado.http1connection | |
18 | import tornado.concurrent | |
19 | import tornado.ioloop | |
20 | from tornado.testing import AsyncHTTPTestCase | |
21 | ||
22 | from webargs import fields, missing | |
23 | from webargs.tornadoparser import parser, use_args, use_kwargs, get_value | |
24 | from webargs.core import parse_json | |
21 | # Python 3.5 | |
22 | import mock | |
23 | except ImportError: | |
24 | # Python 3.6+ | |
25 | from unittest import mock | |
26 | ||
25 | 27 | |
26 | 28 | name = "name" |
27 | 29 | value = "value" |
28 | 30 | |
29 | 31 | |
30 | def test_get_value_basic(): | |
31 | field, multifield = fields.Field(), fields.List(fields.Str()) | |
32 | assert get_value({"foo": 42}, "foo", field) == 42 | |
33 | assert get_value({"foo": 42}, "bar", field) is missing | |
34 | assert get_value({"foos": ["a", "b"]}, "foos", multifield) == ["a", "b"] | |
35 | # https://github.com/marshmallow-code/webargs/pull/30 | |
36 | assert get_value({"foos": ["a", "b"]}, "bar", multifield) is missing | |
37 | ||
38 | ||
39 | class TestQueryArgs(object): | |
40 | def setup_method(self, method): | |
41 | parser.clear_cache() | |
42 | ||
32 | class AuthorSchema(ma.Schema): | |
33 | name = fields.Str(missing="World", validate=lambda n: len(n) >= 3) | |
34 | works = fields.List(fields.Str()) | |
35 | ||
36 | ||
37 | strict_kwargs = {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {} | |
38 | author_schema = AuthorSchema(**strict_kwargs) | |
39 | ||
40 | ||
41 | def test_tornado_multidictproxy(): | |
42 | for dictval, fieldname, expected in ( | |
43 | ({"name": "Sophocles"}, "name", "Sophocles"), | |
44 | ({"name": "Sophocles"}, "works", missing), | |
45 | ({"works": ["Antigone", "Oedipus Rex"]}, "works", ["Antigone", "Oedipus Rex"]), | |
46 | ({"works": ["Antigone", "Oedipus at Colonus"]}, "name", missing), | |
47 | ): | |
48 | proxy = WebArgsTornadoMultiDictProxy(dictval, author_schema) | |
49 | assert proxy.get(fieldname) == expected | |
50 | ||
51 | ||
52 | class TestQueryArgs: | |
43 | 53 | def test_it_should_get_single_values(self): |
44 | query = [(name, value)] | |
45 | field = fields.Field() | |
54 | query = [("name", "Aeschylus")] | |
46 | 55 | request = make_get_request(query) |
47 | ||
48 | result = parser.parse_querystring(request, name, field) | |
49 | ||
50 | assert result == value | |
56 | result = parser.load_querystring(request, author_schema) | |
57 | assert result["name"] == "Aeschylus" | |
51 | 58 | |
52 | 59 | def test_it_should_get_multiple_values(self): |
53 | query = [(name, value), (name, value)] | |
54 | field = fields.List(fields.Field()) | |
60 | query = [("works", "Agamemnon"), ("works", "Nereids")] | |
55 | 61 | request = make_get_request(query) |
56 | ||
57 | result = parser.parse_querystring(request, name, field) | |
58 | ||
59 | assert result == [value, value] | |
62 | result = parser.load_querystring(request, author_schema) | |
63 | assert result["works"] == ["Agamemnon", "Nereids"] | |
60 | 64 | |
61 | 65 | def test_it_should_return_missing_if_not_present(self): |
62 | 66 | query = [] |
63 | field = fields.Field() | |
64 | field2 = fields.List(fields.Int()) | |
65 | 67 | request = make_get_request(query) |
66 | ||
67 | result = parser.parse_querystring(request, name, field) | |
68 | result2 = parser.parse_querystring(request, name, field2) | |
69 | ||
70 | assert result is missing | |
71 | assert result2 is missing | |
72 | ||
73 | def test_it_should_return_empty_list_if_multiple_and_not_present(self): | |
74 | query = [] | |
75 | field = fields.List(fields.Field()) | |
76 | request = make_get_request(query) | |
77 | ||
78 | result = parser.parse_querystring(request, name, field) | |
79 | ||
80 | assert result is missing | |
68 | result = parser.load_querystring(request, author_schema) | |
69 | assert result["name"] is missing | |
70 | assert result["works"] is missing | |
81 | 71 | |
82 | 72 | |
83 | 73 | class TestFormArgs: |
84 | def setup_method(self, method): | |
85 | parser.clear_cache() | |
86 | ||
87 | 74 | def test_it_should_get_single_values(self): |
88 | query = [(name, value)] | |
89 | field = fields.Field() | |
75 | query = [("name", "Aristophanes")] | |
90 | 76 | request = make_form_request(query) |
91 | ||
92 | result = parser.parse_form(request, name, field) | |
93 | ||
94 | assert result == value | |
77 | result = parser.load_form(request, author_schema) | |
78 | assert result["name"] == "Aristophanes" | |
95 | 79 | |
96 | 80 | def test_it_should_get_multiple_values(self): |
97 | query = [(name, value), (name, value)] | |
98 | field = fields.List(fields.Field()) | |
81 | query = [("works", "The Wasps"), ("works", "The Frogs")] | |
99 | 82 | request = make_form_request(query) |
100 | ||
101 | result = parser.parse_form(request, name, field) | |
102 | ||
103 | assert result == [value, value] | |
83 | result = parser.load_form(request, author_schema) | |
84 | assert result["works"] == ["The Wasps", "The Frogs"] | |
104 | 85 | |
105 | 86 | def test_it_should_return_missing_if_not_present(self): |
106 | 87 | query = [] |
107 | field = fields.Field() | |
108 | 88 | request = make_form_request(query) |
109 | ||
110 | result = parser.parse_form(request, name, field) | |
111 | ||
112 | assert result is missing | |
113 | ||
114 | def test_it_should_return_empty_list_if_multiple_and_not_present(self): | |
115 | query = [] | |
116 | field = fields.List(fields.Field()) | |
117 | request = make_form_request(query) | |
118 | ||
119 | result = parser.parse_form(request, name, field) | |
120 | ||
121 | assert result is missing | |
122 | ||
123 | ||
124 | class TestJSONArgs(object): | |
125 | def setup_method(self, method): | |
126 | parser.clear_cache() | |
127 | ||
89 | result = parser.load_form(request, author_schema) | |
90 | assert result["name"] is missing | |
91 | assert result["works"] is missing | |
92 | ||
93 | ||
94 | class TestJSONArgs: | |
128 | 95 | def test_it_should_get_single_values(self): |
129 | query = {name: value} | |
130 | field = fields.Field() | |
96 | query = {"name": "Euripides"} | |
131 | 97 | request = make_json_request(query) |
132 | result = parser.parse_json(request, name, field) | |
133 | ||
134 | assert result == value | |
98 | result = parser.load_json(request, author_schema) | |
99 | assert result["name"] == "Euripides" | |
135 | 100 | |
136 | 101 | def test_parsing_request_with_vendor_content_type(self): |
137 | query = {name: value} | |
138 | field = fields.Field() | |
102 | query = {"name": "Euripides"} | |
139 | 103 | request = make_json_request( |
140 | 104 | query, content_type="application/vnd.api+json; charset=UTF-8" |
141 | 105 | ) |
142 | result = parser.parse_json(request, name, field) | |
143 | ||
144 | assert result == value | |
106 | result = parser.load_json(request, author_schema) | |
107 | assert result["name"] == "Euripides" | |
145 | 108 | |
146 | 109 | def test_it_should_get_multiple_values(self): |
147 | query = {name: [value, value]} | |
148 | field = fields.List(fields.Field()) | |
110 | query = {"works": ["Medea", "Electra"]} | |
149 | 111 | request = make_json_request(query) |
150 | result = parser.parse_json(request, name, field) | |
151 | ||
152 | assert result == [value, value] | |
112 | result = parser.load_json(request, author_schema) | |
113 | assert result["works"] == ["Medea", "Electra"] | |
153 | 114 | |
154 | 115 | def test_it_should_get_multiple_nested_values(self): |
155 | query = {name: [{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}]} | |
156 | field = fields.List( | |
157 | fields.Nested({"id": fields.Field(), "name": fields.Field()}) | |
158 | ) | |
116 | class CustomSchema(ma.Schema): | |
117 | works = fields.List( | |
118 | fields.Nested({"author": fields.Str(), "workname": fields.Str()}) | |
119 | ) | |
120 | ||
121 | custom_schema = CustomSchema(**strict_kwargs) | |
122 | ||
123 | query = { | |
124 | "works": [ | |
125 | {"author": "Euripides", "workname": "Hecuba"}, | |
126 | {"author": "Aristophanes", "workname": "The Birds"}, | |
127 | ] | |
128 | } | |
159 | 129 | request = make_json_request(query) |
160 | result = parser.parse_json(request, name, field) | |
161 | assert result == [{"id": 1, "name": "foo"}, {"id": 2, "name": "bar"}] | |
130 | result = parser.load_json(request, custom_schema) | |
131 | assert result["works"] == [ | |
132 | {"author": "Euripides", "workname": "Hecuba"}, | |
133 | {"author": "Aristophanes", "workname": "The Birds"}, | |
134 | ] | |
135 | ||
136 | def test_it_should_not_include_fieldnames_if_not_present(self): | |
137 | query = {} | |
138 | request = make_json_request(query) | |
139 | result = parser.load_json(request, author_schema) | |
140 | assert result == {} | |
141 | ||
142 | def test_it_should_handle_type_error_on_load_json(self): | |
143 | # but this is different from the test above where the payload was valid | |
144 | # and empty -- missing vs {} | |
145 | request = make_request( | |
146 | body=tornado.concurrent.Future(), | |
147 | headers={"Content-Type": "application/json"}, | |
148 | ) | |
149 | result = parser.load_json(request, author_schema) | |
150 | assert result is missing | |
151 | ||
152 | def test_it_should_handle_value_error_on_parse_json(self): | |
153 | request = make_request("this is json not") | |
154 | result = parser.load_json(request, author_schema) | |
155 | assert result is missing | |
156 | ||
157 | ||
158 | class TestHeadersArgs: | |
159 | def test_it_should_get_single_values(self): | |
160 | query = {"name": "Euphorion"} | |
161 | request = make_request(headers=query) | |
162 | result = parser.load_headers(request, author_schema) | |
163 | assert result["name"] == "Euphorion" | |
164 | ||
165 | def test_it_should_get_multiple_values(self): | |
166 | query = {"works": ["Prometheus Bound", "Prometheus Unbound"]} | |
167 | request = make_request(headers=query) | |
168 | result = parser.load_headers(request, author_schema) | |
169 | assert result["works"] == ["Prometheus Bound", "Prometheus Unbound"] | |
162 | 170 | |
163 | 171 | def test_it_should_return_missing_if_not_present(self): |
164 | query = {} | |
165 | field = fields.Field() | |
166 | request = make_json_request(query) | |
167 | result = parser.parse_json(request, name, field) | |
168 | ||
169 | assert result is missing | |
170 | ||
171 | def test_it_should_return_empty_list_if_multiple_and_not_present(self): | |
172 | query = {} | |
173 | field = fields.List(fields.Field()) | |
174 | request = make_json_request(query) | |
175 | result = parser.parse_json(request, name, field) | |
176 | ||
177 | assert result is missing | |
178 | ||
179 | def test_it_should_handle_type_error_on_parse_json(self): | |
180 | field = fields.Field() | |
181 | request = make_request( | |
182 | body=tornado.concurrent.Future, headers={"Content-Type": "application/json"} | |
183 | ) | |
184 | result = parser.parse_json(request, name, field) | |
185 | assert parser._cache["json"] == {} | |
186 | assert result is missing | |
187 | ||
188 | def test_it_should_handle_value_error_on_parse_json(self): | |
189 | field = fields.Field() | |
190 | request = make_request("this is json not") | |
191 | result = parser.parse_json(request, name, field) | |
192 | assert parser._cache["json"] == {} | |
193 | assert result is missing | |
194 | ||
195 | ||
196 | class TestHeadersArgs(object): | |
197 | def setup_method(self, method): | |
198 | parser.clear_cache() | |
199 | ||
172 | request = make_request() | |
173 | result = parser.load_headers(request, author_schema) | |
174 | assert result["name"] is missing | |
175 | assert result["works"] is missing | |
176 | ||
177 | ||
178 | class TestFilesArgs: | |
200 | 179 | def test_it_should_get_single_values(self): |
201 | query = {name: value} | |
202 | field = fields.Field() | |
203 | request = make_request(headers=query) | |
204 | ||
205 | result = parser.parse_headers(request, name, field) | |
206 | ||
207 | assert result == value | |
180 | query = [("name", "Sappho")] | |
181 | request = make_files_request(query) | |
182 | result = parser.load_files(request, author_schema) | |
183 | assert result["name"] == "Sappho" | |
208 | 184 | |
209 | 185 | def test_it_should_get_multiple_values(self): |
210 | query = {name: [value, value]} | |
211 | field = fields.List(fields.Field()) | |
212 | request = make_request(headers=query) | |
213 | ||
214 | result = parser.parse_headers(request, name, field) | |
215 | ||
216 | assert result == [value, value] | |
217 | ||
218 | def test_it_should_return_missing_if_not_present(self): | |
219 | field = fields.Field(multiple=False) | |
220 | request = make_request() | |
221 | ||
222 | result = parser.parse_headers(request, name, field) | |
223 | ||
224 | assert result is missing | |
225 | ||
226 | def test_it_should_return_empty_list_if_multiple_and_not_present(self): | |
227 | query = {} | |
228 | field = fields.List(fields.Field()) | |
229 | request = make_request(headers=query) | |
230 | ||
231 | result = parser.parse_headers(request, name, field) | |
232 | ||
233 | assert result is missing | |
234 | ||
235 | ||
236 | class TestFilesArgs(object): | |
237 | def setup_method(self, method): | |
238 | parser.clear_cache() | |
239 | ||
240 | def test_it_should_get_single_values(self): | |
241 | query = [(name, value)] | |
242 | field = fields.Field() | |
186 | query = [("works", "Sappho 31"), ("works", "Ode to Aphrodite")] | |
243 | 187 | request = make_files_request(query) |
244 | ||
245 | result = parser.parse_files(request, name, field) | |
246 | ||
247 | assert result == value | |
248 | ||
249 | def test_it_should_get_multiple_values(self): | |
250 | query = [(name, value), (name, value)] | |
251 | field = fields.List(fields.Field()) | |
252 | request = make_files_request(query) | |
253 | ||
254 | result = parser.parse_files(request, name, field) | |
255 | ||
256 | assert result == [value, value] | |
188 | result = parser.load_files(request, author_schema) | |
189 | assert result["works"] == ["Sappho 31", "Ode to Aphrodite"] | |
257 | 190 | |
258 | 191 | def test_it_should_return_missing_if_not_present(self): |
259 | 192 | query = [] |
260 | field = fields.Field() | |
261 | 193 | request = make_files_request(query) |
262 | ||
263 | result = parser.parse_files(request, name, field) | |
264 | ||
265 | assert result is missing | |
266 | ||
267 | def test_it_should_return_empty_list_if_multiple_and_not_present(self): | |
268 | query = [] | |
269 | field = fields.List(fields.Field()) | |
270 | request = make_files_request(query) | |
271 | ||
272 | result = parser.parse_files(request, name, field) | |
273 | ||
274 | assert result is missing | |
275 | ||
276 | ||
277 | class TestErrorHandler(object): | |
194 | result = parser.load_files(request, author_schema) | |
195 | assert result["name"] is missing | |
196 | assert result["works"] is missing | |
197 | ||
198 | ||
199 | class TestErrorHandler: | |
278 | 200 | def test_it_should_raise_httperror_on_failed_validation(self): |
279 | 201 | args = {"foo": fields.Field(validate=lambda x: False)} |
280 | 202 | with pytest.raises(tornado.web.HTTPError): |
281 | 203 | parser.parse(args, make_json_request({"foo": 42})) |
282 | 204 | |
283 | 205 | |
284 | class TestParse(object): | |
285 | def setup_method(self, method): | |
286 | parser.clear_cache() | |
287 | ||
206 | class TestParse: | |
288 | 207 | def test_it_should_parse_query_arguments(self): |
289 | 208 | attrs = {"string": fields.Field(), "integer": fields.List(fields.Int())} |
290 | 209 | |
292 | 211 | [("string", "value"), ("integer", "1"), ("integer", "2")] |
293 | 212 | ) |
294 | 213 | |
295 | parsed = parser.parse(attrs, request) | |
214 | parsed = parser.parse(attrs, request, location="query") | |
296 | 215 | |
297 | 216 | assert parsed["integer"] == [1, 2] |
298 | 217 | assert parsed["string"] == value |
304 | 223 | [("string", "value"), ("integer", "1"), ("integer", "2")] |
305 | 224 | ) |
306 | 225 | |
307 | parsed = parser.parse(attrs, request) | |
226 | parsed = parser.parse(attrs, request, location="form") | |
308 | 227 | |
309 | 228 | assert parsed["integer"] == [1, 2] |
310 | 229 | assert parsed["string"] == value |
336 | 255 | |
337 | 256 | request = make_request(headers={"string": "value", "integer": ["1", "2"]}) |
338 | 257 | |
339 | parsed = parser.parse(attrs, request, locations=["headers"]) | |
258 | parsed = parser.parse(attrs, request, location="headers") | |
340 | 259 | |
341 | 260 | assert parsed["string"] == value |
342 | 261 | assert parsed["integer"] == [1, 2] |
348 | 267 | [("string", "value"), ("integer", "1"), ("integer", "2")] |
349 | 268 | ) |
350 | 269 | |
351 | parsed = parser.parse(attrs, request, locations=["cookies"]) | |
270 | parsed = parser.parse(attrs, request, location="cookies") | |
352 | 271 | |
353 | 272 | assert parsed["string"] == value |
354 | 273 | assert parsed["integer"] == [2] |
360 | 279 | [("string", "value"), ("integer", "1"), ("integer", "2")] |
361 | 280 | ) |
362 | 281 | |
363 | parsed = parser.parse(attrs, request, locations=["files"]) | |
282 | parsed = parser.parse(attrs, request, location="files") | |
364 | 283 | |
365 | 284 | assert parsed["string"] == value |
366 | 285 | assert parsed["integer"] == [1, 2] |
382 | 301 | parser.parse(args, request) |
383 | 302 | |
384 | 303 | |
385 | class TestUseArgs(object): | |
386 | def setup_method(self, method): | |
387 | parser.clear_cache() | |
388 | ||
304 | class TestUseArgs: | |
389 | 305 | def test_it_should_pass_parsed_as_first_argument(self): |
390 | class Handler(object): | |
306 | class Handler: | |
391 | 307 | request = make_json_request({"key": "value"}) |
392 | 308 | |
393 | 309 | @use_args({"key": fields.Field()}) |
402 | 318 | assert result is True |
403 | 319 | |
404 | 320 | def test_it_should_pass_parsed_as_kwargs_arguments(self): |
405 | class Handler(object): | |
321 | class Handler: | |
406 | 322 | request = make_json_request({"key": "value"}) |
407 | 323 | |
408 | 324 | @use_kwargs({"key": fields.Field()}) |
417 | 333 | assert result is True |
418 | 334 | |
419 | 335 | def test_it_should_be_validate_arguments_when_validator_is_passed(self): |
420 | class Handler(object): | |
336 | class Handler: | |
421 | 337 | request = make_json_request({"foo": 41}) |
422 | 338 | |
423 | 339 | @use_kwargs({"foo": fields.Int()}, validate=lambda args: args["foo"] > 42) |
475 | 391 | |
476 | 392 | |
477 | 393 | def make_request(uri=None, body=None, headers=None, files=None): |
478 | uri = uri if uri is not None else u"" | |
479 | body = body if body is not None else u"" | |
394 | uri = uri if uri is not None else "" | |
395 | body = body if body is not None else "" | |
480 | 396 | method = "POST" if body else "GET" |
481 | 397 | # Need to make a mock connection right now because Tornado 4.0 requires a |
482 | 398 | # remote_ip in the context attribute. 4.1 addresses this, and this |
485 | 401 | mock_connection = mock.Mock(spec=tornado.http1connection.HTTP1Connection) |
486 | 402 | mock_connection.context = mock.Mock() |
487 | 403 | mock_connection.remote_ip = None |
488 | content_type = headers.get("Content-Type", u"") if headers else u"" | |
404 | content_type = headers.get("Content-Type", "") if headers else "" | |
489 | 405 | request = tornado.httputil.HTTPServerRequest( |
490 | 406 | method=method, |
491 | 407 | uri=uri, |
508 | 424 | class EchoHandler(tornado.web.RequestHandler): |
509 | 425 | ARGS = {"name": fields.Str()} |
510 | 426 | |
511 | @use_args(ARGS) | |
427 | @use_args(ARGS, location="query") | |
512 | 428 | def get(self, args): |
513 | 429 | self.write(args) |
430 | ||
431 | ||
432 | class EchoFormHandler(tornado.web.RequestHandler): | |
433 | ARGS = {"name": fields.Str()} | |
434 | ||
435 | @use_args(ARGS, location="form") | |
436 | def post(self, args): | |
437 | self.write(args) | |
438 | ||
439 | ||
440 | class EchoJSONHandler(tornado.web.RequestHandler): | |
441 | ARGS = {"name": fields.Str()} | |
514 | 442 | |
515 | 443 | @use_args(ARGS) |
516 | 444 | def post(self, args): |
520 | 448 | class EchoWithParamHandler(tornado.web.RequestHandler): |
521 | 449 | ARGS = {"name": fields.Str()} |
522 | 450 | |
523 | @use_args(ARGS) | |
451 | @use_args(ARGS, location="query") | |
524 | 452 | def get(self, id, args): |
525 | 453 | self.write(args) |
526 | 454 | |
527 | 455 | |
528 | 456 | echo_app = tornado.web.Application( |
529 | [(r"/echo", EchoHandler), (r"/echo_with_param/(\d+)", EchoWithParamHandler)] | |
457 | [ | |
458 | (r"/echo", EchoHandler), | |
459 | (r"/echo_form", EchoFormHandler), | |
460 | (r"/echo_json", EchoJSONHandler), | |
461 | (r"/echo_with_param/(\d+)", EchoWithParamHandler), | |
462 | ] | |
530 | 463 | ) |
531 | 464 | |
532 | 465 | |
536 | 469 | |
537 | 470 | def test_post(self): |
538 | 471 | res = self.fetch( |
539 | "/echo", | |
472 | "/echo_json", | |
540 | 473 | method="POST", |
541 | 474 | headers={"Content-Type": "application/json"}, |
542 | 475 | body=json.dumps({"name": "Steve"}), |
544 | 477 | json_body = parse_json(res.body) |
545 | 478 | assert json_body["name"] == "Steve" |
546 | 479 | res = self.fetch( |
547 | "/echo", | |
480 | "/echo_json", | |
548 | 481 | method="POST", |
549 | 482 | headers={"Content-Type": "application/json"}, |
550 | 483 | body=json.dumps({}), |
576 | 509 | def post(self, args): |
577 | 510 | self.write(args) |
578 | 511 | |
579 | @use_kwargs(ARGS) | |
512 | @use_kwargs(ARGS, location="query") | |
580 | 513 | def get(self, name): |
581 | 514 | self.write({"status": "success"}) |
582 | 515 |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """Tests for the webapp2 parser""" |
2 | try: | |
3 | from urllib.parse import urlencode | |
4 | except ImportError: # PY2 | |
5 | from urllib import urlencode # type: ignore | |
1 | from urllib.parse import urlencode | |
6 | 2 | from webargs.core import json |
7 | 3 | |
8 | 4 | import pytest |
5 | import marshmallow as ma | |
9 | 6 | from marshmallow import fields, ValidationError |
10 | 7 | |
11 | 8 | import webtest |
12 | 9 | import webapp2 |
13 | 10 | from webargs.webapp2parser import parser |
11 | from webargs.core import MARSHMALLOW_VERSION_INFO | |
14 | 12 | |
15 | 13 | hello_args = {"name": fields.Str(missing="World")} |
16 | 14 | |
24 | 22 | } |
25 | 23 | |
26 | 24 | |
25 | class HelloSchema(ma.Schema): | |
26 | name = fields.Str(missing="World", validate=lambda n: len(n) >= 3) | |
27 | ||
28 | ||
29 | # variant which ignores unknown fields | |
30 | exclude_kwargs = ( | |
31 | {"strict": True} if MARSHMALLOW_VERSION_INFO[0] < 3 else {"unknown": ma.EXCLUDE} | |
32 | ) | |
33 | hello_exclude_schema = HelloSchema(**exclude_kwargs) | |
34 | ||
35 | ||
27 | 36 | def test_parse_querystring_args(): |
28 | 37 | request = webapp2.Request.blank("/echo?name=Fred") |
29 | assert parser.parse(hello_args, req=request) == {"name": "Fred"} | |
38 | assert parser.parse(hello_args, req=request, location="query") == {"name": "Fred"} | |
30 | 39 | |
31 | 40 | |
32 | 41 | def test_parse_querystring_multiple(): |
33 | 42 | expected = {"name": ["steve", "Loria"]} |
34 | 43 | request = webapp2.Request.blank("/echomulti?name=steve&name=Loria") |
35 | assert parser.parse(hello_multiple, req=request) == expected | |
44 | assert parser.parse(hello_multiple, req=request, location="query") == expected | |
36 | 45 | |
37 | 46 | |
38 | 47 | def test_parse_form(): |
39 | 48 | expected = {"name": "Joe"} |
40 | 49 | request = webapp2.Request.blank("/echo", POST=expected) |
41 | assert parser.parse(hello_args, req=request) == expected | |
50 | assert parser.parse(hello_args, req=request, location="form") == expected | |
42 | 51 | |
43 | 52 | |
44 | 53 | def test_parse_form_multiple(): |
45 | 54 | expected = {"name": ["steve", "Loria"]} |
46 | 55 | request = webapp2.Request.blank("/echo", POST=urlencode(expected, doseq=True)) |
47 | assert parser.parse(hello_multiple, req=request) == expected | |
56 | assert parser.parse(hello_multiple, req=request, location="form") == expected | |
48 | 57 | |
49 | 58 | |
50 | 59 | def test_parsing_form_default(): |
51 | 60 | request = webapp2.Request.blank("/echo", POST="") |
52 | assert parser.parse(hello_args, req=request) == {"name": "World"} | |
61 | assert parser.parse(hello_args, req=request, location="form") == {"name": "World"} | |
53 | 62 | |
54 | 63 | |
55 | 64 | def test_parse_json(): |
103 | 112 | request = webapp2.Request.blank( |
104 | 113 | "/", headers={"Cookie": response.headers["Set-Cookie"]} |
105 | 114 | ) |
106 | assert parser.parse(hello_args, req=request, locations=("cookies",)) == expected | |
115 | assert parser.parse(hello_args, req=request, location="cookies") == expected | |
107 | 116 | |
108 | 117 | |
109 | 118 | def test_parsing_headers(): |
110 | 119 | expected = {"name": "Fred"} |
111 | 120 | request = webapp2.Request.blank("/", headers=expected) |
112 | assert parser.parse(hello_args, req=request, locations=("headers",)) == expected | |
121 | assert ( | |
122 | parser.parse(hello_exclude_schema, req=request, location="headers") == expected | |
123 | ) | |
113 | 124 | |
114 | 125 | |
115 | 126 | def test_parse_files(): |
118 | 129 | """ |
119 | 130 | |
120 | 131 | class Handler(webapp2.RequestHandler): |
121 | @parser.use_args({"myfile": fields.List(fields.Field())}, locations=("files",)) | |
132 | @parser.use_args({"myfile": fields.List(fields.Field())}, location="files") | |
122 | 133 | def post(self, args): |
123 | 134 | self.response.content_type = "application/json" |
124 | 135 | |
125 | 136 | def _value(f): |
126 | 137 | return f.getvalue().decode("utf-8") |
127 | 138 | |
128 | data = dict((i.filename, _value(i.file)) for i in args["myfile"]) | |
139 | data = {i.filename: _value(i.file) for i in args["myfile"]} | |
129 | 140 | self.response.write(json.dumps(data)) |
130 | 141 | |
131 | 142 | app = webapp2.WSGIApplication([("/", Handler)]) |
138 | 149 | def test_exception_on_validation_error(): |
139 | 150 | request = webapp2.Request.blank("/", POST={"num": "3"}) |
140 | 151 | with pytest.raises(ValidationError): |
141 | parser.parse(hello_validate, req=request) | |
152 | parser.parse(hello_validate, req=request, location="form") | |
142 | 153 | |
143 | 154 | |
144 | 155 | def test_validation_error_with_message(): |
145 | 156 | request = webapp2.Request.blank("/", POST={"num": "3"}) |
146 | 157 | with pytest.raises(ValidationError) as exc: |
147 | parser.parse(hello_validate, req=request) | |
158 | parser.parse(hello_validate, req=request, location="form") | |
148 | 159 | assert "Houston, we've had a problem." in exc.value |
149 | 160 | |
150 | 161 | |
156 | 167 | request = webapp2.Request.blank("/echo", POST=expected) |
157 | 168 | app = webapp2.WSGIApplication([]) |
158 | 169 | app.set_globals(app, request) |
159 | assert parser.parse(hello_args) == expected | |
170 | assert parser.parse(hello_args, location="form") == expected |
0 | 0 | [tox] |
1 | 1 | envlist= |
2 | 2 | lint |
3 | py{27,35,36,37}-marshmallow2 | |
4 | py{35,36,37}-marshmallow3 | |
5 | py37-marshmallowdev | |
3 | py{35,36,37,38}-marshmallow2 | |
4 | py{35,36,37,38}-marshmallow3 | |
5 | py38-marshmallowdev | |
6 | 6 | docs |
7 | 7 | |
8 | 8 | [testenv] |
11 | 11 | marshmallow2: marshmallow==2.15.2 |
12 | 12 | marshmallow3: marshmallow>=3.0.0rc2,<4.0.0 |
13 | 13 | marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz |
14 | commands = | |
15 | py27: pytest --ignore=tests/test_py3/ {posargs} | |
16 | py{35,36,37}: pytest {posargs} | |
14 | commands = pytest {posargs} | |
17 | 15 | |
18 | 16 | [testenv:lint] |
19 | deps = pre-commit~=1.17 | |
17 | deps = pre-commit~=2.4 | |
20 | 18 | skip_install = true |
21 | 19 | commands = pre-commit run --all-files |
22 | 20 |