New upstream version 5.1.2
Sophie Brun
5 years ago
3 | 3 | hooks: |
4 | 4 | - id: black |
5 | 5 | language_version: python3.6 |
6 | - repo: https://github.com/pre-commit/pre-commit-hooks | |
7 | rev: v2.0.0 | |
6 | - repo: https://gitlab.com/pycqa/flake8 | |
7 | rev: 3.7.4 | |
8 | 8 | hooks: |
9 | 9 | - id: flake8 |
10 | additional_dependencies: ["flake8-bugbear==18.8.0"] | |
10 | 11 | - repo: https://github.com/asottile/blacken-docs |
11 | 12 | rev: v0.3.0 |
12 | 13 | hooks: |
13 | 14 | - id: blacken-docs |
14 | 15 | additional_dependencies: [black==18.9b0] |
16 | - repo: https://github.com/pre-commit/mirrors-mypy | |
17 | rev: v0.660 | |
18 | hooks: | |
19 | - id: mypy | |
20 | language_version: python3.6 | |
21 | files: ^webargs/ |
0 | 0 | language: python |
1 | sudo: false | |
2 | 1 | cache: pip |
3 | 2 | install: travis_retry pip install -U tox |
4 | 3 | script: tox |
17 | 16 | - { python: '3.6', env: TOXENV=py36-marshmallow2 } |
18 | 17 | - { python: '3.6', env: TOXENV=py36-marshmallow3 } |
19 | 18 | |
20 | - { python: '3.7', env: TOXENV=py37-marshmallow2, dist: xenial, sudo: required } | |
21 | - { python: '3.7', env: TOXENV=py37-marshmallow3, dist: xenial, sudo: required } | |
19 | - { python: '3.7', env: TOXENV=py37-marshmallow2, dist: xenial } | |
20 | - { python: '3.7', env: TOXENV=py37-marshmallow3, dist: xenial } | |
22 | 21 | |
23 | 22 | - { python: '3.6', env: TOXENV=docs } |
24 | 23 | |
35 | 34 | distributions: sdist bdist_wheel |
36 | 35 | password: |
37 | 36 | secure: TMeTi5OPl2cYsl5hNP4w1xESd2vQUOy8NgZ0c3KbrVSSeBuUCGOKyYJZNGzD9KDMucCvYFuxCwYiDxP8tB5iT85z3rhdVkzppZTy3/3kXMODjlhMzqTnCdJSOoZZ+D5/Y3Zqb8QxU78NggPutfX4bbUU/wNsVbdODXWHe5y2q3k= |
37 | ||
38 | - stage: PyPI Release | |
39 | if: tag IS present | |
40 | python: "2.7" | |
41 | install: skip | |
42 | script: skip | |
43 | deploy: | |
44 | provider: pypi | |
45 | user: sloria | |
46 | on: | |
47 | tags: true | |
48 | distributions: bdist_wheel | |
49 | password: | |
50 | secure: TMeTi5OPl2cYsl5hNP4w1xESd2vQUOy8NgZ0c3KbrVSSeBuUCGOKyYJZNGzD9KDMucCvYFuxCwYiDxP8tB5iT85z3rhdVkzppZTy3/3kXMODjlhMzqTnCdJSOoZZ+D5/Y3Zqb8QxU78NggPutfX4bbUU/wNsVbdODXWHe5y2q3k= |
0 | 0 | Changelog |
1 | 1 | --------- |
2 | ||
3 | 5.1.2 (2019-02-03) | |
4 | ****************** | |
5 | ||
6 | Bug fixes: | |
7 | ||
8 | * Remove lingering usages of ``ValidationError.status_code`` | |
9 | (:issue:`365`). Thanks :user:`decaz` for reporting. | |
10 | * Avoid ``AttributeError`` on Python<3.5.4 (:issue:`366`). | |
11 | * Fix incorrect type annotations for ``error_headers``. | |
12 | * Fix outdated docs (:issue:`367`). Thanks :user:`alexandersoto` for reporting. | |
13 | ||
14 | 5.1.1.post0 (2019-01-30) | |
15 | ************************ | |
16 | ||
17 | * Include LICENSE in sdist (:issue:`364`). | |
18 | ||
19 | 5.1.1 (2019-01-28) | |
20 | ****************** | |
21 | ||
22 | Bug fixes: | |
23 | ||
24 | * Fix installing ``simplejson`` on Python 2 by | |
25 | distributing a Python 2-only wheel (:issue:`363`). | |
26 | ||
27 | 5.1.0 (2019-01-11) | |
28 | ****************** | |
29 | ||
30 | Features: | |
31 | ||
32 | * Error handlers for `AsyncParser` classes may be coroutine functions. | |
33 | * Add type annotations to `AsyncParser` and `AIOHTTPParser`. | |
34 | ||
35 | Bug fixes: | |
36 | ||
37 | * Fix compatibility with Flask<1.0 (:issue:`355`). | |
38 | Thanks :user:`hoatle` for reporting. | |
39 | * Address warning on Python 3.7 about importing from ``collections.abc``. | |
40 | ||
41 | 5.0.0 (2019-01-03) | |
42 | ****************** | |
43 | ||
44 | Features: | |
45 | ||
46 | * *Backwards-incompatible*: A 400 HTTPError is raised when an | |
47 | invalid JSON payload is passed. (:issue:`329`). | |
48 | Thanks :user:`zedrdave` for reporting. | |
49 | ||
50 | Other changes: | |
51 | ||
52 | * *Backwards-incompatible*: `webargs.argmap2schema` is removed. Use | |
53 | `webargs.dict2schema` instead. | |
54 | * *Backwards-incompatible*: `webargs.ValidationError` is removed. | |
55 | Use `marshmallow.ValidationError` instead. | |
56 | ||
57 | ||
58 | .. code-block:: python | |
59 | ||
60 | # <5.0.0 | |
61 | from webargs import ValidationError | |
62 | ||
63 | ||
64 | def auth_validator(value): | |
65 | # ... | |
66 | raise ValidationError("Authentication failed", status_code=401) | |
67 | ||
68 | ||
69 | @use_args({"auth": fields.Field(validate=auth_validator)}) | |
70 | def auth_view(args): | |
71 | return jsonify(args) | |
72 | ||
73 | ||
74 | # >=5.0.0 | |
75 | from marshmallow import ValidationError | |
76 | ||
77 | ||
78 | def auth_validator(value): | |
79 | # ... | |
80 | raise ValidationError("Authentication failed") | |
81 | ||
82 | ||
83 | @use_args({"auth": fields.Field(validate=auth_validator)}, error_status_code=401) | |
84 | def auth_view(args): | |
85 | return jsonify(args) | |
86 | ||
87 | ||
88 | * *Backwards-incompatible*: Missing arguments will no longer be filled | |
89 | in when using ``@use_kwargs`` (:issue:`342,307,252`). Use ``**kwargs`` | |
90 | to account for non-required fields. | |
91 | ||
92 | .. code-block:: python | |
93 | ||
94 | # <5.0.0 | |
95 | @use_kwargs( | |
96 | {"first_name": fields.Str(required=True), "last_name": fields.Str(required=False)} | |
97 | ) | |
98 | def myview(first_name, last_name): | |
99 | # last_name is webargs.missing if it's missing from the request | |
100 | return {"first_name": first_name} | |
101 | ||
102 | ||
103 | # >=5.0.0 | |
104 | @use_kwargs( | |
105 | {"first_name": fields.Str(required=True), "last_name": fields.Str(required=False)} | |
106 | ) | |
107 | def myview(first_name, **kwargs): | |
108 | # last_name will not be in kwargs if it's missing from the request | |
109 | return {"first_name": first_name} | |
110 | ||
111 | ||
112 | * `simplejson <https://pypi.org/project/simplejson/>`_ is now a required | |
113 | dependency on Python 2 (:pr:`334`). | |
114 | This ensures consistency of behavior across Python 2 and 3. | |
2 | 115 | |
3 | 116 | 4.4.1 (2018-01-03) |
4 | 117 | ****************** |
0 | For code of conduct, see https://marshmallow.readthedocs.io/en/dev/code_of_conduct.html | |
0 | For the code of conduct, see https://marshmallow.readthedocs.io/en/dev/code_of_conduct.html |
0 | Copyright 2014-2017 Steven Loria and contributors | |
0 | Copyright 2014-2019 Steven Loria and contributors | |
1 | 1 | |
2 | 2 | Permission is hereby granted, free of charge, to any person obtaining a copy |
3 | 3 | of this software and associated documentation files (the "Software"), to deal |
2 | 2 | ******* |
3 | 3 | |
4 | 4 | .. image:: https://badgen.net/pypi/v/webargs |
5 | :target: https://badge.fury.io/py/webargs | |
5 | :target: https://pypi.org/project/webargs/ | |
6 | 6 | :alt: PyPI version |
7 | 7 | |
8 | 8 | .. image:: https://badgen.net/travis/marshmallow-code/webargs |
73 | 73 | |
74 | 74 | .. image:: https://opencollective.com/marshmallow/donate/button.png |
75 | 75 | :target: https://opencollective.com/marshmallow |
76 | :width: 200 | |
76 | 77 | :alt: Donate to our collective |
77 | 78 | |
78 | 79 | Professional Support |
0 | /* Styles for sponsors.html */ | |
1 | .sponsor .logo img { | |
2 | width: 48px; | |
3 | vertical-align: middle; | |
4 | } | |
5 | .sponsor a.logo { | |
6 | float: left; | |
7 | border-bottom: none; | |
8 | } |
0 | 0 | <div class="sponsors"> |
1 | 1 | {% if tidelift_url %} |
2 | 2 | <div class="sponsor"> |
3 | <a class="logo" href="{{ tidelift_url }}"> | |
3 | <a class="image" href="{{ tidelift_url }}"> | |
4 | 4 | <img src="https://user-images.githubusercontent.com/2379650/45126032-50b69880-b13f-11e8-9c2c-abd16c433495.png" alt="Sponsor"></a> |
5 | <div class="sponsor_text">Professionally-supported {{ project }} is available with the | |
5 | <div class="text">Professionally-supported {{ project }} is available with the | |
6 | 6 | <a href="{{ tidelift_url }}">Tidelift Subscription</a>. |
7 | 7 | </div> |
8 | 8 | </div> |
47 | 47 | # Add any paths that contain custom themes here, relative to this directory. |
48 | 48 | html_theme = "sphinx_typlog_theme" |
49 | 49 | html_theme_path = [sphinx_typlog_theme.get_path()] |
50 | html_static_path = ["_static"] | |
51 | 50 | |
52 | 51 | html_theme_options = { |
53 | 52 | "color": "#268bd2", |
75 | 74 | "sponsors.html", |
76 | 75 | ] |
77 | 76 | } |
78 | ||
79 | ||
80 | def setup(app): | |
81 | app.add_stylesheet("css/extra.css") |
0 | Ecosystem | |
1 | ========= | |
2 | ||
3 | A list of webargs-related libraries can be found at the GitHub wiki here: | |
4 | ||
5 | https://github.com/marshmallow-code/webargs/wiki/Ecosystem |
40 | 40 | |
41 | 41 | from flask import jsonify |
42 | 42 | |
43 | ||
43 | 44 | # Return validation errors as JSON |
44 | 45 | @app.errorhandler(422) |
45 | def handle_validation_error(err): | |
46 | exc = getattr(err, "exc", None) | |
47 | if exc: | |
48 | headers = err.data["headers"] | |
49 | messages = exc.messages | |
50 | else: | |
51 | headers = None | |
52 | messages = ["Invalid request."] | |
46 | @app.errorhandler(400) | |
47 | def handle_error(err): | |
48 | headers = err.data.get("headers", None) | |
49 | messages = err.data.get("messages", ["Invalid request."]) | |
53 | 50 | if headers: |
54 | 51 | return jsonify({"errors": messages}), err.code, headers |
55 | 52 | else: |
130 | 127 | |
131 | 128 | from django.http import JsonResponse |
132 | 129 | |
133 | from webargs import fields, ValidationError | |
130 | from webargs import fields, ValidationError, json | |
134 | 131 | |
135 | 132 | argmap = {"name": fields.Str(required=True)} |
136 | 133 | |
139 | 136 | try: |
140 | 137 | args = parser.parse(argmap, request) |
141 | 138 | except ValidationError as err: |
142 | return JsonResponse(err.messages, status=err.status_code) | |
139 | return JsonResponse(err.messages, status=422) | |
140 | except json.JSONDecodeError: | |
141 | return JsonResponse({"json": ["Invalid JSON body."]}, status=400) | |
143 | 142 | return JsonResponse({"message": "Hello {name}".format(name=name)}) |
144 | 143 | |
145 | 144 | Tornado |
214 | 213 | """Write errors as JSON.""" |
215 | 214 | self.set_header("Content-Type", "application/json") |
216 | 215 | if "exc_info" in kwargs: |
217 | etype, value, traceback = kwargs["exc_info"] | |
218 | if hasattr(value, "messages"): | |
219 | self.write({"errors": value.messages}) | |
216 | etype, exc, traceback = kwargs["exc_info"] | |
217 | if hasattr(exc, "messages"): | |
218 | self.write({"errors": exc.messages}) | |
219 | if getattr(exc, "headers", None): | |
220 | for name, val in exc.headers.items(): | |
221 | self.set_header(name, val) | |
220 | 222 | self.finish() |
221 | 223 | |
222 | 224 | Pyramid |
85 | 85 | quickstart |
86 | 86 | advanced |
87 | 87 | framework_support |
88 | ecosystem | |
88 | 89 | |
89 | 90 | API Reference |
90 | 91 | ------------- |
27 | 27 | # When value is keyed on a variable-unsafe name |
28 | 28 | # or you want to rename a key |
29 | 29 | "content_type": fields.Str(load_from="Content-Type", location="headers"), |
30 | # OR, on marshmallow 3 | |
31 | # "content_type": fields.Str(data_key="Content-Type", location="headers"), | |
32 | # File uploads | |
33 | "profile_image": fields.Field( | |
34 | location="files", validate=lambda f: f.mimetype in ["image/jpeg", "image/png"] | |
35 | ), | |
30 | 36 | } |
31 | 37 | |
32 | 38 | .. note:: |
81 | 87 | |
82 | 88 | .. note:: |
83 | 89 | |
84 | When using `use_kwargs`, any missing values for non-required fields will take the special value `missing <marshmallow.missing>`. | |
90 | When using `use_kwargs`, any missing values will be omitted from the arguments. | |
91 | Use ``**kwargs`` to handle optional arguments. | |
85 | 92 | |
86 | 93 | .. code-block:: python |
87 | 94 | |
88 | 95 | from webargs import fields, missing |
89 | 96 | |
90 | 97 | |
91 | @use_kwargs({"name": fields.Str(), "nickname": fields.Str(required=False)}) | |
92 | def myview(name, nickname): | |
93 | if nickname is missing: | |
98 | @use_kwargs({"name": fields.Str(required=True), "nickname": fields.Str(required=False)}) | |
99 | def myview(name, **kwargs): | |
100 | if "nickname" not in kwargs: | |
94 | 101 | # ... |
95 | 102 | pass |
96 | 103 | |
169 | 176 | -------------- |
170 | 177 | |
171 | 178 | Each parser has a default error handling method. To override the error handling callback, write a function that |
172 | receives an error, the request, and the `marshmallow.Schema` instance. | |
179 | receives an error, the request, the `marshmallow.Schema` instance, status code, and headers. | |
173 | 180 | Then decorate that function with :func:`Parser.error_handler <webargs.core.Parser.error_handler>`. |
174 | 181 | |
175 | 182 | .. code-block:: python |
0 | 0 | -e '.[frameworks]' |
1 | 1 | Sphinx==1.8.3 |
2 | 2 | sphinx-issues==1.2.0 |
3 | # TODO: Use PyPI release of sphinx-typlog-theme after latest changes are released | |
4 | git+https://github.com/typlog/sphinx-typlog-theme.git@45a71b3cd99c7550e5999042504d3b9c83b68791 | |
3 | sphinx-typlog-theme==0.7.1 |
28 | 28 | if isinstance(value, fields.Field) and name != "return" |
29 | 29 | } |
30 | 30 | response_schema = annotations.get("return") |
31 | parsed = parser.parse(reqargs, request, force_all=True) | |
31 | parsed = parser.parse(reqargs, request) | |
32 | 32 | kw.update(parsed) |
33 | 33 | response_data = func(*a, **kw) |
34 | 34 | if response_schema: |
52 | 52 | |
53 | 53 | |
54 | 54 | class UserSchema(Schema): |
55 | id = fields.Int() | |
56 | name = fields.Str() | |
55 | id = fields.Int(required=True) | |
56 | name = fields.Str(required=True) | |
57 | 57 | date_created = fields.DateTime(dump_only=True) |
58 | 58 | |
59 | 59 | |
73 | 73 | # Return validation errors as JSON |
74 | 74 | @app.errorhandler(422) |
75 | 75 | @app.errorhandler(400) |
76 | def handle_validation_error(err): | |
77 | exc = getattr(err, "exc", None) | |
78 | if exc: | |
79 | headers = err.data["headers"] | |
80 | messages = exc.messages | |
81 | else: | |
82 | headers = None | |
83 | messages = ["Invalid request."] | |
76 | def handle_error(err): | |
77 | headers = err.data.get("headers", None) | |
78 | messages = err.data.get("messages", ["Invalid request."]) | |
84 | 79 | if headers: |
85 | 80 | return jsonify({"errors": messages}), err.code, headers |
86 | 81 | else: |
57 | 57 | |
58 | 58 | |
59 | 59 | # Return validation errors as JSON |
60 | @error(400) | |
60 | 61 | @error(422) |
61 | def error422(err): | |
62 | def handle_error(err): | |
62 | 63 | response.content_type = "application/json" |
63 | 64 | return err.body |
64 | 65 |
17 | 17 | """ |
18 | 18 | import datetime as dt |
19 | 19 | |
20 | try: | |
21 | import simplejson as json | |
22 | except ImportError: | |
23 | import json | |
20 | from webargs.core import json | |
24 | 21 | |
25 | 22 | import falcon |
26 | 23 | from webargs import fields, validate |
63 | 63 | # Return validation errors as JSON |
64 | 64 | @app.errorhandler(422) |
65 | 65 | @app.errorhandler(400) |
66 | def handle_validation_error(err): | |
67 | exc = getattr(err, "exc", None) | |
68 | if exc: | |
69 | headers = err.data["headers"] | |
70 | messages = exc.messages | |
71 | else: | |
72 | headers = None | |
73 | messages = ["Invalid request."] | |
66 | def handle_error(err): | |
67 | headers = err.data.get("headers", None) | |
68 | messages = err.data.get("messages", ["Invalid request."]) | |
74 | 69 | if headers: |
75 | 70 | return jsonify({"errors": messages}), err.code, headers |
76 | 71 | else: |
0 | [metadata] | |
1 | license_files = LICENSE | |
2 | ||
0 | 3 | [bdist_wheel] |
1 | universal = 1 | |
4 | # We build separate wheels for | |
5 | # Python 2 and 3 because of the conditional | |
6 | # dependency on simplejson | |
7 | universal = 0 | |
2 | 8 | |
3 | 9 | [flake8] |
4 | ignore = E203, E266, E501, W503, E302, W504 | |
5 | max-line-length = 100 | |
10 | ignore = E203, E266, E501, W503 | |
11 | max-line-length = 80 | |
6 | 12 | max-complexity = 18 |
7 | 13 | select = B,C,E,F,W,T4,B9 |
8 | exclude = .git,.ropeproject,.tox,build,env,venv,__pycache__ | |
9 | 14 | |
10 | [tool:pytest] | |
11 | filterwarnings = | |
12 | ignore::webargs.core.RemovedInWebargs5Warning | |
15 | [mypy] | |
16 | ignore_missing_imports = true |
0 | 0 | # -*- coding: utf-8 -*- |
1 | import sys | |
1 | 2 | import re |
2 | 3 | from setuptools import setup, find_packages |
3 | 4 | |
4 | 5 | INSTALL_REQUIRES = ["marshmallow>=2.15.2"] |
6 | if sys.version_info[0] < 3: | |
7 | INSTALL_REQUIRES.append("simplejson>=2.1.0") | |
8 | ||
5 | 9 | FRAMEWORKS = [ |
6 | 10 | "Flask>=0.12.2", |
7 | 11 | "Django>=1.11.16", |
8 | 12 | "bottle>=0.12.13", |
9 | 13 | "tornado>=4.5.2", |
10 | "pyramid>=1.8.5", | |
14 | "pyramid>=1.9.1", | |
11 | 15 | "webapp2>=3.0.0b1", |
12 | "falcon>=1.3.0", | |
16 | "falcon>=1.4.0", | |
13 | 17 | 'aiohttp>=3.0.0; python_version >= "3.5"', |
14 | 18 | ] |
15 | 19 | EXTRAS_REQUIRE = { |
23 | 27 | ] |
24 | 28 | + FRAMEWORKS, |
25 | 29 | "lint": [ |
30 | 'mypy==0.650; python_version >= "3.5"', | |
26 | 31 | "flake8==3.6.0", |
27 | 32 | 'flake8-bugbear==18.8.0; python_version >= "3.5"', |
28 | 33 | "pre-commit==1.13.0", |
48 | 53 | return version |
49 | 54 | |
50 | 55 | |
51 | __version__ = find_version("webargs/__init__.py") | |
52 | ||
53 | ||
54 | 56 | def read(fname): |
55 | 57 | with open(fname) as fp: |
56 | 58 | content = fp.read() |
59 | 61 | |
60 | 62 | setup( |
61 | 63 | name="webargs", |
62 | version=__version__, | |
64 | version=find_version("webargs/__init__.py"), | |
63 | 65 | description=( |
64 | 66 | "Declarative parsing and validation of HTTP request objects, " |
65 | 67 | "with built-in support for popular web frameworks, including " |
103 | 105 | "Programming Language :: Python :: 3.5", |
104 | 106 | "Programming Language :: Python :: 3.6", |
105 | 107 | "Programming Language :: Python :: 3.7", |
106 | "Programming Language :: Python :: Implementation :: CPython", | |
107 | "Programming Language :: Python :: Implementation :: PyPy", | |
108 | 108 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", |
109 | 109 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", |
110 | 110 | ], |
4 | 4 | from aiohttp import web |
5 | 5 | import marshmallow as ma |
6 | 6 | |
7 | from webargs import fields, ValidationError | |
7 | from webargs import fields | |
8 | 8 | from webargs.aiohttpparser import parser, use_args, use_kwargs |
9 | from webargs.core import MARSHMALLOW_VERSION_INFO | |
9 | from webargs.core import MARSHMALLOW_VERSION_INFO, json | |
10 | 10 | |
11 | 11 | hello_args = {"name": fields.Str(missing="World", validate=lambda n: len(n) >= 3)} |
12 | 12 | hello_multiple = {"name": fields.List(fields.Str())} |
28 | 28 | |
29 | 29 | |
30 | 30 | async def echo(request): |
31 | parsed = await parser.parse(hello_args, request) | |
31 | try: | |
32 | parsed = await parser.parse(hello_args, request) | |
33 | except json.JSONDecodeError: | |
34 | raise web.HTTPBadRequest( | |
35 | body=json.dumps(["Invalid JSON."]).encode("utf-8"), | |
36 | content_type="application/json", | |
37 | ) | |
32 | 38 | return json_response(parsed) |
33 | 39 | |
34 | 40 | |
80 | 86 | |
81 | 87 | async def always_error(request): |
82 | 88 | def always_fail(value): |
83 | raise ValidationError("something went wrong") | |
84 | ||
85 | args = {"text": fields.Str(validate=always_fail)} | |
86 | parsed = await parser.parse(args, request) | |
87 | return json_response(parsed) | |
88 | ||
89 | ||
90 | async def error400(request): | |
91 | def always_fail(value): | |
92 | raise ValidationError("something went wrong", status_code=400) | |
93 | ||
94 | args = {"text": fields.Str(validate=always_fail)} | |
95 | parsed = await parser.parse(args, request) | |
96 | return json_response(parsed) | |
97 | ||
98 | ||
99 | async def error_invalid(request): | |
100 | def always_fail(value): | |
101 | raise ValidationError("something went wrong", status_code=12345) | |
89 | raise ma.ValidationError("something went wrong") | |
102 | 90 | |
103 | 91 | args = {"text": fields.Str(validate=always_fail)} |
104 | 92 | parsed = await parser.parse(args, request) |
200 | 188 | ) |
201 | 189 | add_route(app, ["POST"], "/echo_use_args_multiple", echo_use_args_multiple) |
202 | 190 | add_route(app, ["GET", "POST"], "/error", always_error) |
203 | add_route(app, ["GET", "POST"], "/error400", error400) | |
204 | add_route(app, ["GET"], "/error_invalid", error_invalid) | |
205 | 191 | add_route(app, ["GET"], "/echo_headers", echo_headers) |
206 | 192 | add_route(app, ["GET"], "/echo_cookie", echo_cookie) |
207 | 193 | add_route(app, ["POST"], "/echo_nested", echo_nested) |
0 | import json | |
1 | from bottle import Bottle, HTTPResponse, debug, request, response, error | |
0 | from webargs.core import json | |
1 | from bottle import Bottle, HTTPResponse, debug, request, response | |
2 | 2 | |
3 | 3 | import marshmallow as ma |
4 | from webargs import fields, ValidationError | |
4 | from webargs import fields | |
5 | 5 | from webargs.bottleparser import parser, use_args, use_kwargs |
6 | 6 | from webargs.core import MARSHMALLOW_VERSION_INFO |
7 | 7 | |
37 | 37 | return args |
38 | 38 | |
39 | 39 | |
40 | @app.route("/echo_use_kwargs", method=["GET", "POST"]) | |
41 | @use_kwargs(hello_args) | |
40 | @app.route("/echo_use_kwargs", method=["GET", "POST"], apply=use_kwargs(hello_args)) | |
42 | 41 | def echo_use_kwargs(name): |
43 | 42 | return {"name": name} |
44 | 43 | |
45 | 44 | |
46 | @app.route("/echo_use_args_validated", method=["GET", "POST"]) | |
47 | @use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42) | |
45 | @app.route( | |
46 | "/echo_use_args_validated", | |
47 | method=["GET", "POST"], | |
48 | apply=use_args({"value": fields.Int()}, validate=lambda args: args["value"] > 42), | |
49 | ) | |
48 | 50 | def echo_use_args_validated(args): |
49 | 51 | return args |
50 | 52 | |
60 | 62 | return HTTPResponse(body=json.dumps(arguments), content_type="application/json") |
61 | 63 | |
62 | 64 | |
63 | @app.route("/echo_use_args_with_path_param/<name>") | |
64 | @use_args({"value": fields.Int()}) | |
65 | @app.route( | |
66 | "/echo_use_args_with_path_param/<name>", apply=use_args({"value": fields.Int()}) | |
67 | ) | |
65 | 68 | def echo_use_args_with_path_param(args, name): |
66 | 69 | return args |
67 | 70 | |
68 | 71 | |
69 | @app.route("/echo_use_kwargs_with_path_param/<name>") | |
70 | @use_kwargs({"value": fields.Int()}) | |
72 | @app.route( | |
73 | "/echo_use_kwargs_with_path_param/<name>", apply=use_kwargs({"value": fields.Int()}) | |
74 | ) | |
71 | 75 | def echo_use_kwargs_with_path_param(name, value): |
72 | 76 | return {"value": value} |
73 | 77 | |
75 | 79 | @app.route("/error", method=["GET", "POST"]) |
76 | 80 | def always_error(): |
77 | 81 | def always_fail(value): |
78 | raise ValidationError("something went wrong") | |
79 | ||
80 | args = {"text": fields.Str(validate=always_fail)} | |
81 | return parser.parse(args) | |
82 | ||
83 | ||
84 | @app.route("/error400", method=["GET", "POST"]) | |
85 | def error400(): | |
86 | def always_fail(value): | |
87 | raise ValidationError("something went wrong", status_code=400) | |
82 | raise ma.ValidationError("something went wrong") | |
88 | 83 | |
89 | 84 | args = {"text": fields.Str(validate=always_fail)} |
90 | 85 | return parser.parse(args) |
123 | 118 | return parser.parse(args) |
124 | 119 | |
125 | 120 | |
126 | @error(422) | |
127 | def handle_422(err): | |
121 | @app.error(400) | |
122 | @app.error(422) | |
123 | def handle_error(err): | |
128 | 124 | response.content_type = "application/json" |
129 | 125 | return err.body |
17 | 17 | views.echo_use_kwargs_with_path_param, |
18 | 18 | ), |
19 | 19 | url(r"^error$", views.always_error), |
20 | url(r"^error400$", views.error400), | |
21 | 20 | url(r"^echo_headers$", views.echo_headers), |
22 | 21 | url(r"^echo_cookie$", views.echo_cookie), |
23 | 22 | url(r"^echo_file$", views.echo_file), |
0 | import json | |
0 | from webargs.core import json | |
1 | 1 | from django.http import HttpResponse |
2 | 2 | from django.views.generic import View |
3 | 3 | |
4 | 4 | import marshmallow as ma |
5 | from webargs import fields, ValidationError | |
5 | from webargs import fields | |
6 | 6 | from webargs.djangoparser import parser, use_args, use_kwargs |
7 | 7 | from webargs.core import MARSHMALLOW_VERSION_INFO |
8 | 8 | |
25 | 25 | def echo(request): |
26 | 26 | try: |
27 | 27 | args = parser.parse(hello_args, request) |
28 | except ValidationError as err: | |
29 | return json_response(err.messages, status=err.status_code) | |
28 | except ma.ValidationError as err: | |
29 | return json_response(err.messages, status=parser.DEFAULT_VALIDATION_STATUS) | |
30 | except json.JSONDecodeError: | |
31 | return json_response({"json": ["Invalid JSON body."]}, status=400) | |
30 | 32 | return json_response(args) |
31 | 33 | |
32 | 34 | |
53 | 55 | return json_response( |
54 | 56 | parser.parse(hello_many_schema, request, locations=("json",)) |
55 | 57 | ) |
56 | except ValidationError as err: | |
57 | return json_response(err.messages, status=err.status_code) | |
58 | except ma.ValidationError as err: | |
59 | return json_response(err.messages, status=parser.DEFAULT_VALIDATION_STATUS) | |
58 | 60 | |
59 | 61 | |
60 | 62 | @use_args({"value": fields.Int()}) |
69 | 71 | |
70 | 72 | def always_error(request): |
71 | 73 | def always_fail(value): |
72 | raise ValidationError("something went wrong") | |
74 | raise ma.ValidationError("something went wrong") | |
73 | 75 | |
74 | 76 | argmap = {"text": fields.Str(validate=always_fail)} |
75 | 77 | try: |
76 | 78 | return parser.parse(argmap, request) |
77 | except ValidationError as err: | |
78 | return json_response(err.messages, status=err.status_code) | |
79 | ||
80 | ||
81 | def error400(request): | |
82 | def always_fail(value): | |
83 | raise ValidationError("something went wrong", status_code=400) | |
84 | ||
85 | argmap = {"text": fields.Str(validate=always_fail)} | |
86 | try: | |
87 | return parser.parse(argmap, request) | |
88 | except ValidationError as err: | |
89 | return json_response(err.messages, status=err.status_code) | |
79 | except ma.ValidationError as err: | |
80 | return json_response(err.messages, status=parser.DEFAULT_VALIDATION_STATUS) | |
90 | 81 | |
91 | 82 | |
92 | 83 | def echo_headers(request): |
121 | 112 | def get(self, request): |
122 | 113 | try: |
123 | 114 | args = parser.parse(hello_args, self.request) |
124 | except ValidationError as err: | |
125 | return json_response(err.messages, status=err.status_code) | |
115 | except ma.ValidationError as err: | |
116 | return json_response(err.messages, status=parser.DEFAULT_VALIDATION_STATUS) | |
126 | 117 | return json_response(args) |
127 | 118 | |
128 | 119 | post = get |
0 | import json | |
0 | from webargs.core import json | |
1 | 1 | |
2 | 2 | import falcon |
3 | 3 | import marshmallow as ma |
4 | from webargs import fields, ValidationError | |
4 | from webargs import fields | |
5 | 5 | from webargs.falconparser import parser, use_args, use_kwargs |
6 | 6 | from webargs.core import MARSHMALLOW_VERSION_INFO |
7 | 7 | |
19 | 19 | |
20 | 20 | class Echo(object): |
21 | 21 | def on_get(self, req, resp): |
22 | parsed = parser.parse(hello_args, req) | |
23 | resp.body = json.dumps(parsed) | |
22 | try: | |
23 | parsed = parser.parse(hello_args, req) | |
24 | except json.JSONDecodeError: | |
25 | resp.body = json.dumps(["Invalid JSON."]) | |
26 | resp.status = falcon.HTTP_400 | |
27 | else: | |
28 | resp.body = json.dumps(parsed) | |
24 | 29 | |
25 | 30 | on_post = on_get |
26 | 31 | |
86 | 91 | class AlwaysError(object): |
87 | 92 | def on_get(self, req, resp): |
88 | 93 | def always_fail(value): |
89 | raise ValidationError("something went wrong") | |
94 | raise ma.ValidationError("something went wrong") | |
90 | 95 | |
91 | 96 | args = {"text": fields.Str(validate=always_fail)} |
92 | 97 | resp.body = json.dumps(parser.parse(args, req)) |
93 | 98 | |
94 | 99 | on_post = on_get |
95 | ||
96 | ||
97 | class Error400(object): | |
98 | def on_get(self, req, resp): | |
99 | def always_fail(value): | |
100 | raise ValidationError("something went wrong", status_code=400) | |
101 | ||
102 | args = {"text": fields.Str(validate=always_fail)} | |
103 | resp.body = json.dumps(parser.parse(args, req)) | |
104 | ||
105 | on_post = on_get | |
106 | ||
107 | ||
108 | class ErrorInvalid(object): | |
109 | def on_get(self, req, resp): | |
110 | def always_fail(value): | |
111 | raise ValidationError("something went wrong", status_code=12345) | |
112 | ||
113 | args = {"text": fields.Str(validate=always_fail)} | |
114 | resp.body = json.dumps(parser.parse(args, req)) | |
115 | 100 | |
116 | 101 | |
117 | 102 | class EchoHeaders(object): |
168 | 153 | "/echo_use_kwargs_with_path_param/{name}", EchoUseKwargsWithPathParam() |
169 | 154 | ) |
170 | 155 | app.add_route("/error", AlwaysError()) |
171 | app.add_route("/error400", Error400()) | |
172 | 156 | app.add_route("/echo_headers", EchoHeaders()) |
173 | 157 | app.add_route("/echo_cookie", EchoCookie()) |
174 | 158 | app.add_route("/echo_nested", EchoNested()) |
175 | 159 | app.add_route("/echo_nested_many", EchoNestedMany()) |
176 | 160 | app.add_route("/echo_use_args_hook", EchoUseArgsHook()) |
177 | app.add_route("/error_invalid", ErrorInvalid()) | |
178 | 161 | return app |
0 | import json | |
0 | from webargs.core import json | |
1 | 1 | from flask import Flask, jsonify as J, Response, request |
2 | 2 | from flask.views import MethodView |
3 | 3 | |
4 | 4 | import marshmallow as ma |
5 | from webargs import fields, ValidationError, missing | |
5 | from webargs import fields | |
6 | 6 | from webargs.flaskparser import parser, use_args, use_kwargs |
7 | 7 | from webargs.core import MARSHMALLOW_VERSION_INFO |
8 | 8 | |
80 | 80 | @app.route("/error", methods=["GET", "POST"]) |
81 | 81 | def error(): |
82 | 82 | def always_fail(value): |
83 | raise ValidationError("something went wrong") | |
84 | ||
85 | args = {"text": fields.Str(validate=always_fail)} | |
86 | return J(parser.parse(args)) | |
87 | ||
88 | ||
89 | @app.route("/error400", methods=["GET", "POST"]) | |
90 | def error400(): | |
91 | def always_fail(value): | |
92 | raise ValidationError("something went wrong", status_code=400) | |
83 | raise ma.ValidationError("something went wrong") | |
93 | 84 | |
94 | 85 | args = {"text": fields.Str(validate=always_fail)} |
95 | 86 | return J(parser.parse(args)) |
173 | 164 | |
174 | 165 | |
175 | 166 | @app.route("/echo_use_kwargs_missing", methods=["post"]) |
176 | @use_kwargs({"username": fields.Str(), "password": fields.Str()}) | |
177 | def echo_use_kwargs_missing(username, password): | |
178 | assert password is missing | |
167 | @use_kwargs({"username": fields.Str(required=True), "password": fields.Str()}) | |
168 | def echo_use_kwargs_missing(username, **kwargs): | |
169 | assert "password" not in kwargs | |
179 | 170 | return J({"username": username}) |
180 | 171 | |
181 | 172 | |
182 | 173 | # Return validation errors as JSON |
183 | 174 | @app.errorhandler(422) |
184 | def handle_validation_error(err): | |
185 | assert isinstance(err.data["schema"], ma.Schema) | |
186 | return J({"errors": err.exc.messages}), err.code | |
175 | @app.errorhandler(400) | |
176 | def handle_error(err): | |
177 | if err.code == 422: | |
178 | assert isinstance(err.data["schema"], ma.Schema) | |
179 | return J(err.data["messages"]), err.code |
0 | from webargs.core import json | |
1 | ||
0 | 2 | from pyramid.config import Configurator |
3 | from pyramid.httpexceptions import HTTPBadRequest | |
1 | 4 | import marshmallow as ma |
2 | 5 | |
3 | from webargs import fields, ValidationError | |
6 | from webargs import fields | |
4 | 7 | from webargs.pyramidparser import parser, use_args, use_kwargs |
5 | 8 | from webargs.core import MARSHMALLOW_VERSION_INFO |
6 | 9 | |
17 | 20 | |
18 | 21 | |
19 | 22 | def echo(request): |
20 | return parser.parse(hello_args, request) | |
23 | try: | |
24 | return parser.parse(hello_args, request) | |
25 | except json.JSONDecodeError: | |
26 | error = HTTPBadRequest() | |
27 | error.body = json.dumps(["Invalid JSON."]).encode("utf-8") | |
28 | error.content_type = "application/json" | |
29 | raise error | |
21 | 30 | |
22 | 31 | |
23 | 32 | def echo_query(request): |
59 | 68 | |
60 | 69 | def always_error(request): |
61 | 70 | def always_fail(value): |
62 | raise ValidationError("something went wrong") | |
63 | ||
64 | argmap = {"text": fields.Str(validate=always_fail)} | |
65 | return parser.parse(argmap, request) | |
66 | ||
67 | ||
68 | def error400(request): | |
69 | def always_fail(value): | |
70 | raise ValidationError("something went wrong", status_code=400) | |
71 | raise ma.ValidationError("something went wrong") | |
71 | 72 | |
72 | 73 | argmap = {"text": fields.Str(validate=always_fail)} |
73 | 74 | return parser.parse(argmap, request) |
140 | 141 | echo_use_kwargs_with_path_param, |
141 | 142 | ) |
142 | 143 | add_route(config, "/error", always_error) |
143 | add_route(config, "/error400", error400) | |
144 | 144 | add_route(config, "/echo_headers", echo_headers) |
145 | 145 | add_route(config, "/echo_cookie", echo_cookie) |
146 | 146 | add_route(config, "/echo_file", echo_file) |
14 | 14 | Parser, |
15 | 15 | get_value, |
16 | 16 | dict2schema, |
17 | argmap2schema, | |
18 | 17 | is_json, |
19 | 18 | get_mimetype, |
20 | 19 | MARSHMALLOW_VERSION_INFO, |
124 | 123 | def test_parse_required_arg_raises_validation_error(parser, web_request): |
125 | 124 | web_request.json = {} |
126 | 125 | args = {"foo": fields.Field(required=True)} |
127 | with pytest.raises(ValidationError) as excinfo: | |
126 | with pytest.raises(ValidationError, match="Missing data for required field."): | |
128 | 127 | parser.parse(args, web_request) |
129 | assert "Missing data for required field." in str(excinfo) | |
130 | exc = excinfo.value | |
131 | assert exc.status_code == 422 | |
132 | ||
133 | ||
134 | # https://github.com/marshmallow-code/webargs/issues/180#issuecomment-394869645 | |
135 | def test_overriding_default_status_code(web_request): | |
136 | class MockRequestParserThatReturns400s(MockRequestParser): | |
137 | DEFAULT_VALIDATION_STATUS = 400 | |
138 | ||
139 | parser = MockRequestParserThatReturns400s() | |
140 | ||
141 | web_request.json = {} | |
142 | args = {"foo": fields.Field(required=True)} | |
143 | with pytest.raises(ValidationError) as excinfo: | |
144 | parser.parse(args, web_request) | |
145 | assert "Missing data for required field." in str(excinfo) | |
146 | exc = excinfo.value | |
147 | assert exc.status_code == 400 | |
148 | 128 | |
149 | 129 | |
150 | 130 | def test_arg_not_required_excluded_in_parsed_output(parser, web_request): |
295 | 275 | class CustomError(Exception): |
296 | 276 | pass |
297 | 277 | |
298 | def error_handler(error, req, schema): | |
278 | def error_handler(error, req, schema, status_code, headers): | |
299 | 279 | assert isinstance(schema, Schema) |
300 | 280 | raise CustomError(error) |
301 | 281 | |
315 | 295 | parser = Parser() |
316 | 296 | |
317 | 297 | @parser.error_handler |
318 | def handle_error(error, req, schema): | |
298 | def handle_error(error, req, schema, status_code, headers): | |
319 | 299 | assert isinstance(schema, Schema) |
320 | 300 | raise CustomError(error) |
321 | 301 | |
510 | 490 | parser.parse(args, web_request, locations=("json",)) |
511 | 491 | assert "Content-Type" in excinfo.value.messages |
512 | 492 | assert excinfo.value.messages["Content-Type"] == ["Not a valid string."] |
513 | ||
514 | ||
515 | def test_parse_with_force_all(web_request, parser): | |
516 | web_request.json = {"foo": 42} | |
517 | ||
518 | args = {"foo": fields.Int(), "bar": fields.Int(required=False)} | |
519 | ||
520 | parsed = parser.parse(args, web_request, force_all=True) | |
521 | assert parsed["foo"] == 42 | |
522 | assert parsed["bar"] is missing | |
523 | 493 | |
524 | 494 | |
525 | 495 | def test_parse_nested_with_data_key(web_request): |
846 | 816 | |
847 | 817 | |
848 | 818 | def test_use_kwargs_with_arg_missing(web_request, parser): |
849 | user_args = {"username": fields.Str(), "password": fields.Str()} | |
850 | web_request.json = {"username": "foo"} | |
851 | ||
852 | @parser.use_kwargs(user_args, web_request) | |
853 | def viewfunc(username, password): | |
854 | return {"username": username, "password": password} | |
855 | ||
856 | assert viewfunc() == {"username": "foo", "password": missing} | |
857 | ||
858 | ||
859 | # https://github.com/marshmallow-code/webargs/issues/252 | |
860 | def test_use_kwargs_force_all_false(web_request, parser): | |
861 | 819 | user_args = {"username": fields.Str(required=True), "password": fields.Str()} |
862 | 820 | web_request.json = {"username": "foo"} |
863 | 821 | |
864 | @parser.use_kwargs(user_args, web_request, force_all=False) | |
822 | @parser.use_kwargs(user_args, web_request) | |
865 | 823 | def viewfunc(username, **kwargs): |
866 | 824 | assert "password" not in kwargs |
867 | 825 | return {"username": username} |
868 | 826 | |
869 | 827 | assert viewfunc() == {"username": "foo"} |
870 | ||
871 | class MySchema(Schema): | |
872 | username = fields.Str(required=True) | |
873 | password = fields.Str() | |
874 | ||
875 | web_request.json = {"username": "sloria"} | |
876 | ||
877 | # https://github.com/marshmallow-code/webargs/pull/307#issuecomment-441139909 | |
878 | @parser.use_kwargs(MySchema(partial=True), web_request, force_all=False) | |
879 | def viewfunc2(username, **kwargs): | |
880 | assert "password" not in kwargs | |
881 | return {"username": username} | |
882 | ||
883 | assert viewfunc2() == {"username": "sloria"} | |
884 | 828 | |
885 | 829 | |
886 | 830 | def test_delimited_list_default_delimiter(web_request, parser): |
1003 | 947 | assert errors["name"] == ["Something went wrong."] |
1004 | 948 | |
1005 | 949 | |
1006 | class TestValidationError: | |
1007 | def test_status_code_is_deprecated(self): | |
1008 | with pytest.warns(DeprecationWarning): | |
1009 | err = ValidationError("foo", status_code=401) | |
1010 | assert err.status_code == 401 | |
1011 | ||
1012 | def test_headers_is_deprecated(self): | |
1013 | with pytest.warns(DeprecationWarning): | |
1014 | err = ValidationError("foo", headers={"X-Food-Header": "pizza"}) | |
1015 | assert err.headers == {"X-Food-Header": "pizza"} | |
1016 | ||
1017 | def test_str(self): | |
1018 | err = ValidationError("foo", status_code=403) | |
1019 | assert str(err) == "foo" | |
1020 | ||
1021 | def test_repr(self): | |
1022 | err = ValidationError("foo", status_code=403) | |
1023 | assert repr(err) == ( | |
1024 | "ValidationError({0!r}, " "status_code=403, headers=None)".format("foo") | |
1025 | ) | |
1026 | ||
1027 | ||
1028 | 950 | def test_parse_basic(web_request, parser): |
1029 | 951 | web_request.json = {"foo": "42"} |
1030 | 952 | args = {"foo": fields.Int()} |
1060 | 982 | assert schema.fields["id"].required |
1061 | 983 | if MARSHMALLOW_VERSION_INFO[0] < 3: |
1062 | 984 | assert schema.opts.strict is True |
985 | else: | |
986 | assert schema.opts.register is False | |
1063 | 987 | |
1064 | 988 | |
1065 | 989 | # Regression test for https://github.com/marshmallow-code/webargs/issues/101 |
1094 | 1018 | assert "foo" in schema.fields["nest"].schema.fields |
1095 | 1019 | |
1096 | 1020 | |
1097 | def test_argmap2schema_is_deprecated(): | |
1098 | with pytest.warns(DeprecationWarning): | |
1099 | argmap2schema({"arg": fields.Str()}) | |
1100 | ||
1101 | ||
1102 | 1021 | def test_is_json(): |
1103 | 1022 | assert is_json(None) is False |
1104 | 1023 | assert is_json("application/json") is True |
15 | 15 | def test_use_args_hook(self, testapp): |
16 | 16 | assert testapp.get("/echo_use_args_hook?name=Fred").json == {"name": "Fred"} |
17 | 17 | |
18 | def test_raises_lookup_error_if_invalid_code_is_passed_to_validation_error( | |
19 | self, testapp | |
20 | ): | |
21 | with pytest.raises(LookupError) as excinfo: | |
22 | testapp.get("/error_invalid?text=foo") | |
23 | assert excinfo.value.args[0] == "Status code 12345 not supported" | |
18 | # https://github.com/sloria/webargs/issues/329 | |
19 | def test_invalid_json(self, testapp): | |
20 | res = testapp.post( | |
21 | "/echo", | |
22 | '{"foo": "bar", }', | |
23 | headers={"Accept": "application/json", "Content-Type": "application/json"}, | |
24 | expect_errors=True, | |
25 | ) | |
26 | assert res.status_code == 400 | |
27 | assert res.json["errors"] == {"json": ["Invalid JSON body."]} |
0 | 0 | # -*- coding: utf-8 -*- |
1 | 1 | from __future__ import unicode_literals |
2 | import json | |
3 | 2 | import mock |
4 | 3 | |
5 | 4 | from werkzeug.exceptions import HTTPException |
8 | 7 | from flask import Flask |
9 | 8 | from webargs import fields, ValidationError, missing |
10 | 9 | from webargs.flaskparser import parser, abort |
11 | from webargs.core import MARSHMALLOW_VERSION_INFO | |
10 | from webargs.core import MARSHMALLOW_VERSION_INFO, json | |
12 | 11 | |
13 | 12 | from .apps.flask_app import app |
14 | 13 | from webargs.testing import CommonTestCase |
25 | 24 | def test_parsing_invalid_view_arg(self, testapp): |
26 | 25 | res = testapp.get("/echo_view_arg/foo", expect_errors=True) |
27 | 26 | assert res.status_code == 422 |
28 | assert res.json == {"errors": {"view_arg": ["Not a valid integer."]}} | |
27 | assert res.json == {"view_arg": ["Not a valid integer."]} | |
29 | 28 | |
30 | 29 | def test_use_args_with_view_args_parsing(self, testapp): |
31 | 30 | res = testapp.get("/echo_view_arg_use_args/42") |
34 | 34 | assert testapp.get("/echo_method?name=Steve").json == {"name": "Steve"} |
35 | 35 | assert testapp.get("/echo_method_view").json == {"name": "World"} |
36 | 36 | assert testapp.get("/echo_method_view?name=Steve").json == {"name": "Steve"} |
37 | ||
38 | def test_invalid_status_code_passed_to_validation_error(self, testapp): | |
39 | res = testapp.get("/error_invalid?text=foo", expect_errors=True) | |
40 | assert res.status_code == 500 | |
41 | 37 | |
42 | 38 | # regression test for https://github.com/marshmallow-code/webargs/issues/165 |
43 | 39 | def test_multiple_args(self, testapp): |
0 | 0 | # -*- coding: utf-8 -*- |
1 | 1 | |
2 | import json | |
2 | from webargs.core import json | |
3 | 3 | |
4 | 4 | try: |
5 | from urllib import urlencode # python2 | |
6 | except ImportError: | |
7 | from urllib.parse import urlencode # python3 | |
5 | from urllib.parse import urlencode | |
6 | except ImportError: # PY2 | |
7 | from urllib import urlencode # type: ignore | |
8 | 8 | |
9 | 9 | import mock |
10 | 10 | import pytest |
11 | ||
12 | import marshmallow as ma | |
11 | 13 | |
12 | 14 | import tornado.web |
13 | 15 | import tornado.httputil |
17 | 19 | import tornado.ioloop |
18 | 20 | from tornado.testing import AsyncHTTPTestCase |
19 | 21 | |
20 | from webargs import fields, missing, ValidationError | |
22 | from webargs import fields, missing | |
21 | 23 | from webargs.tornadoparser import parser, use_args, use_kwargs, get_value |
22 | 24 | from webargs.core import parse_json |
23 | 25 | |
327 | 329 | |
328 | 330 | assert parsed["integer"] == [1, 2] |
329 | 331 | assert parsed["string"] == value |
332 | ||
333 | def test_it_should_raise_when_json_is_invalid(self): | |
334 | attrs = {"foo": fields.Str()} | |
335 | ||
336 | request = make_request( | |
337 | body='{"foo": 42,}', headers={"Content-Type": "application/json"} | |
338 | ) | |
339 | with pytest.raises(tornado.web.HTTPError) as excinfo: | |
340 | parser.parse(attrs, request) | |
341 | error = excinfo.value | |
342 | assert error.status_code == 400 | |
343 | assert error.messages == {"json": ["Invalid JSON body."]} | |
330 | 344 | |
331 | 345 | def test_it_should_parse_header_arguments(self): |
332 | 346 | attrs = {"string": fields.Str(), "integer": fields.List(fields.Int())} |
579 | 593 | |
580 | 594 | |
581 | 595 | def always_fail(val): |
582 | raise ValidationError("something went wrong") | |
596 | raise ma.ValidationError("something went wrong") | |
583 | 597 | |
584 | 598 | |
585 | 599 | class AlwaysFailHandler(tornado.web.RequestHandler): |
590 | 604 | self.write(args) |
591 | 605 | |
592 | 606 | |
593 | def always_fail_with_400(val): | |
594 | raise ValidationError("something went wrong", status_code=400) | |
595 | ||
596 | ||
597 | class AlwaysFailWith400Handler(tornado.web.RequestHandler): | |
598 | ARGS = {"name": fields.Str(validate=always_fail_with_400)} | |
599 | ||
600 | @use_args(ARGS) | |
601 | def post(self, args): | |
602 | self.write(args) | |
603 | ||
604 | ||
605 | 607 | validate_app = tornado.web.Application( |
606 | [ | |
607 | (r"/echo", ValidateHandler), | |
608 | (r"/alwaysfail", AlwaysFailHandler), | |
609 | (r"/alwaysfailwith400", AlwaysFailWith400Handler), | |
610 | ] | |
608 | [(r"/echo", ValidateHandler), (r"/alwaysfail", AlwaysFailHandler)] | |
611 | 609 | ) |
612 | 610 | |
613 | 611 | |
643 | 641 | ) |
644 | 642 | assert res.code == 422 |
645 | 643 | |
646 | def test_user_validator_with_status_code(self): | |
647 | res = self.fetch( | |
648 | "/alwaysfailwith400", | |
649 | method="POST", | |
650 | headers={"Content-Type": "application/json"}, | |
651 | body=json.dumps({"name": "Steve"}), | |
652 | ) | |
653 | assert res.code == 400 | |
654 | ||
655 | 644 | def test_use_kwargs_with_error(self): |
656 | 645 | res = self.fetch("/echo", method="GET") |
657 | 646 | assert res.code == 422 |
2 | 2 | try: |
3 | 3 | from urllib.parse import urlencode |
4 | 4 | except ImportError: # PY2 |
5 | from urllib import urlencode | |
6 | import json | |
5 | from urllib import urlencode # type: ignore | |
6 | from webargs.core import json | |
7 | 7 | |
8 | 8 | import pytest |
9 | from marshmallow import fields | |
10 | from webargs import ValidationError | |
9 | from marshmallow import fields, ValidationError | |
11 | 10 | |
12 | 11 | import webtest |
13 | 12 | import webapp2 |
59 | 58 | "/echo", POST=json.dumps(expected), headers={"content-type": "application/json"} |
60 | 59 | ) |
61 | 60 | assert parser.parse(hello_args, req=request) == expected |
61 | ||
62 | ||
63 | def test_parse_invalid_json(): | |
64 | request = webapp2.Request.blank( | |
65 | "/echo", POST='{"foo": "bar", }', headers={"content-type": "application/json"} | |
66 | ) | |
67 | with pytest.raises(json.JSONDecodeError): | |
68 | parser.parse(hello_args, req=request) | |
62 | 69 | |
63 | 70 | |
64 | 71 | def test_parse_json_with_vendor_media_type(): |
7 | 7 | extras = tests |
8 | 8 | deps = |
9 | 9 | marshmallow2: marshmallow==2.15.2 |
10 | marshmallow3: marshmallow>=3.0.0a1,<4.0.0 | |
10 | marshmallow3: marshmallow>=3.0.0rc2,<4.0.0 | |
11 | 11 | commands = |
12 | 12 | py27: pytest --ignore=tests/test_py3/ {posargs} |
13 | 13 | py{35,36,37}: pytest {posargs} |
14 | 14 | |
15 | 15 | [testenv:lint] |
16 | extras = lint | |
17 | commands = pre-commit run --all-files --show-diff-on-failure | |
16 | deps = pre-commit~=1.14 | |
17 | skip_install = true | |
18 | commands = pre-commit run --all-files | |
18 | 19 | |
19 | 20 | [testenv:docs] |
20 | 21 | deps = -rdocs/requirements.txt |
3 | 3 | # Make marshmallow's validation functions importable from webargs |
4 | 4 | from marshmallow import validate |
5 | 5 | |
6 | from webargs.core import argmap2schema, dict2schema, WebargsError, ValidationError | |
6 | from webargs.core import dict2schema, ValidationError | |
7 | 7 | from webargs import fields |
8 | 8 | |
9 | __version__ = "4.4.1" | |
9 | __version__ = "5.1.2" | |
10 | 10 | __author__ = "Steven Loria" |
11 | 11 | __license__ = "MIT" |
12 | 12 | |
13 | 13 | |
14 | __all__ = ( | |
15 | "dict2schema", | |
16 | "argmap2schema", | |
17 | "WebargsError", | |
18 | "ValidationError", | |
19 | "fields", | |
20 | "missing", | |
21 | "validate", | |
22 | ) | |
14 | __all__ = ("dict2schema", "ValidationError", "fields", "missing", "validate") |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """aiohttp request argument parsing module. |
2 | 1 | |
3 | 2 | Example: :: |
22 | 21 | app = web.Application() |
23 | 22 | app.router.add_route('GET', '/', index) |
24 | 23 | """ |
25 | import json | |
26 | import warnings | |
24 | import typing | |
27 | 25 | |
28 | import aiohttp | |
29 | 26 | from aiohttp import web |
27 | from aiohttp.web import Request | |
30 | 28 | from aiohttp import web_exceptions |
29 | from marshmallow import Schema, ValidationError | |
30 | from marshmallow.fields import Field | |
31 | 31 | |
32 | 32 | from webargs import core |
33 | from webargs.core import json | |
33 | 34 | from webargs.asyncparser import AsyncParser |
34 | 35 | |
35 | AIOHTTP_MAJOR_VERSION = int(aiohttp.__version__.split(".")[0]) | |
36 | if AIOHTTP_MAJOR_VERSION < 2: | |
37 | warnings.warn( | |
38 | "Support for aiohttp<2.0.0 is deprecated and is removed in webargs 2.0.0", | |
39 | DeprecationWarning, | |
40 | ) | |
41 | 36 | |
42 | ||
43 | def is_json_request(req): | |
37 | def is_json_request(req: Request) -> bool: | |
44 | 38 | content_type = req.content_type |
45 | 39 | return core.is_json(content_type) |
46 | 40 | |
52 | 46 | # Mapping of status codes to exception classes |
53 | 47 | # Adapted from werkzeug |
54 | 48 | exception_map = {422: HTTPUnprocessableEntity} |
55 | # Collect all exceptions from aiohttp.web_exceptions | |
56 | def _find_exceptions(): | |
49 | ||
50 | ||
51 | def _find_exceptions() -> None: | |
57 | 52 | for name in web_exceptions.__all__: |
58 | 53 | obj = getattr(web_exceptions, name) |
59 | 54 | try: |
68 | 63 | exception_map[obj.status_code] = obj |
69 | 64 | |
70 | 65 | |
66 | # Collect all exceptions from aiohttp.web_exceptions | |
71 | 67 | _find_exceptions() |
72 | 68 | del _find_exceptions |
73 | 69 | |
79 | 75 | match_info="parse_match_info", **core.Parser.__location_map__ |
80 | 76 | ) |
81 | 77 | |
82 | def parse_querystring(self, req, name, field): | |
78 | def parse_querystring(self, req: Request, name: str, field: Field) -> typing.Any: | |
83 | 79 | """Pull a querystring value from the request.""" |
84 | 80 | return core.get_value(req.query, name, field) |
85 | 81 | |
86 | async def parse_form(self, req, name, field): | |
82 | async def parse_form(self, req: Request, name: str, field: Field) -> typing.Any: | |
87 | 83 | """Pull a form value from the request.""" |
88 | 84 | post_data = self._cache.get("post") |
89 | 85 | if post_data is None: |
90 | 86 | self._cache["post"] = await req.post() |
91 | 87 | return core.get_value(self._cache["post"], name, field) |
92 | 88 | |
93 | async def parse_json(self, req, name, field): | |
89 | async def parse_json(self, req: Request, name: str, field: Field) -> typing.Any: | |
94 | 90 | """Pull a json value from the request.""" |
95 | 91 | json_data = self._cache.get("json") |
96 | 92 | if json_data is None: |
97 | 93 | if not (req.body_exists and is_json_request(req)): |
98 | 94 | return core.missing |
99 | 95 | try: |
100 | json_data = await req.json() | |
96 | json_data = await req.json(loads=json.loads) | |
101 | 97 | except json.JSONDecodeError as e: |
102 | 98 | if e.doc == "": |
103 | 99 | return core.missing |
104 | 100 | else: |
105 | raise e | |
101 | return self.handle_invalid_json_error(e, req) | |
106 | 102 | self._cache["json"] = json_data |
107 | 103 | return core.get_value(json_data, name, field, allow_many_nested=True) |
108 | 104 | |
109 | def parse_headers(self, req, name, field): | |
105 | def parse_headers(self, req: Request, name: str, field: Field) -> typing.Any: | |
110 | 106 | """Pull a value from the header data.""" |
111 | 107 | return core.get_value(req.headers, name, field) |
112 | 108 | |
113 | def parse_cookies(self, req, name, field): | |
109 | def parse_cookies(self, req: Request, name: str, field: Field) -> typing.Any: | |
114 | 110 | """Pull a value from the cookiejar.""" |
115 | 111 | return core.get_value(req.cookies, name, field) |
116 | 112 | |
117 | def parse_files(self, req, name, field): | |
113 | def parse_files(self, req: Request, name: str, field: Field) -> None: | |
118 | 114 | raise NotImplementedError( |
119 | 115 | "parse_files is not implemented. You may be able to use parse_form for " |
120 | 116 | "parsing upload data." |
121 | 117 | ) |
122 | 118 | |
123 | def parse_match_info(self, req, name, field): | |
119 | def parse_match_info(self, req: Request, name: str, field: Field) -> typing.Any: | |
124 | 120 | """Pull a value from the request's ``match_info``.""" |
125 | 121 | return core.get_value(req.match_info, name, field) |
126 | 122 | |
127 | def get_request_from_view_args(self, view, args, kwargs): | |
123 | def get_request_from_view_args( | |
124 | self, view: typing.Callable, args: typing.Iterable, kwargs: typing.Mapping | |
125 | ) -> Request: | |
128 | 126 | """Get request object from a handler function or method. Used internally by |
129 | 127 | ``use_args`` and ``use_kwargs``. |
130 | 128 | """ |
139 | 137 | assert isinstance(req, web.Request), "Request argument not found for handler" |
140 | 138 | return req |
141 | 139 | |
142 | def handle_error(self, error, req, schema, error_status_code, error_headers): | |
143 | """Handle ValidationErrors and return a JSON response of error messages to the client.""" | |
144 | error_class = exception_map.get(error_status_code or error.status_code) | |
140 | def handle_error( | |
141 | self, | |
142 | error: ValidationError, | |
143 | req: Request, | |
144 | schema: Schema, | |
145 | error_status_code: typing.Union[int, None] = None, | |
146 | error_headers: typing.Union[typing.Mapping[str, str], None] = None, | |
147 | ) -> "typing.NoReturn": | |
148 | """Handle ValidationErrors and return a JSON response of error messages | |
149 | to the client. | |
150 | """ | |
151 | error_class = exception_map.get( | |
152 | error_status_code or self.DEFAULT_VALIDATION_STATUS | |
153 | ) | |
145 | 154 | if not error_class: |
146 | raise LookupError("No exception for {0}".format(error.status_code)) | |
147 | headers = error_headers or error.headers | |
155 | raise LookupError("No exception for {0}".format(error_status_code)) | |
156 | headers = error_headers | |
148 | 157 | raise error_class( |
149 | 158 | body=json.dumps(error.messages).encode("utf-8"), |
150 | 159 | headers=headers, |
151 | 160 | content_type="application/json", |
152 | 161 | ) |
153 | 162 | |
163 | def handle_invalid_json_error( | |
164 | self, error: json.JSONDecodeError, req: Request, *args, **kwargs | |
165 | ) -> "typing.NoReturn": | |
166 | error_class = exception_map[400] | |
167 | messages = {"json": ["Invalid JSON body."]} | |
168 | raise error_class( | |
169 | body=json.dumps(messages).encode("utf-8"), content_type="application/json" | |
170 | ) | |
171 | ||
154 | 172 | |
155 | 173 | parser = AIOHTTPParser() |
156 | use_args = parser.use_args | |
157 | use_kwargs = parser.use_kwargs | |
174 | use_args = parser.use_args # type: typing.Callable | |
175 | use_kwargs = parser.use_kwargs # type: typing.Callable |
0 | # -*- coding: utf-8 -*- | |
1 | 0 | """Asynchronous request parser. Compatible with Python>=3.5.""" |
2 | 1 | import asyncio |
3 | import collections | |
4 | 2 | import functools |
5 | 3 | import inspect |
6 | import warnings | |
7 | ||
4 | import typing | |
5 | from collections.abc import Mapping | |
6 | ||
7 | from marshmallow import Schema, ValidationError | |
8 | from marshmallow.fields import Field | |
8 | 9 | import marshmallow as ma |
9 | 10 | from marshmallow.utils import missing |
10 | 11 | |
11 | 12 | from webargs import core |
12 | from webargs.core import RemovedInWebargs5Warning | |
13 | ||
14 | Request = typing.TypeVar("Request") | |
15 | ArgMap = typing.Union[Schema, typing.Mapping[str, Field]] | |
16 | Validate = typing.Union[typing.Callable, typing.Iterable[typing.Callable]] | |
13 | 17 | |
14 | 18 | |
15 | 19 | class AsyncParser(core.Parser): |
17 | 21 | either coroutines or regular methods. |
18 | 22 | """ |
19 | 23 | |
20 | async def _parse_request(self, schema, req, locations): | |
24 | async def _parse_request( | |
25 | self, schema: Schema, req: Request, locations: typing.Iterable | |
26 | ) -> typing.Union[dict, list]: | |
21 | 27 | if schema.many: |
22 | 28 | assert ( |
23 | 29 | "json" in locations |
24 | 30 | ), "schema.many=True is only supported for JSON location" |
25 | # The ad hoc Nested field is more like a workaround or a helper, and it servers its | |
26 | # purpose fine. However, if somebody has a desire to re-design the support of | |
27 | # bulk-type arguments, go ahead. | |
31 | # The ad hoc Nested field is more like a workaround or a helper, | |
32 | # and it servers its purpose fine. However, if somebody has a desire | |
33 | # to re-design the support of bulk-type arguments, go ahead. | |
28 | 34 | parsed = await self.parse_arg( |
29 | 35 | name="json", |
30 | 36 | field=ma.fields.Nested(schema, many=True), |
59 | 65 | # TODO: Lots of duplication from core.Parser here. Rethink. |
60 | 66 | async def parse( |
61 | 67 | self, |
62 | argmap, | |
63 | req=None, | |
64 | locations=None, | |
65 | validate=None, | |
66 | force_all=False, | |
67 | error_status_code=None, | |
68 | error_headers=None, | |
69 | ): | |
68 | argmap: ArgMap, | |
69 | req: Request = None, | |
70 | locations: typing.Iterable = None, | |
71 | validate: Validate = None, | |
72 | error_status_code: typing.Union[int, None] = None, | |
73 | error_headers: typing.Union[typing.Mapping[str, str], None] = None, | |
74 | ) -> typing.Union[typing.Mapping, None]: | |
70 | 75 | """Coroutine variant of `webargs.core.Parser`. |
71 | 76 | |
72 | 77 | Receives the same arguments as `webargs.core.Parser.parse`. |
78 | 83 | schema = self._get_schema(argmap, req) |
79 | 84 | try: |
80 | 85 | parsed = await self._parse_request( |
81 | schema=schema, req=req, locations=locations | |
86 | schema=schema, req=req, locations=locations or self.locations | |
82 | 87 | ) |
83 | 88 | result = schema.load(parsed) |
84 | 89 | data = result.data if core.MARSHMALLOW_VERSION_INFO[0] < 3 else result |
85 | 90 | self._validate_arguments(data, validators) |
86 | 91 | except ma.exceptions.ValidationError as error: |
87 | self._on_validation_error( | |
92 | await self._on_validation_error( | |
88 | 93 | error, req, schema, error_status_code, error_headers |
89 | 94 | ) |
90 | 95 | finally: |
91 | 96 | self.clear_cache() |
92 | if force_all: | |
93 | warnings.warn( | |
94 | "Missing arguments will no longer be added to the parsed arguments " | |
95 | "dictionary in version 5.0.0. Pass force_all=False for the new behavior.", | |
96 | RemovedInWebargs5Warning, | |
97 | ) | |
98 | core.fill_in_missing_args(data, schema) | |
99 | 97 | return data |
98 | ||
99 | async def _on_validation_error( | |
100 | self, | |
101 | error: ValidationError, | |
102 | req: Request, | |
103 | schema: Schema, | |
104 | error_status_code: typing.Union[int, None], | |
105 | error_headers: typing.Union[typing.Mapping[str, str], None] = None, | |
106 | ) -> None: | |
107 | error_handler = self.error_callback or self.handle_error | |
108 | await error_handler(error, req, schema, error_status_code, error_headers) | |
100 | 109 | |
101 | 110 | def use_args( |
102 | 111 | self, |
103 | argmap, | |
104 | req=None, | |
105 | locations=None, | |
106 | as_kwargs=False, | |
107 | validate=None, | |
108 | force_all=None, | |
109 | error_status_code=None, | |
110 | error_headers=None, | |
111 | ): | |
112 | argmap: ArgMap, | |
113 | req: typing.Optional[Request] = None, | |
114 | locations: typing.Iterable = None, | |
115 | as_kwargs: bool = False, | |
116 | validate: Validate = None, | |
117 | error_status_code: typing.Optional[int] = None, | |
118 | error_headers: typing.Union[typing.Mapping[str, str], None] = None, | |
119 | ) -> typing.Callable[..., typing.Callable]: | |
112 | 120 | """Decorator that injects parsed arguments into a view function or method. |
113 | 121 | |
114 | 122 | Receives the same arguments as `webargs.core.Parser.use_args`. |
115 | 123 | """ |
116 | 124 | locations = locations or self.locations |
117 | 125 | request_obj = req |
118 | force_all_ = force_all if force_all is not None else as_kwargs | |
119 | 126 | # Optimization: If argmap is passed as a dictionary, we only need |
120 | 127 | # to generate a Schema once |
121 | if isinstance(argmap, collections.Mapping): | |
128 | if isinstance(argmap, Mapping): | |
122 | 129 | argmap = core.dict2schema(argmap)() |
123 | 130 | |
124 | def decorator(func): | |
131 | def decorator(func: typing.Callable) -> typing.Callable: | |
125 | 132 | req_ = request_obj |
126 | 133 | |
127 | 134 | if inspect.iscoroutinefunction(func): |
138 | 145 | req=req_obj, |
139 | 146 | locations=locations, |
140 | 147 | validate=validate, |
141 | force_all=force_all_, | |
142 | 148 | error_status_code=error_status_code, |
143 | 149 | error_headers=error_headers, |
144 | 150 | ) |
145 | 151 | if as_kwargs: |
146 | kwargs.update(parsed_args) | |
152 | kwargs.update(parsed_args or {}) | |
147 | 153 | return await func(*args, **kwargs) |
148 | 154 | else: |
149 | 155 | # Add parsed_args after other positional arguments |
152 | 158 | |
153 | 159 | else: |
154 | 160 | |
155 | @functools.wraps(func) | |
161 | @functools.wraps(func) # type: ignore | |
156 | 162 | def wrapper(*args, **kwargs): |
157 | 163 | req_obj = req_ |
158 | 164 | |
159 | 165 | if not req_obj: |
160 | 166 | req_obj = self.get_request_from_view_args(func, args, kwargs) |
161 | 167 | # NOTE: At this point, argmap may be a Schema, callable, or dict |
162 | parsed_args = yield from self.parse( # noqa: B901 | |
168 | parsed_args = yield from self.parse( # type: ignore | |
163 | 169 | argmap, |
164 | 170 | req=req_obj, |
165 | 171 | locations=locations, |
166 | 172 | validate=validate, |
167 | force_all=force_all_, | |
168 | 173 | error_status_code=error_status_code, |
169 | 174 | error_headers=error_headers, |
170 | 175 | ) |
176 | 181 | new_args = args + (parsed_args,) |
177 | 182 | return func(*new_args, **kwargs) |
178 | 183 | |
179 | wrapper.__wrapped__ = func | |
180 | 184 | return wrapper |
181 | 185 | |
182 | 186 | return decorator |
183 | 187 | |
184 | def use_kwargs(self, *args, **kwargs): | |
188 | def use_kwargs(self, *args, **kwargs) -> typing.Callable: | |
185 | 189 | """Decorator that injects parsed arguments into a view function or method. |
186 | 190 | |
187 | 191 | Receives the same arguments as `webargs.core.Parser.use_kwargs`. |
189 | 193 | """ |
190 | 194 | return super().use_kwargs(*args, **kwargs) |
191 | 195 | |
192 | async def parse_arg(self, name, field, req, locations=None): | |
196 | async def parse_arg( | |
197 | self, name: str, field: Field, req: Request, locations: typing.Iterable = None | |
198 | ) -> typing.Any: | |
193 | 199 | location = field.metadata.get("location") |
194 | 200 | if location: |
195 | 201 | locations_to_check = self._validated_locations([location]) |
203 | 209 | return value |
204 | 210 | return core.missing |
205 | 211 | |
206 | async def _get_value(self, name, argobj, req, location): | |
207 | # Parsing function to call | |
208 | # May be a method name (str) or a function | |
209 | func = self.__location_map__.get(location) | |
210 | if func: | |
211 | if inspect.isfunction(func): | |
212 | function = func | |
213 | else: | |
214 | function = getattr(self, func) | |
215 | if asyncio.iscoroutinefunction(function): | |
216 | value = await function(req, name, argobj) | |
217 | else: | |
218 | value = function(req, name, argobj) | |
212 | async def _get_value( | |
213 | self, name: str, argobj: Field, req: Request, location: str | |
214 | ) -> typing.Any: | |
215 | function = self._get_handler(location) | |
216 | if asyncio.iscoroutinefunction(function): | |
217 | value = await function(req, name, argobj) | |
219 | 218 | else: |
220 | raise ValueError('Invalid location: "{0}"'.format(location)) | |
219 | value = function(req, name, argobj) | |
221 | 220 | return value |
19 | 19 | import bottle |
20 | 20 | |
21 | 21 | from webargs import core |
22 | from webargs.core import json | |
22 | 23 | |
23 | 24 | |
24 | 25 | class BottleParser(core.Parser): |
38 | 39 | if json_data is None: |
39 | 40 | try: |
40 | 41 | self._cache["json"] = json_data = req.json |
41 | except (AttributeError, ValueError): | |
42 | except AttributeError: | |
42 | 43 | return core.missing |
44 | except json.JSONDecodeError as e: | |
45 | if e.doc == "": | |
46 | return core.missing | |
47 | else: | |
48 | return self.handle_invalid_json_error(e, req) | |
43 | 49 | if json_data is None: |
44 | 50 | return core.missing |
45 | 51 | return core.get_value(json_data, name, field, allow_many_nested=True) |
60 | 66 | """Handles errors during parsing. Aborts the current request with a |
61 | 67 | 400 error. |
62 | 68 | """ |
63 | status_code = error_status_code or getattr( | |
64 | error, "status_code", self.DEFAULT_VALIDATION_STATUS | |
69 | status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS | |
70 | raise bottle.HTTPError( | |
71 | status=status_code, | |
72 | body=error.messages, | |
73 | headers=error_headers, | |
74 | exception=error, | |
65 | 75 | ) |
66 | headers = error_headers or getattr(error, "headers", {}) | |
76 | ||
77 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
67 | 78 | raise bottle.HTTPError( |
68 | status=status_code, body=error.messages, headers=headers, exception=error | |
79 | status=400, body={"json": ["Invalid JSON body."]}, exception=error | |
69 | 80 | ) |
70 | 81 | |
71 | 82 | def get_default_request(self): |
0 | 0 | # -*- coding: utf-8 -*- |
1 | 1 | from __future__ import unicode_literals |
2 | 2 | |
3 | import collections | |
4 | 3 | import functools |
5 | 4 | import inspect |
6 | 5 | import logging |
8 | 7 | from distutils.version import LooseVersion |
9 | 8 | |
10 | 9 | try: |
10 | from collections.abc import Mapping | |
11 | except ImportError: | |
12 | from collections import Mapping | |
13 | ||
14 | try: | |
11 | 15 | import simplejson as json |
12 | 16 | except ImportError: |
13 | import json | |
17 | import json # type: ignore | |
14 | 18 | |
15 | 19 | import marshmallow as ma |
20 | from marshmallow import ValidationError | |
16 | 21 | from marshmallow.compat import iteritems |
17 | 22 | from marshmallow.utils import missing, is_collection |
18 | 23 | |
20 | 25 | |
21 | 26 | |
22 | 27 | __all__ = [ |
23 | "WebargsError", | |
24 | 28 | "ValidationError", |
25 | 29 | "dict2schema", |
26 | 30 | "is_multiple", |
30 | 34 | "parse_json", |
31 | 35 | ] |
32 | 36 | |
33 | ||
34 | # Copied from marshmallow.utils | |
35 | def _signature(func): | |
36 | if hasattr(inspect, "signature"): | |
37 | return list(inspect.signature(func).parameters.keys()) | |
38 | if hasattr(func, "__self__"): | |
39 | # Remove bound arg to match inspect.signature() | |
40 | return inspect.getargspec(func).args[1:] | |
41 | # All args are unbound | |
42 | return inspect.getargspec(func).args | |
43 | ||
44 | ||
45 | def get_func_args(func): | |
46 | """Given a callable, return a tuple of argument names. Handles | |
47 | `functools.partial` objects and class-based callables. | |
48 | """ | |
49 | if isinstance(func, functools.partial): | |
50 | return _signature(func.func) | |
51 | if inspect.isfunction(func) or inspect.ismethod(func): | |
52 | return _signature(func) | |
53 | # Callable class | |
54 | return _signature(func.__call__) | |
55 | ||
56 | ||
57 | MARSHMALLOW_VERSION_INFO = tuple(LooseVersion(ma.__version__).version) | |
58 | ||
59 | DEFAULT_VALIDATION_STATUS = 422 | |
60 | ||
61 | ||
62 | class RemovedInWebargs5Warning(DeprecationWarning): | |
63 | pass | |
64 | ||
65 | ||
66 | class WebargsError(Exception): | |
67 | """Base class for all webargs-related errors.""" | |
68 | ||
69 | pass | |
70 | ||
71 | ||
72 | class ValidationError(WebargsError, ma.exceptions.ValidationError): | |
73 | """Raised when validation fails on user input. | |
74 | ||
75 | .. versionchanged:: 4.2.0 | |
76 | status_code and headers arguments are deprecated. Pass | |
77 | error_status_code and error_headers to `Parser.parse`, | |
78 | `Parser.use_args`, and `Parser.use_kwargs` instead. | |
79 | """ | |
80 | ||
81 | def __init__(self, message, status_code=None, headers=None, **kwargs): | |
82 | if status_code is not None: | |
83 | warnings.warn( | |
84 | "The status_code argument to ValidationError is deprecated " | |
85 | "and will be removed in 5.0.0. " | |
86 | "Pass error_status_code to Parser.parse, Parser.use_args, " | |
87 | "or Parser.use_kwargs instead.", | |
88 | RemovedInWebargs5Warning, | |
89 | ) | |
90 | self.status_code = status_code or DEFAULT_VALIDATION_STATUS | |
91 | if headers is not None: | |
92 | warnings.warn( | |
93 | "The headers argument to ValidationError is deprecated " | |
94 | "and will be removed in 5.0.0. " | |
95 | "Pass error_headers to Parser.parse, Parser.use_args, " | |
96 | "or Parser.use_kwargs instead.", | |
97 | RemovedInWebargs5Warning, | |
98 | ) | |
99 | self.headers = headers | |
100 | ma.exceptions.ValidationError.__init__( | |
101 | self, message, status_code=status_code, headers=headers, **kwargs | |
102 | ) | |
103 | ||
104 | def __repr__(self): | |
105 | return "ValidationError({0!r}, status_code={1}, headers={2})".format( | |
106 | self.args[0], self.status_code, self.headers | |
107 | ) | |
37 | MARSHMALLOW_VERSION_INFO = tuple(LooseVersion(ma.__version__).version) # type: tuple | |
38 | ||
39 | DEFAULT_VALIDATION_STATUS = 422 # type: int | |
108 | 40 | |
109 | 41 | |
110 | 42 | def _callable_or_raise(obj): |
117 | 49 | return obj |
118 | 50 | |
119 | 51 | |
120 | def get_field_names_for_argmap(argmap): | |
121 | if isinstance(argmap, ma.Schema): | |
122 | all_field_names = set( | |
123 | [fname for fname, fobj in iteritems(argmap.fields) if not fobj.dump_only] | |
124 | ) | |
125 | else: | |
126 | all_field_names = set(argmap.keys()) | |
127 | return all_field_names | |
128 | ||
129 | ||
130 | def fill_in_missing_args(ret, argmap): | |
131 | # WARNING: We modify ret in-place | |
132 | all_field_names = get_field_names_for_argmap(argmap) | |
133 | missing_args = all_field_names - set(ret.keys()) | |
134 | for key in missing_args: | |
135 | ret[key] = missing | |
136 | return ret | |
137 | ||
138 | ||
139 | 52 | def dict2schema(dct): |
140 | 53 | """Generate a `marshmallow.Schema` class given a dictionary of |
141 | 54 | `Fields <marshmallow.fields.Field>`. |
142 | 55 | """ |
143 | 56 | attrs = dct.copy() |
144 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
145 | ||
146 | class Meta(object): | |
57 | ||
58 | class Meta(object): | |
59 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
147 | 60 | strict = True |
148 | ||
149 | attrs["Meta"] = Meta | |
61 | else: | |
62 | register = False | |
63 | ||
64 | attrs["Meta"] = Meta | |
150 | 65 | return type(str(""), (ma.Schema,), attrs) |
151 | ||
152 | ||
153 | def argmap2schema(argmap): | |
154 | warnings.warn( | |
155 | "argmap2schema is deprecated. Use dict2schema instead.", | |
156 | RemovedInWebargs5Warning, | |
157 | ) | |
158 | return dict2schema(argmap) | |
159 | 66 | |
160 | 67 | |
161 | 68 | def is_multiple(field): |
167 | 74 | return content_type.split(";")[0].strip() if content_type else None |
168 | 75 | |
169 | 76 | |
170 | # Adapted from werkzeug: https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/wrappers.py | |
77 | # Adapted from werkzeug: | |
78 | # https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/wrappers.py | |
171 | 79 | def is_json(mimetype): |
172 | 80 | """Indicates if this mimetype is JSON or not. By default a request |
173 | 81 | is considered to include JSON data if the mimetype is |
220 | 128 | return val |
221 | 129 | |
222 | 130 | |
223 | def parse_json(s): | |
131 | def parse_json(s, encoding="utf-8"): | |
224 | 132 | if isinstance(s, bytes): |
225 | s = s.decode("utf-8") | |
133 | s = s.decode(encoding) | |
226 | 134 | return json.loads(s) |
227 | 135 | |
228 | 136 | |
290 | 198 | raise ValueError(msg) |
291 | 199 | return locations |
292 | 200 | |
293 | def _get_value(self, name, argobj, req, location): | |
201 | def _get_handler(self, location): | |
294 | 202 | # Parsing function to call |
295 | 203 | # May be a method name (str) or a function |
296 | 204 | func = self.__location_map__.get(location) |
299 | 207 | function = func |
300 | 208 | else: |
301 | 209 | function = getattr(self, func) |
302 | value = function(req, name, argobj) | |
303 | 210 | else: |
304 | 211 | raise ValueError('Invalid location: "{0}"'.format(location)) |
305 | return value | |
212 | return function | |
213 | ||
214 | def _get_value(self, name, argobj, req, location): | |
215 | function = self._get_handler(location) | |
216 | return function(req, name, argobj) | |
306 | 217 | |
307 | 218 | def parse_arg(self, name, field, req, locations=None): |
308 | 219 | """Parse a single argument from a request. |
316 | 227 | :param req: The request object to parse. |
317 | 228 | :param tuple locations: The locations ('json', 'querystring', etc.) where |
318 | 229 | to search for the value. |
319 | :return: The unvalidated argument value or `missing` if the value cannot be found | |
320 | on the request. | |
230 | :return: The unvalidated argument value or `missing` if the value cannot | |
231 | be found on the request. | |
321 | 232 | """ |
322 | 233 | location = field.metadata.get("location") |
323 | 234 | if location: |
338 | 249 | assert ( |
339 | 250 | "json" in locations |
340 | 251 | ), "schema.many=True is only supported for JSON location" |
341 | # The ad hoc Nested field is more like a workaround or a helper, and it servers its | |
342 | # purpose fine. However, if somebody has a desire to re-design the support of | |
343 | # bulk-type arguments, go ahead. | |
252 | # The ad hoc Nested field is more like a workaround or a helper, | |
253 | # and it servers its purpose fine. However, if somebody has a desire | |
254 | # to re-design the support of bulk-type arguments, go ahead. | |
344 | 255 | parsed = self.parse_arg( |
345 | 256 | name="json", |
346 | 257 | field=ma.fields.Nested(schema, many=True), |
371 | 282 | def _on_validation_error( |
372 | 283 | self, error, req, schema, error_status_code, error_headers |
373 | 284 | ): |
374 | if isinstance(error, ma.exceptions.ValidationError) and not isinstance( | |
375 | error, ValidationError | |
376 | ): | |
377 | # Raise a webargs error instead | |
378 | kwargs = getattr(error, "kwargs", {}) | |
379 | kwargs["data"] = error.data | |
380 | if MARSHMALLOW_VERSION_INFO[0] < 3: | |
381 | kwargs["fields"] = error.fields | |
382 | kwargs["field_names"] = error.field_names | |
383 | else: | |
384 | kwargs["field_name"] = error.field_name | |
385 | if "status_code" not in kwargs: | |
386 | kwargs["status_code"] = self.DEFAULT_VALIDATION_STATUS | |
387 | error = ValidationError(error.messages, **kwargs) | |
388 | if self.error_callback: | |
389 | if len(get_func_args(self.error_callback)) > 3: | |
390 | self.error_callback( | |
391 | error, req, schema, error_status_code, error_headers | |
392 | ) | |
393 | else: # Backwards compat with webargs<=4.2.0 | |
394 | warnings.warn( | |
395 | "Error handler functions should include error_status_code and " | |
396 | "error_headers args, or include **kwargs in the signature", | |
397 | DeprecationWarning, | |
398 | ) | |
399 | self.error_callback(error, req, schema) | |
400 | else: | |
401 | if len(get_func_args(self.handle_error)) > 3: | |
402 | self.handle_error(error, req, schema, error_status_code, error_headers) | |
403 | else: | |
404 | warnings.warn( | |
405 | "handle_error methods should include error_status_code and " | |
406 | "error_headers args, or include **kwargs in the signature", | |
407 | DeprecationWarning, | |
408 | ) | |
409 | self.handle_error(error, req, schema) | |
285 | error_handler = self.error_callback or self.handle_error | |
286 | error_handler(error, req, schema, error_status_code, error_headers) | |
410 | 287 | |
411 | 288 | def _validate_arguments(self, data, validators): |
412 | 289 | for validator in validators: |
445 | 322 | req=None, |
446 | 323 | locations=None, |
447 | 324 | validate=None, |
448 | force_all=False, | |
449 | 325 | error_status_code=None, |
450 | 326 | error_headers=None, |
451 | 327 | ): |
461 | 337 | :param callable validate: Validation function or list of validation functions |
462 | 338 | that receives the dictionary of parsed arguments. Validator either returns a |
463 | 339 | boolean or raises a :exc:`ValidationError`. |
464 | :param bool force_all: If `True`, missing arguments will be replaced with | |
465 | `missing <marshmallow.utils.missing>`. | |
466 | 340 | :param int error_status_code: Status code passed to error handler functions when |
467 | 341 | a `ValidationError` is raised. |
468 | 342 | :param dict error_headers: Headers passed to error handler functions when a |
476 | 350 | validators = _ensure_list_of_callables(validate) |
477 | 351 | schema = self._get_schema(argmap, req) |
478 | 352 | try: |
479 | parsed = self._parse_request(schema=schema, req=req, locations=locations) | |
353 | parsed = self._parse_request( | |
354 | schema=schema, req=req, locations=locations or self.locations | |
355 | ) | |
480 | 356 | result = schema.load(parsed) |
481 | 357 | data = result.data if MARSHMALLOW_VERSION_INFO[0] < 3 else result |
482 | 358 | self._validate_arguments(data, validators) |
486 | 362 | ) |
487 | 363 | finally: |
488 | 364 | self.clear_cache() |
489 | if force_all: | |
490 | warnings.warn( | |
491 | "Missing arguments will no longer be added to the parsed arguments " | |
492 | "dictionary in version 5.0.0. Pass force_all=False for the new behavior.", | |
493 | RemovedInWebargs5Warning, | |
494 | ) | |
495 | fill_in_missing_args(data, schema) | |
496 | 365 | return data |
497 | 366 | |
498 | 367 | def clear_cache(self): |
527 | 396 | locations=None, |
528 | 397 | as_kwargs=False, |
529 | 398 | validate=None, |
530 | force_all=None, | |
531 | 399 | error_status_code=None, |
532 | 400 | error_headers=None, |
533 | 401 | ): |
548 | 416 | :param callable validate: Validation function that receives the dictionary |
549 | 417 | of parsed arguments. If the function returns ``False``, the parser |
550 | 418 | will raise a :exc:`ValidationError`. |
551 | :param bool force_all: If `True`, missing arguments will be included | |
552 | in the parsed arguments dictionary with the ``missing`` value. | |
553 | If `False`, missing values will be omitted. If `None`, fall back | |
554 | to the value of ``as_kwargs``. | |
555 | 419 | :param int error_status_code: Status code passed to error handler functions when |
556 | 420 | a `ValidationError` is raised. |
557 | 421 | :param dict error_headers: Headers passed to error handler functions when a |
561 | 425 | request_obj = req |
562 | 426 | # Optimization: If argmap is passed as a dictionary, we only need |
563 | 427 | # to generate a Schema once |
564 | if isinstance(argmap, collections.Mapping): | |
428 | if isinstance(argmap, Mapping): | |
565 | 429 | argmap = dict2schema(argmap)() |
566 | 430 | |
567 | 431 | def decorator(func): |
568 | 432 | req_ = request_obj |
569 | force_all_ = force_all if force_all is not None else as_kwargs | |
570 | 433 | |
571 | 434 | @functools.wraps(func) |
572 | 435 | def wrapper(*args, **kwargs): |
580 | 443 | req=req_obj, |
581 | 444 | locations=locations, |
582 | 445 | validate=validate, |
583 | force_all=force_all_, | |
584 | 446 | error_status_code=error_status_code, |
585 | 447 | error_headers=error_headers, |
586 | 448 | ) |
17 | 17 | def get(self, args, request): |
18 | 18 | return HttpResponse('Hello ' + args['name']) |
19 | 19 | """ |
20 | import json | |
21 | ||
22 | 20 | from webargs import core |
21 | from webargs.core import json | |
23 | 22 | |
24 | 23 | |
25 | 24 | class DjangoParser(core.Parser): |
43 | 42 | |
44 | 43 | def parse_json(self, req, name, field): |
45 | 44 | """Pull a json value from the request body.""" |
46 | try: | |
47 | json_data = json.loads(req.body.decode("utf-8")) | |
48 | except (AttributeError, ValueError): | |
49 | return core.missing | |
45 | json_data = self._cache.get("json") | |
46 | if json_data is None: | |
47 | try: | |
48 | self._cache["json"] = json_data = core.parse_json(req.body) | |
49 | except AttributeError: | |
50 | return core.missing | |
51 | except json.JSONDecodeError as e: | |
52 | if e.doc == "": | |
53 | return core.missing | |
54 | else: | |
55 | return self.handle_invalid_json_error(e, req) | |
50 | 56 | return core.get_value(json_data, name, field, allow_many_nested=True) |
51 | 57 | |
52 | 58 | def parse_cookies(self, req, name, field): |
69 | 75 | except AttributeError: # first arg is request |
70 | 76 | return args[0] |
71 | 77 | |
78 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
79 | raise error | |
80 | ||
72 | 81 | |
73 | 82 | parser = DjangoParser() |
74 | 83 | use_args = parser.use_args |
1 | 1 | """Falcon request argument parsing module. |
2 | 2 | """ |
3 | 3 | import falcon |
4 | from falcon.util.uri import parse_query_string | |
4 | 5 | |
5 | 6 | from webargs import core |
6 | from falcon.util.uri import parse_query_string | |
7 | from webargs.core import json | |
7 | 8 | |
8 | 9 | HTTP_422 = "422 Unprocessable Entity" |
9 | 10 | |
37 | 38 | if body: |
38 | 39 | try: |
39 | 40 | return core.parse_json(body) |
40 | except (TypeError, ValueError): | |
41 | pass | |
41 | except json.JSONDecodeError as e: | |
42 | if e.doc == "": | |
43 | return core.missing | |
44 | else: | |
45 | raise | |
42 | 46 | return {} |
43 | 47 | |
44 | 48 | |
111 | 115 | """ |
112 | 116 | json_data = self._cache.get("json_data") |
113 | 117 | if json_data is None: |
114 | self._cache["json_data"] = json_data = parse_json_body(req) | |
118 | try: | |
119 | self._cache["json_data"] = json_data = parse_json_body(req) | |
120 | except json.JSONDecodeError as e: | |
121 | return self.handle_invalid_json_error(e, req) | |
115 | 122 | return core.get_value(json_data, name, field, allow_many_nested=True) |
116 | 123 | |
117 | 124 | def parse_headers(self, req, name, field): |
141 | 148 | |
142 | 149 | def handle_error(self, error, req, schema, error_status_code, error_headers): |
143 | 150 | """Handles errors during parsing.""" |
144 | status = status_map.get(error_status_code or error.status_code) | |
151 | status = status_map.get(error_status_code or self.DEFAULT_VALIDATION_STATUS) | |
145 | 152 | if status is None: |
146 | raise LookupError("Status code {0} not supported".format(error.status_code)) | |
153 | raise LookupError("Status code {0} not supported".format(error_status_code)) | |
147 | 154 | raise HTTPError(status, errors=error.messages, headers=error_headers) |
155 | ||
156 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
157 | status = status_map[400] | |
158 | messages = {"json": ["Invalid JSON body."]} | |
159 | raise HTTPError(status, errors=messages) | |
148 | 160 | |
149 | 161 | |
150 | 162 | parser = FalconParser() |
3 | 3 | Includes all fields from `marshmallow.fields` in addition to a custom |
4 | 4 | `Nested` field and `DelimitedList`. |
5 | 5 | |
6 | All fields can optionally take a special `location` keyword argument, which tells webargs | |
7 | where to parse the request argument from. :: | |
6 | All fields can optionally take a special `location` keyword argument, which | |
7 | tells webargs where to parse the request argument from. :: | |
8 | 8 | |
9 | 9 | args = { |
10 | 10 | 'active': fields.Bool(location='query') |
12 | 12 | location='headers') |
13 | 13 | } |
14 | 14 | |
15 | Note: `data_key` replaced `load_from` in marshmallow 3. When using marshmallow 2, use `load_from`. | |
15 | Note: `data_key` replaced `load_from` in marshmallow 3. | |
16 | When using marshmallow 2, use `load_from`. | |
16 | 17 | """ |
17 | 18 | import marshmallow as ma |
18 | 19 |
22 | 22 | from werkzeug.exceptions import HTTPException |
23 | 23 | |
24 | 24 | from webargs import core |
25 | from webargs.core import json | |
25 | 26 | |
26 | 27 | |
27 | 28 | def abort(http_status_code, exc=None, **kwargs): |
53 | 54 | |
54 | 55 | def parse_json(self, req, name, field): |
55 | 56 | """Pull a json value from the request.""" |
56 | # Pass force in order to handle vendor media types, | |
57 | # e.g. applications/vnd.json+api | |
58 | # this should be unnecessary in Flask 1.0 | |
59 | force = is_json_request(req) | |
60 | # Fail silently so that the webargs parser can handle the error | |
61 | if hasattr(req, "get_json"): | |
62 | # Flask >= 0.10.x | |
63 | json_data = req.get_json(force=force, silent=True) | |
64 | else: | |
65 | # Flask <= 0.9.x | |
66 | json_data = req.json | |
57 | json_data = self._cache.get("json") | |
67 | 58 | if json_data is None: |
68 | return core.missing | |
59 | # We decode the json manually here instead of | |
60 | # using req.get_json() so that we can handle | |
61 | # JSONDecodeErrors consistently | |
62 | data = req.get_data(cache=True) | |
63 | try: | |
64 | self._cache["json"] = json_data = core.parse_json(data) | |
65 | except json.JSONDecodeError as e: | |
66 | if e.doc == "": | |
67 | return core.missing | |
68 | else: | |
69 | return self.handle_invalid_json_error(e, req) | |
69 | 70 | return core.get_value(json_data, name, field, allow_many_nested=True) |
70 | 71 | |
71 | 72 | def parse_querystring(self, req, name, field): |
96 | 97 | """Handles errors during parsing. Aborts the current HTTP request and |
97 | 98 | responds with a 422 error. |
98 | 99 | """ |
99 | status_code = error_status_code or getattr( | |
100 | error, "status_code", self.DEFAULT_VALIDATION_STATUS | |
101 | ) | |
102 | headers = error_headers or getattr(error, "headers", None) | |
100 | status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS | |
103 | 101 | abort( |
104 | 102 | status_code, |
105 | 103 | exc=error, |
106 | 104 | messages=error.messages, |
107 | 105 | schema=schema, |
108 | headers=headers, | |
106 | headers=error_headers, | |
109 | 107 | ) |
108 | ||
109 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
110 | abort(400, exc=error, messages={"json": ["Invalid JSON body."]}) | |
110 | 111 | |
111 | 112 | def get_default_request(self): |
112 | 113 | """Override to use Flask's thread-local request objec by default""" |
32 | 32 | |
33 | 33 | from marshmallow.compat import text_type |
34 | 34 | from webargs import core |
35 | from webargs.core import json | |
35 | 36 | |
36 | 37 | |
37 | 38 | class PyramidParser(core.Parser): |
49 | 50 | |
50 | 51 | def parse_json(self, req, name, field): |
51 | 52 | """Pull a json value from the request.""" |
52 | try: | |
53 | json_data = req.json_body | |
54 | except ValueError: | |
55 | return core.missing | |
53 | json_data = self._cache.get("json") | |
54 | if json_data is None: | |
55 | try: | |
56 | self._cache["json"] = json_data = core.parse_json(req.body, req.charset) | |
57 | except json.JSONDecodeError as e: | |
58 | if e.doc == "": | |
59 | return core.missing | |
60 | else: | |
61 | return self.handle_invalid_json_error(e, req) | |
62 | if json_data is None: | |
63 | return core.missing | |
56 | 64 | return core.get_value(json_data, name, field, allow_many_nested=True) |
57 | 65 | |
58 | 66 | def parse_cookies(self, req, name, field): |
76 | 84 | """Handles errors during parsing. Aborts the current HTTP request and |
77 | 85 | responds with a 400 error. |
78 | 86 | """ |
79 | status_code = error_status_code or getattr(error, "status_code", 422) | |
80 | raise exception_response( | |
81 | status_code, detail=text_type(error), headers=error_headers | |
87 | status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS | |
88 | response = exception_response( | |
89 | status_code, | |
90 | detail=text_type(error), | |
91 | headers=error_headers, | |
92 | content_type="application/json", | |
82 | 93 | ) |
94 | body = json.dumps(error.messages) | |
95 | response.body = body.encode("utf-8") if isinstance(body, text_type) else body | |
96 | raise response | |
97 | ||
98 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
99 | messages = {"json": ["Invalid JSON body."]} | |
100 | response = exception_response( | |
101 | 400, detail=text_type(messages), content_type="application/json" | |
102 | ) | |
103 | body = json.dumps(messages) | |
104 | response.body = body.encode("utf-8") if isinstance(body, text_type) else body | |
105 | raise response | |
83 | 106 | |
84 | 107 | def use_args( |
85 | 108 | self, |
88 | 111 | locations=core.Parser.DEFAULT_LOCATIONS, |
89 | 112 | as_kwargs=False, |
90 | 113 | validate=None, |
91 | force_all=None, | |
92 | 114 | error_status_code=None, |
93 | 115 | error_headers=None, |
94 | 116 | ): |
105 | 127 | :param callable validate: Validation function that receives the dictionary |
106 | 128 | of parsed arguments. If the function returns ``False``, the parser |
107 | 129 | will raise a :exc:`ValidationError`. |
108 | :param bool force_all: If `True`, missing arguments will be included | |
109 | in the parsed arguments dictionary with the ``missing`` value. | |
110 | If `False`, missing values will be omitted. If `None`, fall back | |
111 | to the value of ``as_kwargs``. | |
112 | 130 | :param int error_status_code: Status code passed to error handler functions when |
113 | 131 | a `ValidationError` is raised. |
114 | 132 | :param dict error_headers: Headers passed to error handler functions when a |
121 | 139 | argmap = core.dict2schema(argmap)() |
122 | 140 | |
123 | 141 | def decorator(func): |
124 | force_all_ = force_all if force_all is not None else as_kwargs | |
125 | ||
126 | 142 | @functools.wraps(func) |
127 | 143 | def wrapper(obj, *args, **kwargs): |
128 | 144 | # The first argument is either `self` or `request` |
136 | 152 | req=request, |
137 | 153 | locations=locations, |
138 | 154 | validate=validate, |
139 | force_all=force_all_, | |
140 | 155 | error_status_code=error_status_code, |
141 | 156 | error_headers=error_headers, |
142 | 157 | ) |
6 | 6 | Methods and functions in this module may change without |
7 | 7 | warning and without a major version change. |
8 | 8 | """ |
9 | import json | |
10 | ||
11 | 9 | import pytest |
12 | 10 | import webtest |
13 | 11 | |
14 | from webargs.core import MARSHMALLOW_VERSION_INFO | |
12 | from webargs.core import json | |
15 | 13 | |
16 | 14 | |
17 | 15 | class CommonTestCase(object): |
126 | 124 | res = testapp.post_json("/error", {"text": "foo"}, expect_errors=True) |
127 | 125 | assert res.status_code == 422 |
128 | 126 | |
129 | @pytest.mark.skipif( | |
130 | MARSHMALLOW_VERSION_INFO < (2, 7), | |
131 | reason="status_code only works in marshmallow>=2.7", | |
132 | ) | |
133 | def test_user_validation_error_with_status_code(self, testapp): | |
134 | res = testapp.post_json("/error400", {"text": "foo"}, expect_errors=True) | |
135 | assert res.status_code == 400 | |
136 | ||
137 | 127 | def test_use_args_decorator(self, testapp): |
138 | 128 | assert testapp.get("/echo_use_args?name=Fred").json == {"name": "Fred"} |
139 | 129 | |
193 | 183 | "/echo_file", {"myfile": webtest.Upload("README.rst", b"data")} |
194 | 184 | ) |
195 | 185 | assert res.json == {"myfile": "data"} |
186 | ||
187 | # https://github.com/sloria/webargs/pull/297 | |
188 | def test_empty_json(self, testapp): | |
189 | res = testapp.post( | |
190 | "/echo", | |
191 | "", | |
192 | headers={"Accept": "application/json", "Content-Type": "application/json"}, | |
193 | ) | |
194 | assert res.status_code == 200 | |
195 | assert res.json == {"name": "World"} | |
196 | ||
197 | # https://github.com/sloria/webargs/issues/329 | |
198 | def test_invalid_json(self, testapp): | |
199 | res = testapp.post( | |
200 | "/echo", | |
201 | '{"foo": "bar", }', | |
202 | headers={"Accept": "application/json", "Content-Type": "application/json"}, | |
203 | expect_errors=True, | |
204 | ) | |
205 | assert res.status_code == 400 | |
206 | assert res.json == {"json": ["Invalid JSON body."]} |
18 | 18 | |
19 | 19 | from marshmallow.compat import basestring |
20 | 20 | from webargs import core |
21 | from webargs.core import json | |
21 | 22 | |
22 | 23 | |
23 | 24 | class HTTPError(tornado.web.HTTPError): |
35 | 36 | if content_type and core.is_json(content_type): |
36 | 37 | try: |
37 | 38 | return core.parse_json(req.body) |
38 | except (TypeError, ValueError): | |
39 | except TypeError: | |
39 | 40 | pass |
41 | except json.JSONDecodeError as e: | |
42 | if e.doc == "": | |
43 | return core.missing | |
44 | else: | |
45 | raise | |
40 | 46 | return {} |
41 | 47 | |
42 | 48 | |
83 | 89 | """Pull a json value from the request.""" |
84 | 90 | json_data = self._cache.get("json") |
85 | 91 | if json_data is None: |
86 | self._cache["json"] = json_data = parse_json_body(req) | |
92 | try: | |
93 | self._cache["json"] = json_data = parse_json_body(req) | |
94 | except json.JSONDecodeError as e: | |
95 | return self.handle_invalid_json_error(e, req) | |
87 | 96 | if json_data is None: |
88 | 97 | return core.missing |
89 | 98 | return core.get_value(json_data, name, field, allow_many_nested=True) |
117 | 126 | """Handles errors during parsing. Raises a `tornado.web.HTTPError` |
118 | 127 | with a 400 error. |
119 | 128 | """ |
120 | status_code = error_status_code or getattr( | |
121 | error, "status_code", core.DEFAULT_VALIDATION_STATUS | |
122 | ) | |
129 | status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS | |
123 | 130 | if status_code == 422: |
124 | 131 | reason = "Unprocessable Entity" |
125 | 132 | else: |
132 | 139 | headers=error_headers, |
133 | 140 | ) |
134 | 141 | |
142 | def handle_invalid_json_error(self, error, req, *args, **kwargs): | |
143 | raise HTTPError( | |
144 | 400, | |
145 | log_message="Invalid JSON body.", | |
146 | reason="Bad Request", | |
147 | messages={"json": ["Invalid JSON body."]}, | |
148 | ) | |
149 | ||
135 | 150 | def get_request_from_view_args(self, view, args, kwargs): |
136 | 151 | return args[0].request |
137 | 152 |
26 | 26 | webapp2.Route(r'/hello_dict', MainPage, handler_method='get_kwargs'), |
27 | 27 | ], debug=True) |
28 | 28 | """ |
29 | import webapp2 | |
30 | import webob.multidict | |
31 | ||
29 | 32 | from webargs import core |
30 | import webapp2 | |
31 | import webapp2_extras.json | |
32 | import webob.multidict | |
33 | from webargs.core import json | |
33 | 34 | |
34 | 35 | |
35 | 36 | class Webapp2Parser(core.Parser): |
37 | 38 | |
38 | 39 | def parse_json(self, req, name, field): |
39 | 40 | """Pull a json value from the request.""" |
40 | try: | |
41 | json_data = webapp2_extras.json.decode(req.body) | |
42 | except ValueError: | |
43 | return core.missing | |
41 | json_data = self._cache.get("json") | |
42 | if json_data is None: | |
43 | try: | |
44 | self._cache["json"] = json_data = core.parse_json(req.body) | |
45 | except json.JSONDecodeError as e: | |
46 | if e.doc == "": | |
47 | return core.missing | |
48 | else: | |
49 | raise | |
44 | 50 | return core.get_value(json_data, name, field, allow_many_nested=True) |
45 | 51 | |
46 | 52 | def parse_querystring(self, req, name, field): |