Codebase list python-webargs / 790c5e87-5c5d-47e2-a0e7-28464e7907db/upstream
Import upstream version 8.1.0 Kali Janitor 2 years ago
23 changed file(s) with 204 addition(s) and 166 deletion(s). Raw diff Collapse all Expand all
00 repos:
11 - repo: https://github.com/asottile/pyupgrade
2 rev: v2.11.0
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.9.0
10 - repo: https://github.com/pycqa/flake8
11 rev: 4.0.1
1212 hooks:
1313 - id: flake8
14 additional_dependencies: [flake8-bugbear==21.4.3]
14 additional_dependencies: [flake8-bugbear==21.11.29]
1515 - repo: https://github.com/asottile/blacken-docs
16 rev: v1.10.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.812
22 rev: v0.930
2323 hooks:
2424 - id: mypy
2525 language_version: python3
2626 files: ^src/webargs/
27 additional_dependencies: ["marshmallow>=3,<4"]
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>`_
5353 * Grey Li `@greyli <https://github.com/greyli>`_
5454 * `@michaelizergit <https://github.com/michaelizergit>`_
5555 * 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`).
223
324 8.0.1 (2021-08-12)
425 ******************
1536 * Webargs has a new logo. Thanks to :user:`michaelizergit`! (:issue:`312`)
1637 * Don't build universal wheels. We don't support Python 2 anymore.
1738 (:pr:`632`)
18 * Make the build reproducible (:pr:`#631`).
39 * Make the build reproducible (:pr:`631`).
1940
2041
2142 8.0.0 (2021-04-08)
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
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
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 -------------
487487 location=None,
488488 validate=None,
489489 error_status_code=None,
490 error_headers=None
490 error_headers=None,
491491 ):
492492 ...
493493
516516 as_kwargs=False,
517517 validate=None,
518518 error_status_code=None,
519 error_headers=None
519 error_headers=None,
520520 ):
521521 ...
522522
00 [tool.black]
11 line-length = 88
2 target-version = ['py35', 'py36', 'py37', 'py38']
2 target-version = ['py37', 'py38', 'py39', 'py310']
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.910",
23 "flake8==3.9.2",
24 "flake8-bugbear==21.4.3",
22 "mypy==0.931",
23 "flake8==4.0.1",
24 "flake8-bugbear==21.11.29",
2525 "pre-commit~=2.4",
2626 ],
2727 "docs": [
28 "Sphinx==4.1.2",
29 "sphinx-issues==1.2.0",
30 "furo==2021.8.11b42",
28 "Sphinx==4.3.2",
29 "sphinx-issues==3.0.1",
30 "furo==2022.1.2",
3131 ]
3232 + FRAMEWORKS,
3333 }
7272 packages=find_packages("src"),
7373 package_dir={"": "src"},
7474 package_data={"webargs": ["py.typed"]},
75 install_requires=["marshmallow>=3.0.0"],
75 install_requires=["marshmallow>=3.0.0", "packaging"],
7676 extras_require=EXTRAS_REQUIRE,
7777 license="MIT",
7878 zip_safe=False,
9292 "api",
9393 "marshmallow",
9494 ),
95 python_requires=">=3.6",
95 python_requires=">=3.7",
9696 classifiers=[
9797 "Development Status :: 5 - Production/Stable",
9898 "Intended Audience :: Developers",
9999 "License :: OSI Approved :: MIT License",
100100 "Natural Language :: English",
101101 "Programming Language :: Python :: 3",
102 "Programming Language :: Python :: 3.6",
103102 "Programming Language :: Python :: 3.7",
104103 "Programming Language :: Python :: 3.8",
105104 "Programming Language :: Python :: 3.9",
105 "Programming Language :: Python :: 3.10",
106106 "Programming Language :: Python :: 3 :: Only",
107107 "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
108108 "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__ = "8.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: typing.Dict[str, typing.Optional[str]] = {
75 DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = {
7476 "match_info": RAISE,
7577 "path": RAISE,
7678 **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION,
9092 post_data = await req.post()
9193 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
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:
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
2324 Request = typing.TypeVar("Request")
2425 ArgMap = typing.Union[
2526 ma.Schema,
26 typing.Mapping[str, ma.fields.Field],
27 typing.Dict[str, typing.Union[ma.fields.Field, typing.Type[ma.fields.Field]]],
2728 typing.Callable[[Request], ma.Schema],
2829 ]
2930 ValidateArg = typing.Union[None, typing.Callable, typing.Iterable[typing.Callable]]
3132 ErrorHandler = typing.Callable[..., typing.NoReturn]
3233 # generic type var with no particular meaning
3334 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)
3440
3541
3642 # a value used as the default for arguments, so that when `None` is passed, it
4652 return callable(x)
4753
4854
49 def _callable_or_raise(obj: typing.Optional[T]) -> typing.Optional[T]:
55 def _callable_or_raise(obj: T | None) -> T | None:
5056 """Makes sure an object is callable if it is not ``None``. If not
5157 callable, a ValueError is raised.
5258 """
6167
6268 # Adapted from werkzeug:
6369 # https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/wrappers.py
64 def is_json(mimetype: typing.Optional[str]) -> bool:
70 def is_json(mimetype: str | None) -> bool:
6571 """Indicates if this mimetype is JSON or not. By default a request
6672 is considered to include JSON data if the mimetype is
6773 ``application/json`` or ``application/*+json``.
8894 f"Bytes decoding error : {exc.reason}",
8995 doc=str(exc.object),
9096 pos=exc.start,
91 )
97 ) from exc
9298 return json.loads(decoded)
9399
94100
125131 DEFAULT_LOCATION: str = "json"
126132 #: Default value to use for 'unknown' on schema load
127133 # on a per-location basis
128 DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = {
134 DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = {
129135 "json": None,
130136 "form": None,
131137 "json_or_form": None,
136142 "files": ma.EXCLUDE,
137143 }
138144 #: The marshmallow Schema class to use when creating new schemas
139 DEFAULT_SCHEMA_CLASS: typing.Type = ma.Schema
145 DEFAULT_SCHEMA_CLASS: type[ma.Schema] = ma.Schema
140146 #: Default status code to return for validation errors
141147 DEFAULT_VALIDATION_STATUS: int = DEFAULT_VALIDATION_STATUS
142148 #: Default error message for validation errors
143149 DEFAULT_VALIDATION_MESSAGE: str = "Invalid value."
144150 #: 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]
151 KNOWN_MULTI_FIELDS: list[type] = [ma.fields.List, ma.fields.Tuple]
146152
147153 #: Maps location => method name
148 __location_map__: typing.Dict[str, typing.Union[str, typing.Callable]] = {
154 __location_map__: dict[str, str | typing.Callable] = {
149155 "json": "load_json",
150156 "querystring": "load_querystring",
151157 "query": "load_querystring",
158164
159165 def __init__(
160166 self,
161 location: typing.Optional[str] = None,
167 location: str | None = None,
162168 *,
163 unknown: typing.Optional[str] = _UNKNOWN_DEFAULT_PARAM,
164 error_handler: typing.Optional[ErrorHandler] = None,
165 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,
166172 ):
167173 self.location = location or self.DEFAULT_LOCATION
168 self.error_callback: typing.Optional[ErrorHandler] = _callable_or_raise(
169 error_handler
170 )
174 self.error_callback: ErrorHandler | None = _callable_or_raise(error_handler)
171175 self.schema_class = schema_class or self.DEFAULT_SCHEMA_CLASS
172176 self.unknown = unknown
173177
174 def _makeproxy(
175 self, multidict, schema: ma.Schema, cls: typing.Type = MultiDictProxy
176 ):
178 def _makeproxy(self, multidict, schema: ma.Schema, cls: type = MultiDictProxy):
177179 """Create a multidict proxy object with options from the current parser"""
178180 return cls(multidict, schema, known_multi_fields=tuple(self.KNOWN_MULTI_FIELDS))
179181
217219 schema: ma.Schema,
218220 location: str,
219221 *,
220 error_status_code: typing.Optional[int],
221 error_headers: typing.Optional[typing.Mapping[str, str]]
222 error_status_code: int | None,
223 error_headers: typing.Mapping[str, str] | None,
222224 ) -> typing.NoReturn:
223225 # rewrite messages to be namespaced under the location which created
224226 # them
258260 schema = argmap()
259261 elif callable(argmap):
260262 schema = argmap(req)
263 elif isinstance(argmap, dict):
264 schema = self.schema_class.from_dict(argmap)()
261265 else:
262 schema = self.schema_class.from_dict(argmap)()
266 raise TypeError(f"argmap was of unexpected type {type(argmap)}")
263267 return schema
264268
265269 def parse(
266270 self,
267271 argmap: ArgMap,
268 req: typing.Optional[Request] = None,
272 req: Request | None = None,
269273 *,
270 location: typing.Optional[str] = None,
271 unknown: typing.Optional[str] = _UNKNOWN_DEFAULT_PARAM,
274 location: str | None = None,
275 unknown: str | None = _UNKNOWN_DEFAULT_PARAM,
272276 validate: ValidateArg = None,
273 error_status_code: typing.Optional[int] = None,
274 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,
275279 ):
276280 """Main request parsing method.
277281
309313 else self.DEFAULT_UNKNOWN_BY_LOCATION.get(location)
310314 )
311315 )
312 load_kwargs: typing.Dict[str, typing.Any] = (
313 {"unknown": unknown} if unknown else {}
314 )
316 load_kwargs: dict[str, typing.Any] = {"unknown": unknown} if unknown else {}
315317 if req is None:
316318 raise ValueError("Must pass req object")
317319 data = None
340342 ) from error
341343 return data
342344
343 def get_default_request(self) -> typing.Optional[Request]:
345 def get_default_request(self) -> Request | None:
344346 """Optional override. Provides a hook for frameworks that use thread-local
345347 request objects.
346348 """
349351 def get_request_from_view_args(
350352 self,
351353 view: typing.Callable,
352 args: typing.Tuple,
354 args: tuple,
353355 kwargs: typing.Mapping[str, typing.Any],
354 ) -> typing.Optional[Request]:
356 ) -> Request | None:
355357 """Optional override. Returns the request object to be parsed, given a view
356358 function's args and kwargs.
357359
367369
368370 @staticmethod
369371 def _update_args_kwargs(
370 args: typing.Tuple,
371 kwargs: typing.Dict[str, typing.Any],
372 parsed_args: typing.Tuple,
372 args: tuple,
373 kwargs: dict[str, typing.Any],
374 parsed_args: tuple,
373375 as_kwargs: bool,
374 ) -> typing.Tuple[typing.Tuple, typing.Mapping]:
376 ) -> tuple[tuple, typing.Mapping]:
375377 """Update args or kwargs with parsed_args depending on as_kwargs"""
376378 if as_kwargs:
377379 kwargs.update(parsed_args)
383385 def use_args(
384386 self,
385387 argmap: ArgMap,
386 req: typing.Optional[Request] = None,
388 req: Request | None = None,
387389 *,
388 location: typing.Optional[str] = None,
389 unknown: typing.Optional[str] = _UNKNOWN_DEFAULT_PARAM,
390 location: str | None = None,
391 unknown: str | None = _UNKNOWN_DEFAULT_PARAM,
390392 as_kwargs: bool = False,
391393 validate: ValidateArg = None,
392 error_status_code: typing.Optional[int] = None,
393 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,
394396 ) -> typing.Callable[..., typing.Callable]:
395397 """Decorator that injects parsed arguments into a view function or method.
396398
420422 request_obj = req
421423 # Optimization: If argmap is passed as a dictionary, we only need
422424 # to generate a Schema once
423 if isinstance(argmap, Mapping):
425 if isinstance(argmap, dict):
424426 argmap = self.schema_class.from_dict(argmap)()
425427
426428 def decorator(func):
471473 kwargs["as_kwargs"] = True
472474 return self.use_args(*args, **kwargs)
473475
474 def location_loader(self, name: str):
476 def location_loader(self, name: str) -> typing.Callable[[C], C]:
475477 """Decorator that registers a function for loading a request location.
476478 The wrapped function receives a schema and a request.
477479
492494 :param str name: The name of the location to register.
493495 """
494496
495 def decorator(func):
497 def decorator(func: C) -> C:
496498 self.__location_map__[name] = func
497499 return func
498500
499501 return decorator
500502
501 def error_handler(self, func: ErrorHandler) -> ErrorHandler:
503 def error_handler(self, func: ErrorHandlerT) -> ErrorHandlerT:
502504 """Decorator that registers a custom error handling function. The
503505 function should receive the raised error, request object,
504506 `marshmallow.Schema` instance used to parse the request, error status code,
526528 return func
527529
528530 def pre_load(
529 self, location_data: Mapping, *, schema: ma.Schema, req: Request, location: str
530 ) -> Mapping:
531 self,
532 location_data: typing.Mapping,
533 *,
534 schema: ma.Schema,
535 req: Request,
536 location: str,
537 ) -> typing.Mapping:
531538 """A method of the parser which can transform data after location
532539 loading is done. By default it does nothing, but users can subclass
533540 parsers and override this method.
536543
537544 def _handle_invalid_json_error(
538545 self,
539 error: typing.Union[json.JSONDecodeError, UnicodeDecodeError],
546 error: json.JSONDecodeError | UnicodeDecodeError,
540547 req: Request,
541548 *args,
542 **kwargs
549 **kwargs,
543550 ) -> typing.NoReturn:
544551 """Internal hook for overriding treatment of JSONDecodeErrors.
545552
633640 schema: ma.Schema,
634641 *,
635642 error_status_code: int,
636 error_headers: typing.Mapping[str, str]
643 error_headers: typing.Mapping[str, str],
637644 ) -> typing.NoReturn:
638645 """Called if an error occurs while parsing args. By default, just logs and
639646 raises ``error``.
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]
8787
8888 def __init__(
8989 self,
90 cls_or_instance: typing.Union[ma.fields.Field, type],
90 cls_or_instance: ma.fields.Field | type,
9191 *,
92 delimiter: typing.Optional[str] = None,
93 **kwargs
92 delimiter: str | None = None,
93 **kwargs,
9494 ):
9595 self.delimiter = delimiter or self.delimiter
9696 super().__init__(cls_or_instance, **kwargs)
109109
110110 default_error_messages = {"invalid": "Not a valid delimited tuple."}
111111
112 def __init__(
113 self, tuple_fields, *, delimiter: typing.Optional[str] = None, **kwargs
114 ):
112 def __init__(self, tuple_fields, *, delimiter: str | None = None, **kwargs):
115113 self.delimiter = delimiter or self.delimiter
116114 super().__init__(tuple_fields, **kwargs)
1919 uid=uid, per_page=args["per_page"]
2020 )
2121 """
22 import typing
22 from __future__ import annotations
2323
2424 import flask
2525 from werkzeug.exceptions import HTTPException
5050 class FlaskParser(core.Parser):
5151 """Flask request argument parser."""
5252
53 DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = {
53 DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = {
5454 "view_args": ma.RAISE,
5555 "path": ma.RAISE,
5656 **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION,
2323 server = make_server('0.0.0.0', 6543, app)
2424 server.serve_forever()
2525 """
26 from __future__ import annotations
27
2628 import functools
27 import typing
2829 from collections.abc import Mapping
2930
3031 from webob.multidict import MultiDict
4344 class PyramidParser(core.Parser):
4445 """Pyramid request argument parser."""
4546
46 DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = {
47 DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = {
4748 "matchdict": ma.RAISE,
4849 "path": ma.RAISE,
4950 **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION,
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):
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
12 import typing
23 from unittest import mock
13201321 # - ids=[" 1", ...] will still parse okay because " 1" is valid for fields.Int
13211322 ret = parser.parse(schema, web_request, location="json")
13221323 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
3232 extras = frameworks
3333 commands = mypy src/
3434