├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── build-release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── AUTHORS.rst ├── CHANGELOG.rst ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.rst ├── LICENSE ├── NOTICE ├── README.rst ├── RELEASING.md ├── SECURITY.md ├── docs ├── Makefile ├── _static │ └── logo.png ├── _templates │ ├── donate.html │ └── sponsors.html ├── advanced.rst ├── api.rst ├── authors.rst ├── changelog.rst ├── conf.py ├── contributing.rst ├── ecosystem.rst ├── framework_support.rst ├── index.rst ├── install.rst ├── license.rst ├── make.bat ├── quickstart.rst └── upgrading.rst ├── examples ├── __init__.py ├── aiohttp_example.py ├── annotations_example.py ├── bottle_example.py ├── falcon_example.py ├── flask_example.py ├── flaskrestful_example.py ├── pyramid_example.py ├── requirements.txt ├── schema_example.py └── tornado_example.py ├── pyproject.toml ├── src └── webargs │ ├── __init__.py │ ├── _types.py │ ├── aiohttpparser.py │ ├── asyncparser.py │ ├── bottleparser.py │ ├── core.py │ ├── djangoparser.py │ ├── falconparser.py │ ├── fields.py │ ├── flaskparser.py │ ├── multidictproxy.py │ ├── py.typed │ ├── pyramidparser.py │ ├── testing.py │ └── tornadoparser.py ├── tests ├── __init__.py ├── apps │ ├── __init__.py │ ├── aiohttp_app.py │ ├── bottle_app.py │ ├── django_app │ │ ├── __init__.py │ │ ├── base │ │ │ ├── __init__.py │ │ │ ├── settings.py │ │ │ ├── urls.py │ │ │ └── wsgi.py │ │ ├── echo │ │ │ ├── __init__.py │ │ │ └── views.py │ │ └── manage.py │ ├── falcon_app.py │ ├── flask_app.py │ └── pyramid_app.py ├── conftest.py ├── test_aiohttpparser.py ├── test_bottleparser.py ├── test_core.py ├── test_djangoparser.py ├── test_falconparser.py ├── test_flaskparser.py ├── test_pyramidparser.py └── test_tornadoparser.py └── tox.ini /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: "marshmallow" 2 | tidelift: "pypi/webargs" 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "monthly" 13 | -------------------------------------------------------------------------------- /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | push: 4 | branches: ["dev", "*.x-line"] 5 | tags: ["*"] 6 | pull_request: 7 | 8 | jobs: 9 | tests: 10 | name: ${{ matrix.name }} 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | - { name: "3.9", python: "3.9", tox: py39-marshmallow3 } 17 | - { name: "3.13", python: "3.13", tox: py313-marshmallow3 } 18 | - { name: "3.9", python: "3.9", tox: py39-marshmallow4 } 19 | - { name: "3.13", python: "3.13", tox: py313-marshmallow4 } 20 | - { name: "lowest", python: "3.9", tox: py39-lowest } 21 | - { name: "dev", python: "3.13", tox: py313-marshmallowdev } 22 | steps: 23 | - uses: actions/checkout@v4.0.0 24 | - uses: actions/setup-python@v5 25 | with: 26 | python-version: ${{ matrix.python }} 27 | - run: python -m pip install tox 28 | - run: python -m tox -e${{ matrix.tox }} 29 | build: 30 | name: Build package 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: actions/setup-python@v5 35 | with: 36 | python-version: "3.13" 37 | - name: Install pypa/build 38 | run: python -m pip install build 39 | - name: Build a binary wheel and a source tarball 40 | run: python -m build 41 | - name: Install twine 42 | run: python -m pip install twine 43 | - name: Check build 44 | run: python -m twine check --strict dist/* 45 | - name: Store the distribution packages 46 | uses: actions/upload-artifact@v4 47 | with: 48 | name: python-package-distributions 49 | path: dist/ 50 | # this duplicates pre-commit.ci, so only run it on tags 51 | # it guarantees that linting is passing prior to a release 52 | lint-pre-release: 53 | if: startsWith(github.ref, 'refs/tags') 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v4.0.0 57 | - uses: actions/setup-python@v5 58 | with: 59 | python-version: "3.13" 60 | - run: python -m pip install tox 61 | - run: python -m tox -e lint 62 | publish-to-pypi: 63 | name: PyPI release 64 | if: startsWith(github.ref, 'refs/tags/') 65 | needs: [build, tests, lint-pre-release] 66 | runs-on: ubuntu-latest 67 | environment: 68 | name: pypi 69 | url: https://pypi.org/p/webargs 70 | permissions: 71 | id-token: write 72 | steps: 73 | - name: Download all the dists 74 | uses: actions/download-artifact@v4 75 | with: 76 | name: python-package-distributions 77 | path: dist/ 78 | - name: Publish distribution to PyPI 79 | uses: pypa/gh-action-pypi-publish@release/v1 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | README.html 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.11.8 4 | hooks: 5 | - id: ruff 6 | - id: ruff-format 7 | - repo: https://github.com/python-jsonschema/check-jsonschema 8 | rev: 0.33.0 9 | hooks: 10 | - id: check-github-workflows 11 | - id: check-readthedocs 12 | - repo: https://github.com/asottile/blacken-docs 13 | rev: 1.19.1 14 | hooks: 15 | - id: blacken-docs 16 | additional_dependencies: [black==24.4.2] 17 | - repo: https://github.com/pre-commit/mirrors-mypy 18 | rev: v1.15.0 19 | hooks: 20 | - id: mypy 21 | language_version: python3 22 | files: ^src/webargs/ 23 | additional_dependencies: 24 | - marshmallow>=3,<4 25 | - packaging 26 | - flask 27 | 28 | 29 | # mypy runs under tox in GitHub Actions, skip it in pre-commit.ci 30 | ci: 31 | skip: [mypy] 32 | autoupdate_schedule: monthly 33 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | sphinx: 3 | configuration: docs/conf.py 4 | formats: 5 | - pdf 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3.11" 10 | python: 11 | install: 12 | - method: pip 13 | path: . 14 | extra_requirements: 15 | - docs 16 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Authors 3 | ======= 4 | 5 | Lead 6 | ---- 7 | 8 | * Steven Loria `@sloria `_ 9 | * Jérôme Lafréchoux `@lafrech `_ 10 | * Stephen Rosen `@sirosen `_ 11 | 12 | Contributors (chronological) 13 | ---------------------------- 14 | 15 | * Steven Manuatu `@venuatu `_ 16 | * Javier Santacruz `@jvrsantacruz `_ 17 | * Josh Carp `@jmcarp `_ 18 | * `@philtay `_ 19 | * Andriy Yurchuk `@Ch00k `_ 20 | * Stas Sușcov `@stas `_ 21 | * Josh Johnston `@Trii `_ 22 | * Rory Hart `@hartror `_ 23 | * Jace Browning `@jacebrowning `_ 24 | * marcellarius `@marcellarius `_ 25 | * Damian Heard `@DamianHeard `_ 26 | * Daniel Imhoff `@dwieeb `_ 27 | * `@immerrr `_ 28 | * Brett Higgins `@brettdh `_ 29 | * Vlad Frolov `@frol `_ 30 | * Tuukka Mustonen `@tuukkamustonen `_ 31 | * Francois-Xavier Darveau `@EFF `_ 32 | * Jérôme Lafréchoux `@lafrech `_ 33 | * `@DmitriyS `_ 34 | * Svetlozar Argirov `@zaro `_ 35 | * Florian S. `@nebularazer `_ 36 | * `@daniel98321 `_ 37 | * `@Itayazolay `_ 38 | * `@Reskov `_ 39 | * `@cedzz `_ 40 | * F. Moukayed (כוכב) `@kochab `_ 41 | * Xiaoyu Lee `@lee3164 `_ 42 | * Jonathan Angelo `@jangelo `_ 43 | * `@zhenhua32 `_ 44 | * Martin Roy `@lindycoder `_ 45 | * Kubilay Kocak `@koobs `_ 46 | * `@dodumosu `_ 47 | * Nate Dellinger `@Nateyo `_ 48 | * Karthikeyan Singaravelan `@tirkarthi `_ 49 | * Sami Salonen `@suola `_ 50 | * Tim Gates `@timgates42 `_ 51 | * Lefteris Karapetsas `@lefterisjp `_ 52 | * Utku Gultopu `@ugultopu `_ 53 | * Jason Williams `@jaswilli `_ 54 | * Grey Li `@greyli `_ 55 | * `@michaelizergit `_ 56 | * Legolas Bloom `@TTWShell `_ 57 | * Kevin Kirsche `@kkirsche `_ 58 | * Isira Seneviratne `@Isira-Seneviratne `_ 59 | * Anton Ostapenko `@AVOstap `_ 60 | * Tumuer `@un4gt `_ 61 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | For the code of conduct, see https://marshmallow.readthedocs.io/en/dev/code_of_conduct.html 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing Guidelines 2 | ======================= 3 | 4 | Security Contact Information 5 | ---------------------------- 6 | 7 | To report a security vulnerability, please use the 8 | `Tidelift security contact `_. 9 | Tidelift will coordinate the fix and disclosure. 10 | 11 | Questions, Feature Requests, Bug Reports, and Feedback… 12 | ------------------------------------------------------- 13 | 14 | …should all be reported on the `GitHub Issue Tracker`_ . 15 | 16 | .. _`GitHub Issue Tracker`: https://github.com/marshmallow-code/webargs/issues?state=open 17 | 18 | 19 | Contributing Code 20 | ----------------- 21 | 22 | Integration with a Another Web Framework… 23 | +++++++++++++++++++++++++++++++++++++++++ 24 | 25 | …should be released as a separate package. 26 | 27 | **Pull requests adding support for another framework will not be 28 | accepted**. In order to keep webargs small and easy to maintain, we are 29 | not currently adding support for more frameworks. Instead, release your 30 | framework integration as a separate package and add it to the 31 | `Ecosystem `_ page in 32 | the `GitHub wiki `_ . 33 | 34 | Setting Up for Local Development 35 | ++++++++++++++++++++++++++++++++ 36 | 37 | 1. Fork webargs_ on GitHub. 38 | 39 | :: 40 | 41 | $ git clone https://github.com/marshmallow-code/webargs.git 42 | $ cd webargs 43 | 44 | 2. Install development requirements. **It is highly recommended that you use a virtualenv.** 45 | Use the following command to install an editable version of 46 | webargs along with its development requirements. 47 | 48 | :: 49 | 50 | # After activating your virtualenv 51 | $ pip install -e ".[dev]" 52 | 53 | 3. (Optional, but recommended) Install the pre-commit hooks, which will format and lint your git staged files. 54 | 55 | :: 56 | 57 | # The pre-commit CLI was installed above 58 | $ pre-commit install 59 | 60 | Git Branch Structure 61 | ++++++++++++++++++++ 62 | 63 | Webargs abides by the following branching model: 64 | 65 | 66 | ``dev`` 67 | Current development branch. **New features should branch off here**. 68 | 69 | ``X.Y-line`` 70 | Maintenance branch for release ``X.Y``. **Bug fixes should be sent to the most recent release branch.**. The maintainer will forward-port the fix to ``dev``. Note: exceptions may be made for bug fixes that introduce large code changes. 71 | 72 | **Always make a new branch for your work**, no matter how small. Also, **do not put unrelated changes in the same branch or pull request**. This makes it more difficult to merge your changes. 73 | 74 | Pull Requests 75 | ++++++++++++++ 76 | 77 | 1. Create a new local branch. 78 | 79 | :: 80 | 81 | # For a new feature 82 | $ git checkout -b name-of-feature dev 83 | 84 | # For a bugfix 85 | $ git checkout -b fix-something 1.2-line 86 | 87 | 2. Commit your changes. Write `good commit messages `_. 88 | 89 | :: 90 | 91 | $ git commit -m "Detailed commit message" 92 | $ git push origin name-of-feature 93 | 94 | 3. Before submitting a pull request, check the following: 95 | 96 | - If the pull request adds functionality, it is tested and the docs are updated. 97 | - You've added yourself to ``AUTHORS.rst``. 98 | 99 | 4. Submit a pull request to ``marshmallow-code:dev`` or the appropriate maintenance branch. 100 | The `CI `_ build must be passing before your pull request is merged. 101 | 102 | Running Tests 103 | +++++++++++++ 104 | 105 | To run all tests: :: 106 | 107 | $ pytest 108 | 109 | To run syntax checks: :: 110 | 111 | $ tox -e lint 112 | 113 | (Optional) To run tests in all supported Python versions in their own virtual environments (must have each interpreter installed): :: 114 | 115 | $ tox 116 | 117 | Documentation 118 | +++++++++++++ 119 | 120 | Contributions to the documentation are welcome. Documentation is written in `reStructuredText`_ (rST). A quick rST reference can be found `here `_. Builds are powered by Sphinx_. 121 | 122 | To build the docs in "watch" mode: :: 123 | 124 | $ tox -e watch-docs 125 | 126 | Changes in the `docs/` directory will automatically trigger a rebuild. 127 | 128 | Contributing Examples 129 | +++++++++++++++++++++ 130 | 131 | Have a usage example you'd like to share? Feel free to add it to the `examples `_ directory and send a pull request. 132 | 133 | 134 | .. _Sphinx: http://sphinx.pocoo.org/ 135 | .. _`reStructuredText`: https://docutils.sourceforge.io/rst.html 136 | .. _webargs: https://github.com/marshmallow-code/webargs 137 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Steven Loria and contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | webargs includes some code from third-party libraries. 2 | 3 | 4 | Flask-Restful License 5 | ===================== 6 | 7 | Copyright (c) 2013, Twilio, Inc. 8 | All rights reserved. 9 | 10 | Redistribution and use in source and binary forms, with or without 11 | modification, are permitted provided that the following conditions are met: 12 | 13 | - Redistributions of source code must retain the above copyright notice, this 14 | list of conditions and the following disclaimer. 15 | - Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | - Neither the name of the Twilio, Inc. nor the names of its contributors may be 19 | used to endorse or promote products derived from this software without 20 | specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | Werkzeug License 34 | ================ 35 | 36 | Copyright (c) 2014 by the Werkzeug Team, see AUTHORS for more details. 37 | 38 | Redistribution and use in source and binary forms, with or without 39 | modification, are permitted provided that the following conditions are 40 | met: 41 | 42 | * Redistributions of source code must retain the above copyright 43 | notice, this list of conditions and the following disclaimer. 44 | 45 | * Redistributions in binary form must reproduce the above 46 | copyright notice, this list of conditions and the following 47 | disclaimer in the documentation and/or other materials provided 48 | with the distribution. 49 | 50 | * The names of the contributors may not be used to endorse or 51 | promote products derived from this software without specific 52 | prior written permission. 53 | 54 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 55 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 56 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 57 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 58 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 59 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 60 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 61 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 62 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 63 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 64 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 65 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ******* 2 | webargs 3 | ******* 4 | 5 | |pypi| |build-status| |docs| |marshmallow-support| 6 | 7 | .. |pypi| image:: https://badgen.net/pypi/v/webargs 8 | :target: https://pypi.org/project/webargs/ 9 | :alt: PyPI package 10 | 11 | .. |build-status| image:: https://github.com/marshmallow-code/webargs/actions/workflows/build-release.yml/badge.svg 12 | :target: https://github.com/marshmallow-code/webargs/actions/workflows/build-release.yml 13 | :alt: Build status 14 | 15 | .. |docs| image:: https://readthedocs.org/projects/webargs/badge/ 16 | :target: https://webargs.readthedocs.io/ 17 | :alt: Documentation 18 | 19 | .. |marshmallow-support| image:: https://badgen.net/badge/marshmallow/3,4?list=1 20 | :target: https://marshmallow.readthedocs.io/en/latest/upgrading.html 21 | :alt: marshmallow 3|4 compatible 22 | 23 | Homepage: https://webargs.readthedocs.io/ 24 | 25 | webargs is a Python library for parsing and validating HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, Falcon, and aiohttp. 26 | 27 | .. code-block:: python 28 | 29 | from flask import Flask 30 | from webargs import fields 31 | from webargs.flaskparser import use_args 32 | 33 | app = Flask(__name__) 34 | 35 | 36 | @app.route("/") 37 | @use_args({"name": fields.Str(required=True)}, location="query") 38 | def index(args): 39 | return "Hello " + args["name"] 40 | 41 | 42 | if __name__ == "__main__": 43 | app.run() 44 | 45 | # curl http://localhost:5000/\?name\='World' 46 | # Hello World 47 | 48 | Install 49 | ======= 50 | 51 | :: 52 | 53 | pip install -U webargs 54 | 55 | Documentation 56 | ============= 57 | 58 | Full documentation is available at https://webargs.readthedocs.io/. 59 | 60 | Support webargs 61 | =============== 62 | 63 | webargs is maintained by a group of 64 | `volunteers `_. 65 | If you'd like to support the future of the project, please consider 66 | contributing to our Open Collective: 67 | 68 | .. image:: https://opencollective.com/marshmallow/donate/button.png 69 | :target: https://opencollective.com/marshmallow 70 | :width: 200 71 | :alt: Donate to our collective 72 | 73 | Professional Support 74 | ==================== 75 | 76 | Professionally-supported webargs is available through the 77 | `Tidelift Subscription `_. 78 | 79 | Tidelift gives software development teams a single source for purchasing and maintaining their software, 80 | with professional-grade assurances from the experts who know it best, 81 | while seamlessly integrating with existing tools. [`Get professional support`_] 82 | 83 | .. _`Get professional support`: https://tidelift.com/subscription/pkg/pypi-webargs?utm_source=pypi-webargs&utm_medium=referral&utm_campaign=readme 84 | 85 | .. image:: https://user-images.githubusercontent.com/2379650/45126032-50b69880-b13f-11e8-9c2c-abd16c433495.png 86 | :target: https://tidelift.com/subscription/pkg/pypi-webargs?utm_source=pypi-webargs&utm_medium=referral&utm_campaign=readme 87 | :alt: Get supported marshmallow with Tidelift 88 | 89 | Security Contact Information 90 | ============================ 91 | 92 | To report a security vulnerability, please use the 93 | `Tidelift security contact `_. 94 | Tidelift will coordinate the fix and disclosure. 95 | 96 | Project Links 97 | ============= 98 | 99 | - Docs: https://webargs.readthedocs.io/ 100 | - Changelog: https://webargs.readthedocs.io/en/latest/changelog.html 101 | - Contributing Guidelines: https://webargs.readthedocs.io/en/latest/contributing.html 102 | - PyPI: https://pypi.python.org/pypi/webargs 103 | - Issues: https://github.com/marshmallow-code/webargs/issues 104 | - Ecosystem / related packages: https://github.com/marshmallow-code/webargs/wiki/Ecosystem 105 | 106 | 107 | License 108 | ======= 109 | 110 | MIT licensed. See the `LICENSE `_ file for more details. 111 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | 1. Bump version in `pyproject.toml` and update the changelog 4 | with today's date. 5 | 2. Commit: `git commit -m "Bump version and update changelog"` 6 | 3. Tag the commit: `git tag x.y.z` 7 | 4. Push: `git push --tags origin dev`. CI will take care of the 8 | PyPI release. 9 | 5. Add release notes on Tidelift. 10 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Contact Information 2 | 3 | To report a security vulnerability, please use the 4 | [Tidelift security contact](https://tidelift.com/security). 5 | Tidelift will coordinate the fix and disclosure. 6 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/complexity.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/complexity.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/complexity" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/complexity" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." -------------------------------------------------------------------------------- /docs/_static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marshmallow-code/webargs/a59bfca9c9c88e0cbaa97eb2cdba3683e8cc33a1/docs/_static/logo.png -------------------------------------------------------------------------------- /docs/_templates/donate.html: -------------------------------------------------------------------------------- 1 | {% if donate_url %} 2 | 9 | {% endif %} 10 | -------------------------------------------------------------------------------- /docs/_templates/sponsors.html: -------------------------------------------------------------------------------- 1 |
2 | {% if tidelift_url %} 3 | 10 | {% endif %} 11 |
12 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | .. module:: webargs 5 | 6 | webargs.core 7 | ------------ 8 | 9 | .. automodule:: webargs.core 10 | :inherited-members: 11 | 12 | 13 | webargs.fields 14 | -------------- 15 | 16 | .. automodule:: webargs.fields 17 | :members: Nested, DelimitedList 18 | 19 | 20 | webargs.multidictproxy 21 | ---------------------- 22 | 23 | .. automodule:: webargs.multidictproxy 24 | :members: 25 | 26 | 27 | webargs.asyncparser 28 | ------------------- 29 | 30 | .. automodule:: webargs.asyncparser 31 | :inherited-members: 32 | 33 | webargs.flaskparser 34 | ------------------- 35 | 36 | .. automodule:: webargs.flaskparser 37 | :members: 38 | 39 | webargs.djangoparser 40 | -------------------- 41 | 42 | .. automodule:: webargs.djangoparser 43 | :members: 44 | 45 | webargs.bottleparser 46 | -------------------- 47 | 48 | .. automodule:: webargs.bottleparser 49 | :members: 50 | 51 | webargs.tornadoparser 52 | --------------------- 53 | 54 | .. automodule:: webargs.tornadoparser 55 | :members: 56 | 57 | webargs.pyramidparser 58 | --------------------- 59 | 60 | .. automodule:: webargs.pyramidparser 61 | :members: 62 | 63 | webargs.falconparser 64 | --------------------- 65 | 66 | .. automodule:: webargs.falconparser 67 | :members: 68 | 69 | webargs.aiohttpparser 70 | --------------------- 71 | 72 | .. automodule:: webargs.aiohttpparser 73 | :members: 74 | -------------------------------------------------------------------------------- /docs/authors.rst: -------------------------------------------------------------------------------- 1 | 2 | .. include:: ../AUTHORS.rst 3 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGELOG.rst 2 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | 3 | extensions = [ 4 | "sphinx.ext.autodoc", 5 | "sphinx.ext.viewcode", 6 | "sphinx.ext.intersphinx", 7 | "sphinx_issues", 8 | ] 9 | 10 | primary_domain = "py" 11 | default_role = "py:obj" 12 | 13 | github_user = "marshmallow-code" 14 | github_repo = "webargs" 15 | 16 | issues_github_path = f"{github_user}/{github_repo}" 17 | 18 | intersphinx_mapping = { 19 | "python": ("http://python.readthedocs.io/en/latest/", None), 20 | "marshmallow": ("http://marshmallow.readthedocs.io/en/latest/", None), 21 | } 22 | 23 | 24 | # The master toctree document. 25 | master_doc = "index" 26 | language = "en" 27 | html_domain_indices = False 28 | source_suffix = ".rst" 29 | project = "webargs" 30 | copyright = "Steven Loria and contributors" 31 | version = release = importlib.metadata.version("webargs") 32 | templates_path = ["_templates"] 33 | exclude_patterns = ["_build"] 34 | 35 | # THEME 36 | 37 | html_theme = "furo" 38 | 39 | html_theme_options = { 40 | "light_css_variables": {"color-brand-primary": "#268bd2"}, 41 | } 42 | html_logo = "_static/logo.png" 43 | 44 | html_context = { 45 | "tidelift_url": ( 46 | "https://tidelift.com/subscription/pkg/pypi-webargs" 47 | "?utm_source=pypi-webargs&utm_medium=referral&utm_campaign=docs" 48 | ), 49 | "donate_url": "https://opencollective.com/marshmallow", 50 | } 51 | html_sidebars = { 52 | "*": [ 53 | "sidebar/scroll-start.html", 54 | "sidebar/brand.html", 55 | "sidebar/search.html", 56 | "sidebar/navigation.html", 57 | "donate.html", 58 | "sponsors.html", 59 | "sidebar/ethical-ads.html", 60 | "sidebar/scroll-end.html", 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CONTRIBUTING.rst 2 | -------------------------------------------------------------------------------- /docs/ecosystem.rst: -------------------------------------------------------------------------------- 1 | Ecosystem 2 | ========= 3 | 4 | A list of webargs-related libraries can be found at the GitHub wiki here: 5 | 6 | https://github.com/marshmallow-code/webargs/wiki/Ecosystem 7 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | webargs 3 | ======= 4 | 5 | Release v\ |version|. (:doc:`Changelog `) 6 | 7 | webargs is a Python library for parsing and validating HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, Falcon, and aiohttp. 8 | 9 | Upgrading from an older version? 10 | -------------------------------- 11 | 12 | See the :doc:`Upgrading to Newer Releases ` page for notes on getting your code up-to-date with the latest version. 13 | 14 | 15 | Usage and Simple Examples 16 | ------------------------- 17 | 18 | .. code-block:: python 19 | 20 | from flask import Flask 21 | from webargs import fields 22 | from webargs.flaskparser import use_args 23 | 24 | app = Flask(__name__) 25 | 26 | 27 | @app.route("/") 28 | @use_args({"name": fields.Str(required=True)}, location="query") 29 | def index(args): 30 | return "Hello " + args["name"] 31 | 32 | 33 | if __name__ == "__main__": 34 | app.run() 35 | 36 | # curl http://localhost:5000/\?name\='World' 37 | # Hello World 38 | 39 | By default Webargs will automatically parse JSON request bodies. But it also 40 | has support for: 41 | 42 | **Query Parameters** 43 | :: 44 | 45 | $ curl http://localhost:5000/\?name\='Freddie' 46 | Hello Freddie 47 | 48 | # pass location="query" to use_args 49 | 50 | **Form Data** 51 | :: 52 | 53 | $ curl -d 'name=Brian' http://localhost:5000/ 54 | Hello Brian 55 | 56 | # pass location="form" to use_args 57 | 58 | **JSON Data** 59 | :: 60 | 61 | $ curl -X POST -H "Content-Type: application/json" -d '{"name":"Roger"}' http://localhost:5000/ 62 | Hello Roger 63 | 64 | # pass location="json" (or omit location) to use_args 65 | 66 | and, optionally: 67 | 68 | - Headers 69 | - Cookies 70 | - Files 71 | - Paths 72 | 73 | Why Use It 74 | ---------- 75 | 76 | * **Simple, declarative syntax**. Define your arguments as a mapping rather than imperatively pulling values off of request objects. 77 | * **Code reusability**. If you have multiple views that have the same request parameters, you only need to define your parameters once. You can also reuse validation and pre-processing routines. 78 | * **Self-documentation**. Webargs makes it easy to understand the expected arguments and their types for your view functions. 79 | * **Automatic documentation**. The metadata that webargs provides can serve as an aid for automatically generating API documentation. 80 | * **Cross-framework compatibility**. Webargs provides a consistent request-parsing interface that will work across many Python web frameworks. 81 | * **marshmallow integration**. Webargs uses `marshmallow `_ under the hood. When you need more flexibility than dictionaries, you can use marshmallow `Schemas ` to define your request arguments. 82 | 83 | Get It Now 84 | ---------- 85 | 86 | :: 87 | 88 | pip install -U webargs 89 | 90 | Ready to get started? Go on to the :doc:`Quickstart tutorial ` or check out some `examples `_. 91 | 92 | User Guide 93 | ---------- 94 | 95 | .. toctree:: 96 | :maxdepth: 2 97 | 98 | install 99 | quickstart 100 | advanced 101 | framework_support 102 | ecosystem 103 | 104 | API Reference 105 | ------------- 106 | 107 | .. toctree:: 108 | :maxdepth: 2 109 | 110 | api 111 | 112 | 113 | Project Info 114 | ------------ 115 | 116 | .. toctree:: 117 | :maxdepth: 1 118 | 119 | license 120 | changelog 121 | upgrading 122 | authors 123 | contributing 124 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | Install 2 | ======= 3 | 4 | **webargs** depends on `marshmallow `_ >= 3.0.0. 5 | 6 | From the PyPI 7 | ------------- 8 | 9 | To install the latest version from the PyPI: 10 | 11 | :: 12 | 13 | $ pip install -U webargs 14 | 15 | 16 | Get the Bleeding Edge Version 17 | ----------------------------- 18 | 19 | To get the latest development version of webargs, run 20 | 21 | :: 22 | 23 | $ pip install -U git+https://github.com/marshmallow-code/webargs.git@dev 24 | -------------------------------------------------------------------------------- /docs/license.rst: -------------------------------------------------------------------------------- 1 | 2 | ******* 3 | License 4 | ******* 5 | 6 | .. literalinclude:: ../LICENSE 7 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\complexity.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\complexity.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ========== 3 | 4 | Basic Usage 5 | ----------- 6 | 7 | Arguments are specified as a dictionary of name -> :class:`Field ` pairs. 8 | 9 | .. code-block:: python 10 | 11 | from webargs import fields, validate 12 | 13 | user_args = { 14 | # Required arguments 15 | "username": fields.Str(required=True), 16 | # Validation 17 | "password": fields.Str(validate=lambda p: len(p) >= 6), 18 | # OR use marshmallow's built-in validators 19 | "password": fields.Str(validate=validate.Length(min=6)), 20 | # Default value when argument is missing 21 | "display_per_page": fields.Int(load_default=10), 22 | # Repeated parameter, e.g. "/?nickname=Fred&nickname=Freddie" 23 | "nickname": fields.List(fields.Str()), 24 | # Delimited list, e.g. "/?languages=python,javascript" 25 | "languages": fields.DelimitedList(fields.Str()), 26 | # When value is keyed on a variable-unsafe name 27 | # or you want to rename a key 28 | "user_type": fields.Str(data_key="user-type"), 29 | } 30 | 31 | .. note:: 32 | 33 | See the `marshmallow.fields` documentation for a full reference on available field types. 34 | 35 | To parse request arguments, use the :meth:`parse ` method of a :class:`Parser ` object. 36 | 37 | .. code-block:: python 38 | 39 | from flask import request 40 | from webargs.flaskparser import parser 41 | 42 | 43 | @app.route("/register", methods=["POST"]) 44 | def register(): 45 | args = parser.parse(user_args, request) 46 | return register_user( 47 | args["username"], 48 | args["password"], 49 | fullname=args["fullname"], 50 | per_page=args["display_per_page"], 51 | ) 52 | 53 | 54 | Decorator API 55 | ------------- 56 | 57 | As an alternative to `Parser.parse`, you can decorate your view with :meth:`use_args ` or :meth:`use_kwargs `. The parsed arguments dictionary will be injected as a parameter of your view function or as keyword arguments, respectively. 58 | 59 | .. code-block:: python 60 | 61 | from webargs.flaskparser import use_args, use_kwargs 62 | 63 | 64 | @app.route("/register", methods=["POST"]) 65 | @use_args(user_args) # Injects args dictionary 66 | def register(args): 67 | return register_user( 68 | args["username"], 69 | args["password"], 70 | fullname=args["fullname"], 71 | per_page=args["display_per_page"], 72 | ) 73 | 74 | 75 | @app.route("/settings", methods=["POST"]) 76 | @use_kwargs(user_args) # Injects keyword arguments 77 | def user_settings(username, password, fullname, display_per_page, nickname): 78 | return render_template("settings.html", username=username, nickname=nickname) 79 | 80 | 81 | .. note:: 82 | 83 | When using `use_kwargs`, any missing values will be omitted from the arguments. 84 | Use ``**kwargs`` to handle optional arguments. 85 | 86 | .. code-block:: python 87 | 88 | from webargs import fields, missing 89 | 90 | 91 | @use_kwargs({"name": fields.Str(required=True), "nickname": fields.Str(required=False)}) 92 | def myview(name, **kwargs): 93 | if "nickname" not in kwargs: 94 | # ... 95 | pass 96 | 97 | Request "Locations" 98 | ------------------- 99 | 100 | By default, webargs will search for arguments from the request body as JSON. You can specify a different location from which to load data like so: 101 | 102 | .. code-block:: python 103 | 104 | @app.route("/register") 105 | @use_args(user_args, location="form") 106 | def register(args): 107 | return "registration page" 108 | 109 | Available locations include: 110 | 111 | - ``'querystring'`` (same as ``'query'``) 112 | - ``'json'`` 113 | - ``'form'`` 114 | - ``'headers'`` 115 | - ``'cookies'`` 116 | - ``'files'`` 117 | 118 | Validation 119 | ---------- 120 | 121 | Each :class:`Field ` object can be validated individually by passing the ``validate`` argument. 122 | 123 | .. code-block:: python 124 | 125 | from webargs import fields 126 | 127 | args = {"age": fields.Int(validate=lambda val: val > 0)} 128 | 129 | The validator may return either a `boolean` or raise a :exc:`ValidationError `. 130 | 131 | .. code-block:: python 132 | 133 | from webargs import fields, ValidationError 134 | 135 | 136 | def must_exist_in_db(val): 137 | if not User.query.get(val): 138 | # Optionally pass a status_code 139 | raise ValidationError("User does not exist") 140 | 141 | 142 | args = {"id": fields.Int(validate=must_exist_in_db)} 143 | 144 | .. note:: 145 | 146 | If a validator returns ``None``, validation will pass. A validator must return ``False`` or raise a `ValidationError ` 147 | for validation to fail. 148 | 149 | 150 | There are a number of built-in validators from `marshmallow.validate ` 151 | (re-exported as `webargs.validate`). 152 | 153 | .. code-block:: python 154 | 155 | from webargs import fields, validate 156 | 157 | args = { 158 | "name": fields.Str(required=True, validate=[validate.Length(min=1, max=9999)]), 159 | "age": fields.Int(validate=[validate.Range(min=1, max=999)]), 160 | } 161 | 162 | The full arguments dictionary can also be validated by passing ``validate`` to :meth:`Parser.parse `, :meth:`Parser.use_args `, :meth:`Parser.use_kwargs `. 163 | 164 | 165 | .. code-block:: python 166 | 167 | from webargs import fields 168 | from webargs.flaskparser import parser 169 | 170 | argmap = {"age": fields.Int(), "years_employed": fields.Int()} 171 | 172 | # ... 173 | result = parser.parse( 174 | argmap, validate=lambda args: args["years_employed"] < args["age"] 175 | ) 176 | 177 | 178 | Error Handling 179 | -------------- 180 | 181 | Each parser has a default error handling method. To override the error handling callback, write a function that 182 | receives an error, the request, the `marshmallow.Schema` instance, status code, and headers. 183 | Then decorate that function with :func:`Parser.error_handler `. 184 | 185 | .. code-block:: python 186 | 187 | from webargs.flaskparser import parser 188 | 189 | 190 | class CustomError(Exception): 191 | pass 192 | 193 | 194 | @parser.error_handler 195 | def handle_error(error, req, schema, *, error_status_code, error_headers): 196 | raise CustomError(error.messages) 197 | 198 | Parsing Lists in Query Strings 199 | ------------------------------ 200 | 201 | Use `fields.DelimitedList ` to parse comma-separated 202 | lists in query parameters, e.g. ``/?permissions=read,write`` 203 | 204 | .. code-block:: python 205 | 206 | from webargs import fields 207 | 208 | args = {"permissions": fields.DelimitedList(fields.Str())} 209 | 210 | If you expect repeated query parameters, e.g. ``/?repo=webargs&repo=marshmallow``, use 211 | `fields.List ` instead. 212 | 213 | .. code-block:: python 214 | 215 | from webargs import fields 216 | 217 | args = {"repo": fields.List(fields.Str())} 218 | 219 | Nesting Fields 220 | -------------- 221 | 222 | :class:`Field ` dictionaries can be nested within each other. This can be useful for validating nested data. 223 | 224 | .. code-block:: python 225 | 226 | from webargs import fields 227 | 228 | args = { 229 | "name": fields.Nested( 230 | {"first": fields.Str(required=True), "last": fields.Str(required=True)} 231 | ) 232 | } 233 | 234 | .. note:: 235 | 236 | Of the default supported locations in webargs, only the ``json`` request location supports nested datastructures. You can, however, :ref:`implement your own data loader ` to add nested field functionality to the other locations. 237 | 238 | Next Steps 239 | ---------- 240 | 241 | - Go on to :doc:`Advanced Usage ` to learn how to add custom location handlers, use marshmallow Schemas, and more. 242 | - See the :doc:`Framework Support ` page for framework-specific guides. 243 | - For example applications, check out the `examples `_ directory. 244 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marshmallow-code/webargs/a59bfca9c9c88e0cbaa97eb2cdba3683e8cc33a1/examples/__init__.py -------------------------------------------------------------------------------- /examples/aiohttp_example.py: -------------------------------------------------------------------------------- 1 | """A simple number and datetime addition JSON API. 2 | Run the app: 3 | 4 | $ python examples/aiohttp_example.py 5 | 6 | Try the following with httpie (a cURL-like utility, http://httpie.org): 7 | 8 | $ pip install httpie 9 | $ http GET :5001/ 10 | $ http GET :5001/ name==Ada 11 | $ http POST :5001/add x=40 y=2 12 | $ http POST :5001/dateadd value=1973-04-10 addend=63 13 | $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes 14 | """ 15 | 16 | import asyncio 17 | import datetime as dt 18 | 19 | from aiohttp import web 20 | from aiohttp.web import json_response 21 | 22 | from webargs import fields, validate 23 | from webargs.aiohttpparser import use_args, use_kwargs 24 | 25 | hello_args = {"name": fields.Str(load_default="Friend")} 26 | 27 | 28 | @use_args(hello_args) 29 | async def index(request, args): 30 | """A welcome page.""" 31 | return json_response({"message": "Welcome, {}!".format(args["name"])}) 32 | 33 | 34 | add_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} 35 | 36 | 37 | @use_kwargs(add_args) 38 | async def add(request, x, y): 39 | """An addition endpoint.""" 40 | return json_response({"result": x + y}) 41 | 42 | 43 | dateadd_args = { 44 | "value": fields.Date(required=False), 45 | "addend": fields.Int(required=True, validate=validate.Range(min=1)), 46 | "unit": fields.Str( 47 | load_default="days", validate=validate.OneOf(["minutes", "days"]) 48 | ), 49 | } 50 | 51 | 52 | @use_kwargs(dateadd_args) 53 | async def dateadd(request, value, addend, unit): 54 | """A datetime adder endpoint.""" 55 | value = value or dt.datetime.utcnow() 56 | if unit == "minutes": 57 | delta = dt.timedelta(minutes=addend) 58 | else: 59 | delta = dt.timedelta(days=addend) 60 | result = value + delta 61 | return json_response({"result": result.isoformat()}) 62 | 63 | 64 | def create_app(): 65 | app = web.Application() 66 | app.router.add_route("GET", "/", index) 67 | app.router.add_route("POST", "/add", add) 68 | app.router.add_route("POST", "/dateadd", dateadd) 69 | return app 70 | 71 | 72 | def run(app, port=5001): 73 | loop = asyncio.get_event_loop() 74 | handler = app.make_handler() 75 | f = loop.create_server(handler, "0.0.0.0", port) 76 | srv = loop.run_until_complete(f) 77 | print("serving on", srv.sockets[0].getsockname()) 78 | try: 79 | loop.run_forever() 80 | except KeyboardInterrupt: 81 | pass 82 | finally: 83 | loop.run_until_complete(handler.finish_connections(1.0)) 84 | srv.close() 85 | loop.run_until_complete(srv.wait_closed()) 86 | loop.run_until_complete(app.finish()) 87 | loop.close() 88 | 89 | 90 | if __name__ == "__main__": 91 | app = create_app() 92 | run(app) 93 | -------------------------------------------------------------------------------- /examples/annotations_example.py: -------------------------------------------------------------------------------- 1 | """Example of using Python 3 function annotations to define 2 | request arguments and output schemas. 3 | 4 | Run the app: 5 | 6 | $ python examples/annotations_example.py 7 | 8 | Try the following with httpie (a cURL-like utility, http://httpie.org): 9 | 10 | $ pip install httpie 11 | $ http GET :5001/ 12 | $ http GET :5001/ name==Ada 13 | $ http POST :5001/add x=40 y=2 14 | $ http GET :5001/users/42 15 | """ 16 | 17 | import functools 18 | import random 19 | 20 | from flask import Flask, request 21 | from marshmallow import Schema 22 | 23 | from webargs import fields 24 | from webargs.flaskparser import parser 25 | 26 | app = Flask(__name__) 27 | 28 | ##### Routing wrapper #### 29 | 30 | 31 | def route(*args, **kwargs): 32 | """Combines `Flask.route` and webargs parsing. Allows arguments to be specified 33 | as function annotations. An output schema can optionally be specified by a 34 | return annotation. 35 | """ 36 | 37 | def decorator(func): 38 | @app.route(*args, **kwargs) 39 | @functools.wraps(func) 40 | def wrapped_view(*a, **kw): 41 | annotations = getattr(func, "__annotations__", {}) 42 | reqargs = { 43 | name: value 44 | for name, value in annotations.items() 45 | if isinstance(value, fields.Field) and name != "return" 46 | } 47 | response_schema = annotations.get("return") 48 | schema_cls = Schema.from_dict(reqargs) 49 | partial = request.method != "POST" 50 | parsed = parser.parse(schema_cls(partial=partial), request) 51 | kw.update(parsed) 52 | response_data = func(*a, **kw) 53 | if response_schema: 54 | return response_schema.dump(response_data) 55 | else: 56 | return func(*a, **kw) 57 | 58 | return wrapped_view 59 | 60 | return decorator 61 | 62 | 63 | ##### Fake database and model ##### 64 | 65 | 66 | class Model: 67 | def __init__(self, **kwargs): 68 | self.__dict__.update(kwargs) 69 | 70 | def update(self, **kwargs): 71 | self.__dict__.update(kwargs) 72 | 73 | @classmethod 74 | def insert(cls, db, **kwargs): 75 | collection = db[cls.collection] 76 | new_id = None 77 | if "id" in kwargs: # for setting up fixtures 78 | new_id = kwargs.pop("id") 79 | else: # find a new id 80 | found_id = False 81 | while not found_id: 82 | new_id = random.randint(1, 9999) 83 | if new_id not in collection: 84 | found_id = True 85 | new_record = cls(id=new_id, **kwargs) 86 | collection[new_id] = new_record 87 | return new_record 88 | 89 | 90 | class User(Model): 91 | collection = "users" 92 | 93 | 94 | db = {"users": {}} 95 | 96 | ##### Views ##### 97 | 98 | 99 | @route("/", methods=["GET"]) 100 | def index(name: fields.Str(load_default="Friend")): # noqa: F821 101 | return {"message": f"Hello, {name}!"} 102 | 103 | 104 | @route("/add", methods=["POST"]) 105 | def add(x: fields.Float(required=True), y: fields.Float(required=True)): 106 | return {"result": x + y} 107 | 108 | 109 | class UserSchema(Schema): 110 | id = fields.Int(dump_only=True) 111 | username = fields.Str(required=True) 112 | first_name = fields.Str() 113 | last_name = fields.Str() 114 | 115 | 116 | @route("/users/", methods=["GET", "PATCH"]) 117 | def user_detail(user_id, username: fields.Str(required=True) = None) -> UserSchema(): 118 | user = db["users"].get(user_id) 119 | if not user: 120 | return {"message": "User not found"}, 404 121 | if request.method == "PATCH": 122 | user.update(username=username) 123 | return user 124 | 125 | 126 | # Return validation errors as JSON 127 | @app.errorhandler(422) 128 | @app.errorhandler(400) 129 | def handle_error(err): 130 | headers = err.data.get("headers", None) 131 | messages = err.data.get("messages", ["Invalid request."]) 132 | if headers: 133 | return {"errors": messages}, err.code, headers 134 | else: 135 | return {"errors": messages}, err.code 136 | 137 | 138 | if __name__ == "__main__": 139 | User.insert( 140 | db=db, id=42, username="fred", first_name="Freddie", last_name="Mercury" 141 | ) 142 | app.run(port=5001, debug=True) 143 | -------------------------------------------------------------------------------- /examples/bottle_example.py: -------------------------------------------------------------------------------- 1 | """A simple number and datetime addition JSON API. 2 | Run the app: 3 | 4 | $ python examples/bottle_example.py 5 | 6 | Try the following with httpie (a cURL-like utility, http://httpie.org): 7 | 8 | $ pip install httpie 9 | $ http GET :5001/ 10 | $ http GET :5001/ name==Ada 11 | $ http POST :5001/add x=40 y=2 12 | $ http POST :5001/dateadd value=1973-04-10 addend=63 13 | $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes 14 | """ 15 | 16 | import datetime as dt 17 | 18 | from bottle import error, response, route, run 19 | 20 | from webargs import fields, validate 21 | from webargs.bottleparser import use_args, use_kwargs 22 | 23 | hello_args = {"name": fields.Str(load_default="Friend")} 24 | 25 | 26 | @route("/", method="GET", apply=use_args(hello_args)) 27 | def index(args): 28 | """A welcome page.""" 29 | return {"message": "Welcome, {}!".format(args["name"])} 30 | 31 | 32 | add_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} 33 | 34 | 35 | @route("/add", method="POST", apply=use_kwargs(add_args)) 36 | def add(x, y): 37 | """An addition endpoint.""" 38 | return {"result": x + y} 39 | 40 | 41 | dateadd_args = { 42 | "value": fields.Date(required=False), 43 | "addend": fields.Int(required=True, validate=validate.Range(min=1)), 44 | "unit": fields.Str( 45 | load_default="days", validate=validate.OneOf(["minutes", "days"]) 46 | ), 47 | } 48 | 49 | 50 | @route("/dateadd", method="POST", apply=use_kwargs(dateadd_args)) 51 | def dateadd(value, addend, unit): 52 | """A date adder endpoint.""" 53 | value = value or dt.datetime.utcnow() 54 | if unit == "minutes": 55 | delta = dt.timedelta(minutes=addend) 56 | else: 57 | delta = dt.timedelta(days=addend) 58 | result = value + delta 59 | return {"result": result.isoformat()} 60 | 61 | 62 | # Return validation errors as JSON 63 | @error(400) 64 | @error(422) 65 | def handle_error(err): 66 | response.content_type = "application/json" 67 | return err.body 68 | 69 | 70 | if __name__ == "__main__": 71 | run(port=5001, reloader=True, debug=True) 72 | -------------------------------------------------------------------------------- /examples/falcon_example.py: -------------------------------------------------------------------------------- 1 | """A simple number and datetime addition JSON API. 2 | Demonstrates different strategies for parsing arguments 3 | with the FalconParser. 4 | 5 | Run the app: 6 | 7 | $ pip install gunicorn 8 | $ gunicorn examples.falcon_example:app 9 | 10 | Try the following with httpie (a cURL-like utility, http://httpie.org): 11 | 12 | $ pip install httpie 13 | $ http GET :8000/ 14 | $ http GET :8000/ name==Ada 15 | $ http POST :8000/add x=40 y=2 16 | $ http POST :8000/dateadd value=1973-04-10 addend=63 17 | $ http POST :8000/dateadd value=2014-10-23 addend=525600 unit=minutes 18 | """ 19 | 20 | import datetime as dt 21 | 22 | import falcon 23 | 24 | from webargs import fields, validate 25 | from webargs.core import json 26 | from webargs.falconparser import parser, use_args, use_kwargs 27 | 28 | ### Middleware and hooks ### 29 | 30 | 31 | class JSONTranslator: 32 | def process_response(self, req, resp, resource): 33 | if "result" not in req.context: 34 | return 35 | resp.body = json.dumps(req.context["result"]) 36 | 37 | 38 | def add_args(argmap, **kwargs): 39 | def hook(req, resp, params): 40 | req.context["args"] = parser.parse(argmap, req=req, **kwargs) 41 | 42 | return hook 43 | 44 | 45 | ### Resources ### 46 | 47 | 48 | class HelloResource: 49 | """A welcome page.""" 50 | 51 | hello_args = {"name": fields.Str(load_default="Friend", location="query")} 52 | 53 | @use_args(hello_args) 54 | def on_get(self, req, resp, args): 55 | req.context["result"] = {"message": "Welcome, {}!".format(args["name"])} 56 | 57 | 58 | class AdderResource: 59 | """An addition endpoint.""" 60 | 61 | adder_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} 62 | 63 | @use_kwargs(adder_args) 64 | def on_post(self, req, resp, x, y): 65 | req.context["result"] = {"result": x + y} 66 | 67 | 68 | class DateAddResource: 69 | """A datetime adder endpoint.""" 70 | 71 | dateadd_args = { 72 | "value": fields.Date(required=False), 73 | "addend": fields.Int(required=True, validate=validate.Range(min=1)), 74 | "unit": fields.Str( 75 | load_default="days", validate=validate.OneOf(["minutes", "days"]) 76 | ), 77 | } 78 | 79 | @falcon.before(add_args(dateadd_args)) 80 | def on_post(self, req, resp): 81 | """A datetime adder endpoint.""" 82 | args = req.context["args"] 83 | value = args["value"] or dt.datetime.utcnow() 84 | if args["unit"] == "minutes": 85 | delta = dt.timedelta(minutes=args["addend"]) 86 | else: 87 | delta = dt.timedelta(days=args["addend"]) 88 | result = value + delta 89 | req.context["result"] = {"result": result.isoformat()} 90 | 91 | 92 | app = falcon.API(middleware=[JSONTranslator()]) 93 | app.add_route("/", HelloResource()) 94 | app.add_route("/add", AdderResource()) 95 | app.add_route("/dateadd", DateAddResource()) 96 | -------------------------------------------------------------------------------- /examples/flask_example.py: -------------------------------------------------------------------------------- 1 | """A simple number and datetime addition JSON API. 2 | Run the app: 3 | 4 | $ python examples/flask_example.py 5 | 6 | Try the following with httpie (a cURL-like utility, http://httpie.org): 7 | 8 | $ pip install httpie 9 | $ http GET :5001/ 10 | $ http GET :5001/ name==Ada 11 | $ http POST :5001/add x=40 y=2 12 | $ http POST :5001/subtract x=40 y=2 13 | $ http POST :5001/dateadd value=1973-04-10 addend=63 14 | $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes 15 | """ 16 | 17 | import datetime as dt 18 | 19 | from flask import Flask, jsonify 20 | 21 | from webargs import fields, validate 22 | from webargs.flaskparser import use_args, use_kwargs 23 | 24 | app = Flask(__name__) 25 | 26 | hello_args = {"name": fields.Str(load_default="Friend")} 27 | 28 | 29 | @app.route("/", methods=["GET"]) 30 | @use_args(hello_args) 31 | def index(args): 32 | """A welcome page.""" 33 | return jsonify({"message": "Welcome, {}!".format(args["name"])}) 34 | 35 | 36 | add_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} 37 | 38 | 39 | @app.route("/add", methods=["POST"]) 40 | @use_kwargs(add_args) 41 | def add(x, y): 42 | """An addition endpoint.""" 43 | return jsonify({"result": x + y}) 44 | 45 | 46 | @app.route("/subtract", methods=["POST"]) 47 | @use_kwargs(add_args) 48 | async def subtract(x, y): 49 | """An async subtraction endpoint.""" 50 | return jsonify({"result": x - y}) 51 | 52 | 53 | dateadd_args = { 54 | "value": fields.Date(required=False), 55 | "addend": fields.Int(required=True, validate=validate.Range(min=1)), 56 | "unit": fields.Str( 57 | load_default="days", validate=validate.OneOf(["minutes", "days"]) 58 | ), 59 | } 60 | 61 | 62 | @app.route("/dateadd", methods=["POST"]) 63 | @use_kwargs(dateadd_args) 64 | def dateadd(value, addend, unit): 65 | """A date adder endpoint.""" 66 | value = value or dt.datetime.utcnow() 67 | if unit == "minutes": 68 | delta = dt.timedelta(minutes=addend) 69 | else: 70 | delta = dt.timedelta(days=addend) 71 | result = value + delta 72 | return jsonify({"result": result.isoformat()}) 73 | 74 | 75 | # Return validation errors as JSON 76 | @app.errorhandler(422) 77 | @app.errorhandler(400) 78 | def handle_error(err): 79 | headers = err.data.get("headers", None) 80 | messages = err.data.get("messages", ["Invalid request."]) 81 | if headers: 82 | return jsonify({"errors": messages}), err.code, headers 83 | else: 84 | return jsonify({"errors": messages}), err.code 85 | 86 | 87 | if __name__ == "__main__": 88 | app.run(port=5001, debug=True) 89 | -------------------------------------------------------------------------------- /examples/flaskrestful_example.py: -------------------------------------------------------------------------------- 1 | """A simple number and datetime addition JSON API. 2 | Run the app: 3 | 4 | $ python examples/flaskrestful_example.py 5 | 6 | Try the following with httpie (a cURL-like utility, http://httpie.org): 7 | 8 | $ pip install httpie 9 | $ http GET :5001/ 10 | $ http GET :5001/ name==Ada 11 | $ http POST :5001/add x=40 y=2 12 | $ http POST :5001/dateadd value=1973-04-10 addend=63 13 | $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes 14 | """ 15 | 16 | import datetime as dt 17 | 18 | from flask import Flask 19 | from flask_restful import Api, Resource 20 | 21 | from webargs import fields, validate 22 | from webargs.flaskparser import abort, parser, use_args, use_kwargs 23 | 24 | app = Flask(__name__) 25 | api = Api(app) 26 | 27 | 28 | class IndexResource(Resource): 29 | """A welcome page.""" 30 | 31 | hello_args = {"name": fields.Str(load_default="Friend")} 32 | 33 | @use_args(hello_args) 34 | def get(self, args): 35 | return {"message": "Welcome, {}!".format(args["name"])} 36 | 37 | 38 | class AddResource(Resource): 39 | """An addition endpoint.""" 40 | 41 | add_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} 42 | 43 | @use_kwargs(add_args) 44 | def post(self, x, y): 45 | """An addition endpoint.""" 46 | return {"result": x + y} 47 | 48 | 49 | class DateAddResource(Resource): 50 | dateadd_args = { 51 | "value": fields.Date(required=False), 52 | "addend": fields.Int(required=True, validate=validate.Range(min=1)), 53 | "unit": fields.Str( 54 | load_default="days", validate=validate.OneOf(["minutes", "days"]) 55 | ), 56 | } 57 | 58 | @use_kwargs(dateadd_args) 59 | def post(self, value, addend, unit): 60 | """A date adder endpoint.""" 61 | value = value or dt.datetime.utcnow() 62 | if unit == "minutes": 63 | delta = dt.timedelta(minutes=addend) 64 | else: 65 | delta = dt.timedelta(days=addend) 66 | result = value + delta 67 | return {"result": result.isoformat()} 68 | 69 | 70 | # This error handler is necessary for usage with Flask-RESTful 71 | @parser.error_handler 72 | def handle_request_parsing_error(err, req, schema, *, error_status_code, error_headers): 73 | """webargs error handler that uses Flask-RESTful's abort function to return 74 | a JSON error response to the client. 75 | """ 76 | abort(error_status_code, errors=err.messages) 77 | 78 | 79 | if __name__ == "__main__": 80 | api.add_resource(IndexResource, "/") 81 | api.add_resource(AddResource, "/add") 82 | api.add_resource(DateAddResource, "/dateadd") 83 | app.run(port=5001, debug=True) 84 | -------------------------------------------------------------------------------- /examples/pyramid_example.py: -------------------------------------------------------------------------------- 1 | """A simple number and datetime addition JSON API. 2 | Run the app: 3 | 4 | $ python examples/pyramid_example.py 5 | 6 | Try the following with httpie (a cURL-like utility, http://httpie.org): 7 | 8 | $ pip install httpie 9 | $ http GET :5001/ 10 | $ http GET :5001/ name==Ada 11 | $ http POST :5001/add x=40 y=2 12 | $ http POST :5001/dateadd value=1973-04-10 addend=63 13 | $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes 14 | """ 15 | 16 | import datetime as dt 17 | from wsgiref.simple_server import make_server 18 | 19 | from pyramid.config import Configurator 20 | from pyramid.renderers import JSON 21 | from pyramid.view import view_config 22 | 23 | from webargs import fields, validate 24 | from webargs.pyramidparser import use_args, use_kwargs 25 | 26 | hello_args = {"name": fields.Str(load_default="Friend")} 27 | 28 | 29 | @view_config(route_name="hello", request_method="GET", renderer="json") 30 | @use_args(hello_args) 31 | def index(request, args): 32 | """A welcome page.""" 33 | return {"message": "Welcome, {}!".format(args["name"])} 34 | 35 | 36 | add_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} 37 | 38 | 39 | @view_config(route_name="add", request_method="POST", renderer="json") 40 | @use_kwargs(add_args) 41 | def add(request, x, y): 42 | """An addition endpoint.""" 43 | return {"result": x + y} 44 | 45 | 46 | dateadd_args = { 47 | "value": fields.Date(required=False), 48 | "addend": fields.Int(required=True, validate=validate.Range(min=1)), 49 | "unit": fields.Str( 50 | load_default="days", validate=validate.OneOf(["minutes", "days"]) 51 | ), 52 | } 53 | 54 | 55 | @view_config(route_name="dateadd", request_method="POST", renderer="json") 56 | @use_kwargs(dateadd_args) 57 | def dateadd(request, value, addend, unit): 58 | """A date adder endpoint.""" 59 | value = value or dt.datetime.utcnow() 60 | if unit == "minutes": 61 | delta = dt.timedelta(minutes=addend) 62 | else: 63 | delta = dt.timedelta(days=addend) 64 | result = value + delta 65 | return {"result": result} 66 | 67 | 68 | if __name__ == "__main__": 69 | config = Configurator() 70 | 71 | json_renderer = JSON() 72 | json_renderer.add_adapter(dt.datetime, lambda v, request: v.isoformat()) 73 | config.add_renderer("json", json_renderer) 74 | 75 | config.add_route("hello", "/") 76 | config.add_route("add", "/add") 77 | config.add_route("dateadd", "/dateadd") 78 | config.scan(__name__) 79 | app = config.make_wsgi_app() 80 | port = 5001 81 | server = make_server("0.0.0.0", port, app) 82 | print(f"Serving on port {port}") 83 | server.serve_forever() 84 | -------------------------------------------------------------------------------- /examples/requirements.txt: -------------------------------------------------------------------------------- 1 | python-dateutil==2.9.0.post0 2 | Flask 3 | bottle 4 | tornado 5 | flask-restful 6 | pyramid 7 | -------------------------------------------------------------------------------- /examples/schema_example.py: -------------------------------------------------------------------------------- 1 | """Example implementation of using a marshmallow Schema for both request input 2 | and output with a `use_schema` decorator. 3 | Run the app: 4 | 5 | $ python examples/schema_example.py 6 | 7 | Try the following with httpie (a cURL-like utility, http://httpie.org): 8 | 9 | $ pip install httpie 10 | $ http GET :5001/users/ 11 | $ http GET :5001/users/42 12 | $ http POST :5001/users/ username=brian first_name=Brian last_name=May 13 | $ http PATCH :5001/users/42 username=freddie 14 | $ http GET :5001/users/ limit==1 15 | """ 16 | 17 | import functools 18 | import random 19 | 20 | from flask import Flask, request 21 | from marshmallow import Schema, fields, post_dump 22 | 23 | from webargs.flaskparser import parser, use_kwargs 24 | 25 | app = Flask(__name__) 26 | 27 | ##### Fake database and model ##### 28 | 29 | 30 | class Model: 31 | def __init__(self, **kwargs): 32 | self.__dict__.update(kwargs) 33 | 34 | def update(self, **kwargs): 35 | self.__dict__.update(kwargs) 36 | 37 | @classmethod 38 | def insert(cls, db, **kwargs): 39 | collection = db[cls.collection] 40 | new_id = None 41 | if "id" in kwargs: # for setting up fixtures 42 | new_id = kwargs.pop("id") 43 | else: # find a new id 44 | found_id = False 45 | while not found_id: 46 | new_id = random.randint(1, 9999) 47 | if new_id not in collection: 48 | found_id = True 49 | new_record = cls(id=new_id, **kwargs) 50 | collection[new_id] = new_record 51 | return new_record 52 | 53 | 54 | class User(Model): 55 | collection = "users" 56 | 57 | 58 | db = {"users": {}} 59 | 60 | 61 | ##### use_schema ##### 62 | 63 | 64 | def use_schema(schema_cls, list_view=False, locations=None): 65 | """View decorator for using a marshmallow schema to 66 | (1) parse a request's input and 67 | (2) serializing the view's output to a JSON response. 68 | """ 69 | 70 | def decorator(func): 71 | @functools.wraps(func) 72 | def wrapped(*args, **kwargs): 73 | partial = request.method != "POST" 74 | schema = schema_cls(partial=partial) 75 | use_args_wrapper = parser.use_args(schema, locations=locations) 76 | # Function wrapped with use_args 77 | func_with_args = use_args_wrapper(func) 78 | ret = func_with_args(*args, **kwargs) 79 | 80 | # support (json, status) tuples 81 | if isinstance(ret, tuple) and len(ret) == 2 and isinstance(ret[1], int): 82 | return schema.dump(ret[0], many=list_view), ret[1] 83 | 84 | return schema.dump(ret, many=list_view) 85 | 86 | return wrapped 87 | 88 | return decorator 89 | 90 | 91 | ##### Schemas ##### 92 | 93 | 94 | class UserSchema(Schema): 95 | id = fields.Int(dump_only=True) 96 | username = fields.Str(required=True) 97 | first_name = fields.Str() 98 | last_name = fields.Str() 99 | 100 | @post_dump(pass_many=True) 101 | def wrap_with_envelope(self, data, many, **kwargs): 102 | return {"data": data} 103 | 104 | 105 | ##### Routes ##### 106 | 107 | 108 | @app.route("/users/", methods=["GET", "PATCH"]) 109 | @use_schema(UserSchema) 110 | def user_detail(reqargs, user_id): 111 | user = db["users"].get(user_id) 112 | if not user: 113 | return {"message": "User not found"}, 404 114 | if request.method == "PATCH" and reqargs: 115 | user.update(**reqargs) 116 | return user 117 | 118 | 119 | # You can add additional arguments with use_kwargs 120 | @app.route("/users/", methods=["GET", "POST"]) 121 | @use_kwargs({"limit": fields.Int(load_default=10, location="query")}) 122 | @use_schema(UserSchema, list_view=True) 123 | def user_list(reqargs, limit): 124 | users = db["users"].values() 125 | if request.method == "POST": 126 | User.insert(db=db, **reqargs) 127 | return list(users)[:limit] 128 | 129 | 130 | # Return validation errors as JSON 131 | @app.errorhandler(422) 132 | @app.errorhandler(400) 133 | def handle_validation_error(err): 134 | exc = getattr(err, "exc", None) 135 | if exc: 136 | headers = err.data["headers"] 137 | messages = exc.messages 138 | else: 139 | headers = None 140 | messages = ["Invalid request."] 141 | if headers: 142 | return {"errors": messages}, err.code, headers 143 | else: 144 | return {"errors": messages}, err.code 145 | 146 | 147 | if __name__ == "__main__": 148 | User.insert( 149 | db=db, id=42, username="fred", first_name="Freddie", last_name="Mercury" 150 | ) 151 | app.run(port=5001, debug=True) 152 | -------------------------------------------------------------------------------- /examples/tornado_example.py: -------------------------------------------------------------------------------- 1 | """A simple number and datetime addition JSON API. 2 | Run the app: 3 | 4 | $ python examples/tornado_example.py 5 | 6 | Try the following with httpie (a cURL-like utility, http://httpie.org): 7 | 8 | $ pip install httpie 9 | $ http GET :5001/ 10 | $ http GET :5001/ name==Ada 11 | $ http POST :5001/add x=40 y=2 12 | $ http POST :5001/dateadd value=1973-04-10 addend=63 13 | $ http POST :5001/dateadd value=2014-10-23 addend=525600 unit=minutes 14 | """ 15 | 16 | import datetime as dt 17 | 18 | import tornado.ioloop 19 | from tornado.web import RequestHandler 20 | 21 | from webargs import fields, validate 22 | from webargs.tornadoparser import use_args, use_kwargs 23 | 24 | 25 | class BaseRequestHandler(RequestHandler): 26 | def write_error(self, status_code, **kwargs): 27 | """Write errors as JSON.""" 28 | self.set_header("Content-Type", "application/json") 29 | if "exc_info" in kwargs: 30 | etype, exc, traceback = kwargs["exc_info"] 31 | if hasattr(exc, "messages"): 32 | self.write({"errors": exc.messages}) 33 | if getattr(exc, "headers", None): 34 | for name, val in exc.headers.items(): 35 | self.set_header(name, val) 36 | self.finish() 37 | 38 | 39 | class HelloHandler(BaseRequestHandler): 40 | """A welcome page.""" 41 | 42 | hello_args = {"name": fields.Str(load_default="Friend")} 43 | 44 | @use_args(hello_args) 45 | def get(self, args): 46 | response = {"message": "Welcome, {}!".format(args["name"])} 47 | self.write(response) 48 | 49 | 50 | class AdderHandler(BaseRequestHandler): 51 | """An addition endpoint.""" 52 | 53 | add_args = {"x": fields.Float(required=True), "y": fields.Float(required=True)} 54 | 55 | @use_kwargs(add_args) 56 | def post(self, x, y): 57 | self.write({"result": x + y}) 58 | 59 | 60 | class DateAddHandler(BaseRequestHandler): 61 | """A date adder endpoint.""" 62 | 63 | dateadd_args = { 64 | "value": fields.Date(required=False), 65 | "addend": fields.Int(required=True, validate=validate.Range(min=1)), 66 | "unit": fields.Str( 67 | load_default="days", validate=validate.OneOf(["minutes", "days"]) 68 | ), 69 | } 70 | 71 | @use_kwargs(dateadd_args) 72 | def post(self, value, addend, unit): 73 | """A date adder endpoint.""" 74 | value = value or dt.datetime.utcnow() 75 | if unit == "minutes": 76 | delta = dt.timedelta(minutes=addend) 77 | else: 78 | delta = dt.timedelta(days=addend) 79 | result = value + delta 80 | self.write({"result": result.isoformat()}) 81 | 82 | 83 | if __name__ == "__main__": 84 | app = tornado.web.Application( 85 | [(r"/", HelloHandler), (r"/add", AdderHandler), (r"/dateadd", DateAddHandler)], 86 | debug=True, 87 | ) 88 | port = 5001 89 | app.listen(port) 90 | print(f"Serving on port {port}") 91 | tornado.ioloop.IOLoop.instance().start() 92 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "webargs" 3 | version = "8.7.0" 4 | description = "Declarative parsing and validation of HTTP request objects, with built-in support for popular web frameworks, including Flask, Django, Bottle, Tornado, Pyramid, Falcon, and aiohttp." 5 | readme = "README.rst" 6 | license = { file = "LICENSE" } 7 | authors = [{ name = "Steven Loria", email = "sloria1@gmail.com" }] 8 | maintainers = [ 9 | { name = "Steven Loria", email = "sloria1@gmail.com" }, 10 | { name = "Jérôme Lafréchoux", email = "jerome@jolimont.fr" }, 11 | { name = "Stephen Rosen", email = "sirosen0@gmail.com" }, 12 | ] 13 | classifiers = [ 14 | "Development Status :: 5 - Production/Stable", 15 | "Intended Audience :: Developers", 16 | "License :: OSI Approved :: MIT License", 17 | "Natural Language :: English", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.9", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.11", 22 | "Programming Language :: Python :: 3.12", 23 | "Programming Language :: Python :: 3.13", 24 | "Topic :: Internet :: WWW/HTTP :: Dynamic Content", 25 | "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", 26 | ] 27 | keywords = [ 28 | "webargs", 29 | "http", 30 | "flask", 31 | "django", 32 | "bottle", 33 | "tornado", 34 | "aiohttp", 35 | "request", 36 | "arguments", 37 | "validation", 38 | "parameters", 39 | "rest", 40 | "api", 41 | "marshmallow", 42 | ] 43 | requires-python = ">=3.9" 44 | dependencies = [ 45 | "marshmallow>=3.13.0,<5.0.0", 46 | "packaging>=17.0", 47 | # depend on typing-extensions conditionally for annotations 48 | 'typing_extensions>=4.0; python_version<"3.10"', 49 | ] 50 | 51 | [project.urls] 52 | Changelog = "https://webargs.readthedocs.io/en/latest/changelog.html" 53 | Funding = "https://opencollective.com/marshmallow" 54 | Issues = "https://github.com/marshmallow-code/webargs/issues" 55 | Source = "https://github.com/marshmallow-code/webargs" 56 | Tidelift = "https://tidelift.com/subscription/pkg/pypi-webargs?utm_source=pypi-webargs&utm_medium=pypi" 57 | 58 | [project.optional-dependencies] 59 | frameworks = [ 60 | "Flask>=0.12.5", 61 | "Django>=2.2.0", 62 | "bottle>=0.12.13", 63 | "tornado>=4.5.2", 64 | "pyramid>=1.9.1", 65 | "falcon>=2.0.0", 66 | "aiohttp>=3.0.8", 67 | ] 68 | tests = [ 69 | "webargs[frameworks]", 70 | "pytest", 71 | "pytest-asyncio", 72 | "webtest==3.0.4", 73 | "webtest-aiohttp==2.0.0", 74 | "pytest-aiohttp>=0.3.0", 75 | "packaging>=17.0", 76 | ] 77 | docs = [ 78 | "webargs[frameworks]", 79 | "Sphinx==8.2.3", 80 | "sphinx-issues==5.0.1", 81 | "furo==2024.8.6", 82 | ] 83 | dev = ["webargs[tests]", "tox", "pre-commit>=3.5,<5.0"] 84 | 85 | [build-system] 86 | requires = ["flit_core<4"] 87 | build-backend = "flit_core.buildapi" 88 | 89 | [tool.flit.sdist] 90 | include = [ 91 | "docs/", 92 | "tests/", 93 | "CHANGELOG.rst", 94 | "CONTRIBUTING.rst", 95 | "SECURITY.md", 96 | "NOTICE", 97 | "tox.ini", 98 | ] 99 | exclude = ["docs/_build/"] 100 | 101 | [tool.ruff] 102 | src = ["src"] 103 | fix = true 104 | show-fixes = true 105 | output-format = "full" 106 | 107 | [tool.ruff.format] 108 | docstring-code-format = true 109 | 110 | [tool.ruff.lint] 111 | ignore = ["E203", "E266", "E501", "E731"] 112 | select = [ 113 | "B", # flake8-bugbear 114 | "E", # pycodestyle error 115 | "F", # pyflakes 116 | "I", # isort 117 | "UP", # pyupgrade 118 | "W", # pycodestyle warning 119 | ] 120 | 121 | [tool.pytest.ini_options] 122 | filterwarnings = [ 123 | # https://github.com/Pylons/pyramid/issues/3731 124 | "ignore:.*pkg_resources.*:DeprecationWarning", 125 | # https://github.com/Pylons/webob/issues/437 126 | "ignore:.*'cgi' is deprecated.*:DeprecationWarning", 127 | # https://github.com/sloria/webtest-aiohttp/issues/6 128 | "ignore:.*The object should be created within an async function.*:DeprecationWarning", 129 | ] 130 | 131 | [tool.mypy] 132 | ignore_missing_imports = true 133 | warn_unreachable = true 134 | warn_unused_ignores = true 135 | warn_redundant_casts = true 136 | # warn_return_any = true 137 | warn_no_return = true 138 | no_implicit_optional = true 139 | disallow_untyped_defs = true 140 | 141 | [[tool.mypy.overrides]] 142 | disallow_untyped_defs = false 143 | module = [ 144 | "webargs.fields", 145 | "webargs.testing", 146 | "webargs.aiohttpparser", 147 | "webargs.bottleparser", 148 | "webargs.djangoparser", 149 | "webargs.falconparser", 150 | "tests.*", 151 | ] 152 | -------------------------------------------------------------------------------- /src/webargs/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import importlib.metadata 4 | 5 | # Make marshmallow's validation functions importable from webargs 6 | from marshmallow import validate 7 | from marshmallow.utils import missing 8 | from packaging.version import Version 9 | 10 | from webargs import fields 11 | from webargs.core import ValidationError 12 | 13 | # TODO: Deprecate __version__ et al. 14 | __version__ = importlib.metadata.version("webargs") 15 | __parsed_version__ = Version(__version__) 16 | __version_info__: tuple[int, int, int] | tuple[int, int, int, str, int] = ( 17 | __parsed_version__.release # type: ignore[assignment] 18 | ) 19 | if __parsed_version__.pre: 20 | __version_info__ += __parsed_version__.pre # type: ignore[assignment] 21 | __all__ = ("ValidationError", "fields", "missing", "validate") 22 | -------------------------------------------------------------------------------- /src/webargs/_types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | import typing 5 | 6 | import marshmallow as ma 7 | 8 | if sys.version_info < (3, 10): 9 | from typing_extensions import TypeAlias 10 | else: 11 | from typing import TypeAlias 12 | 13 | 14 | T = typing.TypeVar("T") 15 | 16 | # an arg-map is one of the following 17 | # - a schema 18 | # - a schema class 19 | # - a str->Field mapping 20 | # - a `(Request) -> Schema` callable 21 | ArgMapCallable: TypeAlias = typing.Callable[[T], ma.Schema] 22 | ArgMap: TypeAlias = typing.Union[ 23 | ma.Schema, type[ma.Schema], typing.Mapping[str, ma.fields.Field], ArgMapCallable[T] 24 | ] 25 | 26 | # a 'validate' value is a callable or collection ofcallables 27 | ValidateArg: TypeAlias = typing.Union[ 28 | None, typing.Callable, typing.Iterable[typing.Callable] 29 | ] 30 | CallableList: TypeAlias = list[typing.Callable[..., typing.Any]] 31 | 32 | # error handlers are no-return callables 33 | ErrorHandler: TypeAlias = typing.Callable[..., typing.NoReturn] 34 | AsyncErrorHandler: TypeAlias = typing.Callable[..., typing.Awaitable[typing.NoReturn]] 35 | -------------------------------------------------------------------------------- /src/webargs/aiohttpparser.py: -------------------------------------------------------------------------------- 1 | """aiohttp request argument parsing module. 2 | 3 | Example: :: 4 | 5 | import asyncio 6 | from aiohttp import web 7 | 8 | from webargs import fields 9 | from webargs.aiohttpparser import use_args 10 | 11 | 12 | hello_args = {"name": fields.Str(required=True)} 13 | 14 | 15 | @asyncio.coroutine 16 | @use_args(hello_args) 17 | def index(request, args): 18 | return web.Response(body="Hello {}".format(args["name"]).encode("utf-8")) 19 | 20 | 21 | app = web.Application() 22 | app.router.add_route("GET", "/", index) 23 | """ 24 | 25 | from __future__ import annotations 26 | 27 | import typing 28 | 29 | from aiohttp import web, web_exceptions 30 | from marshmallow import RAISE, Schema, ValidationError 31 | 32 | from webargs import core 33 | from webargs.asyncparser import AsyncParser 34 | from webargs.core import json 35 | from webargs.multidictproxy import MultiDictProxy 36 | 37 | 38 | def is_json_request(req) -> bool: 39 | content_type = req.content_type 40 | return core.is_json(content_type) 41 | 42 | 43 | class HTTPUnprocessableEntity(web.HTTPClientError): 44 | status_code = 422 45 | 46 | 47 | # Mapping of status codes to exception classes 48 | # Adapted from werkzeug 49 | exception_map: dict[int, type[web_exceptions.HTTPException]] = {} 50 | exception_map[422] = HTTPUnprocessableEntity 51 | 52 | 53 | def _find_exceptions() -> None: 54 | for name in web_exceptions.__all__: 55 | obj = getattr(web_exceptions, name) 56 | try: 57 | is_http_exception = issubclass(obj, web_exceptions.HTTPException) 58 | except TypeError: 59 | is_http_exception = False 60 | if not is_http_exception or obj.status_code is None: 61 | continue 62 | old_obj = exception_map.get(obj.status_code, None) 63 | if old_obj is not None and issubclass(obj, old_obj): 64 | continue 65 | exception_map[obj.status_code] = obj 66 | 67 | 68 | # Collect all exceptions from aiohttp.web_exceptions 69 | _find_exceptions() 70 | del _find_exceptions 71 | 72 | 73 | class AIOHTTPParser(AsyncParser[web.Request]): 74 | """aiohttp request argument parser.""" 75 | 76 | DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = { 77 | "match_info": RAISE, 78 | "path": RAISE, 79 | **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION, 80 | } 81 | __location_map__ = dict( 82 | match_info="load_match_info", 83 | path="load_match_info", 84 | **core.Parser.__location_map__, 85 | ) 86 | 87 | def load_querystring(self, req, schema: Schema) -> MultiDictProxy: 88 | """Return query params from the request as a MultiDictProxy.""" 89 | return self._makeproxy(req.query, schema) 90 | 91 | async def load_form(self, req, schema: Schema) -> MultiDictProxy: 92 | """Return form values from the request as a MultiDictProxy.""" 93 | post_data = await req.post() 94 | return self._makeproxy(post_data, schema) 95 | 96 | async def load_json_or_form(self, req, schema: Schema) -> dict | MultiDictProxy: 97 | data = await self.load_json(req, schema) 98 | if data is not core.missing: 99 | return data 100 | return await self.load_form(req, schema) 101 | 102 | async def load_json(self, req, schema: Schema): 103 | """Return a parsed json payload from the request.""" 104 | if not (req.body_exists and is_json_request(req)): 105 | return core.missing 106 | try: 107 | return await req.json(loads=json.loads) 108 | except json.JSONDecodeError as exc: 109 | if exc.doc == "": 110 | return core.missing 111 | return self._handle_invalid_json_error(exc, req) 112 | except UnicodeDecodeError as exc: 113 | return self._handle_invalid_json_error(exc, req) 114 | 115 | def load_headers(self, req, schema: Schema) -> MultiDictProxy: 116 | """Return headers from the request as a MultiDictProxy.""" 117 | return self._makeproxy(req.headers, schema) 118 | 119 | def load_cookies(self, req, schema: Schema) -> MultiDictProxy: 120 | """Return cookies from the request as a MultiDictProxy.""" 121 | return self._makeproxy(req.cookies, schema) 122 | 123 | def load_files(self, req, schema: Schema) -> typing.NoReturn: 124 | raise NotImplementedError( 125 | "load_files is not implemented. You may be able to use load_form for " 126 | "parsing upload data." 127 | ) 128 | 129 | def load_match_info(self, req, schema: Schema) -> typing.Mapping: 130 | """Load the request's ``match_info``.""" 131 | return req.match_info 132 | 133 | def get_request_from_view_args( 134 | self, view: typing.Callable, args: typing.Iterable, kwargs: typing.Mapping 135 | ): 136 | """Get request object from a handler function or method. Used internally by 137 | ``use_args`` and ``use_kwargs``. 138 | """ 139 | req = None 140 | for arg in args: 141 | if isinstance(arg, web.Request): 142 | req = arg 143 | break 144 | if isinstance(arg, web.View): 145 | req = arg.request 146 | break 147 | if not isinstance(req, web.Request): 148 | raise ValueError("Request argument not found for handler") 149 | return req 150 | 151 | def handle_error( 152 | self, 153 | error: ValidationError, 154 | req, 155 | schema: Schema, 156 | *, 157 | error_status_code: int | None, 158 | error_headers: typing.Mapping[str, str] | None, 159 | ) -> typing.NoReturn: 160 | """Handle ValidationErrors and return a JSON response of error messages 161 | to the client. 162 | """ 163 | error_class = exception_map.get( 164 | error_status_code or self.DEFAULT_VALIDATION_STATUS 165 | ) 166 | if not error_class: 167 | raise LookupError(f"No exception for {error_status_code}") 168 | headers = error_headers 169 | raise error_class( 170 | text=json.dumps(error.messages), 171 | headers=headers, 172 | content_type="application/json", 173 | ) 174 | 175 | def _handle_invalid_json_error( 176 | self, error: json.JSONDecodeError | UnicodeDecodeError, req, *args, **kwargs 177 | ) -> typing.NoReturn: 178 | error_class = exception_map[400] 179 | messages = {"json": ["Invalid JSON body."]} 180 | raise error_class(text=json.dumps(messages), content_type="application/json") 181 | 182 | 183 | parser = AIOHTTPParser() 184 | use_args = parser.use_args # type: typing.Callable 185 | use_kwargs = parser.use_kwargs # type: typing.Callable 186 | -------------------------------------------------------------------------------- /src/webargs/asyncparser.py: -------------------------------------------------------------------------------- 1 | """Asynchronous request parser.""" 2 | 3 | from __future__ import annotations 4 | 5 | import typing 6 | 7 | from webargs import core 8 | 9 | 10 | class AsyncParser(core.Parser[core.Request]): 11 | """Asynchronous variant of `webargs.core.Parser`. 12 | 13 | The ``parse`` method is redefined to be ``async``. 14 | """ 15 | 16 | async def parse( 17 | self, 18 | argmap: core.ArgMap, 19 | req: core.Request | None = None, 20 | *, 21 | location: str | None = None, 22 | unknown: str | None = core._UNKNOWN_DEFAULT_PARAM, 23 | validate: core.ValidateArg = None, 24 | error_status_code: int | None = None, 25 | error_headers: typing.Mapping[str, str] | None = None, 26 | ) -> typing.Any: 27 | """Coroutine variant of `webargs.core.Parser`. 28 | 29 | Receives the same arguments as `webargs.core.Parser.parse`. 30 | """ 31 | data = await self.async_parse( 32 | argmap, 33 | req, 34 | location=location, 35 | unknown=unknown, 36 | validate=validate, 37 | error_status_code=error_status_code, 38 | error_headers=error_headers, 39 | ) 40 | return data 41 | -------------------------------------------------------------------------------- /src/webargs/bottleparser.py: -------------------------------------------------------------------------------- 1 | """Bottle request argument parsing module. 2 | 3 | Example: :: 4 | 5 | from bottle import route, run 6 | from marshmallow import fields 7 | from webargs.bottleparser import use_args 8 | 9 | hello_args = {"name": fields.Str(load_default="World")} 10 | 11 | 12 | @route("/", method="GET", apply=use_args(hello_args)) 13 | def index(args): 14 | return "Hello " + args["name"] 15 | 16 | 17 | if __name__ == "__main__": 18 | run(debug=True) 19 | """ 20 | 21 | import bottle 22 | 23 | from webargs import core 24 | 25 | 26 | class BottleParser(core.Parser[bottle.Request]): 27 | """Bottle.py request argument parser.""" 28 | 29 | def _handle_invalid_json_error(self, error, req, *args, **kwargs): 30 | raise bottle.HTTPError( 31 | status=400, body={"json": ["Invalid JSON body."]}, exception=error 32 | ) 33 | 34 | def _raw_load_json(self, req): 35 | """Read a json payload from the request.""" 36 | try: 37 | data = req.json 38 | except AttributeError: 39 | return core.missing 40 | except bottle.HTTPError as err: 41 | if err.body == "Invalid JSON": 42 | self._handle_invalid_json_error(err, req) 43 | else: 44 | raise 45 | 46 | # unfortunately, bottle does not distinguish between an empty body, "", 47 | # and a body containing the valid JSON value null, "null" 48 | # so these can't be properly disambiguated 49 | # as our best-effort solution, treat None as missing and ignore the 50 | # (admittedly unusual) "null" case 51 | # see: https://github.com/bottlepy/bottle/issues/1160 52 | if data is None: 53 | return core.missing 54 | return data 55 | 56 | def load_querystring(self, req, schema): 57 | """Return query params from the request as a MultiDictProxy.""" 58 | return self._makeproxy(req.query, schema) 59 | 60 | def load_form(self, req, schema): 61 | """Return form values from the request as a MultiDictProxy.""" 62 | # For consistency with other parsers' behavior, don't attempt to 63 | # parse if content-type is mismatched. 64 | # TODO: Make this check more specific 65 | if core.is_json(req.content_type): 66 | return core.missing 67 | return self._makeproxy(req.forms, schema) 68 | 69 | def load_headers(self, req, schema): 70 | """Return headers from the request as a MultiDictProxy.""" 71 | return self._makeproxy(req.headers, schema) 72 | 73 | def load_cookies(self, req, schema): 74 | """Return cookies from the request.""" 75 | return req.cookies 76 | 77 | def load_files(self, req, schema): 78 | """Return files from the request as a MultiDictProxy.""" 79 | return self._makeproxy(req.files, schema) 80 | 81 | def handle_error(self, error, req, schema, *, error_status_code, error_headers): 82 | """Handles errors during parsing. Aborts the current request with a 83 | 400 error. 84 | """ 85 | status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS 86 | raise bottle.HTTPError( 87 | status=status_code, 88 | body=error.messages, 89 | headers=error_headers, 90 | exception=error, 91 | ) 92 | 93 | def get_default_request(self): 94 | """Override to use bottle's thread-local request object by default.""" 95 | return bottle.request 96 | 97 | 98 | parser = BottleParser() 99 | use_args = parser.use_args 100 | use_kwargs = parser.use_kwargs 101 | -------------------------------------------------------------------------------- /src/webargs/djangoparser.py: -------------------------------------------------------------------------------- 1 | """Django request argument parsing. 2 | 3 | Example usage: :: 4 | 5 | from django.views.generic import View 6 | from django.http import HttpResponse 7 | from marshmallow import fields 8 | from webargs.djangoparser import use_args 9 | 10 | hello_args = {"name": fields.Str(load_default="World")} 11 | 12 | 13 | class MyView(View): 14 | @use_args(hello_args) 15 | def get(self, args, request): 16 | return HttpResponse("Hello " + args["name"]) 17 | """ 18 | 19 | import django 20 | import django.http 21 | 22 | from webargs import core 23 | 24 | 25 | def is_json_request(req): 26 | return core.is_json(req.content_type) 27 | 28 | 29 | class DjangoParser(core.Parser[django.http.HttpRequest]): 30 | """Django request argument parser. 31 | 32 | .. warning:: 33 | 34 | :class:`DjangoParser` does not override 35 | :meth:`handle_error `, so your Django 36 | views are responsible for catching any :exc:`ValidationErrors` raised by 37 | the parser and returning the appropriate `HTTPResponse`. 38 | """ 39 | 40 | def _raw_load_json(self, req: django.http.HttpRequest): 41 | """Read a json payload from the request for the core parser's load_json 42 | 43 | Checks the input mimetype and may return 'missing' if the mimetype is 44 | non-json, even if the request body is parseable as json.""" 45 | if not is_json_request(req): 46 | return core.missing 47 | 48 | return core.parse_json(req.body) 49 | 50 | def load_querystring(self, req: django.http.HttpRequest, schema): 51 | """Return query params from the request as a MultiDictProxy.""" 52 | return self._makeproxy(req.GET, schema) 53 | 54 | def load_form(self, req: django.http.HttpRequest, schema): 55 | """Return form values from the request as a MultiDictProxy.""" 56 | return self._makeproxy(req.POST, schema) 57 | 58 | def load_cookies(self, req: django.http.HttpRequest, schema): 59 | """Return cookies from the request.""" 60 | return req.COOKIES 61 | 62 | def load_headers(self, req: django.http.HttpRequest, schema): 63 | """Return headers from the request.""" 64 | # Django's HttpRequest.headers is a case-insensitive dict type, but it 65 | # isn't a multidict, so this is not proxied 66 | return req.headers 67 | 68 | def load_files(self, req: django.http.HttpRequest, schema): 69 | """Return files from the request as a MultiDictProxy.""" 70 | return self._makeproxy(req.FILES, schema) 71 | 72 | def get_request_from_view_args(self, view, args, kwargs): 73 | # The first argument is either `self` or `request` 74 | try: # self.request 75 | return args[0].request 76 | except AttributeError: # first arg is request 77 | return args[0] 78 | 79 | 80 | parser = DjangoParser() 81 | use_args = parser.use_args 82 | use_kwargs = parser.use_kwargs 83 | -------------------------------------------------------------------------------- /src/webargs/falconparser.py: -------------------------------------------------------------------------------- 1 | """Falcon request argument parsing module.""" 2 | 3 | from __future__ import annotations 4 | 5 | import falcon 6 | import marshmallow as ma 7 | from falcon.util.uri import parse_query_string 8 | 9 | from webargs import core 10 | 11 | HTTP_422 = "422 Unprocessable Entity" 12 | 13 | # Mapping of int status codes to string status 14 | status_map = {422: HTTP_422} 15 | 16 | 17 | # Collect all exceptions from falcon.status_codes 18 | def _find_exceptions(): 19 | for name in filter(lambda n: n.startswith("HTTP"), dir(falcon.status_codes)): 20 | status = getattr(falcon.status_codes, name) 21 | status_code = int(status.split(" ")[0]) 22 | status_map[status_code] = status 23 | 24 | 25 | _find_exceptions() 26 | del _find_exceptions 27 | 28 | 29 | def is_json_request(req: falcon.Request): 30 | content_type = req.get_header("Content-Type") 31 | return content_type and core.is_json(content_type) 32 | 33 | 34 | # NOTE: Adapted from falcon.request.Request._parse_form_urlencoded 35 | def parse_form_body(req: falcon.Request): 36 | if ( 37 | req.content_type is not None 38 | and "application/x-www-form-urlencoded" in req.content_type 39 | ): 40 | # the type of `req.stream.read()` is `str | Any` according to annotations 41 | # but at runtime it's always `bytes`, so coerce the type for type checker 42 | body: str | bytes | None = req.stream.read(req.content_length or 0) 43 | try: 44 | body = body.decode("ascii") # type: ignore[union-attr] 45 | except UnicodeDecodeError: 46 | body = None 47 | req.log_error( 48 | "Non-ASCII characters found in form body " 49 | "with Content-Type of " 50 | "application/x-www-form-urlencoded. Body " 51 | "will be ignored." 52 | ) 53 | 54 | if body: 55 | return parse_query_string(body, keep_blank=req.options.keep_blank_qs_values) 56 | 57 | return core.missing 58 | 59 | 60 | class HTTPError(falcon.HTTPError): 61 | """HTTPError that stores a dictionary of validation error messages.""" 62 | 63 | def __init__(self, status, errors, *args, **kwargs): 64 | self.errors = errors 65 | super().__init__(status, *args, **kwargs) 66 | 67 | def to_dict(self, *args, **kwargs): 68 | """Override `falcon.HTTPError` to include error messages in responses.""" 69 | ret = super().to_dict(*args, **kwargs) 70 | if self.errors is not None: 71 | ret["errors"] = self.errors 72 | return ret 73 | 74 | 75 | class FalconParser(core.Parser[falcon.Request]): 76 | """Falcon request argument parser. 77 | 78 | Defaults to using the `media` location. See :py:meth:`~FalconParser.load_media` for 79 | details on the media location.""" 80 | 81 | # by default, Falcon will use the 'media' location to load data 82 | # 83 | # this effectively looks the same as loading JSON data by default, but if 84 | # you add a handler for a different media type to Falcon, webargs will 85 | # automatically pick up on that capability 86 | DEFAULT_LOCATION = "media" 87 | DEFAULT_UNKNOWN_BY_LOCATION = dict( 88 | media=ma.RAISE, **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION 89 | ) 90 | __location_map__ = dict(media="load_media", **core.Parser.__location_map__) 91 | 92 | # Note on the use of MultiDictProxy throughout: 93 | # Falcon parses query strings and form values into ordinary dicts, but with 94 | # the values listified where appropriate 95 | # it is still therefore necessary in these cases to wrap them in 96 | # MultiDictProxy because we need to use the schema to determine when single 97 | # values should be wrapped in lists due to the type of the destination 98 | # field 99 | 100 | def load_querystring(self, req: falcon.Request, schema): 101 | """Return query params from the request as a MultiDictProxy.""" 102 | return self._makeproxy(req.params, schema) 103 | 104 | def load_form(self, req: falcon.Request, schema): 105 | """Return form values from the request as a MultiDictProxy 106 | 107 | .. note:: 108 | 109 | The request stream will be read and left at EOF. 110 | """ 111 | form = parse_form_body(req) 112 | if form is core.missing: 113 | return form 114 | return self._makeproxy(form, schema) 115 | 116 | def load_media(self, req: falcon.Request, schema): 117 | """Return data unpacked and parsed by one of Falcon's media handlers. 118 | By default, Falcon only handles JSON payloads. 119 | 120 | To configure additional media handlers, see the 121 | `Falcon documentation on media types`__. 122 | 123 | .. _FalconMedia: https://falcon.readthedocs.io/en/stable/api/media.html 124 | __ FalconMedia_ 125 | 126 | .. note:: 127 | 128 | The request stream will be read and left at EOF. 129 | """ 130 | # if there is no body, return missing instead of erroring 131 | if req.content_length in (None, 0): 132 | return core.missing 133 | return req.media 134 | 135 | def _raw_load_json(self, req: falcon.Request): 136 | """Return a json payload from the request for the core parser's load_json 137 | 138 | Checks the input mimetype and may return 'missing' if the mimetype is 139 | non-json, even if the request body is parseable as json.""" 140 | if not is_json_request(req) or req.content_length in (None, 0): 141 | return core.missing 142 | body = req.stream.read(req.content_length) 143 | if body: 144 | return core.parse_json(body) 145 | return core.missing 146 | 147 | def load_headers(self, req: falcon.Request, schema): 148 | """Return headers from the request.""" 149 | # Falcon only exposes headers as a dict (not multidict) 150 | return req.headers 151 | 152 | def load_cookies(self, req: falcon.Request, schema): 153 | """Return cookies from the request.""" 154 | # Cookies are expressed in Falcon as a dict, but the possibility of 155 | # multiple values for a cookie is preserved internally -- if desired in 156 | # the future, webargs could add a MultiDict type for Cookies here built 157 | # from (req, schema), but Falcon does not provide one out of the box 158 | return req.cookies 159 | 160 | def get_request_from_view_args(self, view, args, kwargs): 161 | """Get request from a resource method's arguments. Assumes that 162 | request is the second argument. 163 | """ 164 | req = args[1] 165 | if not isinstance(req, falcon.Request): 166 | raise TypeError("Argument is not a falcon.Request") 167 | return req 168 | 169 | def load_files(self, req: falcon.Request, schema): 170 | raise NotImplementedError( 171 | f"Parsing files not yet supported by {self.__class__.__name__}" 172 | ) 173 | 174 | def handle_error( 175 | self, error, req: falcon.Request, schema, *, error_status_code, error_headers 176 | ): 177 | """Handles errors during parsing.""" 178 | status = status_map.get(error_status_code or self.DEFAULT_VALIDATION_STATUS) 179 | if status is None: 180 | raise LookupError(f"Status code {error_status_code} not supported") 181 | raise HTTPError(status, errors=error.messages, headers=error_headers) 182 | 183 | def _handle_invalid_json_error(self, error, req: falcon.Request, *args, **kwargs): 184 | status = status_map[400] 185 | messages = {"json": ["Invalid JSON body."]} 186 | raise HTTPError(status, errors=messages) 187 | 188 | 189 | parser = FalconParser() 190 | use_args = parser.use_args 191 | use_kwargs = parser.use_kwargs 192 | -------------------------------------------------------------------------------- /src/webargs/fields.py: -------------------------------------------------------------------------------- 1 | """Field classes. 2 | 3 | Includes all fields from `marshmallow.fields` in addition to a custom 4 | `Nested` field and `DelimitedList`. 5 | 6 | All fields can optionally take a special `location` keyword argument, which 7 | tells webargs where to parse the request argument from. 8 | 9 | .. code-block:: python 10 | 11 | args = { 12 | "active": fields.Bool(location="query"), 13 | "content_type": fields.Str(data_key="Content-Type", location="headers"), 14 | } 15 | """ 16 | 17 | from __future__ import annotations 18 | 19 | import typing 20 | 21 | import marshmallow as ma 22 | 23 | # Expose all fields from marshmallow.fields. 24 | from marshmallow.fields import * # noqa: F403 25 | 26 | __all__ = ["DelimitedList", "DelimitedTuple"] + ma.fields.__all__ 27 | 28 | 29 | # TODO: remove custom `Nested` in the next major release 30 | # 31 | # the `Nested` class is only needed on versions of marshmallow prior to v3.15.0 32 | # in that version, `ma.fields.Nested` gained the ability to consume dict inputs 33 | # prior to that, this subclass adds this capability 34 | # 35 | # if we drop support for ma.__version_info__ < (3, 15) we can do this 36 | class Nested(ma.fields.Nested): # type: ignore[no-redef] 37 | """Same as `marshmallow.fields.Nested`, except can be passed a dictionary 38 | as the first argument, which will be converted to a `marshmallow.Schema`. 39 | 40 | .. note:: 41 | 42 | The schema class here will always be `marshmallow.Schema`, regardless 43 | of whether a custom schema class is set on the parser. Pass an explicit schema 44 | class if necessary. 45 | """ 46 | 47 | def __init__(self, nested, *args, **kwargs): 48 | if isinstance(nested, dict): 49 | nested = ma.Schema.from_dict(nested) 50 | super().__init__(nested, *args, **kwargs) 51 | 52 | 53 | class DelimitedFieldMixin: 54 | """ 55 | This is a mixin class for subclasses of ma.fields.List and ma.fields.Tuple 56 | which split on a pre-specified delimiter. By default, the delimiter will be "," 57 | 58 | Because we want the MRO to reach this class before the List or Tuple class, 59 | it must be listed first in the superclasses 60 | 61 | For example, a DelimitedList-like type can be defined like so: 62 | 63 | >>> class MyDelimitedList(DelimitedFieldMixin, ma.fields.List): 64 | >>> pass 65 | """ 66 | 67 | delimiter: str = "," 68 | # delimited fields set is_multiple=False for webargs.core.is_multiple 69 | is_multiple: bool = False 70 | # NOTE: in 8.x this defaults to "" but in 9.x it will be 'missing' 71 | empty_value: typing.Any = "" 72 | 73 | def _serialize(self, value, attr, obj, **kwargs): 74 | # serializing will start with parent-class serialization, so that we correctly 75 | # output lists of non-primitive types, e.g. DelimitedList(DateTime) 76 | return self.delimiter.join( 77 | format(each) for each in super()._serialize(value, attr, obj, **kwargs) 78 | ) 79 | 80 | def _deserialize(self, value, attr, data, **kwargs): 81 | # attempting to deserialize from a non-string source is an error 82 | if not isinstance(value, (str, bytes)): 83 | raise self.make_error("invalid") 84 | values = value.split(self.delimiter) if value else [] 85 | # convert empty strings to the empty value; typically "" and therefore a no-op 86 | values = [v or self.empty_value for v in values] 87 | return super()._deserialize(values, attr, data, **kwargs) 88 | 89 | 90 | class DelimitedList(DelimitedFieldMixin, ma.fields.List): 91 | """A field which is similar to a List, but takes its input as a delimited 92 | string (e.g. "foo,bar,baz"). 93 | 94 | Like List, it can be given a nested field type which it will use to 95 | de/serialize each element of the list. 96 | 97 | :param Field cls_or_instance: A field class or instance. 98 | :param str delimiter: Delimiter between values. 99 | """ 100 | 101 | default_error_messages = {"invalid": "Not a valid delimited list."} 102 | 103 | def __init__( 104 | self, 105 | cls_or_instance: ma.fields.Field | type, 106 | *, 107 | delimiter: str | None = None, 108 | **kwargs, 109 | ): 110 | self.delimiter = delimiter or self.delimiter 111 | super().__init__(cls_or_instance, **kwargs) 112 | 113 | 114 | class DelimitedTuple(DelimitedFieldMixin, ma.fields.Tuple): 115 | """A field which is similar to a Tuple, but takes its input as a delimited 116 | string (e.g. "foo,bar,baz"). 117 | 118 | Like Tuple, it can be given a tuple of nested field types which it will use to 119 | de/serialize each element of the tuple. 120 | 121 | :param Iterable[Field] tuple_fields: An iterable of field classes or instances. 122 | :param str delimiter: Delimiter between values. 123 | """ 124 | 125 | default_error_messages = {"invalid": "Not a valid delimited tuple."} 126 | 127 | def __init__( 128 | self, 129 | tuple_fields, 130 | *, 131 | delimiter: str | None = None, 132 | **kwargs, 133 | ): 134 | self.delimiter = delimiter or self.delimiter 135 | super().__init__(tuple_fields, **kwargs) 136 | -------------------------------------------------------------------------------- /src/webargs/flaskparser.py: -------------------------------------------------------------------------------- 1 | """Flask request argument parsing module. 2 | 3 | Example: :: 4 | 5 | from flask import Flask 6 | 7 | from webargs import fields 8 | from webargs.flaskparser import use_args 9 | 10 | app = Flask(__name__) 11 | 12 | user_detail_args = {"per_page": fields.Int()} 13 | 14 | 15 | @app.route("/user/") 16 | @use_args(user_detail_args) 17 | def user_detail(args, uid): 18 | return ("The user page for user {uid}, showing {per_page} posts.").format( 19 | uid=uid, per_page=args["per_page"] 20 | ) 21 | """ 22 | 23 | from __future__ import annotations 24 | 25 | import json 26 | import typing 27 | 28 | import flask 29 | import marshmallow as ma 30 | from werkzeug.exceptions import HTTPException 31 | 32 | from webargs import core 33 | 34 | 35 | def abort( 36 | http_status_code: int, exc: Exception | None = None, **kwargs: typing.Any 37 | ) -> typing.NoReturn: 38 | """Raise a HTTPException for the given http_status_code. Attach any keyword 39 | arguments to the exception for later processing. 40 | 41 | From Flask-Restful. See NOTICE file for license information. 42 | """ 43 | try: 44 | flask.abort(http_status_code) 45 | except HTTPException as err: 46 | err.data = kwargs # type: ignore 47 | err.exc = exc # type: ignore 48 | raise err 49 | 50 | 51 | def is_json_request(req: flask.Request) -> bool: 52 | return core.is_json(req.mimetype) 53 | 54 | 55 | class FlaskParser(core.Parser[flask.Request]): 56 | """Flask request argument parser.""" 57 | 58 | DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = { 59 | "view_args": ma.RAISE, 60 | "path": ma.RAISE, 61 | **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION, 62 | } 63 | __location_map__ = dict( 64 | view_args="load_view_args", 65 | path="load_view_args", 66 | **core.Parser.__location_map__, 67 | ) 68 | 69 | def _raw_load_json(self, req: flask.Request) -> typing.Any: 70 | """Return a json payload from the request for the core parser's load_json 71 | 72 | Checks the input mimetype and may return 'missing' if the mimetype is 73 | non-json, even if the request body is parseable as json.""" 74 | if not is_json_request(req): 75 | return core.missing 76 | 77 | return core.parse_json(req.get_data(cache=True)) 78 | 79 | def _handle_invalid_json_error( 80 | self, 81 | error: json.JSONDecodeError | UnicodeDecodeError, 82 | req: flask.Request, 83 | *args: typing.Any, 84 | **kwargs: typing.Any, 85 | ) -> typing.NoReturn: 86 | abort(400, exc=error, messages={"json": ["Invalid JSON body."]}) 87 | 88 | def load_view_args(self, req: flask.Request, schema: ma.Schema) -> typing.Any: 89 | """Return the request's ``view_args`` or ``missing`` if there are none.""" 90 | return req.view_args or core.missing 91 | 92 | def load_querystring(self, req: flask.Request, schema: ma.Schema) -> typing.Any: 93 | """Return query params from the request as a MultiDictProxy.""" 94 | return self._makeproxy(req.args, schema) 95 | 96 | def load_form(self, req: flask.Request, schema: ma.Schema) -> typing.Any: 97 | """Return form values from the request as a MultiDictProxy.""" 98 | return self._makeproxy(req.form, schema) 99 | 100 | def load_headers(self, req: flask.Request, schema: ma.Schema) -> typing.Any: 101 | """Return headers from the request as a MultiDictProxy.""" 102 | return self._makeproxy(req.headers, schema) 103 | 104 | def load_cookies(self, req: flask.Request, schema: ma.Schema) -> typing.Any: 105 | """Return cookies from the request.""" 106 | return req.cookies 107 | 108 | def load_files(self, req: flask.Request, schema: ma.Schema) -> typing.Any: 109 | """Return files from the request as a MultiDictProxy.""" 110 | return self._makeproxy(req.files, schema) 111 | 112 | def handle_error( 113 | self, 114 | error: ma.ValidationError, 115 | req: flask.Request, 116 | schema: ma.Schema, 117 | *, 118 | error_status_code: int | None, 119 | error_headers: typing.Mapping[str, str] | None, 120 | ) -> typing.NoReturn: 121 | """Handles errors during parsing. Aborts the current HTTP request and 122 | responds with a 422 error. 123 | """ 124 | status_code: int = error_status_code or self.DEFAULT_VALIDATION_STATUS 125 | abort( 126 | status_code, 127 | exc=error, 128 | messages=error.messages, 129 | schema=schema, 130 | headers=error_headers, 131 | ) 132 | 133 | def get_default_request(self) -> flask.Request: 134 | """Override to use Flask's thread-local request object by default""" 135 | return flask.request 136 | 137 | 138 | parser = FlaskParser() 139 | use_args = parser.use_args 140 | use_kwargs = parser.use_kwargs 141 | -------------------------------------------------------------------------------- /src/webargs/multidictproxy.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import typing 4 | from collections.abc import MutableMapping 5 | 6 | import marshmallow as ma 7 | 8 | 9 | class MultiDictProxy(MutableMapping): 10 | """ 11 | A proxy object which wraps multidict types along with a matching schema 12 | Whenever a value is looked up, it is checked against the schema to see if 13 | there is a matching field where `is_multiple` is True. If there is, then 14 | the data should be loaded as a list or tuple. 15 | 16 | In all other cases, __getitem__ proxies directly to the input multidict. 17 | """ 18 | 19 | def __init__( 20 | self, 21 | multidict: MutableMapping, 22 | schema: ma.Schema, 23 | known_multi_fields: tuple[type, ...] = ( 24 | ma.fields.List, 25 | ma.fields.Tuple, 26 | ), 27 | ): 28 | self.data = multidict 29 | self.known_multi_fields = known_multi_fields 30 | self.multiple_keys = self._collect_multiple_keys(schema) 31 | 32 | def _is_multiple(self, field: ma.fields.Field) -> bool: 33 | """Return whether or not `field` handles repeated/multi-value arguments.""" 34 | # fields which set `is_multiple = True/False` will have the value selected, 35 | # otherwise, we check for explicit criteria 36 | is_multiple_attr = getattr(field, "is_multiple", None) 37 | if is_multiple_attr is not None: 38 | return is_multiple_attr 39 | return isinstance(field, self.known_multi_fields) 40 | 41 | def _collect_multiple_keys(self, schema: ma.Schema) -> set[str]: 42 | result = set() 43 | for name, field in schema.fields.items(): 44 | if not self._is_multiple(field): 45 | continue 46 | result.add(field.data_key if field.data_key is not None else name) 47 | return result 48 | 49 | def __getitem__(self, key: str) -> typing.Any: 50 | val = self.data.get(key, ma.missing) 51 | if val is ma.missing or key not in self.multiple_keys: 52 | return val 53 | if hasattr(self.data, "getlist"): 54 | return self.data.getlist(key) 55 | if hasattr(self.data, "getall"): 56 | return self.data.getall(key) 57 | if isinstance(val, (list, tuple)): 58 | return val 59 | if val is None: 60 | return None 61 | return [val] 62 | 63 | def __str__(self) -> str: # str(proxy) proxies to str(proxy.data) 64 | return str(self.data) 65 | 66 | def __repr__(self) -> str: 67 | return ( 68 | f"MultiDictProxy(data={self.data!r}, multiple_keys={self.multiple_keys!r})" 69 | ) 70 | 71 | def __delitem__(self, key: str) -> None: 72 | del self.data[key] 73 | 74 | def __setitem__(self, key: str, value: typing.Any) -> None: 75 | self.data[key] = value 76 | 77 | def __getattr__(self, name: str) -> typing.Any: 78 | return getattr(self.data, name) 79 | 80 | def __iter__(self) -> typing.Iterator[str]: 81 | for x in iter(self.data): 82 | # special case for header dicts which produce an iterator of tuples 83 | # instead of an iterator of strings 84 | if isinstance(x, tuple): 85 | yield x[0] 86 | else: 87 | yield x 88 | 89 | def __contains__(self, x: object) -> bool: 90 | return x in self.data 91 | 92 | def __len__(self) -> int: 93 | return len(self.data) 94 | 95 | def __eq__(self, other: object) -> bool: 96 | return self.data == other 97 | 98 | def __ne__(self, other: object) -> bool: 99 | return self.data != other 100 | -------------------------------------------------------------------------------- /src/webargs/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marshmallow-code/webargs/a59bfca9c9c88e0cbaa97eb2cdba3683e8cc33a1/src/webargs/py.typed -------------------------------------------------------------------------------- /src/webargs/pyramidparser.py: -------------------------------------------------------------------------------- 1 | """Pyramid request argument parsing. 2 | 3 | Example usage: :: 4 | 5 | from wsgiref.simple_server import make_server 6 | from pyramid.config import Configurator 7 | from pyramid.response import Response 8 | from marshmallow import fields 9 | from webargs.pyramidparser import use_args 10 | 11 | hello_args = {"name": fields.Str(load_default="World")} 12 | 13 | 14 | @use_args(hello_args) 15 | def hello_world(request, args): 16 | return Response("Hello " + args["name"]) 17 | 18 | 19 | if __name__ == "__main__": 20 | config = Configurator() 21 | config.add_route("hello", "/") 22 | config.add_view(hello_world, route_name="hello") 23 | app = config.make_wsgi_app() 24 | server = make_server("0.0.0.0", 6543, app) 25 | server.serve_forever() 26 | """ 27 | 28 | from __future__ import annotations 29 | 30 | import functools 31 | import typing 32 | from collections.abc import Mapping 33 | 34 | import marshmallow as ma 35 | from pyramid.httpexceptions import exception_response 36 | from pyramid.request import Request 37 | from webob.multidict import MultiDict 38 | 39 | from webargs import core 40 | from webargs.core import json 41 | 42 | F = typing.TypeVar("F", bound=typing.Callable) 43 | 44 | 45 | def is_json_request(req: Request) -> bool: 46 | return core.is_json(req.headers.get("content-type")) 47 | 48 | 49 | class PyramidParser(core.Parser[Request]): 50 | """Pyramid request argument parser.""" 51 | 52 | DEFAULT_UNKNOWN_BY_LOCATION: dict[str, str | None] = { 53 | "matchdict": ma.RAISE, 54 | "path": ma.RAISE, 55 | **core.Parser.DEFAULT_UNKNOWN_BY_LOCATION, 56 | } 57 | __location_map__ = dict( 58 | matchdict="load_matchdict", 59 | path="load_matchdict", 60 | **core.Parser.__location_map__, 61 | ) 62 | 63 | def _raw_load_json(self, req: Request) -> typing.Any: 64 | """Return a json payload from the request for the core parser's load_json 65 | 66 | Checks the input mimetype and may return 'missing' if the mimetype is 67 | non-json, even if the request body is parseable as json.""" 68 | if not is_json_request(req): 69 | return core.missing 70 | 71 | return core.parse_json(req.body, encoding=req.charset) 72 | 73 | def load_querystring(self, req: Request, schema: ma.Schema) -> typing.Any: 74 | """Return query params from the request as a MultiDictProxy.""" 75 | return self._makeproxy(req.GET, schema) 76 | 77 | def load_form(self, req: Request, schema: ma.Schema) -> typing.Any: 78 | """Return form values from the request as a MultiDictProxy.""" 79 | return self._makeproxy(req.POST, schema) 80 | 81 | def load_cookies(self, req: Request, schema: ma.Schema) -> typing.Any: 82 | """Return cookies from the request as a MultiDictProxy.""" 83 | return self._makeproxy(req.cookies, schema) 84 | 85 | def load_headers(self, req: Request, schema: ma.Schema) -> typing.Any: 86 | """Return headers from the request as a MultiDictProxy.""" 87 | return self._makeproxy(req.headers, schema) 88 | 89 | def load_files(self, req: Request, schema: ma.Schema) -> typing.Any: 90 | """Return files from the request as a MultiDictProxy.""" 91 | files = ((k, v) for k, v in req.POST.items() if hasattr(v, "file")) 92 | return self._makeproxy(MultiDict(files), schema) 93 | 94 | def load_matchdict(self, req: Request, schema: ma.Schema) -> typing.Any: 95 | """Return the request's ``matchdict`` as a MultiDictProxy.""" 96 | return self._makeproxy(req.matchdict, schema) 97 | 98 | def handle_error( 99 | self, 100 | error: ma.ValidationError, 101 | req: Request, 102 | schema: ma.Schema, 103 | *, 104 | error_status_code: int | None, 105 | error_headers: typing.Mapping[str, str] | None, 106 | ) -> typing.NoReturn: 107 | """Handles errors during parsing. Aborts the current HTTP request and 108 | responds with a 400 error. 109 | """ 110 | status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS 111 | response = exception_response( 112 | status_code, 113 | detail=str(error), 114 | headers=error_headers, 115 | content_type="application/json", 116 | ) 117 | body = json.dumps(error.messages) 118 | response.body = body.encode("utf-8") if isinstance(body, str) else body 119 | raise response 120 | 121 | def _handle_invalid_json_error( 122 | self, 123 | error: json.JSONDecodeError | UnicodeDecodeError, 124 | req: Request, 125 | *args: typing.Any, 126 | **kwargs: typing.Any, 127 | ) -> typing.NoReturn: 128 | messages = {"json": ["Invalid JSON body."]} 129 | response = exception_response( 130 | 400, detail=str(messages), content_type="application/json" 131 | ) 132 | body = json.dumps(messages) 133 | response.body = body.encode("utf-8") if isinstance(body, str) else body 134 | raise response 135 | 136 | def use_args( 137 | self, 138 | argmap: core.ArgMap, 139 | req: Request | None = None, 140 | *, 141 | location: str | None = core.Parser.DEFAULT_LOCATION, 142 | unknown: str | None = None, 143 | as_kwargs: bool = False, 144 | arg_name: str | None = None, 145 | validate: core.ValidateArg = None, 146 | error_status_code: int | None = None, 147 | error_headers: typing.Mapping[str, str] | None = None, 148 | ) -> typing.Callable[..., typing.Callable]: 149 | """Decorator that injects parsed arguments into a view callable. 150 | Supports the *Class-based View* pattern where `request` is saved as an instance 151 | attribute on a view class. 152 | 153 | :param dict argmap: Either a `marshmallow.Schema`, a `dict` 154 | of argname -> `marshmallow.fields.Field` pairs, or a callable 155 | which accepts a request and returns a `marshmallow.Schema`. 156 | :param req: The request object to parse. Pulled off of the view by default. 157 | :param str location: Where on the request to load values. 158 | :param str unknown: A value to pass for ``unknown`` when calling the 159 | schema's ``load`` method. 160 | :param bool as_kwargs: Whether to insert arguments as keyword arguments. 161 | :param str arg_name: Keyword argument name to use for arguments. Mutually 162 | exclusive with as_kwargs. 163 | :param callable validate: Validation function that receives the dictionary 164 | of parsed arguments. If the function returns ``False``, the parser 165 | will raise a :exc:`ValidationError`. 166 | :param int error_status_code: Status code passed to error handler functions when 167 | a `ValidationError` is raised. 168 | :param dict error_headers: Headers passed to error handler functions when a 169 | a `ValidationError` is raised. 170 | """ 171 | location = location or self.location 172 | 173 | if arg_name is not None and as_kwargs: 174 | raise ValueError("arg_name and as_kwargs are mutually exclusive") 175 | if arg_name is None and not self.USE_ARGS_POSITIONAL: 176 | arg_name = f"{location}_args" 177 | 178 | # Optimization: If argmap is passed as a dictionary, we only need 179 | # to generate a Schema once 180 | if isinstance(argmap, Mapping): 181 | if not isinstance(argmap, dict): 182 | argmap = dict(argmap) 183 | argmap = self.schema_class.from_dict(argmap)() 184 | 185 | def decorator(func: F) -> F: 186 | @functools.wraps(func) 187 | def wrapper( 188 | obj: typing.Any, *args: typing.Any, **kwargs: typing.Any 189 | ) -> typing.Any: 190 | # The first argument is either `self` or `request` 191 | try: # get self.request 192 | request = req or obj.request 193 | except AttributeError: # first arg is request 194 | request = obj 195 | # NOTE: At this point, argmap may be a Schema, callable, or dict 196 | parsed_args = self.parse( 197 | argmap, 198 | req=request, 199 | location=location, 200 | unknown=unknown, 201 | validate=validate, 202 | error_status_code=error_status_code, 203 | error_headers=error_headers, 204 | ) 205 | args, kwargs = self._update_args_kwargs( 206 | args, kwargs, parsed_args, as_kwargs, arg_name 207 | ) 208 | return func(obj, *args, **kwargs) 209 | 210 | wrapper.__wrapped__ = func 211 | return wrapper # type: ignore[return-value] 212 | 213 | return decorator 214 | 215 | 216 | parser = PyramidParser() 217 | use_args = parser.use_args 218 | use_kwargs = parser.use_kwargs 219 | -------------------------------------------------------------------------------- /src/webargs/tornadoparser.py: -------------------------------------------------------------------------------- 1 | """Tornado request argument parsing module. 2 | 3 | Example: :: 4 | 5 | import tornado.web 6 | from marshmallow import fields 7 | from webargs.tornadoparser import use_args 8 | 9 | 10 | class HelloHandler(tornado.web.RequestHandler): 11 | @use_args({"name": fields.Str(load_default="World")}) 12 | def get(self, args): 13 | response = {"message": "Hello {}".format(args["name"])} 14 | self.write(response) 15 | """ 16 | 17 | from __future__ import annotations 18 | 19 | import json 20 | import typing 21 | 22 | import marshmallow as ma 23 | import tornado.concurrent 24 | import tornado.web 25 | from tornado.escape import _unicode 26 | from tornado.httputil import HTTPServerRequest 27 | 28 | from webargs import core 29 | from webargs.multidictproxy import MultiDictProxy 30 | 31 | 32 | class HTTPError(tornado.web.HTTPError): 33 | """`tornado.web.HTTPError` that stores validation errors.""" 34 | 35 | def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: 36 | self.messages = kwargs.pop("messages", {}) 37 | self.headers = kwargs.pop("headers", None) 38 | super().__init__(*args, **kwargs) 39 | 40 | 41 | def is_json_request(req: HTTPServerRequest) -> bool: 42 | content_type = req.headers.get("Content-Type") 43 | return content_type is not None and core.is_json(content_type) 44 | 45 | 46 | class WebArgsTornadoMultiDictProxy(MultiDictProxy): 47 | """ 48 | Override class for Tornado multidicts, handles argument decoding 49 | requirements. 50 | """ 51 | 52 | def __getitem__(self, key: str) -> typing.Any: 53 | try: 54 | value = self.data.get(key, core.missing) 55 | if value is core.missing: 56 | return core.missing 57 | if key in self.multiple_keys: 58 | return [ 59 | _unicode(v) if isinstance(v, (str, bytes)) else v for v in value 60 | ] 61 | if value and isinstance(value, (list, tuple)): 62 | value = value[0] 63 | 64 | if isinstance(value, (str, bytes)): 65 | return _unicode(value) 66 | return value 67 | # based on tornado.web.RequestHandler.decode_argument 68 | except UnicodeDecodeError as exc: 69 | raise HTTPError(400, f"Invalid unicode in {key}: {value[:40]!r}") from exc 70 | 71 | 72 | class WebArgsTornadoCookiesMultiDictProxy(MultiDictProxy): 73 | """ 74 | And a special override for cookies because they come back as objects with a 75 | `value` attribute we need to extract. 76 | Also, does not use the `_unicode` decoding step 77 | """ 78 | 79 | def __getitem__(self, key: str) -> typing.Any: 80 | cookie = self.data.get(key, core.missing) 81 | if cookie is core.missing: 82 | return core.missing 83 | if key in self.multiple_keys: 84 | return [cookie.value] 85 | return cookie.value 86 | 87 | 88 | class TornadoParser(core.Parser[HTTPServerRequest]): 89 | """Tornado request argument parser.""" 90 | 91 | def _raw_load_json(self, req: HTTPServerRequest) -> typing.Any: 92 | """Return a json payload from the request for the core parser's load_json 93 | 94 | Checks the input mimetype and may return 'missing' if the mimetype is 95 | non-json, even if the request body is parseable as json.""" 96 | if not is_json_request(req): 97 | return core.missing 98 | 99 | # request.body may be a concurrent.Future on streaming requests 100 | # this would cause a TypeError if we try to parse it 101 | if isinstance(req.body, tornado.concurrent.Future): 102 | return core.missing 103 | 104 | return core.parse_json(req.body) 105 | 106 | def load_querystring(self, req: HTTPServerRequest, schema: ma.Schema) -> typing.Any: 107 | """Return query params from the request as a MultiDictProxy.""" 108 | return self._makeproxy( 109 | req.query_arguments, schema, cls=WebArgsTornadoMultiDictProxy 110 | ) 111 | 112 | def load_form(self, req: HTTPServerRequest, schema: ma.Schema) -> typing.Any: 113 | """Return form values from the request as a MultiDictProxy.""" 114 | return self._makeproxy( 115 | req.body_arguments, schema, cls=WebArgsTornadoMultiDictProxy 116 | ) 117 | 118 | def load_headers(self, req: HTTPServerRequest, schema: ma.Schema) -> typing.Any: 119 | """Return headers from the request as a MultiDictProxy.""" 120 | return self._makeproxy(req.headers, schema, cls=WebArgsTornadoMultiDictProxy) 121 | 122 | def load_cookies(self, req: HTTPServerRequest, schema: ma.Schema) -> typing.Any: 123 | """Return cookies from the request as a MultiDictProxy.""" 124 | # use the specialized subclass specifically for handling Tornado 125 | # cookies 126 | return self._makeproxy( 127 | req.cookies, schema, cls=WebArgsTornadoCookiesMultiDictProxy 128 | ) 129 | 130 | def load_files(self, req: HTTPServerRequest, schema: ma.Schema) -> typing.Any: 131 | """Return files from the request as a MultiDictProxy.""" 132 | return self._makeproxy(req.files, schema, cls=WebArgsTornadoMultiDictProxy) 133 | 134 | def handle_error( 135 | self, 136 | error: ma.ValidationError, 137 | req: HTTPServerRequest, 138 | schema: ma.Schema, 139 | *, 140 | error_status_code: int | None, 141 | error_headers: typing.Mapping[str, str] | None, 142 | ) -> typing.NoReturn: 143 | """Handles errors during parsing. Raises a `tornado.web.HTTPError` 144 | with a 400 error. 145 | """ 146 | status_code = error_status_code or self.DEFAULT_VALIDATION_STATUS 147 | if status_code == 422: 148 | reason = "Unprocessable Entity" 149 | else: 150 | reason = None 151 | raise HTTPError( 152 | status_code, 153 | log_message=str(error.messages), 154 | reason=reason, 155 | messages=error.messages, 156 | headers=error_headers, 157 | ) 158 | 159 | def _handle_invalid_json_error( 160 | self, 161 | error: json.JSONDecodeError | UnicodeDecodeError, 162 | req: HTTPServerRequest, 163 | *args: typing.Any, 164 | **kwargs: typing.Any, 165 | ) -> typing.NoReturn: 166 | raise HTTPError( 167 | 400, 168 | log_message="Invalid JSON body.", 169 | reason="Bad Request", 170 | messages={"json": ["Invalid JSON body."]}, 171 | ) 172 | 173 | def get_request_from_view_args( 174 | self, 175 | view: typing.Any, 176 | args: tuple[typing.Any, ...], 177 | kwargs: typing.Mapping[str, typing.Any], 178 | ) -> HTTPServerRequest: 179 | return args[0].request 180 | 181 | 182 | parser = TornadoParser() 183 | use_args = parser.use_args 184 | use_kwargs = parser.use_kwargs 185 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marshmallow-code/webargs/a59bfca9c9c88e0cbaa97eb2cdba3683e8cc33a1/tests/__init__.py -------------------------------------------------------------------------------- /tests/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marshmallow-code/webargs/a59bfca9c9c88e0cbaa97eb2cdba3683e8cc33a1/tests/apps/__init__.py -------------------------------------------------------------------------------- /tests/apps/aiohttp_app.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import marshmallow as ma 3 | from aiohttp.web import json_response 4 | 5 | from webargs import fields, validate 6 | from webargs.aiohttpparser import parser, use_args, use_kwargs 7 | from webargs.core import json 8 | 9 | hello_args = {"name": fields.Str(load_default="World", validate=validate.Length(min=3))} 10 | hello_multiple = {"name": fields.List(fields.Str())} 11 | 12 | 13 | class HelloSchema(ma.Schema): 14 | name = fields.Str(load_default="World", validate=validate.Length(min=3)) 15 | 16 | 17 | hello_many_schema = HelloSchema(many=True) 18 | 19 | # variant which ignores unknown fields 20 | hello_exclude_schema = HelloSchema(unknown=ma.EXCLUDE) 21 | 22 | 23 | ##### Handlers ##### 24 | 25 | 26 | async def echo(request): 27 | parsed = await parser.parse(hello_args, request, location="query") 28 | return json_response(parsed) 29 | 30 | 31 | async def echo_form(request): 32 | parsed = await parser.parse(hello_args, request, location="form") 33 | return json_response(parsed) 34 | 35 | 36 | async def echo_json(request): 37 | try: 38 | parsed = await parser.parse(hello_args, request, location="json") 39 | except json.JSONDecodeError as exc: 40 | raise aiohttp.web.HTTPBadRequest( 41 | text=json.dumps(["Invalid JSON."]), 42 | content_type="application/json", 43 | ) from exc 44 | return json_response(parsed) 45 | 46 | 47 | async def echo_json_or_form(request): 48 | try: 49 | parsed = await parser.parse(hello_args, request, location="json_or_form") 50 | except json.JSONDecodeError as exc: 51 | raise aiohttp.web.HTTPBadRequest( 52 | text=json.dumps(["Invalid JSON."]), 53 | content_type="application/json", 54 | ) from exc 55 | return json_response(parsed) 56 | 57 | 58 | @use_args(hello_args, location="query") 59 | async def echo_use_args(request, args): 60 | return json_response(args) 61 | 62 | 63 | @use_kwargs(hello_args, location="query") 64 | async def echo_use_kwargs(request, name): 65 | return json_response({"name": name}) 66 | 67 | 68 | @use_args( 69 | {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form" 70 | ) 71 | async def echo_use_args_validated(request, args): 72 | return json_response(args) 73 | 74 | 75 | async def echo_ignoring_extra_data(request): 76 | return json_response( 77 | await parser.parse(hello_exclude_schema, request, unknown=None) 78 | ) 79 | 80 | 81 | async def echo_multi(request): 82 | parsed = await parser.parse(hello_multiple, request, location="query") 83 | return json_response(parsed) 84 | 85 | 86 | async def echo_multi_form(request): 87 | parsed = await parser.parse(hello_multiple, request, location="form") 88 | return json_response(parsed) 89 | 90 | 91 | async def echo_multi_json(request): 92 | parsed = await parser.parse(hello_multiple, request) 93 | return json_response(parsed) 94 | 95 | 96 | async def echo_many_schema(request): 97 | parsed = await parser.parse(hello_many_schema, request) 98 | return json_response(parsed) 99 | 100 | 101 | @use_args({"value": fields.Int()}, location="query") 102 | async def echo_use_args_with_path_param(request, args): 103 | return json_response(args) 104 | 105 | 106 | @use_kwargs({"value": fields.Int()}, location="query") 107 | async def echo_use_kwargs_with_path_param(request, value): 108 | return json_response({"value": value}) 109 | 110 | 111 | @use_args({"page": fields.Int(), "q": fields.Int()}, location="query") 112 | @use_args({"name": fields.Str()}) 113 | async def echo_use_args_multiple(request, query_parsed, json_parsed): 114 | return json_response({"query_parsed": query_parsed, "json_parsed": json_parsed}) 115 | 116 | 117 | async def always_error(request): 118 | def always_fail(value): 119 | raise ma.ValidationError("something went wrong") 120 | 121 | args = {"text": fields.Str(validate=always_fail)} 122 | parsed = await parser.parse(args, request) 123 | return json_response(parsed) 124 | 125 | 126 | async def echo_headers(request): 127 | parsed = await parser.parse(hello_args, request, location="headers") 128 | return json_response(parsed) 129 | 130 | 131 | async def echo_cookie(request): 132 | parsed = await parser.parse(hello_args, request, location="cookies") 133 | return json_response(parsed) 134 | 135 | 136 | async def echo_nested(request): 137 | args = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})} 138 | parsed = await parser.parse(args, request) 139 | return json_response(parsed) 140 | 141 | 142 | async def echo_multiple_args(request): 143 | args = {"first": fields.Str(), "last": fields.Str()} 144 | parsed = await parser.parse(args, request) 145 | return json_response(parsed) 146 | 147 | 148 | async def echo_nested_many(request): 149 | args = { 150 | "users": fields.Nested({"id": fields.Int(), "name": fields.Str()}, many=True) 151 | } 152 | parsed = await parser.parse(args, request) 153 | return json_response(parsed) 154 | 155 | 156 | async def echo_nested_many_data_key(request): 157 | args = { 158 | "x_field": fields.Nested({"id": fields.Int()}, many=True, data_key="X-Field") 159 | } 160 | parsed = await parser.parse(args, request) 161 | return json_response(parsed) 162 | 163 | 164 | async def echo_match_info(request): 165 | parsed = await parser.parse( 166 | {"mymatch": fields.Int()}, request, location="match_info" 167 | ) 168 | return json_response(parsed) 169 | 170 | 171 | class EchoHandler: 172 | @use_args(hello_args, location="query") 173 | async def get(self, request, args): 174 | return json_response(args) 175 | 176 | 177 | class EchoHandlerView(aiohttp.web.View): 178 | @use_args(hello_args, location="query") 179 | async def get(self, args): 180 | return json_response(args) 181 | 182 | 183 | @use_args(HelloSchema, as_kwargs=True, location="query") 184 | async def echo_use_schema_as_kwargs(request, name): 185 | return json_response({"name": name}) 186 | 187 | 188 | ##### App factory ##### 189 | 190 | 191 | def add_route(app, methods, route, handler): 192 | for method in methods: 193 | app.router.add_route(method, route, handler) 194 | 195 | 196 | def create_app(): 197 | app = aiohttp.web.Application() 198 | 199 | add_route(app, ["GET"], "/echo", echo) 200 | add_route(app, ["POST"], "/echo_form", echo_form) 201 | add_route(app, ["POST"], "/echo_json", echo_json) 202 | add_route(app, ["POST"], "/echo_json_or_form", echo_json_or_form) 203 | add_route(app, ["GET"], "/echo_use_args", echo_use_args) 204 | add_route(app, ["GET"], "/echo_use_kwargs", echo_use_kwargs) 205 | add_route(app, ["POST"], "/echo_use_args_validated", echo_use_args_validated) 206 | add_route(app, ["POST"], "/echo_ignoring_extra_data", echo_ignoring_extra_data) 207 | add_route(app, ["GET"], "/echo_multi", echo_multi) 208 | add_route(app, ["POST"], "/echo_multi_form", echo_multi_form) 209 | add_route(app, ["POST"], "/echo_multi_json", echo_multi_json) 210 | add_route(app, ["GET", "POST"], "/echo_many_schema", echo_many_schema) 211 | add_route( 212 | app, 213 | ["GET", "POST"], 214 | "/echo_use_args_with_path_param/{name}", 215 | echo_use_args_with_path_param, 216 | ) 217 | add_route( 218 | app, 219 | ["GET", "POST"], 220 | "/echo_use_kwargs_with_path_param/{name}", 221 | echo_use_kwargs_with_path_param, 222 | ) 223 | add_route(app, ["POST"], "/echo_use_args_multiple", echo_use_args_multiple) 224 | add_route(app, ["GET", "POST"], "/error", always_error) 225 | add_route(app, ["GET"], "/echo_headers", echo_headers) 226 | add_route(app, ["GET"], "/echo_cookie", echo_cookie) 227 | add_route(app, ["POST"], "/echo_nested", echo_nested) 228 | add_route(app, ["POST"], "/echo_multiple_args", echo_multiple_args) 229 | add_route(app, ["POST"], "/echo_nested_many", echo_nested_many) 230 | add_route(app, ["POST"], "/echo_nested_many_data_key", echo_nested_many_data_key) 231 | add_route(app, ["GET"], "/echo_match_info/{mymatch}", echo_match_info) 232 | add_route(app, ["GET"], "/echo_method", EchoHandler().get) 233 | add_route(app, ["GET"], "/echo_method_view", EchoHandlerView) 234 | add_route(app, ["GET"], "/echo_use_schema_as_kwargs", echo_use_schema_as_kwargs) 235 | return app 236 | -------------------------------------------------------------------------------- /tests/apps/bottle_app.py: -------------------------------------------------------------------------------- 1 | import marshmallow as ma 2 | from bottle import Bottle, HTTPResponse, debug, request, response 3 | 4 | from webargs import fields, validate 5 | from webargs.bottleparser import parser, use_args, use_kwargs 6 | from webargs.core import json 7 | 8 | hello_args = {"name": fields.Str(load_default="World", validate=validate.Length(min=3))} 9 | hello_multiple = {"name": fields.List(fields.Str())} 10 | 11 | 12 | class HelloSchema(ma.Schema): 13 | name = fields.Str(load_default="World", validate=validate.Length(min=3)) 14 | 15 | 16 | hello_many_schema = HelloSchema(many=True) 17 | 18 | # variant which ignores unknown fields 19 | hello_exclude_schema = HelloSchema(unknown=ma.EXCLUDE) 20 | 21 | 22 | app = Bottle() 23 | debug(True) 24 | 25 | 26 | @app.route("/echo", method=["GET"]) 27 | def echo(): 28 | return parser.parse(hello_args, request, location="query") 29 | 30 | 31 | @app.route("/echo_form", method=["POST"]) 32 | def echo_form(): 33 | return parser.parse(hello_args, location="form") 34 | 35 | 36 | @app.route("/echo_json", method=["POST"]) 37 | def echo_json(): 38 | return parser.parse(hello_args, location="json") 39 | 40 | 41 | @app.route("/echo_json_or_form", method=["POST"]) 42 | def echo_json_or_form(): 43 | return parser.parse(hello_args, location="json_or_form") 44 | 45 | 46 | @app.route("/echo_use_args", method=["GET"]) 47 | @use_args(hello_args, location="query") 48 | def echo_use_args(args): 49 | return args 50 | 51 | 52 | @app.route( 53 | "/echo_use_args_validated", 54 | method=["POST"], 55 | apply=use_args( 56 | {"value": fields.Int()}, 57 | validate=lambda args: args["value"] > 42, 58 | location="form", 59 | ), 60 | ) 61 | def echo_use_args_validated(args): 62 | return args 63 | 64 | 65 | @app.route("/echo_ignoring_extra_data", method=["POST"]) 66 | def echo_json_ignore_extra_data(): 67 | return parser.parse(hello_exclude_schema, unknown=None) 68 | 69 | 70 | @app.route( 71 | "/echo_use_kwargs", method=["GET"], apply=use_kwargs(hello_args, location="query") 72 | ) 73 | def echo_use_kwargs(name): 74 | return {"name": name} 75 | 76 | 77 | @app.route("/echo_multi", method=["GET"]) 78 | def echo_multi(): 79 | return parser.parse(hello_multiple, request, location="query") 80 | 81 | 82 | @app.route("/echo_multi_form", method=["POST"]) 83 | def multi_form(): 84 | return parser.parse(hello_multiple, location="form") 85 | 86 | 87 | @app.route("/echo_multi_json", method=["POST"]) 88 | def multi_json(): 89 | return parser.parse(hello_multiple) 90 | 91 | 92 | @app.route("/echo_many_schema", method=["POST"]) 93 | def echo_many_schema(): 94 | arguments = parser.parse(hello_many_schema, request) 95 | return HTTPResponse(body=json.dumps(arguments), content_type="application/json") 96 | 97 | 98 | @app.route( 99 | "/echo_use_args_with_path_param/", 100 | apply=use_args({"value": fields.Int()}, location="query"), 101 | ) 102 | def echo_use_args_with_path_param(args, name): 103 | return args 104 | 105 | 106 | @app.route( 107 | "/echo_use_kwargs_with_path_param/", 108 | apply=use_kwargs({"value": fields.Int()}, location="query"), 109 | ) 110 | def echo_use_kwargs_with_path_param(name, value): 111 | return {"value": value} 112 | 113 | 114 | @app.route("/error", method=["GET", "POST"]) 115 | def always_error(): 116 | def always_fail(value): 117 | raise ma.ValidationError("something went wrong") 118 | 119 | args = {"text": fields.Str(validate=always_fail)} 120 | return parser.parse(args) 121 | 122 | 123 | @app.route("/echo_headers") 124 | def echo_headers(): 125 | return parser.parse(hello_args, request, location="headers") 126 | 127 | 128 | @app.route("/echo_cookie") 129 | def echo_cookie(): 130 | return parser.parse(hello_args, request, location="cookies") 131 | 132 | 133 | @app.route("/echo_file", method=["POST"]) 134 | def echo_file(): 135 | args = {"myfile": fields.Raw()} 136 | result = parser.parse(args, location="files") 137 | myfile = result["myfile"] 138 | content = myfile.file.read().decode("utf8") 139 | return {"myfile": content} 140 | 141 | 142 | @app.route("/echo_nested", method=["POST"]) 143 | def echo_nested(): 144 | args = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})} 145 | return parser.parse(args) 146 | 147 | 148 | @app.route("/echo_nested_many", method=["POST"]) 149 | def echo_nested_many(): 150 | args = { 151 | "users": fields.Nested({"id": fields.Int(), "name": fields.Str()}, many=True) 152 | } 153 | return parser.parse(args) 154 | 155 | 156 | @app.error(400) 157 | @app.error(422) 158 | def handle_error(err): 159 | response.content_type = "application/json" 160 | return err.body 161 | -------------------------------------------------------------------------------- /tests/apps/django_app/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | 3 | DJANGO_MAJOR_VERSION = int(importlib.metadata.version("django").split(".")[0]) 4 | DJANGO_SUPPORTS_ASYNC = DJANGO_MAJOR_VERSION >= 3 5 | -------------------------------------------------------------------------------- /tests/apps/django_app/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marshmallow-code/webargs/a59bfca9c9c88e0cbaa97eb2cdba3683e8cc33a1/tests/apps/django_app/base/__init__.py -------------------------------------------------------------------------------- /tests/apps/django_app/base/settings.py: -------------------------------------------------------------------------------- 1 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 2 | import os 3 | 4 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 5 | SECRET_KEY = "s$28!(eonml-m3jgbq_)bj_&#=)sym2d*kx%@j+r&vwusxz%g$" 6 | DEBUG = True 7 | 8 | TEMPLATE_DEBUG = True 9 | 10 | ALLOWED_HOSTS = ["*"] 11 | # Application definition 12 | 13 | INSTALLED_APPS = ("django.contrib.contenttypes",) 14 | 15 | MIDDLEWARE_CLASSES = ( 16 | "django.contrib.sessions.middleware.SessionMiddleware", 17 | "django.middleware.common.CommonMiddleware", 18 | "django.contrib.messages.middleware.MessageMiddleware", 19 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 20 | ) 21 | 22 | ROOT_URLCONF = "tests.apps.django_app.base.urls" 23 | 24 | WSGI_APPLICATION = "tests.apps.django_app.base.wsgi.application" 25 | LANGUAGE_CODE = "en-us" 26 | 27 | TIME_ZONE = "UTC" 28 | 29 | USE_I18N = True 30 | 31 | USE_L10N = True 32 | 33 | USE_TZ = True 34 | STATIC_URL = "/static/" 35 | -------------------------------------------------------------------------------- /tests/apps/django_app/base/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from tests.apps.django_app.echo import views 4 | 5 | urlpatterns = [ 6 | re_path(r"^echo$", views.echo), 7 | re_path(r"^async_echo$", views.async_echo), 8 | re_path(r"^echo_form$", views.echo_form), 9 | re_path(r"^echo_json$", views.echo_json), 10 | re_path(r"^echo_json_or_form$", views.echo_json_or_form), 11 | re_path(r"^echo_use_args$", views.echo_use_args), 12 | re_path(r"^async_echo_use_args$", views.async_echo_use_args), 13 | re_path(r"^echo_use_args_validated$", views.echo_use_args_validated), 14 | re_path(r"^echo_ignoring_extra_data$", views.echo_ignoring_extra_data), 15 | re_path(r"^echo_use_kwargs$", views.echo_use_kwargs), 16 | re_path(r"^echo_multi$", views.echo_multi), 17 | re_path(r"^echo_multi_form$", views.echo_multi_form), 18 | re_path(r"^echo_multi_json$", views.echo_multi_json), 19 | re_path(r"^echo_many_schema$", views.echo_many_schema), 20 | re_path( 21 | r"^echo_use_args_with_path_param/(?P\w+)$", 22 | views.echo_use_args_with_path_param, 23 | ), 24 | re_path( 25 | r"^echo_use_kwargs_with_path_param/(?P\w+)$", 26 | views.echo_use_kwargs_with_path_param, 27 | ), 28 | re_path(r"^error$", views.always_error), 29 | re_path(r"^echo_headers$", views.echo_headers), 30 | re_path(r"^echo_cookie$", views.echo_cookie), 31 | re_path(r"^echo_file$", views.echo_file), 32 | re_path(r"^echo_nested$", views.echo_nested), 33 | re_path(r"^echo_nested_many$", views.echo_nested_many), 34 | re_path(r"^echo_cbv$", views.EchoCBV.as_view()), 35 | re_path(r"^echo_use_args_cbv$", views.EchoUseArgsCBV.as_view()), 36 | re_path( 37 | r"^echo_use_args_with_path_param_cbv/(?P\d+)$", 38 | views.EchoUseArgsWithParamCBV.as_view(), 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /tests/apps/django_app/base/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for helloapp project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.apps.django_app.base.settings") 13 | 14 | from django.core.wsgi import get_wsgi_application # noqa 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /tests/apps/django_app/echo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marshmallow-code/webargs/a59bfca9c9c88e0cbaa97eb2cdba3683e8cc33a1/tests/apps/django_app/echo/__init__.py -------------------------------------------------------------------------------- /tests/apps/django_app/echo/views.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import marshmallow as ma 4 | from django.http import HttpResponse 5 | from django.views.generic import View 6 | 7 | from webargs import fields, validate 8 | from webargs.core import json 9 | from webargs.djangoparser import parser, use_args, use_kwargs 10 | 11 | hello_args = {"name": fields.Str(load_default="World", validate=validate.Length(min=3))} 12 | hello_multiple = {"name": fields.List(fields.Str())} 13 | 14 | 15 | class HelloSchema(ma.Schema): 16 | name = fields.Str(load_default="World", validate=validate.Length(min=3)) 17 | 18 | 19 | hello_many_schema = HelloSchema(many=True) 20 | 21 | # variant which ignores unknown fields 22 | hello_exclude_schema = HelloSchema(unknown=ma.EXCLUDE) 23 | 24 | 25 | def json_response(data, **kwargs): 26 | return HttpResponse(json.dumps(data), content_type="application/json", **kwargs) 27 | 28 | 29 | def handle_view_errors(f): 30 | if asyncio.iscoroutinefunction(f): 31 | 32 | async def wrapped(*args, **kwargs): 33 | try: 34 | return await f(*args, **kwargs) 35 | except ma.ValidationError as err: 36 | return json_response(err.messages, status=422) 37 | except json.JSONDecodeError: 38 | return json_response({"json": ["Invalid JSON body."]}, status=400) 39 | 40 | else: 41 | 42 | def wrapped(*args, **kwargs): 43 | try: 44 | return f(*args, **kwargs) 45 | except ma.ValidationError as err: 46 | return json_response(err.messages, status=422) 47 | except json.JSONDecodeError: 48 | return json_response({"json": ["Invalid JSON body."]}, status=400) 49 | 50 | return wrapped 51 | 52 | 53 | @handle_view_errors 54 | def echo(request): 55 | return json_response(parser.parse(hello_args, request, location="query")) 56 | 57 | 58 | @handle_view_errors 59 | async def async_echo(request): 60 | return json_response( 61 | await parser.async_parse(hello_args, request, location="query") 62 | ) 63 | 64 | 65 | @handle_view_errors 66 | def echo_form(request): 67 | return json_response(parser.parse(hello_args, request, location="form")) 68 | 69 | 70 | @handle_view_errors 71 | def echo_json(request): 72 | return json_response(parser.parse(hello_args, request, location="json")) 73 | 74 | 75 | @handle_view_errors 76 | def echo_json_or_form(request): 77 | return json_response(parser.parse(hello_args, request, location="json_or_form")) 78 | 79 | 80 | @handle_view_errors 81 | @use_args(hello_args, location="query") 82 | def echo_use_args(request, args): 83 | return json_response(args) 84 | 85 | 86 | @handle_view_errors 87 | @use_args(hello_args, location="query") 88 | async def async_echo_use_args(request, args): 89 | return json_response(args) 90 | 91 | 92 | @handle_view_errors 93 | @use_args( 94 | {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form" 95 | ) 96 | def echo_use_args_validated(args): 97 | return json_response(args) 98 | 99 | 100 | @handle_view_errors 101 | def echo_ignoring_extra_data(request): 102 | return json_response(parser.parse(hello_exclude_schema, request, unknown=None)) 103 | 104 | 105 | @handle_view_errors 106 | @use_kwargs(hello_args, location="query") 107 | def echo_use_kwargs(request, name): 108 | return json_response({"name": name}) 109 | 110 | 111 | @handle_view_errors 112 | def echo_multi(request): 113 | return json_response(parser.parse(hello_multiple, request, location="query")) 114 | 115 | 116 | @handle_view_errors 117 | def echo_multi_form(request): 118 | return json_response(parser.parse(hello_multiple, request, location="form")) 119 | 120 | 121 | @handle_view_errors 122 | def echo_multi_json(request): 123 | return json_response(parser.parse(hello_multiple, request)) 124 | 125 | 126 | @handle_view_errors 127 | def echo_many_schema(request): 128 | return json_response(parser.parse(hello_many_schema, request)) 129 | 130 | 131 | @handle_view_errors 132 | @use_args({"value": fields.Int()}, location="query") 133 | def echo_use_args_with_path_param(request, args, name): 134 | return json_response(args) 135 | 136 | 137 | @handle_view_errors 138 | @use_kwargs({"value": fields.Int()}, location="query") 139 | def echo_use_kwargs_with_path_param(request, value, name): 140 | return json_response({"value": value}) 141 | 142 | 143 | @handle_view_errors 144 | def always_error(request): 145 | def always_fail(value): 146 | raise ma.ValidationError("something went wrong") 147 | 148 | argmap = {"text": fields.Str(validate=always_fail)} 149 | return parser.parse(argmap, request) 150 | 151 | 152 | @handle_view_errors 153 | def echo_headers(request): 154 | return json_response(parser.parse(hello_args, request, location="headers")) 155 | 156 | 157 | @handle_view_errors 158 | def echo_cookie(request): 159 | return json_response(parser.parse(hello_args, request, location="cookies")) 160 | 161 | 162 | @handle_view_errors 163 | def echo_file(request): 164 | args = {"myfile": fields.Raw()} 165 | result = parser.parse(args, request, location="files") 166 | myfile = result["myfile"] 167 | content = myfile.read().decode("utf8") 168 | return json_response({"myfile": content}) 169 | 170 | 171 | @handle_view_errors 172 | def echo_nested(request): 173 | argmap = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})} 174 | return json_response(parser.parse(argmap, request)) 175 | 176 | 177 | @handle_view_errors 178 | def echo_nested_many(request): 179 | argmap = { 180 | "users": fields.Nested({"id": fields.Int(), "name": fields.Str()}, many=True) 181 | } 182 | return json_response(parser.parse(argmap, request)) 183 | 184 | 185 | class EchoCBV(View): 186 | @handle_view_errors 187 | def get(self, request): 188 | location_kwarg = {} if request.method == "POST" else {"location": "query"} 189 | return json_response(parser.parse(hello_args, self.request, **location_kwarg)) 190 | 191 | post = get 192 | 193 | 194 | class EchoUseArgsCBV(View): 195 | @handle_view_errors 196 | @use_args(hello_args, location="query") 197 | def get(self, request, args): 198 | return json_response(args) 199 | 200 | @handle_view_errors 201 | @use_args(hello_args) 202 | def post(self, request, args): 203 | return json_response(args) 204 | 205 | 206 | class EchoUseArgsWithParamCBV(View): 207 | @handle_view_errors 208 | @use_args(hello_args, location="query") 209 | def get(self, request, args, pid): 210 | return json_response(args) 211 | 212 | @handle_view_errors 213 | @use_args(hello_args) 214 | def post(self, request, args, pid): 215 | return json_response(args) 216 | -------------------------------------------------------------------------------- /tests/apps/django_app/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /tests/apps/falcon_app.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | 3 | import falcon 4 | import marshmallow as ma 5 | 6 | from webargs import fields, validate 7 | from webargs.core import json 8 | from webargs.falconparser import parser, use_args, use_kwargs 9 | 10 | hello_args = {"name": fields.Str(load_default="World", validate=validate.Length(min=3))} 11 | hello_multiple = {"name": fields.List(fields.Str())} 12 | 13 | FALCON_MAJOR_VERSION = int(importlib.metadata.version("falcon").split(".")[0]) 14 | FALCON_SUPPORTS_ASYNC = FALCON_MAJOR_VERSION >= 3 15 | 16 | 17 | class HelloSchema(ma.Schema): 18 | name = fields.Str(load_default="World", validate=validate.Length(min=3)) 19 | 20 | 21 | hello_many_schema = HelloSchema(many=True) 22 | 23 | # variant which ignores unknown fields 24 | hello_exclude_schema = HelloSchema(unknown=ma.EXCLUDE) 25 | 26 | 27 | def set_text(resp, value): 28 | if FALCON_MAJOR_VERSION >= 3: 29 | resp.text = value 30 | else: 31 | resp.body = value 32 | 33 | 34 | class Echo: 35 | def on_get(self, req, resp): 36 | parsed = parser.parse(hello_args, req, location="query") 37 | set_text(resp, json.dumps(parsed)) 38 | 39 | 40 | class AsyncEcho: 41 | async def on_get(self, req, resp): 42 | parsed = await parser.async_parse(hello_args, req, location="query") 43 | set_text(resp, json.dumps(parsed)) 44 | 45 | 46 | class EchoForm: 47 | def on_post(self, req, resp): 48 | parsed = parser.parse(hello_args, req, location="form") 49 | set_text(resp, json.dumps(parsed)) 50 | 51 | 52 | class EchoJSON: 53 | def on_post(self, req, resp): 54 | parsed = parser.parse(hello_args, req, location="json") 55 | set_text(resp, json.dumps(parsed)) 56 | 57 | 58 | class EchoMedia: 59 | def on_post(self, req, resp): 60 | parsed = parser.parse(hello_args, req, location="media") 61 | set_text(resp, json.dumps(parsed)) 62 | 63 | 64 | class EchoJSONOrForm: 65 | def on_post(self, req, resp): 66 | parsed = parser.parse(hello_args, req, location="json_or_form") 67 | set_text(resp, json.dumps(parsed)) 68 | 69 | 70 | class EchoUseArgs: 71 | @use_args(hello_args, location="query") 72 | def on_get(self, req, resp, args): 73 | set_text(resp, json.dumps(args)) 74 | 75 | 76 | class AsyncEchoUseArgs: 77 | @use_args(hello_args, location="query") 78 | async def on_get(self, req, resp, args): 79 | set_text(resp, json.dumps(args)) 80 | 81 | 82 | class EchoUseKwargs: 83 | @use_kwargs(hello_args, location="query") 84 | def on_get(self, req, resp, name): 85 | set_text(resp, json.dumps({"name": name})) 86 | 87 | 88 | class EchoUseArgsValidated: 89 | @use_args( 90 | {"value": fields.Int()}, 91 | validate=lambda args: args["value"] > 42, 92 | location="form", 93 | ) 94 | def on_post(self, req, resp, args): 95 | set_text(resp, json.dumps(args)) 96 | 97 | 98 | class EchoJSONIgnoreExtraData: 99 | def on_post(self, req, resp): 100 | set_text( 101 | resp, json.dumps(parser.parse(hello_exclude_schema, req, unknown=None)) 102 | ) 103 | 104 | 105 | class EchoMulti: 106 | def on_get(self, req, resp): 107 | set_text(resp, json.dumps(parser.parse(hello_multiple, req, location="query"))) 108 | 109 | 110 | class EchoMultiForm: 111 | def on_post(self, req, resp): 112 | set_text(resp, json.dumps(parser.parse(hello_multiple, req, location="form"))) 113 | 114 | 115 | class EchoMultiJSON: 116 | def on_post(self, req, resp): 117 | set_text(resp, json.dumps(parser.parse(hello_multiple, req))) 118 | 119 | 120 | class EchoManySchema: 121 | def on_post(self, req, resp): 122 | set_text(resp, json.dumps(parser.parse(hello_many_schema, req))) 123 | 124 | 125 | class EchoUseArgsWithPathParam: 126 | @use_args({"value": fields.Int()}, location="query") 127 | def on_get(self, req, resp, args, name): 128 | set_text(resp, json.dumps(args)) 129 | 130 | 131 | class EchoUseKwargsWithPathParam: 132 | @use_kwargs({"value": fields.Int()}, location="query") 133 | def on_get(self, req, resp, value, name): 134 | set_text(resp, json.dumps({"value": value})) 135 | 136 | 137 | class AlwaysError: 138 | def on_get(self, req, resp): 139 | def always_fail(value): 140 | raise ma.ValidationError("something went wrong") 141 | 142 | args = {"text": fields.Str(validate=always_fail)} 143 | set_text(resp, json.dumps(parser.parse(args, req))) 144 | 145 | on_post = on_get 146 | 147 | 148 | class EchoHeaders: 149 | def on_get(self, req, resp): 150 | class HeaderSchema(ma.Schema): 151 | NAME = fields.Str(load_default="World") 152 | 153 | set_text( 154 | resp, json.dumps(parser.parse(HeaderSchema(), req, location="headers")) 155 | ) 156 | 157 | 158 | class EchoCookie: 159 | def on_get(self, req, resp): 160 | set_text(resp, json.dumps(parser.parse(hello_args, req, location="cookies"))) 161 | 162 | 163 | class EchoNested: 164 | def on_post(self, req, resp): 165 | args = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})} 166 | set_text(resp, json.dumps(parser.parse(args, req))) 167 | 168 | 169 | class EchoNestedMany: 170 | def on_post(self, req, resp): 171 | args = { 172 | "users": fields.Nested( 173 | {"id": fields.Int(), "name": fields.Str()}, many=True 174 | ) 175 | } 176 | set_text(resp, json.dumps(parser.parse(args, req))) 177 | 178 | 179 | def use_args_hook(args, context_key="args", **kwargs): 180 | def hook(req, resp, resource, params): 181 | parsed_args = parser.parse(args, req=req, **kwargs) 182 | req.context[context_key] = parsed_args 183 | 184 | return hook 185 | 186 | 187 | @falcon.before(use_args_hook(hello_args, location="query")) 188 | class EchoUseArgsHook: 189 | def on_get(self, req, resp): 190 | set_text(resp, json.dumps(req.context["args"])) 191 | 192 | 193 | def create_app(): 194 | if FALCON_MAJOR_VERSION >= 3: 195 | app = falcon.App() 196 | else: 197 | app = falcon.API() 198 | 199 | app.add_route("/echo", Echo()) 200 | app.add_route("/echo_form", EchoForm()) 201 | app.add_route("/echo_json", EchoJSON()) 202 | app.add_route("/echo_media", EchoMedia()) 203 | app.add_route("/echo_json_or_form", EchoJSONOrForm()) 204 | app.add_route("/echo_use_args", EchoUseArgs()) 205 | app.add_route("/echo_use_kwargs", EchoUseKwargs()) 206 | app.add_route("/echo_use_args_validated", EchoUseArgsValidated()) 207 | app.add_route("/echo_ignoring_extra_data", EchoJSONIgnoreExtraData()) 208 | app.add_route("/echo_multi", EchoMulti()) 209 | app.add_route("/echo_multi_form", EchoMultiForm()) 210 | app.add_route("/echo_multi_json", EchoMultiJSON()) 211 | app.add_route("/echo_many_schema", EchoManySchema()) 212 | app.add_route("/echo_use_args_with_path_param/{name}", EchoUseArgsWithPathParam()) 213 | app.add_route( 214 | "/echo_use_kwargs_with_path_param/{name}", EchoUseKwargsWithPathParam() 215 | ) 216 | app.add_route("/error", AlwaysError()) 217 | app.add_route("/echo_headers", EchoHeaders()) 218 | app.add_route("/echo_cookie", EchoCookie()) 219 | app.add_route("/echo_nested", EchoNested()) 220 | app.add_route("/echo_nested_many", EchoNestedMany()) 221 | app.add_route("/echo_use_args_hook", EchoUseArgsHook()) 222 | return app 223 | 224 | 225 | def create_async_app(): 226 | # defer import (async-capable versions only) 227 | import falcon.asgi 228 | 229 | app = falcon.asgi.App() 230 | app.add_route("/async_echo", AsyncEcho()) 231 | app.add_route("/async_echo_use_args", AsyncEchoUseArgs()) 232 | return app 233 | -------------------------------------------------------------------------------- /tests/apps/flask_app.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | 3 | import marshmallow as ma 4 | from flask import Flask, Response, request 5 | from flask import jsonify as J 6 | from flask.views import MethodView 7 | 8 | from webargs import fields, validate 9 | from webargs.core import json 10 | from webargs.flaskparser import ( 11 | parser, 12 | use_args, 13 | use_kwargs, 14 | ) 15 | 16 | FLASK_MAJOR_VERSION = int(importlib.metadata.version("flask").split(".")[0]) 17 | FLASK_SUPPORTS_ASYNC = FLASK_MAJOR_VERSION >= 2 18 | 19 | 20 | class TestAppConfig: 21 | TESTING = True 22 | 23 | 24 | hello_args = {"name": fields.Str(load_default="World", validate=validate.Length(min=3))} 25 | hello_multiple = {"name": fields.List(fields.Str())} 26 | 27 | 28 | class HelloSchema(ma.Schema): 29 | name = fields.Str(load_default="World", validate=validate.Length(min=3)) 30 | 31 | 32 | hello_many_schema = HelloSchema(many=True) 33 | 34 | app = Flask(__name__) 35 | app.config.from_object(TestAppConfig) 36 | 37 | 38 | @app.route("/echo", methods=["GET"]) 39 | def echo(): 40 | return J(parser.parse(hello_args, location="query")) 41 | 42 | 43 | @app.route("/echo_form", methods=["POST"]) 44 | def echo_form(): 45 | return J(parser.parse(hello_args, location="form")) 46 | 47 | 48 | @app.route("/echo_json", methods=["POST"]) 49 | def echo_json(): 50 | return J(parser.parse(hello_args, location="json")) 51 | 52 | 53 | @app.route("/echo_json_or_form", methods=["POST"]) 54 | def echo_json_or_form(): 55 | return J(parser.parse(hello_args, location="json_or_form")) 56 | 57 | 58 | @app.route("/echo_use_args", methods=["GET"]) 59 | @use_args(hello_args, location="query") 60 | def echo_use_args(args): 61 | return J(args) 62 | 63 | 64 | def validator(args): 65 | if args["value"] <= 42: 66 | raise ma.ValidationError("invalid") 67 | 68 | 69 | @app.route("/echo_use_args_validated", methods=["POST"]) 70 | @use_args({"value": fields.Int()}, validate=validator, location="form") 71 | def echo_use_args_validated(args): 72 | return J(args) 73 | 74 | 75 | @app.route("/echo_ignoring_extra_data", methods=["POST"]) 76 | def echo_json_ignore_extra_data(): 77 | return J(parser.parse(hello_args, unknown=ma.EXCLUDE)) 78 | 79 | 80 | @app.route("/echo_use_kwargs", methods=["GET"]) 81 | @use_kwargs(hello_args, location="query") 82 | def echo_use_kwargs(name): 83 | return J({"name": name}) 84 | 85 | 86 | @app.route("/echo_multi", methods=["GET"]) 87 | def multi(): 88 | return J(parser.parse(hello_multiple, location="query")) 89 | 90 | 91 | @app.route("/echo_multi_form", methods=["POST"]) 92 | def multi_form(): 93 | return J(parser.parse(hello_multiple, location="form")) 94 | 95 | 96 | @app.route("/echo_multi_json", methods=["POST"]) 97 | def multi_json(): 98 | return J(parser.parse(hello_multiple)) 99 | 100 | 101 | @app.route("/echo_many_schema", methods=["GET", "POST"]) 102 | def many_nested(): 103 | arguments = parser.parse(hello_many_schema) 104 | return Response(json.dumps(arguments), content_type="application/json") 105 | 106 | 107 | @app.route("/echo_use_args_with_path_param/") 108 | @use_args({"value": fields.Int()}, location="query") 109 | def echo_use_args_with_path(args, name): 110 | return J(args) 111 | 112 | 113 | @app.route("/echo_use_kwargs_with_path_param/") 114 | @use_kwargs({"value": fields.Int()}, location="query") 115 | def echo_use_kwargs_with_path(name, value): 116 | return J({"value": value}) 117 | 118 | 119 | @app.route("/error", methods=["GET", "POST"]) 120 | def error(): 121 | def always_fail(value): 122 | raise ma.ValidationError("something went wrong") 123 | 124 | args = {"text": fields.Str(validate=always_fail)} 125 | return J(parser.parse(args)) 126 | 127 | 128 | @app.route("/echo_headers") 129 | def echo_headers(): 130 | return J(parser.parse(hello_args, location="headers")) 131 | 132 | 133 | # as above, but in this case, turn off the default `EXCLUDE` behavior for 134 | # `headers`, so that errors will be raised 135 | @app.route("/echo_headers_raising") 136 | @use_args(HelloSchema(), location="headers", unknown=None) 137 | def echo_headers_raising(args): 138 | return J(args) 139 | 140 | 141 | if FLASK_SUPPORTS_ASYNC: 142 | 143 | @app.route("/echo_headers_raising_async") 144 | @use_args(HelloSchema(), location="headers", unknown=None) 145 | async def echo_headers_raising_async(args): 146 | return J(args) 147 | 148 | 149 | @app.route("/echo_cookie") 150 | def echo_cookie(): 151 | return J(parser.parse(hello_args, request, location="cookies")) 152 | 153 | 154 | @app.route("/echo_file", methods=["POST"]) 155 | def echo_file(): 156 | args = {"myfile": fields.Raw()} 157 | result = parser.parse(args, location="files") 158 | fp = result["myfile"] 159 | content = fp.read().decode("utf8") 160 | return J({"myfile": content}) 161 | 162 | 163 | @app.route("/echo_view_arg/") 164 | def echo_view_arg(view_arg): 165 | return J(parser.parse({"view_arg": fields.Int()}, location="view_args")) 166 | 167 | 168 | if FLASK_SUPPORTS_ASYNC: 169 | 170 | @app.route("/echo_view_arg_async/") 171 | async def echo_view_arg_async(view_arg): 172 | parsed_view_arg = await parser.async_parse( 173 | {"view_arg": fields.Int()}, location="view_args" 174 | ) 175 | return J(parsed_view_arg) 176 | 177 | 178 | @app.route("/echo_view_arg_use_args/") 179 | @use_args({"view_arg": fields.Int()}, location="view_args") 180 | def echo_view_arg_with_use_args(args, **kwargs): 181 | return J(args) 182 | 183 | 184 | if FLASK_SUPPORTS_ASYNC: 185 | 186 | @app.route("/echo_view_arg_use_args_async/") 187 | @use_args({"view_arg": fields.Int()}, location="view_args") 188 | async def echo_view_arg_with_use_args_async(args, **kwargs): 189 | return J(args) 190 | 191 | 192 | @app.route("/echo_nested", methods=["POST"]) 193 | def echo_nested(): 194 | args = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})} 195 | return J(parser.parse(args)) 196 | 197 | 198 | @app.route("/echo_nested_many", methods=["POST"]) 199 | def echo_nested_many(): 200 | args = { 201 | "users": fields.Nested({"id": fields.Int(), "name": fields.Str()}, many=True) 202 | } 203 | return J(parser.parse(args)) 204 | 205 | 206 | @app.route("/echo_nested_many_data_key", methods=["POST"]) 207 | def echo_nested_many_with_data_key(): 208 | args = { 209 | "x_field": fields.Nested({"id": fields.Int()}, many=True, data_key="X-Field") 210 | } 211 | return J(parser.parse(args)) 212 | 213 | 214 | if FLASK_SUPPORTS_ASYNC: 215 | 216 | @app.route("/echo_nested_many_data_key_async", methods=["POST"]) 217 | async def echo_nested_many_with_data_key_async(): 218 | args = { 219 | "x_field": fields.Nested( 220 | {"id": fields.Int()}, many=True, data_key="X-Field" 221 | ) 222 | } 223 | return J(await parser.async_parse(args)) 224 | 225 | 226 | class EchoMethodViewUseArgs(MethodView): 227 | @use_args({"val": fields.Int()}) 228 | def post(self, args): 229 | return J(args) 230 | 231 | 232 | app.add_url_rule( 233 | "/echo_method_view_use_args", 234 | view_func=EchoMethodViewUseArgs.as_view("echo_method_view_use_args"), 235 | ) 236 | 237 | 238 | if FLASK_SUPPORTS_ASYNC: 239 | 240 | class EchoMethodViewUseArgsAsync(MethodView): 241 | @use_args({"val": fields.Int()}) 242 | async def post(self, args): 243 | return J(args) 244 | 245 | app.add_url_rule( 246 | "/echo_method_view_use_args_async", 247 | view_func=EchoMethodViewUseArgsAsync.as_view("echo_method_view_use_args_async"), 248 | ) 249 | 250 | 251 | class EchoMethodViewUseKwargs(MethodView): 252 | @use_kwargs({"val": fields.Int()}) 253 | def post(self, val): 254 | return J({"val": val}) 255 | 256 | 257 | app.add_url_rule( 258 | "/echo_method_view_use_kwargs", 259 | view_func=EchoMethodViewUseKwargs.as_view("echo_method_view_use_kwargs"), 260 | ) 261 | 262 | if FLASK_SUPPORTS_ASYNC: 263 | 264 | class EchoMethodViewUseKwargsAsync(MethodView): 265 | @use_kwargs({"val": fields.Int()}) 266 | async def post(self, val): 267 | return J({"val": val}) 268 | 269 | app.add_url_rule( 270 | "/echo_method_view_use_kwargs_async", 271 | view_func=EchoMethodViewUseKwargsAsync.as_view( 272 | "echo_method_view_use_kwargs_async" 273 | ), 274 | ) 275 | 276 | 277 | @app.route("/echo_use_kwargs_missing", methods=["post"]) 278 | @use_kwargs({"username": fields.Str(required=True), "password": fields.Str()}) 279 | def echo_use_kwargs_missing(username, **kwargs): 280 | assert "password" not in kwargs 281 | return J({"username": username}) 282 | 283 | 284 | if FLASK_SUPPORTS_ASYNC: 285 | 286 | @app.route("/echo_use_kwargs_missing_async", methods=["post"]) 287 | @use_kwargs({"username": fields.Str(required=True), "password": fields.Str()}) 288 | async def echo_use_kwargs_missing_async(username, **kwargs): 289 | assert "password" not in kwargs 290 | return J({"username": username}) 291 | 292 | 293 | # Return validation errors as JSON 294 | @app.errorhandler(422) 295 | @app.errorhandler(400) 296 | def handle_error(err): 297 | if err.code == 422: 298 | assert isinstance(err.data["schema"], ma.Schema) 299 | 300 | return J(err.data["messages"]), err.code 301 | -------------------------------------------------------------------------------- /tests/apps/pyramid_app.py: -------------------------------------------------------------------------------- 1 | import marshmallow as ma 2 | from pyramid.config import Configurator 3 | from pyramid.httpexceptions import HTTPBadRequest 4 | 5 | from webargs import fields, validate 6 | from webargs.core import json 7 | from webargs.pyramidparser import parser, use_args, use_kwargs 8 | 9 | hello_args = {"name": fields.Str(load_default="World", validate=validate.Length(min=3))} 10 | hello_multiple = {"name": fields.List(fields.Str())} 11 | 12 | 13 | class HelloSchema(ma.Schema): 14 | name = fields.Str(load_default="World", validate=validate.Length(min=3)) 15 | 16 | 17 | hello_many_schema = HelloSchema(many=True) 18 | 19 | # variant which ignores unknown fields 20 | hello_exclude_schema = HelloSchema(unknown=ma.EXCLUDE) 21 | 22 | 23 | def echo(request): 24 | return parser.parse(hello_args, request, location="query") 25 | 26 | 27 | def echo_form(request): 28 | return parser.parse(hello_args, request, location="form") 29 | 30 | 31 | def echo_json(request): 32 | try: 33 | return parser.parse(hello_args, request, location="json") 34 | except json.JSONDecodeError as err: 35 | error = HTTPBadRequest() 36 | error.body = json.dumps(["Invalid JSON."]).encode("utf-8") 37 | error.content_type = "application/json" 38 | raise error from err 39 | 40 | 41 | def echo_json_or_form(request): 42 | try: 43 | return parser.parse(hello_args, request, location="json_or_form") 44 | except json.JSONDecodeError as err: 45 | error = HTTPBadRequest() 46 | error.body = json.dumps(["Invalid JSON."]).encode("utf-8") 47 | error.content_type = "application/json" 48 | raise error from err 49 | 50 | 51 | def echo_json_ignore_extra_data(request): 52 | try: 53 | return parser.parse(hello_exclude_schema, request, unknown=None) 54 | except json.JSONDecodeError as err: 55 | error = HTTPBadRequest() 56 | error.body = json.dumps(["Invalid JSON."]).encode("utf-8") 57 | error.content_type = "application/json" 58 | raise error from err 59 | 60 | 61 | def echo_query(request): 62 | return parser.parse(hello_args, request, location="query") 63 | 64 | 65 | @use_args(hello_args, location="query") 66 | def echo_use_args(request, args): 67 | return args 68 | 69 | 70 | @use_args( 71 | {"value": fields.Int()}, validate=lambda args: args["value"] > 42, location="form" 72 | ) 73 | def echo_use_args_validated(request, args): 74 | return args 75 | 76 | 77 | @use_kwargs(hello_args, location="query") 78 | def echo_use_kwargs(request, name): 79 | return {"name": name} 80 | 81 | 82 | def echo_multi(request): 83 | return parser.parse(hello_multiple, request, location="query") 84 | 85 | 86 | def echo_multi_form(request): 87 | return parser.parse(hello_multiple, request, location="form") 88 | 89 | 90 | def echo_multi_json(request): 91 | return parser.parse(hello_multiple, request) 92 | 93 | 94 | def echo_many_schema(request): 95 | return parser.parse(hello_many_schema, request) 96 | 97 | 98 | @use_args({"value": fields.Int()}, location="query") 99 | def echo_use_args_with_path_param(request, args): 100 | return args 101 | 102 | 103 | @use_kwargs({"value": fields.Int()}, location="query") 104 | def echo_use_kwargs_with_path_param(request, value): 105 | return {"value": value} 106 | 107 | 108 | def always_error(request): 109 | def always_fail(value): 110 | raise ma.ValidationError("something went wrong") 111 | 112 | argmap = {"text": fields.Str(validate=always_fail)} 113 | return parser.parse(argmap, request) 114 | 115 | 116 | def echo_headers(request): 117 | return parser.parse(hello_args, request, location="headers") 118 | 119 | 120 | def echo_cookie(request): 121 | return parser.parse(hello_args, request, location="cookies") 122 | 123 | 124 | def echo_file(request): 125 | args = {"myfile": fields.Raw()} 126 | result = parser.parse(args, request, location="files") 127 | myfile = result["myfile"] 128 | content = myfile.file.read().decode("utf8") 129 | return {"myfile": content} 130 | 131 | 132 | def echo_nested(request): 133 | argmap = {"name": fields.Nested({"first": fields.Str(), "last": fields.Str()})} 134 | return parser.parse(argmap, request) 135 | 136 | 137 | def echo_nested_many(request): 138 | argmap = { 139 | "users": fields.Nested({"id": fields.Int(), "name": fields.Str()}, many=True) 140 | } 141 | return parser.parse(argmap, request) 142 | 143 | 144 | def echo_matchdict(request): 145 | return parser.parse({"mymatch": fields.Int()}, request, location="matchdict") 146 | 147 | 148 | class EchoCallable: 149 | def __init__(self, request): 150 | self.request = request 151 | 152 | @use_args({"value": fields.Int()}, location="query") 153 | def __call__(self, args): 154 | return args 155 | 156 | 157 | def add_route(config, route, view, route_name=None, renderer="json"): 158 | """Helper for adding a new route-view pair.""" 159 | route_name = route_name or view.__name__ 160 | config.add_route(route_name, route) 161 | config.add_view(view, route_name=route_name, renderer=renderer) 162 | 163 | 164 | def create_app(): 165 | config = Configurator() 166 | 167 | add_route(config, "/echo", echo) 168 | add_route(config, "/echo_form", echo_form) 169 | add_route(config, "/echo_json", echo_json) 170 | add_route(config, "/echo_json_or_form", echo_json_or_form) 171 | add_route(config, "/echo_query", echo_query) 172 | add_route(config, "/echo_ignoring_extra_data", echo_json_ignore_extra_data) 173 | add_route(config, "/echo_use_args", echo_use_args) 174 | add_route(config, "/echo_use_args_validated", echo_use_args_validated) 175 | add_route(config, "/echo_use_kwargs", echo_use_kwargs) 176 | add_route(config, "/echo_multi", echo_multi) 177 | add_route(config, "/echo_multi_form", echo_multi_form) 178 | add_route(config, "/echo_multi_json", echo_multi_json) 179 | add_route(config, "/echo_many_schema", echo_many_schema) 180 | add_route( 181 | config, "/echo_use_args_with_path_param/{name}", echo_use_args_with_path_param 182 | ) 183 | add_route( 184 | config, 185 | "/echo_use_kwargs_with_path_param/{name}", 186 | echo_use_kwargs_with_path_param, 187 | ) 188 | add_route(config, "/error", always_error) 189 | add_route(config, "/echo_headers", echo_headers) 190 | add_route(config, "/echo_cookie", echo_cookie) 191 | add_route(config, "/echo_file", echo_file) 192 | add_route(config, "/echo_nested", echo_nested) 193 | add_route(config, "/echo_nested_many", echo_nested_many) 194 | add_route(config, "/echo_callable", EchoCallable) 195 | add_route(config, "/echo_matchdict/{mymatch}", echo_matchdict) 196 | 197 | return config.make_wsgi_app() 198 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest.register_assert_rewrite("webargs.testing") 4 | -------------------------------------------------------------------------------- /tests/test_aiohttpparser.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | from unittest import mock 3 | 4 | import pytest 5 | import webtest 6 | import webtest_aiohttp 7 | 8 | from tests.apps.aiohttp_app import create_app 9 | from webargs import fields 10 | from webargs.aiohttpparser import AIOHTTPParser 11 | from webargs.testing import CommonTestCase 12 | 13 | 14 | @pytest.fixture 15 | def web_request(): 16 | req = mock.Mock() 17 | req.query = {} 18 | yield req 19 | req.query = {} 20 | 21 | 22 | class TestAIOHTTPParser(CommonTestCase): 23 | def create_app(self): 24 | return create_app() 25 | 26 | def create_testapp(self, app, event_loop): 27 | return webtest_aiohttp.TestApp(app, loop=event_loop) 28 | 29 | @pytest.fixture 30 | def testapp(self, event_loop): 31 | return self.create_testapp(self.create_app(), event_loop) 32 | 33 | @pytest.mark.skip(reason="files location not supported for aiohttpparser") 34 | def test_parse_files(self, testapp): 35 | pass 36 | 37 | def test_parse_match_info(self, testapp): 38 | assert testapp.get("/echo_match_info/42").json == {"mymatch": 42} 39 | 40 | def test_use_args_on_method_handler(self, testapp): 41 | assert testapp.get("/echo_method").json == {"name": "World"} 42 | assert testapp.get("/echo_method?name=Steve").json == {"name": "Steve"} 43 | assert testapp.get("/echo_method_view").json == {"name": "World"} 44 | assert testapp.get("/echo_method_view?name=Steve").json == {"name": "Steve"} 45 | 46 | # regression test for https://github.com/marshmallow-code/webargs/issues/165 47 | def test_multiple_args(self, testapp): 48 | res = testapp.post_json("/echo_multiple_args", {"first": "1", "last": "2"}) 49 | assert res.json == {"first": "1", "last": "2"} 50 | 51 | # regression test for https://github.com/marshmallow-code/webargs/issues/145 52 | def test_nested_many_with_data_key(self, testapp): 53 | res = testapp.post_json("/echo_nested_many_data_key", {"X-Field": [{"id": 24}]}) 54 | assert res.json == {"x_field": [{"id": 24}]} 55 | 56 | res = testapp.post_json("/echo_nested_many_data_key", {}) 57 | assert res.json == {} 58 | 59 | def test_schema_as_kwargs_view(self, testapp): 60 | assert testapp.get("/echo_use_schema_as_kwargs").json == {"name": "World"} 61 | assert testapp.get("/echo_use_schema_as_kwargs?name=Chandler").json == { 62 | "name": "Chandler" 63 | } 64 | 65 | # https://github.com/marshmallow-code/webargs/pull/297 66 | def test_empty_json_body(self, testapp): 67 | environ = {"CONTENT_TYPE": "application/json", "wsgi.input": BytesIO(b"")} 68 | req = webtest.TestRequest.blank("/echo", environ) 69 | resp = testapp.do_request(req) 70 | assert resp.json == {"name": "World"} 71 | 72 | def test_use_args_multiple(self, testapp): 73 | res = testapp.post_json( 74 | "/echo_use_args_multiple?page=2&q=10", {"name": "Steve"} 75 | ) 76 | assert res.json == { 77 | "query_parsed": {"page": 2, "q": 10}, 78 | "json_parsed": {"name": "Steve"}, 79 | } 80 | 81 | def test_validation_error_returns_422_response(self, testapp): 82 | res = testapp.post_json("/echo_json", {"name": "b"}, expect_errors=True) 83 | assert res.status_code == 422 84 | assert res.json == {"json": {"name": ["Shorter than minimum length 3."]}} 85 | 86 | 87 | @pytest.mark.asyncio 88 | async def test_aiohttpparser_synchronous_error_handler(web_request): 89 | parser = AIOHTTPParser() 90 | 91 | class CustomError(Exception): 92 | pass 93 | 94 | @parser.error_handler 95 | def custom_handle_error(error, req, schema, *, error_status_code, error_headers): 96 | raise CustomError("foo") 97 | 98 | with pytest.raises(CustomError): 99 | await parser.parse( 100 | {"foo": fields.Int(required=True)}, web_request, location="query" 101 | ) 102 | 103 | 104 | @pytest.mark.asyncio 105 | async def test_aiohttpparser_asynchronous_error_handler(web_request): 106 | parser = AIOHTTPParser() 107 | 108 | class CustomError(Exception): 109 | pass 110 | 111 | @parser.error_handler 112 | async def custom_handle_error( 113 | error, req, schema, *, error_status_code, error_headers 114 | ): 115 | async def inner(): 116 | raise CustomError("foo") 117 | 118 | await inner() 119 | 120 | with pytest.raises(CustomError): 121 | await parser.parse( 122 | {"foo": fields.Int(required=True)}, web_request, location="query" 123 | ) 124 | -------------------------------------------------------------------------------- /tests/test_bottleparser.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from webargs.testing import CommonTestCase 4 | 5 | from .apps.bottle_app import app 6 | 7 | 8 | class TestBottleParser(CommonTestCase): 9 | def create_app(self): 10 | return app 11 | 12 | @pytest.mark.skip(reason="Parsing vendor media types is not supported in bottle") 13 | def test_parse_json_with_vendor_media_type(self, testapp): 14 | pass 15 | -------------------------------------------------------------------------------- /tests/test_djangoparser.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from tests.apps.django_app import DJANGO_SUPPORTS_ASYNC 4 | from tests.apps.django_app.base.wsgi import application 5 | from webargs.testing import CommonTestCase 6 | 7 | 8 | class TestDjangoParser(CommonTestCase): 9 | def create_app(self): 10 | return application 11 | 12 | @pytest.mark.skip( 13 | reason="skipping because DjangoParser does not implement handle_error" 14 | ) 15 | def test_use_args_with_validation(self): 16 | pass 17 | 18 | def test_parsing_in_class_based_view(self, testapp): 19 | assert testapp.get("/echo_cbv?name=Fred").json == {"name": "Fred"} 20 | assert testapp.post_json("/echo_cbv", {"name": "Fred"}).json == {"name": "Fred"} 21 | 22 | def test_use_args_in_class_based_view(self, testapp): 23 | res = testapp.get("/echo_use_args_cbv?name=Fred") 24 | assert res.json == {"name": "Fred"} 25 | res = testapp.post_json("/echo_use_args_cbv", {"name": "Fred"}) 26 | assert res.json == {"name": "Fred"} 27 | 28 | def test_use_args_in_class_based_view_with_path_param(self, testapp): 29 | res = testapp.get("/echo_use_args_with_path_param_cbv/42?name=Fred") 30 | assert res.json == {"name": "Fred"} 31 | 32 | @pytest.mark.skipif( 33 | not DJANGO_SUPPORTS_ASYNC, reason="requires a django version with async support" 34 | ) 35 | def test_parse_querystring_args_async(self, testapp): 36 | assert testapp.get("/async_echo?name=Fred").json == {"name": "Fred"} 37 | 38 | @pytest.mark.skipif( 39 | not DJANGO_SUPPORTS_ASYNC, reason="requires a django version with async support" 40 | ) 41 | def test_async_use_args_decorator(self, testapp): 42 | assert testapp.get("/async_echo_use_args?name=Fred").json == {"name": "Fred"} 43 | -------------------------------------------------------------------------------- /tests/test_falconparser.py: -------------------------------------------------------------------------------- 1 | import falcon.testing 2 | import pytest 3 | 4 | from tests.apps.falcon_app import FALCON_SUPPORTS_ASYNC, create_app, create_async_app 5 | from webargs.testing import CommonTestCase 6 | 7 | 8 | class TestFalconParser(CommonTestCase): 9 | def create_app(self): 10 | return create_app() 11 | 12 | @pytest.mark.skip(reason="files location not supported for falconparser") 13 | def test_parse_files(self, testapp): 14 | pass 15 | 16 | def test_use_args_hook(self, testapp): 17 | assert testapp.get("/echo_use_args_hook?name=Fred").json == {"name": "Fred"} 18 | 19 | def test_parse_media(self, testapp): 20 | assert testapp.post_json("/echo_media", {"name": "Fred"}).json == { 21 | "name": "Fred" 22 | } 23 | 24 | def test_parse_media_missing(self, testapp): 25 | assert testapp.post("/echo_media", "").json == {"name": "World"} 26 | 27 | def test_parse_media_empty(self, testapp): 28 | assert testapp.post_json("/echo_media", {}).json == {"name": "World"} 29 | 30 | def test_parse_media_error_unexpected_int(self, testapp): 31 | res = testapp.post_json("/echo_media", 1, expect_errors=True) 32 | assert res.status_code == 422 33 | 34 | # https://github.com/marshmallow-code/webargs/issues/427 35 | @pytest.mark.parametrize("path", ["/echo_json", "/echo_media"]) 36 | def test_parse_json_with_nonutf8_chars(self, testapp, path): 37 | res = testapp.post( 38 | path, 39 | b"\xfe", 40 | headers={"Accept": "application/json", "Content-Type": "application/json"}, 41 | expect_errors=True, 42 | ) 43 | 44 | assert res.status_code == 400 45 | if path.endswith("json"): 46 | assert res.json["errors"] == {"json": ["Invalid JSON body."]} 47 | 48 | # https://github.com/sloria/webargs/issues/329 49 | @pytest.mark.parametrize("path", ["/echo_json", "/echo_media"]) 50 | def test_invalid_json(self, testapp, path): 51 | res = testapp.post( 52 | path, 53 | '{"foo": "bar", }', 54 | headers={"Accept": "application/json", "Content-Type": "application/json"}, 55 | expect_errors=True, 56 | ) 57 | assert res.status_code == 400 58 | if path.endswith("json"): 59 | assert res.json["errors"] == {"json": ["Invalid JSON body."]} 60 | 61 | # Falcon converts headers to all-caps 62 | def test_parsing_headers(self, testapp): 63 | res = testapp.get("/echo_headers", headers={"name": "Fred"}) 64 | assert res.json == {"NAME": "Fred"} 65 | 66 | # `falcon.testing.TestClient.simulate_request` parses request with `wsgiref` 67 | def test_body_parsing_works_with_simulate(self): 68 | app = self.create_app() 69 | client = falcon.testing.TestClient(app) 70 | res = client.simulate_post( 71 | "/echo_json", 72 | json={"name": "Fred"}, 73 | ) 74 | assert res.json == {"name": "Fred"} 75 | 76 | @pytest.mark.skipif( 77 | not FALCON_SUPPORTS_ASYNC, reason="requires a falcon version with async support" 78 | ) 79 | def test_parse_querystring_args_async(self): 80 | app = create_async_app() 81 | client = falcon.testing.TestClient(app) 82 | assert client.simulate_get("/async_echo?name=Fred").json == {"name": "Fred"} 83 | 84 | @pytest.mark.skipif( 85 | not FALCON_SUPPORTS_ASYNC, reason="requires a falcon version with async support" 86 | ) 87 | def test_async_use_args_decorator(self): 88 | app = create_async_app() 89 | client = falcon.testing.TestClient(app) 90 | assert client.simulate_get("/async_echo_use_args?name=Fred").json == { 91 | "name": "Fred" 92 | } 93 | -------------------------------------------------------------------------------- /tests/test_flaskparser.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | from flask import Flask 5 | from marshmallow import Schema 6 | from werkzeug.exceptions import BadRequest, HTTPException 7 | 8 | from webargs import ValidationError, fields, missing 9 | from webargs.core import json 10 | from webargs.flaskparser import abort, parser 11 | from webargs.testing import CommonTestCase 12 | 13 | from .apps.flask_app import FLASK_SUPPORTS_ASYNC, app 14 | 15 | 16 | class TestFlaskParser(CommonTestCase): 17 | def create_app(self): 18 | return app 19 | 20 | def test_parsing_view_args(self, testapp): 21 | res = testapp.get("/echo_view_arg/42") 22 | assert res.json == {"view_arg": 42} 23 | 24 | def test_parsing_invalid_view_arg(self, testapp): 25 | res = testapp.get("/echo_view_arg/foo", expect_errors=True) 26 | assert res.status_code == 422 27 | assert res.json == {"view_args": {"view_arg": ["Not a valid integer."]}} 28 | 29 | def test_use_args_with_view_args_parsing(self, testapp): 30 | res = testapp.get("/echo_view_arg_use_args/42") 31 | assert res.json == {"view_arg": 42} 32 | 33 | def test_use_args_on_a_method_view(self, testapp): 34 | res = testapp.post_json("/echo_method_view_use_args", {"val": 42}) 35 | assert res.json == {"val": 42} 36 | 37 | def test_use_kwargs_on_a_method_view(self, testapp): 38 | res = testapp.post_json("/echo_method_view_use_kwargs", {"val": 42}) 39 | assert res.json == {"val": 42} 40 | 41 | def test_use_kwargs_with_missing_data(self, testapp): 42 | res = testapp.post_json("/echo_use_kwargs_missing", {"username": "foo"}) 43 | assert res.json == {"username": "foo"} 44 | 45 | # regression test for https://github.com/marshmallow-code/webargs/issues/145 46 | def test_nested_many_with_data_key(self, testapp): 47 | post_with_raw_fieldname_args = ( 48 | "/echo_nested_many_data_key", 49 | {"x_field": [{"id": 42}]}, 50 | ) 51 | res = testapp.post_json(*post_with_raw_fieldname_args, expect_errors=True) 52 | assert res.status_code == 422 53 | 54 | res = testapp.post_json("/echo_nested_many_data_key", {"X-Field": [{"id": 24}]}) 55 | assert res.json == {"x_field": [{"id": 24}]} 56 | 57 | res = testapp.post_json("/echo_nested_many_data_key", {}) 58 | assert res.json == {} 59 | 60 | # regression test for https://github.com/marshmallow-code/webargs/issues/500 61 | def test_parsing_unexpected_headers_when_raising(self, testapp): 62 | res = testapp.get( 63 | "/echo_headers_raising", expect_errors=True, headers={"X-Unexpected": "foo"} 64 | ) 65 | assert res.status_code == 422 66 | assert "headers" in res.json 67 | assert "X-Unexpected" in set(res.json["headers"].keys()) 68 | 69 | 70 | class TestFlaskAsyncParser(CommonTestCase): 71 | def create_app(self): 72 | return app 73 | 74 | @pytest.mark.skipif( 75 | not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask" 76 | ) 77 | def test_parsing_view_args_async(self, testapp): 78 | res = testapp.get("/echo_view_arg_async/42") 79 | assert res.json == {"view_arg": 42} 80 | 81 | @pytest.mark.skipif( 82 | not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask" 83 | ) 84 | def test_parsing_invalid_view_arg_async(self, testapp): 85 | res = testapp.get("/echo_view_arg_async/foo", expect_errors=True) 86 | assert res.status_code == 422 87 | assert res.json == {"view_args": {"view_arg": ["Not a valid integer."]}} 88 | 89 | @pytest.mark.skipif( 90 | not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask" 91 | ) 92 | def test_use_args_with_view_args_parsing_async(self, testapp): 93 | res = testapp.get("/echo_view_arg_use_args_async/42") 94 | assert res.json == {"view_arg": 42} 95 | 96 | @pytest.mark.skipif( 97 | not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask" 98 | ) 99 | def test_use_args_on_a_method_view_async(self, testapp): 100 | res = testapp.post_json("/echo_method_view_use_args_async", {"val": 42}) 101 | assert res.json == {"val": 42} 102 | 103 | @pytest.mark.skipif( 104 | not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask" 105 | ) 106 | def test_use_kwargs_on_a_method_view_async(self, testapp): 107 | res = testapp.post_json("/echo_method_view_use_kwargs_async", {"val": 42}) 108 | assert res.json == {"val": 42} 109 | 110 | @pytest.mark.skipif( 111 | not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask" 112 | ) 113 | def test_use_kwargs_with_missing_data_async(self, testapp): 114 | res = testapp.post_json("/echo_use_kwargs_missing_async", {"username": "foo"}) 115 | assert res.json == {"username": "foo"} 116 | 117 | # regression test for https://github.com/marshmallow-code/webargs/issues/145 118 | @pytest.mark.skipif( 119 | not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask" 120 | ) 121 | def test_nested_many_with_data_key_async(self, testapp): 122 | post_with_raw_fieldname_args = ( 123 | "/echo_nested_many_data_key_async", 124 | {"x_field": [{"id": 42}]}, 125 | ) 126 | res = testapp.post_json(*post_with_raw_fieldname_args, expect_errors=True) 127 | assert res.status_code == 422 128 | 129 | res = testapp.post_json( 130 | "/echo_nested_many_data_key_async", {"X-Field": [{"id": 24}]} 131 | ) 132 | assert res.json == {"x_field": [{"id": 24}]} 133 | 134 | res = testapp.post_json("/echo_nested_many_data_key_async", {}) 135 | assert res.json == {} 136 | 137 | # regression test for https://github.com/marshmallow-code/webargs/issues/500 138 | @pytest.mark.skipif( 139 | not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask" 140 | ) 141 | def test_parsing_unexpected_headers_when_raising_async(self, testapp): 142 | res = testapp.get( 143 | "/echo_headers_raising_async", 144 | expect_errors=True, 145 | headers={"X-Unexpected": "foo"}, 146 | ) 147 | assert res.status_code == 422 148 | assert "headers" in res.json 149 | assert "X-Unexpected" in set(res.json["headers"].keys()) 150 | 151 | 152 | @mock.patch("webargs.flaskparser.abort") 153 | def test_abort_called_on_validation_error(mock_abort): 154 | # error handling must raise an error to be valid 155 | mock_abort.side_effect = BadRequest("foo") 156 | 157 | app = Flask("testapp") 158 | 159 | def validate(x): 160 | if x != 42: 161 | raise ValidationError("Invalid value.") 162 | 163 | argmap = {"value": fields.Raw(validate=validate)} 164 | with app.test_request_context( 165 | "/foo", 166 | method="post", 167 | data=json.dumps({"value": 41}), 168 | content_type="application/json", 169 | ): 170 | with pytest.raises(HTTPException): 171 | parser.parse(argmap) 172 | mock_abort.assert_called() 173 | abort_args, abort_kwargs = mock_abort.call_args 174 | assert abort_args[0] == 422 175 | expected_msg = "Invalid value." 176 | assert abort_kwargs["messages"]["json"]["value"] == [expected_msg] 177 | assert type(abort_kwargs["exc"]) is ValidationError 178 | 179 | 180 | @pytest.mark.asyncio 181 | @pytest.mark.skipif(not FLASK_SUPPORTS_ASYNC, reason="requires async support in flask") 182 | async def test_abort_called_on_validation_error_async(): 183 | with mock.patch("webargs.flaskparser.abort") as mock_abort: 184 | # error handling must raise an error to be valid 185 | mock_abort.side_effect = BadRequest("foo") 186 | 187 | app = Flask("testapp") 188 | 189 | def validate(x): 190 | if x != 42: 191 | raise ValidationError("Invalid value.") 192 | 193 | argmap = {"value": fields.Raw(validate=validate)} 194 | with app.test_request_context( 195 | "/foo", 196 | method="post", 197 | data=json.dumps({"value": 41}), 198 | content_type="application/json", 199 | ): 200 | with pytest.raises(HTTPException): 201 | await parser.async_parse(argmap) 202 | mock_abort.assert_called() 203 | abort_args, abort_kwargs = mock_abort.call_args 204 | assert abort_args[0] == 422 205 | expected_msg = "Invalid value." 206 | assert abort_kwargs["messages"]["json"]["value"] == [expected_msg] 207 | assert type(abort_kwargs["exc"]) is ValidationError 208 | 209 | 210 | @pytest.mark.parametrize("mimetype", [None, "application/json"]) 211 | def test_load_json_returns_missing_if_no_data(mimetype): 212 | req = mock.Mock() 213 | req.mimetype = mimetype 214 | req.get_data.return_value = "" 215 | schema = Schema.from_dict({"foo": fields.Raw()})() 216 | assert parser.load_json(req, schema) is missing 217 | 218 | 219 | def test_abort_with_message(): 220 | with pytest.raises(HTTPException) as excinfo: 221 | abort(400, message="custom error message") 222 | assert excinfo.value.data["message"] == "custom error message" 223 | 224 | 225 | def test_abort_has_serializable_data(): 226 | with pytest.raises(HTTPException) as excinfo: 227 | abort(400, message="custom error message") 228 | serialized_error = json.dumps(excinfo.value.data) 229 | error = json.loads(serialized_error) 230 | assert isinstance(error, dict) 231 | assert error["message"] == "custom error message" 232 | 233 | with pytest.raises(HTTPException) as excinfo: 234 | abort( 235 | 400, 236 | message="custom error message", 237 | exc=ValidationError("custom error message"), 238 | ) 239 | serialized_error = json.dumps(excinfo.value.data) 240 | error = json.loads(serialized_error) 241 | assert isinstance(error, dict) 242 | assert error["message"] == "custom error message" 243 | -------------------------------------------------------------------------------- /tests/test_pyramidparser.py: -------------------------------------------------------------------------------- 1 | from webargs.testing import CommonTestCase 2 | 3 | 4 | class TestPyramidParser(CommonTestCase): 5 | def create_app(self): 6 | from .apps.pyramid_app import create_app 7 | 8 | return create_app() 9 | 10 | def test_use_args_with_callable_view(self, testapp): 11 | assert testapp.get("/echo_callable?value=42").json == {"value": 42} 12 | 13 | def test_parse_matchdict(self, testapp): 14 | assert testapp.get("/echo_matchdict/42").json == {"mymatch": 42} 15 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist= 3 | lint 4 | py{39,310,311,312,313}-marshmallow3 5 | py{39,313}-marshmallow4 6 | py313-marshmallowdev 7 | py39-lowest 8 | docs 9 | 10 | [testenv] 11 | extras = tests 12 | deps = 13 | marshmallow3: marshmallow>=3.0.0,<4.0.0 14 | marshmallow4: marshmallow>=4.0.0,<5.0.0 15 | marshmallowdev: https://github.com/marshmallow-code/marshmallow/archive/dev.tar.gz 16 | 17 | # for 'lowest', pin flask to 1.1.3 and markupsafe to 1.1.1 18 | # because flask 1.x does not set upper bounds on the versions of its dependencies 19 | # generally, we can't install just any version from 1.x -- only 1.1.3 and 1.1.4 20 | # markupsafe is a second-order dependency of flask (flask -> jinja2 -> markupsafe) 21 | # and must be pinned explicitly because jinja2 does not pin its dependencies in any 22 | # versions in its 2.x line 23 | # see: 24 | # https://github.com/marshmallow-code/webargs/pull/694 25 | # https://github.com/pallets/flask/pull/4047 26 | # https://github.com/pallets/flask/issues/4455 27 | lowest: flask==1.1.3 28 | lowest: markupsafe==1.1.1 29 | # all non-flask frameworks 30 | lowest: Django==2.2.0 31 | lowest: bottle==0.12.13 32 | lowest: tornado==4.5.2 33 | lowest: pyramid==1.9.1 34 | lowest: falcon==2.0.0 35 | lowest: aiohttp==3.0.8 36 | # pin marshmallow itself to the lowest supported version 37 | lowest: marshmallow==3.13.0 38 | commands = pytest {posargs} 39 | 40 | [testenv:lint] 41 | deps = pre-commit~=3.5 42 | skip_install = true 43 | commands = pre-commit run --all-files 44 | 45 | # a separate `mypy` target which runs `mypy` in an environment with 46 | # `webargs` and `marshmallow` both installed is a valuable safeguard against 47 | # issues in which `mypy` running on every file standalone won't catch things 48 | [testenv:mypy] 49 | deps = mypy==1.11.0 50 | extras = frameworks 51 | commands = mypy src/ {posargs} 52 | 53 | [testenv:docs] 54 | extras = docs 55 | commands = sphinx-build docs/ docs/_build {posargs} 56 | 57 | ; Below tasks are for development only (not run in CI) 58 | 59 | [testenv:watch-docs] 60 | deps = 61 | sphinx-autobuild 62 | extras = docs 63 | commands = sphinx-autobuild --open-browser docs/ docs/_build {posargs} --watch src/webargs --delay 2 64 | 65 | [testenv:watch-readme] 66 | deps = restview 67 | skip_install = true 68 | commands = restview README.rst 69 | --------------------------------------------------------------------------------