Codebase list python-webargs / b25be02
Import upstream version 8.1.0 Kali Janitor 2 years ago
33 changed file(s) with 747 addition(s) and 242 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
00 repos:
11 - repo: https://github.com/asottile/pyupgrade
2 rev: v2.7.3
2 rev: v2.31.0
33 hooks:
44 - id: pyupgrade
5 args: ["--py36-plus"]
5 args: ["--py37-plus"]
66 - repo: https://github.com/psf/black
7 rev: 20.8b1
7 rev: 21.12b0
88 hooks:
99 - id: black
10 - repo: https://gitlab.com/pycqa/flake8
11 rev: 3.8.4
10 - repo: https://github.com/pycqa/flake8
11 rev: 4.0.1
1212 hooks:
1313 - id: flake8
14 additional_dependencies: [flake8-bugbear==20.1.0]
14 additional_dependencies: [flake8-bugbear==21.11.29]
1515 - repo: https://github.com/asottile/blacken-docs
16 rev: v1.8.0
16 rev: v1.12.0
1717 hooks:
1818 - id: blacken-docs
19 additional_dependencies: [black==20.8b1]
20 args: ["--target-version", "py35"]
19 additional_dependencies: [black==21.12b0]
20 args: ["--target-version", "py37"]
2121 - repo: https://github.com/pre-commit/mirrors-mypy
22 rev: v0.790
22 rev: v0.930
2323 hooks:
2424 - id: mypy
2525 language_version: python3
2626 files: ^src/webargs/
27 additional_dependencies: ["marshmallow>=3,<4", "packaging"]
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>`_
00 Changelog
11 ---------
2
3 8.1.0 (2022-01-12)
4 ******************
5
6 Bug fixes:
7
8 * Fix publishing type hints per `PEP-561 <https://www.python.org/dev/peps/pep-0561/>`_.
9 (:pr:`650`).
10 * Add DelimitedTuple to fields.__all__ (:pr:`678`).
11 * Narrow type of ``argmap`` from ``Mapping`` to ``Dict`` (:pr:`682`).
12
13 Other changes:
14
15 * Test against Python 3.10 (:pr:`647`).
16 * Drop support for Python 3.6 (:pr:`673`).
17 * Address distutils deprecation warning in Python 3.10 (:pr:`652`).
18 Thanks :user:`kkirsche` for the PR.
19 * Use postponed evaluation of annotations (:pr:`663`).
20 Thanks :user:`Isira-Seneviratne` for the PR.
21 * Pin mypy version in tox (:pr:`674`).
22 * Improve type annotations for ``__version_info__`` (:pr:`680`).
23
24 8.0.1 (2021-08-12)
25 ******************
26
27 Bug fixes:
28
29 * Fix "``DelimitedList`` deserializes empty string as ``['']``" (:issue:`623`).
30 Thanks :user:`TTWSchell` for reporting and for the PR.
31
32 Other changes:
33
34 * New documentation theme with `furo`. Thanks to :user:`pradyunsg` for writing
35 furo!
36 * Webargs has a new logo. Thanks to :user:`michaelizergit`! (:issue:`312`)
37 * Don't build universal wheels. We don't support Python 2 anymore.
38 (:pr:`632`)
39 * Make the build reproducible (:pr:`631`).
40
41
42 8.0.0 (2021-04-08)
43 ******************
44
45 Features:
46
47 * Add `Parser.pre_load` as a method for allowing users to modify data before
48 schema loading, but without redefining location loaders. See advanced docs on
49 `Parser pre_load` for usage information. (:pr:`583`)
50
51 * *Backwards-incompatible*: ``unknown`` defaults to `None` for body locations
52 (`json`, `form` and `json_or_form`) (:issue:`580`).
53
54 * Detection of fields as "multi-value" for unpacking lists from multi-dict
55 types is now extensible with the ``is_multiple`` attribute. If a field sets
56 ``is_multiple = True`` it will be detected as a multi-value field. If
57 ``is_multiple`` is not set or is set to ``None``, webargs will check if the
58 field is an instance of ``List`` or ``Tuple``. (:issue:`563`)
59
60 * A new attribute on ``Parser`` objects, ``Parser.KNOWN_MULTI_FIELDS`` can be
61 used to set fields which should be detected as ``is_multiple=True`` even when
62 the attribute is not set (:pr:`592`).
63
64 See docs on "Multi-Field Detection" for more details.
65
66 Bug fixes:
67
68 * ``Tuple`` field now behaves as a "multiple" field (:pr:`585`).
269
370 7.0.1 (2020-12-14)
471 ******************
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
2626 toxenvs:
2727 - lint
2828 - mypy
29 - py36
30 - py36-mindeps
3129 - py37
32 - py38
33 - py39
34 - py39-marshmallowdev
30 - py37-mindeps
31 - py310
32 - py310-marshmallowdev
3533 - docs
3634 os: linux
3735 # Build wheels
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 }
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 -------------
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
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 "webtest==3.0.0",
1717 "webtest-aiohttp==2.0.0",
1818 "pytest-aiohttp>=0.3.0",
1919 ]
2020 + FRAMEWORKS,
2121 "lint": [
22 "mypy==0.790",
23 "flake8==3.8.4",
24 "flake8-bugbear==20.11.1",
22 "mypy==0.931",
23 "flake8==4.0.1",
24 "flake8-bugbear==21.11.29",
2525 "pre-commit~=2.4",
2626 ],
27 "docs": ["Sphinx==3.3.1", "sphinx-issues==1.2.0", "sphinx-typlog-theme==0.8.0"]
27 "docs": [
28 "Sphinx==4.3.2",
29 "sphinx-issues==3.0.1",
30 "furo==2022.1.2",
31 ]
2832 + FRAMEWORKS,
2933 }
3034 EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"]
6872 packages=find_packages("src"),
6973 package_dir={"": "src"},
7074 package_data={"webargs": ["py.typed"]},
71 install_requires=["marshmallow>=3.0.0"],
75 install_requires=["marshmallow>=3.0.0", "packaging"],
7276 extras_require=EXTRAS_REQUIRE,
7377 license="MIT",
7478 zip_safe=False,
8892 "api",
8993 "marshmallow",
9094 ),
91 python_requires=">=3.6",
95 python_requires=">=3.7",
9296 classifiers=[
9397 "Development Status :: 5 - Production/Stable",
9498 "Intended Audience :: Developers",
9599 "License :: OSI Approved :: MIT License",
96100 "Natural Language :: English",
97101 "Programming Language :: Python :: 3",
98 "Programming Language :: Python :: 3.6",
99102 "Programming Language :: Python :: 3.7",
100103 "Programming Language :: Python :: 3.8",
101104 "Programming Language :: Python :: 3.9",
105 "Programming Language :: Python :: 3.10",
102106 "Programming Language :: Python :: 3 :: Only",
103107 "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
104108 "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.1.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.
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."]}
00 """Asynchronous request parser."""
1 from __future__ import annotations
2
13 import asyncio
24 import functools
35 import inspect
46 import typing
5 from collections.abc import Mapping
67
78 from marshmallow import Schema, ValidationError
89 import marshmallow as ma
2122 async def parse(
2223 self,
2324 argmap: core.ArgMap,
24 req: typing.Optional[core.Request] = None,
25 req: core.Request | None = None,
2526 *,
26 location: typing.Optional[str] = None,
27 unknown: typing.Optional[str] = core._UNKNOWN_DEFAULT_PARAM,
27 location: str | None = None,
28 unknown: str | None = core._UNKNOWN_DEFAULT_PARAM,
2829 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]:
30 error_status_code: int | None = None,
31 error_headers: typing.Mapping[str, str] | None = None,
32 ) -> typing.Mapping | None:
3233 """Coroutine variant of `webargs.core.Parser`.
3334
3435 Receives the same arguments as `webargs.core.Parser.parse`.
4445 else self.DEFAULT_UNKNOWN_BY_LOCATION.get(location)
4546 )
4647 )
47 load_kwargs: typing.Dict[str, typing.Any] = (
48 {"unknown": unknown} if unknown else {}
49 )
48 load_kwargs: dict[str, typing.Any] = {"unknown": unknown} if unknown else {}
5049 if req is None:
5150 raise ValueError("Must pass req object")
5251 data = None
9594 schema: Schema,
9695 location: str,
9796 *,
98 error_status_code: typing.Optional[int],
99 error_headers: typing.Optional[typing.Mapping[str, str]]
97 error_status_code: int | None,
98 error_headers: typing.Mapping[str, str] | None,
10099 ) -> typing.NoReturn:
101100 # rewrite messages to be namespaced under the location which created
102101 # them
132131 def use_args(
133132 self,
134133 argmap: core.ArgMap,
135 req: typing.Optional[core.Request] = None,
134 req: core.Request | None = None,
136135 *,
137136 location: str = None,
138137 unknown=core._UNKNOWN_DEFAULT_PARAM,
139138 as_kwargs: bool = False,
140139 validate: core.ValidateArg = None,
141 error_status_code: typing.Optional[int] = None,
142 error_headers: typing.Optional[typing.Mapping[str, str]] = None
140 error_status_code: int | None = None,
141 error_headers: typing.Mapping[str, str] | None = None,
143142 ) -> typing.Callable[..., typing.Callable]:
144143 """Decorator that injects parsed arguments into a view function or method.
145144
149148 request_obj = req
150149 # Optimization: If argmap is passed as a dictionary, we only need
151150 # to generate a Schema once
152 if isinstance(argmap, Mapping):
151 if isinstance(argmap, dict):
153152 argmap = self.schema_class.from_dict(argmap)()
154153
155154 def decorator(func: typing.Callable) -> typing.Callable:
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
02 import functools
13 import typing
24 import logging
3 from collections.abc import Mapping
45 import json
56
67 import marshmallow as ma
78 from marshmallow import ValidationError
89 from marshmallow.utils import missing
910
10 from webargs.fields import DelimitedList
11 from webargs.multidictproxy import MultiDictProxy
1112
1213 logger = logging.getLogger(__name__)
1314
1415
1516 __all__ = [
1617 "ValidationError",
17 "is_multiple",
1818 "Parser",
1919 "missing",
2020 "parse_json",
2424 Request = typing.TypeVar("Request")
2525 ArgMap = typing.Union[
2626 ma.Schema,
27 typing.Mapping[str, ma.fields.Field],
27 typing.Dict[str, typing.Union[ma.fields.Field, typing.Type[ma.fields.Field]]],
2828 typing.Callable[[Request], ma.Schema],
2929 ]
3030 ValidateArg = typing.Union[None, typing.Callable, typing.Iterable[typing.Callable]]
3232 ErrorHandler = typing.Callable[..., typing.NoReturn]
3333 # generic type var with no particular meaning
3434 T = typing.TypeVar("T")
35 # type var for callables, to make type-preserving decorators
36 C = typing.TypeVar("C", bound=typing.Callable)
37 # type var for a callable which is an error handler
38 # used to ensure that the error_handler decorator is type preserving
39 ErrorHandlerT = typing.TypeVar("ErrorHandlerT", bound=ErrorHandler)
3540
3641
3742 # a value used as the default for arguments, so that when `None` is passed, it
4752 return callable(x)
4853
4954
50 def _callable_or_raise(obj: typing.Optional[T]) -> typing.Optional[T]:
55 def _callable_or_raise(obj: T | None) -> T | None:
5156 """Makes sure an object is callable if it is not ``None``. If not
5257 callable, a ValueError is raised.
5358 """
5661 return obj
5762
5863
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
6464 def get_mimetype(content_type: str) -> str:
6565 return content_type.split(";")[0].strip()
6666
6767
6868 # Adapted from werkzeug:
6969 # https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/wrappers.py
70 def is_json(mimetype: typing.Optional[str]) -> bool:
70 def is_json(mimetype: str | None) -> bool:
7171 """Indicates if this mimetype is JSON or not. By default a request
7272 is considered to include JSON data if the mimetype is
7373 ``application/json`` or ``application/*+json``.
9494 f"Bytes decoding error : {exc.reason}",
9595 doc=str(exc.object),
9696 pos=exc.start,
97 )
97 ) from exc
9898 return json.loads(decoded)
9999
100100
131131 DEFAULT_LOCATION: str = "json"
132132 #: Default value to use for 'unknown' on schema load
133133 # 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,
134 DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = {
135 "json": None,
136 "form": None,
137 "json_or_form": None,
138138 "querystring": ma.EXCLUDE,
139139 "query": ma.EXCLUDE,
140140 "headers": ma.EXCLUDE,
142142 "files": ma.EXCLUDE,
143143 }
144144 #: The marshmallow Schema class to use when creating new schemas
145 DEFAULT_SCHEMA_CLASS: typing.Type = ma.Schema
145 DEFAULT_SCHEMA_CLASS: type[ma.Schema] = ma.Schema
146146 #: Default status code to return for validation errors
147147 DEFAULT_VALIDATION_STATUS: int = DEFAULT_VALIDATION_STATUS
148148 #: Default error message for validation errors
149149 DEFAULT_VALIDATION_MESSAGE: str = "Invalid value."
150 #: field types which should always be treated as if they set `is_multiple=True`
151 KNOWN_MULTI_FIELDS: list[type] = [ma.fields.List, ma.fields.Tuple]
150152
151153 #: Maps location => method name
152 __location_map__: typing.Dict[str, typing.Union[str, typing.Callable]] = {
154 __location_map__: dict[str, str | typing.Callable] = {
153155 "json": "load_json",
154156 "querystring": "load_querystring",
155157 "query": "load_querystring",
162164
163165 def __init__(
164166 self,
165 location: typing.Optional[str] = None,
167 location: str | None = None,
166168 *,
167 unknown: typing.Optional[str] = _UNKNOWN_DEFAULT_PARAM,
168 error_handler: typing.Optional[ErrorHandler] = None,
169 schema_class: typing.Optional[typing.Type] = None
169 unknown: str | None = _UNKNOWN_DEFAULT_PARAM,
170 error_handler: ErrorHandler | None = None,
171 schema_class: type[ma.Schema] | None = None,
170172 ):
171173 self.location = location or self.DEFAULT_LOCATION
172 self.error_callback: typing.Optional[ErrorHandler] = _callable_or_raise(
173 error_handler
174 )
174 self.error_callback: ErrorHandler | None = _callable_or_raise(error_handler)
175175 self.schema_class = schema_class or self.DEFAULT_SCHEMA_CLASS
176176 self.unknown = unknown
177
178 def _makeproxy(self, multidict, schema: ma.Schema, cls: type = MultiDictProxy):
179 """Create a multidict proxy object with options from the current parser"""
180 return cls(multidict, schema, known_multi_fields=tuple(self.KNOWN_MULTI_FIELDS))
177181
178182 def _get_loader(self, location: str) -> typing.Callable:
179183 """Get the loader function for the given location.
215219 schema: ma.Schema,
216220 location: str,
217221 *,
218 error_status_code: typing.Optional[int],
219 error_headers: typing.Optional[typing.Mapping[str, str]]
222 error_status_code: int | None,
223 error_headers: typing.Mapping[str, str] | None,
220224 ) -> typing.NoReturn:
221225 # rewrite messages to be namespaced under the location which created
222226 # them
256260 schema = argmap()
257261 elif callable(argmap):
258262 schema = argmap(req)
263 elif isinstance(argmap, dict):
264 schema = self.schema_class.from_dict(argmap)()
259265 else:
260 schema = self.schema_class.from_dict(argmap)()
266 raise TypeError(f"argmap was of unexpected type {type(argmap)}")
261267 return schema
262268
263269 def parse(
264270 self,
265271 argmap: ArgMap,
266 req: typing.Optional[Request] = None,
272 req: Request | None = None,
267273 *,
268 location: typing.Optional[str] = None,
269 unknown: typing.Optional[str] = _UNKNOWN_DEFAULT_PARAM,
274 location: str | None = None,
275 unknown: str | None = _UNKNOWN_DEFAULT_PARAM,
270276 validate: ValidateArg = None,
271 error_status_code: typing.Optional[int] = None,
272 error_headers: typing.Optional[typing.Mapping[str, str]] = None
277 error_status_code: int | None = None,
278 error_headers: typing.Mapping[str, str] | None = None,
273279 ):
274280 """Main request parsing method.
275281
307313 else self.DEFAULT_UNKNOWN_BY_LOCATION.get(location)
308314 )
309315 )
310 load_kwargs: typing.Dict[str, typing.Any] = (
311 {"unknown": unknown} if unknown else {}
312 )
316 load_kwargs: dict[str, typing.Any] = {"unknown": unknown} if unknown else {}
313317 if req is None:
314318 raise ValueError("Must pass req object")
315319 data = None
319323 location_data = self._load_location_data(
320324 schema=schema, req=req, location=location
321325 )
322 data = schema.load(location_data, **load_kwargs)
326 preprocessed_data = self.pre_load(
327 location_data, schema=schema, req=req, location=location
328 )
329 data = schema.load(preprocessed_data, **load_kwargs)
323330 self._validate_arguments(data, validators)
324331 except ma.exceptions.ValidationError as error:
325332 self._on_validation_error(
335342 ) from error
336343 return data
337344
338 def get_default_request(self) -> typing.Optional[Request]:
345 def get_default_request(self) -> Request | None:
339346 """Optional override. Provides a hook for frameworks that use thread-local
340347 request objects.
341348 """
344351 def get_request_from_view_args(
345352 self,
346353 view: typing.Callable,
347 args: typing.Tuple,
354 args: tuple,
348355 kwargs: typing.Mapping[str, typing.Any],
349 ) -> typing.Optional[Request]:
356 ) -> Request | None:
350357 """Optional override. Returns the request object to be parsed, given a view
351358 function's args and kwargs.
352359
362369
363370 @staticmethod
364371 def _update_args_kwargs(
365 args: typing.Tuple,
366 kwargs: typing.Dict[str, typing.Any],
367 parsed_args: typing.Tuple,
372 args: tuple,
373 kwargs: dict[str, typing.Any],
374 parsed_args: tuple,
368375 as_kwargs: bool,
369 ) -> typing.Tuple[typing.Tuple, typing.Mapping]:
376 ) -> tuple[tuple, typing.Mapping]:
370377 """Update args or kwargs with parsed_args depending on as_kwargs"""
371378 if as_kwargs:
372379 kwargs.update(parsed_args)
378385 def use_args(
379386 self,
380387 argmap: ArgMap,
381 req: typing.Optional[Request] = None,
388 req: Request | None = None,
382389 *,
383 location: typing.Optional[str] = None,
384 unknown: typing.Optional[str] = _UNKNOWN_DEFAULT_PARAM,
390 location: str | None = None,
391 unknown: str | None = _UNKNOWN_DEFAULT_PARAM,
385392 as_kwargs: bool = False,
386393 validate: ValidateArg = None,
387 error_status_code: typing.Optional[int] = None,
388 error_headers: typing.Optional[typing.Mapping[str, str]] = None
394 error_status_code: int | None = None,
395 error_headers: typing.Mapping[str, str] | None = None,
389396 ) -> typing.Callable[..., typing.Callable]:
390397 """Decorator that injects parsed arguments into a view function or method.
391398
415422 request_obj = req
416423 # Optimization: If argmap is passed as a dictionary, we only need
417424 # to generate a Schema once
418 if isinstance(argmap, Mapping):
425 if isinstance(argmap, dict):
419426 argmap = self.schema_class.from_dict(argmap)()
420427
421428 def decorator(func):
466473 kwargs["as_kwargs"] = True
467474 return self.use_args(*args, **kwargs)
468475
469 def location_loader(self, name: str):
476 def location_loader(self, name: str) -> typing.Callable[[C], C]:
470477 """Decorator that registers a function for loading a request location.
471478 The wrapped function receives a schema and a request.
472479
487494 :param str name: The name of the location to register.
488495 """
489496
490 def decorator(func):
497 def decorator(func: C) -> C:
491498 self.__location_map__[name] = func
492499 return func
493500
494501 return decorator
495502
496 def error_handler(self, func: ErrorHandler) -> ErrorHandler:
503 def error_handler(self, func: ErrorHandlerT) -> ErrorHandlerT:
497504 """Decorator that registers a custom error handling function. The
498505 function should receive the raised error, request object,
499506 `marshmallow.Schema` instance used to parse the request, error status code,
520527 self.error_callback = func
521528 return func
522529
530 def pre_load(
531 self,
532 location_data: typing.Mapping,
533 *,
534 schema: ma.Schema,
535 req: Request,
536 location: str,
537 ) -> typing.Mapping:
538 """A method of the parser which can transform data after location
539 loading is done. By default it does nothing, but users can subclass
540 parsers and override this method.
541 """
542 return location_data
543
523544 def _handle_invalid_json_error(
524545 self,
525 error: typing.Union[json.JSONDecodeError, UnicodeDecodeError],
546 error: json.JSONDecodeError | UnicodeDecodeError,
526547 req: Request,
527548 *args,
528 **kwargs
549 **kwargs,
529550 ) -> typing.NoReturn:
530551 """Internal hook for overriding treatment of JSONDecodeErrors.
531552
619640 schema: ma.Schema,
620641 *,
621642 error_status_code: int,
622 error_headers: typing.Mapping[str, str]
643 error_headers: typing.Mapping[str, str],
623644 ) -> typing.NoReturn:
624645 """Called if an error occurs while parsing args. By default, just logs and
625646 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
2525 class Nested(ma.fields.Nested): # type: ignore[no-redef]
5454 """
5555
5656 delimiter: str = ","
57 # delimited fields set is_multiple=False for webargs.core.is_multiple
58 is_multiple: bool = False
5759
5860 def _serialize(self, value, attr, obj, **kwargs):
5961 # serializing will start with parent-class serialization, so that we correctly
6668 # attempting to deserialize from a non-string source is an error
6769 if not isinstance(value, (str, bytes)):
6870 raise self.make_error("invalid")
69 return super()._deserialize(value.split(self.delimiter), attr, data, **kwargs)
71 values = value.split(self.delimiter) if value else []
72 return super()._deserialize(values, attr, data, **kwargs)
7073
7174
7275 class DelimitedList(DelimitedFieldMixin, ma.fields.List):
8487
8588 def __init__(
8689 self,
87 cls_or_instance: typing.Union[ma.fields.Field, type],
90 cls_or_instance: ma.fields.Field | type,
8891 *,
89 delimiter: typing.Optional[str] = None,
90 **kwargs
92 delimiter: str | None = None,
93 **kwargs,
9194 ):
9295 self.delimiter = delimiter or self.delimiter
9396 super().__init__(cls_or_instance, **kwargs)
106109
107110 default_error_messages = {"invalid": "Not a valid delimited tuple."}
108111
109 def __init__(
110 self, tuple_fields, *, delimiter: typing.Optional[str] = None, **kwargs
111 ):
112 def __init__(self, tuple_fields, *, delimiter: str | None = None, **kwargs):
112113 self.delimiter = delimiter or self.delimiter
113114 super().__init__(tuple_fields, **kwargs)
1919 uid=uid, per_page=args["per_page"]
2020 )
2121 """
22 from __future__ import annotations
23
2224 import flask
2325 from werkzeug.exceptions import HTTPException
2426
2527 import marshmallow as ma
2628
2729 from webargs import core
28 from webargs.multidictproxy import MultiDictProxy
2930
3031
3132 def abort(http_status_code, exc=None, **kwargs):
4950 class FlaskParser(core.Parser):
5051 """Flask request argument parser."""
5152
52 DEFAULT_UNKNOWN_BY_LOCATION = {
53 DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = {
5354 "view_args": ma.RAISE,
5455 "path": ma.RAISE,
5556 **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION,
7980
8081 def load_querystring(self, req, schema):
8182 """Return query params from the request as a MultiDictProxy."""
82 return MultiDictProxy(req.args, schema)
83 return self._makeproxy(req.args, schema)
8384
8485 def load_form(self, req, schema):
8586 """Return form values from the request as a MultiDictProxy."""
86 return MultiDictProxy(req.form, schema)
87 return self._makeproxy(req.form, schema)
8788
8889 def load_headers(self, req, schema):
8990 """Return headers from the request as a MultiDictProxy."""
90 return MultiDictProxy(req.headers, schema)
91 return self._makeproxy(req.headers, schema)
9192
9293 def load_cookies(self, req, schema):
9394 """Return cookies from the request."""
9596
9697 def load_files(self, req, schema):
9798 """Return files from the request as a MultiDictProxy."""
98 return MultiDictProxy(req.files, schema)
99 return self._makeproxy(req.files, schema)
99100
100101 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
101102 """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(
4040 body=json.dumps(["Invalid JSON."]).encode("utf-8"),
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(
5151 body=json.dumps(["Invalid JSON."]).encode("utf-8"),
5252 content_type="application/json",
53 )
53 ) from exc
5454 return json_response(parsed)
5555
5656
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"^echo_form$", views.echo_form),
8 re_path(r"^echo_json$", views.echo_json),
9 re_path(r"^echo_json_or_form$", views.echo_json_or_form),
10 re_path(r"^echo_use_args$", views.echo_use_args),
11 re_path(r"^echo_use_args_validated$", views.echo_use_args_validated),
12 re_path(r"^echo_ignoring_extra_data$", views.echo_ignoring_extra_data),
13 re_path(r"^echo_use_kwargs$", views.echo_use_kwargs),
14 re_path(r"^echo_multi$", views.echo_multi),
15 re_path(r"^echo_multi_form$", views.echo_multi_form),
16 re_path(r"^echo_multi_json$", views.echo_multi_json),
17 re_path(r"^echo_many_schema$", views.echo_many_schema),
18 re_path(
1919 r"^echo_use_args_with_path_param/(?P<name>\w+)$",
2020 views.echo_use_args_with_path_param,
2121 ),
22 url(
22 re_path(
2323 r"^echo_use_kwargs_with_path_param/(?P<name>\w+)$",
2424 views.echo_use_kwargs_with_path_param,
2525 ),
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(
26 re_path(r"^error$", views.always_error),
27 re_path(r"^echo_headers$", views.echo_headers),
28 re_path(r"^echo_cookie$", views.echo_cookie),
29 re_path(r"^echo_file$", views.echo_file),
30 re_path(r"^echo_nested$", views.echo_nested),
31 re_path(r"^echo_nested_many$", views.echo_nested_many),
32 re_path(r"^echo_cbv$", views.EchoCBV.as_view()),
33 re_path(r"^echo_use_args_cbv$", views.EchoUseArgsCBV.as_view()),
34 re_path(
3535 r"^echo_use_args_with_path_param_cbv/(?P<pid>\d+)$",
3636 views.EchoUseArgsWithParamCBV.as_view(),
3737 ),
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
868873 assert viewfunc() == {"username": "foo"}
869874
870875
876 def test_delimited_list_empty_string(web_request, parser):
877 web_request.json = {"dates": ""}
878 schema_cls = Schema.from_dict({"dates": fields.DelimitedList(fields.Str())})
879 schema = schema_cls()
880
881 parsed = parser.parse(schema, web_request)
882 assert parsed["dates"] == []
883
884 data = schema.dump(parsed)
885 assert data["dates"] == ""
886
887
871888 def test_delimited_list_default_delimiter(web_request, parser):
872889 web_request.json = {"ids": "1,2,3"}
873890 schema_cls = Schema.from_dict({"ids": fields.DelimitedList(fields.Int())})
10311048 parser.parse(args, web_request)
10321049
10331050
1051 @pytest.mark.parametrize("input_dict", multidicts)
1052 @pytest.mark.parametrize(
1053 "setting",
1054 [
1055 "is_multiple_true",
1056 "is_multiple_false",
1057 "is_multiple_notset",
1058 "list_field",
1059 "tuple_field",
1060 "added_to_known",
1061 ],
1062 )
1063 def test_is_multiple_detection(web_request, parser, input_dict, setting):
1064 # this custom class "multiplexes" in that it can be given a single value or
1065 # list of values -- a single value is treated as a string, and a list of
1066 # values is treated as a list of strings
1067 class CustomMultiplexingField(fields.String):
1068 def _deserialize(self, value, attr, data, **kwargs):
1069 if isinstance(value, str):
1070 return super()._deserialize(value, attr, data, **kwargs)
1071 return [
1072 self._deserialize(v, attr, data, **kwargs)
1073 for v in value
1074 if isinstance(v, str)
1075 ]
1076
1077 def _serialize(self, value, attr, **kwargs):
1078 if isinstance(value, str):
1079 return super()._serialize(value, attr, **kwargs)
1080 return [
1081 self._serialize(v, attr, **kwargs) for v in value if isinstance(v, str)
1082 ]
1083
1084 class CustomMultipleField(CustomMultiplexingField):
1085 is_multiple = True
1086
1087 class CustomNonMultipleField(CustomMultiplexingField):
1088 is_multiple = False
1089
1090 # the request's query params are the input multidict
1091 web_request.query = input_dict
1092
1093 # case 1: is_multiple=True
1094 if setting == "is_multiple_true":
1095 # the multidict should unpack to a list of strings
1096 #
1097 # order is not necessarily guaranteed by the multidict implementations, but
1098 # both values must be present
1099 args = {"foos": CustomMultipleField()}
1100 result = parser.parse(args, web_request, location="query")
1101 assert result["foos"] in (["a", "b"], ["b", "a"])
1102 # case 2: is_multiple=False
1103 elif setting == "is_multiple_false":
1104 # the multidict should unpack to a string
1105 #
1106 # either value may be returned, depending on the multidict implementation,
1107 # but not both
1108 args = {"foos": CustomNonMultipleField()}
1109 result = parser.parse(args, web_request, location="query")
1110 assert result["foos"] in ("a", "b")
1111 # case 3: is_multiple is not set
1112 elif setting == "is_multiple_notset":
1113 # this should be the same as is_multiple=False
1114 args = {"foos": CustomMultiplexingField()}
1115 result = parser.parse(args, web_request, location="query")
1116 assert result["foos"] in ("a", "b")
1117 # case 4: the field is a List (special case)
1118 elif setting == "list_field":
1119 # this should behave like the is_multiple=True case
1120 args = {"foos": fields.List(fields.Str())}
1121 result = parser.parse(args, web_request, location="query")
1122 assert result["foos"] in (["a", "b"], ["b", "a"])
1123 # case 5: the field is a Tuple (special case)
1124 elif setting == "tuple_field":
1125 # this should behave like the is_multiple=True case and produce a tuple
1126 args = {"foos": fields.Tuple((fields.Str, fields.Str))}
1127 result = parser.parse(args, web_request, location="query")
1128 assert result["foos"] in (("a", "b"), ("b", "a"))
1129 # case 6: the field is custom, but added to the known fields of the parser
1130 elif setting == "added_to_known":
1131 # if it's included in the known multifields and is_multiple is not set, behave
1132 # like is_multiple=True
1133 parser.KNOWN_MULTI_FIELDS.append(CustomMultiplexingField)
1134 args = {"foos": CustomMultiplexingField()}
1135 result = parser.parse(args, web_request, location="query")
1136 assert result["foos"] in (["a", "b"], ["b", "a"])
1137 else:
1138 raise NotImplementedError
1139
1140
10341141 def test_validation_errors_in_validator_are_passed_to_handle_error(parser, web_request):
10351142 def validate(value):
10361143 raise ValidationError("Something went wrong.")
11331240 p = CustomParser()
11341241 ret = p.parse(argmap, web_request)
11351242 assert ret == {"value": "hello world"}
1243
1244
1245 def test_parser_pre_load(web_request):
1246 class CustomParser(MockRequestParser):
1247 # pre-load hook to strip whitespace from query params
1248 def pre_load(self, data, *, schema, req, location):
1249 if location == "query":
1250 return {k: v.strip() for k, v in data.items()}
1251 return data
1252
1253 parser = CustomParser()
1254
1255 # mock data for both query and json
1256 web_request.query = web_request.json = {"value": " hello "}
1257 argmap = {"value": fields.Str()}
1258
1259 # data gets through for 'json' just fine
1260 ret = parser.parse(argmap, web_request)
1261 assert ret == {"value": " hello "}
1262
1263 # but for 'query', the pre_load hook changes things
1264 ret = parser.parse(argmap, web_request, location="query")
1265 assert ret == {"value": "hello"}
1266
1267
1268 # this test is meant to be a run of the WhitspaceStrippingFlaskParser we give
1269 # in the docs/advanced.rst examples for how to use pre_load
1270 # this helps ensure that the example code is correct
1271 # rather than a FlaskParser, we're working with the mock parser, but it's
1272 # otherwise the same
1273 def test_whitespace_stripping_parser_example(web_request):
1274 def _strip_whitespace(value):
1275 if isinstance(value, str):
1276 value = value.strip()
1277 elif isinstance(value, typing.Mapping):
1278 return {k: _strip_whitespace(value[k]) for k in value}
1279 elif isinstance(value, (list, tuple)):
1280 return type(value)(map(_strip_whitespace, value))
1281 return value
1282
1283 class WhitspaceStrippingParser(MockRequestParser):
1284 def pre_load(self, location_data, *, schema, req, location):
1285 if location in ("query", "form"):
1286 ret = _strip_whitespace(location_data)
1287 return ret
1288 return location_data
1289
1290 parser = WhitspaceStrippingParser()
1291
1292 # mock data for query, form, and json
1293 web_request.form = web_request.query = web_request.json = {"value": " hello "}
1294 argmap = {"value": fields.Str()}
1295
1296 # data gets through for 'json' just fine
1297 ret = parser.parse(argmap, web_request)
1298 assert ret == {"value": " hello "}
1299
1300 # but for 'query' and 'form', the pre_load hook changes things
1301 for loc in ("query", "form"):
1302 ret = parser.parse(argmap, web_request, location=loc)
1303 assert ret == {"value": "hello"}
1304
1305 # check that it applies in the case where the field is a list type
1306 # applied to an argument (logic for `tuple` is effectively the same)
1307 web_request.form = web_request.query = web_request.json = {
1308 "ids": [" 1", "3", " 4"],
1309 "values": [" foo ", " bar"],
1310 }
1311 schema = Schema.from_dict(
1312 {"ids": fields.List(fields.Int), "values": fields.List(fields.Str)}
1313 )
1314 for loc in ("query", "form"):
1315 ret = parser.parse(schema, web_request, location=loc)
1316 assert ret == {"ids": [1, 3, 4], "values": ["foo", "bar"]}
1317
1318 # json loading should also work even though the pre_load hook above
1319 # doesn't strip whitespace from JSON data
1320 # - values=[" foo ", ...] will have whitespace preserved
1321 # - ids=[" 1", ...] will still parse okay because " 1" is valid for fields.Int
1322 ret = parser.parse(schema, web_request, location="json")
1323 assert ret == {"ids": [1, 3, 4], "values": [" foo ", " bar"]}
1324
1325
1326 def test_parse_rejects_non_dict_argmap_mapping(parser, web_request):
1327 web_request.json = {"username": 42, "password": 42}
1328 argmap = collections.UserDict(
1329 {"username": fields.Field(), "password": fields.Field()}
1330 )
1331
1332 # UserDict is dict-like in all meaningful ways, but not a subclass of `dict`
1333 # it will therefore be rejected with a TypeError when used
1334 with pytest.raises(TypeError):
1335 parser.parse(argmap, web_request)
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]
2828 # `webargs` and `marshmallow` both installed is a valuable safeguard against
2929 # issues in which `mypy` running on every file standalone won't catch things
3030 [testenv:mypy]
31 deps = mypy
31 deps = mypy==0.930
32 extras = frameworks
3233 commands = mypy src/
3334
3435 [testenv:docs]