Codebase list python-webargs / d683eab9-1961-49db-81b8-6b9eda9c0991/main
New upstream release. Kali Janitor 1 year, 7 months ago
47 changed file(s) with 1465 addition(s) and 533 deletion(s). Raw diff Collapse all Expand all
0 version: 2
1 updates:
2 - package-ecosystem: pip
3 directory: "/"
4 schedule:
5 interval: daily
6 open-pull-requests-limit: 10
0 name: build
1 on:
2 push:
3 pull_request:
4 schedule: # nightly build
5 - cron: '0 0 * * *'
6
7 jobs:
8 mypy:
9 runs-on: ubuntu-latest
10 steps:
11 - uses: actions/checkout@v2
12 - uses: actions/setup-python@v2
13 - run: python -m pip install tox
14 - run: python -m tox -e mypy
15
16 test:
17 strategy:
18 matrix:
19 tox-factor: [""]
20 python-version: ["3.7", "3.8", "3.9", "3.10"]
21 include:
22 - python-version: "3.10"
23 tox-factor: "-marshmallowdev"
24 - python-version: "3.7"
25 tox-factor: "-mindeps"
26 name: "test py${{ matrix.python-version }}${{ matrix.tox-factor }}"
27 runs-on: ubuntu-latest
28 steps:
29 - uses: actions/checkout@v2
30 - uses: actions/setup-python@v2
31 with:
32 python-version: ${{ matrix.python-version }}
33 - run: python -m pip install tox
34 - run: python -m tox -e py${{ matrix.tox-factor }}
35
36 # this duplicates pre-commit.ci, so only run it on tags
37 # it guarantees that linting is passing prior to a release
38 lint-pre-release:
39 if: startsWith(github.ref, 'refs/tags')
40 runs-on: ubuntu-latest
41 steps:
42 - uses: actions/checkout@v2
43 - uses: actions/setup-python@v2
44 - run: python -m pip install tox
45 - run: python -m tox -e lint
46
47 release:
48 needs: [mypy, test, lint-pre-release]
49 runs-on: ubuntu-latest
50 if: startsWith(github.ref, 'refs/tags')
51 steps:
52 - uses: actions/checkout@v2
53 - uses: actions/setup-python@v2
54 - name: install requirements
55 run: python -m pip install build twine
56 - name: build dists
57 run: python -m build
58 - name: check package metadata
59 run: twine check dist/*
60 - name: publish
61 run: twine upload -u __token__ -p ${{ secrets.PYPI_API_TOKEN }} dist/*
00 repos:
11 - repo: https://github.com/asottile/pyupgrade
2 rev: v2.7.3
2 rev: v2.34.0
33 hooks:
44 - id: pyupgrade
5 args: ["--py36-plus"]
5 args: ["--py37-plus"]
6 - repo: https://github.com/python-jsonschema/check-jsonschema
7 rev: 0.16.2
8 hooks:
9 - id: check-github-workflows
10 - id: check-readthedocs
611 - repo: https://github.com/psf/black
7 rev: 20.8b1
12 rev: 22.6.0
813 hooks:
914 - id: black
10 - repo: https://gitlab.com/pycqa/flake8
11 rev: 3.8.4
15 - repo: https://github.com/pycqa/flake8
16 rev: 4.0.1
1217 hooks:
1318 - id: flake8
14 additional_dependencies: [flake8-bugbear==20.1.0]
19 additional_dependencies: [flake8-bugbear==22.1.11]
1520 - repo: https://github.com/asottile/blacken-docs
16 rev: v1.8.0
21 rev: v1.12.1
1722 hooks:
1823 - id: blacken-docs
19 additional_dependencies: [black==20.8b1]
20 args: ["--target-version", "py35"]
24 additional_dependencies: [black==22.1.0]
25 args: ["--target-version", "py37"]
2126 - repo: https://github.com/pre-commit/mirrors-mypy
22 rev: v0.790
27 rev: v0.961
2328 hooks:
2429 - id: mypy
2530 language_version: python3
2631 files: ^src/webargs/
32 additional_dependencies: ["marshmallow>=3,<4", "packaging"]
33
34
35 # mypy runs under tox in GitHub Actions, skip it in pre-commit.ci
36 ci:
37 skip: [mypy]
22 configuration: docs/conf.py
33 formats: all
44 python:
5 version: 3.8
5 version: "3.8"
66 install:
77 - method: pip
88 path: .
66
77 * Steven Loria `@sloria <https://github.com/sloria>`_
88 * Jérôme Lafréchoux `@lafrech <https://github.com/lafrech>`_
9 * Stephen Rosen `@sirosen <https://github.com/sirosen>`_
910
1011 Contributors (chronological)
1112 ----------------------------
4142 * `@zhenhua32 <https://github.com/zhenhua32>`_
4243 * Martin Roy `@lindycoder <https://github.com/lindycoder>`_
4344 * Kubilay Kocak `@koobs <https://github.com/koobs>`_
44 * Stephen Rosen `@sirosen <https://github.com/sirosen>`_
4545 * `@dodumosu <https://github.com/dodumosu>`_
4646 * Nate Dellinger `@Nateyo <https://github.com/Nateyo>`_
4747 * Karthikeyan Singaravelan `@tirkarthi <https://github.com/tirkarthi>`_
5050 * Lefteris Karapetsas `@lefterisjp <https://github.com/lefterisjp>`_
5151 * Utku Gultopu `@ugultopu <https://github.com/ugultopu>`_
5252 * Jason Williams `@jaswilli <https://github.com/jaswilli>`_
53 * Grey Li `@greyli <https://github.com/greyli>`_
54 * `@michaelizergit <https://github.com/michaelizergit>`_
55 * Legolas Bloom `@TTWShell <https://github.com/TTWShell>`_
56 * Kevin Kirsche `@kkirsche <https://github.com/kkirsche>`_
57 * Isira Seneviratne `@Isira-Seneviratne <https://github.com/Isira-Seneviratne>`_
58 * Anton Ostapenko `@AVOstap <https://github.com/AVOstap>`_
00 Changelog
11 ---------
2
3 8.2.0 (2022-07-11)
4 ******************
5
6 Features:
7
8 * A new method, ``webargs.Parser.async_parse``, can be used for async-aware
9 parsing from the base parser class. This can handle async location loader
10 functions and async error handlers.
11
12 * ``webargs.Parser.use_args`` and ``use_kwargs`` can now be used to decorate
13 async functions, and will use ``async_parse`` if the decorated function is
14 also async. They will call the non-async ``parse`` method when used to
15 decorate non-async functions.
16
17 * As a result of the changes to ``webargs.Parser``, ``FlaskParser``,
18 ``DjangoParser``, and ``FalconParser`` now all support async views.
19 Thanks :user:`Isira-Seneviratne` for the initial PR.
20
21 Changes:
22
23 * The implementation of ``AsyncParser`` has changed. Now that
24 ``webargs.Parser`` has built-in support for async usage, the primary
25 purpose of ``AsyncParser`` is to redefine ``parse`` as an alias for
26 ``async_parse``
27
28 * Set ``python_requires>=3.7.2`` in package metadata (:pr:`692`).
29 Thanks :user:`kasium` for the PR.
30
31 8.1.0 (2022-01-12)
32 ******************
33
34 Bug fixes:
35
36 * Fix publishing type hints per `PEP-561 <https://www.python.org/dev/peps/pep-0561/>`_.
37 (:pr:`650`).
38 * Add DelimitedTuple to fields.__all__ (:pr:`678`).
39 * Narrow type of ``argmap`` from ``Mapping`` to ``Dict`` (:pr:`682`).
40
41 Other changes:
42
43 * Test against Python 3.10 (:pr:`647`).
44 * Drop support for Python 3.6 (:pr:`673`).
45 * Address distutils deprecation warning in Python 3.10 (:pr:`652`).
46 Thanks :user:`kkirsche` for the PR.
47 * Use postponed evaluation of annotations (:pr:`663`).
48 Thanks :user:`Isira-Seneviratne` for the PR.
49 * Pin mypy version in tox (:pr:`674`).
50 * Improve type annotations for ``__version_info__`` (:pr:`680`).
51
52 8.0.1 (2021-08-12)
53 ******************
54
55 Bug fixes:
56
57 * Fix "``DelimitedList`` deserializes empty string as ``['']``" (:issue:`623`).
58 Thanks :user:`TTWSchell` for reporting and for the PR.
59
60 Other changes:
61
62 * New documentation theme with `furo`. Thanks to :user:`pradyunsg` for writing
63 furo!
64 * Webargs has a new logo. Thanks to :user:`michaelizergit`! (:issue:`312`)
65 * Don't build universal wheels. We don't support Python 2 anymore.
66 (:pr:`632`)
67 * Make the build reproducible (:pr:`631`).
68
69
70 8.0.0 (2021-04-08)
71 ******************
72
73 Features:
74
75 * Add `Parser.pre_load` as a method for allowing users to modify data before
76 schema loading, but without redefining location loaders. See advanced docs on
77 `Parser pre_load` for usage information. (:pr:`583`)
78
79 * *Backwards-incompatible*: ``unknown`` defaults to `None` for body locations
80 (`json`, `form` and `json_or_form`) (:issue:`580`).
81
82 * Detection of fields as "multi-value" for unpacking lists from multi-dict
83 types is now extensible with the ``is_multiple`` attribute. If a field sets
84 ``is_multiple = True`` it will be detected as a multi-value field. If
85 ``is_multiple`` is not set or is set to ``None``, webargs will check if the
86 field is an instance of ``List`` or ``Tuple``. (:issue:`563`)
87
88 * A new attribute on ``Parser`` objects, ``Parser.KNOWN_MULTI_FIELDS`` can be
89 used to set fields which should be detected as ``is_multiple=True`` even when
90 the attribute is not set (:pr:`592`).
91
92 See docs on "Multi-Field Detection" for more details.
93
94 Bug fixes:
95
96 * ``Tuple`` field now behaves as a "multiple" field (:pr:`585`).
297
398 7.0.1 (2020-12-14)
499 ******************
5555
5656 # The pre-commit CLI was installed above
5757 $ pre-commit install
58
59 .. note::
60
61 webargs uses `black <https://github.com/ambv/black>`_ for code formatting, which is only compatible with Python>=3.6.
62 Therefore, the pre-commit hooks require a minimum Python version of 3.6.
6358
6459 Git Branch Structure
6560 ++++++++++++++++++++
11 include LICENSE
22 include *.rst
33 include tox.ini
4 include src/webargs/py.typed
5353
5454 pip install -U webargs
5555
56 webargs supports Python >= 3.6.
56 webargs supports Python >= 3.7.
5757
5858
5959 Documentation
105105 - Contributing Guidelines: https://webargs.readthedocs.io/en/latest/contributing.html
106106 - PyPI: https://pypi.python.org/pypi/webargs
107107 - Issues: https://github.com/marshmallow-code/webargs/issues
108 - Ecosystem / related packages: https://github.com/marshmallow-code/webargs/wiki/Ecosystem
108109
109110
110111 License
+0
-44
azure-pipelines.yml less more
0 trigger:
1 branches:
2 include: [dev, test-me-*]
3 tags:
4 include: ['*']
5
6 # Run builds nightly to catch incompatibilities with new marshmallow releases
7 schedules:
8 - cron: "0 0 * * *"
9 displayName: Daily midnight build
10 branches:
11 include:
12 - dev
13 always: "true"
14
15 resources:
16 repositories:
17 - repository: sloria
18 type: github
19 endpoint: github
20 name: sloria/azure-pipeline-templates
21 ref: refs/heads/sloria
22
23 jobs:
24 - template: job--python-tox.yml@sloria
25 parameters:
26 toxenvs:
27 - lint
28 - mypy
29 - py36
30 - py36-mindeps
31 - py37
32 - py38
33 - py39
34 - py39-marshmallowdev
35 - docs
36 os: linux
37 # Build wheels
38 - template: job--pypi-release.yml@sloria
39 parameters:
40 python: "3.9"
41 distributions: "sdist bdist_wheel"
42 dependsOn:
43 - tox_linux
0 python-webargs (8.2.0-0kali1) UNRELEASED; urgency=low
1
2 * New upstream release.
3
4 -- Kali Janitor <[email protected]> Mon, 26 Sep 2022 16:24:39 -0000
5
06 python-webargs (7.0.1-0kali1) kali-dev; urgency=medium
17
28 [ Kali Janitor ]
Binary diff not shown
109109
110110 @use_args(UserSchema())
111111 def profile_view(args):
112 username = args["userame"]
112 username = args["username"]
113113 # ...
114114
115115
148148 the `unknown` argument to `fields.Nested`.
149149
150150 Default `unknown`
151 +++++++++++++++++
151 ~~~~~~~~~~~~~~~~~
152152
153153 By default, webargs will pass `unknown=marshmallow.EXCLUDE` except when the
154 location is `json`, `form`, `json_or_form`, `path`, or `path`. In those cases,
155 it uses `unknown=marshmallow.RAISE` instead.
154 location is `json`, `form`, `json_or_form`, or `path`. In those cases, it uses
155 `unknown=marshmallow.RAISE` instead.
156156
157157 You can change these defaults by overriding `DEFAULT_UNKNOWN_BY_LOCATION`.
158158 This is a mapping of locations to values to pass.
179179 # so EXCLUDE will be used
180180 @app.route("/", methods=["GET"])
181181 @parser.use_args({"foo": fields.Int()}, location="query")
182 def get(self, args):
182 def get(args):
183183 return f"foo x 2 = {args['foo'] * 2}"
184184
185185
187187 # so no value will be passed for `unknown`
188188 @app.route("/", methods=["POST"])
189189 @parser.use_args({"foo": fields.Int(), "bar": fields.Int()}, location="json")
190 def post(self, args):
190 def post(args):
191191 return f"foo x bar = {args['foo'] * args['bar']}"
192192
193193
204204 # effect and `INCLUDE` will always be used
205205 @app.route("/", methods=["POST"])
206206 @parser.use_args({"foo": fields.Int(), "bar": fields.Int()}, location="json")
207 def post(self, args):
207 def post(args):
208208 unexpected_args = [k for k in args.keys() if k not in ("foo", "bar")]
209209 return f"foo x bar = {args['foo'] * args['bar']}; unexpected args={unexpected_args}"
210210
211211 Using Schema-Specfied `unknown`
212 +++++++++++++++++++++++++++++++
212 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
213213
214214 If you wish to use the value of `unknown` specified by a schema, simply pass
215215 ``unknown=None``. This will disable webargs' automatic passing of values for
236236 # as a result, the schema's behavior (EXCLUDE) is used
237237 @app.route("/", methods=["POST"])
238238 @use_args(RectangleSchema(), location="json", unknown=None)
239 def get(self, args):
239 def get(args):
240240 return f"area = {args['length'] * args['width']}"
241241
242242
274274
275275
276276 @use_args(RectangleSchema)
277 def post(self, rect: Rectangle):
277 def post(rect: Rectangle):
278278 return f"Area: {rect.length * rect.width}"
279279
280280 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.
329329
330330
331331 Reducing Boilerplate
332 ++++++++++++++++++++
332 ~~~~~~~~~~~~~~~~~~~~
333333
334334 We can reduce boilerplate and improve [re]usability with a simple helper function:
335335
369369 See the "Custom Fields" section of the marshmallow docs for a detailed guide on defining custom fields which you can pass to webargs parsers: https://marshmallow.readthedocs.io/en/latest/custom_fields.html.
370370
371371 Using ``Method`` and ``Function`` Fields with webargs
372 +++++++++++++++++++++++++++++++++++++++++++++++++++++
372 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
373373
374374 Using the :class:`Method <marshmallow.fields.Method>` and :class:`Function <marshmallow.fields.Function>` fields requires that you pass the ``deserialize`` parameter.
375375
434434 structure_dict_pair(r, k, v)
435435 return r
436436
437 Parser pre_load
438 ---------------
439
440 Similar to ``@pre_load`` decorated hooks on marshmallow Schemas,
441 :class:`Parser <webargs.core.Parser>` classes define a method,
442 `pre_load <webargs.core.Parser.pre_load>` which can
443 be overridden to provide per-parser transformations of data.
444 The only way to make use of `pre_load <webargs.core.Parser.pre_load>` is to
445 subclass a :class:`Parser <webargs.core.Parser>` and provide an
446 implementation.
447
448 `pre_load <webargs.core.Parser.pre_load>` is given the data fetched from a
449 location, the schema which will be used, the request object, and the location
450 name which was requested. For example, to define a ``FlaskParser`` which strips
451 whitespace from ``form`` and ``query`` data, one could write the following:
452
453 .. code-block:: python
454
455 from webargs.flaskparser import FlaskParser
456 import typing
457
458
459 def _strip_whitespace(value):
460 if isinstance(value, str):
461 value = value.strip()
462 elif isinstance(value, typing.Mapping):
463 return {k: _strip_whitespace(value[k]) for k in value}
464 elif isinstance(value, (list, tuple)):
465 return type(value)(map(_strip_whitespace, value))
466 return value
467
468
469 class WhitspaceStrippingFlaskParser(FlaskParser):
470 def pre_load(self, location_data, *, schema, req, location):
471 if location in ("query", "form"):
472 return _strip_whitespace(location_data)
473 return location_data
474
475 Note that `Parser.pre_load <webargs.core.Parser.pre_load>` is run after location
476 loading but before ``Schema.load`` is called. It can therefore be called on
477 multiple types of mapping objects, including
478 :class:`MultiDictProxy <webargs.MultiDictProxy>`, depending on what the
479 location loader returns.
480
437481 Returning HTTP 400 Responses
438482 ----------------------------
439483
492536 """
493537 # ...
494538
539 Multi-Field Detection
540 ---------------------
541
542 If a ``List`` field is used to parse data from a location like query parameters --
543 where one or multiple values can be passed for a single parameter name -- then
544 webargs will automatically treat that field as a list and parse multiple values
545 if present.
546
547 To implement this behavior, webargs will examine schemas for ``marshmallow.fields.List``
548 fields. ``List`` fields get unpacked to list values when data is loaded, and
549 other fields do not. This also applies to fields which inherit from ``List``.
550
551 .. note::
552
553 In webargs v8, ``Tuple`` will be treated this way as well, in addition to ``List``.
554
555 What if you have a list which should be treated as a "multi-field" but which
556 does not inherit from ``List``? webargs offers two solutions.
557 You can add the custom attribute `is_multiple=True` to your field or you
558 can add your class to your parser's list of `KNOWN_MULTI_FIELDS`.
559
560 First, let's define a "multiplexing field" which takes a string or list of
561 strings to serve as an example:
562
563 .. code-block:: python
564
565 # a custom field class which can accept values like List(String()) or String()
566 class CustomMultiplexingField(fields.String):
567 def _deserialize(self, value, attr, data, **kwargs):
568 if isinstance(value, str):
569 return super()._deserialize(value, attr, data, **kwargs)
570 return [
571 self._deserialize(v, attr, data, **kwargs)
572 for v in value
573 if isinstance(v, str)
574 ]
575
576 def _serialize(self, value, attr, **kwargs):
577 if isinstance(value, str):
578 return super()._serialize(value, attr, **kwargs)
579 return [self._serialize(v, attr, **kwargs) for v in value if isinstance(v, str)]
580
581
582 If you control the definition of ``CustomMultiplexingField``, you can just add
583 ``is_multiple=True`` to it:
584
585 .. code-block:: python
586
587 # option 1: define the field with is_multiple = True
588 from webargs.flaskparser import parser
589
590
591 class CustomMultiplexingField(fields.Field):
592 is_multiple = True # <----- this marks this as a multi-field
593
594 ... # as above
595
596 If you don't control the definition of ``CustomMultiplexingField``, for example
597 because it comes from a library, you can add it to the list of known
598 multifields:
599
600 .. code-block:: python
601
602 # option 2: add the field to the parer's list of multi-fields
603 class MyParser(FlaskParser):
604 KNOWN_MULTI_FIELDS = list(FlaskParser.KNOWN_MULTI_FIELDS) + [
605 CustomMultiplexingField
606 ]
607
608
609 parser = MyParser()
610
611 In either case, the end result is that you can use the multifield and it will
612 be detected as a list when unpacking query string data:
613
614 .. code-block:: python
615
616 # gracefully handles
617 # ...?foo=a
618 # ...?foo=a&foo=b
619 # and treats them as ["a"] and ["a", "b"] respectively
620 @parser.use_args({"foo": CustomMultiplexingField()}, location="query")
621 def show_foos(foo):
622 ...
623
624
495625 Mixing Locations
496626 ----------------
497627
0 import datetime as dt
10 import sys
21 import os
3 import sphinx_typlog_theme
2 import time
3 import datetime as dt
44
55 # If extensions (or modules to document with autodoc) are in another directory,
66 # add these directories to sys.path here. If the directory is relative to the
2828 "marshmallow": ("http://marshmallow.readthedocs.io/en/latest/", None),
2929 }
3030
31
32 # Use SOURCE_DATE_EPOCH for reproducible build output
33 # https://reproducible-builds.org/docs/source-date-epoch/
34 build_date = dt.datetime.utcfromtimestamp(
35 int(os.environ.get("SOURCE_DATE_EPOCH", time.time()))
36 )
37
3138 # The master toctree document.
3239 master_doc = "index"
33
3440 language = "en"
35
3641 html_domain_indices = False
3742 source_suffix = ".rst"
3843 project = "webargs"
39 copyright = f"2014-{dt.datetime.utcnow():%Y}, Steven Loria and contributors"
44 copyright = f"2014-{build_date:%Y}, Steven Loria and contributors"
4045 version = release = webargs.__version__
4146 templates_path = ["_templates"]
4247 exclude_patterns = ["_build"]
4348
4449 # THEME
4550
46 # Add any paths that contain custom themes here, relative to this directory.
47 html_theme = "sphinx_typlog_theme"
48 html_theme_path = [sphinx_typlog_theme.get_path()]
51 html_theme = "furo"
4952
5053 html_theme_options = {
51 "color": "#268bd2",
52 "logo_name": "webargs",
54 "light_css_variables": {"color-brand-primary": "#268bd2"},
5355 "description": "Declarative parsing and validation of HTTP request objects.",
54 "github_user": github_user,
55 "github_repo": github_repo,
5656 }
57 html_logo = "_static/logo.png"
5758
5859 html_context = {
5960 "tidelift_url": (
6263 ),
6364 "donate_url": "https://opencollective.com/marshmallow",
6465 }
65
6666 html_sidebars = {
67 "**": [
68 "logo.html",
69 "github.html",
70 "globaltoc.html",
67 "*": [
68 "sidebar/scroll-start.html",
69 "sidebar/brand.html",
70 "sidebar/search.html",
71 "sidebar/navigation.html",
7172 "donate.html",
72 "searchbox.html",
7373 "sponsors.html",
74 "sidebar/ethical-ads.html",
75 "sidebar/scroll-end.html",
7476 ]
7577 }
1212 Decorator Usage
1313 +++++++++++++++
1414
15 When using the :meth:`use_args <webargs.flaskparser.FlaskParser.use_args>` decorator, the arguments dictionary will be *before* any URL variable parameters.
15 When using the :meth:`use_args <webargs.flaskparser.FlaskParser.use_args>`
16 decorator, the arguments dictionary will be *before* any URL variable parameters.
1617
1718 .. code-block:: python
1819
00 Install
11 =======
22
3 **webargs** requires Python >= 3.6. It depends on `marshmallow <https://marshmallow.readthedocs.io/en/latest/>`_ >= 3.0.0.
3 **webargs** requires Python >= 3.7. It depends on `marshmallow <https://marshmallow.readthedocs.io/en/latest/>`_ >= 3.0.0.
44
55 From the PyPI
66 -------------
183183
184184 .. code-block:: python
185185
186 from webargs import flaskparser
187
188 parser = flaskparser.FlaskParser()
186 from webargs.flaskparser import parser
189187
190188
191189 class CustomError(Exception):
11 ===========================
22
33 This section documents migration paths to new releases.
4
5 Upgrading to 8.0
6 ++++++++++++++++
7
8 In 8.0, the default values for ``unknown`` were changed.
9 When the location is set to ``json``, ``form``, or ``json_or_form``, the
10 default for ``unknown`` is now ``None``. Previously, the default was ``RAISE``.
11
12 Because ``RAISE`` is the default value for ``unknown`` on marshmallow schemas,
13 this change only affects usage in which the following conditions are met:
14
15 * A schema with ``unknown`` set to ``INCLUDE`` or ``EXCLUDE`` is passed to
16 webargs ``use_args``, ``use_kwargs``, or ``parse``
17
18 * ``unknown`` is not passed explicitly to the webargs function
19
20 * ``location`` is not set (default of ``json``) or is set explicitly to
21 ``json``, ``form``, or ``json_or__form``
22
23 For example
24
25 .. code-block:: python
26
27 import marshmallow as ma
28
29
30 class BodySchema(ma.Schema):
31 foo = ma.fields.String()
32
33 class Meta:
34 unknown = ma.EXCLUDE
35
36
37 @parser.use_args(BodySchema)
38 def foo(data):
39 ...
40
41
42 In this case, under webargs 7.0 the schema ``unknown`` setting of ``EXCLUDE``
43 would be ignored. Instead, ``unknown=RAISE`` would be used.
44
45 In webargs 8.0, the schema ``unknown`` is used.
46
47 To get the webargs 7.0 behavior (overriding the Schema ``unknown``), simply
48 pass ``unknown`` to ``use_args``, as in
49
50 .. code-block:: python
51
52 @parser.use_args(BodySchema, unknown=ma.RAISE)
53 def foo(data):
54 ...
455
556 Upgrading to 7.0
657 ++++++++++++++++
436487 location=None,
437488 validate=None,
438489 error_status_code=None,
439 error_headers=None
490 error_headers=None,
440491 ):
441492 ...
442493
465516 as_kwargs=False,
466517 validate=None,
467518 error_status_code=None,
468 error_headers=None
519 error_headers=None,
469520 ):
470521 ...
471522
88 $ http GET :5001/
99 $ http GET :5001/ name==Ada
1010 $ http POST :5001/add x=40 y=2
11 $ http POST :5001/subtract x=40 y=2
1112 $ http POST :5001/dateadd value=1973-04-10 addend=63
1213 $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes
1314 """
3738 def add(x, y):
3839 """An addition endpoint."""
3940 return jsonify({"result": x + y})
41
42
43 @app.route("/subtract", methods=["POST"])
44 @use_kwargs(add_args)
45 async def subtract(x, y):
46 """An async subtraction endpoint."""
47 return jsonify({"result": x - y})
4048
4149
4250 dateadd_args = {
0 python-dateutil==2.8.1
0 python-dateutil==2.8.2
11 Flask
22 bottle
33 tornado
00 [tool.black]
11 line-length = 88
2 target-version = ['py35', 'py36', 'py37', 'py38']
2 target-version = ['py37', 'py38', 'py39', 'py310']
00 [metadata]
11 license_files = LICENSE
2
3 [bdist_wheel]
4 universal = 1
52
63 [flake8]
74 ignore = E203, E266, E501, W503
1313 "frameworks": FRAMEWORKS,
1414 "tests": [
1515 "pytest",
16 "webtest==2.0.35",
16 "pytest-asyncio",
17 "webtest==3.0.0",
1718 "webtest-aiohttp==2.0.0",
1819 "pytest-aiohttp>=0.3.0",
1920 ]
2021 + FRAMEWORKS,
2122 "lint": [
22 "mypy==0.790",
23 "flake8==3.8.4",
24 "flake8-bugbear==20.11.1",
23 "mypy==0.961",
24 "flake8==4.0.1",
25 "flake8-bugbear==22.7.1",
2526 "pre-commit~=2.4",
2627 ],
27 "docs": ["Sphinx==3.3.1", "sphinx-issues==1.2.0", "sphinx-typlog-theme==0.8.0"]
28 "docs": [
29 "Sphinx==5.0.2",
30 "sphinx-issues==3.0.1",
31 "furo==2022.6.21",
32 ]
2833 + FRAMEWORKS,
2934 }
3035 EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"]
6873 packages=find_packages("src"),
6974 package_dir={"": "src"},
7075 package_data={"webargs": ["py.typed"]},
71 install_requires=["marshmallow>=3.0.0"],
76 install_requires=["marshmallow>=3.0.0", "packaging"],
7277 extras_require=EXTRAS_REQUIRE,
7378 license="MIT",
7479 zip_safe=False,
8893 "api",
8994 "marshmallow",
9095 ),
91 python_requires=">=3.6",
96 python_requires=">=3.7.2",
9297 classifiers=[
9398 "Development Status :: 5 - Production/Stable",
9499 "Intended Audience :: Developers",
95100 "License :: OSI Approved :: MIT License",
96101 "Natural Language :: English",
97102 "Programming Language :: Python :: 3",
98 "Programming Language :: Python :: 3.6",
99103 "Programming Language :: Python :: 3.7",
100104 "Programming Language :: Python :: 3.8",
101105 "Programming Language :: Python :: 3.9",
106 "Programming Language :: Python :: 3.10",
102107 "Programming Language :: Python :: 3 :: Only",
103108 "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
104109 "Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
0 from distutils.version import LooseVersion
0 from __future__ import annotations
1
2 from packaging.version import Version
13 from marshmallow.utils import missing
24
35 # Make marshmallow's validation functions importable from webargs
68 from webargs.core import ValidationError
79 from webargs import fields
810
9 __version__ = "7.0.1"
10 __version_info__ = tuple(LooseVersion(__version__).version)
11 __version__ = "8.2.0"
12 __parsed_version__ = Version(__version__)
13 __version_info__: tuple[int, int, int] | tuple[
14 int, int, int, str, int
15 ] = __parsed_version__.release # type: ignore[assignment]
16 if __parsed_version__.pre:
17 __version_info__ += __parsed_version__.pre # type: ignore[assignment]
1118 __all__ = ("ValidationError", "fields", "missing", "validate")
2121 app = web.Application()
2222 app.router.add_route('GET', '/', index)
2323 """
24 from __future__ import annotations
25
2426 import typing
2527
2628 from aiohttp import web
7072 class AIOHTTPParser(AsyncParser):
7173 """aiohttp request argument parser."""
7274
73 DEFAULT_UNKNOWN_BY_LOCATION = {
75 DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = {
7476 "match_info": RAISE,
7577 "path": RAISE,
7678 **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION,
8385
8486 def load_querystring(self, req, schema: Schema) -> MultiDictProxy:
8587 """Return query params from the request as a MultiDictProxy."""
86 return MultiDictProxy(req.query, schema)
88 return self._makeproxy(req.query, schema)
8789
8890 async def load_form(self, req, schema: Schema) -> MultiDictProxy:
8991 """Return form values from the request as a MultiDictProxy."""
9092 post_data = await req.post()
91 return MultiDictProxy(post_data, schema)
93 return self._makeproxy(post_data, schema)
9294
93 async def load_json_or_form(
94 self, req, schema: Schema
95 ) -> typing.Union[typing.Dict, MultiDictProxy]:
95 async def load_json_or_form(self, req, schema: Schema) -> dict | MultiDictProxy:
9696 data = await self.load_json(req, schema)
9797 if data is not core.missing:
9898 return data
113113
114114 def load_headers(self, req, schema: Schema) -> MultiDictProxy:
115115 """Return headers from the request as a MultiDictProxy."""
116 return MultiDictProxy(req.headers, schema)
116 return self._makeproxy(req.headers, schema)
117117
118118 def load_cookies(self, req, schema: Schema) -> MultiDictProxy:
119119 """Return cookies from the request as a MultiDictProxy."""
120 return MultiDictProxy(req.cookies, schema)
120 return self._makeproxy(req.cookies, schema)
121121
122122 def load_files(self, req, schema: Schema) -> typing.NoReturn:
123123 raise NotImplementedError(
153153 req,
154154 schema: Schema,
155155 *,
156 error_status_code: typing.Optional[int],
157 error_headers: typing.Optional[typing.Mapping[str, str]]
156 error_status_code: int | None,
157 error_headers: typing.Mapping[str, str] | None,
158158 ) -> typing.NoReturn:
159159 """Handle ValidationErrors and return a JSON response of error messages
160160 to the client.
166166 raise LookupError(f"No exception for {error_status_code}")
167167 headers = error_headers
168168 raise error_class(
169 body=json.dumps(error.messages).encode("utf-8"),
169 text=json.dumps(error.messages),
170170 headers=headers,
171171 content_type="application/json",
172172 )
173173
174174 def _handle_invalid_json_error(
175 self,
176 error: typing.Union[json.JSONDecodeError, UnicodeDecodeError],
177 req,
178 *args,
179 **kwargs
175 self, error: json.JSONDecodeError | UnicodeDecodeError, req, *args, **kwargs
180176 ) -> typing.NoReturn:
181177 error_class = exception_map[400]
182178 messages = {"json": ["Invalid JSON body."]}
183 raise error_class(
184 body=json.dumps(messages).encode("utf-8"), content_type="application/json"
185 )
179 raise error_class(text=json.dumps(messages), content_type="application/json")
186180
187181
188182 parser = AIOHTTPParser()
00 """Asynchronous request parser."""
1 import asyncio
2 import functools
3 import inspect
1 from __future__ import annotations
2
43 import typing
5 from collections.abc import Mapping
6
7 from marshmallow import Schema, ValidationError
8 import marshmallow as ma
94
105 from webargs import core
116
12 AsyncErrorHandler = typing.Callable[..., typing.Awaitable[typing.NoReturn]]
13
147
158 class AsyncParser(core.Parser):
16 """Asynchronous variant of `webargs.core.Parser`, where parsing methods may be
17 either coroutines or regular methods.
9 """Asynchronous variant of `webargs.core.Parser`.
10
11 The ``parse`` method is redefined to be ``async``.
1812 """
1913
20 # TODO: Lots of duplication from core.Parser here. Rethink.
2114 async def parse(
2215 self,
2316 argmap: core.ArgMap,
24 req: typing.Optional[core.Request] = None,
17 req: core.Request | None = None,
2518 *,
26 location: typing.Optional[str] = None,
27 unknown: typing.Optional[str] = core._UNKNOWN_DEFAULT_PARAM,
19 location: str | None = None,
20 unknown: str | None = core._UNKNOWN_DEFAULT_PARAM,
2821 validate: core.ValidateArg = None,
29 error_status_code: typing.Optional[int] = None,
30 error_headers: typing.Optional[typing.Mapping[str, str]] = None
31 ) -> typing.Optional[typing.Mapping]:
22 error_status_code: int | None = None,
23 error_headers: typing.Mapping[str, str] | None = None,
24 ) -> typing.Mapping | None:
3225 """Coroutine variant of `webargs.core.Parser`.
3326
3427 Receives the same arguments as `webargs.core.Parser.parse`.
3528 """
36 req = req if req is not None else self.get_default_request()
37 location = location or self.location
38 unknown = (
39 unknown
40 if unknown != core._UNKNOWN_DEFAULT_PARAM
41 else (
42 self.unknown
43 if self.unknown != core._UNKNOWN_DEFAULT_PARAM
44 else self.DEFAULT_UNKNOWN_BY_LOCATION.get(location)
45 )
29 data = await self.async_parse(
30 argmap,
31 req,
32 location=location,
33 unknown=unknown,
34 validate=validate,
35 error_status_code=error_status_code,
36 error_headers=error_headers,
4637 )
47 load_kwargs: typing.Dict[str, typing.Any] = (
48 {"unknown": unknown} if unknown else {}
49 )
50 if req is None:
51 raise ValueError("Must pass req object")
52 data = None
53 validators = core._ensure_list_of_callables(validate)
54 schema = self._get_schema(argmap, req)
55 try:
56 location_data = await self._load_location_data(
57 schema=schema, req=req, location=location
58 )
59 data = schema.load(location_data, **load_kwargs)
60 self._validate_arguments(data, validators)
61 except ma.exceptions.ValidationError as error:
62 await self._async_on_validation_error(
63 error,
64 req,
65 schema,
66 location,
67 error_status_code=error_status_code,
68 error_headers=error_headers,
69 )
7038 return data
71
72 async def _load_location_data(self, schema, req, location):
73 """Return a dictionary-like object for the location on the given request.
74
75 Needs to have the schema in hand in order to correctly handle loading
76 lists from multidict objects and `many=True` schemas.
77 """
78 loader_func = self._get_loader(location)
79 if asyncio.iscoroutinefunction(loader_func):
80 data = await loader_func(req, schema)
81 else:
82 data = loader_func(req, schema)
83
84 # when the desired location is empty (no data), provide an empty
85 # dict as the default so that optional arguments in a location
86 # (e.g. optional JSON body) work smoothly
87 if data is core.missing:
88 data = {}
89 return data
90
91 async def _async_on_validation_error(
92 self,
93 error: ValidationError,
94 req: core.Request,
95 schema: Schema,
96 location: str,
97 *,
98 error_status_code: typing.Optional[int],
99 error_headers: typing.Optional[typing.Mapping[str, str]]
100 ) -> typing.NoReturn:
101 # rewrite messages to be namespaced under the location which created
102 # them
103 # e.g. {"json":{"foo":["Not a valid integer."]}}
104 # instead of
105 # {"foo":["Not a valid integer."]}
106 error.messages = {location: error.messages}
107 error_handler = self.error_callback or self.handle_error
108 # an async error handler was registered, await it
109 if inspect.iscoroutinefunction(error_handler):
110 async_error_handler = typing.cast(AsyncErrorHandler, error_handler)
111 await async_error_handler(
112 error,
113 req,
114 schema,
115 error_status_code=error_status_code,
116 error_headers=error_headers,
117 )
118 # workaround for mypy not understanding `await Awaitable[NoReturn]`
119 # see: https://github.com/python/mypy/issues/8974
120 raise NotImplementedError("unreachable")
121 # the error handler was synchronous (e.g. Parser.handle_error) so it
122 # will raise an error
123 else:
124 error_handler(
125 error,
126 req,
127 schema,
128 error_status_code=error_status_code,
129 error_headers=error_headers,
130 )
131
132 def use_args(
133 self,
134 argmap: core.ArgMap,
135 req: typing.Optional[core.Request] = None,
136 *,
137 location: str = None,
138 unknown=core._UNKNOWN_DEFAULT_PARAM,
139 as_kwargs: bool = False,
140 validate: core.ValidateArg = None,
141 error_status_code: typing.Optional[int] = None,
142 error_headers: typing.Optional[typing.Mapping[str, str]] = None
143 ) -> typing.Callable[..., typing.Callable]:
144 """Decorator that injects parsed arguments into a view function or method.
145
146 Receives the same arguments as `webargs.core.Parser.use_args`.
147 """
148 location = location or self.location
149 request_obj = req
150 # Optimization: If argmap is passed as a dictionary, we only need
151 # to generate a Schema once
152 if isinstance(argmap, Mapping):
153 argmap = self.schema_class.from_dict(argmap)()
154
155 def decorator(func: typing.Callable) -> typing.Callable:
156 req_ = request_obj
157
158 if inspect.iscoroutinefunction(func):
159
160 @functools.wraps(func)
161 async def wrapper(*args, **kwargs):
162 req_obj = req_
163
164 if not req_obj:
165 req_obj = self.get_request_from_view_args(func, args, kwargs)
166 # NOTE: At this point, argmap may be a Schema, callable, or dict
167 parsed_args = await self.parse(
168 argmap,
169 req=req_obj,
170 location=location,
171 unknown=unknown,
172 validate=validate,
173 error_status_code=error_status_code,
174 error_headers=error_headers,
175 )
176 args, kwargs = self._update_args_kwargs(
177 args, kwargs, parsed_args, as_kwargs
178 )
179 return await func(*args, **kwargs)
180
181 else:
182
183 @functools.wraps(func) # type: ignore
184 def wrapper(*args, **kwargs):
185 req_obj = req_
186
187 if not req_obj:
188 req_obj = self.get_request_from_view_args(func, args, kwargs)
189 # NOTE: At this point, argmap may be a Schema, callable, or dict
190 parsed_args = yield from self.parse( # type: ignore
191 argmap,
192 req=req_obj,
193 location=location,
194 unknown=unknown,
195 validate=validate,
196 error_status_code=error_status_code,
197 error_headers=error_headers,
198 )
199 args, kwargs = self._update_args_kwargs(
200 args, kwargs, parsed_args, as_kwargs
201 )
202 return func(*args, **kwargs)
203
204 return wrapper
205
206 return decorator
1818 import bottle
1919
2020 from webargs import core
21 from webargs.multidictproxy import MultiDictProxy
2221
2322
2423 class BottleParser(core.Parser):
4847
4948 def load_querystring(self, req, schema):
5049 """Return query params from the request as a MultiDictProxy."""
51 return MultiDictProxy(req.query, schema)
50 return self._makeproxy(req.query, schema)
5251
5352 def load_form(self, req, schema):
5453 """Return form values from the request as a MultiDictProxy."""
5756 # TODO: Make this check more specific
5857 if core.is_json(req.content_type):
5958 return core.missing
60 return MultiDictProxy(req.forms, schema)
59 return self._makeproxy(req.forms, schema)
6160
6261 def load_headers(self, req, schema):
6362 """Return headers from the request as a MultiDictProxy."""
64 return MultiDictProxy(req.headers, schema)
63 return self._makeproxy(req.headers, schema)
6564
6665 def load_cookies(self, req, schema):
6766 """Return cookies from the request."""
6968
7069 def load_files(self, req, schema):
7170 """Return files from the request as a MultiDictProxy."""
72 return MultiDictProxy(req.files, schema)
71 return self._makeproxy(req.files, schema)
7372
7473 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
7574 """Handles errors during parsing. Aborts the current request with a
0 from __future__ import annotations
1
2 import asyncio
03 import functools
14 import typing
25 import logging
3 from collections.abc import Mapping
46 import json
57
68 import marshmallow as ma
79 from marshmallow import ValidationError
810 from marshmallow.utils import missing
911
10 from webargs.fields import DelimitedList
12 from webargs.multidictproxy import MultiDictProxy
1113
1214 logger = logging.getLogger(__name__)
1315
1416
1517 __all__ = [
1618 "ValidationError",
17 "is_multiple",
1819 "Parser",
1920 "missing",
2021 "parse_json",
2425 Request = typing.TypeVar("Request")
2526 ArgMap = typing.Union[
2627 ma.Schema,
27 typing.Mapping[str, ma.fields.Field],
28 typing.Dict[str, typing.Union[ma.fields.Field, typing.Type[ma.fields.Field]]],
2829 typing.Callable[[Request], ma.Schema],
2930 ]
3031 ValidateArg = typing.Union[None, typing.Callable, typing.Iterable[typing.Callable]]
3233 ErrorHandler = typing.Callable[..., typing.NoReturn]
3334 # generic type var with no particular meaning
3435 T = typing.TypeVar("T")
36 # type var for callables, to make type-preserving decorators
37 C = typing.TypeVar("C", bound=typing.Callable)
38 # type var for a callable which is an error handler
39 # used to ensure that the error_handler decorator is type preserving
40 ErrorHandlerT = typing.TypeVar("ErrorHandlerT", bound=ErrorHandler)
41
42 AsyncErrorHandler = typing.Callable[..., typing.Awaitable[typing.NoReturn]]
3543
3644
3745 # a value used as the default for arguments, so that when `None` is passed, it
4755 return callable(x)
4856
4957
50 def _callable_or_raise(obj: typing.Optional[T]) -> typing.Optional[T]:
58 def _callable_or_raise(obj: T | None) -> T | None:
5159 """Makes sure an object is callable if it is not ``None``. If not
5260 callable, a ValueError is raised.
5361 """
5664 return obj
5765
5866
59 def is_multiple(field: ma.fields.Field) -> bool:
60 """Return whether or not `field` handles repeated/multi-value arguments."""
61 return isinstance(field, ma.fields.List) and not isinstance(field, DelimitedList)
62
63
6467 def get_mimetype(content_type: str) -> str:
6568 return content_type.split(";")[0].strip()
6669
6770
6871 # Adapted from werkzeug:
6972 # https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/wrappers.py
70 def is_json(mimetype: typing.Optional[str]) -> bool:
73 def is_json(mimetype: str | None) -> bool:
7174 """Indicates if this mimetype is JSON or not. By default a request
7275 is considered to include JSON data if the mimetype is
7376 ``application/json`` or ``application/*+json``.
9497 f"Bytes decoding error : {exc.reason}",
9598 doc=str(exc.object),
9699 pos=exc.start,
97 )
100 ) from exc
98101 return json.loads(decoded)
99102
100103
131134 DEFAULT_LOCATION: str = "json"
132135 #: Default value to use for 'unknown' on schema load
133136 # on a per-location basis
134 DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, str] = {
135 "json": ma.RAISE,
136 "form": ma.RAISE,
137 "json_or_form": ma.RAISE,
137 DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = {
138 "json": None,
139 "form": None,
140 "json_or_form": None,
138141 "querystring": ma.EXCLUDE,
139142 "query": ma.EXCLUDE,
140143 "headers": ma.EXCLUDE,
142145 "files": ma.EXCLUDE,
143146 }
144147 #: The marshmallow Schema class to use when creating new schemas
145 DEFAULT_SCHEMA_CLASS: typing.Type = ma.Schema
148 DEFAULT_SCHEMA_CLASS: type[ma.Schema] = ma.Schema
146149 #: Default status code to return for validation errors
147150 DEFAULT_VALIDATION_STATUS: int = DEFAULT_VALIDATION_STATUS
148151 #: Default error message for validation errors
149152 DEFAULT_VALIDATION_MESSAGE: str = "Invalid value."
153 #: field types which should always be treated as if they set `is_multiple=True`
154 KNOWN_MULTI_FIELDS: list[type] = [ma.fields.List, ma.fields.Tuple]
150155
151156 #: Maps location => method name
152 __location_map__: typing.Dict[str, typing.Union[str, typing.Callable]] = {
157 __location_map__: dict[str, str | typing.Callable] = {
153158 "json": "load_json",
154159 "querystring": "load_querystring",
155160 "query": "load_querystring",
162167
163168 def __init__(
164169 self,
165 location: typing.Optional[str] = None,
170 location: str | None = None,
166171 *,
167 unknown: typing.Optional[str] = _UNKNOWN_DEFAULT_PARAM,
168 error_handler: typing.Optional[ErrorHandler] = None,
169 schema_class: typing.Optional[typing.Type] = None
172 unknown: str | None = _UNKNOWN_DEFAULT_PARAM,
173 error_handler: ErrorHandler | None = None,
174 schema_class: type[ma.Schema] | None = None,
170175 ):
171176 self.location = location or self.DEFAULT_LOCATION
172 self.error_callback: typing.Optional[ErrorHandler] = _callable_or_raise(
173 error_handler
174 )
177 self.error_callback: ErrorHandler | None = _callable_or_raise(error_handler)
175178 self.schema_class = schema_class or self.DEFAULT_SCHEMA_CLASS
176179 self.unknown = unknown
180
181 def _makeproxy(self, multidict, schema: ma.Schema, cls: type = MultiDictProxy):
182 """Create a multidict proxy object with options from the current parser"""
183 return cls(multidict, schema, known_multi_fields=tuple(self.KNOWN_MULTI_FIELDS))
177184
178185 def _get_loader(self, location: str) -> typing.Callable:
179186 """Get the loader function for the given location.
193200
194201 def _load_location_data(
195202 self, *, schema: ma.Schema, req: Request, location: str
196 ) -> typing.Mapping:
203 ) -> typing.Any:
197204 """Return a dictionary-like object for the location on the given request.
198205
199206 Needs to have the schema in hand in order to correctly handle loading
200207 lists from multidict objects and `many=True` schemas.
201208 """
202209 loader_func = self._get_loader(location)
203 data = loader_func(req, schema)
204 # when the desired location is empty (no data), provide an empty
205 # dict as the default so that optional arguments in a location
206 # (e.g. optional JSON body) work smoothly
207 if data is missing:
208 data = {}
210 return loader_func(req, schema)
211
212 async def _async_load_location_data(self, schema, req, location):
213 # an async variant of the _load_location_data method
214 # the loader function itself may or may not be async
215 loader_func = self._get_loader(location)
216 if asyncio.iscoroutinefunction(loader_func):
217 data = await loader_func(req, schema)
218 else:
219 data = loader_func(req, schema)
209220 return data
210221
211222 def _on_validation_error(
215226 schema: ma.Schema,
216227 location: str,
217228 *,
218 error_status_code: typing.Optional[int],
219 error_headers: typing.Optional[typing.Mapping[str, str]]
229 error_status_code: int | None,
230 error_headers: typing.Mapping[str, str] | None,
220231 ) -> typing.NoReturn:
221232 # rewrite messages to be namespaced under the location which created
222233 # them
233244 error_headers=error_headers,
234245 )
235246
247 async def _async_on_validation_error(
248 self,
249 error: ValidationError,
250 req: Request,
251 schema: ma.Schema,
252 location: str,
253 *,
254 error_status_code: int | None,
255 error_headers: typing.Mapping[str, str] | None,
256 ) -> typing.NoReturn:
257 # an async-aware variant of the _on_validation_error method
258 error.messages = {location: error.messages}
259 error_handler = self.error_callback or self.handle_error
260 # an async error handler was registered, await it
261 if asyncio.iscoroutinefunction(error_handler):
262 async_error_handler = typing.cast(AsyncErrorHandler, error_handler)
263 await async_error_handler(
264 error,
265 req,
266 schema,
267 error_status_code=error_status_code,
268 error_headers=error_headers,
269 )
270 # the error handler was synchronous (e.g. Parser.handle_error) so it
271 # will raise an error
272 else:
273 error_handler(
274 error,
275 req,
276 schema,
277 error_status_code=error_status_code,
278 error_headers=error_headers,
279 )
280
236281 def _validate_arguments(self, data: typing.Any, validators: CallableList) -> None:
237282 # although `data` is typically a Mapping, nothing forbids a `schema.load`
238283 # from returning an arbitrary object subject to validators
256301 schema = argmap()
257302 elif callable(argmap):
258303 schema = argmap(req)
304 elif isinstance(argmap, dict):
305 schema = self.schema_class.from_dict(argmap)()
259306 else:
260 schema = self.schema_class.from_dict(argmap)()
307 raise TypeError(f"argmap was of unexpected type {type(argmap)}")
261308 return schema
262309
310 def _prepare_for_parse(
311 self,
312 argmap: ArgMap,
313 req: Request | None = None,
314 location: str | None = None,
315 unknown: str | None = _UNKNOWN_DEFAULT_PARAM,
316 validate: ValidateArg = None,
317 ) -> tuple[None, Request, str, CallableList, ma.Schema]:
318 # validate parse() arguments and handle defaults
319 # (shared between sync and async variants)
320 req = req if req is not None else self.get_default_request()
321 if req is None:
322 raise ValueError("Must pass req object")
323 location = location or self.location
324 validators = _ensure_list_of_callables(validate)
325 schema = self._get_schema(argmap, req)
326 return (None, req, location, validators, schema)
327
328 def _process_location_data(
329 self,
330 location_data: typing.Any,
331 schema: ma.Schema,
332 req: Request,
333 location: str,
334 unknown: str | None,
335 validators: CallableList,
336 ):
337 # after the data has been fetched from a registered location,
338 # this is how it is processed
339 # (shared between sync and async variants)
340
341 # when the desired location is empty (no data), provide an empty
342 # dict as the default so that optional arguments in a location
343 # (e.g. optional JSON body) work smoothly
344 if location_data is missing:
345 location_data = {}
346
347 # precedence order: explicit, instance setting, default per location
348 unknown = (
349 unknown
350 if unknown != _UNKNOWN_DEFAULT_PARAM
351 else (
352 self.unknown
353 if self.unknown != _UNKNOWN_DEFAULT_PARAM
354 else self.DEFAULT_UNKNOWN_BY_LOCATION.get(location)
355 )
356 )
357 load_kwargs: dict[str, typing.Any] = {"unknown": unknown} if unknown else {}
358 preprocessed_data = self.pre_load(
359 location_data, schema=schema, req=req, location=location
360 )
361 data = schema.load(preprocessed_data, **load_kwargs)
362 self._validate_arguments(data, validators)
363 return data
364
263365 def parse(
264366 self,
265367 argmap: ArgMap,
266 req: typing.Optional[Request] = None,
368 req: Request | None = None,
267369 *,
268 location: typing.Optional[str] = None,
269 unknown: typing.Optional[str] = _UNKNOWN_DEFAULT_PARAM,
370 location: str | None = None,
371 unknown: str | None = _UNKNOWN_DEFAULT_PARAM,
270372 validate: ValidateArg = None,
271 error_status_code: typing.Optional[int] = None,
272 error_headers: typing.Optional[typing.Mapping[str, str]] = None
373 error_status_code: int | None = None,
374 error_headers: typing.Mapping[str, str] | None = None,
273375 ):
274376 """Main request parsing method.
275377
295397
296398 :return: A dictionary of parsed arguments
297399 """
298 req = req if req is not None else self.get_default_request()
299 location = location or self.location
300 # precedence order: explicit, instance setting, default per location
301 unknown = (
302 unknown
303 if unknown != _UNKNOWN_DEFAULT_PARAM
304 else (
305 self.unknown
306 if self.unknown != _UNKNOWN_DEFAULT_PARAM
307 else self.DEFAULT_UNKNOWN_BY_LOCATION.get(location)
308 )
400 data, req, location, validators, schema = self._prepare_for_parse(
401 argmap, req, location, unknown, validate
309402 )
310 load_kwargs: typing.Dict[str, typing.Any] = (
311 {"unknown": unknown} if unknown else {}
312 )
313 if req is None:
314 raise ValueError("Must pass req object")
315 data = None
316 validators = _ensure_list_of_callables(validate)
317 schema = self._get_schema(argmap, req)
318403 try:
319404 location_data = self._load_location_data(
320405 schema=schema, req=req, location=location
321406 )
322 data = schema.load(location_data, **load_kwargs)
323 self._validate_arguments(data, validators)
407 data = self._process_location_data(
408 location_data, schema, req, location, unknown, validators
409 )
324410 except ma.exceptions.ValidationError as error:
325411 self._on_validation_error(
326412 error,
335421 ) from error
336422 return data
337423
338 def get_default_request(self) -> typing.Optional[Request]:
424 async def async_parse(
425 self,
426 argmap: ArgMap,
427 req: Request | None = None,
428 *,
429 location: str | None = None,
430 unknown: str | None = _UNKNOWN_DEFAULT_PARAM,
431 validate: ValidateArg = None,
432 error_status_code: int | None = None,
433 error_headers: typing.Mapping[str, str] | None = None,
434 ) -> typing.Mapping | None:
435 """Coroutine variant of `webargs.core.Parser.parse`.
436
437 Receives the same arguments as `webargs.core.Parser.parse`.
438 """
439 data, req, location, validators, schema = self._prepare_for_parse(
440 argmap, req, location, unknown, validate
441 )
442 try:
443 location_data = await self._async_load_location_data(
444 schema=schema, req=req, location=location
445 )
446 data = self._process_location_data(
447 location_data, schema, req, location, unknown, validators
448 )
449 except ma.exceptions.ValidationError as error:
450 await self._async_on_validation_error(
451 error,
452 req,
453 schema,
454 location,
455 error_status_code=error_status_code,
456 error_headers=error_headers,
457 )
458 raise ValueError(
459 "_on_validation_error hook did not raise an exception"
460 ) from error
461 return data
462
463 def get_default_request(self) -> Request | None:
339464 """Optional override. Provides a hook for frameworks that use thread-local
340465 request objects.
341466 """
344469 def get_request_from_view_args(
345470 self,
346471 view: typing.Callable,
347 args: typing.Tuple,
472 args: tuple,
348473 kwargs: typing.Mapping[str, typing.Any],
349 ) -> typing.Optional[Request]:
474 ) -> Request | None:
350475 """Optional override. Returns the request object to be parsed, given a view
351476 function's args and kwargs.
352477
362487
363488 @staticmethod
364489 def _update_args_kwargs(
365 args: typing.Tuple,
366 kwargs: typing.Dict[str, typing.Any],
367 parsed_args: typing.Tuple,
490 args: tuple,
491 kwargs: dict[str, typing.Any],
492 parsed_args: tuple,
368493 as_kwargs: bool,
369 ) -> typing.Tuple[typing.Tuple, typing.Mapping]:
494 ) -> tuple[tuple, typing.Mapping]:
370495 """Update args or kwargs with parsed_args depending on as_kwargs"""
371496 if as_kwargs:
372497 kwargs.update(parsed_args)
378503 def use_args(
379504 self,
380505 argmap: ArgMap,
381 req: typing.Optional[Request] = None,
506 req: Request | None = None,
382507 *,
383 location: typing.Optional[str] = None,
384 unknown: typing.Optional[str] = _UNKNOWN_DEFAULT_PARAM,
508 location: str | None = None,
509 unknown: str | None = _UNKNOWN_DEFAULT_PARAM,
385510 as_kwargs: bool = False,
386511 validate: ValidateArg = None,
387 error_status_code: typing.Optional[int] = None,
388 error_headers: typing.Optional[typing.Mapping[str, str]] = None
512 error_status_code: int | None = None,
513 error_headers: typing.Mapping[str, str] | None = None,
389514 ) -> typing.Callable[..., typing.Callable]:
390515 """Decorator that injects parsed arguments into a view function or method.
391516
415540 request_obj = req
416541 # Optimization: If argmap is passed as a dictionary, we only need
417542 # to generate a Schema once
418 if isinstance(argmap, Mapping):
543 if isinstance(argmap, dict):
419544 argmap = self.schema_class.from_dict(argmap)()
420545
421 def decorator(func):
546 def decorator(func: typing.Callable) -> typing.Callable:
422547 req_ = request_obj
423548
424 @functools.wraps(func)
425 def wrapper(*args, **kwargs):
426 req_obj = req_
427
428 if not req_obj:
429 req_obj = self.get_request_from_view_args(func, args, kwargs)
430
431 # NOTE: At this point, argmap may be a Schema, or a callable
432 parsed_args = self.parse(
433 argmap,
434 req=req_obj,
435 location=location,
436 unknown=unknown,
437 validate=validate,
438 error_status_code=error_status_code,
439 error_headers=error_headers,
440 )
441 args, kwargs = self._update_args_kwargs(
442 args, kwargs, parsed_args, as_kwargs
443 )
444 return func(*args, **kwargs)
445
446 wrapper.__wrapped__ = func
549 if asyncio.iscoroutinefunction(func):
550
551 @functools.wraps(func)
552 async def wrapper(*args, **kwargs):
553 req_obj = req_
554
555 if not req_obj:
556 req_obj = self.get_request_from_view_args(func, args, kwargs)
557 # NOTE: At this point, argmap may be a Schema, callable, or dict
558 parsed_args = await self.async_parse(
559 argmap,
560 req=req_obj,
561 location=location,
562 unknown=unknown,
563 validate=validate,
564 error_status_code=error_status_code,
565 error_headers=error_headers,
566 )
567 args, kwargs = self._update_args_kwargs(
568 args, kwargs, parsed_args, as_kwargs
569 )
570 return await func(*args, **kwargs)
571
572 else:
573
574 @functools.wraps(func) # type: ignore
575 def wrapper(*args, **kwargs):
576 req_obj = req_
577
578 if not req_obj:
579 req_obj = self.get_request_from_view_args(func, args, kwargs)
580 # NOTE: At this point, argmap may be a Schema, callable, or dict
581 parsed_args = self.parse(
582 argmap,
583 req=req_obj,
584 location=location,
585 unknown=unknown,
586 validate=validate,
587 error_status_code=error_status_code,
588 error_headers=error_headers,
589 )
590 args, kwargs = self._update_args_kwargs(
591 args, kwargs, parsed_args, as_kwargs
592 )
593 return func(*args, **kwargs)
594
595 wrapper.__wrapped__ = func # type: ignore
447596 return wrapper
448597
449598 return decorator
466615 kwargs["as_kwargs"] = True
467616 return self.use_args(*args, **kwargs)
468617
469 def location_loader(self, name: str):
618 def location_loader(self, name: str) -> typing.Callable[[C], C]:
470619 """Decorator that registers a function for loading a request location.
471620 The wrapped function receives a schema and a request.
472621
487636 :param str name: The name of the location to register.
488637 """
489638
490 def decorator(func):
639 def decorator(func: C) -> C:
491640 self.__location_map__[name] = func
492641 return func
493642
494643 return decorator
495644
496 def error_handler(self, func: ErrorHandler) -> ErrorHandler:
645 def error_handler(self, func: ErrorHandlerT) -> ErrorHandlerT:
497646 """Decorator that registers a custom error handling function. The
498647 function should receive the raised error, request object,
499648 `marshmallow.Schema` instance used to parse the request, error status code,
520669 self.error_callback = func
521670 return func
522671
672 def pre_load(
673 self,
674 location_data: typing.Mapping,
675 *,
676 schema: ma.Schema,
677 req: Request,
678 location: str,
679 ) -> typing.Mapping:
680 """A method of the parser which can transform data after location
681 loading is done. By default it does nothing, but users can subclass
682 parsers and override this method.
683 """
684 return location_data
685
523686 def _handle_invalid_json_error(
524687 self,
525 error: typing.Union[json.JSONDecodeError, UnicodeDecodeError],
688 error: json.JSONDecodeError | UnicodeDecodeError,
526689 req: Request,
527690 *args,
528 **kwargs
691 **kwargs,
529692 ) -> typing.NoReturn:
530693 """Internal hook for overriding treatment of JSONDecodeErrors.
531694
619782 schema: ma.Schema,
620783 *,
621784 error_status_code: int,
622 error_headers: typing.Mapping[str, str]
785 error_headers: typing.Mapping[str, str],
623786 ) -> typing.NoReturn:
624787 """Called if an error occurs while parsing args. By default, just logs and
625788 raises ``error``.
1717 return HttpResponse('Hello ' + args['name'])
1818 """
1919 from webargs import core
20 from webargs.multidictproxy import MultiDictProxy
2120
2221
2322 def is_json_request(req):
4746
4847 def load_querystring(self, req, schema):
4948 """Return query params from the request as a MultiDictProxy."""
50 return MultiDictProxy(req.GET, schema)
49 return self._makeproxy(req.GET, schema)
5150
5251 def load_form(self, req, schema):
5352 """Return form values from the request as a MultiDictProxy."""
54 return MultiDictProxy(req.POST, schema)
53 return self._makeproxy(req.POST, schema)
5554
5655 def load_cookies(self, req, schema):
5756 """Return cookies from the request."""
6564
6665 def load_files(self, req, schema):
6766 """Return files from the request as a MultiDictProxy."""
68 return MultiDictProxy(req.FILES, schema)
67 return self._makeproxy(req.FILES, schema)
6968
7069 def get_request_from_view_args(self, view, args, kwargs):
7170 # The first argument is either `self` or `request`
55 import marshmallow as ma
66
77 from webargs import core
8 from webargs.multidictproxy import MultiDictProxy
98
109 HTTP_422 = "422 Unprocessable Entity"
1110
9695
9796 def load_querystring(self, req, schema):
9897 """Return query params from the request as a MultiDictProxy."""
99 return MultiDictProxy(req.params, schema)
98 return self._makeproxy(req.params, schema)
10099
101100 def load_form(self, req, schema):
102101 """Return form values from the request as a MultiDictProxy
108107 form = parse_form_body(req)
109108 if form is core.missing:
110109 return form
111 return MultiDictProxy(form, schema)
110 return self._makeproxy(form, schema)
112111
113112 def load_media(self, req, schema):
114113 """Return data unpacked and parsed by one of Falcon's media handlers.
1212 "content_type": fields.Str(data_key="Content-Type", location="headers"),
1313 }
1414 """
15 import typing
15 from __future__ import annotations
1616
1717 import marshmallow as ma
1818
1919 # Expose all fields from marshmallow.fields.
2020 from marshmallow.fields import * # noqa: F40
2121
22 __all__ = ["DelimitedList"] + ma.fields.__all__
22 __all__ = ["DelimitedList", "DelimitedTuple"] + ma.fields.__all__
2323
2424
25 # TODO: remove custom `Nested` in the next major release
26 #
27 # the `Nested` class is only needed on versions of marshmallow prior to v3.15.0
28 # in that version, `ma.fields.Nested` gained the ability to consume dict inputs
29 # prior to that, this subclass adds this capability
30 #
31 # if we drop support for ma.__version_info__ < (3, 15) we can do this
2532 class Nested(ma.fields.Nested): # type: ignore[no-redef]
2633 """Same as `marshmallow.fields.Nested`, except can be passed a dictionary as
2734 the first argument, which will be converted to a `marshmallow.Schema`.
5461 """
5562
5663 delimiter: str = ","
64 # delimited fields set is_multiple=False for webargs.core.is_multiple
65 is_multiple: bool = False
5766
5867 def _serialize(self, value, attr, obj, **kwargs):
5968 # serializing will start with parent-class serialization, so that we correctly
6675 # attempting to deserialize from a non-string source is an error
6776 if not isinstance(value, (str, bytes)):
6877 raise self.make_error("invalid")
69 return super()._deserialize(value.split(self.delimiter), attr, data, **kwargs)
78 values = value.split(self.delimiter) if value else []
79 return super()._deserialize(values, attr, data, **kwargs)
7080
7181
7282 class DelimitedList(DelimitedFieldMixin, ma.fields.List):
8494
8595 def __init__(
8696 self,
87 cls_or_instance: typing.Union[ma.fields.Field, type],
97 cls_or_instance: ma.fields.Field | type,
8898 *,
89 delimiter: typing.Optional[str] = None,
90 **kwargs
99 delimiter: str | None = None,
100 **kwargs,
91101 ):
92102 self.delimiter = delimiter or self.delimiter
93103 super().__init__(cls_or_instance, **kwargs)
106116
107117 default_error_messages = {"invalid": "Not a valid delimited tuple."}
108118
109 def __init__(
110 self, tuple_fields, *, delimiter: typing.Optional[str] = None, **kwargs
111 ):
119 def __init__(self, tuple_fields, *, delimiter: str | None = None, **kwargs):
112120 self.delimiter = delimiter or self.delimiter
113121 super().__init__(tuple_fields, **kwargs)
1919 uid=uid, per_page=args["per_page"]
2020 )
2121 """
22 from __future__ import annotations
23 from typing import NoReturn
24
2225 import flask
2326 from werkzeug.exceptions import HTTPException
2427
2528 import marshmallow as ma
2629
2730 from webargs import core
28 from webargs.multidictproxy import MultiDictProxy
2931
3032
31 def abort(http_status_code, exc=None, **kwargs):
33 def abort(http_status_code, exc=None, **kwargs) -> NoReturn:
3234 """Raise a HTTPException for the given http_status_code. Attach any keyword
3335 arguments to the exception for later processing.
3436
3739 try:
3840 flask.abort(http_status_code)
3941 except HTTPException as err:
40 err.data = kwargs
41 err.exc = exc
42 err.data = kwargs # type: ignore
43 err.exc = exc # type: ignore
4244 raise err
4345
4446
4951 class FlaskParser(core.Parser):
5052 """Flask request argument parser."""
5153
52 DEFAULT_UNKNOWN_BY_LOCATION = {
54 DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = {
5355 "view_args": ma.RAISE,
5456 "path": ma.RAISE,
5557 **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION,
7981
8082 def load_querystring(self, req, schema):
8183 """Return query params from the request as a MultiDictProxy."""
82 return MultiDictProxy(req.args, schema)
84 return self._makeproxy(req.args, schema)
8385
8486 def load_form(self, req, schema):
8587 """Return form values from the request as a MultiDictProxy."""
86 return MultiDictProxy(req.form, schema)
88 return self._makeproxy(req.form, schema)
8789
8890 def load_headers(self, req, schema):
8991 """Return headers from the request as a MultiDictProxy."""
90 return MultiDictProxy(req.headers, schema)
92 return self._makeproxy(req.headers, schema)
9193
9294 def load_cookies(self, req, schema):
9395 """Return cookies from the request."""
9597
9698 def load_files(self, req, schema):
9799 """Return files from the request as a MultiDictProxy."""
98 return MultiDictProxy(req.files, schema)
100 return self._makeproxy(req.files, schema)
99101
100102 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
101103 """Handles errors during parsing. Aborts the current HTTP request and
00 from collections.abc import Mapping
1 import typing
12
23 import marshmallow as ma
3
4 from webargs.core import missing, is_multiple
54
65
76 class MultiDictProxy(Mapping):
1413 In all other cases, __getitem__ proxies directly to the input multidict.
1514 """
1615
17 def __init__(self, multidict, schema: ma.Schema):
16 def __init__(
17 self,
18 multidict,
19 schema: ma.Schema,
20 known_multi_fields: typing.Tuple[typing.Type, ...] = (
21 ma.fields.List,
22 ma.fields.Tuple,
23 ),
24 ):
1825 self.data = multidict
26 self.known_multi_fields = known_multi_fields
1927 self.multiple_keys = self._collect_multiple_keys(schema)
2028
21 @staticmethod
22 def _collect_multiple_keys(schema: ma.Schema):
29 def _is_multiple(self, field: ma.fields.Field) -> bool:
30 """Return whether or not `field` handles repeated/multi-value arguments."""
31 # fields which set `is_multiple = True/False` will have the value selected,
32 # otherwise, we check for explicit criteria
33 is_multiple_attr = getattr(field, "is_multiple", None)
34 if is_multiple_attr is not None:
35 return is_multiple_attr
36 return isinstance(field, self.known_multi_fields)
37
38 def _collect_multiple_keys(self, schema: ma.Schema):
2339 result = set()
2440 for name, field in schema.fields.items():
25 if not is_multiple(field):
41 if not self._is_multiple(field):
2642 continue
2743 result.add(field.data_key if field.data_key is not None else name)
2844 return result
2945
3046 def __getitem__(self, key):
31 val = self.data.get(key, missing)
32 if val is missing or key not in self.multiple_keys:
47 val = self.data.get(key, ma.missing)
48 if val is ma.missing or key not in self.multiple_keys:
3349 return val
3450 if hasattr(self.data, "getlist"):
3551 return self.data.getlist(key)
2323 server = make_server('0.0.0.0', 6543, app)
2424 server.serve_forever()
2525 """
26 from __future__ import annotations
27
2628 import functools
2729 from collections.abc import Mapping
2830
3335
3436 from webargs import core
3537 from webargs.core import json
36 from webargs.multidictproxy import MultiDictProxy
3738
3839
3940 def is_json_request(req):
4344 class PyramidParser(core.Parser):
4445 """Pyramid request argument parser."""
4546
46 DEFAULT_UNKNOWN_BY_LOCATION = {
47 DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = {
4748 "matchdict": ma.RAISE,
4849 "path": ma.RAISE,
4950 **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION,
6667
6768 def load_querystring(self, req, schema):
6869 """Return query params from the request as a MultiDictProxy."""
69 return MultiDictProxy(req.GET, schema)
70 return self._makeproxy(req.GET, schema)
7071
7172 def load_form(self, req, schema):
7273 """Return form values from the request as a MultiDictProxy."""
73 return MultiDictProxy(req.POST, schema)
74 return self._makeproxy(req.POST, schema)
7475
7576 def load_cookies(self, req, schema):
7677 """Return cookies from the request as a MultiDictProxy."""
77 return MultiDictProxy(req.cookies, schema)
78 return self._makeproxy(req.cookies, schema)
7879
7980 def load_headers(self, req, schema):
8081 """Return headers from the request as a MultiDictProxy."""
81 return MultiDictProxy(req.headers, schema)
82 return self._makeproxy(req.headers, schema)
8283
8384 def load_files(self, req, schema):
8485 """Return files from the request as a MultiDictProxy."""
8586 files = ((k, v) for k, v in req.POST.items() if hasattr(v, "file"))
86 return MultiDictProxy(MultiDict(files), schema)
87 return self._makeproxy(MultiDict(files), schema)
8788
8889 def load_matchdict(self, req, schema):
8990 """Return the request's ``matchdict`` as a MultiDictProxy."""
90 return MultiDictProxy(req.matchdict, schema)
91 return self._makeproxy(req.matchdict, schema)
9192
9293 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
9394 """Handles errors during parsing. Aborts the current HTTP request and
123124 as_kwargs=False,
124125 validate=None,
125126 error_status_code=None,
126 error_headers=None
127 error_headers=None,
127128 ):
128129 """Decorator that injects parsed arguments into a view callable.
129130 Supports the *Class-based View* pattern where `request` is saved as an instance
5656 return _unicode(value)
5757 return value
5858 # based on tornado.web.RequestHandler.decode_argument
59 except UnicodeDecodeError:
60 raise HTTPError(400, "Invalid unicode in {}: {!r}".format(key, value[:40]))
59 except UnicodeDecodeError as exc:
60 raise HTTPError(400, f"Invalid unicode in {key}: {value[:40]!r}") from exc
6161
6262
6363 class WebArgsTornadoCookiesMultiDictProxy(MultiDictProxy):
9696
9797 def load_querystring(self, req, schema):
9898 """Return query params from the request as a MultiDictProxy."""
99 return WebArgsTornadoMultiDictProxy(req.query_arguments, schema)
99 return self._makeproxy(
100 req.query_arguments, schema, cls=WebArgsTornadoMultiDictProxy
101 )
100102
101103 def load_form(self, req, schema):
102104 """Return form values from the request as a MultiDictProxy."""
103 return WebArgsTornadoMultiDictProxy(req.body_arguments, schema)
105 return self._makeproxy(
106 req.body_arguments, schema, cls=WebArgsTornadoMultiDictProxy
107 )
104108
105109 def load_headers(self, req, schema):
106110 """Return headers from the request as a MultiDictProxy."""
107 return WebArgsTornadoMultiDictProxy(req.headers, schema)
111 return self._makeproxy(req.headers, schema, cls=WebArgsTornadoMultiDictProxy)
108112
109113 def load_cookies(self, req, schema):
110114 """Return cookies from the request as a MultiDictProxy."""
111115 # use the specialized subclass specifically for handling Tornado
112116 # cookies
113 return WebArgsTornadoCookiesMultiDictProxy(req.cookies, schema)
117 return self._makeproxy(
118 req.cookies, schema, cls=WebArgsTornadoCookiesMultiDictProxy
119 )
114120
115121 def load_files(self, req, schema):
116122 """Return files from the request as a MultiDictProxy."""
117 return WebArgsTornadoMultiDictProxy(req.files, schema)
123 return self._makeproxy(req.files, schema, cls=WebArgsTornadoMultiDictProxy)
118124
119125 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
120126 """Handles errors during parsing. Raises a `tornado.web.HTTPError`
3535 async def echo_json(request):
3636 try:
3737 parsed = await parser.parse(hello_args, request, location="json")
38 except json.JSONDecodeError:
38 except json.JSONDecodeError as exc:
3939 raise aiohttp.web.HTTPBadRequest(
40 body=json.dumps(["Invalid JSON."]).encode("utf-8"),
40 text=json.dumps(["Invalid JSON."]),
4141 content_type="application/json",
42 )
42 ) from exc
4343 return json_response(parsed)
4444
4545
4646 async def echo_json_or_form(request):
4747 try:
4848 parsed = await parser.parse(hello_args, request, location="json_or_form")
49 except json.JSONDecodeError:
49 except json.JSONDecodeError as exc:
5050 raise aiohttp.web.HTTPBadRequest(
51 body=json.dumps(["Invalid JSON."]).encode("utf-8"),
51 text=json.dumps(["Invalid JSON."]),
5252 content_type="application/json",
53 )
53 ) from exc
5454 return json_response(parsed)
5555
5656
0 from django import __version__
1
2 DJANGO_MAJOR_VERSION = int(__version__.split(".")[0])
3 DJANGO_SUPPORTS_ASYNC = DJANGO_MAJOR_VERSION >= 3
0 from django.conf.urls import url
0 from django.urls import re_path
11
22 from tests.apps.django_app.echo import views
33
44
55 urlpatterns = [
6 url(r"^echo$", views.echo),
7 url(r"^echo_form$", views.echo_form),
8 url(r"^echo_json$", views.echo_json),
9 url(r"^echo_json_or_form$", views.echo_json_or_form),
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),
13 url(r"^echo_use_kwargs$", views.echo_use_kwargs),
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),
17 url(r"^echo_many_schema$", views.echo_many_schema),
18 url(
6 re_path(r"^echo$", views.echo),
7 re_path(r"^async_echo$", views.async_echo),
8 re_path(r"^echo_form$", views.echo_form),
9 re_path(r"^echo_json$", views.echo_json),
10 re_path(r"^echo_json_or_form$", views.echo_json_or_form),
11 re_path(r"^echo_use_args$", views.echo_use_args),
12 re_path(r"^async_echo_use_args$", views.async_echo_use_args),
13 re_path(r"^echo_use_args_validated$", views.echo_use_args_validated),
14 re_path(r"^echo_ignoring_extra_data$", views.echo_ignoring_extra_data),
15 re_path(r"^echo_use_kwargs$", views.echo_use_kwargs),
16 re_path(r"^echo_multi$", views.echo_multi),
17 re_path(r"^echo_multi_form$", views.echo_multi_form),
18 re_path(r"^echo_multi_json$", views.echo_multi_json),
19 re_path(r"^echo_many_schema$", views.echo_many_schema),
20 re_path(
1921 r"^echo_use_args_with_path_param/(?P<name>\w+)$",
2022 views.echo_use_args_with_path_param,
2123 ),
22 url(
24 re_path(
2325 r"^echo_use_kwargs_with_path_param/(?P<name>\w+)$",
2426 views.echo_use_kwargs_with_path_param,
2527 ),
26 url(r"^error$", views.always_error),
27 url(r"^echo_headers$", views.echo_headers),
28 url(r"^echo_cookie$", views.echo_cookie),
29 url(r"^echo_file$", views.echo_file),
30 url(r"^echo_nested$", views.echo_nested),
31 url(r"^echo_nested_many$", views.echo_nested_many),
32 url(r"^echo_cbv$", views.EchoCBV.as_view()),
33 url(r"^echo_use_args_cbv$", views.EchoUseArgsCBV.as_view()),
34 url(
28 re_path(r"^error$", views.always_error),
29 re_path(r"^echo_headers$", views.echo_headers),
30 re_path(r"^echo_cookie$", views.echo_cookie),
31 re_path(r"^echo_file$", views.echo_file),
32 re_path(r"^echo_nested$", views.echo_nested),
33 re_path(r"^echo_nested_many$", views.echo_nested_many),
34 re_path(r"^echo_cbv$", views.EchoCBV.as_view()),
35 re_path(r"^echo_use_args_cbv$", views.EchoUseArgsCBV.as_view()),
36 re_path(
3537 r"^echo_use_args_with_path_param_cbv/(?P<pid>\d+)$",
3638 views.EchoUseArgsWithParamCBV.as_view(),
3739 ),
00 from django.http import HttpResponse
11 from django.views.generic import View
2 import asyncio
23 import marshmallow as ma
34
45 from webargs import fields
2526
2627
2728 def handle_view_errors(f):
28 def wrapped(*args, **kwargs):
29 try:
30 return f(*args, **kwargs)
31 except ma.ValidationError as err:
32 return json_response(err.messages, status=422)
33 except json.JSONDecodeError:
34 return json_response({"json": ["Invalid JSON body."]}, status=400)
29 if asyncio.iscoroutinefunction(f):
30
31 async def wrapped(*args, **kwargs):
32 try:
33 return await f(*args, **kwargs)
34 except ma.ValidationError as err:
35 return json_response(err.messages, status=422)
36 except json.JSONDecodeError:
37 return json_response({"json": ["Invalid JSON body."]}, status=400)
38
39 else:
40
41 def wrapped(*args, **kwargs):
42 try:
43 return f(*args, **kwargs)
44 except ma.ValidationError as err:
45 return json_response(err.messages, status=422)
46 except json.JSONDecodeError:
47 return json_response({"json": ["Invalid JSON body."]}, status=400)
3548
3649 return wrapped
3750
4255
4356
4457 @handle_view_errors
58 async def async_echo(request):
59 return json_response(
60 await parser.async_parse(hello_args, request, location="query")
61 )
62
63
64 @handle_view_errors
4565 def echo_form(request):
4666 return json_response(parser.parse(hello_args, request, location="form"))
4767
5979 @handle_view_errors
6080 @use_args(hello_args, location="query")
6181 def echo_use_args(request, args):
82 return json_response(args)
83
84
85 @handle_view_errors
86 @use_args(hello_args, location="query")
87 async def async_echo_use_args(request, args):
6288 return json_response(args)
6389
6490
77 hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)}
88 hello_multiple = {"name": fields.List(fields.Str())}
99
10 FALCON_MAJOR_VERSION = int(falcon.__version__.split(".")[0])
11 FALCON_SUPPORTS_ASYNC = FALCON_MAJOR_VERSION >= 3
12
1013
1114 class HelloSchema(ma.Schema):
1215 name = fields.Str(missing="World", validate=lambda n: len(n) >= 3)
2124 class Echo:
2225 def on_get(self, req, resp):
2326 parsed = parser.parse(hello_args, req, location="query")
27 resp.body = json.dumps(parsed)
28
29
30 class AsyncEcho:
31 async def on_get(self, req, resp):
32 parsed = await parser.async_parse(hello_args, req, location="query")
2433 resp.body = json.dumps(parsed)
2534
2635
5160 class EchoUseArgs:
5261 @use_args(hello_args, location="query")
5362 def on_get(self, req, resp, args):
63 resp.body = json.dumps(args)
64
65
66 class AsyncEchoUseArgs:
67 @use_args(hello_args, location="query")
68 async def on_get(self, req, resp, args):
5469 resp.body = json.dumps(args)
5570
5671
187202 app.add_route("/echo_nested_many", EchoNestedMany())
188203 app.add_route("/echo_use_args_hook", EchoUseArgsHook())
189204 return app
205
206
207 def create_async_app():
208 # defer import (async-capable versions only)
209 import falcon.asgi
210
211 app = falcon.asgi.App()
212 app.add_route("/async_echo", AsyncEcho())
213 app.add_route("/async_echo_use_args", AsyncEchoUseArgs())
214 return app
0 from flask import Flask, jsonify as J, Response, request
0 import marshmallow as ma
1 from flask import Flask, jsonify as J, Response, request, __version__ as flask_version
12 from flask.views import MethodView
2 import marshmallow as ma
33
44 from webargs import fields
5 from webargs.flaskparser import parser, use_args, use_kwargs
65 from webargs.core import json
6 from webargs.flaskparser import (
7 parser,
8 use_args,
9 use_kwargs,
10 )
11
12 FLASK_MAJOR_VERSION = int(flask_version.split(".")[0])
13 FLASK_SUPPORTS_ASYNC = FLASK_MAJOR_VERSION >= 2
714
815
916 class TestAppConfig:
122129 @use_args(HelloSchema(), location="headers", unknown=None)
123130 def echo_headers_raising(args):
124131 return J(args)
132
133
134 if FLASK_SUPPORTS_ASYNC:
135
136 @app.route("/echo_headers_raising_async")
137 @use_args(HelloSchema(), location="headers", unknown=None)
138 async def echo_headers_raising_async(args):
139 return J(args)
125140
126141
127142 @app.route("/echo_cookie")
143158 return J(parser.parse({"view_arg": fields.Int()}, location="view_args"))
144159
145160
161 if FLASK_SUPPORTS_ASYNC:
162
163 @app.route("/echo_view_arg_async/<view_arg>")
164 async def echo_view_arg_async(view_arg):
165 parsed_view_arg = await parser.async_parse(
166 {"view_arg": fields.Int()}, location="view_args"
167 )
168 return J(parsed_view_arg)
169
170
146171 @app.route("/echo_view_arg_use_args/<view_arg>")
147172 @use_args({"view_arg": fields.Int()}, location="view_args")
148173 def echo_view_arg_with_use_args(args, **kwargs):
149174 return J(args)
175
176
177 if FLASK_SUPPORTS_ASYNC:
178
179 @app.route("/echo_view_arg_use_args_async/<view_arg>")
180 @use_args({"view_arg": fields.Int()}, location="view_args")
181 async def echo_view_arg_with_use_args_async(args, **kwargs):
182 return J(args)
150183
151184
152185 @app.route("/echo_nested", methods=["POST"])
171204 return J(parser.parse(args))
172205
173206
207 if FLASK_SUPPORTS_ASYNC:
208
209 @app.route("/echo_nested_many_data_key_async", methods=["POST"])
210 async def echo_nested_many_with_data_key_async():
211 args = {
212 "x_field": fields.Nested(
213 {"id": fields.Int()}, many=True, data_key="X-Field"
214 )
215 }
216 return J(await parser.async_parse(args))
217
218
174219 class EchoMethodViewUseArgs(MethodView):
175220 @use_args({"val": fields.Int()})
176221 def post(self, args):
183228 )
184229
185230
231 if FLASK_SUPPORTS_ASYNC:
232
233 class EchoMethodViewUseArgsAsync(MethodView):
234 @use_args({"val": fields.Int()})
235 async def post(self, args):
236 return J(args)
237
238 app.add_url_rule(
239 "/echo_method_view_use_args_async",
240 view_func=EchoMethodViewUseArgsAsync.as_view("echo_method_view_use_args_async"),
241 )
242
243
186244 class EchoMethodViewUseKwargs(MethodView):
187245 @use_kwargs({"val": fields.Int()})
188246 def post(self, val):
193251 "/echo_method_view_use_kwargs",
194252 view_func=EchoMethodViewUseKwargs.as_view("echo_method_view_use_kwargs"),
195253 )
254
255 if FLASK_SUPPORTS_ASYNC:
256
257 class EchoMethodViewUseKwargsAsync(MethodView):
258 @use_kwargs({"val": fields.Int()})
259 async def post(self, val):
260 return J({"val": val})
261
262 app.add_url_rule(
263 "/echo_method_view_use_kwargs_async",
264 view_func=EchoMethodViewUseKwargsAsync.as_view(
265 "echo_method_view_use_kwargs_async"
266 ),
267 )
196268
197269
198270 @app.route("/echo_use_kwargs_missing", methods=["post"])
200272 def echo_use_kwargs_missing(username, **kwargs):
201273 assert "password" not in kwargs
202274 return J({"username": username})
275
276
277 if FLASK_SUPPORTS_ASYNC:
278
279 @app.route("/echo_use_kwargs_missing_async", methods=["post"])
280 @use_kwargs({"username": fields.Str(required=True), "password": fields.Str()})
281 async def echo_use_kwargs_missing_async(username, **kwargs):
282 assert "password" not in kwargs
283 return J({"username": username})
203284
204285
205286 # Return validation errors as JSON
7777 "json_parsed": {"name": "Steve"},
7878 }
7979
80 def test_validation_error_returns_422_response(self, testapp):
81 res = testapp.post_json("/echo_json", {"name": "b"}, expect_errors=True)
82 assert res.status_code == 422
83 assert res.json == {"json": {"name": ["Invalid value."]}}
84
8085
8186 async def test_aiohttpparser_synchronous_error_handler(web_request):
8287 parser = AIOHTTPParser()
0 import collections
01 import datetime
2 import typing
13 from unittest import mock
24
35 import pytest
3436 """A minimal parser implementation that parses mock requests."""
3537
3638 def load_querystring(self, req, schema):
37 return MultiDictProxy(req.query, schema)
39 return self._makeproxy(req.query, schema)
40
41 def load_form(self, req, schema):
42 return MultiDictProxy(req.form, schema)
3843
3944 def load_json(self, req, schema):
4045 return req.json
9398 assert load_json.call_count == 1
9499
95100
96 def test_parse(parser, web_request):
101 @pytest.mark.asyncio
102 @pytest.mark.parametrize("method", ["parse", "async_parse"])
103 async def test_parse(parser, web_request, method):
97104 web_request.json = {"username": 42, "password": 42}
98105 argmap = {"username": fields.Field(), "password": fields.Field()}
99 ret = parser.parse(argmap, web_request)
106 if method == "async_parse":
107 ret = await parser.async_parse(argmap, web_request)
108 else:
109 ret = parser.parse(argmap, web_request)
100110 assert {"username": 42, "password": 42} == ret
101111
102112
213223 assert {"username": 42, "password": 42, "fjords": 42} == ret
214224
215225
216 def test_parse_required_arg_raises_validation_error(parser, web_request):
226 @pytest.mark.asyncio
227 @pytest.mark.parametrize("method", ["parse", "async_parse"])
228 async def test_parse_required_arg_raises_validation_error(parser, web_request, method):
217229 web_request.json = {}
218230 args = {"foo": fields.Field(required=True)}
219231 with pytest.raises(ValidationError, match="Missing data for required field."):
220 parser.parse(args, web_request)
232 if method == "parse":
233 parser.parse(args, web_request)
234 else:
235 await parser.async_parse(args, web_request)
221236
222237
223238 def test_arg_not_required_excluded_in_parsed_output(parser, web_request):
333348 assert handle_error.call_count == 2
334349
335350
351 @pytest.mark.asyncio
352 async def test_handle_error_called_when_async_parsing_raises_error(web_request):
353 with mock.patch("webargs.core.Parser.handle_error") as handle_error:
354 # handle_error must raise an error to be valid
355 handle_error.side_effect = ValidationError("parsing failed")
356
357 def always_fail(*args, **kwargs):
358 raise ValidationError("error occurred")
359
360 p = Parser()
361 assert handle_error.call_count == 0
362 with pytest.raises(ValidationError):
363 await p.async_parse(
364 {"foo": fields.Field()}, web_request, validate=always_fail
365 )
366 assert handle_error.call_count == 1
367 with pytest.raises(ValidationError):
368 await p.async_parse(
369 {"foo": fields.Field()}, web_request, validate=always_fail
370 )
371 assert handle_error.call_count == 2
372
373
336374 def test_handle_error_reraises_errors(web_request):
337375 p = Parser()
338376 with pytest.raises(ValidationError):
391429
392430 with pytest.raises(CustomError):
393431 parser.parse(mock_schema, web_request)
432
433
434 @pytest.mark.asyncio
435 @pytest.mark.parametrize("async_handler", [True, False])
436 async def test_custom_error_handler_decorator_in_async_parse(
437 web_request, async_handler
438 ):
439 class CustomError(Exception):
440 pass
441
442 mock_schema = mock.Mock(spec=Schema)
443 mock_schema.strict = True
444 mock_schema.load.side_effect = ValidationError("parsing json failed")
445 parser = Parser()
446
447 if async_handler:
448
449 @parser.error_handler
450 async def handle_error(error, req, schema, *, error_status_code, error_headers):
451 assert isinstance(schema, Schema)
452 raise CustomError(error)
453
454 else:
455
456 @parser.error_handler
457 def handle_error(error, req, schema, *, error_status_code, error_headers):
458 assert isinstance(schema, Schema)
459 raise CustomError(error)
460
461 with pytest.raises(CustomError):
462 await parser.async_parse(mock_schema, web_request)
394463
395464
396465 def test_custom_error_handler_must_reraise(web_request):
626695 assert viewfunc() == {"username": "foo", "password": "bar"}
627696
628697
698 async def test_use_args_on_async(web_request, parser):
699 user_args = {"username": fields.Str(), "password": fields.Str()}
700 web_request.json = {"username": "foo", "password": "bar"}
701
702 @parser.use_args(user_args, web_request)
703 async def viewfunc(args):
704 return args
705
706 data = await viewfunc()
707 assert data == {"username": "foo", "password": "bar"}
708
709
629710 def test_use_args_stacked(web_request, parser):
630711 query_args = {"page": fields.Int()}
631712 json_args = {"username": fields.Str()}
866947 return {"username": username}
867948
868949 assert viewfunc() == {"username": "foo"}
950
951
952 def test_delimited_list_empty_string(web_request, parser):
953 web_request.json = {"dates": ""}
954 schema_cls = Schema.from_dict({"dates": fields.DelimitedList(fields.Str())})
955 schema = schema_cls()
956
957 parsed = parser.parse(schema, web_request)
958 assert parsed["dates"] == []
959
960 data = schema.dump(parsed)
961 assert data["dates"] == ""
869962
870963
871964 def test_delimited_list_default_delimiter(web_request, parser):
10311124 parser.parse(args, web_request)
10321125
10331126
1127 @pytest.mark.parametrize("input_dict", multidicts)
1128 @pytest.mark.parametrize(
1129 "setting",
1130 [
1131 "is_multiple_true",
1132 "is_multiple_false",
1133 "is_multiple_notset",
1134 "list_field",
1135 "tuple_field",
1136 "added_to_known",
1137 ],
1138 )
1139 def test_is_multiple_detection(web_request, parser, input_dict, setting):
1140 # this custom class "multiplexes" in that it can be given a single value or
1141 # list of values -- a single value is treated as a string, and a list of
1142 # values is treated as a list of strings
1143 class CustomMultiplexingField(fields.String):
1144 def _deserialize(self, value, attr, data, **kwargs):
1145 if isinstance(value, str):
1146 return super()._deserialize(value, attr, data, **kwargs)
1147 return [
1148 self._deserialize(v, attr, data, **kwargs)
1149 for v in value
1150 if isinstance(v, str)
1151 ]
1152
1153 def _serialize(self, value, attr, **kwargs):
1154 if isinstance(value, str):
1155 return super()._serialize(value, attr, **kwargs)
1156 return [
1157 self._serialize(v, attr, **kwargs) for v in value if isinstance(v, str)
1158 ]
1159
1160 class CustomMultipleField(CustomMultiplexingField):
1161 is_multiple = True
1162
1163 class CustomNonMultipleField(CustomMultiplexingField):
1164 is_multiple = False
1165
1166 # the request's query params are the input multidict
1167 web_request.query = input_dict
1168
1169 # case 1: is_multiple=True
1170 if setting == "is_multiple_true":
1171 # the multidict should unpack to a list of strings
1172 #
1173 # order is not necessarily guaranteed by the multidict implementations, but
1174 # both values must be present
1175 args = {"foos": CustomMultipleField()}
1176 result = parser.parse(args, web_request, location="query")
1177 assert result["foos"] in (["a", "b"], ["b", "a"])
1178 # case 2: is_multiple=False
1179 elif setting == "is_multiple_false":
1180 # the multidict should unpack to a string
1181 #
1182 # either value may be returned, depending on the multidict implementation,
1183 # but not both
1184 args = {"foos": CustomNonMultipleField()}
1185 result = parser.parse(args, web_request, location="query")
1186 assert result["foos"] in ("a", "b")
1187 # case 3: is_multiple is not set
1188 elif setting == "is_multiple_notset":
1189 # this should be the same as is_multiple=False
1190 args = {"foos": CustomMultiplexingField()}
1191 result = parser.parse(args, web_request, location="query")
1192 assert result["foos"] in ("a", "b")
1193 # case 4: the field is a List (special case)
1194 elif setting == "list_field":
1195 # this should behave like the is_multiple=True case
1196 args = {"foos": fields.List(fields.Str())}
1197 result = parser.parse(args, web_request, location="query")
1198 assert result["foos"] in (["a", "b"], ["b", "a"])
1199 # case 5: the field is a Tuple (special case)
1200 elif setting == "tuple_field":
1201 # this should behave like the is_multiple=True case and produce a tuple
1202 args = {"foos": fields.Tuple((fields.Str, fields.Str))}
1203 result = parser.parse(args, web_request, location="query")
1204 assert result["foos"] in (("a", "b"), ("b", "a"))
1205 # case 6: the field is custom, but added to the known fields of the parser
1206 elif setting == "added_to_known":
1207 # if it's included in the known multifields and is_multiple is not set, behave
1208 # like is_multiple=True
1209 parser.KNOWN_MULTI_FIELDS.append(CustomMultiplexingField)
1210 args = {"foos": CustomMultiplexingField()}
1211 result = parser.parse(args, web_request, location="query")
1212 assert result["foos"] in (["a", "b"], ["b", "a"])
1213 else:
1214 raise NotImplementedError
1215
1216
10341217 def test_validation_errors_in_validator_are_passed_to_handle_error(parser, web_request):
10351218 def validate(value):
10361219 raise ValidationError("Something went wrong.")
11331316 p = CustomParser()
11341317 ret = p.parse(argmap, web_request)
11351318 assert ret == {"value": "hello world"}
1319
1320
1321 def test_parser_pre_load(web_request):
1322 class CustomParser(MockRequestParser):
1323 # pre-load hook to strip whitespace from query params
1324 def pre_load(self, data, *, schema, req, location):
1325 if location == "query":
1326 return {k: v.strip() for k, v in data.items()}
1327 return data
1328
1329 parser = CustomParser()
1330
1331 # mock data for both query and json
1332 web_request.query = web_request.json = {"value": " hello "}
1333 argmap = {"value": fields.Str()}
1334
1335 # data gets through for 'json' just fine
1336 ret = parser.parse(argmap, web_request)
1337 assert ret == {"value": " hello "}
1338
1339 # but for 'query', the pre_load hook changes things
1340 ret = parser.parse(argmap, web_request, location="query")
1341 assert ret == {"value": "hello"}
1342
1343
1344 # this test is meant to be a run of the WhitspaceStrippingFlaskParser we give
1345 # in the docs/advanced.rst examples for how to use pre_load
1346 # this helps ensure that the example code is correct
1347 # rather than a FlaskParser, we're working with the mock parser, but it's
1348 # otherwise the same
1349 def test_whitespace_stripping_parser_example(web_request):
1350 def _strip_whitespace(value):
1351 if isinstance(value, str):
1352 value = value.strip()
1353 elif isinstance(value, typing.Mapping):
1354 return {k: _strip_whitespace(value[k]) for k in value}
1355 elif isinstance(value, (list, tuple)):
1356 return type(value)(map(_strip_whitespace, value))
1357 return value
1358
1359 class WhitspaceStrippingParser(MockRequestParser):
1360 def pre_load(self, location_data, *, schema, req, location):
1361 if location in ("query", "form"):
1362 ret = _strip_whitespace(location_data)
1363 return ret
1364 return location_data
1365
1366 parser = WhitspaceStrippingParser()
1367
1368 # mock data for query, form, and json
1369 web_request.form = web_request.query = web_request.json = {"value": " hello "}
1370 argmap = {"value": fields.Str()}
1371
1372 # data gets through for 'json' just fine
1373 ret = parser.parse(argmap, web_request)
1374 assert ret == {"value": " hello "}
1375
1376 # but for 'query' and 'form', the pre_load hook changes things
1377 for loc in ("query", "form"):
1378 ret = parser.parse(argmap, web_request, location=loc)
1379 assert ret == {"value": "hello"}
1380
1381 # check that it applies in the case where the field is a list type
1382 # applied to an argument (logic for `tuple` is effectively the same)
1383 web_request.form = web_request.query = web_request.json = {
1384 "ids": [" 1", "3", " 4"],
1385 "values": [" foo ", " bar"],
1386 }
1387 schema = Schema.from_dict(
1388 {"ids": fields.List(fields.Int), "values": fields.List(fields.Str)}
1389 )
1390 for loc in ("query", "form"):
1391 ret = parser.parse(schema, web_request, location=loc)
1392 assert ret == {"ids": [1, 3, 4], "values": ["foo", "bar"]}
1393
1394 # json loading should also work even though the pre_load hook above
1395 # doesn't strip whitespace from JSON data
1396 # - values=[" foo ", ...] will have whitespace preserved
1397 # - ids=[" 1", ...] will still parse okay because " 1" is valid for fields.Int
1398 ret = parser.parse(schema, web_request, location="json")
1399 assert ret == {"ids": [1, 3, 4], "values": [" foo ", " bar"]}
1400
1401
1402 def test_parse_rejects_non_dict_argmap_mapping(parser, web_request):
1403 web_request.json = {"username": 42, "password": 42}
1404 argmap = collections.UserDict(
1405 {"username": fields.Field(), "password": fields.Field()}
1406 )
1407
1408 # UserDict is dict-like in all meaningful ways, but not a subclass of `dict`
1409 # it will therefore be rejected with a TypeError when used
1410 with pytest.raises(TypeError):
1411 parser.parse(argmap, web_request)
00 import pytest
11 from tests.apps.django_app.base.wsgi import application
2 from tests.apps.django_app import DJANGO_SUPPORTS_ASYNC
23
34 from webargs.testing import CommonTestCase
45
2627 def test_use_args_in_class_based_view_with_path_param(self, testapp):
2728 res = testapp.get("/echo_use_args_with_path_param_cbv/42?name=Fred")
2829 assert res.json == {"name": "Fred"}
30
31 @pytest.mark.skipif(
32 not DJANGO_SUPPORTS_ASYNC, reason="requires a django version with async support"
33 )
34 def test_parse_querystring_args_async(self, testapp):
35 assert testapp.get("/async_echo?name=Fred").json == {"name": "Fred"}
36
37 @pytest.mark.skipif(
38 not DJANGO_SUPPORTS_ASYNC, reason="requires a django version with async support"
39 )
40 def test_async_use_args_decorator(self, testapp):
41 assert testapp.get("/async_echo_use_args?name=Fred").json == {"name": "Fred"}
11 import falcon.testing
22
33 from webargs.testing import CommonTestCase
4 from tests.apps.falcon_app import create_app
4 from tests.apps.falcon_app import create_app, create_async_app, FALCON_SUPPORTS_ASYNC
55
66
77 class TestFalconParser(CommonTestCase):
7171 json={"name": "Fred"},
7272 )
7373 assert res.json == {"name": "Fred"}
74
75 @pytest.mark.skipif(
76 not FALCON_SUPPORTS_ASYNC, reason="requires a falcon version with async support"
77 )
78 def test_parse_querystring_args_async(self):
79 app = create_async_app()
80 client = falcon.testing.TestClient(app)
81 assert client.simulate_get("/async_echo?name=Fred").json == {"name": "Fred"}
82
83 @pytest.mark.skipif(
84 not FALCON_SUPPORTS_ASYNC, reason="requires a falcon version with async support"
85 )
86 def test_async_use_args_decorator(self):
87 app = create_async_app()
88 client = falcon.testing.TestClient(app)
89 assert client.simulate_get("/async_echo_use_args?name=Fred").json == {
90 "name": "Fred"
91 }
88 from webargs.flaskparser import parser, abort
99 from webargs.core import json
1010
11 from .apps.flask_app import app
11 from .apps.flask_app import app, FLASK_SUPPORTS_ASYNC
1212 from webargs.testing import CommonTestCase
1313
1414
5656 res = testapp.post_json("/echo_nested_many_data_key", {})
5757 assert res.json == {}
5858
59 # regression test for
60 # https://github.com/marshmallow-code/webargs/issues/500
59 # regression test for https://github.com/marshmallow-code/webargs/issues/500
6160 def test_parsing_unexpected_headers_when_raising(self, testapp):
6261 res = testapp.get(
6362 "/echo_headers_raising", expect_errors=True, headers={"X-Unexpected": "foo"}
63 )
64 assert res.status_code == 422
65 assert "headers" in res.json
66 assert "X-Unexpected" in set(res.json["headers"].keys())
67
68
69 class TestFlaskAsyncParser(CommonTestCase):
70 def create_app(self):
71 return app
72
73 @pytest.mark.skipif(
74 not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask"
75 )
76 def test_parsing_view_args_async(self, testapp):
77 res = testapp.get("/echo_view_arg_async/42")
78 assert res.json == {"view_arg": 42}
79
80 @pytest.mark.skipif(
81 not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask"
82 )
83 def test_parsing_invalid_view_arg_async(self, testapp):
84 res = testapp.get("/echo_view_arg_async/foo", expect_errors=True)
85 assert res.status_code == 422
86 assert res.json == {"view_args": {"view_arg": ["Not a valid integer."]}}
87
88 @pytest.mark.skipif(
89 not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask"
90 )
91 def test_use_args_with_view_args_parsing_async(self, testapp):
92 res = testapp.get("/echo_view_arg_use_args_async/42")
93 assert res.json == {"view_arg": 42}
94
95 @pytest.mark.skipif(
96 not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask"
97 )
98 def test_use_args_on_a_method_view_async(self, testapp):
99 res = testapp.post_json("/echo_method_view_use_args_async", {"val": 42})
100 assert res.json == {"val": 42}
101
102 @pytest.mark.skipif(
103 not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask"
104 )
105 def test_use_kwargs_on_a_method_view_async(self, testapp):
106 res = testapp.post_json("/echo_method_view_use_kwargs_async", {"val": 42})
107 assert res.json == {"val": 42}
108
109 @pytest.mark.skipif(
110 not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask"
111 )
112 def test_use_kwargs_with_missing_data_async(self, testapp):
113 res = testapp.post_json("/echo_use_kwargs_missing_async", {"username": "foo"})
114 assert res.json == {"username": "foo"}
115
116 # regression test for https://github.com/marshmallow-code/webargs/issues/145
117 @pytest.mark.skipif(
118 not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask"
119 )
120 def test_nested_many_with_data_key_async(self, testapp):
121 post_with_raw_fieldname_args = (
122 "/echo_nested_many_data_key_async",
123 {"x_field": [{"id": 42}]},
124 )
125 res = testapp.post_json(*post_with_raw_fieldname_args, expect_errors=True)
126 assert res.status_code == 422
127
128 res = testapp.post_json(
129 "/echo_nested_many_data_key_async", {"X-Field": [{"id": 24}]}
130 )
131 assert res.json == {"x_field": [{"id": 24}]}
132
133 res = testapp.post_json("/echo_nested_many_data_key_async", {})
134 assert res.json == {}
135
136 # regression test for https://github.com/marshmallow-code/webargs/issues/500
137 @pytest.mark.skipif(
138 not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask"
139 )
140 def test_parsing_unexpected_headers_when_raising_async(self, testapp):
141 res = testapp.get(
142 "/echo_headers_raising_async",
143 expect_errors=True,
144 headers={"X-Unexpected": "foo"},
64145 )
65146 assert res.status_code == 422
66147 assert "headers" in res.json
94175 assert type(abort_kwargs["exc"]) == ValidationError
95176
96177
178 @pytest.mark.asyncio
179 @pytest.mark.skipif(not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask")
180 async def test_abort_called_on_validation_error_async():
181 with mock.patch("webargs.flaskparser.abort") as mock_abort:
182 # error handling must raise an error to be valid
183 mock_abort.side_effect = BadRequest("foo")
184
185 app = Flask("testapp")
186
187 def validate(x):
188 return x == 42
189
190 argmap = {"value": fields.Field(validate=validate)}
191 with app.test_request_context(
192 "/foo",
193 method="post",
194 data=json.dumps({"value": 41}),
195 content_type="application/json",
196 ):
197 with pytest.raises(HTTPException):
198 await parser.async_parse(argmap)
199 mock_abort.assert_called()
200 abort_args, abort_kwargs = mock_abort.call_args
201 assert abort_args[0] == 422
202 expected_msg = "Invalid value."
203 assert abort_kwargs["messages"]["json"]["value"] == [expected_msg]
204 assert type(abort_kwargs["exc"]) == ValidationError
205
206
97207 @pytest.mark.parametrize("mimetype", [None, "application/json"])
98208 def test_load_json_returns_missing_if_no_data(mimetype):
99209 req = mock.Mock()
00 [tox]
11 envlist=
22 lint
3 py{36,37,38,39}
4 py36-mindeps
5 py39-marshmallowdev
3 py{37,38,39,310}
4 py37-mindeps
5 py310-marshmallowdev
66 docs
77
88 [testenv]
1010 deps =
1111 !marshmallowdev: marshmallow>=3.0.0,<4.0.0
1212 marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz
13 mindeps: Flask==0.12.5
13
14 # for 'mindeps', pin flask to 1.1.3 and markupsafe to 1.1.1
15 # because flask 1.x does not set upper bounds on the versions of its dependencies
16 # generally, we can't install just any version from 1.x -- only 1.1.3 and 1.1.4
17 # markupsafe is a second-order dependency of flask (flask -> jinja2 -> markupsafe)
18 # and must be pinned explicitly because jinja2 does not pin its dependencies in any
19 # versions in its 2.x line
20 # see:
21 # https://github.com/marshmallow-code/webargs/pull/694
22 # https://github.com/pallets/flask/pull/4047
23 # https://github.com/pallets/flask/issues/4455
24 mindeps: flask==1.1.3
25 mindeps: markupsafe==1.1.1
26 # all non-flask frameworks
1427 mindeps: Django==2.2.0
1528 mindeps: bottle==0.12.13
1629 mindeps: tornado==4.5.2
2841 # `webargs` and `marshmallow` both installed is a valuable safeguard against
2942 # issues in which `mypy` running on every file standalone won't catch things
3043 [testenv:mypy]
31 deps = mypy
44 deps = mypy==0.930
45 extras = frameworks
3246 commands = mypy src/
3347
3448 [testenv:docs]