Import upstream version 8.1.0
Kali Janitor
2 years ago
0 | version: 2 | |
1 | updates: | |
2 | - package-ecosystem: pip | |
3 | directory: "/" | |
4 | schedule: | |
5 | interval: daily | |
6 | open-pull-requests-limit: 10 |
0 | 0 | repos: |
1 | 1 | - repo: https://github.com/asottile/pyupgrade |
2 | rev: v2.7.3 | |
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.8.4 | |
10 | - repo: https://github.com/pycqa/flake8 | |
11 | rev: 4.0.1 | |
12 | 12 | hooks: |
13 | 13 | - id: flake8 |
14 | additional_dependencies: [flake8-bugbear==20.1.0] | |
14 | additional_dependencies: [flake8-bugbear==21.11.29] | |
15 | 15 | - repo: https://github.com/asottile/blacken-docs |
16 | rev: v1.8.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.790 | |
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", "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>`_ |
50 | 50 | * Lefteris Karapetsas `@lefterisjp <https://github.com/lefterisjp>`_ |
51 | 51 | * Utku Gultopu `@ugultopu <https://github.com/ugultopu>`_ |
52 | 52 | * Jason Williams `@jaswilli <https://github.com/jaswilli>`_ |
53 | * Grey Li `@greyli <https://github.com/greyli>`_ | |
54 | * `@michaelizergit <https://github.com/michaelizergit>`_ | |
55 | * Legolas Bloom `@TTWShell <https://github.com/TTWShell>`_ | |
56 | * Kevin Kirsche `@kkirsche <https://github.com/kkirsche>`_ | |
57 | * Isira Seneviratne `@Isira-Seneviratne <https://github.com/Isira-Seneviratne>`_ |
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`). | |
23 | ||
24 | 8.0.1 (2021-08-12) | |
25 | ****************** | |
26 | ||
27 | Bug fixes: | |
28 | ||
29 | * Fix "``DelimitedList`` deserializes empty string as ``['']``" (:issue:`623`). | |
30 | Thanks :user:`TTWSchell` for reporting and for the PR. | |
31 | ||
32 | Other changes: | |
33 | ||
34 | * New documentation theme with `furo`. Thanks to :user:`pradyunsg` for writing | |
35 | furo! | |
36 | * Webargs has a new logo. Thanks to :user:`michaelizergit`! (:issue:`312`) | |
37 | * Don't build universal wheels. We don't support Python 2 anymore. | |
38 | (:pr:`632`) | |
39 | * Make the build reproducible (:pr:`631`). | |
40 | ||
41 | ||
42 | 8.0.0 (2021-04-08) | |
43 | ****************** | |
44 | ||
45 | Features: | |
46 | ||
47 | * Add `Parser.pre_load` as a method for allowing users to modify data before | |
48 | schema loading, but without redefining location loaders. See advanced docs on | |
49 | `Parser pre_load` for usage information. (:pr:`583`) | |
50 | ||
51 | * *Backwards-incompatible*: ``unknown`` defaults to `None` for body locations | |
52 | (`json`, `form` and `json_or_form`) (:issue:`580`). | |
53 | ||
54 | * Detection of fields as "multi-value" for unpacking lists from multi-dict | |
55 | types is now extensible with the ``is_multiple`` attribute. If a field sets | |
56 | ``is_multiple = True`` it will be detected as a multi-value field. If | |
57 | ``is_multiple`` is not set or is set to ``None``, webargs will check if the | |
58 | field is an instance of ``List`` or ``Tuple``. (:issue:`563`) | |
59 | ||
60 | * A new attribute on ``Parser`` objects, ``Parser.KNOWN_MULTI_FIELDS`` can be | |
61 | used to set fields which should be detected as ``is_multiple=True`` even when | |
62 | the attribute is not set (:pr:`592`). | |
63 | ||
64 | See docs on "Multi-Field Detection" for more details. | |
65 | ||
66 | Bug fixes: | |
67 | ||
68 | * ``Tuple`` field now behaves as a "multiple" field (:pr:`585`). | |
2 | 69 | |
3 | 70 | 7.0.1 (2020-12-14) |
4 | 71 | ****************** |
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 |
105 | 105 | - Contributing Guidelines: https://webargs.readthedocs.io/en/latest/contributing.html |
106 | 106 | - PyPI: https://pypi.python.org/pypi/webargs |
107 | 107 | - Issues: https://github.com/marshmallow-code/webargs/issues |
108 | - Ecosystem / related packages: https://github.com/marshmallow-code/webargs/wiki/Ecosystem | |
108 | 109 | |
109 | 110 | |
110 | 111 | License |
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 |
Binary diff not shown
109 | 109 | |
110 | 110 | @use_args(UserSchema()) |
111 | 111 | def profile_view(args): |
112 | username = args["userame"] | |
112 | username = args["username"] | |
113 | 113 | # ... |
114 | 114 | |
115 | 115 | |
148 | 148 | the `unknown` argument to `fields.Nested`. |
149 | 149 | |
150 | 150 | Default `unknown` |
151 | +++++++++++++++++ | |
151 | ~~~~~~~~~~~~~~~~~ | |
152 | 152 | |
153 | 153 | 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. | |
156 | 156 | |
157 | 157 | You can change these defaults by overriding `DEFAULT_UNKNOWN_BY_LOCATION`. |
158 | 158 | This is a mapping of locations to values to pass. |
179 | 179 | # so EXCLUDE will be used |
180 | 180 | @app.route("/", methods=["GET"]) |
181 | 181 | @parser.use_args({"foo": fields.Int()}, location="query") |
182 | def get(self, args): | |
182 | def get(args): | |
183 | 183 | return f"foo x 2 = {args['foo'] * 2}" |
184 | 184 | |
185 | 185 | |
187 | 187 | # so no value will be passed for `unknown` |
188 | 188 | @app.route("/", methods=["POST"]) |
189 | 189 | @parser.use_args({"foo": fields.Int(), "bar": fields.Int()}, location="json") |
190 | def post(self, args): | |
190 | def post(args): | |
191 | 191 | return f"foo x bar = {args['foo'] * args['bar']}" |
192 | 192 | |
193 | 193 | |
204 | 204 | # effect and `INCLUDE` will always be used |
205 | 205 | @app.route("/", methods=["POST"]) |
206 | 206 | @parser.use_args({"foo": fields.Int(), "bar": fields.Int()}, location="json") |
207 | def post(self, args): | |
207 | def post(args): | |
208 | 208 | unexpected_args = [k for k in args.keys() if k not in ("foo", "bar")] |
209 | 209 | return f"foo x bar = {args['foo'] * args['bar']}; unexpected args={unexpected_args}" |
210 | 210 | |
211 | 211 | Using Schema-Specfied `unknown` |
212 | +++++++++++++++++++++++++++++++ | |
212 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
213 | 213 | |
214 | 214 | If you wish to use the value of `unknown` specified by a schema, simply pass |
215 | 215 | ``unknown=None``. This will disable webargs' automatic passing of values for |
236 | 236 | # as a result, the schema's behavior (EXCLUDE) is used |
237 | 237 | @app.route("/", methods=["POST"]) |
238 | 238 | @use_args(RectangleSchema(), location="json", unknown=None) |
239 | def get(self, args): | |
239 | def get(args): | |
240 | 240 | return f"area = {args['length'] * args['width']}" |
241 | 241 | |
242 | 242 | |
274 | 274 | |
275 | 275 | |
276 | 276 | @use_args(RectangleSchema) |
277 | def post(self, rect: Rectangle): | |
277 | def post(rect: Rectangle): | |
278 | 278 | return f"Area: {rect.length * rect.width}" |
279 | 279 | |
280 | 280 | 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. |
329 | 329 | |
330 | 330 | |
331 | 331 | Reducing Boilerplate |
332 | ++++++++++++++++++++ | |
332 | ~~~~~~~~~~~~~~~~~~~~ | |
333 | 333 | |
334 | 334 | We can reduce boilerplate and improve [re]usability with a simple helper function: |
335 | 335 | |
369 | 369 | 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. |
370 | 370 | |
371 | 371 | Using ``Method`` and ``Function`` Fields with webargs |
372 | +++++++++++++++++++++++++++++++++++++++++++++++++++++ | |
372 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
373 | 373 | |
374 | 374 | Using the :class:`Method <marshmallow.fields.Method>` and :class:`Function <marshmallow.fields.Function>` fields requires that you pass the ``deserialize`` parameter. |
375 | 375 | |
434 | 434 | structure_dict_pair(r, k, v) |
435 | 435 | return r |
436 | 436 | |
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 | ||
437 | 481 | Returning HTTP 400 Responses |
438 | 482 | ---------------------------- |
439 | 483 | |
492 | 536 | """ |
493 | 537 | # ... |
494 | 538 | |
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 | ||
495 | 625 | Mixing Locations |
496 | 626 | ---------------- |
497 | 627 |
0 | import datetime as dt | |
1 | 0 | import sys |
2 | 1 | import os |
3 | import sphinx_typlog_theme | |
2 | import time | |
3 | import datetime as dt | |
4 | 4 | |
5 | 5 | # If extensions (or modules to document with autodoc) are in another directory, |
6 | 6 | # add these directories to sys.path here. If the directory is relative to the |
28 | 28 | "marshmallow": ("http://marshmallow.readthedocs.io/en/latest/", None), |
29 | 29 | } |
30 | 30 | |
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 | ||
31 | 38 | # The master toctree document. |
32 | 39 | master_doc = "index" |
33 | ||
34 | 40 | language = "en" |
35 | ||
36 | 41 | html_domain_indices = False |
37 | 42 | source_suffix = ".rst" |
38 | 43 | 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" | |
40 | 45 | version = release = webargs.__version__ |
41 | 46 | templates_path = ["_templates"] |
42 | 47 | exclude_patterns = ["_build"] |
43 | 48 | |
44 | 49 | # THEME |
45 | 50 | |
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" | |
49 | 52 | |
50 | 53 | html_theme_options = { |
51 | "color": "#268bd2", | |
52 | "logo_name": "webargs", | |
54 | "light_css_variables": {"color-brand-primary": "#268bd2"}, | |
53 | 55 | "description": "Declarative parsing and validation of HTTP request objects.", |
54 | "github_user": github_user, | |
55 | "github_repo": github_repo, | |
56 | 56 | } |
57 | html_logo = "_static/logo.png" | |
57 | 58 | |
58 | 59 | html_context = { |
59 | 60 | "tidelift_url": ( |
62 | 63 | ), |
63 | 64 | "donate_url": "https://opencollective.com/marshmallow", |
64 | 65 | } |
65 | ||
66 | 66 | 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", | |
71 | 72 | "donate.html", |
72 | "searchbox.html", | |
73 | 73 | "sponsors.html", |
74 | "sidebar/ethical-ads.html", | |
75 | "sidebar/scroll-end.html", | |
74 | 76 | ] |
75 | 77 | } |
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 | ------------- |
1 | 1 | =========================== |
2 | 2 | |
3 | 3 | 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 | ... | |
4 | 55 | |
5 | 56 | Upgrading to 7.0 |
6 | 57 | ++++++++++++++++ |
436 | 487 | location=None, |
437 | 488 | validate=None, |
438 | 489 | error_status_code=None, |
439 | error_headers=None | |
490 | error_headers=None, | |
440 | 491 | ): |
441 | 492 | ... |
442 | 493 | |
465 | 516 | as_kwargs=False, |
466 | 517 | validate=None, |
467 | 518 | error_status_code=None, |
468 | error_headers=None | |
519 | error_headers=None, | |
469 | 520 | ): |
470 | 521 | ... |
471 | 522 |
0 | 0 | [tool.black] |
1 | 1 | line-length = 88 |
2 | target-version = ['py35', 'py36', 'py37', 'py38'] | |
2 | target-version = ['py37', 'py38', 'py39', 'py310'] |
0 | 0 | [metadata] |
1 | 1 | license_files = LICENSE |
2 | ||
3 | [bdist_wheel] | |
4 | universal = 1 | |
5 | 2 | |
6 | 3 | [flake8] |
7 | 4 | ignore = E203, E266, E501, W503 |
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.790", | |
23 | "flake8==3.8.4", | |
24 | "flake8-bugbear==20.11.1", | |
22 | "mypy==0.931", | |
23 | "flake8==4.0.1", | |
24 | "flake8-bugbear==21.11.29", | |
25 | 25 | "pre-commit~=2.4", |
26 | 26 | ], |
27 | "docs": ["Sphinx==3.3.1", "sphinx-issues==1.2.0", "sphinx-typlog-theme==0.8.0"] | |
27 | "docs": [ | |
28 | "Sphinx==4.3.2", | |
29 | "sphinx-issues==3.0.1", | |
30 | "furo==2022.1.2", | |
31 | ] | |
28 | 32 | + FRAMEWORKS, |
29 | 33 | } |
30 | 34 | EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["lint"] + ["tox"] |
68 | 72 | packages=find_packages("src"), |
69 | 73 | package_dir={"": "src"}, |
70 | 74 | package_data={"webargs": ["py.typed"]}, |
71 | install_requires=["marshmallow>=3.0.0"], | |
75 | install_requires=["marshmallow>=3.0.0", "packaging"], | |
72 | 76 | extras_require=EXTRAS_REQUIRE, |
73 | 77 | license="MIT", |
74 | 78 | zip_safe=False, |
88 | 92 | "api", |
89 | 93 | "marshmallow", |
90 | 94 | ), |
91 | python_requires=">=3.6", | |
95 | python_requires=">=3.7", | |
92 | 96 | classifiers=[ |
93 | 97 | "Development Status :: 5 - Production/Stable", |
94 | 98 | "Intended Audience :: Developers", |
95 | 99 | "License :: OSI Approved :: MIT License", |
96 | 100 | "Natural Language :: English", |
97 | 101 | "Programming Language :: Python :: 3", |
98 | "Programming Language :: Python :: 3.6", | |
99 | 102 | "Programming Language :: Python :: 3.7", |
100 | 103 | "Programming Language :: Python :: 3.8", |
101 | 104 | "Programming Language :: Python :: 3.9", |
105 | "Programming Language :: Python :: 3.10", | |
102 | 106 | "Programming Language :: Python :: 3 :: Only", |
103 | 107 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", |
104 | 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__ = "7.0.1" | |
10 | __version_info__ = tuple(LooseVersion(__version__).version) | |
11 | __version__ = "8.1.0" | |
12 | __parsed_version__ = Version(__version__) | |
13 | __version_info__: tuple[int, int, int] | tuple[ | |
14 | int, int, int, str, int | |
15 | ] = __parsed_version__.release # type: ignore[assignment] | |
16 | if __parsed_version__.pre: | |
17 | __version_info__ += __parsed_version__.pre # type: ignore[assignment] | |
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 = { | |
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, |
83 | 85 | |
84 | 86 | def load_querystring(self, req, schema: Schema) -> MultiDictProxy: |
85 | 87 | """Return query params from the request as a MultiDictProxy.""" |
86 | return MultiDictProxy(req.query, schema) | |
88 | return self._makeproxy(req.query, schema) | |
87 | 89 | |
88 | 90 | async def load_form(self, req, schema: Schema) -> MultiDictProxy: |
89 | 91 | """Return form values from the request as a MultiDictProxy.""" |
90 | 92 | post_data = await req.post() |
91 | return MultiDictProxy(post_data, schema) | |
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 |
113 | 113 | |
114 | 114 | def load_headers(self, req, schema: Schema) -> MultiDictProxy: |
115 | 115 | """Return headers from the request as a MultiDictProxy.""" |
116 | return MultiDictProxy(req.headers, schema) | |
116 | return self._makeproxy(req.headers, schema) | |
117 | 117 | |
118 | 118 | def load_cookies(self, req, schema: Schema) -> MultiDictProxy: |
119 | 119 | """Return cookies from the request as a MultiDictProxy.""" |
120 | return MultiDictProxy(req.cookies, schema) | |
120 | return self._makeproxy(req.cookies, schema) | |
121 | 121 | |
122 | 122 | def load_files(self, req, schema: Schema) -> typing.NoReturn: |
123 | 123 | raise NotImplementedError( |
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: |
18 | 18 | import bottle |
19 | 19 | |
20 | 20 | from webargs import core |
21 | from webargs.multidictproxy import MultiDictProxy | |
22 | 21 | |
23 | 22 | |
24 | 23 | class BottleParser(core.Parser): |
48 | 47 | |
49 | 48 | def load_querystring(self, req, schema): |
50 | 49 | """Return query params from the request as a MultiDictProxy.""" |
51 | return MultiDictProxy(req.query, schema) | |
50 | return self._makeproxy(req.query, schema) | |
52 | 51 | |
53 | 52 | def load_form(self, req, schema): |
54 | 53 | """Return form values from the request as a MultiDictProxy.""" |
57 | 56 | # TODO: Make this check more specific |
58 | 57 | if core.is_json(req.content_type): |
59 | 58 | return core.missing |
60 | return MultiDictProxy(req.forms, schema) | |
59 | return self._makeproxy(req.forms, schema) | |
61 | 60 | |
62 | 61 | def load_headers(self, req, schema): |
63 | 62 | """Return headers from the request as a MultiDictProxy.""" |
64 | return MultiDictProxy(req.headers, schema) | |
63 | return self._makeproxy(req.headers, schema) | |
65 | 64 | |
66 | 65 | def load_cookies(self, req, schema): |
67 | 66 | """Return cookies from the request.""" |
69 | 68 | |
70 | 69 | def load_files(self, req, schema): |
71 | 70 | """Return files from the request as a MultiDictProxy.""" |
72 | return MultiDictProxy(req.files, schema) | |
71 | return self._makeproxy(req.files, schema) | |
73 | 72 | |
74 | 73 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): |
75 | 74 | """Handles errors during parsing. Aborts the current request with a |
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 |
7 | 8 | from marshmallow import ValidationError |
8 | 9 | from marshmallow.utils import missing |
9 | 10 | |
10 | from webargs.fields import DelimitedList | |
11 | from webargs.multidictproxy import MultiDictProxy | |
11 | 12 | |
12 | 13 | logger = logging.getLogger(__name__) |
13 | 14 | |
14 | 15 | |
15 | 16 | __all__ = [ |
16 | 17 | "ValidationError", |
17 | "is_multiple", | |
18 | 18 | "Parser", |
19 | 19 | "missing", |
20 | 20 | "parse_json", |
24 | 24 | Request = typing.TypeVar("Request") |
25 | 25 | ArgMap = typing.Union[ |
26 | 26 | ma.Schema, |
27 | typing.Mapping[str, ma.fields.Field], | |
27 | typing.Dict[str, typing.Union[ma.fields.Field, typing.Type[ma.fields.Field]]], | |
28 | 28 | typing.Callable[[Request], ma.Schema], |
29 | 29 | ] |
30 | 30 | ValidateArg = typing.Union[None, typing.Callable, typing.Iterable[typing.Callable]] |
32 | 32 | ErrorHandler = typing.Callable[..., typing.NoReturn] |
33 | 33 | # generic type var with no particular meaning |
34 | 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) | |
35 | 40 | |
36 | 41 | |
37 | 42 | # a value used as the default for arguments, so that when `None` is passed, it |
47 | 52 | return callable(x) |
48 | 53 | |
49 | 54 | |
50 | def _callable_or_raise(obj: typing.Optional[T]) -> typing.Optional[T]: | |
55 | def _callable_or_raise(obj: T | None) -> T | None: | |
51 | 56 | """Makes sure an object is callable if it is not ``None``. If not |
52 | 57 | callable, a ValueError is raised. |
53 | 58 | """ |
56 | 61 | return obj |
57 | 62 | |
58 | 63 | |
59 | def is_multiple(field: ma.fields.Field) -> bool: | |
60 | """Return whether or not `field` handles repeated/multi-value arguments.""" | |
61 | return isinstance(field, ma.fields.List) and not isinstance(field, DelimitedList) | |
62 | ||
63 | ||
64 | 64 | def get_mimetype(content_type: str) -> str: |
65 | 65 | return content_type.split(";")[0].strip() |
66 | 66 | |
67 | 67 | |
68 | 68 | # Adapted from werkzeug: |
69 | 69 | # https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/wrappers.py |
70 | def is_json(mimetype: typing.Optional[str]) -> bool: | |
70 | def is_json(mimetype: str | None) -> bool: | |
71 | 71 | """Indicates if this mimetype is JSON or not. By default a request |
72 | 72 | is considered to include JSON data if the mimetype is |
73 | 73 | ``application/json`` or ``application/*+json``. |
94 | 94 | f"Bytes decoding error : {exc.reason}", |
95 | 95 | doc=str(exc.object), |
96 | 96 | pos=exc.start, |
97 | ) | |
97 | ) from exc | |
98 | 98 | return json.loads(decoded) |
99 | 99 | |
100 | 100 | |
131 | 131 | DEFAULT_LOCATION: str = "json" |
132 | 132 | #: Default value to use for 'unknown' on schema load |
133 | 133 | # on a per-location basis |
134 | DEFAULT_UNKNOWN_BY_LOCATION: typing.Dict[str, str] = { | |
135 | "json": ma.RAISE, | |
136 | "form": ma.RAISE, | |
137 | "json_or_form": ma.RAISE, | |
134 | DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = { | |
135 | "json": None, | |
136 | "form": None, | |
137 | "json_or_form": None, | |
138 | 138 | "querystring": ma.EXCLUDE, |
139 | 139 | "query": ma.EXCLUDE, |
140 | 140 | "headers": ma.EXCLUDE, |
142 | 142 | "files": ma.EXCLUDE, |
143 | 143 | } |
144 | 144 | #: The marshmallow Schema class to use when creating new schemas |
145 | DEFAULT_SCHEMA_CLASS: typing.Type = ma.Schema | |
145 | DEFAULT_SCHEMA_CLASS: type[ma.Schema] = ma.Schema | |
146 | 146 | #: Default status code to return for validation errors |
147 | 147 | DEFAULT_VALIDATION_STATUS: int = DEFAULT_VALIDATION_STATUS |
148 | 148 | #: Default error message for validation errors |
149 | 149 | DEFAULT_VALIDATION_MESSAGE: str = "Invalid value." |
150 | #: field types which should always be treated as if they set `is_multiple=True` | |
151 | KNOWN_MULTI_FIELDS: list[type] = [ma.fields.List, ma.fields.Tuple] | |
150 | 152 | |
151 | 153 | #: Maps location => method name |
152 | __location_map__: typing.Dict[str, typing.Union[str, typing.Callable]] = { | |
154 | __location_map__: dict[str, str | typing.Callable] = { | |
153 | 155 | "json": "load_json", |
154 | 156 | "querystring": "load_querystring", |
155 | 157 | "query": "load_querystring", |
162 | 164 | |
163 | 165 | def __init__( |
164 | 166 | self, |
165 | location: typing.Optional[str] = None, | |
167 | location: str | None = None, | |
166 | 168 | *, |
167 | unknown: typing.Optional[str] = _UNKNOWN_DEFAULT_PARAM, | |
168 | error_handler: typing.Optional[ErrorHandler] = None, | |
169 | schema_class: typing.Optional[typing.Type] = None | |
169 | unknown: str | None = _UNKNOWN_DEFAULT_PARAM, | |
170 | error_handler: ErrorHandler | None = None, | |
171 | schema_class: type[ma.Schema] | None = None, | |
170 | 172 | ): |
171 | 173 | self.location = location or self.DEFAULT_LOCATION |
172 | self.error_callback: typing.Optional[ErrorHandler] = _callable_or_raise( | |
173 | error_handler | |
174 | ) | |
174 | self.error_callback: ErrorHandler | None = _callable_or_raise(error_handler) | |
175 | 175 | self.schema_class = schema_class or self.DEFAULT_SCHEMA_CLASS |
176 | 176 | self.unknown = unknown |
177 | ||
178 | def _makeproxy(self, multidict, schema: ma.Schema, cls: type = MultiDictProxy): | |
179 | """Create a multidict proxy object with options from the current parser""" | |
180 | return cls(multidict, schema, known_multi_fields=tuple(self.KNOWN_MULTI_FIELDS)) | |
177 | 181 | |
178 | 182 | def _get_loader(self, location: str) -> typing.Callable: |
179 | 183 | """Get the loader function for the given location. |
215 | 219 | schema: ma.Schema, |
216 | 220 | location: str, |
217 | 221 | *, |
218 | error_status_code: typing.Optional[int], | |
219 | error_headers: typing.Optional[typing.Mapping[str, str]] | |
222 | error_status_code: int | None, | |
223 | error_headers: typing.Mapping[str, str] | None, | |
220 | 224 | ) -> typing.NoReturn: |
221 | 225 | # rewrite messages to be namespaced under the location which created |
222 | 226 | # them |
256 | 260 | schema = argmap() |
257 | 261 | elif callable(argmap): |
258 | 262 | schema = argmap(req) |
263 | elif isinstance(argmap, dict): | |
264 | schema = self.schema_class.from_dict(argmap)() | |
259 | 265 | else: |
260 | schema = self.schema_class.from_dict(argmap)() | |
266 | raise TypeError(f"argmap was of unexpected type {type(argmap)}") | |
261 | 267 | return schema |
262 | 268 | |
263 | 269 | def parse( |
264 | 270 | self, |
265 | 271 | argmap: ArgMap, |
266 | req: typing.Optional[Request] = None, | |
272 | req: Request | None = None, | |
267 | 273 | *, |
268 | location: typing.Optional[str] = None, | |
269 | unknown: typing.Optional[str] = _UNKNOWN_DEFAULT_PARAM, | |
274 | location: str | None = None, | |
275 | unknown: str | None = _UNKNOWN_DEFAULT_PARAM, | |
270 | 276 | validate: ValidateArg = None, |
271 | error_status_code: typing.Optional[int] = None, | |
272 | error_headers: typing.Optional[typing.Mapping[str, str]] = None | |
277 | error_status_code: int | None = None, | |
278 | error_headers: typing.Mapping[str, str] | None = None, | |
273 | 279 | ): |
274 | 280 | """Main request parsing method. |
275 | 281 | |
307 | 313 | else self.DEFAULT_UNKNOWN_BY_LOCATION.get(location) |
308 | 314 | ) |
309 | 315 | ) |
310 | load_kwargs: typing.Dict[str, typing.Any] = ( | |
311 | {"unknown": unknown} if unknown else {} | |
312 | ) | |
316 | load_kwargs: dict[str, typing.Any] = {"unknown": unknown} if unknown else {} | |
313 | 317 | if req is None: |
314 | 318 | raise ValueError("Must pass req object") |
315 | 319 | data = None |
319 | 323 | location_data = self._load_location_data( |
320 | 324 | schema=schema, req=req, location=location |
321 | 325 | ) |
322 | data = schema.load(location_data, **load_kwargs) | |
326 | preprocessed_data = self.pre_load( | |
327 | location_data, schema=schema, req=req, location=location | |
328 | ) | |
329 | data = schema.load(preprocessed_data, **load_kwargs) | |
323 | 330 | self._validate_arguments(data, validators) |
324 | 331 | except ma.exceptions.ValidationError as error: |
325 | 332 | self._on_validation_error( |
335 | 342 | ) from error |
336 | 343 | return data |
337 | 344 | |
338 | def get_default_request(self) -> typing.Optional[Request]: | |
345 | def get_default_request(self) -> Request | None: | |
339 | 346 | """Optional override. Provides a hook for frameworks that use thread-local |
340 | 347 | request objects. |
341 | 348 | """ |
344 | 351 | def get_request_from_view_args( |
345 | 352 | self, |
346 | 353 | view: typing.Callable, |
347 | args: typing.Tuple, | |
354 | args: tuple, | |
348 | 355 | kwargs: typing.Mapping[str, typing.Any], |
349 | ) -> typing.Optional[Request]: | |
356 | ) -> Request | None: | |
350 | 357 | """Optional override. Returns the request object to be parsed, given a view |
351 | 358 | function's args and kwargs. |
352 | 359 | |
362 | 369 | |
363 | 370 | @staticmethod |
364 | 371 | def _update_args_kwargs( |
365 | args: typing.Tuple, | |
366 | kwargs: typing.Dict[str, typing.Any], | |
367 | parsed_args: typing.Tuple, | |
372 | args: tuple, | |
373 | kwargs: dict[str, typing.Any], | |
374 | parsed_args: tuple, | |
368 | 375 | as_kwargs: bool, |
369 | ) -> typing.Tuple[typing.Tuple, typing.Mapping]: | |
376 | ) -> tuple[tuple, typing.Mapping]: | |
370 | 377 | """Update args or kwargs with parsed_args depending on as_kwargs""" |
371 | 378 | if as_kwargs: |
372 | 379 | kwargs.update(parsed_args) |
378 | 385 | def use_args( |
379 | 386 | self, |
380 | 387 | argmap: ArgMap, |
381 | req: typing.Optional[Request] = None, | |
388 | req: Request | None = None, | |
382 | 389 | *, |
383 | location: typing.Optional[str] = None, | |
384 | unknown: typing.Optional[str] = _UNKNOWN_DEFAULT_PARAM, | |
390 | location: str | None = None, | |
391 | unknown: str | None = _UNKNOWN_DEFAULT_PARAM, | |
385 | 392 | as_kwargs: bool = False, |
386 | 393 | validate: ValidateArg = None, |
387 | error_status_code: typing.Optional[int] = None, | |
388 | error_headers: typing.Optional[typing.Mapping[str, str]] = None | |
394 | error_status_code: int | None = None, | |
395 | error_headers: typing.Mapping[str, str] | None = None, | |
389 | 396 | ) -> typing.Callable[..., typing.Callable]: |
390 | 397 | """Decorator that injects parsed arguments into a view function or method. |
391 | 398 | |
415 | 422 | request_obj = req |
416 | 423 | # Optimization: If argmap is passed as a dictionary, we only need |
417 | 424 | # to generate a Schema once |
418 | if isinstance(argmap, Mapping): | |
425 | if isinstance(argmap, dict): | |
419 | 426 | argmap = self.schema_class.from_dict(argmap)() |
420 | 427 | |
421 | 428 | def decorator(func): |
466 | 473 | kwargs["as_kwargs"] = True |
467 | 474 | return self.use_args(*args, **kwargs) |
468 | 475 | |
469 | def location_loader(self, name: str): | |
476 | def location_loader(self, name: str) -> typing.Callable[[C], C]: | |
470 | 477 | """Decorator that registers a function for loading a request location. |
471 | 478 | The wrapped function receives a schema and a request. |
472 | 479 | |
487 | 494 | :param str name: The name of the location to register. |
488 | 495 | """ |
489 | 496 | |
490 | def decorator(func): | |
497 | def decorator(func: C) -> C: | |
491 | 498 | self.__location_map__[name] = func |
492 | 499 | return func |
493 | 500 | |
494 | 501 | return decorator |
495 | 502 | |
496 | def error_handler(self, func: ErrorHandler) -> ErrorHandler: | |
503 | def error_handler(self, func: ErrorHandlerT) -> ErrorHandlerT: | |
497 | 504 | """Decorator that registers a custom error handling function. The |
498 | 505 | function should receive the raised error, request object, |
499 | 506 | `marshmallow.Schema` instance used to parse the request, error status code, |
520 | 527 | self.error_callback = func |
521 | 528 | return func |
522 | 529 | |
530 | def pre_load( | |
531 | self, | |
532 | location_data: typing.Mapping, | |
533 | *, | |
534 | schema: ma.Schema, | |
535 | req: Request, | |
536 | location: str, | |
537 | ) -> typing.Mapping: | |
538 | """A method of the parser which can transform data after location | |
539 | loading is done. By default it does nothing, but users can subclass | |
540 | parsers and override this method. | |
541 | """ | |
542 | return location_data | |
543 | ||
523 | 544 | def _handle_invalid_json_error( |
524 | 545 | self, |
525 | error: typing.Union[json.JSONDecodeError, UnicodeDecodeError], | |
546 | error: json.JSONDecodeError | UnicodeDecodeError, | |
526 | 547 | req: Request, |
527 | 548 | *args, |
528 | **kwargs | |
549 | **kwargs, | |
529 | 550 | ) -> typing.NoReturn: |
530 | 551 | """Internal hook for overriding treatment of JSONDecodeErrors. |
531 | 552 | |
619 | 640 | schema: ma.Schema, |
620 | 641 | *, |
621 | 642 | error_status_code: int, |
622 | error_headers: typing.Mapping[str, str] | |
643 | error_headers: typing.Mapping[str, str], | |
623 | 644 | ) -> typing.NoReturn: |
624 | 645 | """Called if an error occurs while parsing args. By default, just logs and |
625 | 646 | raises ``error``. |
17 | 17 | return HttpResponse('Hello ' + args['name']) |
18 | 18 | """ |
19 | 19 | from webargs import core |
20 | from webargs.multidictproxy import MultiDictProxy | |
21 | 20 | |
22 | 21 | |
23 | 22 | def is_json_request(req): |
47 | 46 | |
48 | 47 | def load_querystring(self, req, schema): |
49 | 48 | """Return query params from the request as a MultiDictProxy.""" |
50 | return MultiDictProxy(req.GET, schema) | |
49 | return self._makeproxy(req.GET, schema) | |
51 | 50 | |
52 | 51 | def load_form(self, req, schema): |
53 | 52 | """Return form values from the request as a MultiDictProxy.""" |
54 | return MultiDictProxy(req.POST, schema) | |
53 | return self._makeproxy(req.POST, schema) | |
55 | 54 | |
56 | 55 | def load_cookies(self, req, schema): |
57 | 56 | """Return cookies from the request.""" |
65 | 64 | |
66 | 65 | def load_files(self, req, schema): |
67 | 66 | """Return files from the request as a MultiDictProxy.""" |
68 | return MultiDictProxy(req.FILES, schema) | |
67 | return self._makeproxy(req.FILES, schema) | |
69 | 68 | |
70 | 69 | def get_request_from_view_args(self, view, args, kwargs): |
71 | 70 | # The first argument is either `self` or `request` |
5 | 5 | import marshmallow as ma |
6 | 6 | |
7 | 7 | from webargs import core |
8 | from webargs.multidictproxy import MultiDictProxy | |
9 | 8 | |
10 | 9 | HTTP_422 = "422 Unprocessable Entity" |
11 | 10 | |
96 | 95 | |
97 | 96 | def load_querystring(self, req, schema): |
98 | 97 | """Return query params from the request as a MultiDictProxy.""" |
99 | return MultiDictProxy(req.params, schema) | |
98 | return self._makeproxy(req.params, schema) | |
100 | 99 | |
101 | 100 | def load_form(self, req, schema): |
102 | 101 | """Return form values from the request as a MultiDictProxy |
108 | 107 | form = parse_form_body(req) |
109 | 108 | if form is core.missing: |
110 | 109 | return form |
111 | return MultiDictProxy(form, schema) | |
110 | return self._makeproxy(form, schema) | |
112 | 111 | |
113 | 112 | def load_media(self, req, schema): |
114 | 113 | """Return data unpacked and parsed by one of Falcon's media handlers. |
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] |
54 | 54 | """ |
55 | 55 | |
56 | 56 | delimiter: str = "," |
57 | # delimited fields set is_multiple=False for webargs.core.is_multiple | |
58 | is_multiple: bool = False | |
57 | 59 | |
58 | 60 | def _serialize(self, value, attr, obj, **kwargs): |
59 | 61 | # serializing will start with parent-class serialization, so that we correctly |
66 | 68 | # attempting to deserialize from a non-string source is an error |
67 | 69 | if not isinstance(value, (str, bytes)): |
68 | 70 | 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) | |
70 | 73 | |
71 | 74 | |
72 | 75 | class DelimitedList(DelimitedFieldMixin, ma.fields.List): |
84 | 87 | |
85 | 88 | def __init__( |
86 | 89 | self, |
87 | cls_or_instance: typing.Union[ma.fields.Field, type], | |
90 | cls_or_instance: ma.fields.Field | type, | |
88 | 91 | *, |
89 | delimiter: typing.Optional[str] = None, | |
90 | **kwargs | |
92 | delimiter: str | None = None, | |
93 | **kwargs, | |
91 | 94 | ): |
92 | 95 | self.delimiter = delimiter or self.delimiter |
93 | 96 | super().__init__(cls_or_instance, **kwargs) |
106 | 109 | |
107 | 110 | default_error_messages = {"invalid": "Not a valid delimited tuple."} |
108 | 111 | |
109 | def __init__( | |
110 | self, tuple_fields, *, delimiter: typing.Optional[str] = None, **kwargs | |
111 | ): | |
112 | def __init__(self, tuple_fields, *, delimiter: str | None = None, **kwargs): | |
112 | 113 | self.delimiter = delimiter or self.delimiter |
113 | 114 | super().__init__(tuple_fields, **kwargs) |
19 | 19 | uid=uid, per_page=args["per_page"] |
20 | 20 | ) |
21 | 21 | """ |
22 | from __future__ import annotations | |
23 | ||
22 | 24 | import flask |
23 | 25 | from werkzeug.exceptions import HTTPException |
24 | 26 | |
25 | 27 | import marshmallow as ma |
26 | 28 | |
27 | 29 | from webargs import core |
28 | from webargs.multidictproxy import MultiDictProxy | |
29 | 30 | |
30 | 31 | |
31 | 32 | def abort(http_status_code, exc=None, **kwargs): |
49 | 50 | class FlaskParser(core.Parser): |
50 | 51 | """Flask request argument parser.""" |
51 | 52 | |
52 | DEFAULT_UNKNOWN_BY_LOCATION = { | |
53 | DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = { | |
53 | 54 | "view_args": ma.RAISE, |
54 | 55 | "path": ma.RAISE, |
55 | 56 | **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION, |
79 | 80 | |
80 | 81 | def load_querystring(self, req, schema): |
81 | 82 | """Return query params from the request as a MultiDictProxy.""" |
82 | return MultiDictProxy(req.args, schema) | |
83 | return self._makeproxy(req.args, schema) | |
83 | 84 | |
84 | 85 | def load_form(self, req, schema): |
85 | 86 | """Return form values from the request as a MultiDictProxy.""" |
86 | return MultiDictProxy(req.form, schema) | |
87 | return self._makeproxy(req.form, schema) | |
87 | 88 | |
88 | 89 | def load_headers(self, req, schema): |
89 | 90 | """Return headers from the request as a MultiDictProxy.""" |
90 | return MultiDictProxy(req.headers, schema) | |
91 | return self._makeproxy(req.headers, schema) | |
91 | 92 | |
92 | 93 | def load_cookies(self, req, schema): |
93 | 94 | """Return cookies from the request.""" |
95 | 96 | |
96 | 97 | def load_files(self, req, schema): |
97 | 98 | """Return files from the request as a MultiDictProxy.""" |
98 | return MultiDictProxy(req.files, schema) | |
99 | return self._makeproxy(req.files, schema) | |
99 | 100 | |
100 | 101 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): |
101 | 102 | """Handles errors during parsing. Aborts the current HTTP request and |
0 | 0 | from collections.abc import Mapping |
1 | import typing | |
1 | 2 | |
2 | 3 | import marshmallow as ma |
3 | ||
4 | from webargs.core import missing, is_multiple | |
5 | 4 | |
6 | 5 | |
7 | 6 | class MultiDictProxy(Mapping): |
14 | 13 | In all other cases, __getitem__ proxies directly to the input multidict. |
15 | 14 | """ |
16 | 15 | |
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 | ): | |
18 | 25 | self.data = multidict |
26 | self.known_multi_fields = known_multi_fields | |
19 | 27 | self.multiple_keys = self._collect_multiple_keys(schema) |
20 | 28 | |
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): | |
23 | 39 | result = set() |
24 | 40 | for name, field in schema.fields.items(): |
25 | if not is_multiple(field): | |
41 | if not self._is_multiple(field): | |
26 | 42 | continue |
27 | 43 | result.add(field.data_key if field.data_key is not None else name) |
28 | 44 | return result |
29 | 45 | |
30 | 46 | 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: | |
33 | 49 | return val |
34 | 50 | if hasattr(self.data, "getlist"): |
35 | 51 | return self.data.getlist(key) |
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 | 29 | from collections.abc import Mapping |
28 | 30 | |
33 | 35 | |
34 | 36 | from webargs import core |
35 | 37 | from webargs.core import json |
36 | from webargs.multidictproxy import MultiDictProxy | |
37 | 38 | |
38 | 39 | |
39 | 40 | def is_json_request(req): |
43 | 44 | class PyramidParser(core.Parser): |
44 | 45 | """Pyramid request argument parser.""" |
45 | 46 | |
46 | DEFAULT_UNKNOWN_BY_LOCATION = { | |
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, |
66 | 67 | |
67 | 68 | def load_querystring(self, req, schema): |
68 | 69 | """Return query params from the request as a MultiDictProxy.""" |
69 | return MultiDictProxy(req.GET, schema) | |
70 | return self._makeproxy(req.GET, schema) | |
70 | 71 | |
71 | 72 | def load_form(self, req, schema): |
72 | 73 | """Return form values from the request as a MultiDictProxy.""" |
73 | return MultiDictProxy(req.POST, schema) | |
74 | return self._makeproxy(req.POST, schema) | |
74 | 75 | |
75 | 76 | def load_cookies(self, req, schema): |
76 | 77 | """Return cookies from the request as a MultiDictProxy.""" |
77 | return MultiDictProxy(req.cookies, schema) | |
78 | return self._makeproxy(req.cookies, schema) | |
78 | 79 | |
79 | 80 | def load_headers(self, req, schema): |
80 | 81 | """Return headers from the request as a MultiDictProxy.""" |
81 | return MultiDictProxy(req.headers, schema) | |
82 | return self._makeproxy(req.headers, schema) | |
82 | 83 | |
83 | 84 | def load_files(self, req, schema): |
84 | 85 | """Return files from the request as a MultiDictProxy.""" |
85 | 86 | files = ((k, v) for k, v in req.POST.items() if hasattr(v, "file")) |
86 | return MultiDictProxy(MultiDict(files), schema) | |
87 | return self._makeproxy(MultiDict(files), schema) | |
87 | 88 | |
88 | 89 | def load_matchdict(self, req, schema): |
89 | 90 | """Return the request's ``matchdict`` as a MultiDictProxy.""" |
90 | return MultiDictProxy(req.matchdict, schema) | |
91 | return self._makeproxy(req.matchdict, schema) | |
91 | 92 | |
92 | 93 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): |
93 | 94 | """Handles errors during parsing. Aborts the current HTTP request and |
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): |
96 | 96 | |
97 | 97 | def load_querystring(self, req, schema): |
98 | 98 | """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 | ) | |
100 | 102 | |
101 | 103 | def load_form(self, req, schema): |
102 | 104 | """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 | ) | |
104 | 108 | |
105 | 109 | def load_headers(self, req, schema): |
106 | 110 | """Return headers from the request as a MultiDictProxy.""" |
107 | return WebArgsTornadoMultiDictProxy(req.headers, schema) | |
111 | return self._makeproxy(req.headers, schema, cls=WebArgsTornadoMultiDictProxy) | |
108 | 112 | |
109 | 113 | def load_cookies(self, req, schema): |
110 | 114 | """Return cookies from the request as a MultiDictProxy.""" |
111 | 115 | # use the specialized subclass specifically for handling Tornado |
112 | 116 | # cookies |
113 | return WebArgsTornadoCookiesMultiDictProxy(req.cookies, schema) | |
117 | return self._makeproxy( | |
118 | req.cookies, schema, cls=WebArgsTornadoCookiesMultiDictProxy | |
119 | ) | |
114 | 120 | |
115 | 121 | def load_files(self, req, schema): |
116 | 122 | """Return files from the request as a MultiDictProxy.""" |
117 | return WebArgsTornadoMultiDictProxy(req.files, schema) | |
123 | return self._makeproxy(req.files, schema, cls=WebArgsTornadoMultiDictProxy) | |
118 | 124 | |
119 | 125 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): |
120 | 126 | """Handles errors during parsing. Raises a `tornado.web.HTTPError` |
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 |
2 | import typing | |
1 | 3 | from unittest import mock |
2 | 4 | |
3 | 5 | import pytest |
34 | 36 | """A minimal parser implementation that parses mock requests.""" |
35 | 37 | |
36 | 38 | def load_querystring(self, req, schema): |
37 | return MultiDictProxy(req.query, schema) | |
39 | return self._makeproxy(req.query, schema) | |
40 | ||
41 | def load_form(self, req, schema): | |
42 | return MultiDictProxy(req.form, schema) | |
38 | 43 | |
39 | 44 | def load_json(self, req, schema): |
40 | 45 | return req.json |
868 | 873 | assert viewfunc() == {"username": "foo"} |
869 | 874 | |
870 | 875 | |
876 | def test_delimited_list_empty_string(web_request, parser): | |
877 | web_request.json = {"dates": ""} | |
878 | schema_cls = Schema.from_dict({"dates": fields.DelimitedList(fields.Str())}) | |
879 | schema = schema_cls() | |
880 | ||
881 | parsed = parser.parse(schema, web_request) | |
882 | assert parsed["dates"] == [] | |
883 | ||
884 | data = schema.dump(parsed) | |
885 | assert data["dates"] == "" | |
886 | ||
887 | ||
871 | 888 | def test_delimited_list_default_delimiter(web_request, parser): |
872 | 889 | web_request.json = {"ids": "1,2,3"} |
873 | 890 | schema_cls = Schema.from_dict({"ids": fields.DelimitedList(fields.Int())}) |
1031 | 1048 | parser.parse(args, web_request) |
1032 | 1049 | |
1033 | 1050 | |
1051 | @pytest.mark.parametrize("input_dict", multidicts) | |
1052 | @pytest.mark.parametrize( | |
1053 | "setting", | |
1054 | [ | |
1055 | "is_multiple_true", | |
1056 | "is_multiple_false", | |
1057 | "is_multiple_notset", | |
1058 | "list_field", | |
1059 | "tuple_field", | |
1060 | "added_to_known", | |
1061 | ], | |
1062 | ) | |
1063 | def test_is_multiple_detection(web_request, parser, input_dict, setting): | |
1064 | # this custom class "multiplexes" in that it can be given a single value or | |
1065 | # list of values -- a single value is treated as a string, and a list of | |
1066 | # values is treated as a list of strings | |
1067 | class CustomMultiplexingField(fields.String): | |
1068 | def _deserialize(self, value, attr, data, **kwargs): | |
1069 | if isinstance(value, str): | |
1070 | return super()._deserialize(value, attr, data, **kwargs) | |
1071 | return [ | |
1072 | self._deserialize(v, attr, data, **kwargs) | |
1073 | for v in value | |
1074 | if isinstance(v, str) | |
1075 | ] | |
1076 | ||
1077 | def _serialize(self, value, attr, **kwargs): | |
1078 | if isinstance(value, str): | |
1079 | return super()._serialize(value, attr, **kwargs) | |
1080 | return [ | |
1081 | self._serialize(v, attr, **kwargs) for v in value if isinstance(v, str) | |
1082 | ] | |
1083 | ||
1084 | class CustomMultipleField(CustomMultiplexingField): | |
1085 | is_multiple = True | |
1086 | ||
1087 | class CustomNonMultipleField(CustomMultiplexingField): | |
1088 | is_multiple = False | |
1089 | ||
1090 | # the request's query params are the input multidict | |
1091 | web_request.query = input_dict | |
1092 | ||
1093 | # case 1: is_multiple=True | |
1094 | if setting == "is_multiple_true": | |
1095 | # the multidict should unpack to a list of strings | |
1096 | # | |
1097 | # order is not necessarily guaranteed by the multidict implementations, but | |
1098 | # both values must be present | |
1099 | args = {"foos": CustomMultipleField()} | |
1100 | result = parser.parse(args, web_request, location="query") | |
1101 | assert result["foos"] in (["a", "b"], ["b", "a"]) | |
1102 | # case 2: is_multiple=False | |
1103 | elif setting == "is_multiple_false": | |
1104 | # the multidict should unpack to a string | |
1105 | # | |
1106 | # either value may be returned, depending on the multidict implementation, | |
1107 | # but not both | |
1108 | args = {"foos": CustomNonMultipleField()} | |
1109 | result = parser.parse(args, web_request, location="query") | |
1110 | assert result["foos"] in ("a", "b") | |
1111 | # case 3: is_multiple is not set | |
1112 | elif setting == "is_multiple_notset": | |
1113 | # this should be the same as is_multiple=False | |
1114 | args = {"foos": CustomMultiplexingField()} | |
1115 | result = parser.parse(args, web_request, location="query") | |
1116 | assert result["foos"] in ("a", "b") | |
1117 | # case 4: the field is a List (special case) | |
1118 | elif setting == "list_field": | |
1119 | # this should behave like the is_multiple=True case | |
1120 | args = {"foos": fields.List(fields.Str())} | |
1121 | result = parser.parse(args, web_request, location="query") | |
1122 | assert result["foos"] in (["a", "b"], ["b", "a"]) | |
1123 | # case 5: the field is a Tuple (special case) | |
1124 | elif setting == "tuple_field": | |
1125 | # this should behave like the is_multiple=True case and produce a tuple | |
1126 | args = {"foos": fields.Tuple((fields.Str, fields.Str))} | |
1127 | result = parser.parse(args, web_request, location="query") | |
1128 | assert result["foos"] in (("a", "b"), ("b", "a")) | |
1129 | # case 6: the field is custom, but added to the known fields of the parser | |
1130 | elif setting == "added_to_known": | |
1131 | # if it's included in the known multifields and is_multiple is not set, behave | |
1132 | # like is_multiple=True | |
1133 | parser.KNOWN_MULTI_FIELDS.append(CustomMultiplexingField) | |
1134 | args = {"foos": CustomMultiplexingField()} | |
1135 | result = parser.parse(args, web_request, location="query") | |
1136 | assert result["foos"] in (["a", "b"], ["b", "a"]) | |
1137 | else: | |
1138 | raise NotImplementedError | |
1139 | ||
1140 | ||
1034 | 1141 | def test_validation_errors_in_validator_are_passed_to_handle_error(parser, web_request): |
1035 | 1142 | def validate(value): |
1036 | 1143 | raise ValidationError("Something went wrong.") |
1133 | 1240 | p = CustomParser() |
1134 | 1241 | ret = p.parse(argmap, web_request) |
1135 | 1242 | assert ret == {"value": "hello world"} |
1243 | ||
1244 | ||
1245 | def test_parser_pre_load(web_request): | |
1246 | class CustomParser(MockRequestParser): | |
1247 | # pre-load hook to strip whitespace from query params | |
1248 | def pre_load(self, data, *, schema, req, location): | |
1249 | if location == "query": | |
1250 | return {k: v.strip() for k, v in data.items()} | |
1251 | return data | |
1252 | ||
1253 | parser = CustomParser() | |
1254 | ||
1255 | # mock data for both query and json | |
1256 | web_request.query = web_request.json = {"value": " hello "} | |
1257 | argmap = {"value": fields.Str()} | |
1258 | ||
1259 | # data gets through for 'json' just fine | |
1260 | ret = parser.parse(argmap, web_request) | |
1261 | assert ret == {"value": " hello "} | |
1262 | ||
1263 | # but for 'query', the pre_load hook changes things | |
1264 | ret = parser.parse(argmap, web_request, location="query") | |
1265 | assert ret == {"value": "hello"} | |
1266 | ||
1267 | ||
1268 | # this test is meant to be a run of the WhitspaceStrippingFlaskParser we give | |
1269 | # in the docs/advanced.rst examples for how to use pre_load | |
1270 | # this helps ensure that the example code is correct | |
1271 | # rather than a FlaskParser, we're working with the mock parser, but it's | |
1272 | # otherwise the same | |
1273 | def test_whitespace_stripping_parser_example(web_request): | |
1274 | def _strip_whitespace(value): | |
1275 | if isinstance(value, str): | |
1276 | value = value.strip() | |
1277 | elif isinstance(value, typing.Mapping): | |
1278 | return {k: _strip_whitespace(value[k]) for k in value} | |
1279 | elif isinstance(value, (list, tuple)): | |
1280 | return type(value)(map(_strip_whitespace, value)) | |
1281 | return value | |
1282 | ||
1283 | class WhitspaceStrippingParser(MockRequestParser): | |
1284 | def pre_load(self, location_data, *, schema, req, location): | |
1285 | if location in ("query", "form"): | |
1286 | ret = _strip_whitespace(location_data) | |
1287 | return ret | |
1288 | return location_data | |
1289 | ||
1290 | parser = WhitspaceStrippingParser() | |
1291 | ||
1292 | # mock data for query, form, and json | |
1293 | web_request.form = web_request.query = web_request.json = {"value": " hello "} | |
1294 | argmap = {"value": fields.Str()} | |
1295 | ||
1296 | # data gets through for 'json' just fine | |
1297 | ret = parser.parse(argmap, web_request) | |
1298 | assert ret == {"value": " hello "} | |
1299 | ||
1300 | # but for 'query' and 'form', the pre_load hook changes things | |
1301 | for loc in ("query", "form"): | |
1302 | ret = parser.parse(argmap, web_request, location=loc) | |
1303 | assert ret == {"value": "hello"} | |
1304 | ||
1305 | # check that it applies in the case where the field is a list type | |
1306 | # applied to an argument (logic for `tuple` is effectively the same) | |
1307 | web_request.form = web_request.query = web_request.json = { | |
1308 | "ids": [" 1", "3", " 4"], | |
1309 | "values": [" foo ", " bar"], | |
1310 | } | |
1311 | schema = Schema.from_dict( | |
1312 | {"ids": fields.List(fields.Int), "values": fields.List(fields.Str)} | |
1313 | ) | |
1314 | for loc in ("query", "form"): | |
1315 | ret = parser.parse(schema, web_request, location=loc) | |
1316 | assert ret == {"ids": [1, 3, 4], "values": ["foo", "bar"]} | |
1317 | ||
1318 | # json loading should also work even though the pre_load hook above | |
1319 | # doesn't strip whitespace from JSON data | |
1320 | # - values=[" foo ", ...] will have whitespace preserved | |
1321 | # - ids=[" 1", ...] will still parse okay because " 1" is valid for fields.Int | |
1322 | ret = parser.parse(schema, web_request, location="json") | |
1323 | assert ret == {"ids": [1, 3, 4], "values": [" foo ", " bar"]} | |
1324 | ||
1325 | ||
1326 | def test_parse_rejects_non_dict_argmap_mapping(parser, web_request): | |
1327 | web_request.json = {"username": 42, "password": 42} | |
1328 | argmap = collections.UserDict( | |
1329 | {"username": fields.Field(), "password": fields.Field()} | |
1330 | ) | |
1331 | ||
1332 | # UserDict is dict-like in all meaningful ways, but not a subclass of `dict` | |
1333 | # it will therefore be rejected with a TypeError when used | |
1334 | with pytest.raises(TypeError): | |
1335 | parser.parse(argmap, web_request) |
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 | extras = frameworks | |
32 | 33 | commands = mypy src/ |
33 | 34 | |
34 | 35 | [testenv:docs] |