Codebase list python-webargs / 90367ef
Import upstream version 8.0.1 Kali Janitor 2 years ago
25 changed file(s) with 569 addition(s) and 102 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.11.0
33 hooks:
44 - id: pyupgrade
55 args: ["--py36-plus"]
88 hooks:
99 - id: black
1010 - repo: https://gitlab.com/pycqa/flake8
11 rev: 3.8.4
11 rev: 3.9.0
1212 hooks:
1313 - id: flake8
14 additional_dependencies: [flake8-bugbear==20.1.0]
14 additional_dependencies: [flake8-bugbear==21.4.3]
1515 - repo: https://github.com/asottile/blacken-docs
16 rev: v1.8.0
16 rev: v1.10.0
1717 hooks:
1818 - id: blacken-docs
1919 additional_dependencies: [black==20.8b1]
2020 args: ["--target-version", "py35"]
2121 - repo: https://github.com/pre-commit/mirrors-mypy
22 rev: v0.790
22 rev: v0.812
2323 hooks:
2424 - id: mypy
2525 language_version: python3
2626 files: ^src/webargs/
27 additional_dependencies: ["marshmallow>=3,<4"]
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>`_
00 Changelog
11 ---------
2
3 8.0.1 (2021-08-12)
4 ******************
5
6 Bug fixes:
7
8 * Fix "``DelimitedList`` deserializes empty string as ``['']``" (:issue:`623`).
9 Thanks :user:`TTWSchell` for reporting and for the PR.
10
11 Other changes:
12
13 * New documentation theme with `furo`. Thanks to :user:`pradyunsg` for writing
14 furo!
15 * Webargs has a new logo. Thanks to :user:`michaelizergit`! (:issue:`312`)
16 * Don't build universal wheels. We don't support Python 2 anymore.
17 (:pr:`632`)
18 * Make the build reproducible (:pr:`#631`).
19
20
21 8.0.0 (2021-04-08)
22 ******************
23
24 Features:
25
26 * Add `Parser.pre_load` as a method for allowing users to modify data before
27 schema loading, but without redefining location loaders. See advanced docs on
28 `Parser pre_load` for usage information. (:pr:`583`)
29
30 * *Backwards-incompatible*: ``unknown`` defaults to `None` for body locations
31 (`json`, `form` and `json_or_form`) (:issue:`580`).
32
33 * Detection of fields as "multi-value" for unpacking lists from multi-dict
34 types is now extensible with the ``is_multiple`` attribute. If a field sets
35 ``is_multiple = True`` it will be detected as a multi-value field. If
36 ``is_multiple`` is not set or is set to ``None``, webargs will check if the
37 field is an instance of ``List`` or ``Tuple``. (:issue:`563`)
38
39 * A new attribute on ``Parser`` objects, ``Parser.KNOWN_MULTI_FIELDS`` can be
40 used to set fields which should be detected as ``is_multiple=True`` even when
41 the attribute is not set (:pr:`592`).
42
43 See docs on "Multi-Field Detection" for more details.
44
45 Bug fixes:
46
47 * ``Tuple`` field now behaves as a "multiple" field (:pr:`585`).
248
349 7.0.1 (2020-12-14)
450 ******************
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
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 }
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 ++++++++++++++++
0 python-dateutil==2.8.1
0 python-dateutil==2.8.2
11 Flask
22 bottle
33 tornado
00 [metadata]
11 license_files = LICENSE
2
3 [bdist_wheel]
4 universal = 1
52
63 [flake8]
74 ignore = E203, E266, E501, W503
1919 ]
2020 + FRAMEWORKS,
2121 "lint": [
22 "mypy==0.790",
23 "flake8==3.8.4",
24 "flake8-bugbear==20.11.1",
22 "mypy==0.910",
23 "flake8==3.9.2",
24 "flake8-bugbear==21.4.3",
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.1.2",
29 "sphinx-issues==1.2.0",
30 "furo==2021.8.11b42",
31 ]
2832 + FRAMEWORKS,
2933 }
3034 EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"]
66 from webargs.core import ValidationError
77 from webargs import fields
88
9 __version__ = "7.0.1"
9 __version__ = "8.0.1"
1010 __version_info__ = tuple(LooseVersion(__version__).version)
1111 __all__ = ("ValidationError", "fields", "missing", "validate")
7070 class AIOHTTPParser(AsyncParser):
7171 """aiohttp request argument parser."""
7272
73 DEFAULT_UNKNOWN_BY_LOCATION = {
73 DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = {
7474 "match_info": RAISE,
7575 "path": RAISE,
7676 **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION,
8383
8484 def load_querystring(self, req, schema: Schema) -> MultiDictProxy:
8585 """Return query params from the request as a MultiDictProxy."""
86 return MultiDictProxy(req.query, schema)
86 return self._makeproxy(req.query, schema)
8787
8888 async def load_form(self, req, schema: Schema) -> MultiDictProxy:
8989 """Return form values from the request as a MultiDictProxy."""
9090 post_data = await req.post()
91 return MultiDictProxy(post_data, schema)
91 return self._makeproxy(post_data, schema)
9292
9393 async def load_json_or_form(
9494 self, req, schema: Schema
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(
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
77 from marshmallow import ValidationError
88 from marshmallow.utils import missing
99
10 from webargs.fields import DelimitedList
10 from webargs.multidictproxy import MultiDictProxy
1111
1212 logger = logging.getLogger(__name__)
1313
1414
1515 __all__ = [
1616 "ValidationError",
17 "is_multiple",
1817 "Parser",
1918 "missing",
2019 "parse_json",
5453 if obj and not _iscallable(obj):
5554 raise ValueError(f"{obj!r} is not callable.")
5655 return obj
57
58
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)
6256
6357
6458 def get_mimetype(content_type: str) -> str:
131125 DEFAULT_LOCATION: str = "json"
132126 #: Default value to use for 'unknown' on schema load
133127 # 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,
128 DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = {
129 "json": None,
130 "form": None,
131 "json_or_form": None,
138132 "querystring": ma.EXCLUDE,
139133 "query": ma.EXCLUDE,
140134 "headers": ma.EXCLUDE,
147141 DEFAULT_VALIDATION_STATUS: int = DEFAULT_VALIDATION_STATUS
148142 #: Default error message for validation errors
149143 DEFAULT_VALIDATION_MESSAGE: str = "Invalid value."
144 #: field types which should always be treated as if they set `is_multiple=True`
145 KNOWN_MULTI_FIELDS: typing.List[typing.Type] = [ma.fields.List, ma.fields.Tuple]
150146
151147 #: Maps location => method name
152148 __location_map__: typing.Dict[str, typing.Union[str, typing.Callable]] = {
174170 )
175171 self.schema_class = schema_class or self.DEFAULT_SCHEMA_CLASS
176172 self.unknown = unknown
173
174 def _makeproxy(
175 self, multidict, schema: ma.Schema, cls: typing.Type = MultiDictProxy
176 ):
177 """Create a multidict proxy object with options from the current parser"""
178 return cls(multidict, schema, known_multi_fields=tuple(self.KNOWN_MULTI_FIELDS))
177179
178180 def _get_loader(self, location: str) -> typing.Callable:
179181 """Get the loader function for the given location.
319321 location_data = self._load_location_data(
320322 schema=schema, req=req, location=location
321323 )
322 data = schema.load(location_data, **load_kwargs)
324 preprocessed_data = self.pre_load(
325 location_data, schema=schema, req=req, location=location
326 )
327 data = schema.load(preprocessed_data, **load_kwargs)
323328 self._validate_arguments(data, validators)
324329 except ma.exceptions.ValidationError as error:
325330 self._on_validation_error(
520525 self.error_callback = func
521526 return func
522527
528 def pre_load(
529 self, location_data: Mapping, *, schema: ma.Schema, req: Request, location: str
530 ) -> Mapping:
531 """A method of the parser which can transform data after location
532 loading is done. By default it does nothing, but users can subclass
533 parsers and override this method.
534 """
535 return location_data
536
523537 def _handle_invalid_json_error(
524538 self,
525539 error: typing.Union[json.JSONDecodeError, UnicodeDecodeError],
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.
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):
1919 uid=uid, per_page=args["per_page"]
2020 )
2121 """
22 import typing
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: typing.Dict[str, typing.Optional[str]] = {
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)
2424 server.serve_forever()
2525 """
2626 import functools
27 import typing
2728 from collections.abc import Mapping
2829
2930 from webob.multidict import MultiDict
3334
3435 from webargs import core
3536 from webargs.core import json
36 from webargs.multidictproxy import MultiDictProxy
3737
3838
3939 def is_json_request(req):
4343 class PyramidParser(core.Parser):
4444 """Pyramid request argument parser."""
4545
46 DEFAULT_UNKNOWN_BY_LOCATION = {
46 DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = {
4747 "matchdict": ma.RAISE,
4848 "path": ma.RAISE,
4949 **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION,
6666
6767 def load_querystring(self, req, schema):
6868 """Return query params from the request as a MultiDictProxy."""
69 return MultiDictProxy(req.GET, schema)
69 return self._makeproxy(req.GET, schema)
7070
7171 def load_form(self, req, schema):
7272 """Return form values from the request as a MultiDictProxy."""
73 return MultiDictProxy(req.POST, schema)
73 return self._makeproxy(req.POST, schema)
7474
7575 def load_cookies(self, req, schema):
7676 """Return cookies from the request as a MultiDictProxy."""
77 return MultiDictProxy(req.cookies, schema)
77 return self._makeproxy(req.cookies, schema)
7878
7979 def load_headers(self, req, schema):
8080 """Return headers from the request as a MultiDictProxy."""
81 return MultiDictProxy(req.headers, schema)
81 return self._makeproxy(req.headers, schema)
8282
8383 def load_files(self, req, schema):
8484 """Return files from the request as a MultiDictProxy."""
8585 files = ((k, v) for k, v in req.POST.items() if hasattr(v, "file"))
86 return MultiDictProxy(MultiDict(files), schema)
86 return self._makeproxy(MultiDict(files), schema)
8787
8888 def load_matchdict(self, req, schema):
8989 """Return the request's ``matchdict`` as a MultiDictProxy."""
90 return MultiDictProxy(req.matchdict, schema)
90 return self._makeproxy(req.matchdict, schema)
9191
9292 def handle_error(self, error, req, schema, *, error_status_code, error_headers):
9393 """Handles errors during parsing. Aborts the current HTTP request and
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`
00 import datetime
1 import typing
12 from unittest import mock
23
34 import pytest
3435 """A minimal parser implementation that parses mock requests."""
3536
3637 def load_querystring(self, req, schema):
37 return MultiDictProxy(req.query, schema)
38 return self._makeproxy(req.query, schema)
39
40 def load_form(self, req, schema):
41 return MultiDictProxy(req.form, schema)
3842
3943 def load_json(self, req, schema):
4044 return req.json
868872 assert viewfunc() == {"username": "foo"}
869873
870874
875 def test_delimited_list_empty_string(web_request, parser):
876 web_request.json = {"dates": ""}
877 schema_cls = Schema.from_dict({"dates": fields.DelimitedList(fields.Str())})
878 schema = schema_cls()
879
880 parsed = parser.parse(schema, web_request)
881 assert parsed["dates"] == []
882
883 data = schema.dump(parsed)
884 assert data["dates"] == ""
885
886
871887 def test_delimited_list_default_delimiter(web_request, parser):
872888 web_request.json = {"ids": "1,2,3"}
873889 schema_cls = Schema.from_dict({"ids": fields.DelimitedList(fields.Int())})
10311047 parser.parse(args, web_request)
10321048
10331049
1050 @pytest.mark.parametrize("input_dict", multidicts)
1051 @pytest.mark.parametrize(
1052 "setting",
1053 [
1054 "is_multiple_true",
1055 "is_multiple_false",
1056 "is_multiple_notset",
1057 "list_field",
1058 "tuple_field",
1059 "added_to_known",
1060 ],
1061 )
1062 def test_is_multiple_detection(web_request, parser, input_dict, setting):
1063 # this custom class "multiplexes" in that it can be given a single value or
1064 # list of values -- a single value is treated as a string, and a list of
1065 # values is treated as a list of strings
1066 class CustomMultiplexingField(fields.String):
1067 def _deserialize(self, value, attr, data, **kwargs):
1068 if isinstance(value, str):
1069 return super()._deserialize(value, attr, data, **kwargs)
1070 return [
1071 self._deserialize(v, attr, data, **kwargs)
1072 for v in value
1073 if isinstance(v, str)
1074 ]
1075
1076 def _serialize(self, value, attr, **kwargs):
1077 if isinstance(value, str):
1078 return super()._serialize(value, attr, **kwargs)
1079 return [
1080 self._serialize(v, attr, **kwargs) for v in value if isinstance(v, str)
1081 ]
1082
1083 class CustomMultipleField(CustomMultiplexingField):
1084 is_multiple = True
1085
1086 class CustomNonMultipleField(CustomMultiplexingField):
1087 is_multiple = False
1088
1089 # the request's query params are the input multidict
1090 web_request.query = input_dict
1091
1092 # case 1: is_multiple=True
1093 if setting == "is_multiple_true":
1094 # the multidict should unpack to a list of strings
1095 #
1096 # order is not necessarily guaranteed by the multidict implementations, but
1097 # both values must be present
1098 args = {"foos": CustomMultipleField()}
1099 result = parser.parse(args, web_request, location="query")
1100 assert result["foos"] in (["a", "b"], ["b", "a"])
1101 # case 2: is_multiple=False
1102 elif setting == "is_multiple_false":
1103 # the multidict should unpack to a string
1104 #
1105 # either value may be returned, depending on the multidict implementation,
1106 # but not both
1107 args = {"foos": CustomNonMultipleField()}
1108 result = parser.parse(args, web_request, location="query")
1109 assert result["foos"] in ("a", "b")
1110 # case 3: is_multiple is not set
1111 elif setting == "is_multiple_notset":
1112 # this should be the same as is_multiple=False
1113 args = {"foos": CustomMultiplexingField()}
1114 result = parser.parse(args, web_request, location="query")
1115 assert result["foos"] in ("a", "b")
1116 # case 4: the field is a List (special case)
1117 elif setting == "list_field":
1118 # this should behave like the is_multiple=True case
1119 args = {"foos": fields.List(fields.Str())}
1120 result = parser.parse(args, web_request, location="query")
1121 assert result["foos"] in (["a", "b"], ["b", "a"])
1122 # case 5: the field is a Tuple (special case)
1123 elif setting == "tuple_field":
1124 # this should behave like the is_multiple=True case and produce a tuple
1125 args = {"foos": fields.Tuple((fields.Str, fields.Str))}
1126 result = parser.parse(args, web_request, location="query")
1127 assert result["foos"] in (("a", "b"), ("b", "a"))
1128 # case 6: the field is custom, but added to the known fields of the parser
1129 elif setting == "added_to_known":
1130 # if it's included in the known multifields and is_multiple is not set, behave
1131 # like is_multiple=True
1132 parser.KNOWN_MULTI_FIELDS.append(CustomMultiplexingField)
1133 args = {"foos": CustomMultiplexingField()}
1134 result = parser.parse(args, web_request, location="query")
1135 assert result["foos"] in (["a", "b"], ["b", "a"])
1136 else:
1137 raise NotImplementedError
1138
1139
10341140 def test_validation_errors_in_validator_are_passed_to_handle_error(parser, web_request):
10351141 def validate(value):
10361142 raise ValidationError("Something went wrong.")
11331239 p = CustomParser()
11341240 ret = p.parse(argmap, web_request)
11351241 assert ret == {"value": "hello world"}
1242
1243
1244 def test_parser_pre_load(web_request):
1245 class CustomParser(MockRequestParser):
1246 # pre-load hook to strip whitespace from query params
1247 def pre_load(self, data, *, schema, req, location):
1248 if location == "query":
1249 return {k: v.strip() for k, v in data.items()}
1250 return data
1251
1252 parser = CustomParser()
1253
1254 # mock data for both query and json
1255 web_request.query = web_request.json = {"value": " hello "}
1256 argmap = {"value": fields.Str()}
1257
1258 # data gets through for 'json' just fine
1259 ret = parser.parse(argmap, web_request)
1260 assert ret == {"value": " hello "}
1261
1262 # but for 'query', the pre_load hook changes things
1263 ret = parser.parse(argmap, web_request, location="query")
1264 assert ret == {"value": "hello"}
1265
1266
1267 # this test is meant to be a run of the WhitspaceStrippingFlaskParser we give
1268 # in the docs/advanced.rst examples for how to use pre_load
1269 # this helps ensure that the example code is correct
1270 # rather than a FlaskParser, we're working with the mock parser, but it's
1271 # otherwise the same
1272 def test_whitespace_stripping_parser_example(web_request):
1273 def _strip_whitespace(value):
1274 if isinstance(value, str):
1275 value = value.strip()
1276 elif isinstance(value, typing.Mapping):
1277 return {k: _strip_whitespace(value[k]) for k in value}
1278 elif isinstance(value, (list, tuple)):
1279 return type(value)(map(_strip_whitespace, value))
1280 return value
1281
1282 class WhitspaceStrippingParser(MockRequestParser):
1283 def pre_load(self, location_data, *, schema, req, location):
1284 if location in ("query", "form"):
1285 ret = _strip_whitespace(location_data)
1286 return ret
1287 return location_data
1288
1289 parser = WhitspaceStrippingParser()
1290
1291 # mock data for query, form, and json
1292 web_request.form = web_request.query = web_request.json = {"value": " hello "}
1293 argmap = {"value": fields.Str()}
1294
1295 # data gets through for 'json' just fine
1296 ret = parser.parse(argmap, web_request)
1297 assert ret == {"value": " hello "}
1298
1299 # but for 'query' and 'form', the pre_load hook changes things
1300 for loc in ("query", "form"):
1301 ret = parser.parse(argmap, web_request, location=loc)
1302 assert ret == {"value": "hello"}
1303
1304 # check that it applies in the case where the field is a list type
1305 # applied to an argument (logic for `tuple` is effectively the same)
1306 web_request.form = web_request.query = web_request.json = {
1307 "ids": [" 1", "3", " 4"],
1308 "values": [" foo ", " bar"],
1309 }
1310 schema = Schema.from_dict(
1311 {"ids": fields.List(fields.Int), "values": fields.List(fields.Str)}
1312 )
1313 for loc in ("query", "form"):
1314 ret = parser.parse(schema, web_request, location=loc)
1315 assert ret == {"ids": [1, 3, 4], "values": ["foo", "bar"]}
1316
1317 # json loading should also work even though the pre_load hook above
1318 # doesn't strip whitespace from JSON data
1319 # - values=[" foo ", ...] will have whitespace preserved
1320 # - ids=[" 1", ...] will still parse okay because " 1" is valid for fields.Int
1321 ret = parser.parse(schema, web_request, location="json")
1322 assert ret == {"ids": [1, 3, 4], "values": [" foo ", " bar"]}
2929 # issues in which `mypy` running on every file standalone won't catch things
3030 [testenv:mypy]
3131 deps = mypy
32 extras = frameworks
3233 commands = mypy src/
3334
3435 [testenv:docs]