Import upstream version 8.1.0
Kali Janitor
2 years ago
0 | 0 | repos: |
1 | 1 | - repo: https://github.com/asottile/pyupgrade |
2 | rev: v2.11.0 | |
2 | rev: v2.31.0 | |
3 | 3 | hooks: |
4 | 4 | - id: pyupgrade |
5 | args: ["--py36-plus"] | |
5 | args: ["--py37-plus"] | |
6 | 6 | - repo: https://github.com/psf/black |
7 | rev: 20.8b1 | |
7 | rev: 21.12b0 | |
8 | 8 | hooks: |
9 | 9 | - 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 | |
12 | 12 | hooks: |
13 | 13 | - id: flake8 |
14 | additional_dependencies: [flake8-bugbear==21.4.3] | |
14 | additional_dependencies: [flake8-bugbear==21.11.29] | |
15 | 15 | - repo: https://github.com/asottile/blacken-docs |
16 | rev: v1.10.0 | |
16 | rev: v1.12.0 | |
17 | 17 | hooks: |
18 | 18 | - 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"] | |
21 | 21 | - repo: https://github.com/pre-commit/mirrors-mypy |
22 | rev: v0.812 | |
22 | rev: v0.930 | |
23 | 23 | hooks: |
24 | 24 | - id: mypy |
25 | 25 | language_version: python3 |
26 | 26 | files: ^src/webargs/ |
27 | additional_dependencies: ["marshmallow>=3,<4"] | |
27 | additional_dependencies: ["marshmallow>=3,<4", "packaging"] |
6 | 6 | |
7 | 7 | * Steven Loria `@sloria <https://github.com/sloria>`_ |
8 | 8 | * Jérôme Lafréchoux `@lafrech <https://github.com/lafrech>`_ |
9 | * Stephen Rosen `@sirosen <https://github.com/sirosen>`_ | |
9 | 10 | |
10 | 11 | Contributors (chronological) |
11 | 12 | ---------------------------- |
41 | 42 | * `@zhenhua32 <https://github.com/zhenhua32>`_ |
42 | 43 | * Martin Roy `@lindycoder <https://github.com/lindycoder>`_ |
43 | 44 | * Kubilay Kocak `@koobs <https://github.com/koobs>`_ |
44 | * Stephen Rosen `@sirosen <https://github.com/sirosen>`_ | |
45 | 45 | * `@dodumosu <https://github.com/dodumosu>`_ |
46 | 46 | * Nate Dellinger `@Nateyo <https://github.com/Nateyo>`_ |
47 | 47 | * Karthikeyan Singaravelan `@tirkarthi <https://github.com/tirkarthi>`_ |
53 | 53 | * Grey Li `@greyli <https://github.com/greyli>`_ |
54 | 54 | * `@michaelizergit <https://github.com/michaelizergit>`_ |
55 | 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>`_ |
0 | 0 | Changelog |
1 | 1 | --------- |
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`). | |
2 | 23 | |
3 | 24 | 8.0.1 (2021-08-12) |
4 | 25 | ****************** |
15 | 36 | * Webargs has a new logo. Thanks to :user:`michaelizergit`! (:issue:`312`) |
16 | 37 | * Don't build universal wheels. We don't support Python 2 anymore. |
17 | 38 | (:pr:`632`) |
18 | * Make the build reproducible (:pr:`#631`). | |
39 | * Make the build reproducible (:pr:`631`). | |
19 | 40 | |
20 | 41 | |
21 | 42 | 8.0.0 (2021-04-08) |
55 | 55 | |
56 | 56 | # The pre-commit CLI was installed above |
57 | 57 | $ 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. | |
63 | 58 | |
64 | 59 | Git Branch Structure |
65 | 60 | ++++++++++++++++++++ |
53 | 53 | |
54 | 54 | pip install -U webargs |
55 | 55 | |
56 | webargs supports Python >= 3.6. | |
56 | webargs supports Python >= 3.7. | |
57 | 57 | |
58 | 58 | |
59 | 59 | Documentation |
26 | 26 | toxenvs: |
27 | 27 | - lint |
28 | 28 | - mypy |
29 | - py36 | |
30 | - py36-mindeps | |
31 | 29 | - py37 |
32 | - py38 | |
33 | - py39 | |
34 | - py39-marshmallowdev | |
30 | - py37-mindeps | |
31 | - py310 | |
32 | - py310-marshmallowdev | |
35 | 33 | - docs |
36 | 34 | os: linux |
37 | 35 | # Build wheels |
0 | 0 | Install |
1 | 1 | ======= |
2 | 2 | |
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. | |
4 | 4 | |
5 | 5 | From the PyPI |
6 | 6 | ------------- |
487 | 487 | location=None, |
488 | 488 | validate=None, |
489 | 489 | error_status_code=None, |
490 | error_headers=None | |
490 | error_headers=None, | |
491 | 491 | ): |
492 | 492 | ... |
493 | 493 | |
516 | 516 | as_kwargs=False, |
517 | 517 | validate=None, |
518 | 518 | error_status_code=None, |
519 | error_headers=None | |
519 | error_headers=None, | |
520 | 520 | ): |
521 | 521 | ... |
522 | 522 |
0 | 0 | [tool.black] |
1 | 1 | line-length = 88 |
2 | target-version = ['py35', 'py36', 'py37', 'py38'] | |
2 | target-version = ['py37', 'py38', 'py39', 'py310'] |
13 | 13 | "frameworks": FRAMEWORKS, |
14 | 14 | "tests": [ |
15 | 15 | "pytest", |
16 | "webtest==2.0.35", | |
16 | "webtest==3.0.0", | |
17 | 17 | "webtest-aiohttp==2.0.0", |
18 | 18 | "pytest-aiohttp>=0.3.0", |
19 | 19 | ] |
20 | 20 | + FRAMEWORKS, |
21 | 21 | "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", | |
25 | 25 | "pre-commit~=2.4", |
26 | 26 | ], |
27 | 27 | "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", | |
31 | 31 | ] |
32 | 32 | + FRAMEWORKS, |
33 | 33 | } |
72 | 72 | packages=find_packages("src"), |
73 | 73 | package_dir={"": "src"}, |
74 | 74 | package_data={"webargs": ["py.typed"]}, |
75 | install_requires=["marshmallow>=3.0.0"], | |
75 | install_requires=["marshmallow>=3.0.0", "packaging"], | |
76 | 76 | extras_require=EXTRAS_REQUIRE, |
77 | 77 | license="MIT", |
78 | 78 | zip_safe=False, |
92 | 92 | "api", |
93 | 93 | "marshmallow", |
94 | 94 | ), |
95 | python_requires=">=3.6", | |
95 | python_requires=">=3.7", | |
96 | 96 | classifiers=[ |
97 | 97 | "Development Status :: 5 - Production/Stable", |
98 | 98 | "Intended Audience :: Developers", |
99 | 99 | "License :: OSI Approved :: MIT License", |
100 | 100 | "Natural Language :: English", |
101 | 101 | "Programming Language :: Python :: 3", |
102 | "Programming Language :: Python :: 3.6", | |
103 | 102 | "Programming Language :: Python :: 3.7", |
104 | 103 | "Programming Language :: Python :: 3.8", |
105 | 104 | "Programming Language :: Python :: 3.9", |
105 | "Programming Language :: Python :: 3.10", | |
106 | 106 | "Programming Language :: Python :: 3 :: Only", |
107 | 107 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", |
108 | 108 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", |
0 | from distutils.version import LooseVersion | |
0 | from __future__ import annotations | |
1 | ||
2 | from packaging.version import Version | |
1 | 3 | from marshmallow.utils import missing |
2 | 4 | |
3 | 5 | # Make marshmallow's validation functions importable from webargs |
6 | 8 | from webargs.core import ValidationError |
7 | 9 | from webargs import fields |
8 | 10 | |
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] | |
11 | 18 | __all__ = ("ValidationError", "fields", "missing", "validate") |
21 | 21 | app = web.Application() |
22 | 22 | app.router.add_route('GET', '/', index) |
23 | 23 | """ |
24 | from __future__ import annotations | |
25 | ||
24 | 26 | import typing |
25 | 27 | |
26 | 28 | from aiohttp import web |
70 | 72 | class AIOHTTPParser(AsyncParser): |
71 | 73 | """aiohttp request argument parser.""" |
72 | 74 | |
73 | DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = { | |
75 | DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = { | |
74 | 76 | "match_info": RAISE, |
75 | 77 | "path": RAISE, |
76 | 78 | **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION, |
90 | 92 | post_data = await req.post() |
91 | 93 | return self._makeproxy(post_data, schema) |
92 | 94 | |
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: | |
96 | 96 | data = await self.load_json(req, schema) |
97 | 97 | if data is not core.missing: |
98 | 98 | return data |
153 | 153 | req, |
154 | 154 | schema: Schema, |
155 | 155 | *, |
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, | |
158 | 158 | ) -> typing.NoReturn: |
159 | 159 | """Handle ValidationErrors and return a JSON response of error messages |
160 | 160 | to the client. |
172 | 172 | ) |
173 | 173 | |
174 | 174 | 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 | |
180 | 176 | ) -> typing.NoReturn: |
181 | 177 | error_class = exception_map[400] |
182 | 178 | messages = {"json": ["Invalid JSON body."]} |
0 | 0 | """Asynchronous request parser.""" |
1 | from __future__ import annotations | |
2 | ||
1 | 3 | import asyncio |
2 | 4 | import functools |
3 | 5 | import inspect |
4 | 6 | import typing |
5 | from collections.abc import Mapping | |
6 | 7 | |
7 | 8 | from marshmallow import Schema, ValidationError |
8 | 9 | import marshmallow as ma |
21 | 22 | async def parse( |
22 | 23 | self, |
23 | 24 | argmap: core.ArgMap, |
24 | req: typing.Optional[core.Request] = None, | |
25 | req: core.Request | None = None, | |
25 | 26 | *, |
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, | |
28 | 29 | 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: | |
32 | 33 | """Coroutine variant of `webargs.core.Parser`. |
33 | 34 | |
34 | 35 | Receives the same arguments as `webargs.core.Parser.parse`. |
44 | 45 | else self.DEFAULT_UNKNOWN_BY_LOCATION.get(location) |
45 | 46 | ) |
46 | 47 | ) |
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 {} | |
50 | 49 | if req is None: |
51 | 50 | raise ValueError("Must pass req object") |
52 | 51 | data = None |
95 | 94 | schema: Schema, |
96 | 95 | location: str, |
97 | 96 | *, |
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, | |
100 | 99 | ) -> typing.NoReturn: |
101 | 100 | # rewrite messages to be namespaced under the location which created |
102 | 101 | # them |
132 | 131 | def use_args( |
133 | 132 | self, |
134 | 133 | argmap: core.ArgMap, |
135 | req: typing.Optional[core.Request] = None, | |
134 | req: core.Request | None = None, | |
136 | 135 | *, |
137 | 136 | location: str = None, |
138 | 137 | unknown=core._UNKNOWN_DEFAULT_PARAM, |
139 | 138 | as_kwargs: bool = False, |
140 | 139 | 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, | |
143 | 142 | ) -> typing.Callable[..., typing.Callable]: |
144 | 143 | """Decorator that injects parsed arguments into a view function or method. |
145 | 144 | |
149 | 148 | request_obj = req |
150 | 149 | # Optimization: If argmap is passed as a dictionary, we only need |
151 | 150 | # to generate a Schema once |
152 | if isinstance(argmap, Mapping): | |
151 | if isinstance(argmap, dict): | |
153 | 152 | argmap = self.schema_class.from_dict(argmap)() |
154 | 153 | |
155 | 154 | def decorator(func: typing.Callable) -> typing.Callable: |
0 | from __future__ import annotations | |
1 | ||
0 | 2 | import functools |
1 | 3 | import typing |
2 | 4 | import logging |
3 | from collections.abc import Mapping | |
4 | 5 | import json |
5 | 6 | |
6 | 7 | import marshmallow as ma |
23 | 24 | Request = typing.TypeVar("Request") |
24 | 25 | ArgMap = typing.Union[ |
25 | 26 | ma.Schema, |
26 | typing.Mapping[str, ma.fields.Field], | |
27 | typing.Dict[str, typing.Union[ma.fields.Field, typing.Type[ma.fields.Field]]], | |
27 | 28 | typing.Callable[[Request], ma.Schema], |
28 | 29 | ] |
29 | 30 | ValidateArg = typing.Union[None, typing.Callable, typing.Iterable[typing.Callable]] |
31 | 32 | ErrorHandler = typing.Callable[..., typing.NoReturn] |
32 | 33 | # generic type var with no particular meaning |
33 | 34 | 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) | |
34 | 40 | |
35 | 41 | |
36 | 42 | # a value used as the default for arguments, so that when `None` is passed, it |
46 | 52 | return callable(x) |
47 | 53 | |
48 | 54 | |
49 | def _callable_or_raise(obj: typing.Optional[T]) -> typing.Optional[T]: | |
55 | def _callable_or_raise(obj: T | None) -> T | None: | |
50 | 56 | """Makes sure an object is callable if it is not ``None``. If not |
51 | 57 | callable, a ValueError is raised. |
52 | 58 | """ |
61 | 67 | |
62 | 68 | # Adapted from werkzeug: |
63 | 69 | # 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: | |
65 | 71 | """Indicates if this mimetype is JSON or not. By default a request |
66 | 72 | is considered to include JSON data if the mimetype is |
67 | 73 | ``application/json`` or ``application/*+json``. |
88 | 94 | f"Bytes decoding error : {exc.reason}", |
89 | 95 | doc=str(exc.object), |
90 | 96 | pos=exc.start, |
91 | ) | |
97 | ) from exc | |
92 | 98 | return json.loads(decoded) |
93 | 99 | |
94 | 100 | |
125 | 131 | DEFAULT_LOCATION: str = "json" |
126 | 132 | #: Default value to use for 'unknown' on schema load |
127 | 133 | # 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] = { | |
129 | 135 | "json": None, |
130 | 136 | "form": None, |
131 | 137 | "json_or_form": None, |
136 | 142 | "files": ma.EXCLUDE, |
137 | 143 | } |
138 | 144 | #: 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 | |
140 | 146 | #: Default status code to return for validation errors |
141 | 147 | DEFAULT_VALIDATION_STATUS: int = DEFAULT_VALIDATION_STATUS |
142 | 148 | #: Default error message for validation errors |
143 | 149 | DEFAULT_VALIDATION_MESSAGE: str = "Invalid value." |
144 | 150 | #: 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] | |
146 | 152 | |
147 | 153 | #: Maps location => method name |
148 | __location_map__: typing.Dict[str, typing.Union[str, typing.Callable]] = { | |
154 | __location_map__: dict[str, str | typing.Callable] = { | |
149 | 155 | "json": "load_json", |
150 | 156 | "querystring": "load_querystring", |
151 | 157 | "query": "load_querystring", |
158 | 164 | |
159 | 165 | def __init__( |
160 | 166 | self, |
161 | location: typing.Optional[str] = None, | |
167 | location: str | None = None, | |
162 | 168 | *, |
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, | |
166 | 172 | ): |
167 | 173 | 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) | |
171 | 175 | self.schema_class = schema_class or self.DEFAULT_SCHEMA_CLASS |
172 | 176 | self.unknown = unknown |
173 | 177 | |
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): | |
177 | 179 | """Create a multidict proxy object with options from the current parser""" |
178 | 180 | return cls(multidict, schema, known_multi_fields=tuple(self.KNOWN_MULTI_FIELDS)) |
179 | 181 | |
217 | 219 | schema: ma.Schema, |
218 | 220 | location: str, |
219 | 221 | *, |
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, | |
222 | 224 | ) -> typing.NoReturn: |
223 | 225 | # rewrite messages to be namespaced under the location which created |
224 | 226 | # them |
258 | 260 | schema = argmap() |
259 | 261 | elif callable(argmap): |
260 | 262 | schema = argmap(req) |
263 | elif isinstance(argmap, dict): | |
264 | schema = self.schema_class.from_dict(argmap)() | |
261 | 265 | else: |
262 | schema = self.schema_class.from_dict(argmap)() | |
266 | raise TypeError(f"argmap was of unexpected type {type(argmap)}") | |
263 | 267 | return schema |
264 | 268 | |
265 | 269 | def parse( |
266 | 270 | self, |
267 | 271 | argmap: ArgMap, |
268 | req: typing.Optional[Request] = None, | |
272 | req: Request | None = None, | |
269 | 273 | *, |
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, | |
272 | 276 | 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, | |
275 | 279 | ): |
276 | 280 | """Main request parsing method. |
277 | 281 | |
309 | 313 | else self.DEFAULT_UNKNOWN_BY_LOCATION.get(location) |
310 | 314 | ) |
311 | 315 | ) |
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 {} | |
315 | 317 | if req is None: |
316 | 318 | raise ValueError("Must pass req object") |
317 | 319 | data = None |
340 | 342 | ) from error |
341 | 343 | return data |
342 | 344 | |
343 | def get_default_request(self) -> typing.Optional[Request]: | |
345 | def get_default_request(self) -> Request | None: | |
344 | 346 | """Optional override. Provides a hook for frameworks that use thread-local |
345 | 347 | request objects. |
346 | 348 | """ |
349 | 351 | def get_request_from_view_args( |
350 | 352 | self, |
351 | 353 | view: typing.Callable, |
352 | args: typing.Tuple, | |
354 | args: tuple, | |
353 | 355 | kwargs: typing.Mapping[str, typing.Any], |
354 | ) -> typing.Optional[Request]: | |
356 | ) -> Request | None: | |
355 | 357 | """Optional override. Returns the request object to be parsed, given a view |
356 | 358 | function's args and kwargs. |
357 | 359 | |
367 | 369 | |
368 | 370 | @staticmethod |
369 | 371 | 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, | |
373 | 375 | as_kwargs: bool, |
374 | ) -> typing.Tuple[typing.Tuple, typing.Mapping]: | |
376 | ) -> tuple[tuple, typing.Mapping]: | |
375 | 377 | """Update args or kwargs with parsed_args depending on as_kwargs""" |
376 | 378 | if as_kwargs: |
377 | 379 | kwargs.update(parsed_args) |
383 | 385 | def use_args( |
384 | 386 | self, |
385 | 387 | argmap: ArgMap, |
386 | req: typing.Optional[Request] = None, | |
388 | req: Request | None = None, | |
387 | 389 | *, |
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, | |
390 | 392 | as_kwargs: bool = False, |
391 | 393 | 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, | |
394 | 396 | ) -> typing.Callable[..., typing.Callable]: |
395 | 397 | """Decorator that injects parsed arguments into a view function or method. |
396 | 398 | |
420 | 422 | request_obj = req |
421 | 423 | # Optimization: If argmap is passed as a dictionary, we only need |
422 | 424 | # to generate a Schema once |
423 | if isinstance(argmap, Mapping): | |
425 | if isinstance(argmap, dict): | |
424 | 426 | argmap = self.schema_class.from_dict(argmap)() |
425 | 427 | |
426 | 428 | def decorator(func): |
471 | 473 | kwargs["as_kwargs"] = True |
472 | 474 | return self.use_args(*args, **kwargs) |
473 | 475 | |
474 | def location_loader(self, name: str): | |
476 | def location_loader(self, name: str) -> typing.Callable[[C], C]: | |
475 | 477 | """Decorator that registers a function for loading a request location. |
476 | 478 | The wrapped function receives a schema and a request. |
477 | 479 | |
492 | 494 | :param str name: The name of the location to register. |
493 | 495 | """ |
494 | 496 | |
495 | def decorator(func): | |
497 | def decorator(func: C) -> C: | |
496 | 498 | self.__location_map__[name] = func |
497 | 499 | return func |
498 | 500 | |
499 | 501 | return decorator |
500 | 502 | |
501 | def error_handler(self, func: ErrorHandler) -> ErrorHandler: | |
503 | def error_handler(self, func: ErrorHandlerT) -> ErrorHandlerT: | |
502 | 504 | """Decorator that registers a custom error handling function. The |
503 | 505 | function should receive the raised error, request object, |
504 | 506 | `marshmallow.Schema` instance used to parse the request, error status code, |
526 | 528 | return func |
527 | 529 | |
528 | 530 | 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: | |
531 | 538 | """A method of the parser which can transform data after location |
532 | 539 | loading is done. By default it does nothing, but users can subclass |
533 | 540 | parsers and override this method. |
536 | 543 | |
537 | 544 | def _handle_invalid_json_error( |
538 | 545 | self, |
539 | error: typing.Union[json.JSONDecodeError, UnicodeDecodeError], | |
546 | error: json.JSONDecodeError | UnicodeDecodeError, | |
540 | 547 | req: Request, |
541 | 548 | *args, |
542 | **kwargs | |
549 | **kwargs, | |
543 | 550 | ) -> typing.NoReturn: |
544 | 551 | """Internal hook for overriding treatment of JSONDecodeErrors. |
545 | 552 | |
633 | 640 | schema: ma.Schema, |
634 | 641 | *, |
635 | 642 | error_status_code: int, |
636 | error_headers: typing.Mapping[str, str] | |
643 | error_headers: typing.Mapping[str, str], | |
637 | 644 | ) -> typing.NoReturn: |
638 | 645 | """Called if an error occurs while parsing args. By default, just logs and |
639 | 646 | raises ``error``. |
12 | 12 | "content_type": fields.Str(data_key="Content-Type", location="headers"), |
13 | 13 | } |
14 | 14 | """ |
15 | import typing | |
15 | from __future__ import annotations | |
16 | 16 | |
17 | 17 | import marshmallow as ma |
18 | 18 | |
19 | 19 | # Expose all fields from marshmallow.fields. |
20 | 20 | from marshmallow.fields import * # noqa: F40 |
21 | 21 | |
22 | __all__ = ["DelimitedList"] + ma.fields.__all__ | |
22 | __all__ = ["DelimitedList", "DelimitedTuple"] + ma.fields.__all__ | |
23 | 23 | |
24 | 24 | |
25 | 25 | class Nested(ma.fields.Nested): # type: ignore[no-redef] |
87 | 87 | |
88 | 88 | def __init__( |
89 | 89 | self, |
90 | cls_or_instance: typing.Union[ma.fields.Field, type], | |
90 | cls_or_instance: ma.fields.Field | type, | |
91 | 91 | *, |
92 | delimiter: typing.Optional[str] = None, | |
93 | **kwargs | |
92 | delimiter: str | None = None, | |
93 | **kwargs, | |
94 | 94 | ): |
95 | 95 | self.delimiter = delimiter or self.delimiter |
96 | 96 | super().__init__(cls_or_instance, **kwargs) |
109 | 109 | |
110 | 110 | default_error_messages = {"invalid": "Not a valid delimited tuple."} |
111 | 111 | |
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): | |
115 | 113 | self.delimiter = delimiter or self.delimiter |
116 | 114 | super().__init__(tuple_fields, **kwargs) |
19 | 19 | uid=uid, per_page=args["per_page"] |
20 | 20 | ) |
21 | 21 | """ |
22 | import typing | |
22 | from __future__ import annotations | |
23 | 23 | |
24 | 24 | import flask |
25 | 25 | from werkzeug.exceptions import HTTPException |
50 | 50 | class FlaskParser(core.Parser): |
51 | 51 | """Flask request argument parser.""" |
52 | 52 | |
53 | DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = { | |
53 | DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = { | |
54 | 54 | "view_args": ma.RAISE, |
55 | 55 | "path": ma.RAISE, |
56 | 56 | **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION, |
23 | 23 | server = make_server('0.0.0.0', 6543, app) |
24 | 24 | server.serve_forever() |
25 | 25 | """ |
26 | from __future__ import annotations | |
27 | ||
26 | 28 | import functools |
27 | import typing | |
28 | 29 | from collections.abc import Mapping |
29 | 30 | |
30 | 31 | from webob.multidict import MultiDict |
43 | 44 | class PyramidParser(core.Parser): |
44 | 45 | """Pyramid request argument parser.""" |
45 | 46 | |
46 | DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, typing.Optional[str]] = { | |
47 | DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = { | |
47 | 48 | "matchdict": ma.RAISE, |
48 | 49 | "path": ma.RAISE, |
49 | 50 | **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION, |
123 | 124 | as_kwargs=False, |
124 | 125 | validate=None, |
125 | 126 | error_status_code=None, |
126 | error_headers=None | |
127 | error_headers=None, | |
127 | 128 | ): |
128 | 129 | """Decorator that injects parsed arguments into a view callable. |
129 | 130 | Supports the *Class-based View* pattern where `request` is saved as an instance |
56 | 56 | return _unicode(value) |
57 | 57 | return value |
58 | 58 | # 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 | |
61 | 61 | |
62 | 62 | |
63 | 63 | class WebArgsTornadoCookiesMultiDictProxy(MultiDictProxy): |
35 | 35 | async def echo_json(request): |
36 | 36 | try: |
37 | 37 | parsed = await parser.parse(hello_args, request, location="json") |
38 | except json.JSONDecodeError: | |
38 | except json.JSONDecodeError as exc: | |
39 | 39 | raise aiohttp.web.HTTPBadRequest( |
40 | 40 | body=json.dumps(["Invalid JSON."]).encode("utf-8"), |
41 | 41 | content_type="application/json", |
42 | ) | |
42 | ) from exc | |
43 | 43 | return json_response(parsed) |
44 | 44 | |
45 | 45 | |
46 | 46 | async def echo_json_or_form(request): |
47 | 47 | try: |
48 | 48 | parsed = await parser.parse(hello_args, request, location="json_or_form") |
49 | except json.JSONDecodeError: | |
49 | except json.JSONDecodeError as exc: | |
50 | 50 | raise aiohttp.web.HTTPBadRequest( |
51 | 51 | body=json.dumps(["Invalid JSON."]).encode("utf-8"), |
52 | 52 | content_type="application/json", |
53 | ) | |
53 | ) from exc | |
54 | 54 | return json_response(parsed) |
55 | 55 | |
56 | 56 |
0 | from django.conf.urls import url | |
0 | from django.urls import re_path | |
1 | 1 | |
2 | 2 | from tests.apps.django_app.echo import views |
3 | 3 | |
4 | 4 | |
5 | 5 | 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( | |
19 | 19 | r"^echo_use_args_with_path_param/(?P<name>\w+)$", |
20 | 20 | views.echo_use_args_with_path_param, |
21 | 21 | ), |
22 | url( | |
22 | re_path( | |
23 | 23 | r"^echo_use_kwargs_with_path_param/(?P<name>\w+)$", |
24 | 24 | views.echo_use_kwargs_with_path_param, |
25 | 25 | ), |
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( | |
35 | 35 | r"^echo_use_args_with_path_param_cbv/(?P<pid>\d+)$", |
36 | 36 | views.EchoUseArgsWithParamCBV.as_view(), |
37 | 37 | ), |
0 | import collections | |
0 | 1 | import datetime |
1 | 2 | import typing |
2 | 3 | from unittest import mock |
1320 | 1321 | # - ids=[" 1", ...] will still parse okay because " 1" is valid for fields.Int |
1321 | 1322 | ret = parser.parse(schema, web_request, location="json") |
1322 | 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) |
0 | 0 | [tox] |
1 | 1 | envlist= |
2 | 2 | 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 | |
6 | 6 | docs |
7 | 7 | |
8 | 8 | [testenv] |
28 | 28 | # `webargs` and `marshmallow` both installed is a valuable safeguard against |
29 | 29 | # issues in which `mypy` running on every file standalone won't catch things |
30 | 30 | [testenv:mypy] |
31 | deps = mypy | |
31 | deps = mypy==0.930 | |
32 | 32 | extras = frameworks |
33 | 33 | commands = mypy src/ |
34 | 34 |